RPG Development Log #3

I thought this blog post would be about the creating the plot placing menu but turns out there was a decent amount of work to on the messaging system before I could even get started!

Before

Before going down the route of refactoring, the test NPC had display dialogue code that looked something like this:

public override Task Interact(ICharacter initiator) {
    return MessageDisplayManager.Display(new[] {
        "Hi, I can talk.", 
        "Here's some more text."
    });
}

This was fairly basic. Pass in an array of messages to display and the display manager would handle showing text when the user pressed the action key. It returned a task so that the caller could know when the message was finished displaying. For our purposes, this allowed the npc to start facing the direction they were facing before the conversation started.

Issues

When designing how the user would select a choice from several options (e.g. picking a building from a plot) a couple issues came to mind.

  • Options should be able to be shown while there is also a message being shown
  • Options should be able to be shown while there is not a message being shown
  • When an option is shown with a message also being shown, the message should be automatically advanced once the option is selected.
  • It should be easy to show different dialogue depending on what option is chosen.

None of these were particularly compatible with how the current display manager worked. There was no way to conditionally show messages or control advancement of a message outside of user input.

What to do?

The message display manager needed some work to be able to satisfy the above constraints. It would be short sighted to make the messages tightly coupled with option choosing so a more general approach was taken where messages could be shown in an asynchronous manner, allowing multiple display calls to be chained together with C#’s await syntax. Additionally messages could also contain a lambda function to perform an action once all the dialogue characters had been drawn on screen.

How it ended up

The test NPC code morphed into something a little larger to test the scenarios.

public override async Task Interact(ICharacter initiator) {
    await MessageDisplayManager.DisplaySet(new[] {
        new Message("Hi, I can talk."),
        new Message("Here's some more text.", async () => {
            MessageDisplayManager.AdvanceToNextMessage();
            await MessageDisplayManager.DisplaySet(new[] {
                new Message("Interupt")
            });
            await MessageDisplayManager.DisplaySet(new[] {
                new Message("Post Interupt")
            });
        }),
        new Message("Last Message")
    });
    await MessageDisplayManager.DisplaySet(new[] {
        new Message("A new message still!")
    });
}

With a video for good measure.

From the above code you can see a lambda function is called after the second message. It manually specifies that the message should advance and then invokes a nested display call. All of the display calls can be awaited in order to make the control flow easy to work with.

Learnings along the way

There were a couple interesting interactions with Godot that made this take a while to get worked out (and a while to write this post!)

  • There was an issue adding the message box (the actual object doing the displaying of messages) to Godot’s scene tree
    • To chain the calls of displaying messages, the message manager had to make a decision on when to add or remove the message box from the scene tree. It tried its best to leave it in the tree as long as it had text to display but occasionally it removed the message box and then immediately had to add it back. Sometimes this would error out. Turns out I was adding the message box through an immediate call, while removing in in a deferred call. Once they were both updated to use deferred things started working without error.
  • There was also an issue where sometimes the final message at the end of the chain would never show. I was having a race condition where a display call was being invoked in the middle of a removal call. This led to the message being added to the queue but the message box never being added to scene tree again! My issue was that I was using the lock statement. Which if you look carefully says it allows the same thread to acquire the lock even if it hasn’t been exited yet! Switching to a Semaphore fixed that issue.

That was a lot of talk for not a lot of changes! Hopefully next time will have something more fun to look at.