2
votes

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:

  1. 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.

  2. 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.

1

1 Answers

3
votes

Its a matter of the order. First move the object, then do the collision test. If the object collides with the wall, then the velocity is set zero and the position of the object is corrected. Finally you can draw and animate the object:

class Player(pg.sprite.Sprite):
    # [...]

        def update(self):
        self.get_keys()
        self.rect = self.image.get_rect()
        self.rect.center = self.pos
        
        self.pos += self.vel * self.game.dt
        self.hit_rect.center = [self.pos.x, self.pos.y]
        
        collide_with_walls(self, self.game.walls, "x")
        self.hit_rect.center = [self.pos.x, self.pos.y]
        collide_with_walls(self, self.game.walls, "y")
        self.hit_rect.center = [self.pos.x, self.pos.y]
        
        self.rect.midbottom = self.hit_rect.midbottom
        self.animate()