Search code examples
pythonpygamesprite2d-gamessprite-sheet

Pygame: What is the best way to display these ships and their movement from sprite sheet?


Attached below is one of the sprite sheets I am using. Right now, I am wondering the best way to display the ships with their animation when moving. The ships have 9 frames. I have found old examples with deprecated code, or examples that have you break the images into individual files. I would love for someone to show me the best way to use these sprites as they are. Thank you for your time.

Sprite Sheet


Solution

  • What is the best way can't be answered, because in the end that's subjective; and depends on your own goals. Best performance? Easiest code? Most flexible code?

    You could start with something like this I hacked together during my last boring meeting:

    import pygame
    import random
    from pygame import Vector2
    
    SPRITE_SHEET = None
    
    GREEN_SHIP  = pygame.Rect(0, 292, 32, 32)
    RED_SHIP    = pygame.Rect(0, 324, 32, 32)
    BLUE_SHIP   = pygame.Rect(0, 356, 32, 32)
    YELLOW_SHIP = pygame.Rect(0, 388, 32, 32)
    
    
    class EnemyController:
    
        def __init__(self):
            self.direction = Vector2(1, 0)
    
        def update(self, sprite, events, dt):
            if not pygame.display.get_surface().get_rect().contains(sprite.rect):
                self.direction *= -1
            sprite.direction = self.direction
    
    class PlayerController:
    
        movement = {
            pygame.K_UP:    Vector2( 0, -1),
            pygame.K_DOWN:  Vector2( 0,  1),
            pygame.K_LEFT:  Vector2(-1,  0),
            pygame.K_RIGHT: Vector2( 1,  0)
        }
    
        def update(self, sprite, events, dt):
            pressed = pygame.key.get_pressed()
            v = Vector2(0, 0)
            for key in PlayerController.movement:
                if pressed[key]:
                    v += PlayerController.movement[key]
    
            sprite.direction = v
    
            for e in events:
                if e.type == pygame.KEYDOWN:
                    if e.key == pygame.K_SPACE:
                        sprite.groups()[0].add(Explosion(sprite.pos))
    
    class Animation:
        def __init__(self, frames, speed, sprite):
            self.sprite = sprite
            self.speed = speed
            self.ticks = 0
            self.frames = frames
            self.running = 0
            self.start()
    
        def cycle_func(self, iterable):
            saved = []
            for element in iterable:
                yield element
                saved.append(element)
            if hasattr(self.sprite, 'on_animation_end'):
                self.sprite.on_animation_end()
            while saved:
                for element in saved:
                    yield element
                if hasattr(self.sprite, 'on_animation_end'):
                    self.sprite.on_animation_end()
        def stop(self):
            self.running = 0
            if self.idle_image:
                self.sprite.image = self.idle_image
    
        def start(self):
            if not self.running:
                self.running = 1
                self.cycle = self.cycle_func(self.frames)
                self.sprite.image = next(self.cycle)
    
        def update(self, dt):
            self.ticks += dt
            if self.ticks >= self.speed:
                self.ticks = self.ticks % self.speed
                if self.running:
                    self.sprite.image = next(self.cycle)
    
    class AnimatedSprite(pygame.sprite.Sprite):
    
        def __init__(self, pos, frames, speed):
            super().__init__()
            self.animation = Animation(frames, speed, self)
            self.rect = self.image.get_rect(center=pos)
            self.pos = Vector2(pos)
            self.animation.start()
    
        def update(self, events, dt):
            self.animation.update(dt)
    
    class Explosion(AnimatedSprite):
    
        frames = None
    
        def __init__(self, pos):
            if not Explosion.frames:
                Explosion.frames = parse_sprite_sheet(SPRITE_SHEET, pygame.Rect(0, 890, 64, 64), 6, 4)
    
            super().__init__(pos, Explosion.frames, 50)
    
        def on_animation_end(self):
            self.kill()
    
    class DirectionalImageSprite(pygame.sprite.Sprite):
    
        directions = [(1,0),(1,-1),(0,-1),(-1,-1),(-1,0),(-1,1),(0,1),(1,1),(0,0)]
    
        def __init__(self, pos, directional_images_rect):
            super().__init__()
            images = parse_sprite_sheet(SPRITE_SHEET, directional_images_rect, 9, 1)
            self.images = { x: img for (x, img) in zip(DirectionalImageSprite.directions, images) }
            self.direction = Vector2(0, 0)
            self.image = self.images[(self.direction.x, self.direction.y)]
            self.rect = self.image.get_rect(center=pos)
            self.pos = pos
    
    class SpaceShip(DirectionalImageSprite):
    
        def __init__(self, pos, controller, directional_images_rect):
            super().__init__(pos, directional_images_rect)
            self.controller = controller
            self.speed = 3
    
        def update(self, events, dt):
            super().update(events, dt)
    
            if self.controller:
                self.controller.update(self, events, dt)
    
            self.image = self.images[(self.direction.x, self.direction.y)]
            if self.direction.length():
                self.pos = self.pos + self.direction.normalize() * self.speed
    
            self.rect.center = int(self.pos[0]), int(self.pos[1])
    
    def parse_sprite_sheet(sheet, start_rect, frames_in_row, lines):
        frames = []
        rect = start_rect.copy()
        for _ in range(lines):
            for _ in range(frames_in_row):
                frame = sheet.subsurface(rect)
                frames.append(frame)
                rect.move_ip(rect.width, 0)
            rect.move_ip(0, rect.height)
            rect.x = start_rect.x
        return frames
    
    def main():
        screen = pygame.display.set_mode((800, 600))
        global SPRITE_SHEET
        SPRITE_SHEET = pygame.image.load("ipLRR.png").convert_alpha()
        clock = pygame.time.Clock()
        dt = 0
        all_sprites = pygame.sprite.Group(
            SpaceShip((400, 300), PlayerController(), YELLOW_SHIP),
            SpaceShip((400, 100), EnemyController(), GREEN_SHIP)
        )
    
        while True:
            events = pygame.event.get()
    
            for e in events:
                if e.type == pygame.QUIT:
                    return
    
            all_sprites.update(events, dt)
    
            screen.fill((0, 0, 0))
            all_sprites.draw(screen)
            pygame.display.flip()
            dt = clock.tick(120)
    
    main()
    

    enter image description here

    I pass the function parse_sprite_sheet the image, along with the position and size of the first frame of the animation/bunch of related sub-images (using a Rect). Also, I pass the number of images in the row and the number of rows (since the explosion animation uses 4 rows with 4 images each). Then I use subsurface the get the part of the sprite sheet I'm interested in in a nested loop.


    The Animation class is updated by the sprite it's attached to and changes the image of the sprite when enough time has passed.

    Also a method named on_animation_end is called on the sprite once the animation ends. I use this to kill the Explosion sprites once the explosion is done.


    For the directional images of SpaceShip, I define the directions in a list once (in the correct order) and then attach each direction an image by creation a dictionary.

    This way I can easily look up the correct image, since the direction the SpaceShip is heading is stored in the direction attribute.


    That's it basically. Some animations in your sprite sheet are a little more tricky, as the size of the tile changes, but it's doable.

    You'll get the idea.