The User Interface
In this section, we will set up the user interface and write a script that updates the user
interface based on the ChatPlayer node set up in the previous section. We’ll also need to make
a new main scene to handle the connection between the level scene (already created) and the UI. By
the end of this section, we should be able to do the following from the user interface:
See a constantly updated control hint message.
See when other
ChatEntitynodes are nearby.Write, send, and receive messages while in a conversation with another entity.
Part 1: The User Interface Scene
Let’s start by setting up the user interface scene along with its script.
Setting Up the Scene
The user interface scene is provided premade here: user_interface.tscn. Everything
should be set up except the scripts. Open up user_interface.tscn. You should see the following
scene (though the background may be a different color):
You will also need the files dynamic_label.tscn and NewMsgEdit.cs. The
first is a label scene that will be added from script during gameplay. The second is a short script
that makes it so that the enter key will send a message. Attach the NewMsgEdit.cs script now to
the NewMsgEdit node (this node is under the MsgGroup node).
Finally, if you want, you can change the project’s default background color to black. This setting can be found in the project settings under Rendering > Environment > Defaults > Default Clear Color.
Writing the Script
Now, we’ll add the main script for interacting with the UI. It will be attached to the root node
of the scene and will listen for signals from the ChatPlayer node and update it when
necessary.
Add a C# script to the UserInterface node. Save the script as UserInterface.cs.
Properties
First, we’ll add a property that will reference the ChatPlayer node attached to the
seraphis.tscn scene. We won’t worry about setting it from inside the UI script and will instead
have it set by the main scene (up next).
// A reference to the ChatPlayer node (will be set by dependency injection via Main.cs)
public ChatPlayer MyChatPlayer;
Next, we’ll add properties that will hold references to the various child nodes of the
UI scene. This will make them easier to access from the code. (These will have values assigned to
them later in the _Ready() method.)
// Various nodes that will need to be updated during gameplay
private Label _controlInfo;
private VBoxContainer _entityBox;
private ScrollContainer _msgScroll;
private VBoxContainer _msgBox;
private TextEdit _newMsgEdit;
private Button _newMsgSend;
We’ll also have a property that will reference the dynamic_label.tscn scene that we downloaded earlier. This will be used to add labels dynamically for nearby entities and messages. Set the path to point wherever you have this scene saved.
// Scenes that will need to be instanced during gameplay
private PackedScene _dynamicLabelScene = GD.Load<PackedScene>("res://dynamic_label.tscn");
The last property that we’ll have is a flag to indicate whether a new message has just been added to the UI. This will be used to help us keep the messages’ scroll container scrolled all the way to the bottom so that the newest messages are always visible.
// Flag for when a message has been added to the message box and so we need to scroll to end
private bool _justAddedNewMsg = false;
Methods for Initialization
Now on to methods. First we’ll override the node’s _Ready() method. In this method, we’ll
assign nodes to the properties declared earlier and we’ll connect up a method to handle what
happens when the send button is pressed. We’ll also call a helper function
ConnectPlayerSignals() that connects all the relevant signals of ChatPlayer to methods in
this script.
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
// Get all the various child nodes
_controlInfo = GetNodeOrNull<Label>("ControlGroup/ControlInfo");
_entityBox = GetNodeOrNull<VBoxContainer>("EntityGroup/EntityScroll/EntityBox");
_msgScroll = GetNodeOrNull<ScrollContainer>("MsgGroup/MsgScroll");
_msgBox = GetNodeOrNull<VBoxContainer>("MsgGroup/MsgScroll/MsgBox");
_newMsgEdit = GetNodeOrNull<TextEdit>("MsgGroup/NewMsgBox/NewMsgEdit");
_newMsgSend = GetNodeOrNull<Button>("MsgGroup/NewMsgBox/NewMsgSend");
// Connect to the send message button (and propogate the signal)
if (_newMsgSend != null)
_newMsgSend.Pressed += OnNewMsgSendPressed;
// Connect to the signals of the ChatPlayer
CallDeferred("ConnectPlayerSignals");
}
// Connects to the various signals of the currently registered ChatPlayer
private void ConnectPlayerSignals()
{
MyChatPlayer.ControlHintUpdated += SetControlHint;
MyChatPlayer.ChatEntityAdded += AddEntity;
MyChatPlayer.ChatEntityRemoved += RemoveEntity;
MyChatPlayer.ConvoStarted += EnableNewMsg;
MyChatPlayer.ConvoEnded += DisableNewMsg;
MyChatPlayer.MsgAdded += AddMsg;
}
Note
In the _Ready() method, we use CallDeferred() to call ConnectPlayerSignals() so
that we can be sure ChatPlayer has been loaded in the scene and MyChatPlayer has been
set to reference it before we try to access it.
Methods for Updating
In _Ready(), we attached the method OnNewMsgSendPressed() to the Pressed signal of the
send button. As such, this method will be called when the send button is pressed (or when the enter
key is pressed). We just need it to call the SendMsg() method of the ChatPlayer node.
// Called when the send button is pressed
public void OnNewMsgSendPressed()
{
MyChatPlayer.SendMsg(_newMsgEdit.Text);
_newMsgEdit.Text = "";
}
Next, we’ll override the node’s _Process() method. In this method, if a new message has just
arrived, we’ll make sure the ScrollContainer is scrolled all the way to the bottom.
// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
// Check if a new message has been added to the message box and scroll if needed
if (_justAddedNewMsg)
{
_msgScroll.ScrollVertical = Mathf.RoundToInt(_msgScroll.GetVScrollBar().MaxValue);
_justAddedNewMsg = false;
}
}
This next method will set the text of the “Control Hint” section.
// Set the text of the control hint label
public void SetControlHint(string controlHint)
{
_controlInfo.Text = controlHint;
}
Also, write the following methods for adding or removing an entity from the “Nearby Entities” section. When adding an entity, we’ll need to create a new instance of dynamic_label.tscn and add it as a child in the appropriate place. When removing, we’ll need to search through all the labels and find the one that matches the entity we are trying to remove.
// Add a ChatEntity to the entity box
public void AddEntity(ChatEntity newEntity)
{
// Add the new entity to the box
Label newEntityLabel = _dynamicLabelScene.Instantiate<Label>();
newEntityLabel.Text = newEntity.ChatName;
_entityBox.AddChild(newEntityLabel);
}
// Remove a ChatEntity from the entity box
public void RemoveEntity(ChatEntity oldEntity)
{
string entityString = oldEntity.ChatName;
// Look through all the entities in the box
foreach (Node child in _entityBox.GetChildren())
{
// If we find the entity, remove it and exit
if (child is Label entityLabel && entityLabel.Text == entityString)
{
entityLabel.QueueFree();
break;
}
}
}
The following two methods are for enabling and disabling text input and button presses in the UI. They will be enabled when the player is in a conversation and disabled when they are not.
// Enable the controls for typing and sending a new message
public void EnableNewMsg()
{
// Enable controls
_newMsgEdit.Editable = true;
_newMsgSend.Disabled = false;
// Put focus on the text editor
_newMsgEdit.GrabFocus();
}
// Disable the controls for typing and sending a new message
public void DisableNewMsg()
{
// Disable controls
_newMsgEdit.Editable = false;
_newMsgSend.Disabled = true;
// Release focus
_newMsgEdit.ReleaseFocus();
_newMsgSend.ReleaseFocus();
}
The last method to write will allow us to add new chat messages to the UI. We’ll need to create a
new instance of the dynamic_label.tscn scene and add it as a child in the appropriate place.
Also, we’ll want to set _justAddedNewMsg flag to true so that it will scroll to the new
message.
// Add a ChatMsg to the message box
public void AddMsg(ChatEntity sender, string msg)
{
// Add the new message to the box
Label newMsgLabel = _dynamicLabelScene.Instantiate<Label>();
newMsgLabel.Text = sender.ChatName + ": " + msg;
_msgBox.AddChild(newMsgLabel);
// Make sure we will scroll to the new message
_justAddedNewMsg = true;
}
Part 2: The Main Scene
Now we will create the main scene where we can bring the level and the UI together.
Setting Up the Scene
Create a new scene and add an HSplitContainer as the root node. Rename the node to Main and
save the scene. Using an HSplitContainer allows us to put the level on one side of the screen
and the UI on the other side and gives us a draggable border between them. Set the following
property so that the HSplitContainer takes up the whole screen:
Control > Layout > Anhors Preset = “Full Rect”
Add a SubViewportContainer as a child of the Main node. Then add a SubViewport as a
child of the SubViewportContainer. Set the following properties for the
SubViewportContainer so that the level will expand to fill the left 80% of the screen by
default:
SubViewportContainer > Stretch = On (checked)
Control > Layout > Container Sizing > Horizontal = Expand (checked)
Control > Layout > Container Sizing > Stretch Ratio = 4
Add a MarginContainer as a child of the Main node (below the SubViewportContainer).
This is used to add a margin around the user interface so that it has some separation from the edge
of the window. Set the following properties:
Control > Theme Overrides > Constants > Margin Top = 12
Control > Theme Overrides > Constants > Margin Right = 12
Control > Theme Overrides > Constants > Margin Bottom = 12
Also set the following properties on the MarginContainer so that the UI will expand to fill the
right 20% of the screen by default:
Control > Layout > Container Sizing > Horizontal = Expand (checked)
Control > Layout > Container Sizing > Stretch Ratio = 1
Finally, we can add the level and UI scenes. Instantiate level.tscn as a child of the
SubViewport, and instantiate user_interface.tscn as a child of the MarginContainer. The
main scene should now look something like this:
Writing the Script
If you remember, our UI script has a property that is meant to point to the ChatPlayer node.
Rather than having the UI script set that property itself, we’ll have our Main node do so.
Add a C# script to the Main node. Save the script as Main.cs. In this script, we’ll just need
to override the _Ready() method with the following code. This code searches in the
SubViewport for a node called ChatPlayer . It then gets the UI node and sets the
MyChatPlayer property of the UI to point to the ChatPlayer node that was found.
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
// Connect the UI up to the first ChatPlayer found
ChatPlayer chatPlayer = GetNode("SubViewportContainer/SubViewport").FindChild("ChatPlayer") as ChatPlayer;
UserInterface userInterface = GetNode<UserInterface>("MarginContainer/UserInterface");
userInterface.MyChatPlayer = chatPlayer;
}
Try It Out
The main scene should now be ready to run! Press F5 (or the “Run Project” button). When the dialogue box asks you to select a main scene, click the “Select Current” button to select the main.tscn scene.
Tip
You can also change the main scene in the project settings under Application > Run > Main Scene.
Confirm that you can still move Seraphis around as before. You should now also be able to see a control hint message that changes based on the situation, you should see Gralk show up on the “Nearby Entities” section, and you should be able to press SHIFT to start a conversation and ESC to end a conversation with Gralk. Finally, you should be able to send messages to and receive messages from Gralk via the UI.
At this point, if everything is working correctly, feel free to delete the print statements that we
had temporarily added to the ChatEntity, ChatAI, and ChatPlayer scripts.