Pong-Spiel

Hier werden einzelne Projekte mit MicroPython vorgestellt
Antworten
Heinrichs
Beiträge: 185
Registriert: Do 21. Okt 2010, 18:31

Pong-Spiel

Beitrag von Heinrichs » Di 15. Dez 2020, 14:23

Bei Pong handelt es sich um ein einfaches Tennis-Video-Game. Anfang der Siebziger Jahre erschien es zunächst in Spielhallen; wenige Jahre später kam es dann auch als Video-Game für Heim-Computer auf den Markt.

pong_1.jpg
Pong-Spiel
pong_1.jpg (2.59 KiB) 10113 mal betrachtet

Am linken und rechten Rand sind jeweils ein Tennis-Schläger (durch ein Rechteck dargestellt) zu sehen; diese können mit einem Joystick auf- und ab bewegt werden. Damit kann der Ball (durch ein kleines Quadrat oder einen Kreis dargestellt) im Spiel gehalten werden; dazu müssen die Spieler den Schläger mit dem Joystick so positionieren, dass der Ball nicht am Schläger vorbei auf die dahinter liegende Begrenzung gelangt.

Diese Spiel-Idee kann man relativ einfach auf dem TTGO T-Display realisieren. Wir beschränken uns dabei allerdings auf einen einzigen Spieler, der gegen den Microcontroller antritt. Hier kommt es nun darauf an, den Ball möglichst lange im Spiel zu halten.

Als Joy-Stick benutzen wir hier das Modul KY-023, welches für wenige Euro zu erwerben ist. Diees Modul schließen wir folgendermaßen an:

Code: Alles auswählen

# KY-023  --  ESP32
# GND     --  G
# +5V     --  3V (real: 3.3 V)
# VRY     --  36

Im Wesentlichen besteht dieses Modul aus zwei Potentiometern, deren Mittenabgriffe eine Spannung (gegenüber GND) ausgeben, die nahezu proportional (s. u.) zur jeweiligen Auslenkung ist. In unserem Fall benutzen wir das Y-Signal; dieses wird mit einem ADC-Objekt in einen Zahlenwert umgesetzt. Über diesen Zahlenwert kann der Schläger (das rote Rechteck auf der rechten Seite) gesteuert werden. Zusätzlich ermittelt das Programm, ob der Ball auf den Schläger trifft oder nicht. Trifft er auf den Schläger wird der Ball reflektiert; dabei weicht der Ausfallswinkel (zufallsbedingt) mal etwas mehr, mal etwas weniger vom Einfallswinkel ab. Trifft der Ball nicht auf den Schläger, ist das Spiel beendet und die erzielte Spieldauer wird angezeigt.


Nach dem Start des Programms kann man zunächst den Schwierigkeitsgrad festlegen. Man kann mit den Tasten A (oben links) und B (unten links) wählen zwischen den Einstellungen
  • slow
  • medium
  • fast
pong_menue_klein.jpg
Pong-Menü
pong_menue_klein.jpg (33.13 KiB) 10109 mal betrachtet

Dadurch wird die (durchschnittliche) Geschwindigkeit des Balls festgelegt. Nachdem man die Taste B betätigt hat, beginnt das Spiel.

pong_spiel_klein.jpg
Pong-Spiel (TTGO)
pong_spiel_klein.jpg (16.32 KiB) 10109 mal betrachtet

Hier nun das kommentierte Programm:

Code: Alles auswählen

# Pong Version 1.3
# Joystick KY-023 anschließen (s.u.)
# Beachte: ADC des ESP32 arbeitet nicht ganz linear!


from machine import Pin, SPI, ADC
from time import sleep, time
from random import randint
import vga1_8x8 as font0
import vga1_bold_16x32 as font1
import vga1_16x16 as font2
import st7789

# Joystick:
# KY-023  --  ESP32
# GND     --  G
# +5V     --  3V (real: 3.3 V)
# VRY --  36
pot = ADC(Pin(36))  # ADC0
pot.atten(ADC.ATTN_11DB)  # Messbereich: 0 - 3.6 V;
pot.width(ADC.WIDTH_12BIT) # 12 Bit: 0-4095

# Konstanten für Grafik:
max_x = 239
max_y = 134
ball_size = 10
ball_size_2 = int(ball_size / 2)
up = ball_size_2
up2 = ball_size
down1 = max_y - ball_size_2
down2 = max_y - ball_size
left1 = ball_size_2
left2 = ball_size
right1 = max_x - ball_size
right2 = max_x - ball_size * 2
mid_y = int(max_y / 2)
rack_size = 30
rack_size_2 = int(rack_size / 2)

########################## Menu-Funktionen #############################

def menu_rahmen():
  display.rect(0, 0, 239, 134, st7789.YELLOW)
  display.rect(0, 0 ,20 ,20, st7789.YELLOW)
  display.text(font2, 'A', 2, 2, st7789.YELLOW)
  display.rect(0, 114 ,20 ,20, st7789.YELLOW)
  display.text(font2, 'B', 2, 117, st7789.YELLOW)
  display.text(font0, 'Mit Taster A scrollen', 35, 30, st7789.RED)
  display.text(font0, b'Mit Taster B ausw\x84hlen', 35, 40, st7789.RED)

def taster_eingabe(): # gibt Taster-Nummer zurück (A = 1, B = 2)
  tasterA = Pin(0, Pin.IN, Pin.PULL_UP) 
  tasterB = Pin(35, Pin.IN) # hat auf dem Board schon einen 100K-Pullup (bei Key)
  t = 0
  weiter = True
  while weiter:
    if tasterA.value() == 0:
      t = 1  
    if tasterB.value() == 0:
      t = 2  
    if t > 0:
      sleep(0.2)
      if tasterA.value() and tasterB.value() == True:
        weiter = False
  return t

def auswahl_anzeigen(awliste):
  for item in awliste:
    if item[4]:  # if selected...
      display.text(font2,item[1],item[2],item[3],st7789.YELLOW) 
    else:
      display.text(font2,item[1],item[2],item[3],st7789.WHITE)

def waehlen(awliste):  # gibt das ausgewählte Item zurück
  global itemindex
  weiter = True
  while weiter:   
    menu_rahmen()
    auswahl_anzeigen(awliste)

    while True:
      taster = taster_eingabe()
      if taster == 1: # Scrollen 
        itemindex += 1
        itemindex = itemindex % len(auswahlliste)
        for item in awliste: # erst alle auf False setzen
          item[4] = False
        awliste[itemindex][4]=True  # dann indiziertes Item auf True
        auswahl_anzeigen(awliste)
      if taster == 2: # auswählen
        return awliste[itemindex] 
        weiter = False

################## pong-Funktionen ################################

def pong_rahmen():
  display.rect(0, 0, right1, max_y, st7789.YELLOW)
  
def welcome():
  display.text(font1, 'Pong 1.3', 50, 30, st7789.GREEN, st7789.BLACK)  
  display.text(font1, 'Welcome!', 50, 70, st7789.GREEN, st7789.BLACK)
  
def rack_pos():
  pot_wert = pot.read()
  spannung = 0.0007815*pot_wert + 0.13
  spannung = spannung - 1.667  # Mittelstellung auf 0; Wert negativ, wenn Joystick nach oben/rechts ausgelenkt.
  # oben/rechts: -1.52V; unten/links: 1.68V
  return int(spannung/1.7*50) + mid_y

def show_rack(y):
  display.fill_rect(right1, y-rack_size_2, ball_size, rack_size, st7789.RED)
  
def del_rack(y):
  display.fill_rect(right1, y-rack_size_2, ball_size, rack_size, bg)
  
def show_ball(x,y):
  display.rect(x-ball_size_2, y-ball_size_2, ball_size, ball_size, st7789.WHITE)
  
def del_ball(x,y):
  display.rect(x-ball_size_2, y-ball_size_2, ball_size, ball_size, bg)
  

# Display initialisieren...
spi = SPI(2, baudrate=20000000, polarity=1, sck=Pin(18), mosi=Pin(19))
display = st7789.ST7789(spi, 135, 240,  reset=Pin(23, Pin.OUT), cs=Pin(5, Pin.OUT), dc=Pin(16, Pin.OUT), backlight=Pin(4, Pin.OUT), rotation=3) # Landscape
display.init()
display.fill(0)  # loeschen; Bildschirm schwarz
bg = st7789.color565(20, 20, 20)


############################### Hauptprogramm #########################

welcome()
sleep(2)
display.fill(0)  # loeschen; Bildschirm schwarz

# Menue-Rahmen mit Taster-Kennung, Überschrift
menu_rahmen()
display.text(font2, 'Auswahl', 50, 5) # Überschrift
sleep(0.5)

# Menue-Auswahlliste initialisieren (Zahl, Text, x-Position, y-Position, ausgewählt/nicht ausgewählt)
auswahl0 = [0.06, ' slow ', 35, 60, False]
auswahl1 = [0.04, ' medium ', 35, 75, False]
auswahl2 = [0.02, ' fast ', 35, 90, False]
auswahlliste = [auswahl0, auswahl1, auswahl2]
itemindex = 0  # Standardvorgabe: slow
auswahlliste[itemindex][4] = True

# Auswahl durchführen und ausgeben (hier: delta_t als Maß für den Schwierigkeitsgrad):
delta_t = waehlen(auswahlliste)[0] # das erste Element des ausgewählten Items -> delta_t
# print(delta_t)

# Initialisierung:
ball_pos_x = 20
ball_pos_y = 50
ball_delta_x = 5
ball_delta_y = randint(-1,1)

display.fill(bg)
pong_rahmen()
sleep(1)

# Animation:
time1 = time()
while True:
  s_position = rack_pos()
  # print(s_position)
  show_rack(s_position)
  show_ball(ball_pos_x, ball_pos_y)
  sleep(delta_t)  # ggf. schneller
  del_rack(s_position)
  del_ball(ball_pos_x,ball_pos_y)
  if ball_pos_x >= right2 and abs(ball_pos_y - s_position) > rack_size_2:
    time2 = time()  
    display.text(font1, 'Game Over!', 35, 25, st7789.RED, bg)
    display.text(font1, 'Time: '+str(time2-time1)+' s', 35, 65, st7789.RED, bg)
    break      
  if ball_pos_x >= right2 or ball_pos_x <= left2:
    ball_delta_x = - ball_delta_x
    ball_delta_y = ball_delta_y + randint(-1,1)  # kleine zufällige Änderung von ball_delta_y an linker/rechter Seite
    if abs(ball_delta_y) >3:  # ball_delta_y soll nicht zu groß werden
        ball_delta_y = randint(-1,1) 
    print(ball_delta_y)  
  ball_pos_x = ball_pos_x + ball_delta_x
  if ball_pos_y > down2 or ball_pos_y < up2:  
    ball_delta_y = - ball_delta_y
 
  ball_pos_y = ball_pos_y + ball_delta_y


Bemerkungen zu den im Programm benutzten Klassen und Funktionen

Informationen zu Pin, SPI, sleep sowie zum Display findet man in dem Skript WLAN MIT DEM ESP32.

Weitere Informationen zum Betrieb des Displays gibt es hier.

Die Programmierung des Menü-Teils ist schon an dieser Stelle vorgestellt worden.

Zum Messen von Spannungswerten benutzen wir hier eine Instanz von ADC:

pot = ADC(Pin(36)) # ADC0

Damit wird der GPIO36 als Analog-Eingang festgelegt. Über den Bezeichner pot können wir dann den Verstärkungsgrad (Eigenschaft atten) und die Bit-Breite des Messwertes (width) einstellen:

pot.atten(ADC.ATTN_11DB) # Messbereich: 0 - 3.6 V;
pot.width(ADC.WIDTH_12BIT) # 12 Bit: 0-4095

Der Messwert lässt sich dann mit

pot_wert = pot.read()

auslesen. Der zugehörige Spannungswert ergibt sich bei meinem ESP32 dann durch

spannung = 0.0007815*pot_wert + 0.13

Diese Beziehung wurde durch eine lineare Anpassung an eine Messkurve aufgestellt.

Die Zeitmessung wurde mit der time-Funktion des Moduls time durchgeführt. Diese Funktion gibt die Zeitdauer vom Einschalten bzw. Hard Reset bis zur Abfrage (in Sekunden) an.

Die "zufälligen" Änderungen beim Ausfallswinkel werden mit Hilfe der Funktion randint(a, b) erzeugt. Diese Funktion liefert ganzzahlige Zufallswerte im Bereich von a bis b (einschließlich!); sie wird zuvor aus dem Modul random importiert.


.
Dateianhänge
ttgo_pong_gekürzt.zip
Video zum Pong-Spiel (gekürzt)
(6.08 MiB) 1563-mal heruntergeladen

Antworten