[Pygame] Objekte bewegen sich je nach Anzahl unterschiedlich schnell

Hier werden alle anderen GUI-Toolkits sowie Spezial-Toolkits wie Spiele-Engines behandelt.
Benutzeravatar
Dennis89
User
Beiträge: 1158
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo,

ich schon wieder. Ich bin die letzten Tage auch immer wieder an einem Hobby-Code dran. Es geht um das Spiel Space-Invaders, von dem es Code im Internet gibt. Da ich eigentlich noch nie was mit Pygame gemacht habe, dachte ich, dass ich den Code jetzt nach meinen Bedürfnissen umschreibe. Habe angefangen und das gleich als Chance gesehen, endlich mal `attr` zu verwenden, weil ich das ja hier schon öfters sah.

Der Ablauf soll so sein, bei jedem getroffenen Feind, bekommt dieser eine zufällige neue Position und ein weiterer kommt hinzu. Die Anzahl habe ich mal allgemein auf 100 beschränkt.
Dann hätte ich gerne eine feste Anzahl von Geschossen, damit ich mit dem nächsten Schuss nicht warten muss, bis das Geschoss getroffen oder aus dem Bildschirm ist.

Ich habe eine Liste mit Geschossen, wenn ich nur ein Geschoss in der Liste habe, dann fliegt das fast in Lichtgeschwindigkeit durch die Gegend und wenn ich die Anzahl nur auf 5 erhöhe, läuft alles viel zäher.
Ich finde meinen Fehler nicht wirklich.
Zum schauen ob ein Geschoss einen Feind trifft, habe ich mich gegen zwei `for` - Schleifen entschieden und nutze `itertools.product` ist dass vielleicht nicht das richtige? Allerdings fand ich nichts besseres.
Könnt ihr den Fehler erkennen und mir bitte weiterhelfen?

Falls ihr das direkt testen wollt, habe ich den Code mit Bilder und Sound auf github:
https://github.com/Dennis-89/SpaceInvader

Code: Alles auswählen

import pygame
import random
from pygame import mixer
from pathlib import Path
from attr import define, field
from itertools import product

WIDTH = 800
HIGH = 600
SCREEN_SIZE = (WIDTH, HIGH)
BACKGROUND_IMAGE = Path(__file__).parent / "space.jpg"
ENEMY_IMAGE = Path(__file__).parent / "monster.png"
IMAGE_SIZE_ENEMY = (63, 63)
PLAYER_IMAGE = Path(__file__).parent / "Player.png"
IMAGE_SIZE_PLAYER = (63, 63)
BULLET_IMAGE = Path(__file__).parent / "rocket.png"
IMAGE_SIZE_BULLET = (64, 64)

SCORE_TEXT_X = 10
SCORE_TEXT_Y = 10

START_POSITION_PLAYER_X = 370
START_POSITION_Y = HIGH - 120

PLAYER_SPEED = 1
BULLET_SPEED = 0.2

NUMBER_OF_ENEMY = 10
ENEMY_SPEED = 0.3
ENEMY_Y_STEP = 40
MAX_DISTANCE_BULLET = 28
MAX_ENEMIES = 80
MAX_BULLETS = 1


@define
class CollisionDetector:
    x = field()
    y = field()

    def __add__(self, other):
        return CollisionDetector(abs(self.x - other.x), abs(self.y - other.y))

    def __call__(self):
        return self.x < MAX_DISTANCE_BULLET and self.y < MAX_DISTANCE_BULLET


@define
class Figure(CollisionDetector):
    direction = field()
    default_direction = field(default="+")
    y_direction_blocked = field(default=True)

    @classmethod
    def new(cls, x, y, move_in_x, blocked):
        return cls(x, y, {"+": move_in_x, "-": -move_in_x}, y_direction_blocked=blocked)

    def calculate_next_position(self, direction=None):
        if direction is None:
            direction = self.default_direction
        self.x += self.direction[direction]
        if self.x < 0 or self.x > WIDTH - IMAGE_SIZE_PLAYER[0]:
            self.x = WIDTH - IMAGE_SIZE_PLAYER[0] if direction == "+" else 0
            if not self.y_direction_blocked:
                self._change_y_position()

    def _change_y_position(self):
        if self.y > HIGH:
            self.y = 50
        else:
            self.y += ENEMY_Y_STEP
        self.default_direction = "+" if self.default_direction == "-" else "-"


@define
class Bullet(CollisionDetector):
    is_active = field(default=False)

    def change_y_position(self):
        self.y -= BULLET_SPEED


@define
class Game:
    screen = field()
    background = field()
    player_image = field()
    enemy_image = field()
    bullet_image = field()
    player = field()
    bullets = field()
    enemies = field()
    font = field()
    game_on = field(default=True)
    is_gun_loaded = field(default=True)
    score = field(default=0)

    def run(self):
        while self.game_on:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.game_on = False
            keys = pygame.key.get_pressed()
            self.process_user_input(keys)
            self.clear_screen()
            self.move_figure(self.player_image, (self.player.x, self.player.y))
            for enemy, bullet in product(self.enemies, self.bullets):
                enemy.calculate_next_position()
                if (enemy + bullet)() and bullet.is_active:
                    bullet.y = self.player.y
                    bullet.is_active = False
                    self.score += 1
                    enemy.x = random.randint(0, WIDTH - IMAGE_SIZE_ENEMY[0])
                    enemy.y = random.randint(50, 150)
                    ex_sound = mixer.Sound("explosion.wav")
                    ex_sound.play()
                    if len(self.enemies) < MAX_ENEMIES:
                        self.enemies.append(
                            Figure.new(
                                random.randint(0, WIDTH - IMAGE_SIZE_ENEMY[0]),
                                random.randint(50, 150),
                                ENEMY_SPEED,
                                False,
                            )
                        )
                self.move_figure(self.enemy_image, (enemy.x, enemy.y))
                if bullet.is_active:
                    self.move_figure(self.bullet_image, (bullet.x, bullet.y))
                    bullet.change_y_position()
                    if bullet.y < -50:
                        bullet.y = self.player.y
                        bullet.x = self.player.x
                        bullet.is_active = False
            self.show_score((SCORE_TEXT_X, SCORE_TEXT_Y))
            pygame.display.update()

    def process_user_input(self, keys):
        if keys[pygame.K_LEFT]:
            self.player.calculate_next_position("-")
        elif keys[pygame.K_RIGHT]:
            self.player.calculate_next_position("+")
        elif keys[pygame.K_SPACE]:
            for bullet in self.bullets:
                if not bullet.is_active:
                    mixer.Sound("laser.wav").play()
                    bullet.x = self.player.x
                    bullet.is_active = True
                    break

    def clear_screen(self):
        self.screen.blit(self.background, (0, 0))

    def move_figure(self, image, position):
        self.screen.blit(image, position)

    def show_score(self, position):
        self.screen.blit(
            self.font.render(f"Score : {self.score}", True, (255, 255, 255)), position
        )


def main():
    pygame.init()
    pygame.display.set_caption("Space Invader")
    screen = pygame.display.set_mode(SCREEN_SIZE)
    background = pygame.image.load(BACKGROUND_IMAGE)
    player_image = pygame.transform.scale(
        pygame.image.load(PLAYER_IMAGE), IMAGE_SIZE_PLAYER
    )
    player = Figure.new(START_POSITION_PLAYER_X, START_POSITION_Y, PLAYER_SPEED, True)
    enemies = [
        Figure.new(
            random.randint(0, WIDTH - IMAGE_SIZE_ENEMY[0]),
            random.randint(50, 150),
            ENEMY_SPEED,
            False,
        )
        for _ in range(NUMBER_OF_ENEMY)
    ]
    bullets = [
        Bullet(x=0, y=START_POSITION_Y, is_active=False) for _ in range(MAX_BULLETS)
    ]
    enemy_image = pygame.transform.scale(
        pygame.image.load(ENEMY_IMAGE), IMAGE_SIZE_ENEMY
    )
    bullet_image = pygame.transform.scale(
        pygame.image.load(BULLET_IMAGE), IMAGE_SIZE_BULLET
    )
    font = pygame.font.Font("freesansbold.ttf", 32)

    game = Game(
        screen,
        background,
        player_image,
        enemy_image,
        bullet_image,
        player,
        bullets,
        enemies,
        font,
    )
    game.run()


if __name__ == "__main__":
    main()
Der Nutzen dahinter ist einfach nur weiter programmieren zu lernen.

Vielen Dank und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
sparrow
User
Beiträge: 4198
Registriert: Freitag 17. April 2009, 10:28

Ich bin gerade unterwegs und kann deshalb nicht in den Code schauen. Und noch nie was mit pyGame gemacht. Allerdings früher durchaus mit Spieleprogrammierung, und die Engines haben meist eine optimierte Methode Kollisionen von Sprites zu erkennen. Ich bin mir fast sicher, dass es die in pyGame auch gibt. Nutzt du die?
Benutzeravatar
Dennis89
User
Beiträge: 1158
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo,

danke für die schnelle Antwort. Ne, ich habe mir selbst eine Klasse mit `__add__` geschrieben und schaue mir die x und y Differenz von zwei Objekte an. Habe aber nachgeschaut und ja du hast recht `pygame` bietet da was an:
https://www.pygame.org/docs/ref/sprite. ... llide_rect

Die `Sprite`-Objekte kann man wohl gruppieren und dann auf Kollision prüfen. Ich schaue mal ob und wie ich das eingebaut bekomme.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
__blackjack__
User
Beiträge: 13123
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Dennis89: Was auf jeden Fall schon mal falsch aussieht ist das in jedem Schleifendurchlauf die Positionen für Geschoss und Gegner verändert werden. Das sollte pro Geschoss und pro Gegner nur maximal einmal pro `display.update()` passieren.

Und ich würde erwarten dass das alles viel zu schnell abläuft wenn man das nicht mit `pygame.Clock` begrenzt.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
Dennis89
User
Beiträge: 1158
Registriert: Freitag 11. Dezember 2020, 15:13

Danke für die Antwort und den Hinweis.

Ich habe nun auch `Group` versucht und gemerkt, dass ich die `update`-Methode verwenden kann und es eine eigene `draw`-Methode gibt und ich vieles von meinem Code wegwerfen kann.
Bitte macht euch mit dem gezeigten Code erst mal keine weiteren Mühe, ich werde heute und eventuell auch noch morgen, erst mal alles umbauen und die aktuelle Version dann hier posten.

Den Teil den ich jetzt schon mit `Group` und seiner `update` - und `draw`-Methode habe, läuft unbeschreiblich schnell durch und mit `Clock` stockend, aber da muss ich mich später erst noch ordentlich einlesen.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Sirius3
User
Beiträge: 17761
Registriert: Sonntag 21. Oktober 2012, 17:20

@Dennis89: Dein Klassendesign ist falsch; Vererbung ist eine ›ist ein‹-Relation. Also eine Figure ist ein CollisionDetector; das macht keinen Sinn.
CollisionDetector ist eine Klasse mit zwei Erscheinungsformen, einmal als Positionsobjekt (x, y) und einmal als Abstandsobjekt (dx, dy) was aber auch die Attribute (x, y) hat. Die Umwandlungs von einer Ausformung in die andere findet per + statt. Warum?
__call__ halte ich in Deinem Fall auch für die falsche Methode.
Und da beide Methoden immer zusammen aufgerufen werden, macht es keinen Sinn, das in zwei aufzuspalten, also eine `hit`-Methode würde reichen, und die wäre am besten direkt auf der Bullet-Klasse definiert.
Wenn Du eine new-Methode brauchst, dann ist `attrs.define` wohl das falsche Mittel, schreib doch eine normale Klasse.
`Path(__file__).parent` sollte nur einmal aufgerufen werden und als Konstante IMAGE_PATH definiert werden.
`game_on` sollte eine lokale Variable sein, weil sie als Attribut keinen Sinn macht.
Benutzeravatar
__blackjack__
User
Beiträge: 13123
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Ich würde nicht attrs weglassen sondern dieses Wörterbuch. Das kann man als Konstante auf der Klasse definieren mit den Werte +1 und -1 und auf dem Exemplar dann den `move_in_x`-Wert und den dann halt zur Laufzeit mit 1 oder -1 multiplizieren.

"+" und "-" ist auch nicht so wirklich schön IMHO. Man könnte da eine Aufzählung definieren mit LEFT und RIGHT mit den Werten 1 und -1.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
Dennis89
User
Beiträge: 1158
Registriert: Freitag 11. Dezember 2020, 15:13

Danke für die weiteren Anmerkungen. Naja so wirklich begründen kann ich das alles nicht. Auch `__add__` habe ich noch nie benutzt und fand die Idee irgendwie ganz cool. Dass das mit der Vererbung keinen Sinn macht, sehe ich ein. Viel mehr sollte das eine Art Eigenschaft sein als ein "ist ein"-Objekt, aber der Plan ging nicht auf.

Im folgenden Stand habe ich noch eine `new`-Methode drin und finde das irgendwie schön so, ihr nicht?
Ich muss leider mal zwischendurch fragen, damit ich mich nicht gleich wieder ganz verrenne. Ich habe jetzt einen `player` und `enemies`. Das sind beides `pygame.sprite.Group` - Objekte und in `player` steckt halt nur einer drin und in `enemies` 10. Passt dass so? Durch `Group` habe ich die geschickte `draw` und `update`- Methode.
Die Feine laufen jetzt flüssig immer von links nach rechts und wenn sie aus dem Bildschirm sind, kommen sie links etwas weiter unten wieder rein.
Bevor ich jetzt die Geschosse einbaue, wollte ich nachfragen, ob das vom Aufbau her so passt? Ich denke dass ich eine extra Klasse für die Geschosse machen werde, da `update` sonst zu chaotisch wird.

Code: Alles auswählen

import random
from pathlib import Path

import pygame
from pygame import mixer

WIDTH = 800
HIGH = 600
SCREEN_SIZE = (WIDTH, HIGH)

IMAGE_PATH = Path(__file__).parent

BACKGROUND_IMAGE = IMAGE_PATH / "space.jpg"
ENEMY_IMAGE = IMAGE_PATH / "monster.png"
IMAGE_SIZE_ENEMY = (63, 63)
PLAYER_IMAGE = IMAGE_PATH / "Player.png"
IMAGE_SIZE_PLAYER = (63, 63)
BULLET_IMAGE = IMAGE_PATH / "rocket.png"
IMAGE_SIZE_BULLET = (64, 64)

SCORE_TEXT_X = 10
SCORE_TEXT_Y = 10

START_POSITION_PLAYER_X = 370
START_POSITION_Y = HIGH - 120

PLAYER_SPEED = 1

NUMBER_OF_ENEMY = 10
ENEMY_SPEED = 1
ENEMY_Y_STEP = 40


class Figure(pygame.sprite.Sprite):
    def __init__(self, rect, image):
        pygame.sprite.Sprite.__init__(self)
        self.rect = rect
        self.image = image

    @classmethod
    def new(cls, x, y, image_size, image):
        return cls(pygame.Rect(x, y, *image_size), image)

    def update(self, x, y=0):
        self.rect.move_ip(x, 0)
        if y == 0:
            if self.rect.x <= 0:
                self.rect.x = 0
            elif self.rect.x >= WIDTH - IMAGE_SIZE_ENEMY[0]:
                self.rect.x = WIDTH - IMAGE_SIZE_ENEMY[0]
        elif self.rect.x >= WIDTH:
            self.rect.x = 0
            self.rect.y += y
        elif self.rect.y > START_POSITION_Y - IMAGE_SIZE_ENEMY[0]:
            self.rect.y = random.randint(50, 150)


def control_game(
    screen,
    background,
    player,
    enemies,
    font,
):
    clock = pygame.time.Clock()
    game_on = True
    score = 0
    while game_on:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                game_on = False
        keys = pygame.key.get_pressed()
        process_user_input(keys, player)
        screen.blit(background, (0, 0))
        screen.blit(
            font.render(f"Score : {score}", True, (255, 255, 255)),
            (SCORE_TEXT_X, SCORE_TEXT_Y),
        )
        player.draw(screen)
        enemies.update(ENEMY_SPEED, ENEMY_Y_STEP)
        enemies.draw(screen)
        pygame.display.update()
        clock.tick(120)


def process_user_input(keys, player):
    if keys[pygame.K_LEFT]:
        player.update(-PLAYER_SPEED)
    elif keys[pygame.K_RIGHT]:
        player.update(PLAYER_SPEED)


