Kontrolliere das Spiele-UI mit Code

Intro

In diesem Tutorial werden wir einen Charakter mit einer Lebensbar verbinden und den Gesundheitsverlust animieren (dam -dam -dam DAM!).

../../_images/lifebar_tutorial_final_result.gif

Hier ist, was du erschaffen wirst: eine Bar und einen Zähler wirst du animieren, wenn der Charakter Schaden nimmt. Sie verblassen, wenn es stirbt.

Sie werden lernen:

  • Wie man einen Charakter mit einer GUI verbindet, indem man Signale benutzt
  • Wie man eine GUI mit GDScript kontrolliert
  • Wie man eine Lebensbar mit einer Tween-Node animiert

Wenn du stattdessen lernen willst, wie man ein Interface konfiguriert, werfe einen Blick auf das Schritt-für-Schritt UI-Tutorial:

  • Erstelle einen Hauptmenü-Bildschirm
  • Erstelle eine Spielbenutzeroberfläche

Wenn du mit der Entwicklung beginnst, kümmerst Du Dich zunächst um die Kernaspekte des Spiels: die Grundmechaniken, Eingabesteuerung, Sieg- und Verlustbedingungen. Die UI kommt etwas später nach. Halte sämtliche Elemente des Projekts soweit es geht modular. Jeder Charakter sollte in einer eigenen Szene mit dazugehörigen Skripten untergebracht werden. Gleiches gilt für UI-Elemente. Dies beugt potenziellen Bugs vor und hält das Projekt überschaubar und erlaubt es Teammitgliedern, gemeinsam an unterschiedlichen Teilen des Spiels zu arbeiten.

Wenn erstmal das Haupt-Gameplay und die UI fertig sind, wirst du es nötig haben, diese irgendwie zu verbinden. In unserem Beispiel haben wir den Feind, der den Spieler mit einem konstanten Zeitintervall attackiert. Wir möchten, dass die Lebensbar updatet, wenn der Spieler Schaden nimmt.

Dafür benutzt man Signale.

Bemerkung

Signale sind die Implementierung des Beobachter-Entwurfsmusters (Observer) in Godot. Sie erlauben das Absenden von Nachrichten, die von einem anderen Objekt empfangen werden kann. Dazu wird ein Node mit dem Objekt verbunden, welches das Signal emittiert und erhält so die Informationen des Signals. Ein mächtiges Werkzeug, vor allem bei der Umsetzung von Benutzeroberflächen und Errungenschaften. Da das Verbinden zweier Nodes gleichzeitig eine gewisse Abhängigkeit voneinander erzeugt, sollten sie allerdings stets mit Bedacht verwendet werden, bei zuvielen Verbindungen kann es schwierig werden, den Überblick zu behalten. Für mehr Informationen, schau Dir das signals video tutorial von GDquest an.

Lade und sieh dir das Startprojekt an

Lade Dir das Godot-Projekt herunter: ui_code_life_bar.zip. Es enthält sämtliche Nutzerinhalte und Skripte, die Du benötigst, um loszulegen. Entpacke das Zip-Archiv, um die beiden Ordner zu erhalten: start und end.

Lade das start-Projekt in Godot. Im Dateisystem-Fenster, doppelklicke auf LevelMockup.tscn, um es zu öffnen. Es ist ein RPG-Mock-up in dem sich zwei Charaktere gegenüberstehen. Der rosafarbene Gegner greift das grüne Viereck an und fügt ihm regelmäßig Schaden zu, bis es stirbt. Probier das Spiel aus: eine grundlegende Kampfmechnik ist bereits enthalten. Allerdings funktioniert die GUI noch nicht, da der Spieler bisher nicht mit der Lebensanzeige verbunden ist.

Bemerkung

Das ist typisch für jedes Spiel: Als Erstes implementierst Du die Mechanik, die mit der GUI interagiert, in diesem Fall den Lebensverlust des Spielers, und erst dann fügst du die Benutzeroberfläche hinzu. Das liegt daran, dass die UI darauf reagiert, was im Spiel geschieht. Somit kann sie nicht korrekt funktionieren, solange der Spielablauf nicht integriert ist und muss unter Umständen von neuem erstellt werden.

Die Szene enthält einen Hintergrund-Sprite, eine GUI sowie zwei Charaktere.

../../_images/lifebar_tutorial_life_bar_step_tut_LevelMockup_scene_tree.png

Der Szenenbaum mit der GUI Szene ist so gesetzt, dass die Kinder angezeigt werden

Die GUI-Szene kapselt die gesamte grafische Benutzeroberfläche des Spiels ein. Es wird mit einem Barebones-Skript geliefert, in dem der Pfad zu den Knoten in der Szene angegeben wird:

onready var number_label = $Bars/LifeBar/Count/Background/Number
onready var bar = $Bars/LifeBar/TextureProgress
onready var tween = $Tween
public class Gui : MarginContainer
{
    private Tween _tween;
    private Label _numberLabel;
    private TextureProgress _bar;

    public override void _Ready()
    {
        // C# doesn't have an onready feature, this works just the same.
        _bar = (TextureProgress) GetNode("Bars/LifeBar/TextureProgress");
        _tween = (Tween) GetNode("Tween");
        _numberLabel = (Label) GetNode("Bars/LifeBar/Count/Background/Number");
    }
}
  • number_label```zeigt einen Lebenszähler als eine Nummer. Es ist eine ``Label-Node
  • bar ist die Lebensanzeige, ein TextureProgress-Node
  • tween ist eine Node im Komponentenstil, die man animieren und ihr verschiedene Werte oder Methoden von anderen Nodes zuweisen kann

Bemerkung

Das Projekt ist organisatorisch einfach gestaltet, geeignet für Game Jams und kleinere Spiele.

Auf der Wurzelebene des Projekts, im res://-Verzeichnis, findest Du LevelMockup. Das ist die Hauptszene des Spiels, mit der wir auch arbeiten werden. Sämtliche Komponenten, aus denen das Spiel besteht, befinden sich im scenes/-Ordner. Der assets/-Ordner enthält alle Sprites und die Schriftart (Font) für die HP-Anzeige. Unter scripts/ findest du die Objekte für Spieler und Gegner sowie die Controller-Skripte der GUI.

Klicke auf das Bearbeiten-Szenen-Icon auf der rechten Seite der Node in dem Szenenbaum, um die Szene im Editor zu öffnen. Du wirst eine LifeBar und EnergieBar sehen, die selbst Unterszenen sind.

../../_images/lifebar_tutorial_Player_with_editable_children_on.png

Der Szenenbaum, mit der Spielerszene ausgewählt, um die Kinder anzuzeigen

Setze die Lifebar so mit der maximalen Gesundheit des Spielers

Wir müssen der GUI irgendwie mitteilen, wie der aktuelle Gesundheitszustand des Players ist, die Textur des Lebensbalkens aktualisieren und den verbleibenden Gesundheitszustand im HP-Zähler in der oberen linken Ecke des Bildschirms anzeigen. Dazu senden wir die Gesundheit des Spielers bei jedem Schaden an die GUI. Die GUI aktualisiert dann die Knoten `` Lifebar`` und `` Number`` mit diesem Wert.

Wir könnten hier anhalten, um die Zahl anzuzeigen, aber wir müssen den max_value des Balkens initialisieren, damit er in den richtigen Proportionen aktualisiert wird. Der erste Schritt besteht also darin, der GUI zu sagen, was das max_health des grünen Zeichens ist.

Tipp

Die Leiste, eine TextureProgress, hat standardmäßig einen Maximalwert von 100. Wenn Sie nicht die Gesundheit des Charakters mit einer Number anzeigen wollen, brauchen Sie nicht ihre Maximalwert Eigenschaft ändern. Sie senden einen Prozentsatz von dem Spieler stattdessen zur GUI:..`health/max_health * 100`.

../../_images/lifebar_tutorial_TextureProgress_default_max_value.png

Klicke das Skript Icon rechts neben GUI im Szenen Reiter um sein Skript zu öffnen. In der _ready Funktion werden wir eine neue Variable namens max_health (maximales leben) für den Player (Spieler) speichern und ihr den Wert von bar``s (Balken) ``max_value (maximaler Wert) geben:

func _ready():
    var player_max_health = $"../Characters/Player".max_health
    bar.max_value = player_max_health
public override void _Ready()
{
    // Add this below _bar, _tween, and _numberLabel.
    var player = (Player) GetNode("../Characters/Player");
    _bar.MaxValue = player.MaxHealth;
}

Schauen wir uns das mal genauer an. $"../Characters/Player"` ist eine Abkürzung, die im Szenenbaum einen Node nach oben geht und von dort aus den Node Characters/Player abruft. Das gibt uns Zugang zum Node. Der zweite Teil der Anweisung, .max_health, greift auf die max_health-Variable auf dem Player-Node zu.

Die zweite Zeile weist den Wert bar.max_value zu. Die zwei Zeilen könnten auch zu einer Kombiniert werden, doch player_max_health wird später in der Anleitung noch gebraucht werden.

Am Anfang des Spieles wird health auf max_health gesetzt, damit wir es gebrauchen können. Warum brauchen wir max_health immer noch? Es gibt zwei Gründe:

Wir können nicht davon ausgehen, dass health in jedem Fall max_health entspricht: Eine spätere Version des Spiels könnte beispielsweise einen Level laden, in welchem der Spieler bereits etwas Gesundheit verloren hat.

Bemerkung

Wenn du eine Szene im Spiel öffnest, kreiert Godot Nodes einer nach der anderen, in der Reihenfolge des Szenen Reiters, von oben nach unten. GUI und Player sind nicht teil des selben Node-Zweiges. Um sicher zu stellen, dass beide existieren, wenn wir auf sie zugreifen, benutzen wir die _ready Funktion. Godot führt _ready genau dann aus, wenn es alle Nodes geladen hat, bevor das Spiel startet. Es ist die perfekte Funktion um alles einzustellen und die Spiel Sitzung vorzubereiten. Mehr zu _ready: Scripting (Fortsetzung)

Aktualisiere die Lebensanzeige mithilfe eines Signals, wenn der Spieler getroffen wird

Unserer GUI ist nun bereit um den neuen health-Wert des Spielers zu empfangen. Um dies zu erreichen werden wir Gebrauch von Signals machen.

Bemerkung

Es gibt viele nützliche eingebaute Signale wie enter_tree und exit_tree, die alle Knoten aussenden, wenn sie jeweils erzeugt und zerstört werden. Du kannst auch deine eigenen mit dem Schlüsselwort signal erstellen. Auf dem Node Player findest du zwei Signale, die wir erstellt haben: died und health_changed.

Warum holen wir nicht direkt den Player Knoten in der _process Funktion und betrachten den Health-Wert? Wenn wir auf diese Weise auf Nodes zugreifen, entsteht eine enge Kopplung zwischen ihnen. Wenn du es sparsam machst, kann es funktionieren. Wenn das Spiel größer wird, hast du möglicherweise viel mehr Verbindungen. Wenn du auf diese Weise Nodes erhältst, wird es schnell komplex. Nicht nur das: Du musst in der Funktion _process` ständig auf die Zustandsänderung achten. Diese Überprüfung findet 60 Mal pro Sekunde statt und kann das Spiel verlangsamen, wenn es in einem großen Spiel mit zu vielen Verbindungen passiert.

Auf einem bestimmten Frame kannst du dir die Eigenschaft eines anderen Nodes ansehen, vor dessen Aktualisierung: Du erhältst einen Wert aus dem letzten Frame. Dies führt zu obskuren Bugs, die schwer zu beheben sind. Mit Signalen kann man das verhindern: Ein Signal wird garantiert direkt nach der Änderung mit dem neuen Wert aufgerufen und verhindert damit diese Fehler.

Bemerkung

Das Observer-Pattern, von dem die Signals abgeleitet sind, fügt den einzelnen Nodes trotzdem eine kleine Menge an Kopplung hinzu. Aber es erzeugt im Allgemeinen nicht so viel Kopplung und ist sicherer als der direkte Zugriff auf Funktionen anderer Klassen bzw. Nodes. Es kann in Ordnung sein, dass Elternknoten Werte von den Kindknoten erhalten, aber bei der Kommunikation von Knoten in unterschiedlichen Verzweigungen sollten Signals bevorzugt werden. Im Buch „Game Programming Patterns“, das online kostenlos verfügbar ist, ist das Observer Pattern noch detaillierter beschrieben: <http://gameprogrammingpatterns.com/observer.html>

In diesem Sinne verbinden wir das GUI mit dem Player. Klicke auf den Knoten Player im Szene-Fenster, um ihn auszuwählen. Gehen Sie zum Inspektor und klicken Sie auf das Fenster „Node“. Dies ist der Ort, an dem man Knoten verbinden kann, um den von dir ausgewählten Knoten anzuhören.

Der erste Abschnitt zeigt die eigenen Signale, welche in Player.gd definiert sind.

  • died wird aufgerufen sobald der Spieler stirbt, Wir benutzen es gleich um die UI zu verbergen.
  • health_changed wird aufgerufen, sobald der Spieler Schaden nimmt.
../../_images/lifebar_tutorial_health_changed_signal.png

Wir verbinden das health_changed Signal.

Wähle health_changed aus und klicke auf „Connect“ unten rechts im Fenster um das „Signal Verbinden“-Fenster zu öffnen. Links kannst du den Node auswählen, der auf das Signal reagieren soll. Wähle hier die GUI-Node aus. Rechts kannst du zusätzliche Werte auswählen, die mit dem Signal gesendet werden sollen. Wir haben uns allerdings bereits in Player.gd darum gekümmert. Ich empfehle dir, vorallem bei mehreren Argumenten, die Argumente lieber im Code als in diesem Fenster zu definieren, da es im Code einfacher ist.

../../_images/lifebar_tutorial_connect_signal_window_health_changed.png

Das „Connect Signal“-Fenster mit der ausgewählten GUI Node

Tipp

Man kann optional auch Signale im Code verbinden. Dies jedoch im Editor zu tun hat zwei Vorteile:

  1. Godot kann automatisch Funktionen in dem Script erstellen, mit dem es verbunden ist
  2. Ein „Emitter“-Icon erscheint neben der Node im Szenen-Fenster.

Am unteren Rand des Fensters finden Sie den Pfad zu dem von Ihnen ausgewählten Knoten. Wir sind an der zweiten Zeile „Methode im Knoten“ interessiert. Dies ist die Methode auf dem GUI` Knoten, die aufgerufen wird, wenn das Signal ausgegeben wird. Dieses Verfahren empfängt die mit dem Signal gesendeten Werte und ermöglicht deren Verarbeitung. Rechts gibt es einen Button „Make Function“, der standardmäßig eingeschaltet ist. Klicke auf den Button Verbinden am unteren Rand des Fensters. Godot erstellt die Methode innerhalb des GUI Knoten. Der Skripteditor öffnet sich mit dem Cursor innerhalb einer neuen _on_Player_health_changed Funktion.

Bemerkung

Wenn eine Node im Editor verbunden wird, generiert Godot eine Methode nach dem folgenden Musster: _on_EmitterName_signal_name. Falls die Methode bereits existiert, bleibt die „Erstelle Funktion“. Der Name kann nach belieben verändert werden.

../../_images/lifebar_tutorial_godot_generates_signal_callback.png

Godot writes the callback method for you and takes you to it

Inside the parentheses after the function name, add a player_health argument. When the player emits the health_changed signal, it will send its current health alongside it. Your code should look like:

func _on_Player_health_changed(player_health):
    pass
public void OnPlayerHealthChanged(int playerHealth)
{
}

Bemerkung

The engine does not convert PascalCase to snake_case, for C# examples we’ll be using PascalCase for method names & camelCase for method parameters, which follows the official C# naming conventions.

../../_images/lifebar_tutorial_player_gd_emits_health_changed_code.png

In Player.gd, when the Player emits the health_changed signal, it also sends its health value

Inside _on_Player_health_changed, let’s call a second function called update_health and pass it the player_health variable.

Bemerkung

We could directly update the health value on LifeBar and Number. There are two reasons to use this method instead:

  1. The name makes it clear for our future selves and teammates that when the player took damage, we update the health count on the GUI
  2. Wir werden diese Methode etwas später wiederverwenden

Create a new update_health method below _on_Player_health_changed. It takes a new_value as its only argument:

func update_health(new_value):
    pass
public void UpdateHealth(int health)
{
}

This method needs to:

  • set the Number node’s text to new_value converted to a string
  • set the TextureProgress’s value to new_value
func update_health(new_value):
    number_label.text = str(new_value)
    bar.value = new_value
public void UpdateHealth(int health)
{
    _numberLabel.Text = health.ToString();
    _bar.Value = health;
}

Tipp

str is a built-in function that converts about any value to text. Number’s text property requires a string, so we can’t assign it to new_value directly

Also call update_health at the end of the _ready function to initialize the Number node’s text with the right value at the start of the game. Press F5 to test the game: the life bar updates with every attack!

../../_images/lifebar_tutorial_LifeBar_health_update_no_anim.gif

Both the Number node and the TextureProgress update when the Player takes a hit

Animate the loss of life with the Tween node

Our interface is functional, but it could use some animation. That’s a good opportunity to introduce the Tween node, an essential tool to animate properties. Tween animates anything you’d like from a start to an end state over a certain duration. For example, it can animate the health on the TextureProgress from its current level to the Player’s new health when the character takes damage.

The GUI scene already contains a Tween child node stored in the tween variable. Let’s now use it. We have to make some changes to update_health.

We will use the Tween node’s interpolate_property method. It takes seven arguments:

  1. A reference to the node who owns the property to animate
  2. The property’s identifier as a string
  3. Der Start Wert
  4. Der End-Wert
  5. Die Animationsdauer in Sekunden
  6. Der Typ des Übergangs
  7. Die einfache Handhabung in Kombination mit der Gleichung.

Die letzten beiden kombinierten Argumente entsprechen einer Lockerungsgleichung. Das steuert, wie sich der Wert vom Anfang bis zum Ende entwickelt.

Klicke auf das Skript Symbol neben der GUI Node, um es wieder zu öffnen. Die Number Node benötigt Text, damit es sich selbst aktualisiert und die Bar benötigt einen float oder integer. Wir können interpolate_property nutzen, um eine Nummer, aber nicht direkt einen Text zu animieren. Wir werden es stattdessen nutzen, um eine neue GUI Variable namens animated_health zu animieren.

Definieren Sie oben im Skript eine neue Variable, benennen Sie sie animated_health, und setzen Sie ihren Wert auf 0. Navigieren Sie zurück zur Methode update_health und löschen Sie ihren Inhalt. Lassen Sie uns den Wert von animated_health animieren. Rufen Sie die Methode interpolate_property des Tween Knotens auf:

func update_health(new_value):
    tween.interpolate_property(self, "animated_health", animated_health, new_value, 0.6, Tween.TRANS_LINEAR, Tween.EASE_IN)
// Add this to the top of your class.
private float _animatedHealth = 0;

public void UpdateHealth(int health)
{
    _tween.InterpolateProperty(this, "_animatedHealth", _animatedHealth, health, 0.6f, Tween.TransitionType.Linear,
        Tween.EaseType.In);
}

Lassen Sie uns den Aufruf aufschlüsseln:

tween.interpolate_property(self, "animated_health", ...

We target animated_health on self, that is to say the GUI node. Tween’s interpolate_property takes the property’s name as a string. That’s why we write it as "animated_health".

... _health", animated_health, new_value, 0.6 ...

The starting point is the current value the bar’s at. We still have to code this part, but it’s going to be animated_health. The end point of the animation is the Player’s health after the health_changed: that’s new_value. And 0.6 is the animation’s duration in seconds.

...  0.6, tween.TRANS_LINEAR, Tween.EASE_IN)

The last two arguments are constants from the Tween class. TRANS_LINEAR means the animation should be linear. EASE_IN doesn’t do anything with a linear transition, but we must provide this last argument or we’ll get an error.

The animation will not play until we activated the Tween node with tween.start(). We only have to do this once if the node is not active. Add this code after the last line:

if not tween.is_active():
    tween.start()
if (!_tween.IsActive())
{
    _tween.Start();
}

Bemerkung

Although we could animate the health property on the Player, we shouldn’t. Characters should lose life instantly when they get hit. It makes it a lot easier to manage their state, like to know when one died. You always want to store animations in a separate data container or node. The tween node is perfect for code-controlled animations. For hand-made animations, check out AnimationPlayer.

Assign the animated_health to the LifeBar

Now the animated_health variable animates but we don’t update the actual Bar and Number nodes anymore. Let’s fix this.

So far, the update_health method looks like this:

func update_health(new_value):
    tween.interpolate_property(self, "animated_health", animated_health, new_value, 0.6, Tween.TRANS_LINEAR, Tween.EASE_IN)
    if not tween.is_active():
        tween.start()
public void UpdateHealth(int health)
{
    _tween.InterpolateProperty(this, "_animatedHealth", _animatedHealth, health, 0.6f, Tween.TransitionType.Linear,
        Tween.EaseType.In);

    if(!_tween.IsActive())
    {
        _tween.Start();
    }
}

In this specific case, because number_label takes text, we need to use the _process method to animate it. Let’s now update the Number and TextureProgress nodes like before, inside of _process:

func _process(delta):
    number_label.text = str(animated_health)
    bar.value = animated_health
public override void _Process(float delta)
{
    _numberLabel.Text = _animatedHealth.ToString();
    _bar.Value = _animatedHealth;
}

Bemerkung

number_label and bar are variables that store references to the Number and TextureProgress nodes.

Play the game to see the bar animate smoothly. But the text displays decimal number and looks like a mess. And considering the style of the game, it’d be nice for the life bar to animate in a choppier fashion.

../../_images/lifebar_tutorial_number_animation_messed_up.gif

The animation is smooth, but the number is broken

We can fix both problems by rounding out animated_health. Use a local variable named round_value to store the rounded animated_health. Then assign it to number_label.text and bar.value:

func _process(delta):
    var round_value = round(animated_health)
    number_label.text = str(round_value)
    bar.value = round_value
public override void _Process(float delta)
{
    var roundValue = Mathf.Round(_animatedHealth);
    _numberLabel.Text = roundValue.ToString();
    _bar.Value = roundValue;
}

Try the game again to see a nice blocky animation.

../../_images/lifebar_tutorial_number_animation_working.gif

By rounding out animated_health, we kill two birds with one stone

Tipp

Every time the player takes a hit, the GUI calls _on_Player_health_changed, which in turn calls update_health. This updates the animation and the number_label and bar follow in _process. The animated life bar that shows the health going down gradually is a trick. It makes the GUI feel alive. If the Player takes 3 damage, it happens in an instant.

Fade the bar when the Player dies

