How-To 'Moorhuhn'-Klon in Godot Part 4

Highscores und UI-Elemente

Punkte zählen und abspeichern

Im letzten Part haben wir unsere Moorschweine bereit gemacht und eine neue Levelszene erstellt, die uns periodisch neue Ziele vor die Flinte wirft. Damit jetzt aber auch langsam die Highscorejagd beginnen kann, sollten wir dafür sorgen, dass überhaupt einmal Punkte gezählt werden! Gut, dass wir in den vorherigen Parts bereits eine Variable für die Punkte eines Moorschweins angelegt haben und diese auch bereits für die jeweiligen Größen im Level setzen. Was uns allerdings noch fehlt ist eine zentrale Stelle, die die Punkte zusammen zählt sobald ein Moorschwein getroffen wurde. Dafür bietet sich unser Levelskript an, dass ja auch unsere Schweine spawned. Zuvor möchte ich allerdings erst unser Moorschwein Skript anpassen, so dass wir auch mitgeteilt bekommen, wann der Spieler sich Punkte verdient hat.

Öffnet die Moorschwein.gd und fügt nach der ersten Zeile folgendes ein:

1
signal killed(points)

Mit dieser Zeile definieren wir ein neues Signal für unser Moorschwein. Durch die Angabe (points) teilen wir Godot mit, dass das Signal einen Parameter übergibt. Sobald ihr speichert und die Moorschwein Szene öffnet sollte euch das Signal bei den Signalen im Inspektor angezeigt werden. Jetzt könnten wir es wie bereits zuvor die Signale der Area2D verwenden. Dazu aber aber später mehr.

Jetzt sorgen wir erst einmal dafür, dass das Signal ausgelöst wird sobald unser Schwein stirbt. Da unser Schwein auch stirbt, sobald es den Bildschirm links oder rechts verlässt müssen wir unsere kill Methode so anpassen, dass sie das Signal nur sendet, wenn das Moorschwein auch wirklich erschossen wurde. Passen wir unsere Methoden also dementsprechend an:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func _physics_process(delta:float) -> void:
m_start += delta
position.x += delta * HORIZONTAL_SPEED
position.y = sin((m_start)*VERTICAL_SPEED)*AMPLITUDE + m_start_position.y
if global_position.x > get_viewport_rect().size.x:
kill(false)
if global_position.x < 0:
kill(false)


func kill(grant_points: bool) -> void:
if grant_points:
emit_signal("killed",POINTS)
queue_free()

func _on_MoorSchwein_input_event(viewport, event, shape_idx) -> void:
if event is InputEventMouseButton and event.is_pressed() and event.button_index == BUTTON_LEFT:
kill(true)

Wir fügen unserer kill Methode einen neuen booleschen Parameter hinzu. Hat dieser den Wert “true” senden wir das Signal mit dem Befehl emit_signal. Diese erwartet als ersten Parameter den Namen des Signals, dass gesendet werden soll und übergibt allen Empfängern des Signals die darauf folgenden Parameter, also unsere Punktzahl. Dann müssen wir nur noch noch die Methodenaufrufe dementsprechend anpassen.

Damit ist alles bereit um unsere Punkte zu zählen!

Wechseln wir also in unser Levelskript und legen dort eine neue Variable an:

1
var mPoints : int = 0

Diese initialisieren wir direkt mit dem Wert 0. Als nächstes erstellen wir eine neue Funktion, die wir mit unserem im Moorschwein erzeugten Signal verbinden wollen:

1
2
3
func _on_pig_killed(points : int) -> void:
mPoints += points
print(mPoints)

