Dynamically controlling sprite blit order with pygame - python

I'm currently writing a Tower Defence game. Everything works fine but there's a visual issue that's bugging me.
Basically, a sprite that has a lower rect.y and should be higher on the map to pass behind a slower mob with a higher rect.y, can be blitted after (and therefore in front of) the foreground sprite.
I could fix it, but not while keeping the random spread-out spawn points that I like.
Could anybody suggest anything to help? I omitted irrelevant code, so there's not much to sift through.
class Pokemon(pygame.sprite.Sprite):
def __init__(self, name, health, speed, attack, typeOf, typeOf2, level, order):
pygame.sprite.Sprite.__init__(self)
self.order = order
self.image = pygame.image.load('C:/Users/#/#/#/pygames/tower_defense/Distributable/images/POKEMON/%sright.png'%(name))
self.rect = self.image.get_rect()
# X/Y axis locations
self.rect.centerx = 0-self.rect.width - self.order*100
self.rect.centery = random.randint(500, 540)
Then the creation block -
for e in range(numberOfEnemies):
poke = random.randint(1, 2)
if poke == 1:
level = random.randint(2, 5)
Pidgey = Pokemon('Pidgey', 15, 2, 5, 'Normal', 'Flying', level, e)
enemies.append(Pidgey)
enemy_sprites.add(Pidgey)
else:
level = random.randint(2, 4)
Rattata = Pokemon('Rattata', 13, 3, 5, 'Normal', None, level, e)
enemies.append(Rattata)
enemy_sprites.add(Rattata)
enemyNumber += 1

If you need to control the layers of the sprites, you can use a LayeredUpdates sprite group. The sprites need a self._layer attribute or you can pass the layer when you add the sprite to the group, e.g.:
layered_group.add(sprite, layer=desired_layer)
When a sprite is moved, you can call the change_layer method of the sprite group:
layered_group.change_layer(sprite, sprite.rect.bottom)
However, if you have to update the layers of all sprites each frame, it would be more efficient to sort the sprites by their rect.bottom position, iterate over the list and blit them:
blit = screen.blit # Local variable to improve the performance.
for sprite in sorted(all_sprites, key=lambda spr: spr.rect.bottom):
blit(sprite.image, sprite.rect)

Related

How to make bullets aim at player in pygame [duplicate]

This question already has answers here:
calculating direction of the player to shoot pygame
(1 answer)
Moving forward after angle change. Pygame
(1 answer)
Shooting a bullet in pygame in the direction of mouse
(2 answers)
Closed 2 years ago.
So far, the enemies in my game only fire straight down. I want to be able to aim at the player. This is my Enemy class's shoot method.
class Enemy(sprite.Sprite):
def shoot(self):
# Origin position, direction, speed
# Direction of (0,1) means straight down. 0 pixels along the x axis, and +1 pixel along the y axis
# Speed of (10,10) means that every frame, the object will move 10 px along the x and y axis.
self.bullets.shoot(self.rect.center, (0,1), (10,10))
self.bullets is an instance of my BulletPool class.
class BulletPool(sprite.Group):
def shoot(self, pos, direction, speed):
# Selects a bullet from the pool
default = self.add_bullet() if self.unlimited else None
bullet = next(self.get_inactive_bullets().__iter__(), default)
if bullet is None:
return False
# Sets up bullet movement
bullet.rect.center = pos
bullet.start_moving(direction)
bullet.set_speed(speed)
# Adds bullet to a Group of active bullets
self.active_bullets.add(bullet)
And here is my Bullet class.
class Bullet(sprite.Sprite):
def update(self):
self.move()
def set_speed(self, speed):
self.speed = Vector2(speed)
def start_moving(self, direction):
self.direction = Vector2(direction)
self.is_moving = True
def stop_moving(self):
self.is_moving = False
def move(self):
if self.is_moving:
x_pos = int(self.direction.x*self.speed.x)
y_pos = int(self.direction.y*self.speed.y)
self.rect = self.rect.move(x_pos,y_pos)
Using this, I can only make sprites go straight up (0,-1), down (0,1), left (-1,0) or right (1,0), as well as combining combining x and axes to make a 45 degree angle, (i.e. (1,1) is going down and right). I don't know how to angle something to make it go towards a particular direction other than these. Should I change the way I move my objects? I use the same methods to move my player, and it works perfectly when it's just taking controls from the arrow keys.

Finding proper X/Y coordinate modifiers with given angle in Python/Pygame

I am trying to make a sprite move directly towards the mouse, utilizing the angle between them. This angle is found via the atan2 function. While this angle works fine for rotating the sprite towards the mouse, the sprite moves in the wrong directions depending on the quadrant of the given angle. It will sometimes freeze up in one quadrant, or move directly opposite the mouse.
I am using basic Trig functions to find the angle, and calculate proper additions to the X and Y variables of the sprite. It is also important to note that the angle I calculate, while it doesn't work for movement, does work perfectly for rotation. What's odd is that I pass the X-difference between the two spots, and THEN the Y-difference, which is the opposite of how the inverse tangent function is supposed to be handled. Therefore, I'm not even sure how this angle has been making rotation work correctly.
I've attempted to pass the Y-difference and the X-difference (in that order) into the atan2 function. However, this causes the rotation on my sprite to be wrong, pointing me towards the idea that the angle as a whole is also incorrect. I've also tried following along with numerous other programs, all of which use the same formulas as me. However, these don't work, even when I change the order of the arguments to the atan2 function to match the example programs.
def main():
ExitLoop = False
image = IMAGELOADER.AllImages["Fighter1"]
image2 = IMAGELOADER.AllImages["Fighter2"]
Fighter1 = FighterClass.Fighter(image,(700,700))
Fighter2 = FighterClass.Fighter(image2,(300,300))
while not ExitLoop:
ScreenController.Refresh()
mouse_pos = pygame.mouse.get_pos()
Fighter2.set_x(mouse_pos[0]-32)
Fighter2.set_y(mouse_pos[1]-32)
angle = math.atan2(Fighter1.get_x()-mouse_pos[0]+32, Fighter1.get_y()-mouse_pos[1]+32)
degrees_angle = math.degrees(angle)
Fighter1.rotate(degrees_angle)
xval = Fighter1.get_x()
yval = Fighter1.get_y()
speed = Fighter1.get_speed()
changex = (speed*math.cos(angle))
changey = (speed*math.sin(angle))
Fighter1.set_x(xval+changex)
Fighter1.set_y(yval+changey)
for event in pygame.event.get():
if event.type == pygame.QUIT:
ExitLoop = True
ScreenController.Draw(Fighter1.get_image(),Fighter1.get_rect(),False)
ScreenController.Draw(Fighter2.get_image(),Fighter2.get_rect(),False)
ScreenController.DisplayUpdate()
clock.tick(60)
Class Code (Relevant to the fighter class)
import pygame
import WoodysFunctions
class Fighter(pygame.sprite.Sprite):
def __init__(self,image,XnY):
pygame.sprite.Sprite.__init__(self)
self.image = image
self.__image_source = image
self.rect = self.image.get_rect()
self.__mask = pygame.mask.from_surface(self.image)
self.rect.x = XnY[0]
self.rect.y = XnY[1]
self.__speed = 1
def get_image(self):
return self.image
def get_rect(self):
return self.rect
def get_mask(self):
return self.__mask
def get_x(self):
return self.rect.x
def get_y(self):
return self.rect.y
def get_speed(self):
return self.__speed
def set_image(self,value):
self.image = value
def set_rect(self,value):
self.__rect = value
def set_mask(self,value):
self.__mask = value
def set_x(self,value):
self.rect.x = value
def set_y(self,value):
self.rect.y = value
def set_speed(self,value):
self.__speed = value
def rotate(self,angle):
old_center = self.rect.center
self.image = pygame.transform.rotate(self.__image_source,angle)
self.rect = self.image.get_rect()
self.rect.center = old_center
Expected output: Sprite moves straight towards the mouse
Actual behavior: Sprite moves in wrong directions, with behavior showing patterns depending on quadrant of calculated angle.
Edit:
I changed the program so that the X and Y variables of the sprite are stored in variables separate from the rect object. This prevents decimal truncation. I also recalculated the angle between the sprite and the mouse pointer after the rotation code is finished. In the recalculation, the X and Y difference parameters are swapped to match the inverse tangent function instead of the inverse cotangent function. This recalculated angle is used for angular movement, and the first angle, with the X difference passed first, is used for rotation. It is important to note that after I calculated the changeX and changeY variables using the recalculated angle (with the Y difference passed first), I multiplied them by -1, as otherwise the sprite will move away from the mouse pointer.
I cannot be 100% sure, but I think the problem is that pygame.Rect stores position as integers, because it's supposed to store coordinates and dimensions in pixel unit, and of course you cannot paint half pixel.
Since you are dealing with any angle and trigonometric functions, you end with floats which are truncated when you do:
def set_x(self,value):
self.rect.x = value
Here, if value is 1.4, self.rect.x becomes 1. So you lose "accuracy."
This loss of accuracy is propagated each iteration of the main loop (each frame), resulting in an unexpected motion direction.
The best solution is to store all your value in a separate data structure and update the rect attribute only for drawing in the screen.

How do I save a level layout in a variable

Currently, My next project is going to be a platformer and when I look around stackoverflow for research on several mechanics, I see many people doing the same thing: They save a layout with some variable, then go and unload it somewhere and it just renders in the game. I was interested, so I looked further and I found nothing on how to load/unload states like that, or maybe I'm just not wording my search correctly.
Either way, How do I do this?
ex: I would save a level layout as either an array or a single multi-line string and then somehow generate a single tile sprite for each letter, like T.
import pygame
# Storage method A
level = '''
X X X X X
X X X X X
T T X X X
X X X T T
T T T T T
'''
# Storage Method B
level2 = [
'XXXXX',
'XXXXX',
'TTXXX',
'XXXTT',
'TTTTT'
]
# X is blank space, T is tiles
# Then what? Thats what I need to know.
# If someone already answered this and I'm just not using the right keywords let me know.
You will need to calculate the pixel-positions for each tile. To draw any tile, you need to know
the size of the canvas
the size of your grid
the position of the tile in your grid
1: Finding the size of your canvas should be trivial.
2: For the second storage method you can do
height = len(level2)
width = len(level2[0]) #Assuming all rows are of equal length and there's at least one row
3: We're going to iterate through the rows and characters which will keep track of our position in the grid on the side.
def draw_tiles(canvas_width, canvas_height, width, height, level2):
for row in range(height):
for column in range(width):
if list(level2[row])[column] == 'T':
pixel_x = int(canvas_width/width)*column
pixel_y = int(canvas_height/height)*row
draw_tile(pixel_x, pixel_y)
Now all you need to do is define the draw_tile(x, y) function to draw a tile on the canvas with its top-left corner being on the pixel co-ordinates (x, y). I'm sure pygame has something for that.
Make sure you set the grid width/height so that canvas_width/width and canvas_height/height are both integers. Otherwise your tiles will be slightly offset due to rounding.
You could iterate over the enumerated rows and characters in the layout, create the tile instances and add them to a sprite group.
In the example I just give the tiles different colors depending on the character in the layout (X=blue, T=green) before I add them to the group, but you could also create completely different Tile types or subclasses if the character is a 'T' or an 'X'.
import pygame
class Tile(pygame.sprite.Sprite):
def __init__(self, pos, color):
super().__init__()
self.image = pygame.Surface((50, 50))
self.image.fill(color)
self.rect = self.image.get_rect(topleft=pos)
def create_tile_group(layout):
"""Turn the layout into a sprite group with Tile instances."""
group = pygame.sprite.Group()
for y, row in enumerate(layout):
for x, tile in enumerate(row):
if tile == 'T':
color = (50, 150, 50)
else:
color = (0, 0, 200)
group.add(Tile((x*tile_size, y*tile_size), color))
return group
pygame.init()
screen = pygame.display.set_mode((250, 250))
clock = pygame.time.Clock()
layout1 = [
'XXXXX',
'XTXXX',
'XXXXT',
'XXXXX',
'TTTTT',
]
tile_size = 50
tile_group = create_tile_group(layout1)
loop = True
while loop:
for event in pygame.event.get():
if event.type == pygame.QUIT:
loop = False
tile_group.update()
screen.fill((30, 30, 30))
tile_group.draw(screen)
pygame.display.flip()
clock.tick(30)
If you get performance problems because you blit too many small surfaces, you could blit them onto a big background surface before the while loop starts and then just blit the background once each frame.
There is no magic here: "it just renders in the game" is not accurate. There's more software behind the rendering call, a module that defines the tile sprites, scans the level character by character, and places the sprites accordingly. The developer (e.g. you) decides on the level representation, sprite form, sprite placement, etc.
You have several bits of code to write. The good news is that you decide the format; you just have to stay consistent when you write those modules.

Dealing with Sprites and Collisions Using Pygame

I am learning python using pygame and I am working on something that involves sprites and collisions. I've looked at some examples but I still don't quite understand it. What I am attempting to do is to be able to add sprites(a ball) when the user presses the "=" key and also be able to remove the last sprite added when pressing "-". I am not able to remove just the last one, I have only been able to remove all of them.
So far I have been able to add the balls to the window and have them bounce off the walls and one another(sort of). When 2 balls collide, they don't completely touch yet they bounce off. Sometimes the balls get stuck and won't move and sometimes the balls bounce off the frame which they aren't suppose to.
Its my first time working with sprite groups and would appreciate any help/guidance into making this work smoothly.Thanks.
The code:
ball.py
import pygame
from pygame.locals import *
class Ball(pygame.sprite.Sprite):
def __init__(self, x, y, vx, vy):
super().__init__();
self.image = pygame.image.load("ball.png").convert()
self.image.set_colorkey(pygame.Color(0, 0, 0))
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
self.vx = vx
self.vy = vy
def draw(self, SCREEN):
SCREEN.blit(self.image, (self.rect.x, self.rect.y))
def move(self, SCREEN, balls):
l_collide = self.rect.x + self.image.get_width() + self.vx > SCREEN.get_width()
r_collide = self.rect.x + self.vx < 0
t_collide = self.rect.y + self.vy < 0
b_collide = self.rect.y + self.image.get_height() + self.vy > SCREEN.get_height()
a = pygame.sprite.spritecollide(self, balls, False, False)
if len(a) > 1:
self.vx *= -1
self.vy *= -1
if l_collide or r_collide:
self.vx *= -1
if t_collide or b_collide:
self.vy *= -1
self.rect.x += self.vx
self.rect.y += self.vy
ball_animation.py
import pygame
import sys
import random
import math
from pygame.locals import *
from ball.ball import Ball
from random import randint
def ball_list(num):
ball_list = pygame.sprite.Group()
for x in range(num):
rand_x = random.randint(0,400)
rand_y = random.randint(0,400)
vx = 4
vy = 5
ball_list.add(Ball(rand_x, rand_y, vx, vy))
return ball_list
def main():
pygame.init()
FPS = 30
FPS_CLOCK = pygame.time.Clock()
# COLOR LIST
BLACK = pygame.Color(0, 0, 0)
# Code to create the initial window
window_size = (500, 500)
SCREEN = pygame.display.set_mode(window_size)
# set the title of the window
pygame.display.set_caption("Bouncing Ball Animation")
# change the initial background color to white
SCREEN.fill(BLACK)
balls = ball_list(0)
while True: # <--- main game loop
for event in pygame.event.get():
if event.type == QUIT: # QUIT event to exit the game
pygame.quit()
sys.exit()
if event.type == KEYDOWN:
if event.key == K_EQUALS:
balls.add(Ball(randint(0,400),randint(0,400), 4,5))
if event.key == K_MINUS:
try:
balls.remove()
except IndexError:
print('There is no balls to take!')
SCREEN.fill(BLACK)
for x in balls:
x.move(SCREEN,balls)
x.draw(SCREEN)
pygame.display.update() # Update the display when all events have been processed
FPS_CLOCK.tick(FPS)
if __name__ == "__main__":
main()
Removing Sprites on Press
The problem is sprite.Group.remove(sprites) wants you to specify which sprites it should remove. sprites here should be a sprite/list of sprites that you want to remove from the group. This means to remove the last ball added on key press you need to keep a list of the ball sprites and pop() the most recently added item from it, and then use the result of the pop() as the sprite to remove from the group. sprite.Group has a .sprites() method which returns a list of all sprites in the group, in the order they were added. This list is generated from the group and is not actually an interface with it, so doing things to this list won't affect the group. We can still however use it to get the last added sprite. Here is what it looks like:
elif event.key == K_0:
try:
sprite_list = balls.sprites()
to_remove = sprite_list[-1] # Get last element of list
balls.remove(to_remove)
except IndexError:
print('There is no balls to take!')
Collisions
So this is a bit more involved and not so simple to fix in your code. To understand what the problem is, look at what your collision velocity adjustments are actually doing for the screen border case.
l_collide = self.rect.x + self.image.get_width() + self.vx > SCREEN.get_width()
r_collide = self.rect.x + self.vx < 0
t_collide = self.rect.y + self.vy < 0
b_collide = self.rect.y + self.image.get_height() + self.vy > SCREEN.get_height()
#################
if l_collide or r_collide:
self.vx *= -1
if t_collide or b_collide:
self.vy *= -1
Consider a single time-step in your code. We check to see if the sprite is sitting over the edge of the boundaries by any amount. If its hanging over, we reverse the velocity. There is a case where your edge checking will get you into trouble. If your self.vx is less than the difference between your current position X and the boundary of the x dimension, you will reverse your speed, travel self.vx back towards the boundary, but not make it past. In the next time-step, you will see that you are still over the boundary, and your program will again reverse self.vx, actually sending you away from the boundary. In this case you will bound back and forth each time-step by self.vx. Normally this wouldn't happen in your code, except for when you spawn a new ball sprite over the boundary further than your self.vx or self.vy for that ball. This can be remedied by making sure you don't spawn balls off the edges, or better yet, only reversing your velocity if you need to.
if (l_collide and self.vx>0) or (r_collide and self.vx<0):
self.vx *= -1
if (t_collide and self.vy<0) or (b_collide and self.vy>0):
self.vy *= -1
Notice here we only reverse the velocity if we are over the edge AND the velocity is headed deeper in that direction. Now for your sprites you have two options, just like with the boundaries:
Only initiate a new ball in empty space where it cannot collide.
Implement some way to calculate the correct velocity adjustment and only apply it if the velocity is headed in the opposite direction.
From what I read in the documentation, sprite.Group looks like it is meant for checking if sprites are overlapping, and not for physics simulation. I recommend doing some research on 2d physics simulation to get a nice conceptualization of what information you should want to communicate between objects. I'm sure there are some nice tutorials out there.
Finally, to address your other question about why they are colliding when they don't appear to be touching. sprite.spritecollide is returning which sprites have rectangles that intersect. If your ball.png is color keyed for transparency, this does not affect the rect of the sprite. Pygame appears to have functionality implemented designed to handle this problem in the collided keyword of sprite.spritecollide:
pygame.sprite.spritecollide()
Find sprites in a group that intersect another sprite.
spritecollide(sprite, group, dokill, collided = None) -> Sprite_list
The collided argument is a callback function used to calculate if two sprites >are colliding. it should take two sprites as values, and return a bool value >indicating if they are colliding. If collided is not passed, all sprites must >have a “rect” value, which is a rectangle of the sprite area, which will be >used to calculate the collision.
collided callables:
collide_rect
collide_rect_ratio
collide_circle
collide_circle_ratio
collide_mask
That's from the pygame documentation. The documentation for the collide_circle function states that your sprite should have a radius attribute, or else one will be calculated to fit the entire rectangle inside a circle. As such, in your Ball.__init__ function I would recommend adding:
self.radius = self.rect.width/2
This will make collide_circle use a radius that approximates your ball image, assuming it is centered and circular and occupies the entire image. Next, you must add the collision specification to your collision check by changing:
a = pygame.sprite.spritecollide(self, balls, False, False)
to
a = pygame.sprite.spritecollide(self, balls, False, pygame.sprite.collide_circle)
If you solve the problem of not spawning new ball objects inside each other, this should all work nicely. If you can't get them to spawn inside each other, think about a different data-structure or different way of collision checking to get the results you want. Best of luck!
I can see two questions in your text
You want to only remove one sprite, rather than all the sprites in the spritegroup
If you look at the pygame documentation, you can see that spritegroup.remove has an optional argument. You can remove a single sprite by putting your desired sprite as the argument, such as myspritegroup.remove(mysprite).
You have issues with the colliding
Your collision works for me as long as the balls don't spawn on top of each other on creation which you can simply check. Good luck :)

