The Player
In this section we will create the ChatPlayer node, which will be another extension of
ChatEntity. This node will be attached to the player character and will allow it to interact
with the ChatAI nodes. It will be controlled by keyboard and UI input from the player. By the end
of this section, we should be able to see when the player gets close enough to “see” Gralk, when
they start and end conversations, and when they send and respond to messages.
Note
Since we don’t have the UI set up yet, all of this will be shown in the output terminal for now and messages will need to be coded directly in our scripts. After we create the UI in the next section, everything will be visible on the game screen.
Part 1: Writing the ChatPlayer Script
To start, we’ll write the ChatPlayer script, which will extend ChatEntity and will define
how players can interact with AI-powered NPCs. Right click the filesystem dock and create a new
script. Make sure the language is set to C# and it inherits from ChatEntity. Save the script as
ChatPlayer.cs.
Once the script is created, open it up and add the [GlobalClass] attribute above the class
declaration.
using Godot;
[GlobalClass]
public partial class ChatPlayer : ChatEntity
{
(...)
}
Warning
Godot may automatically add a _Ready() method to the script. If it does,
delete it. An empty _Ready() method will prevent the base _Ready()
method from being called.
Properties
With the ChatPlayer class set up, we can add a few properties to it. First, add a
_controlHint property. This will hold a message that will be displayed to the player to
indicate what kinds of inputs and actions they can do at any given moment.
// The current control hint
private string _controlHint;
Next, add a few signals. We’ll set up these signals to be emitted whenever ChatPlayer does
anything that we might want the UI to be aware of.
// Signals for connecting to the UI
[Signal]
public delegate void ControlHintUpdatedEventHandler(string controlHint);
[Signal]
public delegate void ChatEntityAddedEventHandler(ChatEntity chatEntity);
[Signal]
public delegate void ChatEntityRemovedEventHandler(ChatEntity chatEntity);
[Signal]
public delegate void ConvoStartedEventHandler();
[Signal]
public delegate void ConvoEndedEventHandler();
[Signal]
public delegate void MsgAddedEventHandler(ChatEntity sender, string msg);
Note
Our use of signals here follows the “call down, signal up” convention. Although the UI will not
technically be the parent or ancestor of ChatPlayer, it will exist somewhat higher up in
the scene tree. Additionally, it would never really make sense to use the UI where there is
no ChatPlayer node, but it could make sense to use the ChatPlayer node in a situation
where there is no UI. Using signals here makes it so that the UI doesn’t have to exist in order
for ChatPlayer to function properly.
Control Hint Methods
The first method to add in the ChatPlayer script is the _Process() method, which we’ll use
to keep the control hint updated. Each frame, we’ll evaluate the current game situation and
determine whether we need to update the control hint. If we do update it, we’ll also emit a signal
indicating that.
// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
// Figure out what the control hint should be for this frame
string newControlHint = "Use the arrow keys to move";
if (InConvo())
{
newControlHint = "Press ESC to end the conversation";
}
else
{
ChatEntity nearestChatEntity = NearestChatEntity();
if (nearestChatEntity != null)
newControlHint = $"Press SHIFT to talk with {nearestChatEntity.ChatName}";
}
// If the control hint should be different, update it
if (newControlHint != _controlHint)
{
_controlHint = newControlHint;
EmitSignal("ControlHintUpdated", _controlHint);
GD.Print($"Control Hint: {_controlHint}");
}
}
Note
We’ve added a GD.Print() statement to this method so that we can see in the output terminal
when the control hint changes. The other methods we’ll write for ChatPlayer will have
similar print statements. This is a temporary solution until we complete the UI in the next
section.
Conversation Methods
The next few methods we’ll write will control starting and ending conversations. We want the player
to be able to trigger these methods using keyboard input, so first we need to create two new
actions in the project’s input map: start_convo and end_convo. Let’s add the SHIFT key as
an input event for start_convo and the ESC key as an input event for end_convo.
With that set up, we can override the built-in _Input() method to listen for these actions and
call the corresponding method, either StartConvo() or EndConvo(). We’ll also use
InConvo() to check and make sure the player’s input is valid for the current situation.
// Listen for input to start/end a conversation
public override void _Input(InputEvent @event)
{
// If input is "start_convo", try to start conversation with the closest entity
if (@event.IsActionPressed("start_convo") && !InConvo())
{
ChatEntity nearestChatEntity = NearestChatEntity();
if (nearestChatEntity != null)
StartConvo(nearestChatEntity);
}
// If input is "end_convo", try to end conversation
else if (@event.IsActionPressed("end_convo") && InConvo())
{
EndConvo(_inConvoWith);
}
}
Now we can write StartConvo() and EndConvo() for starting and ending conversations. These
will be overrides of the ChatEntity methods. We’ll first call the base methods. Then, assuming
everything was set up correctly, we’ll notify the NPC that we started or ended a conversation with
them and will emit the corresponding signal.
// Attempts to start a conversation with another ChatEntity.
// Will fail and return false if otherChatEntity is already in a conversation.
public override bool StartConvo(ChatEntity otherChatEntity)
{
// Do the basic stuff
if (!base.StartConvo(otherChatEntity))
return false;
// If otherChatEntity is a ChatAI, notify it of the start of the conversation
if (otherChatEntity is ChatAI otherChatAI)
otherChatAI.Notify($"{ChatName} has started a conversation with you.");
// Indicate that a conversation was started
EmitSignal(SignalName.ConvoStarted);
GD.Print($"Conversations: Started a conversation with {otherChatEntity.ChatName}");
// Return success
return true;
}
// Attempts to end a conversation with another ChatEntity
// Will fail and return false if otherChatEntity is not in a conversation with this ChatEntity
public override bool EndConvo(ChatEntity otherChatEntity)
{
// Do the basic stuff
if (!base.EndConvo(otherChatEntity))
return false;
// If otherChatEntity is a ChatAI, notify it of the end of the conversation
if (otherChatEntity is ChatAI otherChatAI)
otherChatAI.Notify($"{ChatName} has ended their conversation with you.");
// Indicate that a conversation was ended
EmitSignal(SignalName.ConvoEnded);
GD.Print($"Conversations: Ended a conversation with {otherChatEntity.ChatName}");
// Return success
return true;
}
Messaging Methods
Next, we’ll override the SendMsg() and ReceiveMsg() methods from ChatEntity. Besides
calling the base method in SendMsg() (which does the work of sending the message to
the entity that we’re currently in a conversation with), we’ll also emit a signal that can be
picked up by the UI.
// A useful shorthand for sending a message
public override void SendMsg(string msg)
{
// Send the message to ChatEntity _inConvoWith
base.SendMsg(msg);
// Emit a signal that there is a new message (for UI)
EmitSignal(SignalName.MsgAdded, this, msg);
}
// Called when ChatEntity _inConvoWith emits a MsgSent signal
public override void ReceiveMsg(string msg)
{
// Emit a signal that there is a new message (for UI)
EmitSignal(SignalName.MsgAdded, _inConvoWith, msg);
}
Note
The SendMsg() method should already have a print statement in its base definition in the
ChatEntity class. Because of this, we haven’t added any print statements here.
Nearby ChatEntity Methods
Finally, the last two methods that we’ll write for ChatPlayer will be OnChatEntityEntered()
and OnChatEntityExited(). These are also overrides of ChatEntity methods. The base
methods simply keep track of nearby entities as they come into or move out of the player’s
vicinity. Besides calling these base methods, we’ll also emit a signal that the UI can pick up.
// Called when another ChatEntity enters the collision area of this ChatEntity
protected override void OnChatEntityEntered(ChatEntity enteringChatEntity)
{
base.OnChatEntityEntered(enteringChatEntity);
EmitSignal("ChatEntityAdded", enteringChatEntity);
GD.Print($"Nearby Entities: {enteringChatEntity.ChatName} has entered your vicinity");
}
// Called when another ChatEntity exits the collision area of this ChatEntity
protected override void OnChatEntityExited(ChatEntity exitingChatEntity)
{
base.OnChatEntityExited(exitingChatEntity);
EmitSignal("ChatEntityRemoved", exitingChatEntity);
GD.Print($"Nearby Entities: {exitingChatEntity.ChatName} has exited your vicinity");
}
Part 2: Using the ChatPlayer Node
Having written the ChatPlayer script, we’re now ready to add a ChatPlayer node to the
Seraphis scene. Doing so will enable the player to interact with Gralk and other ChatAI NPCs.
Attaching to Seraphis
First, open up the seraphis.tscn scene that we made previously. Add a new ChatPlayer node as
a child of the Seraphis node.
Note
Since we used the [GlobalClass] attribute on the ChatPlayer script, it should show up
as an option in the “Create New Node” dialogue box. If it is not showing up, you may have to
rebuild the project first. You can do this by clicking the “Build” button in the top-right
corner of the screen. For more information about Godot’s global classes, see
https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/c_sharp_global_classes.html.
On the ChatPlayer node, set the ChatName property to “Seraphis”. In this project, setting
ChatDescr is not necessary for player characters. Feel free to leave it blank or set it to any
appropriate description.
Since the ChatPlayer node inherits from Area2D, it expects to find some sort of collision
node as one of its children. Let’s add a CollisionShape2D as a child of the ChatPlayer
node. Then set the following property:
CollisionShape2D > Shape = CircleShape2D
For the CircleShape2D, set the following property:
CircleShape2D > Radius = 75px
Tip
You can change the shape to anything that seems appropriate to you. But remember that the
ChatPlayer node won’t be able to “see” anything until it enters the collision shape, so
make sure that the shape extends some distance beyond the borders of the sprite.
Your Seraphis scene should now look something like this:
Updating Seraphis Script
Finally, we also need to make a small change the Seraphis.cs script. Right now, it will be possible for the player to move around while in a conversation. This might be confusing when editing messages, as arrow key input could be meant to move the character around on the map or it could be meant to move the cursor around in the text input box. To solve this problem, we’ll just make it impossible for the player to move while in a conversation.
To implement this solution, we’ll need our Seraphis.cs script to have a reference to the
ChatPlayer node. Open up the script and declare a new property called MyChatPlayer. Then,
in the _Ready() method, search for the ChatPlayer node and set MyChatPlayer to
reference it.
// The ChatPlayer node
public ChatPlayer MyChatPlayer;
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
// Set up MyChatPlayer
MyChatPlayer = FindChild("ChatPlayer") as ChatPlayer;
if (MyChatPlayer == null)
GD.PrintErr(Name + " cannot find ChatPlayer");
}
Now that we have access to the ChatPlayer node, we can check to see whether it is in a
conversation by calling the InConvo() method. If it is in a conversation, we’ll prevent the
player from moving by setting velocity to zero. Otherwise, we’ll allow the player to move like
normal. To achieve this behavior, change the _PhysicsProcess() method to the following:
// Called once per physics tick
public override void _PhysicsProcess(double delta)
{
// In a conversation, cannot move
if (MyChatPlayer.InConvo())
{
Velocity = Vector2.Zero;
}
// Not in a conversation, can move
else
{
// Get input vector
var velocity = Input.GetVector("move_left", "move_right", "move_up", "move_down");
// Set correct magnitude
if (velocity.Length() > 0)
velocity = velocity.Normalized() * Speed;
// Set Velocity property of this CharacterBody2D
Velocity = velocity;
}
// Call Godot's built-in function for physics-based movement
MoveAndSlide();
}
Try It Out
Now that the ChatEntity, ChatAI, and ChatPlayer scripts have all been completed and we
have them attached to the Gralk and Seraphis scenes, we should be able to hold a simple
conversation between the player and Gralk.
First, since we haven’t created the UI yet, we don’t have a way to write messages while the game is
running. For now, let’s hard code a message into the ChatPlayer script. Add the following line
just before the return statement in the StartConvo() method. Now the message “Hello! How are
you on this fine day?” will be sent automatically whenever we start a conversation.
public override bool StartConvo(ChatEntity otherChatEntity)
{
(...)
// (Temporary) Send greeting message to otherChatEntity
SendMsg("Hello! How are you on this fine day?");
// Return success
return true;
}
With this set up, let’s test everything to make sure it is working. Open level.tscn and run the scene. You should immediately see the following message in the Output tab:
Control Hint: Use the arrow keys to move
Move toward Gralk. When you get close enough, the following messages should show up:
Nearby Entities: Gralk has entered your vicinity
Control Hint: Press SHIFT to talk with Gralk
Press SHIFT to start a conversation with Gralk. You should then see the following:
NOTIFY: Seraphis has started a conversation with you.
Conversations: Started a conversation with Gralk
Seraphis: Hello! How are you on this fine day?
Control Hint: Press ESC to end the conversation
After a moment, you should get a response from Gralk that looks something like this:
Gralk: Ah, greetings, traveler! I am Gralk the Wise, guardian of this bridge. I'm as well as a troll can be, thank you. Now, before you cross, there's a small matter of a riddle to solve. Do you have a subject in mind, preferably related to the Forgotten Realms universe?
End the conversation by pressing ESC. You should see the following:
NOTIFY: Seraphis has ended their conversation with you.
Conversations: Ended a conversation with Gralk
Control Hint: Press SHIFT to talk with Gralk
Finally, move away from Gralk. Once you have gotten far enough away, the following messages should show up in the Output tab:
Nearby Entities: Gralk has exited your vicinity
Control Hint: Use the arrow keys to move
Before moving on to the next section, it would be a good idea to delete the line that we added to
the StartConvo() in the ChatPlayer script.