Skip to content
Snippets Groups Projects
Commit 90855fa3 authored by Robert Seimetz's avatar Robert Seimetz
Browse files

Merge branch 'master' of edugit.org:Teckids/hacknfun/material/spieleprogrammieren

parents f65c6adc b8dca4c1
No related branches found
No related tags found
No related merge requests found
......@@ -3,38 +3,38 @@
## Das Grundgerüst
Zuerst brauchen wir einen Startpunkt. Heißt wir brauchen einen Bildschirm, eine Welt und ein paar Charaktere und Objekte. So ein Standard-Grundgerüst haben wir im main_template vorgegeben, dieses könnt ihr benutzen. Das funktioniert so:
1. Ein *screen* ist der Rahmen des Spiels und hat eine definierte Höhe und Breite (Wir benutzen englische Begriffe daher -> *width* und *height*).
2. Als 2D-Welt wird eine *Tile-Map* geladen. Das ist eine Karte, die wir mit Hilfe des Programmes *Tiled* erstellt haben.
3. Um Objekte (*Sprites*) zusammenzufassen, werden *Sprite-Gruppen* erstellt. So kann z.B. allgemein definiert werden, was bei einer Kollision zwischen Sprites der Gruppe *Player* und *Enemy* passiert, und muss dies nicht für jeden einzelnen Sprite tun.
4. In der settings.py kann eingestellt werden, ob es Scrolling geben soll (kein Autoscrolling), welche Schriftart für Texte benutzt werden sollen und ob es Gravitation gibt (bei Rennspielen braucht man beispielsweise keine).
1. Ein _screen_ ist der Rahmen des Spiels und hat eine definierte Höhe und Breite (Wir benutzen englische Begriffe daher -> _width_ und _height_).
2. Als 2D-Welt wird eine _Tile-Map_ geladen. Das ist eine Karte, die wir mit Hilfe des Programmes _Tiled_ erstellt haben.
3. Um Objekte (_Sprites_) zusammenzufassen, werden _Sprite-Gruppen_ erstellt. So kann z.B. allgemein definiert werden, was bei einer Kollision zwischen Sprites der Gruppe _Player_ und _Enemy_ passiert, und muss dies nicht für jeden einzelnen Sprite tun.
4. In der settings.py kann eingestellt werden, ob es Scrolling geben soll (kein Autoscrolling), welche Schriftart für Texte benutzt werden sollen und ob es Gravitation gibt (bei Rennspielen braucht man beispielsweise keine).
## Sprites
### Python-Wissen: Klassen
In Python, und vielen anderen Programmiersprachen, gibt es Klassen. Klassen beschreiben, welche Eigenschaften und Funktionen Objekte dieser Klasse haben. Klassen sind quasi wie ein Bauplan aus dem man dann mehrmals dasselbe Objekt herstellen kann. Von diesem Bauplan kann man mit Hilfe von *Parametern* abweichen, zum Beispiel kann man jedes mal ein anderes Bild einfügen. Gibt es mehrere Klassen, die viele gemeinsame Eigenschaften haben, ist es sinnvoll, eine übergeordnete Klasse (auch *Super-Klasse*) mit den allgemeinenen Eigenschaften und Kinderklassen mit den speziellen Eigenschaften zu erstellen.
In Python, und vielen anderen Programmiersprachen, gibt es Klassen. Klassen beschreiben, welche Eigenschaften und Funktionen Objekte dieser Klasse haben. Klassen sind quasi wie ein Bauplan aus dem man dann mehrmals dasselbe Objekt herstellen kann. Von diesem Bauplan kann man mit Hilfe von _Parametern_ abweichen, zum Beispiel kann man jedes mal ein anderes Bild einfügen. Gibt es mehrere Klassen, die viele gemeinsame Eigenschaften haben, ist es sinnvoll, eine übergeordnete Klasse (auch _Super-Klasse_) mit den allgemeinenen Eigenschaften und Kinderklassen mit den speziellen Eigenschaften zu erstellen.
Für uns heißt das: Die Klasse *_Character* hat die Parameter *location* (Spawnkoordinaten), *scale* (Größe) und *image* (gewünschtes Bild). Die *Player* im Spiel, als auch die *Enemies*, sind solche *_Character*, daher haben sie alles auch diese Attribute. Es gibt aber auch Unterschiede, z.B. lassen sich *Player* steuern, *Enemies* nicht. Hier kommt das Konzept der *Vererbung* ins Spiel. Die gemeinsamen Eigenschaften sind in der Klasse *_Character* definiert. Darauf basierend gibt es die Klassen *LinearPlayer* und *AnimatedCoin*, welche beide von der Klasse *_Character* erben. Das bedeutet, dass sie deren Eigenschaften ebenfalls haben, und zusätzlich neue Eigenschaften und Funktionen definieren können. Am Ende übergibt man also der Klasse *Player* die vier *Parameter*, die die *_Character*-Klasse braucht und ein weiteres.
Für uns heißt das: Die Klasse _\_Character_ hat die Parameter _location_ (Spawnkoordinaten), _scale_ (Größe) und _image_ (gewünschtes Bild). Die _Player_ im Spiel, als auch die _Enemies_, sind solche _\_Character_, daher haben sie alles auch diese Attribute. Es gibt aber auch Unterschiede, z.B. lassen sich _Player_ steuern, _Enemies_ nicht. Hier kommt das Konzept der _Vererbung_ ins Spiel. Die gemeinsamen Eigenschaften sind in der Klasse _\_Character_ definiert. Darauf basierend gibt es die Klassen _LinearPlayer_ und _AnimatedCoin_, welche beide von der Klasse _\_Character_ erben. Das bedeutet, dass sie deren Eigenschaften ebenfalls haben, und zusätzlich neue Eigenschaften und Funktionen definieren können. Am Ende übergibt man also der Klasse _Player_ die vier _Parameter_, die die _\_Character_-Klasse braucht und ein weiteres.
### Wie fügt ich selber Sprites ein?
Um selber einen Sprite hinzuzufügen, müssen wir erstmal eine passende Klasse, also einen passenden Bauplan finden. Folgende gibt es schon vorgefertigt:
```mermaid
graph TD;
A(Was will ich erstellen?)-->B(Spieler);
A(Was will ich erstellen?)-->C(Gegner);
A(Was will ich erstellen?)-->D(Objekte);
B(Spieler)-->E(Der Spieler soll sich in eine Richtung bewegen);
E(Der Spieler soll sich in eine Richtung bewegen)-->G(LinearPlayer);
G(LinearPlayer)-->T(Geschosse dafür: Projectiles);
B(Spieler)-->F(Der Spieler soll sich frei bewegen);
F(Der Spieler soll sich frei bewegen)-->S(AnglePlayer);
S(AnglePlayer)-->U(Geschosse dafür: AngleProjectiles);
C(Gegner)-->H(Der Gegner soll sich in eine Richtung bewegen);
H(Der Gegner soll sich in eine Richtung bewegen)-->I(LinearEnemy);
I(LinearEnemy)-->T(Geschosse dafür: Projectiles);
......@@ -44,7 +44,7 @@ graph TD;
C(Gegner)-->L(Der Gegner soll dem Spieler folgen);
L(Der Gegner soll dem Spieler folgen)-->M(FollowingEnemy);
M(FollowingEnemy)-->T(Geschosse dafür: Projectiles);
D(Objekte)-->N(Ist das Objekt animiert?);
N(Ist das Objekt animiert?)-->O(Nein);
O(Nein)-->P(_Object);
......@@ -54,13 +54,13 @@ graph TD;
Jedes Objekt braucht unterschiedliche Parameter, wie oben beschrieben. Hier sind sie jeweils aufgelistet:
* Beide Spieler
- Beide Spieler
1. Spawnkoordinaten (x, y)
2. Gewünschte Größe in pixel (Breite, Höhe)
3. Dateiname des Bildes (mit Dateiendung)
4. Genutzte Karte (standardmäßig tilemap)
5. Spritegroup der Wände (standardmäßig platforms)
* Gegner
- Gegner
1. Spawnkoordinaten (x, y)
2. Gewünschte Größe in pixel (Breite, Höhe)
3. Dateiname des Bildes (mit Dateiendung)
......@@ -68,20 +68,23 @@ Jedes Objekt braucht unterschiedliche Parameter, wie oben beschrieben. Hier sind
5. Spritegroup der Wände (standardmäßig platforms)
6. Die Geschwindigkeit des Gegners
7. Entweder:
* Die Richtung der Bewegung ([speedx, speedy]) (Bei LinearEnemy)
* Den Spieler der verfolgt werden soll (meist player) (Bei FollowingEnemy)
* Wie oft die Richtung geändert werden soll (in Sekunden) (Bei RandomEnemy)
* Objekte
1. Spawnkoordinaten (x, y)
2. Größe des Bildes (Breite, Höhe)
3. Dateiname des Bildes (mit Dateiendung)
4. Bei Animationen: Größe des einzelnen Sprites
* Projektile
1. Spawnkoordinaten (bei schießender Figur: rect.topleft/topright der Figur)
2. Variable in der Bewegung gespeichert ist (bei schießender Figur: z.B. player.move oder enemy.move) oder Winkel des Pfeils (bei AngleProjectile. Bei schießendem Spieler player.angle)
3. Größe des Bildes [x, y]
4. Dateiname des Bildes (mit Dateiendung)
5. Geschwindigkeit der Pfeile
- Die Richtung der Bewegung ([speedx, speedy]) (Bei LinearEnemy)
- Den Spieler der verfolgt werden soll (meist player) (Bei FollowingEnemy)
- Wie oft die Richtung geändert werden soll (in Sekunden) (Bei RandomEnemy)
- Objekte
1. Spawnkoordinaten (x, y)
2. Größe des Bildes (Breite, Höhe)
3. Dateiname des Bildes (mit Dateiendung)
4. Bei Animationen: Größe des einzelnen Sprites
- Projektile
1. Spawnkoordinaten (bei schießender Figur: rect.topleft/topright der Figur)
2. Variable in der Bewegung gespeichert ist (bei schießender Figur: z.B. player.move oder enemy.move) oder Winkel des Pfeils (bei AngleProjectile. Bei schießendem Spieler player.angle)
3. Größe des Bildes [x, y]
4. Dateiname des Bildes (mit Dateiendung)
5. Geschwindigkeit der Pfeile
Wenn man diese Parameter übergibt sieht das etwa so aus:
......@@ -96,7 +99,7 @@ platforms -> Die Spritegruppe der Plattformen und Wände
"""
```
Man kann aber auch etwas tricksen. In Tiled kann man nämlich auch einfach ein Objekt auf der Objektebene hinzufügen, das entsprechend bennenen und dann die Koordinaten und Größe des Objekts im Code verwenden.
Man kann aber auch etwas tricksen. In Tiled kann man nämlich auch einfach ein Objekt auf der Objektebene hinzufügen, das entsprechend bennenen und dann die Koordinaten und Größe des Objekts im Code verwenden.
Dazu muss man folgendes machen:
......@@ -110,7 +113,7 @@ class Coin(_Object):
2. Erstelle Objekte in Tiled mit gleichen Namen
3. Erstelle eine Spritegruppe und nutze eine *for-Schleife* um alle Objekte mit dem Namen der Gruppe hinzuzufügen. Dann kannst du sie zeichnen
3. Erstelle eine Spritegruppe und nutze eine _for-Schleife_ um alle Objekte mit dem Namen der Gruppe hinzuzufügen. Dann kannst du sie zeichnen
```python
coins = pygame.sprite.RenderClear() # Erstellen der Spritegruppe
......@@ -127,7 +130,7 @@ def update():
## Wie zeichne ich auf den Bildschirm?
Ähnlich zu einem Film, der aus vielen Bildern (*Frames*) besteht, funktioniert auch PyGame. Erst wird eine einfarbige Fläche erstellt, auf die dann die Sprites vorgezeichnet werden und wenn alles fertig wird, wird es auf einmal gemalt. Dabei gilt:
Ähnlich zu einem Film, der aus vielen Bildern (_Frames_) besteht, funktioniert auch PyGame. Erst wird eine einfarbige Fläche erstellt, auf die dann die Sprites vorgezeichnet werden und wenn alles fertig wird, wird es auf einmal gemalt. Dabei gilt:
Der Code wird von oben nach unten gelesen. Daher muss sie immer diesem Aufbau folgen:
......@@ -135,22 +138,22 @@ Der Code wird von oben nach unten gelesen. Daher muss sie immer diesem Aufbau fo
def update():
screen.fill((0,0,0)) # Das füllt den Bildschirm mit einer Farbe. Daher bitte immer zuerst.
screen.blit(tilemap_image, (0,0)) # Auf den schwarzen Bildschirm wird die Karte gemalt.
# Hier werden Charaktere und Objekte "vorgezeichnet". Da ist die Reihenfolge relativ egal.
coins.draw(objects_surface)
enemies.draw(characters_surface)
enemies.draw(characters_surface)
screen.blit(player.image, (player.rect.left, player.rect.top))
# Zuletzt wird alles gemalt
pygame.display.flip()
```
Das findet für jeden Frame erneut statt und steht in der *update-Funktion*, die immer wieder aufgerufen wird.
Damit in dem Spiel etwas passiert und nicht immer das gleiche Bild entsteht, muss zwischen den Aufrufen der *update-Funktion* noch etwas geschehen: Spiel-Logik.
Das findet für jeden Frame erneut statt und steht in der _update-Funktion_, die immer wieder aufgerufen wird.
Damit in dem Spiel etwas passiert und nicht immer das gleiche Bild entsteht, muss zwischen den Aufrufen der _update-Funktion_ noch etwas geschehen: Spiel-Logik.
## Die Spiel-Logik
Ein interaktives Spiel braucht eine Steuerung. Vor jedem *update*-Aufruf wird geprüft, ob gerade eine Pfeiltaste gedrückt ist. Wenn das so ist, wird die Position des Players verändert, sodass die Figur im nächsten Frame eine andere Position hat.
Ein interaktives Spiel braucht eine Steuerung. Vor jedem _update_-Aufruf wird geprüft, ob gerade eine Pfeiltaste gedrückt ist. Wenn das so ist, wird die Position des Players verändert, sodass die Figur im nächsten Frame eine andere Position hat.
```python
for event in pygame.event.get(): #Jedes Event wird in jedem Frame geprüft
......@@ -158,7 +161,7 @@ for event in pygame.event.get(): #Jedes Event wird in jedem Frame geprüft
exit(0)
if event.type == pygame.KEYDOWN: # Hier wird geprüft ob irgendeine Taste gedrückt wurde.
if event.key == pygame.K_LSHIFT: # Danach schaut man, *welche* Taste das ist.
player.running = True
player.running = True
if event.type == pygame.KEYUP: # Hier wird geprüft ob irgendeine Taste losgelassen wurde.
if event.key == pygame.K_LSHIFT: # Danach schaut man wieder, welche Taste genau.
player.running = False
......@@ -171,54 +174,53 @@ if (pygame.sprite.spritecollide(player, coins, True)):
player.score +=100
```
### Wie ändere ich die vorgegebenen Templates?
Ein großer Teil der Arbeit besteht darin, das was es schon gibt, auf die eigenen Bedürfnisse anzupassen. Sagen wir mal, wir wollen, dass Spieler nicht nur mit WASD steuerbar ist sondern auch mit den Pfeiltasten. Dann tun wir folgendes:
1. Wir erstellen eine neue *Klasse* (kein Objekt!) mit der Vorlage, die wir wollen (hier LinearPlayer)
1. Wir erstellen eine neue _Klasse_ (kein Objekt!) mit der Vorlage, die wir wollen (hier LinearPlayer)
```python
class Player(LinearPlayer): # Wir erstellen einen neue Klasse, die von *LinearPlayer* erbt
def __init__(self, location, size, image, tilemap, walls):
def __init__(self, location, size, image, tilemap, walls):
super().__init__(self, location, size, image, tilemap, walls) # Diese Klasse hat exakt dieselben Eigenschaften wie die Vorlage (=Vererbung). Mit super() erhält man die Super-Klasse, also LinearPlayer
```
Nun können wir die Klasse verändern oder neue Dinge hinzufügen.
Nun können wir die Klasse verändern oder neue Dinge hinzufügen.
2. Bestehendes Abändern
```python
class Player(LinearPlayer): # Wir erstellen einen neue Klasse, die von *LinearPlayer* erbt
def __init__(self, location, size, image, tilemap, walls):
def __init__(self, location, size, image, tilemap, walls):
super().__init__(self, location, size, image, tilemap, walls) # Diese Klasse hat exakt dieselben Eigenschaften wie die Vorlage (=Vererbung)
self.keys = { # self.keys ist schon in der übergeordneten Klasse definiert. Das überschreiben wir hier.
"up": pygame.K_UP,
"down": pygame.K_DOWN,
"left": pygame.K_LEFT,
"right": pygame.K_RIGHT, #Hier standen vorher die Kasten W, A, S und D. Eine Übersicht über alle Tasten ist hier zu finden: https://www.pygame.org/docs/ref/event.html
"jump": pygame.K_SPACE
"right": pygame.K_RIGHT, #Hier standen vorher die Kasten W, A, S und D. Eine Übersicht über alle Tasten ist hier zu finden: https://www.pygame.org/docs/ref/event.html
"jump": pygame.K_SPACE
}
```
3. Neues hinzufügen
```python
class Player(LinearPlayer): # Wir erstellen einen neue Klasse, die von *LinearPlayer* erbt
def __init__(self, location, size, image, tilemap, walls):
def __init__(self, location, size, image, tilemap, walls):
super().__init__(self, location, size, image, tilemap, walls) # Diese Klasse hat exakt dieselben Eigenschaften wie die Vorlage (=Vererbung)
self.keys = { # self.keys ist schon in der übergeordneten Klasse definiert. Das überschreiben wir hier.
"up": pygame.K_UP,
"down": pygame.K_DOWN,
"left": pygame.K_LEFT,
"right": pygame.K_RIGHT, #Hier standen vorher die Kasten W, A, S und D. Eine Übersicht über alle Tasten ist hier zu finden: https://www.pygame.org/docs/ref/event.html
"right": pygame.K_RIGHT, #Hier standen vorher die Kasten W, A, S und D. Eine Übersicht über alle Tasten ist hier zu finden: https://www.pygame.org/docs/ref/event.html
"jump": pygame.K_SPACE,
"say_hi": pygame.K_9
}
def say_hi(self, sound): # Ab hier beginnt eine neue Funktion
for event in events:
if event.type == pygame.KEYDOWN:
......@@ -236,20 +238,20 @@ Es gibt aber nicht nur ganze Klassen sondern auch einzelne Funktionen. Also anst
## Timer
Manchmal braucht man einen Timer, zum Beispiel um einen Gegner spawnen zu lassen oder um die Zeit im Spiel anzuzeigen.
Die Zeit in Sekunden wird mithilfe der Funktion `time()` ausgegeben.
Manchmal braucht man einen Timer, zum Beispiel um einen Gegner spawnen zu lassen oder um die Zeit im Spiel anzuzeigen.
Die Zeit in Sekunden wird mithilfe der Funktion `time()` ausgegeben.
## Musik & Sound
Zwischen Musik und Sound gibt es ein paar feine Unterschiede.
| Musik | Sound |
|---------------------------------------|------------------------------------------------|
| ------------------------------------- | ---------------------------------------------- |
| Kann nur eine Datei abgespielt werden | Können mehrere gleichzeitig existieren |
| Wiederholt sich | Wiederholt sich (eigentlich) nicht |
| Startet sofort | Muss erst definiert werden und dann abgespielt |
| Startet sofort | Muss erst definiert werden und dann abgespielt |
Musik wird mit der Funktion *add_music()* gestartet. Diese benötigt die Parameter *filename* und wie oft es sich wiederholen soll. -1 ist eine unendliche Wiederholung. Die Datei muss im Verzeichnis ./sound/ sein.
Musik wird mit der Funktion _add_music()_ gestartet. Diese benötigt die Parameter _filename_ und wie oft es sich wiederholen soll. -1 ist eine unendliche Wiederholung. Die Datei muss im Verzeichnis ./sound/ sein.
Sound muss erst definiert werden um ihn dann zu einem späteren Zeitpunkt abzuspielen. Ein Sound kann auch öfter benutzt werden.
......@@ -266,17 +268,16 @@ sound.play()
sound.play()
```
## Text
Oft muss man Text auf den Bildschirm spielen. Zum Beispiel um einen Score auf dem Bildschirm zu zeigen.
Oft muss man Text auf den Bildschirm spielen. Zum Beispiel um einen Score auf dem Bildschirm zu zeigen.
```python
def update():
screen.blit(add_text(text, size, color, bold), (x, y))
```
Was passiert hier genau? Zuerst wird der Text selbst erstellt. Dazu braucht die `add_text`-Funktion den "Text", die Schriftgröße, die Farbe in RGB (R, G, B) und die Angabe, ob der Text fett sein soll. Danach wird gesagt, *wo* der Text erscheinen soll.
Was passiert hier genau? Zuerst wird der Text selbst erstellt. Dazu braucht die `add_text`-Funktion den "Text", die Schriftgröße, die Farbe in RGB (R, G, B) und die Angabe, ob der Text fett sein soll. Danach wird gesagt, _wo_ der Text erscheinen soll.
```python
def update():
......@@ -286,7 +287,7 @@ def update():
Man kann auch den Text mit Variablen kombinieren. Sollen die Leben des Spielers (Hier gespeichert in player.lives) angezeigt werden, würde man folgenden Text nutzen:
```python
(f"Lives: {player.lives}")
f"Lives: {player.lives}"
```
Die Schriftgröße kann in der settings.py editiert werden.
......@@ -309,4 +310,4 @@ rotate_image("Dateiname des Bildes", (Zahl um wie viel Grad das Bild gedreht wer
## Neustarten des Programmes
Code immer wieder zu stoppen und zu starten kann nervig sein. Daher kann die `retry()` ein Programm einfacher neustarten. Dabei braucht sie immer `__file__` als *Parameter*. Das hat einen ganz einfachen Grund: Die Funktion ruft einen neuen Prozess auf, der die ganze Datei neustartet und daher den genauen Dateipfad braucht. Deswegen ist es auch ganz wichtig, dass die Funktion nicht dazu benutzt wird, um von einem Chekpoint neuzustarten, das wird nicht funktionieren. Nur wenn man das ganze Programm neustarten will, ist das eine Option.
Code immer wieder zu stoppen und zu starten kann nervig sein. Daher kann die `retry()` ein Programm einfacher neustarten. Dabei braucht sie immer `__file__` als _Parameter_. Das hat einen ganz einfachen Grund: Die Funktion ruft einen neuen Prozess auf, der die ganze Datei neustartet und daher den genauen Dateipfad braucht. Deswegen ist es auch ganz wichtig, dass die Funktion nicht dazu benutzt wird, um von einem Chekpoint neuzustarten, das wird nicht funktionieren. Nur wenn man das ganze Programm neustarten will, ist das eine Option.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment