Bildrotation und Verarbeitung von Tastaturbefehlen in Pygame

Hier werden alle anderen GUI-Toolkits sowie Spezial-Toolkits wie Spiele-Engines behandelt.
Antworten
Kensen468
User
Beiträge: 3
Registriert: Freitag 12. April 2013, 22:26

Hallo zusammen

Letztes Mal, als ich hier um Hilfe bitten wollte, fiel mir die Lösung während des Tippens des Posts ein, diesmal ist diese "Erleuchtung" leider ausgeblieben...

Ich poste zuerst den Code. Es ist ein Spiel, bei dem der Spieler eine Figur aus der Vogelperspektive lenkt. Später wird's drei verschiedene Modi, diverse Gegner und Waffen, sowie richtige Spielziele geben. Davon ist noch nicht viel implementiert.
Ich habe im Code selbst einiges an englischen Kommentaren, die ich gerne übersetzen könnte, sollte das gewünscht sein. Dazu hat es noch zwei Tags "PROBLEM XX" und einmal "FRAGE". Diese stehen nicht im Original-Code, sondern nur hier im Forum, um die problematischen Bereiche hervorzuheben.

Code: Alles auswählen

import pygame
import random
import os
import sys
import math

###############################################################################
#-------------------------PROGRAM VARIABLES-----------------------------------#
###############################################################################
#all variables concerning the build-up of the game
global screen

width = 1024
height = 768




###############################################################################
#-------------------------GAME VARIABLES--------------------------------------#
###############################################################################
#all variables concerning the game progress
weapon = 0#chosen weapon (1=blaster (unlimited ammo),2=autoblaster,3=rockets)
level = 1 #game starts at level 1; there will be a load function ingame
gamemode = 2 #0 = spacebattle, 1 = jump'n'run, 2 = strategic
shooting = False #turned True by the space key, keeps auto blaster firing
kup = kdown = kright = kleft = False #key events are handled by these
keystate = 0 #for movement keys
speed = 0#applicable in all three modes, just handled and passed differently
turnspeed = 0#applicable in modes 0 and 2, otherwise not passed at all
direction = 0 #the direction in gamemodes 0 and 2

pygame.mixer.pre_init(44100, -16, 2, 2048)

pygame.init()
screen = pygame.display.set_mode((width, height))

clock = pygame.time.Clock() #fps will be limited to 60

###############################################################################
#-------------------------SOUNDS----------------------------------------------#
###############################################################################


#function to load the soundfiles
def soundload(folder, name):
    fullname = os.path.join("data\\"+folder, name)
    try:
        sound = pygame.mixer.Sound(fullname)
    except:
        raise SystemExit, "Could not load or play soundfile: ", name
    return sound

global shotsounds
shotsounds = [soundload("sounds", "shotsmall.ogg")
              ,soundload("sounds", "shotauto.ogg")
              ,soundload("sounds", "shotrocket.ogg")]

###############################################################################
#-------------------------IMAGES----------------------------------------------#
###############################################################################


#function to load image files with transparency
def imgload(folder, name):
    fullname = os.path.join("data\\"+folder, name)
    try:
        image = pygame.image.load(fullname)
    except pygame.error, message:
        print "Image not found: ", name
        raise SystemExit, message
    image = image.convert_alpha()
    return image

#function to load image files without transparency
def imgloadnoalpha(folder, name):
    filename = os.path.join("data\\"+folder, name)
    try:
        image = pygame.image.load(filename)
    except pygame.error, message:
        print "Image not found: ", name
        raise SystemExit, message
    image = image.convert()
    return image


#The images are loadid into tuples here
global plstrat #the images for the strategy view
plstrat = [imgload("player", "stratmove1.png")
           , imgload("player", "stratstand.png")
           , imgload("player", "stratmove2.png")]





class PlayerClass(pygame.sprite.Sprite):
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.x = (width / 2)
        self.y = (height / 2)
        self.rect = pygame.rect.Rect(self.x,self.y,64,96)
        self.imgnr = 0
        self.imgmod = 0.15
        self.rad = 0

    def update(self, speed, direction, keystate):
        tempo = speed
        heading = direction
        keys = keystate
        if keystate > 0:
            self.rad = heading * math.pi / 180
            self.x += tempo * math.sin(self.rad)
            self.y += tempo * math.cos(self.rad)
        self.imgnr += self.imgmod
        if self.imgnr > 3:
            self.imgmod = -0.15
            self.inum = 2.5
        elif self.imgnr < 1:
            self.imgmod = 0.15
            self.inum = 1

        if gamemode == 0:
            pass#replace by imgload
        elif gamemode == 1:
            pass#replace by imgload
        elif gamemode == 2:
            if keys <= 0:
                bild = plstrat[1]
            elif keys > 0:
                bild = plstrat[int(round(self.imgnr - 1))]
        rotatedimg = pygame.transform.rotate(bild, self.rad) #PROBLEM 1

        rect = bild.get_rect()
        rect.center = (self.x,self.y)

        screen.blit(bild,rect)
        

