Inhaltsverzeichnis

Pygame Legacy 2020

Auch in diesem Semester arbeiten wir wieder mit GitHub. Erstelle für die Aufgaben zu Pygame ein neues Repository mit Namen pygame_projects und füge diesem die jeweiligen Aufträge hinzu. Gib dieses Repo wieder frei für den Benutze anschae.

1. Installation

Achtung: Pygame funktioniert unter Umständen nicht mit der aktuellsten Version von Python. Im Oktober 2020 sollte Python 3.8 am besten funktionieren (nicht getestet).

Auftrag:

  1. Installiere Pygame für deine Python 3.7 Installation. Der Pfad dieser Executable dürfte wie folgt ausschauen C:\Users\<username>\AppData\Local\Programs\Python\Python37\python.exe
  2. Installiere Pygame mit pip
  3. Wichtig: Wenn du an einem Pygame Projekt arbeitest, solltest du in VSCode immer den python Pfad dieser Python Installation verwenden. Dies erreichst du, in dem du in den Einstellungen des aktuellen Pojekts (Preferences: Open Workspace Settings → wechsle in JSON Ansicht) den die Variable "python.pythonPath" richtig setzt.

2. Theorie I: Uniforme Bewegung, Vektoren & Kollisionen

2.1 Uniforme Bewegung

Uniforme Bewegung bedeutet, dass man sich mit konstanter Geschwindigkeit fortbewegt.

Bewegt man sich während der Zeit $t$ mit der konstanten Geschwindigkeit $v$, so legt man folgende Distanz zurück: $$s = v t$$

2.1.1 Uniforme Bewegung in 2 Dimensionen

Betrachtet man die Bewegung in 2 Dimensionen, so ist es am einfachsten, wenn man die Position und die Geschwindigkeiten als Vektor betrachtet:

$$\vec{s} = \begin{pmatrix}s_x \\ s_y \end{pmatrix}$$ $$\vec{v} = \begin{pmatrix}v_x \\ v_y \end{pmatrix}$$

Dabei nennt man …:

Ein Vektor hat immer sowohl eine Richtung als auch eine Länge. Die Länge berechnet man mit dem Satz von Pythagoras: $$v = |\vec{v}| = \sqrt{v_x^2 + v_y^2}$$

Wie beschreibt man nun die Bewegung eines Objekts in 2 Dimensionen? Dieses Objekt befinde sich zum Zeitpunkt $t=0$ am Ort $\vec{s_0} = \begin{pmatrix}s_x^0 \\ s_y^0 \end{pmatrix}$ und es bewege sich mit der Geschwindigkeit $\vec{v_0} = \begin{pmatrix}v_x^0 \\ v_y^0 \end{pmatrix}$. Die Position des Objekts ist dann gegeben durch:

$$\vec{s}(t) = \vec{s_0} + \vec{v_0}t$$

Beachte, dass die Position eine Funktion der Zeit ist: Je länger sich das bewegt, desto weiter ist es von seiner ursprünglichen Position $\vec{s}_0$ entfernt.

2.1.2 Beispiel

Machen wir ein Beispiel.

An welcher Position befindet man sich nach der $1$ Zeiteinheit , $2$ Zeiteinheiten oder $60$ Zeiteinheiten? Die Position ist gegeben durch: $$\vec{s}(t) = \vec{s_0} + \vec{v_0}t = \begin{pmatrix}5 \\ 2 \end{pmatrix} + \begin{pmatrix}-2 \\ 4 \end{pmatrix}t = \begin{pmatrix}5 - 2 t \\2 - 4 t \end{pmatrix}$$

Nach der Zeiteinheit $1$ befindet man sich also an der Position $\vec{s}(1) = \begin{pmatrix}3 \\-2 \end{pmatrix}$

2.2 Vektoren in Numpy

Numpy ist eine Python-Library, die dafür konzipiert wurde, Mathematik zu betreiben. Die darin vorhandenen Numpy-Arrays sind dafür geeignet, Vektoren in Python darzustellen.

Das Erstellen eines solchen Numpy-Arrays geht ganz einfach: Nimm eine normale Liste L und wandle diese in ein solches Array um: np.array(L). Wichtig: Importiere dazu zuerst numpy wie folgt: import numpy as np.

Das oben aufgeführte Beispiel kann man in Numpy wie folgt aufsetzen:

s0 = np.array([5,2])
v0 = np.array([-2,4])
t = 6
s = s0 + v0*t

2.3 Kollisionen

Seien v1 und v2 die Geschwindigkeitsvektoren kollidierender Bälle bevor sie kollidieren. Welche Geschwindigkeitsvektoren haben sie nach der Kollision? Diese sind gegeben durch eine Formel, welche du im folgenden Dossier findest: Dossier Kollision. Eine solche Kollision heisst elastische Kollision oder elastischer Stoss.

Tipp: Am einfachsten arbeitet man mit Numpy-Arrays (z.B. v1 = np.array([3,-7])). Dies macht die Berechnung der neuen Geschwindigkeitsvektoren relativ einfach.

Quelle: https://en.wikipedia.org/wiki/Elastic_collision#Two-dimensional (letzte beiden Formeln des Anschnitts)

3. Bouncing Balls v01 (Auftrag 1)

3.1 Auftrag in Kürze

  1. Lese zuerst den Auftrag hier und die unten aufgeführten Bemerkungen usw. genau durch.
  2. Lade den Code, der dir für diese Aufgabe zur Verfügung gestellt wird, herunter (bouncing_balls_v01.py) und füge diesen deinem GitHub Repo pygame_projects hinzu. Falls du dieses noch nicht für die Lehrperson freigegeben hast, hole dies gleich nach!
  3. Führe den Code aus.
  4. Verschaffe dir nun einen Überblick über den Code. Setze einen Breakpoint ganz zu Beginn der play()-Methode und gehe Schritt-für-Schritt durch den Code. Es ist wichtig, dass du verstehst, was in jedem Schritt passiert!
  5. Erweitere dann den Code so, dass die Punkte unten umgesetzt sind. Wichtig: Die Bälle sollen sich nach den Regeln der Physik verhalten.

3.2 Erweiterungen für Code

  1. Jedem Ball wird bei dessen Erstellung die Geschwindigkeitskomponenten in x- und y-Richtung mitgegeben. Die Bälle sollen sich entsprechend bewegen.
  2. An den Wänden sollen die Bälle abprallen (Einfallswinkel = Ausfallswinkel).
  3. Kollidieren zwei Bälle, so sollen wie voneinander abprallen (siehe Theorie unten).
  4. Bei Kollisionen kann es zu 'Tänzen' kommen. Baue einen Schutzmechanismus ein, der solche Tänze so gut wie möglich verhindert.

Zusatzaufgaben:

3.3 Bemerkungen

3.4 Erklärungen Pygame

3.5 Code

Dieser Code dient als Startpunkt:

bouncing_balls_v01.py
import os
import pygame
from PIL import Image, ImageDraw
 
 
### CONSTANTS ###
 
# Windows
WIN_HEIGHT = 800
WIN_WIDTH = 500
 
# Framerate
FPS = 30
TIME_DELAY = int(1000 / FPS)
 
# Colors
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
BG_COLOR = GREEN
 
 
### METHODS ###
 
def draw_bmp_circle(radius, color,path):
    """
    creates circle image and saves on disk
    """
    img = Image.new('RGBA', (radius,radius), (255, 0, 0, 0))
    draw = ImageDraw.Draw(img)
    draw.ellipse((0,0,radius-1,radius-1), fill=color)
    img.save(path, 'PNG')
 
 
### CLASSES ###
 
class Ball(pygame.sprite.Sprite):
    def __init__(self, xy_center, v, radius, mass, color):
        super().__init__() # call __init__ of parent class (i.e. of pygame.sprite.Sprite)
 
        # CREATE BALL IMAGE (if does not exist yet)
        file_path = f"data/ball_r={radius}_col={str(color).replace(' ','').replace(',','_')}.png"
        if not os.path.isfile(file_path):
            draw_bmp_circle(radius,color,file_path) # draw image and save on disk if doesnt exist yet
 
        # ASSIGN CLASS ATTRIBUTES
        self.image = pygame.image.load(file_path) # load image
        self.rect = self.image.get_rect() # create rectangle containing ball image
        self.rect.center = (int(xy_center[0]),int(xy_center[1])) # set center coords of ball
        self.mass = mass # is relevant for realistic collisions
        self.color = color
        self.vx = v[0] # velocity
        self.vy = v[1]
        self.mask = pygame.mask.from_surface(self.image) # creates a mask, used for collision detection (see manual about pygame.sprite.collide_mask())
 
    def update(self):
        """
        determines motion of particles:
 
        """
        # TODO:
        # 1. General motion of particles
        # 2. Bouncing off walls
        # do NOT deal with collisions in here
 
    def collide(self,ball):
        """
        ball self collides with other ball, given as argument
        this method updates velocities of BOTH balls
        """
        # TODO
        # 1. two balls (self and ball) collide -> deal with collision
        # Theory of collision btw two balls: https://en.wikipedia.org/wiki/Elastic_collision#Two-dimensional (last formula)
        # 2. try to avoid 'ball dances'
 
 
class Game:
    """
    Main GAME class
    """
    def __init__(self):
        pygame.init()
        pygame.font.init()
        self.time_delay = TIME_DELAY
        self.screen = pygame.display.set_mode((WIN_WIDTH, WIN_HEIGHT)) # create screen which will display everything
        self.win = pygame.display.set_mode((WIN_WIDTH, WIN_HEIGHT))
        pygame.display.set_caption("Bouncing Balls") # Game title
 
    def play(self):
        # CREATE GAME GROUPS
        Balls = pygame.sprite.Group()
 
        # CREATE OBJECTS AND ASSIGN TO GROUPS
        balls_list = [
            Ball([0.25*WIN_WIDTH, 0.75*WIN_HEIGHT], [ -4,  7], 100, 1, BLUE)
            ,Ball([0.25*WIN_WIDTH, 0.50*WIN_HEIGHT], [ -5, -10], 100, 1, BLACK)
            ,Ball([0.50*WIN_WIDTH, 0.75*WIN_HEIGHT], [  7,  -7], 100, 1, WHITE)
            ,Ball([0.50*WIN_WIDTH, 0.25*WIN_HEIGHT], [ -4,  7], 100, 1, RED)
            ]
        Balls.add(balls_list)
 
        # GAME PERMANENT LOOP
        while True:
            pygame.time.delay(TIME_DELAY)
 
            # KEY EVENTS
            for event in pygame.event.get():
                # Exit app if click quit button
                if event.type == pygame.QUIT:
                    run = False
 
            keys = pygame.key.get_pressed()
 
            if keys[pygame.K_ESCAPE]:
                self.exit()
 
            self.screen.fill(BG_COLOR)  # draw empty screen
 
            # COLLISION DETECTION
            # see manual for all types of collisions: https://www.pygame.org/docs/ref/sprite.html#pygame.sprite.spritecollide
            # TODO: check for collisions between any two balls. If there is any, call the collision() method of the Ball class.
 
            # DRAW
            Balls.draw(self.screen)
 
            # UPDATE
            Balls.update()
 
            pygame.display.update()
 
        pygame.quit()
 
 
if __name__ == "__main__":
    Game().play()

4. Theorie II: Uniforme Beschleunigung & Gravitation

4.1 Uniforme Beschleunigung

Eine Beschleunigung ist eine Änderung der Geschwindigkeit pro Zeit

$$a = \frac{\Delta v}{t}$$

wobei:

Eine Uniforme Beschleunigung ist einfach eine konstante Beschleunigung. Die Beschleunigung ist also immer gleich stark und verändert sich nicht mit der Zeit.

4.1.1 Uniforme Beschleunigung in 1D

Bewegung entlang der $x-$Achse:

Nachdem für die Zeit $t$ mit $a$ beschleunigt wurde, befindet man sich an der Position $s$ und hat Geschwindigkeit $v$, gegeben durch:

$$v(t) = v_0 + a t$$ $$s(t) = s_0 + v_0 t + \frac12 a t^2$$

4.1.2 Uniforme Beschleunigung in 2D

Ein Objekt befindet sich zum Zeitpunkt $t=0$ am Ort $\vec{s_0} = \begin{pmatrix}s_x^0 \\ s_y^0 \end{pmatrix}$ und bewegt sich mit der Geschwindigkeit $\vec{v_0} = \begin{pmatrix}v_x^0 \\ v_y^0 \end{pmatrix}$. Das Objekte werde jetzt durch den Beschleunigungsvektor $\vec{a} = \begin{pmatrix}a_x \\ a_y \end{pmatrix}$ beschleunigt. Dann verändern sich Geschwindigkeit und Position wie folgt mit der Zeit:

$$\vec{v}(t) = \vec{v_0} + \vec{a}t$$ $$\vec{s}(t) = \vec{s_0} + \vec{v_0}t + \frac12 \vec{a} t^2$$

4.2 Gravitation

Lässt man etwas fallen, so wird es mit der Beschleunigung $g = 9.81\text{m}/\text{s}^2$ nach unten beschleunigt. Da diese Beschleunigung sehr wichtig ist, hat sie einen eigenen Namen erhalten und man schreibt typischerweise $g$ anstelle von $a$. Auf der Erdoberfläche ist die Gravitationsbeschleunigung in etwa konstant, weshalb man guten Gewissens bei der Gravitation von ein uniformen Beschleunigung sprechen kann.

In 2 Dimensionen beschreibt man die Gravitation mit dem folgenden Vektor: $$\vec{g} = \begin{pmatrix}0 \\ - 9.81\text{m}/\text{s}^2 \end{pmatrix}$$

Beachte: Hier wird die übliche Konvention in Physik und Mathematik verwendet, dass nach oben durch die positive $y-$Richtung dargestellt wird. In Pygame ist dies anders. Dort sollte der Gravitationsvektor z.B. wie folgt aussehen: g = np.array([0,1]). Beachte, dass der genaue Zahlenwert nicht relevant ist und durch ausprobieren eingestellt werden sollte.

Ganz wichtig: Eine Änderung der Geschwindigkeit in eine Richtung tritt nur dann auf, wenn in diese Richtung eine Beschleunigung wirkt. Beispiele:

5. Bouncing Balls v02 (Auftrag 2)

Mache eine Kopie der letzten Aufgabe und benenne das File um in bouncing_balls_v02.py.

5.1 Auftrag in Kürze

Füge deinem Programm Gravitation hinzu.

5.2 Auftrag im Detail

  1. Kommentiere alle Bälle bis auf einen aus. Setze dessen Geschwindigkeit auf Null. Startet man nun das Programm, so soll ein Ball gezeigt werden, der sich nicht bewegt.
  2. Studiere die Theorie oben über uniforme Beschleunigung und Gravitation genau.
  3. Modifiziere nun die update() Methode der Ball-Klasse: Füge Gravitation hinzu. Bei jedem Update soll nun ein Ball nach unten beschleunigt werden.
  4. Teste deinen Code: Der Ball sollte nun nach unten beschleunigt werden, am Boden abprallen und wieder genau die ursprüngliche Höhe erreiche. Springt der Ball höher oder weniger hoch, so ist etwas falsch.
  5. Füge nun weitere Bälle hinzu und geniesse das Spektakel.
  6. Falls die Bälle zu schnell werden, könntest du eine maximale Geschwindigkeit einführen: Sobald eine Ball diese Geschwindigkeit erreicht hat, wird er nicht mehr schneller.

6. Einfaches Spiel mit uniformer Beschleunigung (Auftrag 3)

Programmiere ein einfaches Spiel (ähnlich zu Jumping Dagobert), welches unten aufgeführte Eigenschaften haben soll.

6.1 Bewegung der Spielfigur

6.2 Punkte sammeln

Ideensammlung Game Mode:

7. Sprite Animationen (Auftrag 4)

Nutze dein Programm jump_game_v01.py als Startpunkt und speichere es ab unter jump_game_v02.py. Erweitere den Code nun so, dass du ein richtiges Spiel erhälst.

Dein Spiel soll animierte Sprites beinhalten

Erstelle Pixel Art (inkl. Animationen) mit Piskel: https://www.piskelapp.com/

Schaue dazu das Tutorial: https://www.youtube.com/watch?v=lJN2C7-dyxE&list=PLO3K3VFvlU6Akj3W29_nMLZFnwNOVbAzI&index=1

8. Projekt (Auftrag 5)

Auftrag in Kürze: Programmiert in 1er oder 2er Gruppen ein Retro Game im Jump 'n Run Stil.

Auftrag in Detail:

Ideen:

Lade Bild und lasse alle weissen Bereiche transparent werden:

im = pygame.image.load('fig.png').convert()
im.set_colorkey((255,255,255))

9.1 Programme

Gratis online Programm für Pixelart: https://www.piskelapp.com/