Aus Linux-Magazin 12/2021

Spieleentwicklung mit Go und dem Fyne-Framework

© Andrii Yurlov / 123RF.com

Mit dem Fyne-Framework für Go lassen sich nicht nur GUIs erstellen, sondern auch Spiele für den Desktop schreiben. Mike Schilli nimmt sich dabei eines Klassikers an.

Die Fußball-EM vor einem Jahr war ein ziemlicher Reinfall für Jogi Löws Gurkentruppe, aber eine Szene des Spiels Tschechien gegen Schottland ist mir trotzdem in Erinnerung geblieben: Der Torwart der Schotten war weit aus dem Tor herausgelaufen; der tschechische Spieler Patrik Schick bemerkte das an der Mittellinie stehend und beförderte mit einem sehenswerten Bogenschuss den Ball ins Gehäuse des aushäusigen Torwarts. Seither versuche ich immer wieder, dieses Kunststück auf meiner Position als Knipser der Amateurmannschaft “Beer Fit” in San Francisco zu replizieren, bislang allerdings ohne Erfolg. Ich beschloss deshalb, daraus ein in Go geschriebenes Videospiel für die Snapshot-Kolumne zu machen.

Das zugrunde liegende physikalische Modell für den Lupferschuss nennt sich “schiefer Wurf” und wird in jedem guten Schulphysikbuch heruntergebetet. Das weiß ich zufällig genau, denn während meines Elektrotechnikstudiums an der TU München schwitzte ich mich durch manche Prüfung im Mörderfach “Technische Mechanik”. Und auch viele, viele Jahre später, mit zitternden Händen ein total vergilbtes Diplom haltend, brauchte ich nur einen kurzen Auffrischer, um die Formeln für die Ballposition abhängig vom Startpunkt, dem Winkel und der Geschwindigkeit des Abschusses sowie der verstrichenen Zeit herzuleiten.

Abbildung 1: Klassischer Lupfer: Der Ball fliegt über den Keeper und purzelt ins Tor hinein.

Abbildung 1: Klassischer Lupfer: Der Ball fliegt über den Keeper und purzelt ins Tor hinein.

Die Flugbahn des Fußballs, der über den Kopf des herausgeeilten Torwarts in hohem Bogen ins dahinterliegende Netz segelt, ist keineswegs der einzige Anwendungsfall des schiefen Wurfs (Abbildung 1). Dieselbe seit Langem bekannte Formel errechnet auch die Flugbahnen ballistischer Geschosse, von der Kanonenkugel bis zur Mittelstreckenrakete.

In erster Näherung

Damit die Formel (Abbildung 2) für die Flugbahn in X/Y-Koordinaten in Abhängigkeit von der verstrichenen Zeit simpel bleibt, berücksichtigt die Implementierung im Spiel neben dem Abschusswinkel und der Anfangsgeschwindigkeit, mit der der Angreifer den Ball in die Luft kickt, nur die Gravitation, die den Ball auf der Bogenbahn wieder auf die Erde zurückholt. Sie vernachlässigt den Luftwiderstand des Leders in der Atmosphäre. Den könnte man mit unterschiedlichen Strömungsmodellen einarbeiten, aber dann dürfte auch eventuell herrschender Gegen- oder Rückenwind eine Rolle spielen, nebst atmosphärischen Bedingungen wie Nebel oder Nieselregen. Deshalb nimmt das Programm einfach an, dass der Ball im Vakuum fliegt – schließlich geht es nur ums Prinzip, nicht um Genauigkeit [1].

Abbildung 2: Formel für X/Y-Koordinaten auf der Wurfparabel, abhängig von der verstrichenen Zeit. Quelle: Wikipedia

Abbildung 2: Formel für X/Y-Koordinaten auf der Wurfparabel, abhängig von der verstrichenen Zeit. Quelle: Wikipedia

Listing 1 setzt die Formel in Go-Code um und packt sie in die Funktion »chipShot()«, die als Eingabeparameter die Abschussgeschwindigkeit, den Winkel im Radiant-Format und die verstrichene Zeit in Sekunden entgegennimmt. Zurück kommt die Position des Balls auf der Bogenbahn zum gegebenen Zeitpunkt als X- und Y-Wert. Da die Formel für die Y-Koordinate auf der Flugbahn blind negative Werte liefert, die Erdoberfläche aber einem Fußball keinen Eintritt in den Untergrund gewährt, setzt Zeile 10 den Höhenwert auf null, sobald die Flugparabel negative Werte annimmt.

Listing 1

physics.go

package main
import (
  "math"
)
func chipShot(v float64, a float64, t float64) (float64, float64) {
  const g = 9.81
  x := v * t * math.Cos(a)
  y := v*t*math.Sin(a) - g/2*t*t
  if y < 0 {
    y = 0
  }
  return x, y
}

Zu Testzwecken plottet Listing 2 mittels des Go-Standard-Plotters »plot« die Flugroute des Balls bei verschiedenen Ausgangsparametern in ein X/Y-Koordinatensystem und erzeugt eine PNG-Datei. Die zeigt dann seine Flugbahn nach dem Abschuss mit 10 Meter pro Sekunde und einem Anstellwinkel von 45 Grad. Tritt der Stürmer beherzter zu und der Ball startet mit 15 m/s bei gleichem Winkel, fliegt er entsprechend höher in die Luft und legt auch eine größere Entfernung zurück, bevor er wieder zur Erde zurückkommt. Den Abschusswinkel definieren die Zeilen 16 bis 19 in Listing 2 jeweils nicht im Grad- sondern im Radiant-Format, genau wie ihn die Sinus- und Kosinus-Funktionen des »math«-Pakets in Go erwarten. Da 180 Grad dem Wert Pi entsprechen, muss die Funktion nur die entsprechenden Bruchteile ausrechnen. So wird aus 45 Grad ein Viertel Pi und aus 30 Grad ein Sechstel Pi.

Listing 2

plot.go

package main
import (
  "gonum.org/v1/plot"
  "gonum.org/v1/plot/plotter"
  "gonum.org/v1/plot/plotutil"
  "gonum.org/v1/plot/vg"
  "math"
)
func main() {
  p := plot.New()
  p.Title.Text = "Projectile Motion"
  p.X.Label.Text = "X"
  p.Y.Label.Text = "Y"
  err := plotutil.AddLinePoints(p,
    "v=10/a=45", shoot(10, math.Pi/4),
    "v=15/a=45", shoot(15, math.Pi/4),
    "v=10/a=60", shoot(10, math.Pi/3),
    "v=10/a=30", shoot(10, math.Pi/6),
  )
  if err != nil {
    panic(err)
  }
  err = p.Save(8*vg.Inch, 8*vg.Inch, "curve.png")
  if err != nil {
    panic(err)
  }
}
func shoot(v float64, a float64) plotter.XYs {
  n := 20
  pts := make(plotter.XYs, n)
  t := 0.0
  for i := range pts {
    pts[i].X, pts[i].Y = chipShot(v, a, t)
    t += 0.25
  }
  return pts
}

Zu jedem Graphen definiert die in Listing 2 ab Zeile 30 implementierte Funktion »shoot()« jeweils 20 Zeitpunkte im Abstand von 0,25 Sekunden. Sie berechnet mit »chipShot()« aus Listing 1 die X/Y-Koordinaten der aktuellen Ballposition und speichert die Messpunkte in einem Array namens »pts« vom Typ »plotter.XYs«. Die Position reicht sie nach Abschluss der For-Schleife wieder ans Hauptprogramm zurück, das die Daten mittels »AddLinePoints« an den Plotter weiterschiebt. Er zeichnet gleich vier solcher Datensätze als Kurven samt Legende ins Koordinatensystem, bis »Save()« in Zeile 24 die Grafik als PNG-Datei in acht mal acht Zoll Größe abspeichert.

Mach ein Spiel draus

Diese physikalischen Grundlagen der ballistischen Flugbahn verpacken die restlichen Listings dieser Ausgabe in ein Desktop-Spiel namens Chipshot (die englische Bezeichnung des Bogenlampen-Schusses [2] im Fußball). In Abbildung 3 hat der Nutzer mit dem oberen Regler eine Startgeschwindigkeit des Balls von 15 m/s eingestellt, mit dem unteren einen Abschusswinkel von 45 Grad. Den Torwart symbolisiert das lachsfarbene Rechteck unten in der Mitte, das Fußballtor der grüne Quader weiter rechts.

Abbildung 3: Lupferspiel: Der Ball fliegt &uuml;ber den Torwart hinweg ins Tor.

Abbildung 3: Lupferspiel: Der Ball fliegt über den Torwart hinweg ins Tor.

Mit den eingestellten Parametern fliegt der Ball, nachdem der User den Button Shoot links oben gedrückt hat, knapp über den Torwart hinweg und rollt mit letzter Kraft ins Tor. Aber das klappt nicht immer: Abbildung 4 zeigt zum Beispiel einen Versuch, bei dem der Ball zwar über den Keeper fliegt, aber dann auf dem Weg zum Tor verhungert, da er nicht genug Bewegungsenergie mitbringt und nach dem Aufschlag am Boden wegen Reibung vorzeitig ausrollt. In Abbildung 5 schließlich kommt der Ball vorzeitig herunter, und der Torwart fängt ihn. Game over!

Abbildung 4: Zu schwach geschossen: Der Ball verhungert auf dem Weg zum Tor.

Abbildung 4: Zu schwach geschossen: Der Ball verhungert auf dem Weg zum Tor.

Abbildung 5: Zu kurz geschossen: Der Torwart f&auml;ngt den Ball. Game over!

Abbildung 5: Zu kurz geschossen: Der Torwart fängt den Ball. Game over!

Simple Videospiele dieser Art hielten in den 1980-ern erstmals Einzug in sogenannte Spielhallen. Dort standen Riesenkästen mit eingebauten Bildschirmen, in deren Münzschlitze User Kleingeld steckten, um das eingebaute Spiel mittels Joystick und Feuerknopf ein paar Minuten traktieren zu dürfen. Die Konzepte solcher Spiele und das Erstellen von Programmen, die sie ermöglichen, beschreibt das Buch “Classic Game Design” von Franz Lanzinger [3], einem Pionier der Technik.

Lanzinger hat laut eigener Aussage einmal auf der Touristenmeile der kalifornischen Stadt Santa Cruz einen Automaten mit dem damals populären Videospiel “Crystal Castles” [4] entdeckt. Mithilfe der ihm bekannten Kombination der beiden Feuerknöpfe fand er heraus, dass die Besucher des Vergnügungsparks während der Lebensdauer des Automaten sage und schreibe 100 000 Quarters (Vierteldollarmünzen) hineingesteckt hatten. Hochgerechnet auf die damals produzierte Stückzahl von 5000 Automaten ergeben sich daraus (recht optimistisch geschätzte) Gesamteinnahmen des Spiels von 125 Millionen Dollar.

Von Bild zu Bild

Alle Videospiele haben gemein, dass der Rechner die mehr oder weniger flüssig dargestellten Bewegungen mehrmals pro Sekunde in sogenannten Frames ausrechnet und anzeigt. Videospiele basieren meist auf vorgefertigten Engines, die die Darstellung übernehmen. Sie gewähren der Applikation Zugriff auf das Spielgeschehen, in dem sie zu jedem Frame eine Callback-Funktion anspringen, in der der Spieleprogrammierer dann seine Spielfiguren voranschiebt oder prüft, ob sie mit aufgestellten Hindernissen kollidieren.

Ein Mensch, dessen Gehirn scheinbar spielerisch den Überblick über eine komplexe Situation auf dem Bildschirm behält, bekommt sofort mit, wenn sich der Ball auf dem Videospielfeld dem Torwart oder dem Tor nähert. Ein Programm ist hingegen darauf angewiesen, in jedem Spiel-Frame wieder und wieder zu testen, ob der Ball tatsächlich schon an einem der überwachten Objekte angeschlagen ist. Das Programm kann das rasend schnell, und deswegen sieht es so aus, als besäße es eine ähnliche Mustererkennung wie der Mensch, doch das Verfahren basiert auf einer Illusion.

Ganz großes Tennis