When the green character dies, it plays a death animation and fades out. At this point, we shouldn’t show the interface anymore. Let’s fade the bar as well when the character died. We will reuse the same Tween node as it manages multiple animations in parallel for us.

First, the GUI needs to connect to the Player’s died signal to know when it died. Press F1 to jump back to the 2D Workspace. Select the Player node in the Scene dock and click on the Node tab next to the Inspector.

Find the died signal, select it, and click the Connect button.

../../_images/lifebar_tutorial_player_died_signal_enemy_connected.png

The signal should already have the Enemy connected to it

In the Connecting Signal window, connect to the GUI node again. The Path to Node should be ../../GUI and the Method in Node should show _on_Player_died. Leave the Make Function option on and click Connect at the bottom of the window. This will take you to the GUI.gd file in the Script Workspace.

../../_images/lifebar_tutorial_player_died_connecting_signal_window.png

You should get these values in the Connecting Signal window

Bemerkung

You should see a pattern by now: every time the GUI needs a new piece of information, we emit a new signal. Use them wisely: the more connections you add, the harder they are to track.

To animate a fade on a UI element, we have to use its modulate property. modulate is a Color that multiplies the colors of our textures.

Bemerkung

modulate comes from the CanvasItem class, All 2D and UI nodes inherit from it. It lets you toggle the visibility of the node, assign a shader to it, and modify it using a color with modulate.

modulate takes a Color value with 4 channels: red, green, blue and alpha. If we darken any of the first three channels it darkens the interface. If we lower the alpha channel, our interface fades out.

We’re going to tween between two color values: from a white with an alpha of 1, that is to say at full opacity, to a pure white with an alpha value of 0, completely transparent. Let’s add two variables at the top of the _on_Player_died method and name them start_color and end_color. Use the Color() constructor to build two Color values.

func _on_Player_died():
    var start_color = Color(1.0, 1.0, 1.0, 1.0)
    var end_color = Color(1.0, 1.0, 1.0, 0.0)
public void OnPlayerDied()
{
    var startColor = new Color(1.0f, 1.0f, 1.0f);
    var endColor = new Color(1.0f, 1.0f, 1.0f, 0.0f);
}

Color(1.0, 1.0, 1.0) corresponds to white. The fourth argument, respectively 1.0 and 0.0 in start_color and end_color, is the alpha channel.

We then have to call the interpolate_property method of the Tween node again:

tween.interpolate_property(self, "modulate", start_color, end_color, 1.0, Tween.TRANS_LINEAR, Tween.EASE_IN)
_tween.InterpolateProperty(this, "modulate", startColor, endColor, 1.0f, Tween.TransitionType.Linear,
  Tween.EaseType.In);

This time, we change the modulate property and have it animate from start_color to the end_color. The duration is of one second, with a linear transition. Here again, because the transition is linear, the easing does not matter. Here’s the complete _on_Player_died method:

func _on_Player_died():
    var start_color = Color(1.0, 1.0, 1.0, 1.0)
    var end_color = Color(1.0, 1.0, 1.0, 0.0)
    tween.interpolate_property(self, "modulate", start_color, end_color, 1.0, Tween.TRANS_LINEAR, Tween.EASE_IN)
public void OnPlayerDied()
{
    var startColor = new Color(1.0f, 1.0f, 1.0f);
    var endColor = new Color(1.0f, 1.0f, 1.0f, 0.0f);

    _tween.InterpolateProperty(this, "modulate", startColor, endColor, 1.0f, Tween.TransitionType.Linear,
        Tween.EaseType.In);
}

And that is it. You may now play the game to see the final result!

../../_images/lifebar_tutorial_final_result.gif

The final result. Congratulations for getting there!

Bemerkung

Using the exact same techniques, you can change the color of the bar when the Player gets poisoned, turn the bar red when its health drops low, shake the UI when they take a critical hit… the principle is the same: emit a signal to forward the information from the Player to the GUI and let the GUI process it.