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 10:20] – 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 | ||
| + | <nodisp 2> | ||
| + | |||
| + | ++++Lösung| | ||
| + | |||
| + | <code python> | ||
| + | from microbit import * | ||
| + | import random | ||
| + | |||
| + | player_x = 2 | ||
| + | delay = 200 | ||
| + | |||
| + | asteroid = { | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | |||
| + | t0 = running_time() | ||
| + | |||
| + | while True: | ||
| + | # UPDATE ASTEROID | ||
| + | t = running_time() | ||
| + | if t - t0 >= delay: | ||
| + | if asteroid[' | ||
| + | asteroid[' | ||
| + | else: | ||
| + | asteroid[' | ||
| + | asteroid[' | ||
| + | t0 = t | ||
| + | | ||
| + | # UPDATE PLAYER | ||
| + | if button_a.get_presses() and player_x > 0: | ||
| + | player_x -= 1 | ||
| + | if button_b.get_presses() and player_x < 4: | ||
| + | player_x += 1 | ||
| + | |||
| + | # CHECK COLLISIONS | ||
| + | if asteroid[' | ||
| + | display.show(Image.SKULL) | ||
| + | break | ||
| + | | ||
| + | # VIEW | ||
| + | display.clear() | ||
| + | display.set_pixel(player_x, | ||
| + | display.set_pixel(asteroid[' | ||
| + | | ||
| + | sleep(50) | ||
| + | </ | ||
| + | |||
| + | ++++ | ||
| + | |||
| + | </ | ||
| + | |||
| + | |||
| + | |||
| + | ===== Version 2 ===== | ||
| + | |||
| + | Mache Kopie von Game und speichere unter Namen `asteroids_v02.py`. Verbessere nun dieses mithilfe der Tipps unten. | ||
| + | |||
| + | ++++Tipps| | ||
| + | |||
| + | * **Display: | ||
| + | * `display.set_pixel(3, | ||
| + | * Arbeite mit **Koordinaten**, | ||
| + | * **Bewegung Player:** | ||
| + | * `button_a.get_presses()` anstelle `button_a.is_pressed()` | ||
| + | * **Update Spiel:** | ||
| + | * Vermeide lange Delays wie `sleep(1000)`, | ||
| + | * Arbeite stattdessen mit `time.ticks_ms()` (siehe unten). | ||
| + | * Speichere den Zeitpunkt, zu dem der Asteroid *zuletzt bewegt wurde* in einer Variablen `t_last_update`. | ||
| + | * Überprüfe in jedem Durchlauf, ob seit der letzten Bewegung genügend Zeit (z.B. 500ms) vergangen ist. Falls ja: Bewege Asteroid und überschreibe `t_last_update` mit der aktuellen Zeit. | ||
| + | * kurzes Delay wie `sleep(20)` verhindert nervöses Flackern | ||
| + | * **Programmierstil: | ||
| + | * Verwende sinnvolle, aussagekräftige Variablennamen, | ||
| + | * Halte dich an Python-Konventionen: | ||
| + | |||
| + | <code python> | ||
| + | import time | ||
| + | ... | ||
| + | time.ticks_ms() # gibt an, seit wie vielen Millisekunden Micro:bit schon läuft. | ||
| + | # Alternative: | ||
| + | running_time() | ||
| + | ... | ||
| + | </ | ||
| + | |||
| + | ++++ | ||
| + | |||
| + | |||
| + | ===== 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(), | ||
| + | </ | ||
| + | |||
| + | ++++ | ||