Work in progress

The content of this page was not yet updated for Godot 4.2 and may be outdated. If you know how to improve this page or you can confirm that it's up to date, feel free to open a pull request.

Punktestand und Neustart

In diesem Teil werden wir einen Punktestand, Hintergrundmusik und die Möglichkeit, das Spiel neu zu starten, hinzufügen.

Wir müssen den aktuellen Punktestand in einer Variablen speichern und ihn mit einer minimalen Benutzeroberfläche auf dem Bilschirm anzeigen. Wir werden ein Text-Label dafür benutzen.

Fügen Sie in der Main-Szene einen neuen Child-Node Control zu Main hinzu und nennen Sie ihn UserInterface. Sie werden automatisch zum 2D-Bildschirm weitergeleitet, wo Sie Ihre Benutzeroberfläche (User Interface oder UI) bearbeiten können.

Fügen Sie einen Label-Node hinzu und nenne ihn ScoreLabel

image1

Legen Sie im Inspektor den Text des Labels auf einen Platzhalter fest, wie "Score: 0".

image2

Außerdem ist der Text standardmäßig weiß, so wie der Hintergrund unseres Spiels. Wir müssen seine Farbe ändern, um ihn im Spiel sehen zu können.

Scrollen Sie nach unten zu Theme-Überschreibungen, erweitern Sie Colors und aktivieren Sie Font Color, um den Text schwarz zu färben (was gut mit der weißen 3D-Szene kontrastiert)

image3

Klicken Sie schließlich auf den Text im Viewport und ziehen Sie ihn, um ihn von der linken oberen Ecke zu entfernen.

image4

Der UserInterface-Node erlaubt es uns, unsere Benutzeroberfläche in einem Zweig des Szenenbaums zu gruppieren und eine Theme-Ressource zu verwenden, die sich auf alle seine Child-Elemente überträgt. Wir werden ihn benutzen, um die Schriftart unseres Spiels festzulegen.

Ein UI-Theme erstellen

Wählen Sie erneut den UserInterface-Node. Im Inspektor erstellen Sie eine neue Theme-Ressource unter Theme -> Theme.

image5

Klicken Sie auf die neu erstellte Ressource, um den Theme-Editor im Unteren Bedienfeld zu öffnen. Er gibt Ihnen eine Vorschau, wie alle Built-in-UI-Widgets mit Ihrer Theme-Ressource aussehen werden.

|image6|

Standardmäßig hat ein Theme nur eine Eigenschaft, die Default-Schriftart.

Siehe auch

Sie können in der Theme-Ressource mehr Eigenschaften hinzufügen, um komplexe Benutzeroberflächen zu gestalten, aber das würde den Rahmen dieser Artikelreihe sprengen. Um mehr über das Erstellen und Bearbeiten von Themes zu erfahren, siehe Einführung in GUI-Skinning.

Diese erwartet eine Schriftartdatei, wie solche, die Sie auf Ihrem Computer haben. Zwei gängige Schriftdateiformate sind TrueType Font (TTF) und OpenType Font (OTF).

Erweitern Sie im Dateisystem-Dock das fonts-Verzeichnis und ziehen Sie die Datei Montserrat-Medium.ttf, die wir in das Projekt aufgenommen haben, auf die Default-Schriftart. Der Text wird in der Vorschau des Themes wieder erscheinen.

Der Text ist ein wenig klein. Setzen Sie die Default-Schriftgröße auf 22 Pixel, um den Text zu vergrößern.

|image7|

Den Spielstand im Auge behalten

Lassen Sie uns als nächstes mit dem Punktestand arbeiten. Hängen Sie ein neues Skript an das ScoreLabel und definieren Sie die Variable score.

extends Label

var score = 0

Die Punktzahl sollte sich jedes Mal um 1 erhöhen, wenn wir ein Monster zerstampfen. Wir können das Signal squashed benutzen, um mitzubekommen, wann das passiert. Da wir jedoch Monster im Code instanziieren, können wir das Mob-Signal nicht über den Editor mit dem ScoreLabel verbinden.

Stattdessen müssen wir jedes Mal, wenn ein Monster gespawnt wird, eine neue Verbindung über Code erstellen.

Öffnen Sie das Skript main.gd. Wenn es noch geöffnet ist, können Sie auf seinen Namen in der linken Spalte des Skripteditors klicken.

|image8|

Alternativ können Sie auf die Datei main.gd im Dateisystem-Dock doppelklicken.

Fügen Sie am Ende der Funktion _on_mob_timer_timeout() die folgende Zeile hinzu:

func _on_mob_timer_timeout():
    #...
    # We connect the mob to the score label to update the score upon squashing one.
    mob.squashed.connect($UserInterface/ScoreLabel._on_mob_squashed.bind())

Diese Zeile bedeutet, dass wenn der Mob das squashed-Signal sendet, der ScoreLabel-Node es empfängt und die Funktion _on_mob_squashed() aufruft.

Gehen Sie zurück zum Skript ScoreLabel.gd, um die Callback-Funktion _on_mob_squashed() zu definieren.

Dort wird die Punktzahl erhöht und der angezeigte Text aktualisiert.

func _on_mob_squashed():
    score += 1
    text = "Score: %s" % score

Die zweite Zeile verwendet den Wert der Variable score, um den Platzhalter %s zu ersetzen. Wenn Sie dieses Feature verwenden, wandelt Godot die Werte automatisch in String-Text um, was bei der Ausgabe von Text in Labels oder bei der Verwendung der Funktion print() praktisch ist.

Siehe auch

Mehr über die String-Formatierung erfahren Sie hier: GDScript Format-Strings. In C# können Sie die String-Interpolation mit "$" verwenden.

Sie können nun das Spiel testen und ein paar Gegner zerstampfen, um zu sehen wie die Punktzahl ansteigt.

|image9|

Bemerkung

In einem komplexen Spiel möchten Sie vielleicht die Benutzeroberfläche vollständig von der Spielwelt trennen. In diesem Fall würden Sie die Punktzahl nicht im Label aufbewahren. Stattdessen sollten Sie sie in einem seperaten dedizierten Objekt speichern. Beim Prototyping oder wenn ihr Projekt simpel ist, ist es in Ordnung, den Code auch simpel zu halten. Programmieren ist immer ein Balanceakt.

Einen neuen Versuch wagen

Wir werden nun die Möglichkeit hinzufügen, nach dem Tod das Spiel neu zu starten. Wenn der Spieler stirbt, werden wir eine Nachricht auf dem Bildschirm anzeigen und auf eine Eingabe warten.

Gehen Sie zurück zur Szene main.tscn, wählen Sie den UserInterface-Node, fügen einen Child-Node ColorRect hinzu und nennen ihn Retry. Dieser Node füllt ein Rechteck mit einer einheitlichen Farbe und dient als Overlay, um den Bildschirm zu verdunkeln.

Um ihn über das gesamte Ansichtsfenster zu legen, können Sie das Menü Anker-Vorgaben in der Toolbar verwenden.

|image10|

Öffnen Sie es und wenden den Befehl Vollständiges Rechteck an.

|image11|

Es passiert nichts. Nun, fast nichts; nur die vier grünen Stifte bewegen sich zu den Ecken des Auswahlfeldes.

|image12|

Das liegt daran, dass UI-Nodes (alle mit einem grünen Icon) mit Ankern und Rändern relativ zur Bounding Box ihres Parents arbeiten. Hier hat der UserInterface-Node eine kleine Größe und der Retry-Node ist dadurch begrenzt.

Wählen Sie das UserInterface und wenden Sie Anker-Vorgaben -> Vollständiges Rechteck darauf an. Der Retry-Node sollte sich nun über den gesamten Viewport erstrecken.

Ändern wir seine Farbe so, dass er den Spielbereich verdunkelt. Wählen Sie Retry und setzen Sie im Inspektor seine Farbe auf etwas Dunkles und Transparentes. Ziehen Sie dazu in der Farbauswahl den Schieberegler A nach links. Er steuert den Alphakanal der Farbe, d.h. ihre Deckkraft/Transparenz.

|image13|

Als nächstes fügen Sie ein Label als Child von Retry hinzu und geben ihm den Text "Press Enter to retry.". Um es zu verschieben und in der Mitte des Bildschirms zu verankern, wenden Sie Anker-Vorgaben -> Mitte auf es an.

|image14|

Die Neustart-Option programmieren

Wir können nun den Code zum Ein- und Ausblenden des Retry-Nodes verwenden, wenn der Spieler stirbt und neu beginnt.

Öffnen Sie das Skript main.gd. Zuerst wollen wir das Overlay zu Beginn des Spiels ausblenden. Fügen Sie diese Zeile in die Funktion _ready() ein.

func _ready():
    $UserInterface/Retry.hide()

Wenn der Spieler getroffen wird, zeigen wir das Overlay.

func _on_player_hit():
    #...
    $UserInterface/Retry.show()

Schließlich, wenn der Retry-Node sichtbar ist, müssen wir auf die Eingaben des Spielers reagieren und das Spiel neu starten, wenn er Enter drückt. Um dies zu tun, benutzen wir den Built-in-_unhandled_input()-Callback, der bei jeder Eingabe ausgelöst wird.

