Section 7: Game Interface

In this section, we took the time to add a UI to our game to start the game, pause the game, view stats, and play game over/victory screens.

Section Intro - Game Interface

There are no notes for this lecture.

Setting Up the Start Menu

In this lecture, we designed an interface with Godot's nodes. There are no notes for this lecture.

Grabbing UI Containers

In this lecture, we applied a class to our containers. Containers are nodes used for storing and arranging additional UI nodes. We'll be storing most of interfaces in containers. Since that's the case, it'll be easier to select them with a class attched to them. So, we added a class called UIContainer.

In this class, we exported a property with an enum.

public partial class UIContainer : VBoxContainer
{
    [Export] public ContainerType container { get; private set; }
}

Here's the enum definition.

public enum ContainerType
{
    Start,
    Pause,
    Victory,
    Defeat,
    Stats,
    Reward
}

It contains a list of the type of UI elements we'll be creating in our game.

To select a container, we created a dictionary called containers. Dictionaries are a feature in C# for storing a collection of data. The main difference between a dictionary and array is that a dictionary allows developers to configure the index for each item in an array. You don't have to use numeric indexes.

We can define a dictionary by importing the following namespace:

using System.Collections.Generic;

Next, we can use the Dictionary type with the data type for the key and value, respectively.

private Dictionary<ContainerType, UIContainer> containers;

For the value, we're initializing the field from the _Ready() method.

containers = GetChildren()
    .Where((element) => element is UIContainer)
    .Cast<UIContainer>()
    .ToDictionary((element) => element.container);

We're using Linq to help us create the dictionary. Firstly, we're using the GetChildren() method, which is available on nodes for grabbing a list of child nodes in the current node. Since this script is applied to the root node of the UI scene, we'll grab all the nodes directly under it.

Next, we're filtering the nodes to check if they have the UIContainer class attached to it. The result is an array of nodes, but we want to cast the results into the UIContainer class, which is what we're doing with the Cast method. Lastly, we're converting the array into a dictionary with the ToDictionary() method, which accepts a lambda function for specifying the key for each item in the array.

Lastly, we toggled the visibility of a node by setting the Visible property.

containers[ContainerType.Start].Visible = true;

To access an item from a dictionary, we can use the enum inside the square brackets instead of a numeric index.

Resources

Starting the Game

In this lecture, we learned how to pause and unpause the game. Games in Godot can be paused by grabbing the scene tree and setting the Paused property to true.

GetTree().Paused = true;

Not everything in Godot will be paused, but you can expect most of Godot's behavior to be paused from physics process to input methods.

To unpause the game, we listened for a button press on the button node. Every button node has a signal called Paused, which we can listen to like so:

containers[ContainerType.Start].ButtonNode.Pressed += HandleStartPressed;

Reparenting Nodes

In this lecture, we learned how to reparent nodes. First, we created a custom event by defining a class for storing our events called GameEvents.

using System;

public class GameEvents
{
    public static Action OnStartGame;

    public static void RaiseStartGame() => OnStartGame?.Invoke();
}

We're using the static keyword so that we can access the event outside the class without needing an instance. Static members are similar to constants except that their values can change.

As a naming convention we'll be following in this course, event names always start with the word On<Name>. Event handlers will be called Handle<Name> and raisers will be called Raise<Name>.

After creating this event, we can subscribe to it like so:

GameEvents.OnStartGame += HandleStartGame;

This event will be raised when the game starts. Once it does, we'll reparent the camera node. Reparenting is the process of moving a child node to a completely different parent node. Every node has access to a method called Reparent. This method accepts an instance of a Node class to move a node. We exported a field for storing the target.

[Export] private Node target;

Next, we called the Reparent method with the target like so:

Reparent(target);

The event Keyword

In this lecture, we learned about the event keyword to prevent us from causing errors in our game. When we define custom events, we have the option of adding the event keyword like so:

public static event Action OnStartGame;

If we add this keyword, we'll only be able to register and unregister methods. We're not allowed to completely override the field. For example, we can't do the following:

GameEvents.OnStartGame = HandleStartGame;

We can only use the += operator when assigning a method.

Handling the End Game Event

In this lecture, we updated our games events to include an event for when the game ends. First, we added the event and method for raising the event in the GameEvents class.

public class GameEvents
{
    public static event Action OnStartGame;
    public static event Action OnEndGame;

    public static void RaiseStartGame() => OnStartGame?.Invoke();
    public static void RaiseEndGame() => OnEndGame?.Invoke();
}

Afterward, we raised this event from the PlayerDeathState class.

GameEvents.RaiseEndGame();

Lastly, we subscribed to this event. During this event, we reparented the Camera node in our game.

public override void _Ready()
{
    GameEvents.OnStartGame += HandleStartGame;
    GameEvents.OnEndGame += HandleEndGame;
}

