Chapters

Hide chapters

Unity Apprentice

First Edition · Unity 2020.3.x LTS Release · C# · Unity

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

8. Scriptable Objects
Written by Eric Van de Kerckhove

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

In the previous chapter, you learned the basics of Unity’s user interface system and used the most common UI elements to create a title screen and a window. This knowledge will come in handy when completing the game with a flexible dialogue system that you’ll also be able to use in your own games with ease.

This chapter is all about giving the NPCs a way to talk to the player’s avatar and letting the player choose how to respond. This lets you add some personality to the characters.

The dialogue system you’ll create will teach you the basics of how to use scriptable objects to save conversations as files. This same concept will allow you to create items, weapons and settings presets as files for your own games. Scriptable objects are immensely powerful once you know how to use them, and they’ll make your game development journey a lot easier.

Dialogue user interface

Before diving into the concept of the dialogue system, there’s a new window you need to meet: the Dialogue Window. This GameObject is a child of Canvas in the Hierarchy and is disabled by default to hide it. Open the Chapter 8 Starter project in Unity and then open the Dining Hall scene in RW / Scenes. Then, expand the Canvas and select Dialogue Window. In the Inspector, click the checkbox in front of its name to make the window visible in both the Scene and Game views.

To get a good look at the different parts, fully expand and enable all of Dialogue Window’s children in the Hierarchy.

Take a look at the bottom of the Game view to see the Dialogue Window. The dialogue window might seem quite a bit more complex than what you’ve seen up until now, but it’s actually just made up of some images and text.

Here’s an overview:

  1. Speaker Tag and Speaker Text: The tag here is a dark image element that automatically resizes itself to the size of Speaker Text. Speaker Text is a text element that shows the name of the speaker — this can be “Guard” or “Potato guy,” for example. You can test this out by changing the text value of Speaker Text.
  2. Line Text: This is a text element that shows what the speaker is saying or asking.
  3. Option Selection Image and Answers: The white triangle on the left is an image that acts like a selection cursor. It can be moved to highlight the selection option. The two text elements are the possible answers when a question is asked. All of these UI elements will only be visible when a question is asked.
  4. Continue Indicator Image: This is an arrow image that blinks on and off via a small script. It’s used to indicate that the interact button can be pressed to advance the conversation.

Now that you know the different parts of the dialogue window, disable Option Selection Image and Answers again, followed by the Dialogue Window GameObject itself so it won’t be in the way. From now on, the different parts will be shown or hidden with scripting.

Dialogue manager overview

This brings you to the Dialogue Manager — it’s a child of the GameObject called Managers in the Hierarchy.

    public static DialogueManager Instance;

    // Player input

    public PlayerAvatar playerAvatar;
    private PlayerInput playerInput;

    ...

    // Conversation
if (Instance == null)
{
    Instance = this;
}

Creating a dialogue system

A dialogue system in video games can assume many forms, but in essence it allows you to speak with characters and make a choice now and then. Think of old school RPGs like EarthBound, Secret of Mana or Breath of Fire — they all allow you to walk around and talk to the people (and creatures) around you.

Uhcifuhh Nwiq onm uzyali TaomuvuoRwanjid Tica: Zefriiyex Diwt: Foxv be Vaqpigyabaor Zazmagmewuep Zsoejeq Cobh Qooloxii Doqi Norz Bu! Fuefuraa Pipe Laq Momqi. Vaevifeo Rahu LuoluhoaYexavof Avlorna cojyipporoaw Qiajolai Qoyvij

Lines of dialogue

The first order of business is to create the DialogueLine class, which holds the data for a single line of dialogue. Create a new C# script named DialogueLine in RW / Scripts / Dialogue and open it in your code editor. Unity’s default script template will make this yet another class that derives from MonoBehaviour to become a component. DialogueLine shouldn’t be a component, though, but a simple class that holds some variables instead.

public class DialogueLine
{
}
using System.Collections;
using System.Collections.Generic;
using System;
[Serializable]
public string speaker; // 1

[TextArea(2, 3)] // 2
public string text; // 3

Conversations

A basic conversation is a simple list of dialogue lines with one or more speakers that will play one line after the other like a movie script.

Writing scriptable objects

