Conversation

Anatomy of a Conversation to Oblivion

In Consigned to Oblivion a conversation is a linear exchange with no options or alternative lines and basic dialogue follows a prompt -> response format.
Each prompt or response may span multiple dialog boxes, and one sided conversations can occur when the main character does some observational explication.

A line of Speech

Speech is a simple data container. It holds information about the text string being spoken and the image sprite of the speaker. Eventually, I added per-speech configuration data for animation, position, timers and the UI container prefab.

Using Odin Inspector attributes I set up a nice GUI including a foldout that contains a summary of the options set. You how this comes together in the last section.

UI

The UI is quite straight forward, on a basic level it consists of an avatar image of the speaker, and a text box.
This information can be presented and nested in any UI elements as long as the location is exposed to the DialogueBox monobehaviour on the root parent.

dialoguebox

uishader

I created all of the graphics except for the avatars. The UI elements consist of a sliced border image and a grayscale mask for the emmission on the edges (using a custom shader).

DialogueBox is a consumer of the Speech class, taking in its data/animation properties. It is responsible for presenting the Speech information given to it, animating (fade in/out etc, from right/left/top/bottom). It is also responsible for its own dismisal either on a timer, or by user input (clicking it).

All processing happens inside coroutines, and the main entry point (below) returns an IEnumerator, this is so the calling class can take care of all execution itself, enumerating and sequencing multiple DialogueBox's

    public IEnumerator DoDialogue(Speech speech, Action callback = null, bool disposeOnEnd = true)
    {
        // wait until initialized
        yield return new WaitUntil(() => isActiveAndEnabled);

        // cache data
        SetData(speech.Image, speech.Text, speech.Position);

        // do tween in
        yield return StartCoroutine(Animate(speech.Anim, speech.Position, AnimationDirection.In));

        // reset the input based out condition (click)
        isDismissed = false;

        // Timed dialogue or must be dismissed?
        if (speech.IsTimed)
        {
            yield return new WaitForSeconds(speech.Timer);
        }
        else
        {
            yield return new WaitUntil(() => isDismissed);
        }

        // tween out
        yield return StartCoroutine(Animate(speech.Anim, speech.Position, AnimationDirection.Out));

        // Execute any callback
        if (callback != null)
        {
            callback.Invoke();
        }

        // cleanup dialogue
        if (disposeOnEnd)
        {
            Destroy(gameObject);
        }

    }

Conversation

Given the above, a Conversation is a simple construct, its a monobehaviour wrapper around a collection of Speech. Since each Speech carries its own data and DialogueBox prefab reference, the majority of the work of this class is simple, iterate over each Speech instantiating the DialogueBox prefab and passing in the Speech data. As with the case above, StartCoroutine returns a Coroutine which is a YieldInstruction, so rather than manually iterating over the enumerator of DoDialogue, we can just yield as a nested coroutine and wait for it to finish.

/// <summary>
///     A Collection of Speech items that can be executed asynchronously
/// </summary>
public class Conversation : MonoBehaviour
{
    /// <summary>
    ///     True if the conversation blocks player input
    /// </summary>
    private bool BlockInput = false;

    /// <summary>
    ///     The conversation list
    /// </summary>
    private List<Speech> conversationList;

    /// <summary>
    ///     Starts the conversation.
    /// </summary>
    public void StartConversation()
    {
        StartCoroutine(DoConversation());
    }

    /// <summary>
    ///     Does the conversation execution.
    /// </summary>
    private IEnumerator DoConversation()
    {
        // disable player controller if required
        if (BlockInput)
        {
            GlobalGameStateManager.Instance.DisablePlayerInput();
        }

        // iterate over conversations
        if (conversationList.Count > 0)
        {
            for (var i = 0; i < conversationList.Count; i++)
            {
                var conversation = conversationList[i];
                var dialog = Instantiate(conversation.DialoguePrefab);

                // run DialogueBox as nested coroutine
                yield return StartCoroutine(dialog.DoDialogue(conversation));
            }
        }

        // return player input
        if (BlockInput)
        {
            GlobalGameStateManager.Instance.EnablePlayerInput();
        }
    }
}

With all the Editor GUI tweaks, a Conversation looks like this in the editor: conversation

In partnership with the interaction/trigger systems I built, any conversation can be created from any interactable event/trigger/collision/action even if the conversation is contained within another gameobject. My team have leveraged this well, naturally creating conversations and storing them stored to other objects in the scene hierarchy for ease of editing (under a conversation container object), even if they are activated from some deeply nested gameobject.

Final Thoughts

I would have liked the game to leverage conversation trees and decisions, this would have made each playthough slightly diferent in terms of interaction between characters. Not only that, the extra interaction and investment with the story would have drawn the player into the game more.

The final positioning of the dialogue boxes is flawed, in that, conversations that happen between characters have the player read and then click on oposite sides of the screen. This is tiring, it would have been better if all conversations ended up with the text box centered at the bottom/top of the screen, and the avatar offset to the left/right. This way you know who is talking, but interation with the conversation is not jarring.

Finally, I would have liked to bring ScriptableObject to this, it is a perfect application, Speech is a self contained data structure requiring no outside references to function. Having these as editable assets would have been much better, it would have allowed for easier reuse of repeated text, and could have even enabled random dialogue selection for cirtain events.