Spaß mit Pygame: Avoider Game Stage 1

Spieleprogrammierung
Pygame
Autor:in

Jörg Kantel

Veröffentlichungsdatum

24. Juni 2023

Angestachelt durch die gestrigen Video-Tutorials zu Pygame, aber auch durch meine (Wieder-) Entdeckung von Greenfoot, und erneut inspiriert durch das vorgestern vorgestellte Buch »Creative Greenfoot von Michael Haungs (Packt, 2015), habe ich – wie angedroht – angefangen, das dort vorgestellte Avoider Game nach Pygame (und Pygbag, damit es auch im Browser gespielt werden kann) zu portieren. Natürlich strikt objektorientiert.

Auch wenn ich mir über die Sinnhaftigkeit immer noch nicht sicher bin, habe ich die Klasse GameWorld bei dieser Implementierung erst einmal beibehalten. Rauswerfen kann ich sie immer noch.

Die Implementierung folgt einer Processing.py-Version dieses Programmes, die ich vor Jahren verbrochen und hier veröffentlicht hatte. Es brauchte ein paar Überlegungen, die von setup() und draw() beeinflußte Processing-Logik in eine Pygame-Logik zu überfürhren, aber im Endeffekt habe ich ein nettes Skript zusammengeschustert, das neben der GameWorld, zu der die Klasse HUD (Head Up Dislay) gehört, aus den Klassen Player und Enemy besteht, die beides Unterklassen von pygame.sprite.Sprite sind.

Als Kollisionserkennung habe ich Pygames sprite.collide_circle verwendet und dann auch noch den Radius des Players (des Totenschädels) reduziert. Denn es wirklte unnatürlich, wenn der Schädel schon getroffen sein sollte, wenn die Gegner gerade einmal die gekreuzten Knochen berührten.

Ansonsten ist der Quellcode ziemlich selbsterklärend und ich hoffe leicht nachvollziehbar:

# Avoider Game Stage 1
import pygame as pg
import asyncio
from pygame.locals import *
from random import randint
import os, sys

## Settings
WIDTH, HEIGHT = 640, 480
TITLE = "Avoider Game, Stage 1"
FPS = 60                   # Frame per second
TW, TH = 24, 24            # Größe der einzelnen Sprites
TW2 = TW // 2
PLAYER_Y = HEIGHT // 1.2
NO_ENEMIES = 10
SPEED_MIN = 2
SPEED_MAX = 7
START_ZONE = HEIGHT // 1.5
DANGER_ZONE = PLAYER_Y

## Hier wird der Pfad zum Verzeichnis der Assets gesetzt
DATAPATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")

## Farben
BG_COLOR = (128, 57, 52)

# Klassen
## Class GameWorld
class GameWorld:

    def __init__(self):
        # Pygame und das Fenster initialisieren
        pg.init()
        self.screen = pg.display.set_mode((WIDTH, HEIGHT))
        pg.display.set_caption(TITLE)

        self.clock = pg.time.Clock()
        self.keep_going = True

        # Load Assets
        self.skull_image = pg.image.load(os.path.join(DATAPATH, "skull2.png")).convert_alpha()
        self.smiley0_image = pg.image.load(os.path.join(DATAPATH, "smiley0.png")).convert_alpha()
        self.smiley1_image = pg.image.load(os.path.join(DATAPATH, "smiley1.png")).convert_alpha()
        self.smiley2_image = pg.image.load(os.path.join(DATAPATH, "smiley4.png")).convert_alpha()
        self.score_font = pg.font.Font(os.path.join(DATAPATH, "comichelvetic_medium.ttf"), 25)

        # Game State
        self.start_game = True
        self.play_game = False
        self.game_over = False


    def reset(self):
        # Neustart oder Status zurücksetzen
        # Hier werden alle Elemente der GameWorld initialisiert
        self.all_sprites = pg.sprite.Group()
        self.enemies = pg.sprite.Group()
        for _ in range(NO_ENEMIES):
            enemy = Enemy(randint(TW2, WIDTH - TW2), -randint(50, 250), self)
            self.all_sprites.add(enemy)
            self.enemies.add(enemy)        
        self.player = Player(self)
        self.all_sprites.add(self.player)
        self.hud = HUD(self)
          
    def events(self):
        for event in pg.event.get():
            if ((event.type == pg.QUIT)
                or (event.type == pg.KEYDOWN
                and event.key == pg.K_ESCAPE)):
                if self.playing:
                    self.playing = False
                self.keep_going = False
 
    def update(self):
        self.all_sprites.update()
        self.hud.update(self.player.score, self.player.lives)

    def draw(self):
        self.screen.fill(BG_COLOR)
        self.all_sprites.draw(self.screen)
        self.screen.blit(self.hud.score, self.hud.score_rect)
        self.screen.blit(self.hud.lives, self.hud.lives_rect)
        pg.display.flip()
    
    def start_screen(self):
        pass

    def game_over_screen(self):
        text = "Game Over"       # Debugging
        print(text)
        # self.screen_font = self.score_font
        # text = self.screen_font.render(text, True, (255, 255, 255))
        # text_rect = text.get_rect()
        # text_rect.centerx = WIDTH // 2
        # text_rect.y = HEIGHT // 2
        # self.screen.blit(text, text_rect)
        self.keep_going = False   # Debugging