There’s a far better alternative for storing data in a central location, and it’s built right into Unity: scriptable objects! You can use a scriptable object as a data container and store it as an asset, like an image or a sound effect. You can then reference the scriptable object in a component and read its contents. As a bonus, you can edit its properties right in the Inspector like components and the asset files are easily edited in a text editor. Yes, scriptable objects are truly marvelous, and you should use them when you can.

public DialogueLine[] dialogueLines;
public class Conversation : ScriptableObject
{
    public DialogueLine[] dialogueLines;
}
[CreateAssetMenu(fileName = "Conversation", menuName = "Dialogue/Conversation", order = 1)]

Creating scriptable objects

Create a new folder in the RW folder named Conversations, and right-click on the new folder. Next, select Create ▸ Dialogue ▸ Conversation to create a new conversation, and name it Weird Taste.

Dialogue starter component

To make the NPCs interactable, you’ll use the interaction system. You learned about this in the previous chapter, but here’s a quick refresher:

public class DialogueStarter : InteractableObject
{
}

public override void Interact(PlayerAvatar playerAvatar)
{
}
public Conversation conversation;
private void Awake()
{
    gameObject.layer = LayerMask.NameToLayer("Interaction");
}
foreach (var line in conversation.dialogueLines)
{
    print(line.speaker + " : " + line.text);
}

Improving the dialogue manager

The Dialogue Manager serves three purposes:

Starting a conversation

The first order of business is to add these variables below the // Conversation comment, above the Awake method:

private Conversation activeConversation; // 1
private int dialogueIndex; // 2
private DialogueStarter dialogueStarter; // 3
private void ShowTextUI()
{
    answersParent.SetActive(false); // 1
    optionSelector.gameObject.SetActive(false); // 2
    continueIndicator.SetActive(true); // 3
}
private void ShowLine()
{
    DialogueLine currentLine =
        activeConversation.dialogueLines[dialogueIndex]; // 1
    speakerText.text = currentLine.speaker; // 2
    lineText.text = currentLine.text; // 3

    ShowTextUI(); // 4
}
private void SetDialogueWindowVisibility(bool visible)
{
    dialogueWindow.SetActive(visible);
}
public void StartConversation(Conversation conversation,
                              DialogueStarter dialogueStarter) // 1
{
    playerAvatar.DisableInput(); // 2
    activeConversation = conversation; // 3
    this.dialogueStarter = dialogueStarter; // 4
    dialogueIndex = 0; // 5
    SetDialogueWindowVisibility(true); // 6
    ShowLine(); // 7
    playerInput.enabled = true; // 8
}
DialogueManager.Instance.StartConversation(conversation, this); // 1
canBeInteractedWith = false; // 2

Showing more lines of dialogue and ending a conversation

First, the avatar must be able to interact with the dialogue starter again once it’s done talking. To achieve that, open the DialogueStarter script again from RW / Scripts / Dialogue and add the following method below Interact:

public void OnConversationEnd()
{
    canBeInteractedWith = true;
}
public void EndConversation()
{
    playerInput.enabled = false; // 1
    playerAvatar.EnableInput(); // 2
    activeConversation = null; // 3
    SetDialogueWindowVisibility(false); // 4
    dialogueStarter.OnConversationEnd(); // 5
}
private void NormalTextInteract(DialogueLine currentLine) // 1
{
    dialogueIndex++; // 2

    if (activeConversation.dialogueLines.Length > dialogueIndex) // 3
    {
        ShowLine();
    }
    else
    {
        EndConversation();
    }
}
if (!activeConversation || !context.performed) // 1
{
    return;
}

DialogueLine currentLine =
   activeConversation.dialogueLines[dialogueIndex]; // 2
NormalTextInteract(currentLine); // 3

Handling questions

Now that you can let the characters talk to the player avatar, it might be interesting if they can ask questions as well. This makes the conversations feel more dynamic and interesting. Every line of dialogue will have the option of becoming a question, which will prompt the player for two possible answers. Each of these answers will be linked to a different conversation asset that will be loaded by the Dialogue Manager.