def main():
    pygame.init()
    pygame.display.set_caption("Space Invader")
    screen = pygame.display.set_mode(SCREEN_SIZE)
    background = pygame.image.load(BACKGROUND_IMAGE)
    player_image = pygame.transform.scale(
        pygame.image.load(PLAYER_IMAGE), IMAGE_SIZE_PLAYER
    )
    enemy_image = pygame.transform.scale(
        pygame.image.load(ENEMY_IMAGE), IMAGE_SIZE_ENEMY
    )
    player = pygame.sprite.Group()
    player.add(
        Figure.new(
            START_POSITION_PLAYER_X,
            START_POSITION_Y,
            IMAGE_SIZE_PLAYER,
            player_image,
        )
    )
    enemies = pygame.sprite.Group()
    enemies.add(
        *[
            Figure.new(
                random.randint(0, WIDTH - IMAGE_SIZE_ENEMY[0]),
                random.randint(50, 150),
                IMAGE_SIZE_ENEMY,
                enemy_image,
            )
            for _ in range(NUMBER_OF_ENEMY)
        ]
    )

    font = pygame.font.Font("freesansbold.ttf", 32)

    control_game(
        screen,
        background,
        player,
        enemies,
        font,
    )


if __name__ == "__main__":
    main()
Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
Dennis89
User
Beiträge: 1158
Registriert: Freitag 11. Dezember 2020, 15:13

Hab das mal so eingebaut, aber mein Problem ist erst mal, dass da nicht nur eine Rakete kommt. Der `process_user_input` Aufruf kommt viel zu schnell hintereinander, das ist gut für die Bewegung nach links und rechts des Spielers, aber für die Rakete nicht.
Wäre eine Kombination aus KEYDOWN-Event und der `keys`-Liste sinnvoll?

Code: Alles auswählen

import random
from pathlib import Path

import pygame
from pygame import mixer
from itertools import product

WIDTH = 800
HIGH = 600
SCREEN_SIZE = (WIDTH, HIGH)

FILES_PATH = Path(__file__).parent

BACKGROUND_IMAGE = FILES_PATH / "space.jpg"
ENEMY_IMAGE = FILES_PATH / "monster.png"
IMAGE_SIZE_ENEMY = (63, 63)
PLAYER_IMAGE = FILES_PATH / "Player.png"
IMAGE_SIZE_PLAYER = (63, 63)
BULLET_IMAGE = FILES_PATH / "rocket.png"
IMAGE_SIZE_BULLET = (64, 64)

SHOOT_SOUND = FILES_PATH / "laser.wav"

SCORE_TEXT_X = 10
SCORE_TEXT_Y = 10

START_POSITION_PLAYER_X = 370
START_POSITION_Y = HIGH - 120

PLAYER_SPEED = 1

NUMBER_OF_ENEMY = 10
ENEMY_SPEED = 1
ENEMY_Y_STEP = 40

NUMBER_OF_BULLETS = 100
BULLET_SPEED = 2


class Figure(pygame.sprite.Sprite):
    def __init__(self, rect, image):
        pygame.sprite.Sprite.__init__(self)
        self.rect = rect
        self.image = image

    @classmethod
    def new(cls, x, y, image_size, image):
        return cls(pygame.Rect(x, y, *image_size), image)

    def update(self, x, y=0):
        self.rect.move_ip(x, 0)
        if y == 0:
            if self.rect.x <= 0:
                self.rect.x = 0
            elif self.rect.x >= WIDTH - IMAGE_SIZE_ENEMY[0]:
                self.rect.x = WIDTH - IMAGE_SIZE_ENEMY[0]
        elif self.rect.x >= WIDTH:
            self.rect.x = 0
            self.rect.y += y
        elif self.rect.y > START_POSITION_Y - IMAGE_SIZE_ENEMY[0]:
            self.rect.y = random.randint(50, 150)


class Bullet(pygame.sprite.Sprite):
    def __init__(self, rect, image):
        pygame.sprite.Sprite.__init__(self)
        self.rect = rect
        self.image = image
        self.is_active = False

    @classmethod
    def new(cls, x, y, image_size, image):
        return cls(pygame.Rect(x, y, *image_size), image)

    def update(self):
        if self.is_active:
            if self.rect.y == HIGH + IMAGE_SIZE_BULLET[0]:
                self.rect.y = START_POSITION_Y
            self.rect.y -= BULLET_SPEED
            if self.rect.y < 0:
                self.is_active = False
                self.rect.y = HIGH + IMAGE_SIZE_BULLET[0]


def is_collided_with(bullet, enemy):
    if bullet.is_active:
        return pygame.sprite.collide_rect(bullet, enemy)


def control_game(
    screen,
    background,
    players,
    enemies,
    bullets,
    font,
):
    clock = pygame.time.Clock()
    game_on = True
    score = 0
    while game_on:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                game_on = False
        keys = pygame.key.get_pressed()
        process_user_input(keys, players, bullets)
        for bullet, enemy in product(bullets, enemies):
            if is_collided_with(bullet, enemy):
                bullet.is_active = False
                bullet.rect.y = HIGH + START_POSITION_Y
                score += 1
        screen.blit(background, (0, 0))
        screen.blit(
            font.render(f"Score : {score}", True, (255, 255, 255)),
            (SCORE_TEXT_X, SCORE_TEXT_Y),
        )
        players.draw(screen)
        enemies.update(ENEMY_SPEED, ENEMY_Y_STEP)
        enemies.draw(screen)
        bullets.update()
        bullets.draw(screen)
        pygame.display.update()
        clock.tick(120)


def process_user_input(keys, players, bullets):
    if keys[pygame.K_LEFT]:
        players.update(-PLAYER_SPEED)
    elif keys[pygame.K_RIGHT]:
        players.update(PLAYER_SPEED)
    elif keys[pygame.K_SPACE]:
        mixer.Sound(SHOOT_SOUND).play()
        for bullet in bullets:
            if not bullet.is_active:
                bullet.is_active = True
                for player in players:
                    bullet.rect.x = player.rect.x
                return True


def main():
    pygame.init()
    pygame.display.set_caption("Space Invader")
    screen = pygame.display.set_mode(SCREEN_SIZE)
    background = pygame.image.load(BACKGROUND_IMAGE)
    player_image = pygame.transform.scale(
        pygame.image.load(PLAYER_IMAGE), IMAGE_SIZE_PLAYER
    )
    enemy_image = pygame.transform.scale(
        pygame.image.load(ENEMY_IMAGE), IMAGE_SIZE_ENEMY
    )
    bullet_image = pygame.transform.scale(
        pygame.image.load(BULLET_IMAGE), IMAGE_SIZE_BULLET
    )
    player = pygame.sprite.Group()
    player.add(
        Figure.new(
            START_POSITION_PLAYER_X,
            START_POSITION_Y,
            IMAGE_SIZE_PLAYER,
            player_image,
        )
    )
    enemies = pygame.sprite.Group()
    enemies.add(
        *[
            Figure.new(
                random.randint(0, WIDTH - IMAGE_SIZE_ENEMY[0]),
                random.randint(50, 150),
                IMAGE_SIZE_ENEMY,
                enemy_image,
            )
            for _ in range(NUMBER_OF_ENEMY)
        ]
    )
    bullets = pygame.sprite.Group()
    bullets.add(
        *[
            Bullet.new(
                0,
                START_POSITION_Y,
                IMAGE_SIZE_BULLET,
                bullet_image,
            )
            for _ in range(NUMBER_OF_BULLETS)
        ]
    )

    font = pygame.font.Font("freesansbold.ttf", 32)

    control_game(
        screen,
        background,
        player,
        enemies,
        bullets,
        font,
    )


if __name__ == "__main__":
    main()
Danke und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
Dennis89
User
Beiträge: 1158
Registriert: Freitag 11. Dezember 2020, 15:13

So dass ist das beste Ergebnis, das ich hinbekommen habe. Mit einem Custom-Event und einem Timer reduziere ich die Abschussgeschwindigkeit. Aber, ich habe jetzt die doofe `Gun`-Klasse, die eigentlich gar nichts macht, außer es mir einfach macht, die Namen hin und her zu reichen. Und ich weis das Klassen kein Container für Namen sind. Immerhin merke ich mir damit den Zustand von `is_ready` , aber ich denke dass das anders und schöner geht.
Dann wird mein Punktestand nicht richtig gezählt, sondern immer in 100er Schritten anstatt in 1er. Und zu guter Letzt werden nicht alle Leertastendrücke erkannt, auch wenn ich mir sicher bin, dass ich die Taste nicht zwei mal innerhalb der 100 ms gedrückt habe.

Code: Alles auswählen

import random
from itertools import product
from pathlib import Path

import pygame
from pygame import mixer

WIDTH = 800
HIGH = 600
SCREEN_SIZE = (WIDTH, HIGH)

FILES_PATH = Path(__file__).parent

BACKGROUND_IMAGE = FILES_PATH / "space.jpg"
ENEMY_IMAGE = FILES_PATH / "monster.png"
IMAGE_SIZE_ENEMY = (63, 63)
PLAYER_IMAGE = FILES_PATH / "Player.png"
IMAGE_SIZE_PLAYER = (63, 63)
BULLET_IMAGE = FILES_PATH / "rocket.png"
IMAGE_SIZE_BULLET = (64, 64)

SHOOT_SOUND = FILES_PATH / "laser.wav"

SCORE_TEXT_X = 10
SCORE_TEXT_Y = 10

START_POSITION_PLAYER_X = 370
START_POSITION_Y = HIGH - 120

PLAYER_SPEED = 5

NUMBER_OF_ENEMY = 30
ENEMY_SPEED = 1
ENEMY_Y_STEP = 40

NUMBER_OF_BULLETS = 100
BULLET_SPEED = 5


class Figure(pygame.sprite.Sprite):
    def __init__(self, rect, image):
        pygame.sprite.Sprite.__init__(self)
        self.rect = rect
        self.image = image

    @classmethod
    def new(cls, x, y, image_size, image):
        return cls(pygame.Rect(x, y, *image_size), image)

    def update(self, x, y=0):
        self.rect.move_ip(x, 0)
        if y == 0:
            if self.rect.x <= 0:
                self.rect.x = 0
            elif self.rect.x >= WIDTH - IMAGE_SIZE_ENEMY[0]:
                self.rect.x = WIDTH - IMAGE_SIZE_ENEMY[0]
        elif self.rect.x >= WIDTH:
            self.rect.x = 0
            self.rect.y += y
        elif self.rect.y > START_POSITION_Y - IMAGE_SIZE_ENEMY[0]:
            self.rect.y = random.randint(50, 150)


class Bullet(pygame.sprite.Sprite):
    def __init__(self, rect, image):
        pygame.sprite.Sprite.__init__(self)
        self.rect = rect
        self.image = image
        self.is_active = False

    @classmethod
    def new(cls, x, y, image_size, image):
        return cls(pygame.Rect(x, y, *image_size), image)

    def update(self):
        if self.is_active:
            if self.rect.y == HIGH + START_POSITION_Y:
                self.rect.y = START_POSITION_Y
            self.rect.y -= BULLET_SPEED
            if self.rect.y < 0:
                self.is_active = False
                self.rect.y = HIGH + IMAGE_SIZE_BULLET[0]


class Gun:
    def __init__(self):
        self.is_ready = True
        self.fire_event = pygame.USEREVENT + 1


def is_collided_with(bullet, enemy):
    if bullet.is_active:
        return pygame.sprite.collide_rect(bullet, enemy)


def control_game(
    screen,
    background,
    players,
    enemies,
    bullets,
    font,
):
    clock = pygame.time.Clock()
    game_on = True
    score = 0
    gun = Gun()
    while game_on:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                game_on = False
            if event.type == gun.fire_event:
                gun.is_ready = True
                pygame.time.set_timer(gun.fire_event, 0)
        keys = pygame.key.get_pressed()
        process_user_input(keys, players, bullets, gun)
        for bullet, enemy in product(bullets, enemies):
            if is_collided_with(bullet, enemy):
                bullet.is_active = False
                bullet.rect.y = HIGH + START_POSITION_Y
                score += 1
        screen.blit(background, (0, 0))
        screen.blit(
            font.render(f"Score : {score}", True, (255, 255, 255)),
            (SCORE_TEXT_X, SCORE_TEXT_Y),
        )
        players.draw(screen)
        enemies.update(ENEMY_SPEED, ENEMY_Y_STEP)
        enemies.draw(screen)
        bullets.update()
        bullets.draw(screen)
        pygame.display.update()
        clock.tick(100)


def process_user_input(keys, players, bullets, gun):
    if keys[pygame.K_LEFT]:
        players.update(-PLAYER_SPEED)
    elif keys[pygame.K_RIGHT]:
        players.update(PLAYER_SPEED)
    elif keys[pygame.K_SPACE] and gun.is_ready:
        gun.is_ready = False
        mixer.Sound(SHOOT_SOUND).play()
        for bullet in bullets:
            if not bullet.is_active:
                bullet.is_active = True
                for player in players:
                    bullet.rect.x = player.rect.x
        pygame.time.set_timer(gun.fire_event, 100)


def main():
    pygame.init()
    pygame.display.set_caption("Space Invader")
    screen = pygame.display.set_mode(SCREEN_SIZE)
    background = pygame.image.load(BACKGROUND_IMAGE)
    player_image = pygame.transform.scale(
        pygame.image.load(PLAYER_IMAGE), IMAGE_SIZE_PLAYER
    )
    enemy_image = pygame.transform.scale(
        pygame.image.load(ENEMY_IMAGE), IMAGE_SIZE_ENEMY
    )
    bullet_image = pygame.transform.scale(
        pygame.image.load(BULLET_IMAGE), IMAGE_SIZE_BULLET
    )
    player = pygame.sprite.Group()
    player.add(
        Figure.new(
            START_POSITION_PLAYER_X,
            START_POSITION_Y,
            IMAGE_SIZE_PLAYER,
            player_image,
        )
    )
    enemies = pygame.sprite.Group()
    enemies.add(
        *[
            Figure.new(
                random.randint(0, WIDTH - IMAGE_SIZE_ENEMY[0]),
                random.randint(50, 150),
                IMAGE_SIZE_ENEMY,
                enemy_image,
            )
            for _ in range(NUMBER_OF_ENEMY)
        ]
    )
    bullets = pygame.sprite.Group()
    bullets.add(
        *[
            Bullet.new(
                0,
                START_POSITION_Y,
                IMAGE_SIZE_BULLET,
                bullet_image,
            )
            for _ in range(NUMBER_OF_BULLETS)
        ]
    )

    font = pygame.font.Font("freesansbold.ttf", 32)

    control_game(
        screen,
        background,
        player,
        enemies,
        bullets,
        font,
    )


if __name__ == "__main__":
    main()
Vielleicht sehe ich die Sache morgen mit frischem Kopf wieder anders.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
__blackjack__
User
Beiträge: 13123
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Dennis89: Nur ganz kurz reingeschaut: Verlorene Tastendrücke: Das kann passieren wenn man sich immer nur eine Momentaufnahme der gerade gedrückten Tasten geben lässt. Steht auch in der Doku. Und Du lässt die Leertaste auch immer wieder kurz los auch wenn *Du* das nicht wirklich machst: Das ist eine Taste mit automatischer Wiederholung, das macht das System für Dich. Ist doch nett, oder? 😈

Die 100 Punkte pro Treffer kommen dadurch zustande das jeder Schuss aus allen Geschossen besteht. Du solltest die Schleife vielleicht nach dem ersten nicht-aktiven und dann aktivierten Geschoss abbrechen und nicht *alle* nicht-aktiven Geschosse auf einmal los schicken. Das ist ein bisschen Overkill. 🙂
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
Dennis89
User
Beiträge: 1158
Registriert: Freitag 11. Dezember 2020, 15:13

Hi und Danke.
Ist doch nett, oder? 😈
Ja, ich bin ein großer Freund von Dingen, die ungefragt und leise irgendwas für mich machen. 🦧
Was ist denn der Hintergrund dafür? Ist das nur bei der Leertaste so? Wird damit erreicht, das wenn ich die Taste gedrückt halte (egal in welcher Anwendung) es für den User wie viele Leerzeichen aussieht, dass System das aber nur machen kann, wenn es immer wieder einzelne Signale dafür bekommt?
Jetzt ist es so, dass wenn ich die Leertaste drücke, bei jedem Tastendruck der Ton für das Abschießen kommt, aber nicht immer eine Rakete.


Den zweiten Teil verstehe ich nicht ganz. Ich verstehe, dass ich nur eine Kollision werten soll, wenn es ein aktives Geschoss ist. Das mit dem Teil "nach dem ersten nicht-aktiven" verstehe ich nicht. Wenn der erste Schuss den Index 0 hat, dann wird der bei einem Treffer nicht gewertet, weil es kein nicht-aktives Geschoss davor gibt. Und wenn ich nach der ersten erkannten Kollision die Schleife abbreche, was ist dann wenn gerade zwei Geschosse fliegen und die Feinde in y-Position so versetzt sind, dass beide Geschosse gleichzeitig treffen? Okay, von der ersten Koordinatenüberschneidung bis zu letzten, wird die Schleife sicher wieder aufgerufen, das ist vermutlich kein Argument. Also habe ich die Schleife mal geändert, dass ich nur werte wenn die Kollision da ist und das Geschoss aktiv ist und danach breche ich ab.

Das Ergebnis verwirrt mich. Obwohl ich das Geschoss auf "nicht aktive" setze werden Punkte gezählt bis es den Feind durchquert hat. 🤯 Ist `itertools.product` überhaupt das Richtige? Nur weil ich nach der Kollision das Geschoss auf "nicht-aktive" setze, ist das in meinem `product` ja noch eine "Aufnahme" von davor? Aber trotzdem darf ein Feind mit einem Geschoss nur ein mal zählen und das tut es nicht, wenn ich bewusst und zu 100% eine Rakete auf nur einen Feind abfeuere. Sind das in `product` sowas wie Kopien meiner Objekte?

Damit wir immer vom gleichen reden, hier noch mal der ganze Code

Code: Alles auswählen

import random
from itertools import product
from pathlib import Path

import pygame
from pygame import mixer

WIDTH = 800
HIGH = 600
SCREEN_SIZE = (WIDTH, HIGH)

FILES_PATH = Path(__file__).parent

BACKGROUND_IMAGE = FILES_PATH / "space.jpg"
ENEMY_IMAGE = FILES_PATH / "monster.png"
IMAGE_SIZE_ENEMY = (63, 63)
PLAYER_IMAGE = FILES_PATH / "Player.png"
IMAGE_SIZE_PLAYER = (63, 63)
BULLET_IMAGE = FILES_PATH / "rocket.png"
IMAGE_SIZE_BULLET = (64, 64)

SHOOT_SOUND = FILES_PATH / "laser.wav"

SCORE_TEXT_X = 10
SCORE_TEXT_Y = 10

START_POSITION_PLAYER_X = 370
START_POSITION_Y = HIGH - 120

PLAYER_SPEED = 5

NUMBER_OF_ENEMY = 30
ENEMY_SPEED = 1
ENEMY_Y_STEP = 40

NUMBER_OF_BULLETS = 100
BULLET_SPEED = 5


class Figure(pygame.sprite.Sprite):
    def __init__(self, rect, image):
        pygame.sprite.Sprite.__init__(self)
        self.rect = rect
        self.image = image

    @classmethod
    def new(cls, x, y, image_size, image):
        return cls(pygame.Rect(x, y, *image_size), image)

    def update(self, x, y=0):
        self.rect.move_ip(x, 0)
        if y == 0:
            if self.rect.x <= 0:
                self.rect.x = 0
            elif self.rect.x >= WIDTH - IMAGE_SIZE_ENEMY[0]:
                self.rect.x = WIDTH - IMAGE_SIZE_ENEMY[0]
        elif self.rect.x >= WIDTH:
            self.rect.x = 0
            self.rect.y += y
        elif self.rect.y > START_POSITION_Y - IMAGE_SIZE_ENEMY[0]:
            self.rect.y = random.randint(50, 150)


class Bullet(pygame.sprite.Sprite):
    def __init__(self, rect, image):
        pygame.sprite.Sprite.__init__(self)
        self.rect = rect
        self.image = image
        self.is_active = False

    @classmethod
    def new(cls, x, y, image_size, image):
        return cls(pygame.Rect(x, y, *image_size), image)

    def update(self):
        if self.is_active:
            if self.rect.y == HIGH + IMAGE_SIZE_BULLET[0]:
                self.rect.y = START_POSITION_Y
            self.rect.y -= BULLET_SPEED
            if self.rect.y < 0:
                self.is_active = False
                self.rect.y = HIGH + IMAGE_SIZE_BULLET[0]


class Gun:
    def __init__(self):
        self.is_ready = True
        self.fire_event = pygame.USEREVENT + 1


def is_collided_with(bullet, enemy):
    if bullet.is_active:
        return pygame.sprite.collide_rect(bullet, enemy)


def control_game(
    screen,
    background,
    players,
    enemies,
    bullets,
    font,
):
    clock = pygame.time.Clock()
    game_on = True
    score = 0
    gun = Gun()
    while game_on:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                game_on = False
            if event.type == gun.fire_event:
                gun.is_ready = True
                pygame.time.set_timer(gun.fire_event, 0)
        keys = pygame.key.get_pressed()
        process_user_input(keys, players, bullets, gun)
        for bullet, enemy in product(bullets, enemies):
            if is_collided_with(bullet, enemy) and bullet.is_active:
                bullet.is_active = False
                bullet.rect.y = HIGH + IMAGE_SIZE_BULLET[0]
                score += 1
                break
        screen.blit(background, (0, 0))
        screen.blit(
            font.render(f"Score : {score}", True, (255, 255, 255)),
            (SCORE_TEXT_X, SCORE_TEXT_Y),
        )
        players.draw(screen)
        enemies.update(ENEMY_SPEED, ENEMY_Y_STEP)
        enemies.draw(screen)
        bullets.update()
        bullets.draw(screen)
        pygame.display.update()
        clock.tick(100)


def process_user_input(keys, players, bullets, gun):
    if keys[pygame.K_LEFT]:
        players.update(-PLAYER_SPEED)
    elif keys[pygame.K_RIGHT]:
        players.update(PLAYER_SPEED)
    elif keys[pygame.K_SPACE] and gun.is_ready:
        gun.is_ready = False
        mixer.Sound(SHOOT_SOUND).play()
        for bullet in bullets:
            if not bullet.is_active:
                bullet.is_active = True
                for player in players:
                    bullet.rect.x = player.rect.x
        pygame.time.set_timer(gun.fire_event, 100)


def main():
    pygame.init()
    pygame.display.set_caption("Space Invader")
    screen = pygame.display.set_mode(SCREEN_SIZE)
    background = pygame.image.load(BACKGROUND_IMAGE)
    player_image = pygame.transform.scale(
        pygame.image.load(PLAYER_IMAGE), IMAGE_SIZE_PLAYER
    )
    enemy_image = pygame.transform.scale(
        pygame.image.load(ENEMY_IMAGE), IMAGE_SIZE_ENEMY
    )
    bullet_image = pygame.transform.scale(
        pygame.image.load(BULLET_IMAGE), IMAGE_SIZE_BULLET
    )
    player = pygame.sprite.Group()
    player.add(
        Figure.new(
            START_POSITION_PLAYER_X,
            START_POSITION_Y,
            IMAGE_SIZE_PLAYER,
            player_image,
        )
    )
    enemies = pygame.sprite.Group()
    enemies.add(
        *[
            Figure.new(
                random.randint(0, WIDTH - IMAGE_SIZE_ENEMY[0]),
                random.randint(50, 150),
                IMAGE_SIZE_ENEMY,
                enemy_image,
            )
            for _ in range(NUMBER_OF_ENEMY)
        ]
    )
    bullets = pygame.sprite.Group()
    bullets.add(
        *[
            Bullet.new(
                0,
                START_POSITION_Y,
                IMAGE_SIZE_BULLET,
                bullet_image,
            )
            for _ in range(NUMBER_OF_BULLETS)
        ]
    )

    font = pygame.font.Font("freesansbold.ttf", 32)

    control_game(
        screen,
        background,
        player,
        enemies,
        bullets,
        font,
    )


if __name__ == "__main__":
    main()
Danke und Grüße
Dennis

Ich weis zwar, dass ich das auf die Position ganz unter den Bildschirm setze, aber
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
grubenfox
User
Beiträge: 433
Registriert: Freitag 2. Dezember 2022, 15:49

von kollisionen hatte __blackjack__ nichts geschrieben, er schrieb vom los schicken...
__blackjack__ hat geschrieben: Sonntag 7. April 2024, 14:20 und nicht *alle* nicht-aktiven Geschosse auf einmal los schicken.
Benutzeravatar
Dennis89
User
Beiträge: 1158
Registriert: Freitag 11. Dezember 2020, 15:13

Wie leicht es sein kann, wenn man richtig liest.
Danke, damit ist das Problem mit dem zählen der Punkte erledigt. 🙂

Dann ist jetzt "nur" noch die für mich fragwürdige Gun-Klasse und dass `Bullet` und `Figure` doch seehr ähnlichen/gleichen Code haben. Sollte man hier vielleicht über eine Klasse nachdenken, von der `Figure` und `Bullet` dann erben können?

Danke und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
Dennis89
User
Beiträge: 1158
Registriert: Freitag 11. Dezember 2020, 15:13

Soo, bin wieder zurück zu meiner `Game`-Klasse um die unnötige `Gun`-Klasse los zu werden. Für die Ähnlichkeit der zwei weiteren Klassen habe ich noch keine Lösung gefunden. Zumindest keine elegante.