private void HandleEndGame()
{
    Reparent(GetTree().CurrentScene);
}

Once again, we're using the Reparent method. In this method, we're passing on the GetTree().CurrentScene property, which contains the root node of the current scene in our game.

Stats UI

In this lecture, we created a UI for displaying the player's stats. There are no notes for this lecture.

Dynamically Updating Labels

In this lecture, we updated the labels for displaying the player's stats. We used the resource to help us perform this task. The great thing about resources is that they're independent from a node. They can be applied to multiple nodes and share data. So, in the StatResource class, we added an event called OnUpdate and then raised it from the set accessor.

public partial class StatResource : Resource
{
    public event Action OnZero;
    public event Action OnUpdate;

    [Export] public Stat StatType { get; private set; }

    private float _statValue;

    [Export]
    public float StatValue
    {
        get => _statValue;
        set
        {
            _statValue = Mathf.Clamp(value, 0, Mathf.Inf);

            OnUpdate?.Invoke();

            if (_statValue == 0)
            {
                OnZero?.Invoke();
            }
        }
    }
}

Next, we subscribed to this event from a custom class attached to a node called StatLabel. In this class, we're setting the Text property to the new value.

Text = statResource.StatValue.ToString();

It's important to note that we are using the ToString() method to convert the value into a string since the Text property only accepts strings.

Counting the Enemies

In this lecture, we counted the enemies in our game, kept track of this information, and then updated the label in the stats UI to display this information. Firstly, to grab the number of enemies, we stored the enemies in a node and attached a script to it. From this script, we used the GetChildCount method to count the number of child nodes.

int totalEnemies = GetChildCount();

Next, we raised an event to allow other nodes to gather this data. Godot has a signal that we can subscribe to when a child node is deleted. Since we're deleting our enemies from the game, we decided to use it to update the count. The signal is called ChildExitingTree.

ChildExitingTree += HandleChildExitingTree;

Here's the method handler.

private void HandleChildExitingTree(Node node)
{
    int totalEnemies = GetChildCount() - 1;

    GameEvents.RaiseNewEnemyCount(totalEnemies);
}

In this method, we're grabbing the count again and subtracting one. It's important to subtract 1 since this signal gets called before the enemy is deleted.

For the OnNewEnemyCount event, we defined the event with a generic.

public static event Action<int> OnNewEnemyCount;

If you plan on sending data with an event, you must add a generic to describe the type of data you plan on sending. Whenever you subscribe to this event, the method must accept the argument. Otherwise, C# will complain.

public partial class EnemyCountLabel : Label
{
    public override void _Ready()
    {
        GameEvents.OnNewEnemyCount += HandleNewEnemyCount;
    }

    private void HandleNewEnemyCount(int count)
    {
        Text = count.ToString();
    }
}

Defeat UI

In this lecture, we designed and coded a UI for the defeat screen. When the player is defeated, we raised an event from their PlayerDeathState class. Specifically, we decided to raise this event when the animation is finished.

private void HandleAnimationFinished(StringName animName)
{
    GameEvents.RaiseEndGame();

    characterNode.QueueFree();
}

When the event is raised, we're subscribing to this event and then reparenting the node so that it doesn't get deleted.

private void HandleEndGame()
{
    Reparent(GetTree().CurrentScene);
}

Victory UI

In this lecture, we worked on the victory UI. When the player wins the game, the game should pause to prevent them from performing any other actions. Instead, we decided to display a victory screen to let the player know they've defeatd all enemies. The most important step in this process pausing the game when the event was raised.

private void HandleVictory()
{
    containers[ContainerType.Stats].Visible = false;
    containers[ContainerType.Victory].Visible = true;

    GetTree().Paused = true;
}

In this method, we hid the stats UI and displayed the victory UI before pausing the game.

Pause UI

In this lecture, we worked on creating a pause UI. We didn't really learn anything new in this lecture aside from learning how to toggle a boolean value. In the _Input method, we performed a few steps.

public override void _Input(InputEvent inputEvent)
{
    if (!canPause) { return; }

    if (!Input.IsActionJustPressed(GameConstants.INPUT_PAUSE)) { return; }

    containers[ContainerType.Stats].Visible = GetTree().Paused;
    GetTree().Paused = !GetTree().Paused;
    containers[ContainerType.Pause].Visible = GetTree().Paused;
}

Firstly, we checked if the player can pause. If another UI element is visible, such as the defeat, start, or victory screen are displaying, the player shouldn't be able to pause. Next, we're checking the Pause action for a key press. If it wasn't pressed, we didn't bother running the rest of the method.

Lastly, we toggled the stats, Paused property, and pause UI. The order does matter since we don't want to use the Paused property twice for setting a property on a container.

Last updated