Diese erhält als Parameter die Punktzahl für ein Moorschwein und addiert diese zur Gesamtpunktzahl hinzu. Damit wir sehen, dass es funktioniert geben wir die Punktzahl mit dem Printbefehl in der Ausgabe aus. Jetzt stellt sich nur noch die Frage, wie wir unsere Methode mit dem Signal für jedes einzelne Schwein verbinden. Denken wir zurück an die Area2D, so haben wir Signal über den Inspektor verbunden. Dazu würden wir allerdings eine Instanz des Moorschweins in unserem Leveltree benötigen. Außerdem wäre die Verbindung dann auch nur für genau diese Instanz unseres Moorschweins, dabei erzeugen wir doch dynamisch ganz viele von den kleinen Rackern. Gut, dass wir genauso einfach wie wir Moorschweine dynamisch erzeugen auch ihre Signale via Code verbinden können. fügt dafür folgende Zeile in der Funktion _build_pig vor den returnbefehl ein:

1
pig.connect("killed", self, "_on_pig_killed")

Mit der Methode Connect können wir unserem Moorschwein mitteilen, dass wir auf ein Signal reagieren wollen. Dafür brauch es folgende Parameter:

  • Der Name des Signals (hier “killed”)
  • Die Zielnode (Da wir im Level darauf reagieren wollen referenzieren wir dieses hier selbst)
  • Der Name der Methode, die von der Zielnode aufgerufen werden soll (_on_pig_killed)

Damit sollten wir jetzt sehen, wie sich unsere Punktzahl im Spiel erhöht sobald wir ein Schwein treffen. Testet es ruhig einmal aus und achtet nach einem Treffer auf das Ausgabenfenster.

Soweit so gut. Unser Spieler hat dieses Ausgabenfenster im fertigen Spiel allerdings nicht, also sorgen wir einmal dafür, dass unser Punktestand auch im Spiel angezeigt wird.

Unsere UI

Legen wir eine neue Szene an und verwenden diesmal als Wurzel ein Control Node. Nennt diese UI und speichert die Szene in unserem Ordner “gui” unter “gameobjects”. Als nächstes sorgen wir dafür, dass unsere UI sich über den ganzen Bildschirm erstreckt. Wählt dazu unsere Wurzel aus und klickt auf den Punkt Layout im 2D Bereich. Im sich öffnenden Kontextmenü wählen wir “Full Rect”.

Um Anzeigeelemente auf dem Bildschirm anzuordnen, bietet Godot eine Menge an Containern. Ein HBoxContainer zum Beispiel ordnet alle seine Kindelemente horizontal nebeneinander an. Der VBoxContainer tut dasselbe nur vertikal. Wir brauchen für unsere UI als erstes einen MarginContainer. Dieser versetzt deine Kindelemente um einen eingestellten Wert. Darin erzeugen wir einen HBoxContainer, der wiederum zwei Labels beinhaltet. Diese nennen wir PointsLabel und Points.
Euer Baum sollte wie folgt aussehen:

Widmen wir uns nun den Optionen im Inspektor. Wählt zuerst unsere Wurzel aus und öffnet dann im Inspektor den Bereich “Mouse”. Wählt dort als Filter “Ignore”. Damit sorgen wir dafür, dass unsere UI Ebene Mausklicks ignoriert, aber unsere Moorschweine immer noch angeklickt werden können.
Im MarginContainer vergeben wir unser “Custom Constants” für jedes der 4 “Margins” den Wert 25.
Im HBoxContainer stellen wir wieder unter “Custom Constants” eine “Separation” von 5 ein. dadurch erhöhen wir den Abstand zwischen den Elementen in der HBox. Damit wir auch mal was sehen kommen wir zu unseren beiden Labels. Tragt in den Bereich “Text” des Labels “Pointslabel” “Punkte:” ein. Beim anderen Label könnt ihr eine beliebige Zahl eintragen um auszuprobieren, wie das ganze im fertigen Zustand aussehen wird. Leider ist der Standardfont recht klein und gibt auch nicht so viel her. Deswegen sollten wir das ganze ein wenig aufhübschen. Dazu suchen wir uns im Internet ersteinmal einen Font der uns gefällt. Ich habe mich für den Dokto Font entschieden. Ihr solltet nun eine .ttf Datei zur Verfügung haben. Zieht diese im Dateisystem einfach in den Ordner assets.

Sobald wir das erledigt haben, sollten wir den Font unseren Labeln hinzufügen. Wählt wieder “PointsLabel” aus und klickt im Inspektor unter “Custom Fonts” auf den Bereich Fonts und wählt “Neues DynamicFont”
Zieht jetzt eure Fontdatei in den Bereich “Font Data” unter dem Reiter Font. Die Schrift unseres Labels sollte sich anpassen. Um die Schrift zu vergrößern müssen wir den Wert “Size” unter “Settings” anpassen. Ich habe diesen auf 36 gesetzt. Passt das ganze so an wie es euch am Besten gefällt. Da wir für unser anderes Label die gleichen Einstellungen verwenden wollen, lohnt es sich den DynamicFont abzuspeichern. Klickt dafür einfach noch einmal auf den Punkt “Font” unter “Custom Fonts” und wählt “Speichern”. Speichert den Font als “LabelFont.tres” in eurem “assets” Ordner. Um diesen jetzt dem Points Label hinzu zufügen, zieht ihr die .tres Datei einfach auf den “Font” in “CustomFonts” des “Pointslabels”.

Damit ist unsere UI fast funktionstüchtig. Was jetzt noch fehlt, ist, dass wir die Punkte aktualisieren, sobald diese sich im Level verändern. Wählt dafür das Wurzelnode aus und fügt ein neues Skript hinzu. Dieses Skript sollte folgendermaßen aussehen:

1
2
3
4
5
6
7
8
9
extends Control

var mPoints : Label

func _ready():
mPoints = $MarginContainer/HBoxContainer/Points

func update_points(points: int) -> void:
mPoints.text = String(points)

Zu Beginn merken wir uns unsere Points-Node. Um eine Node in Godot anzusprechen gibt es die Methode get_node(). Diese lässt sich durch das Zeichen $ gefolgt von dem Pfad zur Node abkürzen. Der Pfad ist hierbei relativ zu unserer aktuellen Position. Da wir uns im Wurzelverzeichnis befinden, müssen wir uns durch die Container zum Label navigieren.

Dann haben wir noch die Methode update_points. Diese erhält die aktuelle Punktzahl, wandelt diese in einen String und schreibt sie in das Textattribut unseres Labels. Damit ist unsere UI erst einmal fertig. Jetzt müssen wir sie nur noch unserem Level hinzufügen und unser Levelskript ein wenig anpassen.

Wechseln wir in die Levelszene und fügen dort einen CanvasLayer hinzu. Der CanvasLayer wird immer statisch gezeichnet. Ohne diesen würde unsere UI vom Bildschirm verschwinden, wenn sich der Bildabschnitt bewegt. In unserem Spiel haben wir aktuell zwar eine feste Kameraposition, aber das könnte sich ja noch ändern. Fügt nun unter den CanvasLayer unsere UI ein und öffnet dann das Levelskript. Hier müssen wir dank unserer Vorarbeit nicht mehr all zu viel machen. Wir müssen nur noch die Update_points Methode unserer UI an den richtigen Stellen aufrufen.

Zum Einen ändern in der _ready Methode um unsere Punktzahl zum Start des Levels auf 0 zu setzen:

1
2
func _ready():
$CanvasLayer/UI.update_points(mPoints)

Und dann nochmal in der _on_pig_killed Methode. Ich habe direkt unseren print Aufruf ersetzt:

1
2
3
func _on_pig_killed(points : int) -> void:
mPoints += points
$CanvasLayer/UI.update_points(mPoints)

Und das war es auch schon. Wenn wir unser Level jetzt starten sehen wir oben links unseren Punktestand, welcher sich erhöht wenn wir ein Moorschwein treffen.

Damit beende ich diesen Part. Solltet Ihr Probleme haben schaut doch mal im Git-Repository vorbei.