Player = PlayerClass()
plgrp = pygame.sprite.RenderPlain(Player)

###############################################################################
#-------------------------WELCOME TO THE GAME---------------------------------#
###############################################################################


running = True
while running:
    clock.tick(60)

    if gamemode == 0:
        turnspeed = 50
        pygame.display.set_caption("Outer Space")
    elif gamemode == 1:
        pygame.display.set_caption("World")
    elif gamemode == 2:
        turnspeed = 50
        pygame.display.set_caption("Strategic View")
    Player.moving = False#TEST

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
            pygame.quit()
            sys.exit()

##        if not hasattr(event, "key"): #FRAGE 2
##            continue
    [b]#PROBLEM 3[/b]
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_KP8:
                kup = True
                keystate += 1

            if event.key == pygame.K_KP2:
                kdown = True
                keystate += 1

            if event.key == pygame.K_KP4:
                kleft = True
                keystate += 1

            if event.key == pygame.K_KP6:
                kright = True
                keystate += 1

            if event.key == pygame.K_ESCAPE:
                running = False
                pygame.quit()
                sys.exit()

        elif event.type == pygame.KEYUP:
            if event.key == pygame.K_KP8:
                kup = False
                keystate -= 1
            elif event.key == pygame.K_KP2:
                kdown = False
                keystate -= 1
            elif event.key == pygame.K_KP4:
                kleft = False
                keystate -= 1
            elif event.key == pygame.K_KP6:
                kright = False
                keystate -= 1

        if kup:
            if gamemode == 0:
                speed += 0.8
            elif gamemode == 1:
                speed = 1
            elif gamemode == 2:
                if speed < 0:
                    speed += 0.8
                elif speed >= 0:
                    speed = 0.8
            
        elif kdown:
            if gamemode == 0:
                speed -= 0.3
            elif gamemode == 1:
                speed = -1
            elif gamemode == 2:
                if speed > 0:
                    speed -= 0.8
                elif speed <= 0:
                    speed = -0.8
                    
        elif kright:
            if gamemode == 0 or gamemode == 2:
                direction += turnspeed
            elif gamemode == 1:
                pass
            
        elif kleft:
            if gamemode == 0 or gamemode == 2:
                direction += -turnspeed
            elif gamemode == 1:
                pass

        if keystate < 0:
            keystate = 0
        elif keystate == 0:
            speed = 0 #Note: all three game modes need the speed variable

    screen.fill((0, 0, 0))

    plgrp.update(speed, direction, keystate)

    pygame.display.update()
Problem 1
An der markierten Stelle in der PlayerClass.update-Funktion sollte eigentlich das Bild um self.rad gedreht werden, was aber nicht geschieht. Zwar bewegt sich die Spielfigur in die neue Richtung, das Bild wird aber nicht gedreht, sondern starrt starr an den unteren Bildschirmrand. Ich sitze jetzt Stunden daran und finde wirklich nicht heraus, was ich da falsch überlegt habe :/

Frage 2
Was würde diese Stelle mit dem "if not hasattr..." und dem "continue" bewirken, wenn sie nicht auskommentiert wäre? Ich weiss aus der Dokumentation, dass die Bedingung ist, dass das Objekt "event" das Attribut "key" nicht haben darf, damit das "continue" ausgelöst wird. Nur verstehe ich die Verwendung im konkreten Zusammenhang nicht genau. Unter welchen Bedingungen trifft das hier zu (oder eben nicht...)? Wohin zielt das "continue"? Sprich: Wo fährt der Interpreter weiter, wenn das "if not" eintrifft? Und wo fährt er weiter, wenn es nicht eintrifft? Da wäre ich dankbar für eine Klärung :)

Problem 3
Im folgenden Block müssten eigentlich die Tastenbefehle an die PlayerClass.update-Funktion übergeben werden. Das Problem ist nun, dass ich die Bewegen-Taste (NUMPAD 8 oder 2) loslassen muss, damit eine Richtungstaste verwendet werden kann. Halte ich die Bewegungstaste gedrückt, geht die Figur stur geradeaus, auch wenn ich eine Richtungstaste (NUMPAD 4 oder 6) drücke. Lasse ich die Bewegentaste los, kann ich die Drehung vollziehen (Bild dreht eben nicht mit, wie bei Problem 1 beschrieben) und danach mit der Bewegen-Taste in die entsprechende Richtung gehen. Warum funktioniert das nicht, während ich die jeweilige Bewegentaste gedrückt halte?

