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-05-27 10:05] – [Version 4] sca | talit:asteroids_game [2024-06-03 11:47] (aktuell) – [Version 5] sca | ||
|---|---|---|---|
| Zeile 179: | Zeile 179: | ||
| **Ziel:** beliebig viele, unabhängige Asteroiden | **Ziel:** beliebig viele, unabhängige Asteroiden | ||
| - | Mache eine Kopie von der letzten Version und nenne sie `asteroids_v05.py`. | + | Mache eine Kopie von der letzten Version und nenne sie `asteroids_v05.py`. |
| - | | + | 1. **Plan:** Das Ziel ist, dass wir **beliebig viele Asteroiden** |
| - | * **Asteroiden:** | + | ++++Lösung| |
| - | * **Mehrere** Asteroiden | + | Da jeder Asteroid |
| - | * Asteroiden mit **OOP**, siehe Tipps | + | |
| - | * Asteroiden | + | 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. |
| - | | + | |
| - | * Asteroiden | + | Diese Lösung ist sehr elegant, da alle Informationen, |
| - | | + | |
| - | * Player | + | Verwende auch für den Player |
| - | * die beiden Klassen sollten sich möglichst ähnlich sein, z.B. gleiche Methoden-Namen | + | |
| + | Eine gute Alternative zu Dicts wäre die objektorientierte Programmierung (OOP). Verwende aber Dicts, auch wenn du OOP bereits kennst. | ||
| + | ++++ | ||
| + | </ | ||
| + | | ||
| ++++Tipps| | ++++Tipps| | ||
| * Tipp: Kommentiere Collision temporär aus, damit du dich auf die Asteroiden konzentrieren kannst. | * Tipp: Kommentiere Collision temporär aus, damit du dich auf die Asteroiden konzentrieren kannst. | ||
| - | | + | |
| - | * Speichere die Asteroiden-Objekte in einer Liste | + | |
| - | * Update jeden Asteroiden, indem du dessen `update()`-Methode aufrufst. | + | |
| - | Asteroiden-Klasse muss folgende Attribute und Methoden beinhalten. Natürlich kannst du andere (aber sinnvolle) Namen verwenden und auch weitere (sinnvolle) Attribute und Methoden definieren. | + | ++++ |
| - | <code python> | + | |
| - | # Class | + | |
| - | class Asteroid(): | + | |
| - | def __init__(self, | + | |
| - | self.x = ... | + | |
| - | self.y = ... | + | |
| - | self.brightness = ... | + | |
| - | self.update_time = ... # time between next move | + | |
| - | self.time_last_update = ... | + | |
| - | self.is_active = True # optional, see info for update-Method | + | |
| + | ===== 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): | def update(self): | ||
| """ | """ | ||
| - | | + | if enough time has passed, asteroid' |
| - | In here, check if enough time has passed s.t. asteroid moves to next position. | + | |
| - | If yes, move asteroid. | + | |
| - | + | ||
| - | Somewhere need to check if asteroid is out of screen. Can be done here or outside in game loop. | + | |
| - | If done here, class need attribute self.is_active = False/True | + | |
| """ | """ | ||
| pass | pass | ||
| - | | + | class Game: |
| + | | ||
| + | .... | ||
| + | self.player = Player(...) # create player and attache to Game-class as attribute | ||
| + | self.asteroids = [] # also create empty list for asteroids | ||
| + | .... | ||
| + | |||
| + | def spawn_asteroids(self): | ||
| """ | """ | ||
| - | | + | |
| + | If it is, new asteroid spawns with given probability | ||
| """ | """ | ||
| pass | pass | ||
| - | # Create object | + | def update_asteroids(self): |
| - | a = Asteroid(...) | + | """ |
| + | updates position | ||
| + | """ | ||
| + | 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(), | ||
| + | </ | ||
| + | |||
| + | ++++ | ||