Problem with 2D collision detection from beginner

I've taken an introductory course in Computer Science, but a short while back I decided to try and make a game. I'm having a problem with collision detection. My idea was to move an object, and if there is a collision, move it back the way it came until there is no longer a collision. Here is my code:
class Player(object):
...
def move(self):
#at this point, velocity = some linear combination of (5, 0)and (0, 5)
#gPos and velocity are types Vector2
self.hitBox = Rect(self.gPos.x, self.gPos.y, 40, 40)
self.gPos += self.velocity
while CheckCollisions(self):
self.gPos -= self.velocity/n #see footnote
self.hitBox = Rect(self.gPos.x, self.gPos.y, 40, 40)
...
def CheckCollisions(obj):
#archList holds all 'architecture' objects, solid == True means you can't walk
#through it. colliderect checks to see if the rectangles are overlapping
for i in archList:
if i.solid:
if i.hitBox.colliderect(obj.hitBox):
return True
return False
*I substituted several different values for n, both integers and floats, to change the increment by which the player moves back. I thought by trying a large float, it would only move one pixel at a time
When I run the program, the sprite for the player vibrates very fast over a range of about 5 pixels whenever I run into a wall. If I let go of the arrow key, the sprite will get stuck in the wall permanently. I wondering why the sprite is inside the wall in the first place, since by the time I blit the sprite to the screen, it should have been moved just outside of the wall.
Is there something wrong with my method, or does the problem lie within my execution?
Looks like you're setting the hitbox BEFORE updating the position. The Fix seems simple.
Find:
self.hitBox = Rect(self.gPos.x, self.gPos.y, 40, 40)
self.gPos += self.velocity
Replace:
self.gPos += self.velocity
self.hitBox = Rect(self.gPos.x, self.gPos.y, 40, 40)
Other Suggestions: What you should do is check the position BEFORE you move there, and if it's occupied, don't move. This is untested so please just use this as psuedocode intended to illustrate the point:
class Player(object):
...
def move(self):
#at this point, velocity = some linear combination of (5, 0)and (5, 5)
#gPos and velocity are types Vector2
selfCopy = self
selfCopy.gPos += self.velocity
selfCopy.hitBox = Rect(selfCopy.gPos.x, selfCopy.gPos.y, 40, 40)
if not CheckCollisions(selfCopy)
self.gPos += self.velocity
...
def CheckCollisions(obj):
#archList holds all 'architecture' objects, solid == True means you can't walk
#through it. colliderect checks to see if the rectangles are overlapping
for i in archList:
if i.solid:
if i.hitBox.colliderect(obj.hitBox):
return True
return False

Categories