## Ende Class GameWorld

## Class Player
class Player(pg.sprite.Sprite):

    def __init__(self, _world):
        super().__init__()
        self.game_world = _world
        self.image = self.game_world.skull_image
        self.rect = self.image.get_rect()
        self.x, self.y = WIDTH/2, PLAYER_Y
        self.radius = TW2
        self.score = 0
        self.lives = 5
    
    def update(self):
        x, y = pg.mouse.get_pos()
        if x <= TW2:
            x = TW2
        elif x >= WIDTH - TW2:
            x = WIDTH - TW2
        self.rect.center = (x, self.y)
        self.check_and_handle_collisions()
    
    def check_and_handle_collisions(self):
        for enemy in self.game_world.enemies:
            if pg.sprite.collide_circle(self, enemy):
                enemy.reset()
                self.lives -= 1
## End Class Player

## Class Enemy
class Enemy(pg.sprite.Sprite):

    def __init__(self, _x, _y, _world):
        super().__init__()
        self.x, self.y = _x, _y
        self.game_world = _world
        self.image = self.game_world.smiley0_image
        self.rect = self.image.get_rect()
        self.rect.center = (self.x, self.y)
        self.dy = randint(SPEED_MIN, SPEED_MAX)
        self.radius = TW2
    
    def update(self):
        self.over = False
        self.y += self.dy
        if self.y <= START_ZONE:
            self.image = self.game_world.smiley0_image
        elif self.y <= DANGER_ZONE:
            self.image = self.game_world.smiley1_image
        else:
            self.image = self.game_world.smiley2_image
        self.rect.center = (self.x, self.y)
        if self.rect.top >= HEIGHT:
            self.over = True
            self.game_world.player.score += 1
            self.reset()
    
    def reset(self):
        self.x = randint(TW2, WIDTH - TW2)
        self.y = -randint(50, 250)
        self.rect.center = (self.x, self.y)
        self.dy = randint(SPEED_MIN, SPEED_MAX)
    # End Class Enemy

class HUD():

    def __init__(self, _world):
        self.game_world = _world
        self.score_x = 30
        self.score_y = 15
        self.score_font = self.game_world.score_font
        self.score = self.lives = ""
        self.live_count_x = WIDTH - 150
        self.live_count_y = 15
    
    def update(self, points, lives):
        self.score = self.score_font.render(f"Score: {points}", True, (255, 255, 255))
        self.score_rect = self.score.get_rect()
        self.score_rect.x = self.score_x
        self.score_rect.y = self.score_y
        self.lives = self.score_font.render(f"Lives: {lives}", True, (255, 255, 255))
        self.lives_rect = self.lives.get_rect()
        self.lives_rect.x = self.live_count_x
        self.lives_rect.y = self.live_count_y
    # End Class HUD

# Hauptprgramm
world = GameWorld()
world.start_screen()

# Hauptschleife
async def main():
    while world.keep_going:
        world.reset()
        world.playing = True
        while world.playing:
            world.clock.tick(FPS)
            if world.player.lives == 0:
                world.playing = False
            world.events()
            world.update()
            world.draw()
            await asyncio.sleep(0)      # Very important, and keep it 0
        world.game_over_screen()
        print("After Game Over Screen")
    pg.quit()
    sys.exit()

asyncio.run(main())

Ich habe die Slots für einen Start- und einen Ende-Screen schon eingebaut, aber noch nicht implementiert. Außerdem sollen natürlich noch – wie in der Processing.py-Vorlage – PowerUps und PowerDowns implementiert werden, und auch optisch möchte ich das Spiel noch ein wenig aufpeppen.

Ihr könnt es hier spielen. Der Schädel wird mit der Maus gesteuert, aber er kann sich nur horizontal bewegen. Und Ihr bekommt fünf Leben zum Start. Verwirkt Ihr diese, ist das Spiel gnadenlos zu Ende.

Die Assets habe ich – wie schon so oft – den freien (CC-BY 4.0) Twitter Emojis (Twmojis) entnommen und mit einem Bildbearbeitungsprogramm meines Vertrauens auf die gewünschte Größe skaliert. Und das Spiel habe ich auch auf meinen Itch.io-Account hochgeladen, so daß Ihr es auch dort im Browser spielen könnt.

Den Quellcode und die Assets findet Ihr wie immer auch in meinem GitHub-Repositorium.