Wenn der Spieler die vordefinierte ui_accept-Eingabeaktion gedrückt hat und Retry sichtbar ist, laden wir die aktuelle Szene neu.

func _unhandled_input(event):
    if event.is_action_pressed("ui_accept") and $UserInterface/Retry.visible:
        # This restarts the current scene.
        get_tree().reload_current_scene()

Die Funktion get_tree() ermöglicht uns den Zugriff auf das globale Object SceneTree, mit dem wir die akuelle Szene neu laden und starten können.

Musik hinzufügen

Um Musik hinzuzufügen, die kontinuirlich im Hintergrund spielt, werden wir ein weiteres Feature von Godot verwenden: Autoloads.

Um Audio abzuspielen, müssen Sie lediglich einen AudioStreamPlayer-Node zu Ihrer Szene hinzufügen und eine Audiodatei an diesen anhängen. Wenn Sie die Szene starten, kann sie automatisch abgespielt werden. Wenn Sie jedoch die Szene neu laden, wie wir es tun, um erneut zu spielen, werden auch die Audio-Nodes zurückgesetzt, und die Musik beginnt wieder von vorne.

Sie können das Autoload-Feature verwenden, um Godot zu veranlassen, einen Node oder eine Szene automatisch zu Beginn des Spiels zu laden, und zwar außerhalb der aktuellen Szene. Sie können damit auch global zugängliche Objekte erstellen.

Erstellen Sie eine neue Szene, indem Sie im Menü Szene auf Neue Szene klicken oder indem Sie das +-Icon neben der aktuell geöffneten Szene verwenden.

|image15|

Klicken Sie auf die Anderer Node-Button, um einen AudioStreamPlayer zu erstellen und ihn in MusicPlayer umzubenennen.

|image16|

Wir haben einen Musik-Soundtrack in dem Ordner art/ beigefügt, namens House In a Forest Loop.ogg. Klicken und ziehen sie diese Datei in die Eigenschaft Stream im Inspektor. Schalten Sie außerdem Autoplay ein, damit die Musik automatisch zu Beginn des Spiels abgespielt wird.

|image17|

Anschließend speichern Sie die Szene unter dem Namen MusicPlayer.tscn.

Sie müssen es für Autoload registrieren. Gehen Sie zu Projekt -> Projekteinstellungen... und klicken sie auf den Autoload-Tab.

Geben Sie in das Feld Pfad den Pfad zu Ihrer Szene ein. Klicken Sie das Order-Icon um die Dateiauswahl zu öffnen und doppelklicken Sie auf MusicPlayer.tscn. Dann klicken Sie auf den Hinzufügen-Button auf der rechten Seite, um den Node zu registrieren.

|image18|

MusicPlayer.tscn wird nun in jede Szene geladen, die Sie öffnen oder spielen. Wenn Sie also das Spiel jetzt starten, wird die Musik automatisch in jeder Szene gespielt.

Bevor wir diese Lektion abschließen, werfen wir noch einen kurzen Blick darauf was im inneren des Spiels vorgeht. Wenn Sie es starten, ändert sich ihr Szene-Dock und sie erhalten zwei Tabs: Remote und Lokal.

image19

Der Remote-Tab ermöglicht es Ihnen, den Szenenbaum Ihres laufenden Spiels zu visualisieren. Dort sehen Sie den Main-Node und alles, was die Szene enthält, sowie die instanziierten Mobs am unteren Ende.

image20

Ganz oben befinden sich der automatisch geladene MusicPlayer und ein Root-Node, der den Viewport Ihres Spiels darstellt.

Und das war's für diese Lektion. Im nächsten Teil fügen wir eine Animation hinzu, damit sich das Spiel besser anfühlt und aussieht.

Hier ist das vollständige Skript main.gd als Referenz.

extends Node

@export var mob_scene: PackedScene

func _ready():
    $UserInterface/Retry.hide()


func _on_mob_timer_timeout():
    # Create a new instance of the Mob scene.
    var mob = mob_scene.instantiate()

    # Choose a random location on the SpawnPath.
    # We store the reference to the SpawnLocation node.
    var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
    # And give it a random offset.
    mob_spawn_location.progress_ratio = randf()

    var player_position = $Player.position
    mob.initialize(mob_spawn_location.position, player_position)

    # Spawn the mob by adding it to the Main scene.
    add_child(mob)

    # We connect the mob to the score label to update the score upon squashing one.
    mob.squashed.connect($UserInterface/ScoreLabel._on_mob_squashed.bind())

func _on_player_hit():
    $MobTimer.stop()
    $UserInterface/Retry.show()

func _unhandled_input(event):
    if event.is_action_pressed("ui_accept") and $UserInterface/Retry.visible:
        # This restarts the current scene.
        get_tree().reload_current_scene()