Code: Alles auswählen

import random
from itertools import product
from pathlib import Path

import pygame
from attr import define, field
from pygame import mixer
from pygame.sprite import Group, Sprite
from pygame.time import Clock, set_timer

WIDTH = 800
HIGH = 600
SCREEN_SIZE = (WIDTH, HIGH)

FILES_PATH = Path(__file__).parent

BACKGROUND_IMAGE = FILES_PATH / "space.jpg"
ENEMY_IMAGE = FILES_PATH / "monster.png"
IMAGE_SIZE_ENEMY = (63, 63)
PLAYER_IMAGE = FILES_PATH / "Player.png"
IMAGE_SIZE_PLAYER = (63, 63)
BULLET_IMAGE = FILES_PATH / "rocket.png"
IMAGE_SIZE_BULLET = (64, 64)

SHOOT_SOUND = FILES_PATH / "laser.wav"

SCORE_TEXT_X = 10
SCORE_TEXT_Y = 10

START_POSITION_PLAYER_X = 370
START_POSITION_Y = HIGH - 120

PLAYER_SPEED = 5

NUMBER_OF_ENEMY = 30
ENEMY_SPEED = 1
ENEMY_Y_STEP = 40

NUMBER_OF_BULLETS = 100
BULLET_SPEED = 5


class Figure(Sprite):
    def __init__(self, rect, image):
        Sprite.__init__(self)
        self.rect = rect
        self.image = image

    @classmethod
    def new(cls, x, y, image_size, image):
        return cls(pygame.Rect(x, y, *image_size), image)

    def update(self, x, y=0):
        self.rect.move_ip(x, 0)
        if y == 0:
            if self.rect.x <= 0:
                self.rect.x = 0
            elif self.rect.x >= WIDTH - IMAGE_SIZE_ENEMY[0]:
                self.rect.x = WIDTH - IMAGE_SIZE_ENEMY[0]
        elif self.rect.x >= WIDTH:
            self.rect.x = 0
            self.rect.y += y
        elif self.rect.y > START_POSITION_Y - IMAGE_SIZE_ENEMY[0]:
            self.rect.y = random.randint(50, 150)


class Bullet(Sprite):
    def __init__(self, rect, image):
        Sprite.__init__(self)
        self.rect = rect
        self.image = image
        self.is_active = False

    @classmethod
    def new(cls, x, y, image_size, image):
        return cls(pygame.Rect(x, y, *image_size), image)

    def update(self):
        if self.is_active:
            if self.rect.y == HIGH + IMAGE_SIZE_BULLET[0]:
                self.rect.y = START_POSITION_Y
            self.rect.y -= BULLET_SPEED
            if self.rect.y < 0:
                self.is_active = False
                self.rect.y = HIGH + IMAGE_SIZE_BULLET[0]


@define
class Game:
    screen = field()
    background = field()
    players = field()
    enemies = field()
    bullets = field()
    font = field()
    fire_event = field()
    score = field(default=0)
    gun_is_ready = field(default=True)

    def run(self):
        clock = Clock()
        game_on = True
        while game_on:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    game_on = False
                if event.type == self.fire_event:
                    self.gun_is_ready = True
                    set_timer(self.fire_event, 0)
            keys = pygame.key.get_pressed()
            self.process_user_input(keys)
            for bullet, enemy in product(self.bullets, self.enemies):
                if is_collided_with(bullet, enemy) and bullet.is_active:
                    bullet.is_active = False
                    bullet.rect.y = HIGH + IMAGE_SIZE_BULLET[0]
                    enemy.rect.x = random.randint(0, WIDTH - IMAGE_SIZE_ENEMY[0])
                    enemy.rect.y = random.randint(50, 150)
                    self.score += 1
                    break
            self.screen.blit(self.background, (0, 0))
            self.screen.blit(
                self.font.render(f"Score : {self.score}", True, (255, 255, 255)),
                (SCORE_TEXT_X, SCORE_TEXT_Y),
            )
            self.players.draw(self.screen)
            self.enemies.update(ENEMY_SPEED, ENEMY_Y_STEP)
            self.enemies.draw(self.screen)
            self.bullets.update()
            self.bullets.draw(self.screen)
            pygame.display.update()
            clock.tick(100)

    def process_user_input(self, keys):
        if keys[pygame.K_LEFT]:
            self.players.update(-PLAYER_SPEED)
        elif keys[pygame.K_RIGHT]:
            self.players.update(PLAYER_SPEED)
        elif keys[pygame.K_SPACE] and self.gun_is_ready:
            self.gun_is_ready = False
            mixer.Sound(SHOOT_SOUND).play()
            for bullet in self.bullets:
                if not bullet.is_active:
                    bullet.is_active = True
                    for player in self.players:
                        bullet.rect.x = player.rect.x
                    break
            set_timer(self.fire_event, 100)


def is_collided_with(bullet, enemy):
    if bullet.is_active:
        return pygame.sprite.collide_rect(bullet, enemy)


def main():
    pygame.init()
    pygame.display.set_caption("Space Invader")
    screen = pygame.display.set_mode(SCREEN_SIZE)
    background = pygame.image.load(BACKGROUND_IMAGE)
    player_image = pygame.transform.scale(
        pygame.image.load(PLAYER_IMAGE), IMAGE_SIZE_PLAYER
    )
    enemy_image = pygame.transform.scale(
        pygame.image.load(ENEMY_IMAGE), IMAGE_SIZE_ENEMY
    )
    bullet_image = pygame.transform.scale(
        pygame.image.load(BULLET_IMAGE), IMAGE_SIZE_BULLET
    )
    player = Group()
    player.add(
        Figure.new(
            START_POSITION_PLAYER_X,
            START_POSITION_Y,
            IMAGE_SIZE_PLAYER,
            player_image,
        )
    )
    enemies = Group()
    enemies.add(
        *[
            Figure.new(
                random.randint(0, WIDTH - IMAGE_SIZE_ENEMY[0]),
                random.randint(50, 150),
                IMAGE_SIZE_ENEMY,
                enemy_image,
            )
            for _ in range(NUMBER_OF_ENEMY)
        ]
    )
    bullets = pygame.sprite.Group()
    bullets.add(
        *[
            Bullet.new(
                -IMAGE_SIZE_BULLET[0],
                START_POSITION_Y,
                IMAGE_SIZE_BULLET,
                bullet_image,
            )
            for _ in range(NUMBER_OF_BULLETS)
        ]
    )

    font = pygame.font.Font("freesansbold.ttf", 32)

    game = Game(
        screen, background, player, enemies, bullets, font, pygame.USEREVENT + 1
    )
    game.run()


if __name__ == "__main__":
    main()
Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
__blackjack__
User
Beiträge: 13123
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

`sprite.Group` kann man beim erstellen schon die Sprites mitgeben.

Die `new()`-Methode macht nicht wirklich Sinn, weil man hier ja eine `__init__()` hat in der man das machen kann. Ausserdem ist es redundant die Bildgrösse noch mal als eigenes Argument zu übergeben, die kann man ja vom Bild-Surface abfragen.

`is_collided_with()` wäre ein Kandidat als Methode auf `Bullet`. Ausserdem sollte das nur `True` und `False` liefern und nicht `True`, `False`, und `None`. Und der ``and bullet.is_active``-Teil beim Aufruf ist redundant, denn das wird in der Funktion/Methode ja bereits getestet.

Die `Game`-Klasse würde ich erst einmal auf eine Funktion reduzieren. Vielleicht macht die Sinn, aber im Moment sieht das verdächtig nach einer Funktion aus die als Klasse verkleidet ist.

`fire_event` würde ich zu einer Konstanten machen.

`game_on` kann man sich sparen wenn man die Funktion oder Methode einfach mit ``return`` verlässt.

Für `players` würde ich ein `sprite.GroupSingle` nehmen und die etwas irreführende ``for player in players:``-Schleife loswerden.

Den Schuss-Sound sollte man vielleicht nur abspielen wenn auch tatsächlich ein nicht-aktives Geschoss aktiviert wurde.

``set_timer(fire_event, 0)`` ist mir ein Rätsel. Wenn das einmal eingetreten ist, dann wird das *ständig* neu ausgelöst? Dann bremst das doch gar nichts. Und es ist ja auch eher ein GUN_READY Ereignis, denn es wird ja benutzt um die Waffe wieder scharf zu schalten, nach dem sie abgefeuert wurde.

Das sich Spieler und Gegner eine `update()`-Methode teilen in der im Grunde anhand der Anzahl der Argumente entschieden wird ob es sich wie ein Spieler oder ein Gegner verhält ist unschön. Direkt falsch ist das in beiden Fällen die Konstanten für die Grösse des Gegners verwendet werden. Das funktioniert hier natürlich, aber nur weil beide gleich gross sind. Und an einigen Stellen müsste es der Index 1 statt 0 beim Zugriff auf die Grössenkonstanten sein. Was hier nur funktioniert weil Höhe und Breite jeweils gleich sind.

Optisch ist es auch gut das Spieler und Geschoss annähernd gleich gross sind, sonst sähe das eventuell komisch aus das die Geschosse nicht in der Mitte des Spielers starten sondern an der gleichen linken oberen Ecke.

Es ist so ein bisschen über den Code verteilt das nicht-aktive Geschosse ausserhalb der Anzeige platziert werden. Das könnte man mit dem `is_active()`-Flag koppeln. Dann ist das an *einer* Stelle im Code, der Zusammenhang zwischen dieser Position und „aktiv“ wird klarer, und es ist auch sichergestellt, das nicht-aktive Geschosse immer ausserhalb der Anzeige sind.

Für die Gegner kommt ``random.randint(50, 150)`` dreimal im Code vor und die zufällige horizontale Platzierung immerhin zweimal.

Zwischstand (ungetestet):

Code: Alles auswählen

import random
from itertools import product
from pathlib import Path

import pygame
from pygame import mixer
from pygame.sprite import Group, GroupSingle
from pygame.time import Clock, set_timer

WIDTH = 800
HEIGHT = 600
SCREEN_SIZE = (WIDTH, HEIGHT)

WHITE = (255, 255, 255)

FILES_PATH = Path(__file__).parent

BACKGROUND_IMAGE_PATH = FILES_PATH / "space.jpg"
ENEMY_IMAGE_PATH = FILES_PATH / "monster.png"
ENEMY_IMAGE_SIZE = (63, 63)
PLAYER_IMAGE_PATH = FILES_PATH / "Player.png"
PLAYER_IMAGE_SIZE = (63, 63)
BULLET_IMAGE_PATH = FILES_PATH / "rocket.png"
BULLET_IMAGE_SIZE = (64, 64)

SHOOT_SOUND_PATH = FILES_PATH / "laser.wav"

SCORE_TEXT_POSITION = (10, 10)

START_POSITION_PLAYER_X = 370
START_POSITION_Y = HEIGHT - 120

PLAYER_SPEED = 5

NUMBER_OF_ENEMIES = 30
ENEMY_SPEED = 1
ENEMY_Y_STEP = 40

NUMBER_OF_BULLETS = 100
BULLET_SPEED = 5

GUN_READY_EVENT = pygame.USEREVENT + 1


def load_image(file_path, size):
    return pygame.transform.scale(pygame.image.load(file_path), size)


class Sprite(pygame.sprite.Sprite):
    def __init__(self, image, position=(0, 0)):
        pygame.sprite.Sprite.__init__(self)
        self.image = image
        self.rect = image.get_rect(topleft=position)


class Player(Sprite):
    def update(self, screen, x_delta):
        self.rect.move_ip(x_delta, 0)
        self.rect.clamp_ip(screen.get_rect())


class Enemy(Sprite):
    def update(self, screen, x_delta, y_delta):
        self.rect.move_ip(x_delta, 0)
        if self.rect.left >= screen.get_width():
            self.rect.left = 0
            self.rect.y += y_delta

        if self.rect.bottom > START_POSITION_Y:
            self.move_to_top()

    def move_to_top(self):
        self.rect.top = random.randint(50, 150)

    def move_to_random_start_position(self, screen):
        self.move_to_top()
        self.rect.x = random.randint(0, screen.get_width() - self.rect.width)

    @classmethod
    def at_random_start_position(cls, image, screen):
        enemy = cls(image)
        enemy.move_to_random_start_position(screen)
        return enemy


class Bullet(Sprite):
    def __init__(self, image):
        Sprite.__init__(self, image)
        self._is_active = None
        self.is_active = False

    @property
    def is_active(self):
        return self._is_active

    @is_active.setter
    def is_active(self, value):
        if not value:
            self.rect.bottom = -1
        self._is_active = value

    def update(self):
        if self.is_active:
            self.rect.y -= BULLET_SPEED
            if self.rect.top < 0:
                self.is_active = False

    def is_collided_with(self, enemy):
        return self.is_active and pygame.sprite.collide_rect(self, enemy)


def run(screen, background, players, enemies, bullets, font):
    score = 0
    gun_is_ready = True
    clock = Clock()
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return

            elif event.type == GUN_READY_EVENT:
                gun_is_ready = True

        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT]:
            players.update(-PLAYER_SPEED)

        elif keys[pygame.K_RIGHT]:
            players.update(PLAYER_SPEED)

        elif keys[pygame.K_SPACE] and gun_is_ready:
            gun_is_ready = False
            for bullet in bullets:
                if not bullet.is_active:
                    mixer.Sound(SHOOT_SOUND_PATH).play()
                    bullet.is_active = True
                    bullet.rect.midtop = players.sprite.rect.midtop
                    break
            set_timer(GUN_READY_EVENT, 100)

        for bullet, enemy in product(bullets, enemies):
            if bullet.is_collided_with(enemy):
                bullet.is_active = False
                enemy.move_to_random_start_position(screen)
                score += 1
                break

        screen.blit(background, (0, 0))
        screen.blit(
            font.render(f"Score : {score}", True, WHITE), SCORE_TEXT_POSITION
        )
        enemies.update(ENEMY_SPEED, ENEMY_Y_STEP)
        bullets.update()
        for group in [players, enemies, bullets]:
            group.draw(screen)
        pygame.display.update()
        clock.tick(100)


def main():
    pygame.init()
    screen = pygame.display.set_mode(SCREEN_SIZE)
    pygame.display.set_caption("Space Invader")
    enemy_image = load_image(ENEMY_IMAGE_PATH, ENEMY_IMAGE_SIZE)
    bullet_image = load_image(BULLET_IMAGE_PATH, BULLET_IMAGE_SIZE)
    run(
        screen,
        pygame.image.load(BACKGROUND_IMAGE_PATH),
        GroupSingle(
            Player(
                load_image(PLAYER_IMAGE_PATH, PLAYER_IMAGE_SIZE),
                (START_POSITION_PLAYER_X, START_POSITION_Y),
            )
        ),
        Group(
            (
                Enemy.at_random_start_position(enemy_image, screen)
                for _ in range(NUMBER_OF_ENEMIES)
            )
        ),
        Group((Bullet(bullet_image) for _ in range(NUMBER_OF_BULLETS))),
        pygame.font.Font("freesansbold.ttf", 32),
    )


if __name__ == "__main__":
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
Dennis89
User
Beiträge: 1158
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo,

vielen Dank für die Verbesserungen! 😊

Es kann sein, dass das `set_timer(self.fire_event, 0)` noch von Versuchen drin war und ich übersehen habe.

Das `Game` nach einer verkleideten Klasse aussieht, liegt daran, dass ich verzweifelt eine Möglichkeit gesucht habe mir `fire_event` und `gun_is_ready` zu merken. Ein, zwei Posts vorher war es noch eine Funktion. Dabei spielte auch folgender Gedanke eine Rolle. Ich wollte `run` nicht so lange machen und habe deswegen die Tastatur-Abfrage in eine extra Funktion geschrieben. Ich hielt es für unübersichtlicher, das zurück zu holen und mir dadurch die Klasse zu sparen. Habe immer etwas im Hinterkopf, dass Funktionen so kurz wie möglich sein sollten, aber das trifft bei so einer Ablaufsteuerung wohl nicht zu bzw. verkompliziert das Ganze mehr.

Dein Code gefällt mir wesentlich besser! Eine Frage hätte ich aber dazu noch.
`Player` erbt von`Sprite` und `Bullet` auch. (Ich glaube gerade im schreiben ist mir die Antwort eingefallen) Muss ich in `Bullet` die `__init__` von `Sprite` extra aufrufen, weil `Bullet` wegen `is_active` und `_is_active` eine eigene `__init__` hat und `Player` im Gegensatz dazu nicht. `Player` hat die gleichen Attribute wie `Sprite` und `Bullet` hat zu den Attributen von `Sprite` noch die zwei weiteren und damit die alle auf der Klasse verfügbar sind, der Aufruf?


Danke und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
__blackjack__
User
Beiträge: 13123
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Dennis89: Die `run()`-Funktion ist in der Tat nicht ganz so kurz. Da kann man sicher noch was heraus ziehen, in eigene Funktionen oder vielleicht auch in eine `Game`-Klasse. Ich wollte das aber erst mal in einer Funktion haben um Komplexität loszuwerden und dann zu sehen was man da wieder raus verteilen kann. Aber zum Beispiel auch in bereits vorhandene Klassen. Das abfeuern eines Geschosses könnte man beispielsweise in den Spieler verlagern (ungetestet):

Code: Alles auswählen

import random
import time
from itertools import product
from pathlib import Path

import pygame
from pygame import mixer
from pygame.sprite import Group, GroupSingle
from pygame.time import Clock

WIDTH = 800
HEIGHT = 600
SCREEN_SIZE = (WIDTH, HEIGHT)

WHITE = (255, 255, 255)

FILES_PATH = Path(__file__).parent

BACKGROUND_IMAGE_PATH = FILES_PATH / "space.jpg"
ENEMY_IMAGE_PATH = FILES_PATH / "monster.png"
ENEMY_IMAGE_SIZE = (63, 63)
PLAYER_IMAGE_PATH = FILES_PATH / "Player.png"
PLAYER_IMAGE_SIZE = (63, 63)
BULLET_IMAGE_PATH = FILES_PATH / "rocket.png"
BULLET_IMAGE_SIZE = (64, 64)

SHOOT_SOUND_PATH = FILES_PATH / "laser.wav"

SCORE_TEXT_POSITION = (10, 10)

START_POSITION_PLAYER_X = 370
START_POSITION_Y = HEIGHT - 120

PLAYER_SPEED = 5
GUN_COOLOFF_TIME = 0.001  # in seconds.

NUMBER_OF_ENEMIES = 30
ENEMY_SPEED = 1
ENEMY_Y_STEP = 40

NUMBER_OF_BULLETS = 100
BULLET_SPEED = 5


def load_image(file_path, size):
    return pygame.transform.scale(pygame.image.load(file_path), size)


class Sprite(pygame.sprite.Sprite):
    def __init__(self, image, position=(0, 0)):
        pygame.sprite.Sprite.__init__(self)
        self.image = image
        self.rect = image.get_rect(topleft=position)


class Player(Sprite):
    def __init__(self, image, position, bullets):
        Sprite.__init__(self, image, position)
        self.gun_ready_timestamp = time.monotonic()
        self.bullets = bullets

    def fire_gun(self):
        now = time.monotonic()
        if now >= self.gun_ready_timestamp:
            self.gun_ready_timestamp = now + GUN_COOLOFF_TIME
            for bullet in self.bullets:
                if not bullet.is_active:
                    bullet.is_active = True
                    bullet.rect.midtop = self.rect.midtop
                    return True

        return False

    def update(self, screen, x_delta):
        self.rect.move_ip(x_delta, 0)
        self.rect.clamp_ip(screen.get_rect())


class Enemy(Sprite):
    def update(self, screen, x_delta, y_delta):
        self.rect.move_ip(x_delta, 0)
        if self.rect.left >= screen.get_width():
            self.rect.left = 0
            self.rect.y += y_delta

        if self.rect.bottom > START_POSITION_Y:
            self.move_to_top()

    def move_to_top(self):
        self.rect.top = random.randint(50, 150)

    def move_to_random_start_position(self, screen):
        self.move_to_top()
        self.rect.x = random.randint(0, screen.get_width() - self.rect.width)

    @classmethod
    def at_random_start_position(cls, image, screen):
        enemy = cls(image)
        enemy.move_to_random_start_position(screen)
        return enemy


class Bullet(Sprite):
    def __init__(self, image):
        Sprite.__init__(self, image)
        self._is_active = None
        self.is_active = False

    @property
    def is_active(self):
        return self._is_active

    @is_active.setter
    def is_active(self, value):
        if not value:
            self.rect.bottom = -1
        self._is_active = value

    def update(self):
        if self.is_active:
            self.rect.y -= BULLET_SPEED
            if self.rect.top < 0:
                self.is_active = False

    def is_collided_with(self, enemy):
        return self.is_active and pygame.sprite.collide_rect(self, enemy)


def run(screen, background, players, enemies, bullets, font):
    score = 0
    clock = Clock()
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return

        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT]:
            players.update(-PLAYER_SPEED)

        elif keys[pygame.K_RIGHT]:
            players.update(PLAYER_SPEED)

        elif keys[pygame.K_SPACE]:
            if players.sprite.fire_gun():
                mixer.Sound(SHOOT_SOUND_PATH).play()

        for bullet, enemy in product(bullets, enemies):
            if bullet.is_collided_with(enemy):
                bullet.is_active = False
                enemy.move_to_random_start_position(screen)
                score += 1
                break

        screen.blit(background, (0, 0))
        screen.blit(
            font.render(f"Score : {score}", True, WHITE), SCORE_TEXT_POSITION
        )
        enemies.update(ENEMY_SPEED, ENEMY_Y_STEP)
        bullets.update()
        for group in [players, enemies, bullets]:
            group.draw(screen)
        pygame.display.update()
        clock.tick(100)


def main():
    pygame.init()
    screen = pygame.display.set_mode(SCREEN_SIZE)
    pygame.display.set_caption("Space Invader")
    enemy_image = load_image(ENEMY_IMAGE_PATH, ENEMY_IMAGE_SIZE)
    bullet_image = load_image(BULLET_IMAGE_PATH, BULLET_IMAGE_SIZE)
    bullets = Group((Bullet(bullet_image) for _ in range(NUMBER_OF_BULLETS)))
    run(
        screen,
        pygame.image.load(BACKGROUND_IMAGE_PATH),
        GroupSingle(
            Player(
                load_image(PLAYER_IMAGE_PATH, PLAYER_IMAGE_SIZE),
                (START_POSITION_PLAYER_X, START_POSITION_Y),
                bullets,
            )
        ),
        Group(
            (
                Enemy.at_random_start_position(enemy_image, screen)
                for _ in range(NUMBER_OF_ENEMIES)
            )
        ),
        bullets,
        pygame.font.Font("freesansbold.ttf", 32),
    )


if __name__ == "__main__":
    main()
`Player` hat hier jetzt auch seine eigene `__init__()` in der die `__init__()` von `Sprite` aufgerufen wird, damit die Attribute die dort gesetzt werden, auch auf `Player`-Objekten existieren. `Enemy` hat weiterhin keine eigene `__init__()`. Dann wird ja automatisch die von `Sprite` geerbte `__init__()` aufgerufen.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
Dennis89
User
Beiträge: 1158
Registriert: Freitag 11. Dezember 2020, 15:13

Guten Morgen,

ja das sieht ja noch besser aus, vielen Dank.

Interessant und viel eleganter finde ich auch sowas wie `screen.get_width()` oder sowas wie `midtop`. Da sollte man ja vor dem Beginn des Programmierens die Doku fast auswendig lernen. Ich wäre gar nicht auf die Idee gekommen, nach zu schauen, ob es da schon was fertiges gibt.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
Kebap
User
Beiträge: 687
Registriert: Dienstag 15. November 2011, 14:20
Wohnort: Dortmund

Dennis89 hat geschrieben: Sonntag 7. April 2024, 17:27 Ja, ich bin ein großer Freund von Dingen, die ungefragt und leise irgendwas für mich machen. 🦧
Was ist denn der Hintergrund dafür? Ist das nur bei der Leertaste so? Wird damit erreicht, das wenn ich die Taste gedrückt halte (egal in welcher Anwendung) es für den User wie viele Leerzeichen aussieht, dass System das aber nur machen kann, wenn es immer wieder einzelne Signale dafür bekommt
Das macht dein Betriebsssystem für dich. Du kannst ja mal einen beliebigen Texteditor öffnen und bspw. die Taste "e" drücken und länger festhalten.
Du wirst sehen, dass nach einer kurzen Wartezeit X nicht nur 1x der Buchstabe E erscheint, sondern dann alle Y Momente ein weiterer hinzukommt.
Dein Betriebssystem wird dich wahrscheinlich die Dauer von X und Y beeinflussen lassen, oder gar die Funktion komplett deaktivieren, je nachdem.
Ist aber ganz praktisch, wenn man eine Taste wirklich mehrfach drücken will, tun sonst irgendwann die Finger weh, wie beim Zocken am Controller.
MorgenGrauen: 1 Welt, 8 Rassen, 13 Gilden, >250 Abenteuer, >5000 Waffen & Rüstungen,
>7000 NPC, >16000 Räume, >200 freiwillige Programmierer, nur Text, viel Spaß, seit 1992.
Antworten