I am working on a tile based rpg in pygame but I've encountered an unusual error with the way my player is animated.
When the player is walking, their sprite cycles through 4 images stored in a list (I have used basic coloured surfaces for this example), and the variable self.walking is set to True. The sprite should only animate when self.walking is true. When the player collides with a wall, their velocity is set to zero in that direction, even if the player continues to walk in that direction.
When the player's velocity is zero in both x and y directions, self.walking should be set to False. (This has to be in both directions to allow the player to slide along walls.) The player's sprite should not animate when self.walking is False, it should remain on the first sprite in the list.
self.walking is initialised in the Player class and is set to False and True in the self.animate() function within the Player class.
However, I have encounted two problems with this:
When the player collides with a wall and continues to move towards that wall, the sprite will flash rapidly from yellow to red, showing the player is still being animated for one frame but then immediately set back to sprite 1 in the next frame. I have checked this by having the program print the player's velocity as the program loops and I found that for one frame the velocity is set to zero, and for the next the velocity is not zero.
When the player moves into a corner, sometimes the player will continue to cycle through it's animation frames despite colliding with two walls.
I have tried to stop these errors from occuring by setting velocity to zero in the collide_with_walls function but it doesn't seem to help.
The player and wall objects use a separate rectangle called hit_rect to handle collisions. For this example hit_rect is the same as the basic rect for both objects.
import pygame as pg
import sys
vec = pg.math.Vector2
WHITE = ( 255, 255, 255)
BLACK = ( 0, 0, 0)
RED = ( 255, 0, 0)
YELLOW = ( 255, 255, 0)
BLUE = ( 0, 0, 255)
WIDTH = 512 # 32 by 24 tiles
HEIGHT = 384
FPS = 60
TILESIZE = 32
PLAYER_SPEED = 3 * TILESIZE
MAP = ["1111111111111",
"1...........1",
"1.P.........1",
"1...11111...1",
"1...1...1...1",
"1...1...1...1",
"1...11111...1",
"1...........1",
"1...........1",
"1111111111111"]
def collide_with_walls(sprite, group, dir):
if dir == "x":
hits = pg.sprite.spritecollide(sprite, group, False, collide_hit_rect)
if hits:
sprite.vel.x = 0
if hits[0].rect.centerx > sprite.hit_rect.centerx:
sprite.pos.x = hits[0].rect.left - sprite.hit_rect.width / 2
if hits[0].rect.centerx < sprite.hit_rect.centerx:
sprite.pos.x = hits[0].rect.right + sprite.hit_rect.width / 2
sprite.hit_rect.centerx = sprite.pos.x
if dir == "y":
hits = pg.sprite.spritecollide(sprite, group, False, collide_hit_rect)
if hits:
sprite.vel.y = 0
if hits[0].rect.centery > sprite.hit_rect.centery:
sprite.pos.y = hits[0].rect.top - sprite.hit_rect.height / 2
if hits[0].rect.centery < sprite.hit_rect.centery:
sprite.pos.y = hits[0].rect.bottom + sprite.hit_rect.height / 2
sprite.hit_rect.centery = sprite.pos.y
def collide_hit_rect(one, two):
return one.hit_rect.colliderect(two.rect)
############### PLAYER CLASS ####################
class Player(pg.sprite.Sprite):
def __init__(self, game, x, y):
self.groups = game.all_sprites
pg.sprite.Sprite.__init__(self, self.groups)
self.game = game
self.vel = vec(0, 0)
self.pos = vec(x, y) *TILESIZE
self.current_frame = 0
self.last_update = 0
self.walking = False
self.walking_sprites = [pg.Surface((TILESIZE, TILESIZE)),
pg.Surface((TILESIZE, TILESIZE)),
pg.Surface((TILESIZE, TILESIZE)),
pg.Surface((TILESIZE, TILESIZE))]
self.walking_sprites[0].fill(YELLOW)
self.walking_sprites[1].fill(RED)
self.walking_sprites[2].fill(YELLOW)
self.walking_sprites[3].fill(BLUE)
self.image = self.walking_sprites[0]
self.rect = self.image.get_rect()
self.hit_rect = self.rect
self.hit_rect.bottom = self.rect.bottom
def update(self):
self.get_keys()
self.rect = self.image.get_rect()
self.rect.center = self.pos
self.hit_rect.centerx = self.pos.x
collide_with_walls(self, self.game.walls, "x")
self.hit_rect.centery = self.pos.y
collide_with_walls(self, self.game.walls, "y")
self.pos += self.vel * self.game.dt
self.rect.midbottom = self.hit_rect.midbottom
self.animate()
def get_keys(self):
self.vel = vec(0,0)
keys = pg.key.get_pressed()
if keys[pg.K_LEFT] or keys[pg.K_a]:
self.vel.x = -PLAYER_SPEED
if keys[pg.K_RIGHT] or keys[pg.K_d]:
self.vel.x = PLAYER_SPEED
if keys[pg.K_UP] or keys[pg.K_w]:
self.vel.y = -PLAYER_SPEED
if keys[pg.K_DOWN] or keys[pg.K_s]:
self.vel.y = PLAYER_SPEED
def animate(self):
now = pg.time.get_ticks()
if self.vel == vec(0,0): # If the player's velocity is zero in both directions...
self.walking = False
else: # If it is not...
self.walking = True
# show walk animation
if self.walking:
if now - self.last_update > 200:
self.last_update = now
self.current_frame = (self.current_frame + 1) % len(self.walking_sprites)
self.image = self.walking_sprites[self.current_frame]
self.rect = self.image.get_rect()
self.rect.midbottom = self.hit_rect.midbottom
# idle sprite
if not self.walking:
self.current_frame = 0
self.image = self.walking_sprites[self.current_frame]
self.rect = self.image.get_rect()
self.rect.midbottom = self.hit_rect.midbottom
############### OBSTACLE CLASS ####################
class Obstacle(pg.sprite.Sprite):
def __init__(self, game, x, y):
self.groups = game.walls
pg.sprite.Sprite.__init__(self, self.groups)
self.x = x * TILESIZE
self.y = y * TILESIZE
self.w = TILESIZE
self.h = TILESIZE
self.game = game
self.image = pg.Surface((self.w,self.h))
self.image.fill(BLACK)
self.rect = self.image.get_rect()
self.hit_rect = self.rect
self.rect.x = self.x
self.rect.y = self.y
############### GAME CLASS ####################
class Game:
def __init__(self):
pg.init()
self.screen = pg.display.set_mode((WIDTH, HEIGHT))
pg.display.set_caption("Hello Stack Overflow")
self.clock = pg.time.Clock()
pg.key.set_repeat(500, 100)
def new(self):
self.all_sprites = pg.sprite.Group()
self.walls = pg.sprite.Group()
for row, tiles in enumerate(MAP):
for col, tile in enumerate(tiles):
if tile == "1":
Obstacle(self, col, row)
elif tile == "P":
print("banana!")
self.player = Player(self, col, row)
def quit(self):
pg.quit()
sys.exit()
def run(self):
# game loop - set self.playing = False to end the game
self.playing = True
while self.playing:
self.dt = self.clock.tick(FPS) / 1000
self.events()
self.update()
self.draw()
def events(self):
# catch all events here
for event in pg.event.get():
if event.type == pg.QUIT:
self.quit()
def update(self):
self.player.update()
def draw(self):
self.screen.fill(WHITE)
for wall in self.walls:
self.screen.blit(wall.image, wall.rect)
for sprite in self.all_sprites:
self.screen.blit(sprite.image, sprite.rect)
pg.display.flip()
# create the game object
g = Game()
while True:
g.new()
g.run()
pg.quit()
In the grand scheme of things this is a relatively minor graphical error, but it's frustrating to leave it alone.
TL;DR - My player sprite continues to animate for one frame when colliding with a wall, causing it to flash annoying. I want the sprite to be static when the player is not walking.