Unterschiede

Hier werden die Unterschiede zwischen zwei Versionen der Seite angezeigt.

Link zu der Vergleichsansicht

Beide Seiten, vorherige Überarbeitung Vorherige Überarbeitung
Nächste Überarbeitung
Vorherige Überarbeitung
talit:asteroids_game [2024-04-29 10:29] – [Version 1: Challenge] scatalit: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 88: Zeile 88:
 </nodisp> </nodisp>
  
 +
 +
 +===== 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,2,9)` um einzelne Pixel anzusteuern anstelle `Image("00000:00.....")`
 +   * Arbeite mit **Koordinaten**, z.B. `player_x = 2`. Speichere die Koordinaten des Asteroiden in einem Dictionary mit zwei key:value-pairs: `'x' : 2, 'y' : 3`ö
 +   * **Bewegung Player:**
 +     * `button_a.get_presses()` anstelle `button_a.is_pressed()`
 +   * **Update Spiel:**
 +     * Vermeide lange Delays wie `sleep(1000)`, da diese auch die Bewegung des Players stören.
 +     * 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, z.B. `asteroid` (für $x-$Koordinate von Asteroid) anstelle `a`.
 +     * Halte dich an Python-Konventionen: `player_x` anstelle `PlayerX`
 +
 +<code python>
 +import time
 +...
 +time.ticks_ms() # gibt an, seit wie vielen Millisekunden Micro:bit schon läuft.
 +# Alternative:
 +running_time()
 +...
 +</code>
 +
 +++++
 +
 +
 +===== 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: für Überschriften (wie in Vorgabe) und Erklärungen (nicht zu viel, nicht zu wenig) auf *Englisch* (höherer Lässigkeitsfaktor, ähm coolness factor!)
 +   * 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
 +""" all constants: 'variables' that don't change, use CAPITAL letters only """
 +
 +# VARIABLES
 +""" variables (that do change) that need to be declared before the while-loop """
 +
 +while True:
 +    # get current time
 +
 +    # update asteroid position
 +
 +    # update player (button presses)
 +
 +    # collision check
 +
 +    # update display
 +    """ ONLY place in code where we have display.... First clear, then show """
 +
 +    # short sleep
 +</code>
 +++++
 +
 +===== 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.<WRAP>
 +++++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, die einen Asteroiden betreffen, in einer einzigen Variable (ein Dictionary) gespeichert sind.
 +
 +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.
 +++++
 +</WRAP>
 +   1. **Programmieren:** Mache eine Kopie der letzten Version deines Codes und implementiere die neuen Features (siehe Lösungen von 1.). 
 +
 +++++Tipps|
 +
 +   * Tipp: Kommentiere Collision temporär aus, damit du dich auf die Asteroiden konzentrieren kannst.
 +   * Information für Asteroid: $x-$ und $y-$Position, Geschwindigkeit (resp. Anzahl ms bis zum nächsten Update), Zeit von letztem Update
 +
 +++++
 +
 +===== Version Final =====
 +
 +**Slides:** {{ :talit:microbit_model_view.pdf |Model vs. View}}
 +
 +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`: Beinhaltet das **Modell** und damit die ganze Logik des Spiels. Wird von den anderen Files importiert.
 +   * `asteroids_game_microbit.py`: Kümmert sich *ausschliesslich* um die **View auf dem Microbit**
 +   * `asteroids_game_console.py`: Kümmert sich *ausschliesslich* um die **View in der Konsole**
 +   * `asteroids_game_pyqt.py`: Kümmert sich *ausschliesslich* um die **View auf der Desktop App**
 +
 +=== 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 == 'microbit':
 +        try:
 +            return time.ticks_ms()
 +        except Exception as e:
 +            print("An error occurred on microbit:", str(e))
 +    else:
 +        try:
 +            return time.time()*1000
 +        except Exception as e:
 +            print("An error occurred on Windows/Mac/Linux/... (not microbit):", str(e))
 +
 +class Player:
 +    def __init__(self,...):
 +        pass
 + 
 +    def update(self,move):
 +        """
 +        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's y position increases by 1
 +        """
 +        pass
 +
 +class Game:
 +    def __init__(self,...): # pass game settings as parameters
 +        ....
 +        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
 +</code>
 +
 +++++
 +
 +=== Teil II: Micro-Bit ===
 +
 +   1. Erstelle im Online-Editor ein neues File (heisst `main.py`).
 +   1. Erstelle darin ("Create file") ein neues File mit Namen `asteroids_game_model.py` und füge dort deinen Code vom Teil I ein. Wenn du dort alles richtig gemacht hast, musst du in diesem File gar nichts mehr anpassen. Falls du dies musst, übernehme die Änderungen auch im File auf dem Computer. Es ist wichtig, dass diese beiden immer identisch sind.
 +   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's led-matrix.
 +    """
 +    pass
 +    
 +g = Game(...) # create Game object
 +
 +while True:
 +    # call methods of Game object
 +    ...
 +    show(g)
 +    sleep(...)
 +</code>
 +++++
 +=== 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 'Asteroiden-Regen' generiert
 +
 +
 +=== 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:** Importiere PyQt5 mit **pip** (siehe [[talit:python_setup#installation_von_python_modulen|Tutorial "Installation von Python Modulen"]])
 +
 +<code python>
 +from asteroids_game_model import Game
 +import sys
 +import random
 +from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel
 +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("black")
 +        self.COL_CELL_DEFAULT = QColor("white")
 +        self.initUI()
 +        self.timer = QTimer()
 +        self.timer.timeout.connect(self.update)
 +        self.timer.start(500)  # Trigger the checkSomething function every ... milliseconds
 +
 +    def initUI(self):
 +        self.setWindowTitle('Grid App')
 +        layout = QVBoxLayout()
 +        self.setLayout(layout)
 +        grid = QWidget()
 +        grid.setStyleSheet("QWidget {{ background-color: {0}; }}".format(self.COL_BACKGROUND.name()))
 +        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,self.CELL_WIDTH)
 +                cell.setStyleSheet("background-color: {}".format(self.COL_CELL_DEFAULT.name()))
 +                row_layout.addWidget(cell)
 +                self.cells.append(cell)
 +
 +        self.show()
 +        self.setFixedSize(self.size().width(),self.size().height()) # -> window non-resizable
 +
 +    def draw_cell_at_position(self,x,y,color):
 +        """
 +        changes color of cell at given position
 +        """ 
 +        cell = self.cells[y*self.COLUMNS + x]
 +        cell.setStyleSheet("background-color: {}".format(color.name()))
 +
 +    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, 255), random.randint(0, 255), random.randint(0, 255))
 +                self.draw_cell_at_position(x,y,rnd_color)
 +                
 +    def keyPressEvent(self, event):
 +        """
 +        deals with keypressed events
 +        """
 +        key = event.key()
 +        if key == Qt.Key_Up:
 +            print('key up pressed')
 +        elif key == Qt.Key_Down:
 +            print('key down pressed')
 +        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("Do something!")
 +
 +if __name__ == '__main__':
 +    app = QApplication(sys.argv)
 +    grid_app = GridApp()
 +    sys.exit(app.exec_())
 +</code>
 +
 +++++
 +
 +=== 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('images','asteroid.png')
 +</code>
 +
 +**Bild in Cell anzeigen:**
 +
 +<code python>
 +...
 +pixmap = QPixmap(PATH_IMG_ASTEROID)
 +cell.setPixmap(pixmap.scaled(cell.size(), aspectRatioMode=Qt.AspectRatioMode.KeepAspectRatio))
 +</code>
 +
 +++++
  • talit/asteroids_game.1714386559.txt.gz
  • Zuletzt geändert: 2024-04-29 10:29
  • von sca