Das unten vorgestellte Video-Spiel Chipshot besteht aus reinem Go-Code; als Grafik-Library dient das plattformunabhängige Fyne [5], das schon im letzten Snapshot mit einem Fotosortierer auftrumpfte [6]. Die Abbildungen 3 bis**5 zeigen das Spiel in Aktion. Der User stellt über die beiden weißen Regler oben links die Startgeschwindigkeit des Balles zwischen 0 und 30 ein, den Anstellwinkel setzt er auf einen Wert zwischen 0 und 90 Grad. Klickt er dann mit der Maus auf den Shoot-Button links oben, fängt der Ball auf seiner parabelartigen Bahn an zu fliegen.

Sobald das Leder wieder auf dem Boden aufschlägt, rollt es noch eine Zeit lang aus und kullert mit etwas Glück ins Tor hinein, was den Zähler Goals oben um eins erhöht. Kommt der Ball allerdings schon vor dem Torwart herunter, fängt der ihn ab und lacht schadenfroh, weil es dafür keinen Punkt gibt. Dasselbe gilt, falls der Angreifer zu stark draufballert oder zu wenig Schmackes gibt, sodass der Ball übers Tor fliegt beziehungsweise auf dem Weg dorthin verhungert.

Punktet der Spieler, erhöht sich der Torzähler, und das Spiel erzeugt eine neue Situation, indem es Torwart und Tor umstellt. Versagt der Angreifer und der Ball geht nicht ins Tor, darf der Spieler dieselbe Situation noch einmal mit unterschiedlichen Reglereinstellungen probieren, aber das Spiel stellt den Torzähler zur Strafe auf null zurück.

Programmierter Zufall

Listing 3 baut das Spiel auf. Damit nach einem Neustart des Programms nicht immer dieselbe Ausgangsposition hochkommt, setzt Zeile 37 mit »rand.Seed()« den Go-internen Zufallsgenerator auf einen Wert, der mit den Nanosekunden der aktuellen Uhrzeit gefüttert ziemlich weit gestreute Ausgangsdaten liefert.

Listing 3

chipshot.go

package main
import (
  "fmt"
  col "golang.org/x/image/colornames"
  "image/color"
  "math/rand"
  "os"
  "time"
  "fyne.io/fyne/v2"
  "fyne.io/fyne/v2/app"
  "fyne.io/fyne/v2/canvas"
  "fyne.io/fyne/v2/container"
  "fyne.io/fyne/v2/data/binding"
  "fyne.io/fyne/v2/widget"
)
type UI struct {
  ball, goal, goalText, goalie,
  goalieText fyne.CanvasObject
}
var (
  gameWidth  = float32(1200)
  gameHeight = float32(800)
  goalWidth  = float32(30)
  goalHeight = float32(60)
  minDist    = 30
  textHover  = float32(50)
)
func main() {
  a := app.New()
  ui := UI{}
  rand.Seed(time.Now().UnixNano())
  w := a.NewWindow("Chipshot")
  w.Resize(fyne.NewSize(gameWidth, gameHeight))
  w.SetFixedSize(true)
  goalieDist, goalDist := itemsXPos()
  ui.goalie = canvas.NewRectangle(col.Lightsalmon)
  ui.goalie.Move(fyne.NewPos(goalieDist, gameHeight-goalHeight))
  ui.goalie.Resize(fyne.NewSize(goalWidth, goalHeight))
  ui.goal = canvas.NewRectangle(col.Lightgreen)
  ui.goal.Move(fyne.NewPos(goalDist, gameHeight-goalHeight))
  ui.goal.Resize(fyne.NewSize(goalWidth, goalHeight))
  ui.ball = canvas.NewCircle(col.Red)
  ui.ball.Move(fyne.NewPos(0, gameHeight-30))
  ui.ball.Resize(fyne.NewSize(15, 30))
  ui.goalText = canvas.NewText("Goal!!!", col.Red)
  placeTextHover(ui.goalText, ui.goal)
  ui.goalieText = canvas.NewText("Caught it!!!", col.Red)
  placeTextHover(ui.goalieText, ui.goalie)
  play := container.NewWithoutLayout(ui.goal, ui.goalie, ui.ball, ui.goalText, ui.goalieText)
  velo := binding.NewFloat()
  veloSlide := widget.NewSliderWithData(0, 30, velo)
  formVelo := binding.FloatToStringWithFormat(velo, "Velocity: %0.2f")
  veloLabel := widget.NewLabelWithData(formVelo)
  veloSlide.SetValue(15)
  angle := binding.NewFloat()
  angleSlide := widget.NewSliderWithData(0, 90, angle)
  formAngle := binding.FloatToStringWithFormat(angle, "Angle: %0.2f°")
  angleLabel := widget.NewLabelWithData(formAngle)
  angleSlide.SetValue(45)
  countText := canvas.NewText("Goals: 0", &color.Black)
  count := 0
  shoot := widget.NewButton("Shoot", func() {
    v, _ := velo.Get()
    a, _ := angle.Get()
    success := animate(v, a, ui)
    if success {
      count++
      goalieDist, goalDist := itemsXPos()
      ui.goalie.Move(fyne.NewPos(goalieDist, gameHeight-goalHeight))
      ui.goal.Move(fyne.NewPos(goalDist, gameHeight-goalHeight))
      placeTextHover(ui.goalieText, ui.goalie)
      placeTextHover(ui.goalText, ui.goal)
    } else {
      count = 0
    }
    countText.Text = fmt.Sprintf("Goals: %d", count)
    countText.Refresh()
    // return ball to origin
    ui.ball.Move(fyne.NewPos(0, gameHeight-30))
  })
  quit := widget.NewButton("Quit",
    func() { os.Exit(0) })
  buttons := container.NewHBox(shoot, quit, countText)
  con := container.NewVBox(play, buttons, veloSlide,
    veloLabel, angleSlide, angleLabel)
  w.SetContent(con)
  w.ShowAndRun()
}
func randRange(from, to int) float32 {
  return float32(rand.Intn(to-from+1) + from)
}
func itemsXPos() (float32, float32) {
  d1 := randRange(minDist, 2*int(gameWidth)/3)
  d2 := randRange(int(d1)+minDist, int(gameWidth-goalWidth))
  return d1, d2
}