Bitte geizt nicht mit Erklärungen und Verweisen ;) Ich möchte gerne möglichst viel aus meinen Fehlern lernen können :)

Live long and prosper
Kensen
BlackJack

@Kensen468: ``global`` ist ein Schlüsselwort, welches Du am besten gleich wieder vergisst. Zumal man es in *dem* Quelltext auch weglassen kann, ohne dass sich auch nur irgend etwas ändert, denn auf Modulebene hat es absolut gar keinen Effekt.

Ad Problem 1: Du erstellst ein rotiertes Bild von `bild` und bindest es an den Namen `rotatedimg` und machst dann mit dem rotierten Bild *gar nichts*. Sondern blittest das unrotierte Bild `bild` auf den `screen`. Ist das nicht offensichtlich warum das unrotierte angezeigt wird wenn Du das unrotierte anzeigst‽

Ad Frage 2: Hast Du den Abschnitt break and continue Statements, and else Clauses on Loops im Tutorial in der Python-Dokumentation gelesen? Was genau ist an der Beschreibung und dem Beispiel unklar? Ansonsten gibt es noch einen kurzen Abschnitt zu der Anweisung in der Sprachreferenz: The continue statement.

Grundsätzlich würde ich versuchen ``continue`` zu vermeiden, weil das ein verschachtelter Sprung ist, der den Ablauf eines Programms schwerer nachvollziehbar macht und auch Änderungen am Code erschweren kann.

Ad Problem 3: Das liegt an Deiner Auswertung von `kup`, `kdown`, `kleft`, und `kright`. Das hast Du so geschrieben, dass wenn *eines* dieser Flags wahr ist, kein anderes mehr ausgewertet wird.

Sonstige Anmerkungen: Der ganze Code auf Modulebene ausser der Definition von Konstanten, Funktionen, und Klassen sollte in einer Funktion verschwinden. Das dieser Code momentan mit Funktions und Klassendefinitionen vermischt ist, macht das Programm nicht gerade übersichtlicher. Dann wirst Du vielleicht wieder in Versuchung kommen ``global`` an Stellen zu benutzen, wo es tatsächlich einen Effekt hätte: Lass es! ``global`` ist keine Lösung für ein sauberes, nachvollziehbares Programm. Werte ausser Konstanten, die innerhalb einer Funktion benötigt werden, sollten diese als Argumente betreten. Und statt einen globalen Namen neu zu binden sollte man Rückgabewerte verwenden, oder wenn mehrere Funktionen sich Zustand über Aufrufe hinweg teilen, dann kapselt man das sinnvoll in einer Klasse.

Argh: ``os.path.join("data\\"+folder, name)``. Die `os.path.join()`-Funktion ist doch gerade dazu da um genau solche ``+``-Operationen zu vermeiden. Das läuft trotz `os.path.join()` so nur unter Windows. Der Aufruf muss so aussehen: ``os.path.join('data', folder, name)``, damit da eine Pfadangabe heraus kommt, die beispielsweise auch unter Linux oder MacOS verwendbar ist.

Nackte ``except:``\s ohne Angabe von konkreten Ausnahmen, die man dort erwartet sollte man vermeiden. Damit behandelt man *alle* Ausnahmen, inklusive `NameError` oder `AttributeError` wenn man sich innerhalb des ``try``-Blocks irgendwo bei einem Namen oder Attribut vertippt hat, und alle Ausnahmen mit denen man nicht gerechnet hat. Und die findet man dann nicht, weil das Programm sie nicht mehr samt Traceback ausgibt.

`SystemExit` löst man nicht mit ``raise`` aus, dafür gibt es die `sys.exit()`-Funktion. Mit der Syntax, die Du dafür verwendest löst man heutzutage gar keine Ausnahme mehr aus, die ist „deprecated”. Man erzeugt ganz normal ein Exemplar von der Ausnahme und gibt die Argumente nicht zusätzlich durch Kommata getrennt nach der Ausnahme an.

Auch `sys.exit()` verwendet man nicht in einer Funktion die etwas laden soll. Allgemein verwendbare Funktionen dürfen nicht einfach so das ganze Programm abbrechen. Der Aufrufer möchte vielleicht darauf reagieren können, wenn etwas nicht funktioniert hat, zum Beispiel ein alternatives Verzeichnis zum Bilder laden ausprobieren, ein Platzhalterbild verwenden, oder dem Benutzer eine Nachricht anzeigen, was genau nicht geladen werden konnte, und vielleicht wo man das herbekommen und hinkopieren muss, oder etwas in der Art. Das geht alles nicht wenn die Funktion einfach eigenmächtig das Programm abbricht.

