Medevac’s Event Based Dialogue System

While working on Medevac, the one system I spent the most time on and am the most proud of is the event based dialogue system. Going into this, I wanted to achieve two main objectives: create a system that allowed other developers to make changes to the dialogue without needing to look at any code, and create a system that would be easy to expand if needed. Most of this system is based loosely off of the Observer pattern described in Game Programming Patterns. If you haven’t yet given that book a read, I highly recommend it.

Note: All code shown is Unity C#, and has been drastically stripped down/simplified from its current state for the sake of demonstration.

Dialogue Event Listener

public class DialogueEventListener
{
    private DialogueEventType mType;
    private List<DialogueEventCondition> mConditions;
    private AudioClip mClip;
    //...
}

At the core of everything was the DialogueEventListener, something like the observer mention before. The class contains an enum of which type of event it is listening for, a list of conditions, and the AudioClip for the dialogue that will be played. The event listeners were built to be self contained and modular, so that any event type could have an arbitrary number of conditions attached to it. When an event that matched the event type is fired, the listener iterates through all of its conditions. If all conditions are met, the AudioClip is played.

Dialogue Event Condition

public class DialogueEventCondition
{
    private DialogueEventConditionType mType;
    //...
    public virtual bool IsTrue()
    {
        return true;
    }
}

The DialogueEventCondition class is slightly more complicated than DialogueEventListener. All conditions inherit from the DialogueEventCondition class shown above, which contains an enum of which type of condition it is, and a virtual IsTrue() function. An example of possible specific condition is given below.

public class ShipHealthBelowCondition : DialogueEventCondition
{
    private float mValue;
    //...
    public override bool IsTrue()
    {
        return GetShipHealth < mValue;
    }
}

Dialogue System Controller

The DialogueSystemController is what ties everything together. It manages all of the DialogueEventListeners, receives all events fired, and loads all of listeners from a config file (more on this later). At its most basic, the controller looks something like this:

public class DialogueSystemController : MonoBehaviour {

    private List<DialogueEventListener> mEventListeners;

    private void LoadDialogue()
    {
        //...
    }

    public void ReceiveEvent(DialogueEventType type)
    {
        foreach(DialogueEventListener listener in mEventListeners)
        {
            if (listener.Type == type)
                listener.TryRun(); //Listener checks conditions and runs if true
        }
    }
}

Loading From A Config File

In order to allow other developers to add/edit/remove dialogue events, I created a simple config file system so that they could edit a config file instead of needing to change any code. The config file follows this basic format:

AudioClipFilepath
DialogueEventType
NumberOfConditions
Condition0Type, value0, ..., valueN
...
ConditionNType, value0, ..., valueN

All of these blocks are separated by empty lines. Here’s an example block from Medevac:

Dialogue\Audio\Tutorial\03_Bring_Back
SURVIVOR_PICKUP
2
SPECIFIC_SURVIVOR, wounded1
SPECIFIC_SURVIVOR, wounded2

So, this event will trigger when a survivor is picked up, and both survivors wounded1 and wounded2 are already saved. The function that parses this config file and loads the data looks something like this:

private void LoadDialogue()
{
    mEventListeners = new List<DialogueEventListener>();

    TextAsset currentLevelDialogue = Resources.Load<TextAsset>(filepath);
    StringReader reader = new StringReader(currentLevelDialogue.text);
    string line;

    line = reader.ReadLine();

    while (line != null)
    {
        // Allows commenting in the config files with #, and makes sure it skips empty lines
        if (line != "" && line[0] != '#')
        {
            // Filepath for AudioClip
            line = reader.ReadLine();
            AudioClip clip = Resources.Load<AudioClip>(line);

            // Event type
            DialogueEventType eventType = (DialogueEventType)System.Enum.Parse(typeof(DialogueEventType), reader.ReadLine());

            // Create new listener
            DialogueEventListener newListener = new DialogueEventListener(eventType, clip);

            // Add conditions to listener
            int numConditions = int.Parse(reader.ReadLine());
            for (int i = 0; i < numConditions; i++)
            {
                line = reader.ReadLine();
                string[] entries = line.Split(',');

                // This removes empty spaces at the beginning of entries
                for (int j = 0; j < entries.Length; j++)
                {
                    if (entries[j][0] == ' ')
                        entries[j] = entries[j].Substring(1);
                }

                DialogueEventConditionType conditionType = (DialogueEventConditionType)System.Enum.Parse(typeof(DialogueEventConditionType), entries[0]);
                // Separate function since each condition is a separate class
                AddCondition(newListener, conditionType, entries);
            }

            // Add new listener to list
            mEventListeners.Add(newListener);
        }

        line = reader.ReadLine();
    }
}

Possible Changes and Improvements

The one big change I would like to make is to yank out the config file system entirely, and replace it with some kind of custom Unity inspector. While it made sense at the time to use config files as a quick and easy solution, the time needed to type out every config file turned out to be a chore.

Conclusion

This project taught me two things: The importance of a solid base when designing a system, and the importance of a user friendly front end. I was able to design a system that was easy to program, debug, and expand the functionality of. However, the config file system I chose to use ended up being a chore on other developers, and it would have definitely been worth it to spend the time learning to create a more friendly front end. I spent a lot of time on this system, and am proud of the work I did, so much that I plan on continuing this into the summer. I plant on focusing on the front end, and will hopefully be able to develop a system more friendly than config files.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s