Die verwendeten UI-Elemente definiert die Struktur vom Typ »UI« in Zeile 17. In ihr finden der Fußball, das Tor, der Torwart sowie die über dem Tor beziehungsweise Torwart dargestellten Texte Platz. Die globalen Variablen im Block ab Zeile 22 legen die Dimensionen des Spielfelds und der Spielfiguren fest. Die Hauptfunktion »main()« ab Zeile 31 erzeugt erst eine neue Fyne-Applikation. Dann definiert sie ein Fenster und setzt es auf eine fixe Größe. Tor und Torwart stellt sie durch Fyne-Rechtecke in den Farben Hellgrün und Lachs dar.

Die Funktion »itemsXPos()« ab Zeile 97 liefert die Positionen für Tor und Torwart, sowohl beim Programmstart als auch nach dem Meistern einer Standardsituation. Sie stellt sicher, dass keine unsinnigen Konstellationen entstehen, etwa, dass der Torwart hinter dem Tor steht.

Wenn der Torwart einen Ball abfängt oder es im Tor schnackelt, kommt über diesen Spielfiguren eine Schrift hoch, die das Ereignis dreimal blinkend meldet. Die zugehörigen grafischen Widgets definieren Zeile 48 und Zeile 50. Die Funktion »placeTextHover()« (später in Listing 4 definiert) stellt jedoch anfangs sicher, dass sie sich zunächst mit »Hide()« verstecken. Erst später sorgt die Funktion »blink()« bei Bedarf dafür, dass sie ihren Text mehrmals aufblitzen lassen.

Der Ball liegt als Kreis-Widget vor, das Zeile 45 anlegt und mit roter Farbe füllt. Das virtuelle Leder startet an der X-Position 0, also am linken Spielfeldrand. Alle diese Widgets verpackt Zeile 52 in den Spielfeld-Container »play«, den später Zeile 87 unter die Knöpfe und Regler platziert, mit denen der User das Spielgeschehen beeinflusst. Die beiden Regler »velo« und »angle« lassen sich mit der Maus verschieben, und durch Fynes Binding-Schnittstelle zeigen sie den jeweils eingestellten Wert ohne Zeitverzug im zugehörigen Label an. Praktisch!

Der Button Shoot löst einen Schuss aus, und zwar mit den in den Reglern vorgegebenen Werten für Geschwindigkeit und Abschusswinkel des Balls. Die zugehörige Callback-Funktion ab Zeile 65 liest erst die eingestellten Reglerwerte aus und ruft dann die Funktion »animate()« aus Listing 4 auf. Die zeichnet den Ballverlauf ins Spielfeld ein und gibt den Wert »true« zurück, falls der Ball im Tor landet. Ist er verhungert oder hat der Torhüter ihn abgefangen, kommt der Wert »false« zurück.

Im Erfolgsfall erhöht sich der Torzähler »count« um eins, und »itemsXPos()« knobelt eine neue Spielsituation aus. Die Fyne-Funktion »Move()« stellt die Widgets für Tor und Torwart (englisch “Goal” und “Goalie”) auf die neuen Positionen ein, und auch die zugehörigen Texttafeln wandern mit. Wie bei allen Änderungen an UI-Widgets passt erst ein nachfolgender Aufruf von »Refresh()« das Spielfeld entsprechend an.

Wie in grafischen Anwendungen üblich, definiert das Hauptprogramm zuerst alle möglichen Reaktionen auf Benutzereingaben und tritt dann in Zeile 90 mit »ShowAndRun()« in die endlos währende Haupt-Event-Schleife ein. Klickt der User den Quit-Button, läutet »os.Exit()« das Programmende ein, und die UI fällt sang- und klanglos in sich zusammen.

Action!

Was nun auf der Spielfläche passiert, wenn der User Shoot anklickt, definiert Listing 4 mit der Funktion »animate()«. Mit der Anfangsgeschwindigkeit des Balls (»velo«) sowie dem Abschusswinkel »angle« in Grad zeichnet sie die Flugbahn in den Spiel-Container ein und wertet eventuelle Kollisionen des Balls mit dem Torwart oder dem Tor anhand derer aktueller Positionen aus.

Listing 4

animate.go

package main
import (
  "math"
  "time"
  "fyne.io/fyne/v2"
  "fyne.io/fyne/v2/canvas"
)
func animate(velo float64, angle float64, ui UI) bool {
  nap := 10 // ms
  now := 0
  angle = math.Pi * angle / 180 // radient
  rollout := 20
  for {
    pos := ui.ball.Position()
    x, y := chipShot(velo, angle, float64(now)/100)
    if y == 0 {
      rollout--
      if rollout < 0 {
        break
      }
    }
    goalYOff := float32(30)
    pos.X = float32(x) * 20
    pos.Y = gameHeight - goalYOff - float32(y)*100
    ui.ball.Move(pos)
    canvas.Refresh(ui.ball)
    // goal?
    if pos.X >= ui.goal.Position().X &&
      pos.X <= ui.goal.Position().X+ui.goal.Size().Width &&
      pos.Y > gameHeight-goalYOff-ui.goal.Size().Height {
      go blink(ui.goalText)
      return true
    }
    // goalie?
    if pos.X >= ui.goalie.Position().X &&
      pos.X <= ui.goalie.Position().X+ui.goalie.Size().Width &&
      pos.Y > gameHeight-goalYOff-ui.goalie.Size().Height {
      go blink(ui.goalieText)
      break
    }
    time.Sleep(time.Duration(nap) * time.Millisecond * time.Duration(nap))
    now += nap
  }
  return false
}
func blink(tw fyne.CanvasObject) {
  for i := 0; i < 3; i++ {
    tw.Show()
    canvas.Refresh(tw)
    time.Sleep(250 * time.Millisecond)
    tw.Hide()
    canvas.Refresh(tw)
    time.Sleep(250 * time.Millisecond)
  }
}
func placeTextHover(tw, w fyne.CanvasObject) {
  textPos := w.Position()
  textPos.Y = textPos.Y - textHover
  tw.Move(textPos)
  tw.Hide()
}

Dazu wandelt Zeile 12 den vom Regler kommenden Gradwert des Abschusswinkels ins Radiant-Format um. Die Endlosschleife ab Zeile 14 arbeitet die Animation des Videospiels ab, indem sie einzelne aufeinanderfolgende Frames im zeitlichen Abstand von 10 Millisekunden berechnet. Dabei bestimmt der Aufruf der Funktion »chipShot()« (aus Listing 1) in Zeile 16 die zum aktuellen Frame am Zeitwert »now« gehörende Ballposition auf der Parabelbahn als X- und Y-Werte. Das erfolgt 100 Mal pro Sekunde, damit die Anzeige nicht ruckelt. Die Zeilen 26 und 27 frischen bei jedem durchlaufenen Frame die Ballposition auf; mehr bewegt sich während der Flugphase nicht auf dem Spielfeld.

