Pygame: Spielfigur auf dem Bildschirm "zittert"

Hier werden alle anderen GUI-Toolkits sowie Spezial-Toolkits wie Spiele-Engines behandelt.
Antworten
Pythoguras
User
Beiträge: 8
Registriert: Donnerstag 19. September 2013, 11:35

Sonntag 22. September 2013, 16:59

Hallo zusammen,

ich habe einen ersten Entwurf für ein Spiel auf Pygame-Basis programmiert, bei dem man eine Spielfigur (=Bilddatei) auf dem Bildschirm durch Mausklicks bewegen kann.

Leider muss ich dabei irgendwo Fehler gemacht haben, denn die Spielfigur zittert/wackelt mit hoher Frequenz um etwa 3 Pixel in diagonaler Richtung hin und her, nachdem man sie mit der Maus an eine andere Position geschickt hat (was generell funktioniert). Ich dachte zunächst auf enen Rundungsfehler, da meine Bilddatei eine ungerade Anzahl an Pixeln (in der Breite) hatte, welche für die Festlegung der Position durch 2 geteilt wird. Als ich die Bildbreite angepasst habe, war das Problem allerdings nicht behoben.

Vorgehensweise des Programms: Wird die rechte Maustaste gedrückt, prüft das Programm, ob die Koordinaten des Mauszeigers sich von der aktuellen Position der Figur unterscheiden. Falls dem so ist, wird PlayerIsMoving = True und bei den folgenden Durchläufen der Game Loop werden die X und Y-Positionen der Figur jeweils um 1 inkrementiert oder dekrementiert (je nach dem, in welche Richtung geklickt wurde) - solange, bis die Figur ihren Zielort erreicht hat.

Bilddatei: Link

Code: Alles auswählen

import pygame, sys, os
from pygame.locals import *

os.environ['SDL_VIDEO_CENTERED'] = '1' # Window centered

# Colors
GREEN = (50, 180, 50)

FPS = 30
WINDOWWIDTH = 1000
WINDOWHEIGHT = 700
BGCOLOR = GREEN

ICONPATH = 'Icons\\'

# Main Function
def main():
    pygame.init()
    global FPSCLOCK, SCREENSURF
    
    FPSCLOCK = pygame.time.Clock()
    SCREENSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
    pygame.display.set_caption('Testspiel')
    SCREENSURF.fill(BGCOLOR)

    # Load Images
    try: playerIcon = pygame.image.load(os.path.join(ICONPATH, 'playerIcon.png'))
    finally: None

    mouseX = 0
    mouseY = 0

    playerX = WINDOWWIDTH  / 2 - playerIcon.get_size()[0] / 2
    playerY = WINDOWHEIGHT / 2 - playerIcon.get_size()[1] / 2
    
    PlayerIsMoving = False    

# Game Loop
    while True:
        SCREENSURF.fill(BGCOLOR)
        if PlayerIsMoving:
                if destX > playerX:
                    playerX += 1
                else:
                    playerX -= 1
                if destY > playerY:
                    playerY += 1
                else:
                    playerY -= 1
        
        # Set Player Position    
        SCREENSURF.blit(playerIcon, (playerX, playerY))
        
        for event in pygame.event.get():
            if event.type == QUIT:
                pygame.quit()
                sys.exit
            elif event.type == MOUSEBUTTONUP:
                if event.button == 3:
                    mouseX, mouseY = event.pos
                    if (mouseX != playerX) or (mouseY != playerY):
                        PlayerIsMoving = True
                        destinationX = mouseX - playerIcon.get_size()[0] / 2
                        destinationY = mouseY - playerIcon.get_size()[1] / 2
                    else: PlayerIsMoving = False               
        pygame.display.update()
        FPSCLOCK.tick(FPS)

if __name__ == '__main__':
        main()
Sieht jemand, woran es hapern könnte?

Mfg
Pythoguras
Zuletzt geändert von Anonymous am Sonntag 22. September 2013, 17:21, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Code-Tags gesetzt.
BlackJack