Rodtukzufeub Ik, fioj teuwzukb. Subliysuxeiq Gof’y oin bxo gaad. A hbixn og’c sakbl. Xevcebyepeay Fohqu! Qi fau rijp ungimi? Ko. Noj!

Dialogue question class

First, create a new C# script in the RW / Scripts / Dialogue folder and name it DialogueQuestion. Open the new file in a code editor and remove both the Start and Update methods. This will be another serializable class like DialogueLine, so replace the class declaration line with this:

public class DialogueQuestion
using System;
[Serializable]
// 1
public string firstOption;
public string secondOption;

// 2
public Conversation conversationWhenFirstOptionWasSelected;
public Conversation conversationWhenSecondOptionWasSelected;
public bool thisIsAQuestion; // 1
public DialogueQuestion dialogueQuestion; // 2

Setting up the editor for questions

The boss of this place is standing before the huge wooden door at the back. She’s going to ask the player avatar whether he’s ready to enter the arena. The player will then have the choice of answering yes or no, which will trigger another conversation. In other words, you’ll need to create three conversations for the boss.

Preparing the dialogue manager for questions

The dialogue manager can’t differentiate between statements and questions at the moment, so it’s up to you to make it smarter. This involves some more scripting, of course, so open the DialogueManager script from RW / Scripts / Dialogue in your code editor once more.

private bool firstOptionSelected;
private IEnumerator UpdateOptionSelectorPostion() // 1
{
    yield return new WaitForEndOfFrame(); // 2

    if (firstOptionSelected) // 3
    {
        optionSelector.position =
            new Vector3(optionSelector.position.x,
            firstOption.GetComponent<RectTransform>().position.y, optionSelector.position.z);
    }
    else
    {
        optionSelector.position =
            new Vector3(optionSelector.position.x,
            secondOption.GetComponent<RectTransform>().position.y, optionSelector.position.z);
    }
}
private void ShowQuestionUI(DialogueLine currentLine) // 1
{
    answersParent.SetActive(true); // 2
    continueIndicator.SetActive(false); // 3
    optionSelector.gameObject.SetActive(true); // 4
    firstOption.text = currentLine.dialogueQuestion.firstOption; // 5
    secondOption.text = currentLine.dialogueQuestion.secondOption; // 6
    firstOptionSelected = true; // 7
    StartCoroutine(UpdateOptionSelectorPostion()); // 8
}
ShowTextUI();
if (!currentLine.thisIsAQuestion) // 1
{
    ShowTextUI();
}
else // 2
{
    ShowQuestionUI(currentLine);
}
private void QuestionInteract(DialogueLine currentLine) // 1
{
    if (firstOptionSelected) // 2
    {
        StartConversation(currentLine.dialogueQuestion.conversationWhenFirstOptionWasSelected, dialogueStarter);
    }
    else // 3
    {
        StartConversation(currentLine.dialogueQuestion.conversationWhenSecondOptionWasSelected, dialogueStarter);
    }
}
NormalTextInteract(currentLine);
if (!currentLine.thisIsAQuestion)
{
    NormalTextInteract(currentLine);
}
else
{
    QuestionInteract(currentLine);
}
if (activeConversation != null && context.performed) // 1
{
    DialogueLine currentLine =
        activeConversation.dialogueLines[dialogueIndex]; // 2

    if (currentLine.thisIsAQuestion) // 3
    {
        firstOptionSelected = !firstOptionSelected;
        StartCoroutine(UpdateOptionSelectorPostion());
    }
}

Key points

  • A dialogue system allows you to speak with (non-playable) characters and make choices.
  • Variables from regular classes can be edited in the Unity editor by marking them serializable using the [Serializable] attribute.
  • Instances of serializable classes can be saved to and loaded from a disk or memory.
  • Unity comes with a lot of built-in property attributes to help display your components the way you want to in the Inspector. A good example is [TextArea], which creates a multiline text field for a string variable.
  • You can use a scriptable object as a data container and store it as an asset.
  • Use the [CreateAssetMenu] attribute on scriptable object scripts to add them to the Create menu in the editor.
  • Use LayerMask.NameToLayer("layer name") to get the ID of a layer.
  • Test your scripts along the way by using print() to make sure what you have up until that point works.
  • A coroutine is a method that can spread its execution over multiple frames.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now