Unterschiede
Hier werden die Unterschiede zwischen zwei Versionen der Seite angezeigt.
Beide Seiten, vorherige Überarbeitung Vorherige Überarbeitung Nächste Überarbeitung | Vorherige Überarbeitung | ||
talit:asteroids_game [2024-04-29 11:21] – [Version 1: Challenge] sca | talit:asteroids_game [2024-06-03 11:47] (aktuell) – [Version 5] sca | ||
---|---|---|---|
Zeile 32: | Zeile 32: | ||
Nach Challenge: | Nach Challenge: | ||
- | * Erstelle **neues Repo** auf GitHub mit Name `asteroids`. | + | * Erstelle **neues Repo** auf GitHub mit Name `asteroids_game`. |
* Gebe der LP (anschae) frei | * Gebe der LP (anschae) frei | ||
* Füge dein Game mit Name `asteroids_v01.py` hinzu: add / commit / push | * Füge dein Game mit Name `asteroids_v01.py` hinzu: add / commit / push | ||
Zeile 89: | Zeile 89: | ||
- | |||
- | </ | ||
===== Version 2 ===== | ===== Version 2 ===== | ||
Zeile 100: | Zeile 98: | ||
* **Display: | * **Display: | ||
* `display.set_pixel(3, | * `display.set_pixel(3, | ||
- | * Arbeite mit **Koordinaten**, | + | * Arbeite mit **Koordinaten**, |
* **Bewegung Player:** | * **Bewegung Player:** | ||
* `button_a.get_presses()` anstelle `button_a.is_pressed()` | * `button_a.get_presses()` anstelle `button_a.is_pressed()` | ||
Zeile 106: | Zeile 104: | ||
* Vermeide lange Delays wie `sleep(1000)`, | * Vermeide lange Delays wie `sleep(1000)`, | ||
* Arbeite stattdessen mit `time.ticks_ms()` (siehe unten). | * Arbeite stattdessen mit `time.ticks_ms()` (siehe unten). | ||
- | * Speichere den Zeitpunkt zu dem der Asteroid zuletzt bewegt wurde in einer Variablen. | + | * Speichere den Zeitpunkt, zu dem der Asteroid |
- | * Überprüfe in jedem Durchlauf, ob seit der letzten Bewegung genügend Zeit (z.B. 500ms) vergangen | + | * Überprüfe in jedem Durchlauf, ob seit der letzten Bewegung genügend Zeit (z.B. 500ms) vergangen |
* kurzes Delay wie `sleep(20)` verhindert nervöses Flackern | * kurzes Delay wie `sleep(20)` verhindert nervöses Flackern | ||
* **Programmierstil: | * **Programmierstil: | ||
- | * Verwende sinnvolle, aussagekräftige Variablennamen, | + | * Verwende sinnvolle, aussagekräftige Variablennamen, |
- | * Halte dich an Python-Konventionen: | + | * Halte dich an Python-Konventionen: |
<code python> | <code python> | ||
Zeile 117: | Zeile 115: | ||
... | ... | ||
time.ticks_ms() # gibt an, seit wie vielen Millisekunden Micro:bit schon läuft. | time.ticks_ms() # gibt an, seit wie vielen Millisekunden Micro:bit schon läuft. | ||
+ | # Alternative: | ||
+ | running_time() | ||
... | ... | ||
</ | </ | ||
Zeile 122: | Zeile 122: | ||
++++ | ++++ | ||
- | </nodisp> | + | |
+ | ===== Version 3 ===== | ||
+ | |||
+ | **Ziel:** Code sauber **strukturieren**. | ||
+ | |||
+ | Mache Kopie von der letzten Version des Games und speichere es unter dem Namen `asteroids_v03.py`. Verbessere das Game nun dieses mithilfe der Tipps unten. Falls du unzufrieden mit deine alten Version bist, kannst du auch mit einem leeren File beginnen und alles neu programmieren. | ||
+ | |||
+ | * Code sauber strukturieren wie in Template unten | ||
+ | * Kommentare hinzufügen: | ||
+ | * Alle Wert, die sich nicht verändern (quasi die Game-Settings) werden in KONSTANTEN am Anfang des Spiels gespeichert. | ||
+ | |||
+ | |||
+ | ++++ Template| | ||
+ | <code python asteroids_v03.py> | ||
+ | from microbit import * | ||
+ | |||
+ | # CONSTANT | ||
+ | """ | ||
+ | |||
+ | # VARIABLES | ||
+ | """ | ||
+ | |||
+ | while True: | ||
+ | # get current time | ||
+ | |||
+ | # update asteroid position | ||
+ | |||
+ | # update player (button presses) | ||
+ | |||
+ | # collision check | ||
+ | |||
+ | # update display | ||
+ | """ | ||
+ | |||
+ | # short sleep | ||
+ | </ | ||
+ | ++++ | ||
+ | |||
+ | ===== Version 4 ===== | ||
+ | |||
+ | **Ziel:** Weitere **Features** implementieren. | ||
+ | |||
+ | Mache eine Kopie von der letzten Version v03 und nenne sie `asteroids_v04.py`. WICHTIG: Bespreche deine Lösung von v03 mit der Lehrperson, bevor du weiter arbeitest. | ||
+ | |||
+ | Implementiere nun folgende Features: | ||
+ | |||
+ | * Game wird schneller mit der Zeit. Probiere verschiedene Einstellungen aus, Stichwort: Game-Balancing | ||
+ | * Game Over: | ||
+ | * zeigt z.B. SAD oder ANGRY-Smiley an | ||
+ | * zeigt Score an: definiere dazu eine Art Levels (z.B. für jede weiteren 10s, die man überlebt, steigt Level um 1 | ||
+ | * kann dann Taste drücken, um Game neu zu starten | ||
+ | * Tipp: Mache zuerst einen Plan dazu, wie du das umsetzen kannst. Beginne erst nachher mit Programmieren. | ||
+ | |||
+ | ===== Version 5 ===== | ||
+ | |||
+ | **Ziel:** beliebig viele, unabhängige Asteroiden | ||
+ | |||
+ | Mache eine Kopie von der letzten Version und nenne sie `asteroids_v05.py`. | ||
+ | |||
+ | 1. **Plan:** Das Ziel ist, dass wir **beliebig viele Asteroiden** haben können. Diese werden zufälligerweise gespawned. Es kann also sein, dass wir einmal $0$ und später $7$ Asteroiden haben. Jeder Asteroid ist *unabhängig* und bewegt sich in seinem eigenen Tempo (per Zufall bestimmen, wenn Asteroid erzeugt wird). Daher können sich Asteroiden auch überholen.\\ Bespreche mit KollegIn: Wie kann man das technisch umsetzen? Welche programmiererischen Mittel kann man da verwenden? Vergleicht eure Antworten mit den Lösungen unten.< | ||
+ | ++++Lösung| | ||
+ | Da jeder Asteroid unterschiedliche Werte hat, bietet es sich an, **Dictionaries** zu verwenden: Jeder Asteroid ist ein Dictionary, in dem alle relevanten Werte stehen (welche sind das?). Alle Asteroiden-Dictionaries speichert man dann in einer Liste. Erreicht ein Asteroid den unteren Rand, wird er aus der Liste gekickt. Wird ein neuer Asteroid gespawned, wird er hinzugefügt. | ||
+ | |||
+ | Das **Spawnen** kann man wie folgt umsetzen: Alle $200$ms (oder anderer Wert) wird mit einer *gewissen Wahrscheinlichkeit* (z.B. $10$%) ein Asteroid erzeug. Verwende dazu das random-Modul. | ||
+ | |||
+ | Diese Lösung ist sehr elegant, da alle Informationen, | ||
+ | |||
+ | Verwende auch für den Player ein Dictionary. | ||
+ | |||
+ | Eine gute Alternative zu Dicts wäre die objektorientierte Programmierung (OOP). Verwende aber Dicts, auch wenn du OOP bereits kennst. | ||
+ | ++++ | ||
+ | </ | ||
+ | 1. **Programmieren: | ||
+ | |||
+ | ++++Tipps| | ||
+ | |||
+ | * Tipp: Kommentiere Collision temporär aus, damit du dich auf die Asteroiden konzentrieren kannst. | ||
+ | * Information für Asteroid: $x-$ und $y-$Position, | ||
+ | |||
+ | ++++ | ||
+ | |||
+ | ===== Version Final ===== | ||
+ | |||
+ | **Slides:** {{ : | ||
+ | |||
+ | Nun geht es in die finale Phase! Ziel ist, zuerst ein komplett abstraktes **Modell** des Games zu erstellen. Dieses soll dann genutzt werden, um verschiedene Versionen des Spiels zu erstellen: | ||
+ | |||
+ | 1. Konsolen-App (im Terminal) | ||
+ | 1. Micro-Bit | ||
+ | 1. Desktop-App mit PyQt5 | ||
+ | |||
+ | Lasse (zumindest für den Anfang) allen Schnick-Schnack weg: kein Score, kein Restart, ... einfach nur 1x das Game spielen bis zu einer Collision | ||
+ | |||
+ | In deinem Repo sollst du dann (zusätzlich zu den bisherigen Files) vier neue Files haben: | ||
+ | |||
+ | * `asteroids_game_model.py`: | ||
+ | * `asteroids_game_microbit.py`: | ||
+ | * `asteroids_game_console.py`: | ||
+ | * `asteroids_game_pyqt.py`: | ||
+ | |||
+ | === Teil I: Modell === | ||
+ | |||
+ | 1. Erstelle ein File `asteroids_model.py`. | ||
+ | 1. Dieses Soll die bisherigen Klassen `Asteroid` und `Player` beinhalten. | ||
+ | 1. Erstelle weiter eine Klasse `Game`, die das gesamte Game beinhaltet. Halte dich dabei an die Vorgaben im **Template** unten. | ||
+ | 1. **Entferne alles**, was **nichts mit dem Modell** zu tun hat. | ||
+ | 1. Das ganze Dokument darf also keine `print()`, `display...` usw. enthalten. | ||
+ | 1. Auch sollen die Klassen keine Attribute wie `self.brightness` haben, da diese zur View gehört: Stellt man Asteroiden in einer Desktop-App dar, haben sie vielleicht eine Farbe oder ein Bild anstelle einer brightness. | ||
+ | 1. Achtung: Zeitabfragen funktionieren auf dem Computer anders als auf dem Mircobit. Verwende deshalb die Funktion `get_time_in_ms()` im Template. | ||
+ | |||
+ | ++++Template| | ||
+ | |||
+ | <code python asteroids_model.py> | ||
+ | # ALL IMPORT STATEMENTS | ||
+ | import sys | ||
+ | import ... | ||
+ | |||
+ | def get_time_in_ms(): | ||
+ | """ | ||
+ | Time is handled differently on microbit than on regular computers. | ||
+ | Call this function to get current (system) time. | ||
+ | """ | ||
+ | if sys.platform == ' | ||
+ | try: | ||
+ | return time.ticks_ms() | ||
+ | except Exception as e: | ||
+ | print(" | ||
+ | else: | ||
+ | try: | ||
+ | return time.time()*1000 | ||
+ | except Exception as e: | ||
+ | print(" | ||
+ | |||
+ | class Player: | ||
+ | def __init__(self, | ||
+ | pass | ||
+ | |||
+ | def update(self, | ||
+ | """ | ||
+ | move = 1 -> move to right 1 step | ||
+ | move = -1 -> move to right 1 step | ||
+ | """ | ||
+ | pass | ||
+ | |||
+ | class Asteroid: | ||
+ | def __init__(self, | ||
+ | pass | ||
+ | |||
+ | def update(self): | ||
+ | """ | ||
+ | if enough time has passed, asteroid' | ||
+ | """ | ||
+ | pass | ||
+ | |||
+ | class Game: | ||
+ | def __init__(self, | ||
+ | .... | ||
+ | self.player = Player(...) # create player and attache to Game-class as attribute | ||
+ | self.asteroids = [] # also create empty list for asteroids | ||
+ | .... | ||
+ | |||
+ | def spawn_asteroids(self): | ||
+ | """ | ||
+ | Checks if enough time has passed s.t. new asteroid is allowed to spawn. | ||
+ | If it is, new asteroid spawns with given probability | ||
+ | """ | ||
+ | pass | ||
+ | |||
+ | def update_asteroids(self): | ||
+ | """ | ||
+ | updates position of all asteroids | ||
+ | """ | ||
+ | pass | ||
+ | |||
+ | def player_is_colliding(self): | ||
+ | """ | ||
+ | checks if player is colliding with an asteroid | ||
+ | returns False or True | ||
+ | """ | ||
+ | pass | ||
+ | </ | ||
+ | |||
+ | ++++ | ||
+ | |||
+ | === Teil II: Micro-Bit === | ||
+ | |||
+ | 1. Erstelle im Online-Editor ein neues File (heisst `main.py`). | ||
+ | 1. Erstelle darin (" | ||
+ | 1. Importiere im `main.py`-File deine Game-Klasse (siehe Template unten) ... | ||
+ | 1. ... und implementiere das Spiel. Rufe dazu die Methoden deiner Game-Klasse auf. | ||
+ | 1. Wenn du alles richtig machst, solltest du nur um die 20 Zeilen benötigen. | ||
+ | 1. **Speichere** deinen Microbit Code dann auch in deinem Repo in einem File: `asteroids_game_microbit.py` | ||
+ | |||
+ | ++++Template| | ||
+ | |||
+ | <code python> | ||
+ | from microbit import * | ||
+ | from asteroids_game_model import Game | ||
+ | |||
+ | def show(game): | ||
+ | """ | ||
+ | Takes game object (instance of Game class) and visualizes it on microbit' | ||
+ | """ | ||
+ | pass | ||
+ | |||
+ | g = Game(...) # create Game object | ||
+ | |||
+ | while True: | ||
+ | # call methods of Game object | ||
+ | ... | ||
+ | show(g) | ||
+ | sleep(...) | ||
+ | </ | ||
+ | ++++ | ||
+ | === Teil III: Konsolen-App === | ||
+ | |||
+ | 1. Erstelle ein neues File (im gleichen Ordner) mit Namen `asteroids_game_console.py`. | ||
+ | 1. Importiere in diesem deine Game-Klasse (wie in Mirobit-Version) ... | ||
+ | 1. ... und implementiere das Game | ||
+ | 1. Achtung: Keyevents und damit die Player-Navigation kann in der Konsole problematisch sein (zumindest auf Mac). Es ist deshalb auch i.O., wenn man hier: | ||
+ | 1. den Player nicht anzeigt | ||
+ | 1. Collisions deaktiviert | ||
+ | 1. damit einfach eine Art ' | ||
+ | |||
+ | |||
+ | === Teil IV: Desktop-App === | ||
+ | |||
+ | 1. Erstelle ein neues File (im gleichen Ordner) mit Namen `asteroids_game_pyqt.py`. | ||
+ | 1. Studiere das Template unten, führe es aus und verstehe die wichtigsten Schritte (nicht alle Details) zu verstehen. | ||
+ | 1. Importiere wieder deine Game-Klasse ... | ||
+ | 1. ... und implementiere das Spiel. | ||
+ | |||
+ | ++++PyQt5 Template| | ||
+ | |||
+ | **Wichtig: | ||
+ | |||
+ | <code python> | ||
+ | from asteroids_game_model import Game | ||
+ | import sys | ||
+ | import random | ||
+ | from PyQt5.QtWidgets import QApplication, | ||
+ | from PyQt5.QtGui import QColor | ||
+ | from PyQt5.QtCore import Qt,QTimer | ||
+ | |||
+ | N_COLUMNS = 4 | ||
+ | N_ROWS = 7 | ||
+ | |||
+ | class GridApp(QWidget): | ||
+ | def __init__(self): | ||
+ | super().__init__() | ||
+ | self.COLUMNS = N_COLUMNS | ||
+ | self.ROWS = N_ROWS | ||
+ | self.CELL_WIDTH = 70 | ||
+ | self.SPACING = 5 | ||
+ | self.COL_BACKGROUND = QColor(" | ||
+ | self.COL_CELL_DEFAULT = QColor(" | ||
+ | self.initUI() | ||
+ | self.timer = QTimer() | ||
+ | self.timer.timeout.connect(self.update) | ||
+ | self.timer.start(500) | ||
+ | |||
+ | def initUI(self): | ||
+ | self.setWindowTitle(' | ||
+ | layout = QVBoxLayout() | ||
+ | self.setLayout(layout) | ||
+ | grid = QWidget() | ||
+ | grid.setStyleSheet(" | ||
+ | grid_layout = QVBoxLayout(grid) | ||
+ | grid_layout.setSpacing(self.SPACING) | ||
+ | layout.addWidget(grid) | ||
+ | |||
+ | self.cells = [] # Store references to the cell widgets | ||
+ | |||
+ | for y in range(self.ROWS): | ||
+ | row_layout = QHBoxLayout() | ||
+ | grid_layout.addLayout(row_layout) | ||
+ | for x in range(self.COLUMNS): | ||
+ | cell = QLabel() | ||
+ | cell.setFixedSize(self.CELL_WIDTH, | ||
+ | cell.setStyleSheet(" | ||
+ | row_layout.addWidget(cell) | ||
+ | self.cells.append(cell) | ||
+ | |||
+ | self.show() | ||
+ | self.setFixedSize(self.size().width(), | ||
+ | |||
+ | def draw_cell_at_position(self, | ||
+ | """ | ||
+ | changes color of cell at given position | ||
+ | """ | ||
+ | cell = self.cells[y*self.COLUMNS + x] | ||
+ | cell.setStyleSheet(" | ||
+ | |||
+ | def draw_all_cells(self): | ||
+ | """ | ||
+ | draw all cells | ||
+ | """ | ||
+ | for y in range(self.ROWS): | ||
+ | for x in range(self.COLUMNS): | ||
+ | rnd_color = QColor(random.randint(0, | ||
+ | self.draw_cell_at_position(x, | ||
+ | |||
+ | def keyPressEvent(self, | ||
+ | """ | ||
+ | deals with keypressed events | ||
+ | """ | ||
+ | key = event.key() | ||
+ | if key == Qt.Key_Up: | ||
+ | print(' | ||
+ | elif key == Qt.Key_Down: | ||
+ | print(' | ||
+ | self.draw_all_cells() | ||
+ | |||
+ | def update(self): | ||
+ | # Perform your periodic check here | ||
+ | # This function will be called every ... ms (value in self.timer.start(...)) | ||
+ | # Update the necessary data or trigger actions as needed | ||
+ | print(" | ||
+ | |||
+ | if __name__ == ' | ||
+ | app = QApplication(sys.argv) | ||
+ | grid_app = GridApp() | ||
+ | sys.exit(app.exec_()) | ||
+ | </ | ||
+ | |||
+ | ++++ | ||
+ | |||
+ | === Teil V (falls Zeit / für Schnelle): Pimp my Desktop-App === | ||
+ | |||
+ | Wandle deine Desktop-App-Version in ein richtig cooles Game: | ||
+ | |||
+ | * Bilder für Asteroiden und Player (auch versch. Asteroiden-Bilder möglich) | ||
+ | * Explosion-Bild bei Collision. | ||
+ | * Schnick-Schnack wieder einbauen: | ||
+ | * Restart | ||
+ | * Score | ||
+ | * ... | ||
+ | * Asteroiden abschiessen können | ||
+ | * ... | ||
+ | |||
+ | ++++Tipps| | ||
+ | |||
+ | **Bilder Laden:** | ||
+ | Es bietet sich an, Bilder in Unterordnern abzulegen. Lädt man das Bild in Python, muss der relative Pfad (in Bezug auf Location von Python-File) angegeben werden. Da Pfade auf versch. Systemen (z.B. Windows und Mac) anders angegeben werden, muss man die `os.path.join(...)` Funktion verwenden. | ||
+ | |||
+ | Beispiel: Bild `asteroid.png` ist im Unterordner `images`. Gebe Pfad dann wie folgt an: | ||
+ | <code python> | ||
+ | import os | ||
+ | ... | ||
+ | |||
+ | PATH_IMG_ASTEROID = os.path.join(' | ||
+ | </ | ||
+ | |||
+ | **Bild in Cell anzeigen: | ||
+ | |||
+ | <code python> | ||
+ | ... | ||
+ | pixmap = QPixmap(PATH_IMG_ASTEROID) | ||
+ | cell.setPixmap(pixmap.scaled(cell.size(), | ||
+ | </code> | ||
+ | |||
+ | ++++ |