Sonntag 22. September 2013, 17:25

@Pythoguras: Das liegt daran das Dein Code die Figur grundsätzlich ein Pixel in X- und Y-Richtung bewegt. Also auch wenn das Ziel in X- oder Y-Richtung erreicht ist da Code der die Figur dort wieder um ein Pixel weg bewegt und dann im nächsten durchlauf wieder hinbewegt, und dann wieder wegbewegt, und so weiter.

Edit: Noch ein paar Anmerkungen zum Quelltext:

Die Namensgebung und Quelltextformatierung weicht vom Style Guide for Python Code ab. Insbesondere die durchgehend in Grossbuchstaben geschriebenen Namen die *nicht* für Konstante Werte stehen sind äusserst unschön. Das ist eine Konvention die es in mehreren Programmiersprachen gibt, nicht nur in Python und man verwirrt damit selbst erfahrene Programmierer, beziehungsweise gerade die, weil so ein Name Erwartungen auslöst, die nicht erfüllt werden.

``global`` hat in aller Regel in sauberen Programmen nichts zu suchen. Hier ist es sogar völlig sinnfrei einsetzt, weil das Weglassen dieser Zeile keine Auswirkungen auf den Programmablauf hat.

Ebenfalls total sinnlos ist das ``try``/``finally``-Konstrukt. Was denkst Du denn was das dort bewirkt?

Die '\\' am Ende von `ICONPATH` sind kontraproduktiv. Du verwendest schon `os.path.join()` und sabotierst das mit dem Windows-Pfadtrenner dann wieder. Ohne würde das auch unter Linux und MacOS funktionieren. Genau deshalb nimmt man ja `os.path.join()`.

Die Position würde man bei Pygame besser mit einem `Rect`-Objekt abbilden als mit zwei separaten Namen an die ganze Zahlen gebunden werden. Diese Objekte haben (berechnete) Attribute für verschiedene Punkte, zum Beispiel den Mittelpunkt und eine `move()`-Methode um das ganze (virtuelle) Rechteck relativ zu den aktuellen Werten zu verschieben. Damit kann man kürzeren und klareren Code schreiben. Zum Beispiel um den Mittelpunkt des Rechtecks für das Spieler-Bild auf den Mittelpunkt des Screen-Surface zu setzen:

Code: Alles auswählen

    player_rect = player_icon.get_rect()
    player_rect.center = screen_surface.get_rect().center
BlackJack

Sonntag 22. September 2013, 17:55

@Pythoguras: Okay, das ist nicht der Code den Du hast laufen lassen. Denn `destX` und `destY` sind nicht definiert und führen zu einem Programmabbruch.

Edit: `sys.exit()` hast Du nicht *aufgerufen*, sondern nur die Funktion referenziert, was keinen Effekt hat.

Das aktualisieren der Anzeige, also löschen der alten Anzeige, blitten der neuen Anzeige, und letzliches Anzeigen ist über die gesamte Schleife verteilt. Das gehört semantisch zusammen, deshalb macht es auch Sinn das optisch im Quelltext zusammen zu schreiben.

Das Flag ob sich der Spieler bewegt oder nicht, hängt IMHO von falschen Bedingungen ab. Ich hätte ja erwartet dass das Kriterium ist ob das Ziel erreicht wurde. Und nicht ob man mit der Maus exakt auf die aktuelle Spielerposition klicken kann. Überhaupt ist das Flag eigentlich unnötig wenn man die Bewegung richtig programmiert.

Das ganze mal vereinfacht, mit `Rect`, und ohne zitterndes Bild am Zielpunkt:

Code: Alles auswählen

#!/usr/bin/env python
import pygame, sys, os
from pygame.locals import *

os.environ['SDL_VIDEO_CENTERED'] = '1' # Window centered

# Colors
GREEN = (50, 180, 50)

FPS = 30
SCREEN_SIZE = 1000, 700
BGCOLOR = GREEN

ICONPATH = 'Icons'


def main():
    pygame.init()

    clock = pygame.time.Clock()
    screen = pygame.display.set_mode(SCREEN_SIZE)
    pygame.display.set_caption('Testspiel')

    player_icon = pygame.image.load('test.png')

    player_rect = player_icon.get_rect()
    destination = player_rect.center = screen.get_rect().center

    while True:
        screen.fill(BGCOLOR)
        screen.blit(player_icon, player_rect)
        pygame.display.update()

        destination_x, destination_y = destination
        if destination_x > player_rect.centerx:
            player_rect.x += 1
        elif destination_x < player_rect.centerx:
            player_rect.x -= 1
        if destination_y > player_rect.centery:
            player_rect.y += 1
        elif destination_y < player_rect.centery:
            player_rect.y -= 1

        for event in pygame.event.get():
            if event.type == QUIT:
                pygame.quit()
                sys.exit()
            elif event.type == MOUSEBUTTONUP and event.button == 3:
                destination = event.pos

        clock.tick(FPS)


if __name__ == '__main__':
    main()
Edit2: Nächster Schritt wäre dann `pygame.sprite` zu verwenden.
Pythoguras
User
Beiträge: 8
Registriert: Donnerstag 19. September 2013, 11:35

Sonntag 22. September 2013, 19:46

Hi BlackJack,

vielen Dank für deine Anmerkungen - den Quelltext habe ich in der Tat noch um ein einige zeilen gekürzt, die für die Sache nicht relevant waren - da ist wohl versehentlich etwas zu viel rausgelöscht worden.

Die try-finally-Anweisungen habe ich eingefügt, damit das Programm wenigstens nicht abschmiert, wenn die Dateien nicht vorhanden sind - das "None" steht da nur für die korrekte Syntax und soll in Zukunft noch geändert werden. Die Sache mit den globalen Variablen habe ich aus einem Buchübernommen - dort waren im Beispielprogramm allerdings auch noch einige Funktionen implementiert, die auf die Variablen zugreifen.

Bei den großgeschriebenen Konstanten/Variablen habe ich mich auch weitestgehend an das Buch gehalten; aber deine Anmerkung zum Style macht natürlich Sinn wenn man drüber nachdenkt :)

Mfg
Pythoguras
BlackJack

Sonntag 22. September 2013, 21:58

@Pythoguras: Das „abschmieren” des Programms kannst Du an der Stelle mit einem ``finally`` überhaupt nicht verhindern. Dabei ist es auch egal was in dem Block stehen würde. Das würde zwar noch ausgeführt, aber es hält eine Ausnahme nicht davon ab weiter „nach oben” zu wandern und in diesem Fall damit das Programm zu beenden. Das ist letztendlich auch gar nicht schlimm, denn wenn die Datei nicht geladen werden kann, dann kann das Programm doch sowieso nicht sinnvoll fortgeführt werden.

Edit: Das ganze mit einer von `Sprite` abgeleiteten Klasse für die Spielfigur:

Code: Alles auswählen

#!/usr/bin/env python
import pygame, sys, os
from pygame.locals import *

os.environ['SDL_VIDEO_CENTERED'] = '1' # Window centered

# Colors
GREEN = (50, 180, 50)

FPS = 30
SCREEN_SIZE = 1000, 700
BGCOLOR = GREEN

ICONPATH = 'Icons'


class Player(pygame.sprite.Sprite):
    def __init__(self, image, position):
        pygame.sprite.Sprite.__init__(self)
        self.image = image
        self.rect = image.get_rect()
        self.rect.center = position
        self.destination = position

    def update(self):
        destination_x, destination_y = self.destination
        if destination_x > self.rect.centerx:
            self.rect.x += 1
        elif destination_x < self.rect.centerx:
            self.rect.x -= 1
        if destination_y > self.rect.centery:
            self.rect.y += 1
        elif destination_y < self.rect.centery:
            self.rect.y -= 1

    def draw(self, surface):
        surface.blit(self.image, self.rect)


def main():
    pygame.init()

    clock = pygame.time.Clock()
    screen = pygame.display.set_mode(SCREEN_SIZE)
    pygame.display.set_caption('Testspiel')

    player = Player(pygame.image.load('test.png'), screen.get_rect().center)

    while True:
        player.update()
        screen.fill(BGCOLOR)
        player.draw(screen)
        pygame.display.update()

        for event in pygame.event.get():
            if event.type == QUIT:
                pygame.quit()
                sys.exit()
            elif event.type == MOUSEBUTTONUP and event.button == 3:
                player.destination = event.pos

        clock.tick(FPS)


if __name__ == '__main__':
    main()
Benutzeravatar
Madmartigan
User
Beiträge: 200
Registriert: Donnerstag 18. Juli 2013, 07:59
Wohnort: Berlin

Mittwoch 25. September 2013, 14:52

Nur eine zusätzliche Anmerkung:

Die Bewegung des Sprite Objekts verläuft mit jener update() Funktion sehr linear und stellt keine direkte Bewegung auf das Ziel dar.
Gerade wenn |dx - dy| sehr groß ist, wird der Effekt deutlich. Für ein direktes "Hinbewegen" wäre daher eventuell eine vektorisierte Variante besser.
Für noch realistischere Bewegungsformen könnte man dann die Geschwindigkeit non-linearisieren. Stichpunkt EaseIn/EaseOut für Be- und Entschleunigung.

Soll nur ein Tipp sein...
Pythoguras
User
Beiträge: 8
Registriert: Donnerstag 19. September 2013, 11:35

Mittwoch 25. September 2013, 23:45

Hi und danke euch beiden,

die Art der Bewegung finde ich zwar auch nicht sehr ästhetisch, aber mein Versuch das Ganze etwas natürlicher wirken zu lassen ist leider gescheitert... Ich hatte dazu zunächst den Quotienten |dy / dx | berechnet (bzw. größeren durch kleineren Wert) und dann pro Durchlauf der Gameloop jeweils eine Koordinate der Spielfigur um eine größere Anzahl Pixel verändert als die andere. Nur ist dann die Bewegungsgeschwindigkeit nie konstant, was ziemlich plump ausschaute. Deshalb werde ich diese Sache zunächst so belassen und mich um andere "Baustellen" an dem entstehenden Spiel kümmern ;)

Mfg
Pythoguras
EyDu
User
Beiträge: 4871
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Donnerstag 26. September 2013, 00:24

Du solltest die Schrittweite der Bewegung noch abhängig von der Zeit machen. Der Weg über ``Clock.tick`` ist schon ein guter Ansatz, jedoch ist nicht garantiert, dass auch die exakte Zeit abgelaufen ist. Wenn man es genau nimmt, dann ist sie es nie, die kleinen Schwankungen fallen aber kaum auf. Problematisch wird es erst dann, wenn dein Programm doch mal etwas länger rechnet als erwartet. Entweder weil es viel zu tun hat oder weil dein Rechner im Hintergrund kurz mal etwas erledigt.

Dann solltest du auch weg von der Bewegung auf diskreten Pixeln und mit Vektoren rechnen, dass vereinfacht vieles (und macht natürlich auch ein paar Dinge am Anfang komlizierter). So kannst du dann leicht Beschleunigung und Geschwindigkeit auf den Objekten umsetzen und hast immer weiche Bewegungen. Zu letzterem bietet sich ein Sigmoid an oder du gehst einfach über den Sinus.
Das Leben ist wie ein Tennisball.
BlackJack

Donnerstag 26. September 2013, 07:09

Eine ganz simple 2D-Vektor-Klasse gibt es in der Standardbibliothek schon im `turtle`-Modul. Andererseits ist das auch eine nette Übung für eine eigene Klasse.
Antworten