Ich würde die „Fehlerbehandlung” mit den Programmabbrüchen einfach weg lassen. Denn wenn dort eine Ausnahme auftritt, dann wird das Programm ja sowieso abgebrochen. Mit dem Unterschied, dass da ein Traceback mit ausgegeben wird, der zum finden der Ursache wichtig ist.

Für allgemein verwendbare Ladefunktionen würde man in der Funktion auch nicht den Ordner 'data/' hart kodieren. Wenn man etwas aus einem anderen Ordner laden möchte, müsste man sich eine weitere Funktion schreiben, die letztendlich das selbe noch mal macht.

Kommentare sollten einen Mehrwert zum Quelltext liefern. Kommentare wie ``#function to load the soundfiles`` vor einer Funktion `soundload()` tun das nicht. Sachen die offensichtlich noch einmal im Quelltext stehen muss man nicht Kommentieren. Kommentare sollten nicht sagen *was* gemacht wird, denn das steht da ja als hoffentlich verständlicher Quelltext, sondern *warum* etwas so gemacht wird, wie es im Quelltext steht. Für die Fälle wo das nicht einfach zu erkennen ist.

Die beiden Bilderladefunktionen unterscheiden sich so minimal, dass man die in eine zusammenfassen sollte. Mit einem Argument (`True`/`False`) für Alpha-Umwandlung.

Namen sollten klar und verständlich sein. Das schliesst Abkürzungen aus, die nicht allgemein bekannt sind. `plstrat`, `imgmod`, und `plgrp` sind demnach schlechte Namen.

Der Namenszusatz 'Class' für Klassennamen ist überflüssig. Das es sich um eine Klasse handelt, sieht man schon an der Schreibweise des Namens. Damit bietet 'Class' keine zusätzliche Information.

In der `update()`-Methode wird `speed` unnötigerweise zu `tempo` „umbenannt”.

Die `Sprite.update()`-Methode darf das Sprite nicht selbst blitten. Das verträgt sich nicht mit der `RenderPlain.draw()`-Methode deren Aufgabe das blitten ist. `Sprite.update()` ist nur zur Aktualisierung des Zustands und entsprechendem setzen der `image`- und `rect`-Attribute zuständig, die `RenderPlain.draw()` dann zum blitten verwendet.

Dass Du überall diese magische Zahl `gamemode` verwendest um abhängig davon unterschiedliche Dinge zu tun, macht das Programm komplexer. Da würde man alles verhalten welches zu einem Spielmodus gehört eher in Objekte oder Paramtersätze verpacken um diese über das gesamte Programm verteilten Entscheidungen anhand einer letztendlich globalen Information zu vermeiden. Dann muss man beim verändern, hinzufügen, oder entfernen eines Spielmodus nicht das gesamte Programm nach nötigen Veränderungen absuchen, sondern hätte das alles an einer zentralen Stelle.
Kensen468
User
Beiträge: 3
Registriert: Freitag 12. April 2013, 22:26

Danke für deine ausführliche Antwort! Daraus habe ich viel lernen können :)
Und sorry für die späte Antwort, war einiges los dieser Tage hier...

Kurz schnell noch dieser Hinweis: Die Tutorials, die ich durchgearbeitet habe, stammen teilweise noch von 2005 oder sogar noch früher. Ich habe schon eine ganze Menge an deprecated Code rausgeschmissen, der hier:

Code: Alles auswählen

SystemExit` löst man nicht mit ``raise`` aus
ist dank deines Hinweises mittlerweile auch geflogen :)

Danke auch für den Hinweis zu "global", das habe ich oft verwendet, es kam auch in vielen Tutorials vor. Mittlerweile baue ich meine Programme so um, dass das auch gestrichen werden kann.

Zu Frage 1: Hab grad in die Tischkante gebissen... Wahrscheinlich hatte ich zuviel Stress dieser Tage oder was weiss ich... aber der Fehler ist ja offensichtlich... Danke für den Hinweis, ich hab's wirklich nicht gesehen gehabt, bis du's mir gesagt hast -.-

zur 2: du triffst den Nagel auf den Kopf ;) Ich habe das ganze mit "continue" in einem Tutorial gesehen, erstmals übernommen, ersetzt, aber nie begriffen, darum habe ich es hier auch erwähnt, in der Hoffnung, dass jemand von euch da den Durchblick hat. Im Programm ist's mittlerweile ganz verschwunden.

Die verschiedenen Exit-Befehle sind auch rausgeflogen, ausser jenen, wo der Spieler das Programm beenden will (da gehört's ja hin).

Ich überarbeite mal den ganzen Code nochmals am Wochenende deinen Hinweisen gemäss.

Vielen Dank nochmals! Du hast mir viel weitergeholfen, auch dabei, python besser zu verstehen :)

Live long and prosper
Kensen
Antworten