Python pygame character idle animation

Viewed 318

I have an update method for the player class that checks a list that I have stored the two image files for the idle animation. In my update method I'm checking if enough time has passed and I should update the image or not. It animates once initially running the script but stops, how do I make it continuous?

import pygame
import os
from os import path

os.environ['SDL_VIDEO_CENTERED'] = '1'
img_dir = path.join(path.dirname(__file__), 'img')

WIDTH = 720
HEIGHT = 360
FPS = 30

BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GREEN = (0, 255, 0)

pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
clock = pygame.time.Clock()

class Player(pygame.sprite.Sprite):
    def __init__(self, size):
        pygame.sprite.Sprite.__init__(self)
        self.size = size
        self.image = playerIdle_anim[self.size][0]
        self.rect = self.image.get_rect()
        self.rect.centerx = WIDTH / 2
        self.rect.bottom = HEIGHT - 10
        self.speedx = 0
        self.speedy = 0
        self.frame = 0
        self.last_update = pygame.time.get_ticks()
        self.frame_rate = 500

    def update(self):
        now = pygame.time.get_ticks()
        if now - self.last_update > self.frame_rate:
            self.last_update = now
            self.frame += 1
            if self.frame == 1:
                center = self.rect.center 
                self.image = playerIdle_anim[self.size][self.frame]
                self.rect = self.image.get_rect()
                self.rect.center = center
                self.frame = 0
            if self.frame == 0:
                center = self.rect.center 
                self.image = playerIdle_anim[self.size][self.frame]
                self.rect = self.image.get_rect()
                self.rect.center = center
                
                
                


        self.speedx = 0
        self.speedy = 0
        keystate = pygame.key.get_pressed()
        if keystate[pygame.K_LEFT]:
            self.speedx = -8
        if keystate[pygame.K_RIGHT]:
            self.speedx = 8
        if keystate[pygame.K_UP]:
            self.speedy = -8
        if keystate[pygame.K_DOWN]:
            self.speedy = 8
        self.rect.x += self.speedx
        self.rect.y += self.speedy

playerIdle_anim = {}
playerIdle_anim['lg'] = []

for i in range(2):
    filename = 'player_idle0{}.png'.format(i)
    img = pygame.image.load(path.join(img_dir, filename)).convert()
    img.set_colorkey(BLACK)
    img_lg = pygame.transform.scale(img, (128, 75))
    playerIdle_anim['lg'].append(img_lg)
    
        
all_sprites = pygame.sprite.Group()
player = Player('lg')
all_sprites.add(player)

running = True
while running:

    clock.tick(FPS)
    for event in pygame.event.get():

        if event.type == pygame.QUIT:
            running = False

    #draw / render
    all_sprites.update()
    screen.fill(WHITE)
    all_sprites.draw(screen)
    pygame.display.update()

pygame.quit()
quit()
2 Answers
  1. you do not need to type in editor same operations in every case of frame index. I think you just need to calculate index of frame and draw it in Update() in one algorythm

Change cases:

   def update(self):
        now = pygame.time.get_ticks()
        if now - self.last_update > self.frame_rate:
            self.last_update = now
            self.frame += 1
            if self.frame == 1:
                center = self.rect.center 
                self.image = playerIdle_anim[self.size][self.frame]
                self.rect = self.image.get_rect()
                self.rect.center = center
                self.frame = 0
            if self.frame == 0:
                center = self.rect.center 
                self.image = playerIdle_anim[self.size][self.frame]
                self.rect = self.image.get_rect()
                self.rect.center = center
    ................

to calculation:

    def update(self):
        now = pygame.time.get_ticks()
        if now - self.last_update > self.frame_rate:
            self.last_update = now            
            self.frame = (self.frame + 1) % len(playerIdle_anim['lg'])
            center = self.rect.center 
            self.image = playerIdle_anim[self.size][self.frame]
            self.rect = self.image.get_rect()
            self.rect.center = center
    ................
  1. Also, may be you should set frames list (playerIdle_anim['lg']) inside class.

Instead of:

 class Player(pygame.sprite.Sprite):
     def __init__(self, size):
         pygame.sprite.Sprite.__init__(self)
         self.size = size

write:

class Player(pygame.sprite.Sprite):
    def __init__(self, FramesList):
        pygame.sprite.Sprite.__init__(self)
        self.frames = FramesList        
        self.image = self.frames[0]
        ................
    def update(self):
        now = pygame.time.get_ticks()
        if now - self.last_update > self.frame_rate:
            self.last_update = now            
            self.frame = (self.frame + 1) % len(self.frames)
            center = self.rect.center
            self.image = self.frames[self.frame]
        ................

................
player = Player(playerIdle_anim['lg'])
................

I hope you understand my English

@PSV offers good advice about your animation code layout, and the changes will fix and simply the code, but it does not explain what is wrong.

The reason your animation stops is because the calculation of the next-frame-index is not quite right. It always stays on frame 0 right?

Let's mentally walk through your `update() function:

def update(self):
    now = pygame.time.get_ticks()
    if now - self.last_update > self.frame_rate:
        self.last_update = now
        self.frame += 1                          # <<- UPDATE HERE
        if self.frame == 1:
            ...            
            self.frame = 0
        if self.frame == 0:                      # <<-- NOT an "else"
            ...

When the animation starts, self.frame is 0 (set in __init__). Some time passes and self.frame += 1 happens. Now we check if self.frame == 1, it is, so we set the image to frame['lg'][1], and self.frame back to 0 again. Some time passes, and self.frame += 1 happens ...

The code is forever having self.frame += 1 before testing, so it always goes down the path to set frame #1. However, because there's two if statements (rather than an if and else) as soon as self.frame is set back to zero (as part of the index being 1), the next clause if self.frame == 0 is immediately allowed too, so you get two updates! But the human never sees the first update, just the final result.

Removing the self.frame += 1; Adding a self.frame = 1 and changing the second if to be an else will fix the code simply:

def update(self):
    now = pygame.time.get_ticks()
    if now - self.last_update > self.frame_rate:
        self.last_update = now
        if self.frame == 1:
            center = self.rect.center 
            self.image = playerIdle_anim[self.size][self.frame]
            self.rect = self.image.get_rect()
            self.rect.center = center
            self.frame = 0
        elif self.frame == 0:                      # <-- HERE
            center = self.rect.center 
            self.image = playerIdle_anim[self.size][self.frame]
            self.rect = self.image.get_rect()
            self.rect.center = center
            self.frame = 1                         # <-- HERE

However this is rather dogey. What about when your animation is 12 frames, you don't want to have 12 nearly identical clauses. There's got to be a better way of handling the index updates.

The code @PSV answered with solves this "next image index" update problem by using the modulus (division remainder) of the frame-count compared with the current frame. So if you have say 7 frame in the list, using this in the modulus returns 0 -> 6, stopping the index overflowing. It's equivalent to something like:

self.frame += 1
frame_count = len( playerIdle_anim[self.size] )
if ( self.frame >=  frame_count ):
    self.frame = 0

This allows you to reduce you frame-picking code down to two sections: updating the index, and setting the image:

now = pygame.time.get_ticks()
if now - self.last_update > self.frame_rate:
    # Update image index
    self.last_update = now
    self.frame += 1
    # ensure the frame-index loops around the list
    if ( self.frame >= len( playerIdle_anim[self.size] ) ):
        self.frame = 0
    # Change the frame to the next (or 0th) image
    center = self.rect.center 
    self.image = playerIdle_anim[self.size][self.frame]
    self.rect = self.image.get_rect()
    self.rect.center = center


 
Related