Beendet der Ball seine Flugbahn und kehrt wieder zur Erde zurück, liefert die Physik-Funktion »chipShot()« einen Y-Wert von null, und – als vereinfachte Näherung – lässt die Variable »rollout« den Ball noch 20 Frames am Boden weiterrollen. In Wirklichkeit spränge er zurück in die Luft und würde erst nach ein paar Hopsern entsprechend der Bodenreibung ausrollen, aber das vernachlässigt das Programm, damit die Formel einfach bleibt.

Etwaige Kollisionen des Balls mit dem Tor oder dem Torwart berechnen die If-Konstrukte der Zeilen 29 und 36. Sie prüfen, ob sich die aktuelle Ballposition irgendwo innerhalb der geometrischen Koordinaten von Tor oder Torwart befindet. Sie melden mit blinkendem Text einen Treffer, falls der Ball ins Tor rollt, oder aktivieren die Torwartmeldung, falls der Keeper ihn sich geschnappt hat.

Bevor die For-Schleife in die nächste Runde geht, schläft Zeile 42 zehn Millisekunden und zählt das Nickerchen zur aktuellen Zeit in »now« hinzu. Dann geht es weiter zum nächsten Frame. Die blinkende Anzeige eines Tors oder Torwarterfolgs erledigt die Funktion »blink()« ab Zeile 48. Sie wird jeweils mit »go« aus »animate()« aufgerufen, damit sie nicht den laufenden Betrieb aufhält, sondern im Hintergrund läuft, während das Hauptprogramm sich weiter um Benutzereingaben kümmern kann.

Das Binary »chipshot« entsteht, wie unter Go üblich, mit der Sequenz aus Listing 5. Dabei löst der Compiler zunächst die im Code verwendeten Pakete und deren Abhängigkeiten auf.

Listing 5

Binary erstellen

$ go mod init chipshot
$ go mod tidy
$ go build chipshot.go animate.go physics.go
$ ./chipshot

Der Rat vom Coach

Noch einige Tipps an die aufstrebende Fußballjugend: Steht der Torwart weit vor dem Tor und schon fast vor dem Angreifer, führt nur ein steil hochgeschossener Ball (etwa 60 Grad) an ihm vorbei. In dem Fall braucht der Schuss Schmackes, damit der Ball auf seiner steilen Bahn auch erst kurz vor dem Tor wieder herunterkommt und hoffentlich hineinkullert. In Spielsituationen, in denen der Torwart nur mäßig weit herausgelaufen ist, tut es oft ein Schuss mit 45 Grad Anstellwinkel und mäßiger Geschwindigkeit. Und wie immer hilft: trainieren, trainieren, trainieren!

Der Autor

Michael Schilli arbeitet als Software Engineer in der San Francisco Bay Area in Kalifornien. In seiner seit 1997 laufenden Kolumne forscht er jeden Monat nach praktischen Anwendungen verschiedener Programmiersprachen. Unter mailto:mschilli@perlmeister.com beantwortet er gern Ihre Fragen.

Infos

  1. Schiefer Wurf: https://de.wikipedia.org/wiki/Wurfparabel
  2. Bogenlampe: https://de.wikipedia.org/wiki/Bogenlampe_(Sport)
  3. “Classic Game Design: From Pong to Pac-Man with Unity”: https://www.amazon.com/dp/B07S3ZW1Z8
  4. “Crystal Castles”: https://en.wikipedia.org/wiki/Crystal_Castles_(video_game)
  5. Fyne-Projekt: https://fyne.io
  6. Snapshot: Mike Schilli, “Die Schlechten ins Kröpfchen”, LM 11/2021, S. 86, https://www.lm-online.de/44800
  7. Listings zu diesem Artikel: http://www.linux-magazin.de/static/listings/magazin/2021/11/snapshot/
DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 7 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben