Cyber Monday Sale — Save on Everything.All videos. All books. Now 50% off.

Ends in... ::
Home Unity Tutorials

How to Make an Adventure Game Like King’s Quest

In this tutorial you will learn how to implement the core functionality of text-based games like King’s Quest I using Unity.

5/5 2 Ratings

Version

  • C# 7.3, Unity 2020.1, Unity

Text-based games have existed ever since teleprinters were interfaced with mainframe computers. With the availability of video terminals, universities started developing text-based adventure video games.

With the advent of graphics came graphic adventure games. While text adventure games only described the environment textually, graphic adventure games let you see and interact with environment.

When King’s Quest I was released in 1984, its key selling point was the use of non-static screens which responded dynamically to player input. For instance, the player could move the character in realtime.

In this tutorial you’ll learn how to implement the core functionality of King’s Quest I by using Unity to make a simple clone called Wolf’s Quest. You’ll learn how to:

  • Parse text commands.
  • Fake 3D in a 2D world.
  • Implement text based interactions within the Unity Editor.

Time to get started!

Note: This tutorial assumes you have some experience with Unity and an intermediate knowledge of C#. In addition, this tutorial assumes you’re using Unity 2020.1 or newer and C# 7.

Getting Started

Download the project materials by clicking the Download Materials button at the top or bottom of this tutorial. Open Unity, extract the zip file and open the starter project.

The project contains some folders to help you get started. Open Assets/RW and find the following directories:

  • Animations contains pre-made animations and the animator for the main character.
  • Resources has font files and a single audio file for ambience.
  • Scenes contains the main scene with which you’ll work.
  • Scripts has all the project scripts.
  • Sprites contains all the pixel art for the project.

Switch to Game View inside Unity Editor and use the dropdown to set the aspect ratio to 4:3.

Setting the Game View's aspect ratio

Now, navigate to RW/Scenes and open Main. Click Play and you’ll see a text overlay. Click the close button at the top right to hide it.

Text-Based Games preview

Notice the werewolf character animates on its own. The werewolf’s animations were included so you can focus on the core implementation. Currently, you can’t move the character, so that’s what you’ll work on next.

There’s a black area for typing commands at the bottom of the Game View. Type something there and press Return. Nothing interesting happens yet, but you’ll change that shortly. :]

Stop the game and look at the Hierarchy.

The Scene's Hierarchy

The Boundary GameObject has colliders to prevent the character from going outside the camera view.

The Main Camera is setup and has an attached Audio Source which plays a nice ambient sound on loop.

Inspector for the Main Camera

There’s a Canvas set up for the overlay UI. It has the UI Manager component attached and contains the following child GameObjects:

  • InputField: This GameObject has an attached Input Field component. You’ll use it to enter the in-game commands.
  • Dialogue: This is the text overlay UI you saw earlier. It has a Canvas component attached to it. It also has a Close Button child GameObject which disables the said Canvas component upon clicking.

There’s also the Character GameObject with the Animator component already setup. Notice it also has an Edge Collider and Rigidbody 2D components.

Why you ask? You’ll find that out soon. :]

Now, expand the Environment GameObject.

Structure of the Environment GameObject

Notice it has several GameObjects. You’ll set up some of them later for interactions within the game. For now, notice how most of them have a Sprite Renderer while some of them have a Box Collider 2D component.

Finally, briefly look at the werewolf’s animator. Open the Animator window and select the Character in the hierarchy.

Character's Animator

As you can see, there are two Int parameters, X and Y, to drive transitions in the state machine. The states are as follows:

  • WalkRight: To transition to this state, the (X,Y) parameter values should be (1, 0).
  • WalkLeft: To transition to this state, the (X,Y) parameter values should be (-1, 0).
  • WalkUp: For this state, the (X,Y) parameter values should be (0, 1).
  • WalkDown: For this state, the (X,Y) parameter values should be (0, -1).

Wow, that was quite the tour! Now it’s time to start coding. In the next section, you’ll implement character movement.

Moving the Character

In King’s Quest I, you press an arrow key once to move the character. To stop the character you press the same key. If you press any other arrow key during movement, the character changes direction.

You’ll replicate that character movement style in this section.

Navigate to RW/Scripts and open CharacterMovement.cs inside your code editor.

Replace //TODO: Require statement goes here with [RequireComponent(typeof(Animator))].

It’s good practice to ensure an essential component, in this case an Animator, is present on the GameObject the script is attached to, instead of assuming it’s there.

Now paste the following code inside the class:

[SerializeField] private float speed = 2f;
private Animator animator;
private Vector2 currentDirection = Vector2.zero;

private void Awake()
{
    animator = GetComponent<Animator>();
    animator.speed = 0;
}

The code you added:

  • Declares the variable speed which stores the speed at which the character moves. SerializeField makes this private field accessible via the Inspector.
  • The variable animator is declared and later initialized inside Awake to store the Animator component attached to the GameObject. The animator’s speed is set to zero in Awake so the character’s animation is paused in the beginning. This script toggles the animator.speed between zero and one as you’ll see later.
  • currentDirection is a Vector2 which stores the 2D direction in which the character moves. It’s initialized to its default value.

Now you need to code the actual movement. Copy and paste the following below Awake:

private void StopMovement()
{
    animator.speed = 0;
    StopAllCoroutines();
}

private void ToggleMovement(Vector2 direction)
{
    StopMovement();

    if (currentDirection != direction)
    {
        animator.speed = 1;
        animator.SetInteger("X", (int)direction.x);
        animator.SetInteger("Y", (int)direction.y);
        StartCoroutine(MovementRoutine(direction));

        currentDirection = direction;
    }
    else
    {
        currentDirection = Vector2.zero;
    }
}

private IEnumerator MovementRoutine(Vector2 direction)
{
    while (true)
    {
        transform.Translate(direction * speed * Time.deltaTime);
        yield return null;
    }
}

MovementRoutine translates the character by the value of speed per second in the 2D direction specified by the Vector2 input parameter direction. This coroutine keeps running until it’s stopped elsewhere.

StopMovement sets the animator speed to zero. It also stops any running coroutines, which essentially stops the MovementRoutine.

Note: It’s good practice to store a reference to a coroutine when starting it and then use that reference to stop it. However, you won’t need more than one coroutine in this script, so keep it simple.

ToggleMovement accepts a Vector2 parameter direction. Before doing anything, it calls StopMovement. Then it checks to see if the direction is updated.

If currentDirection is the same as direction, the value of currentDirection is reset to the default value of Vector2.zero. Then the method returns, which effectively stops the character’s movement.

However, if the direction has changed, the animator.speed is first set to one to enable animation, followed by setting X and Y animator integer parameters to the values of direction.x and direction.y respectively to set the animation state. Finally, the MovementRoutine starts.

To make this code work, you need to use it inside the Update loop. Paste the following after Awake:

private void Update()
{
    if (Input.GetKeyDown(KeyCode.UpArrow)) ToggleMovement(Vector2.up);
    if (Input.GetKeyDown(KeyCode.LeftArrow)) ToggleMovement(Vector2.left);
    if (Input.GetKeyDown(KeyCode.DownArrow)) ToggleMovement(Vector2.down);
    if (Input.GetKeyDown(KeyCode.RightArrow)) ToggleMovement(Vector2.right);
}

This code polls the input for arrow keys and calls ToggleMovement when the user presses an arrow key. Each arrow key is associated with a 2D direction. It then passes this direction to ToggleMovement.

Save everything and return to the Main scene inside Unity. Press Play and move the character by pressing any arrow key. Press the same arrow key to stop the character or press another arrow key to change direction.

Previewing movement

Notice the colliders in the scene prevent the character from going through, thanks to the Edge Collider attached to the character you saw before and the Box Collider 2D components on the other objects. You might also notice some weird sprite sorting issues. Don’t worry, you’ll fix these issues later on.

There are some issues you’ll want to solve right away: the character continues animating if it hits a collider and the MovementRoutine is still running when this happens.

To correct this behavior, paste the following after all of the methods inside CharacterMovement.cs:

private void OnCollisionEnter2D(Collision2D other)
{
    StopMovement();
}

This ensures the animation, as well as the MovementRoutine, stops when the character hits a collider. OnCollisionEnter2D works because the character has a Rigid Body 2D component attached. Every physics interaction the werewolf has will trigger this method to run. Save everything and play again to test it.

That leaves just the sorting issues to ‘sort’ out. :] You’ll fix them in the next section.

Faking 3D in a 2D World

King’s Quest I faked the 3D effect by making the main character appear behind or in-front of the in-game sprites based on his location. You can replicate this effect in Unity by using the 2D Sorting feature.

Sorting Sprites

In Unity, all the 2D renderers, which includes Sprite Renderers, are associated with a Sorting Layer. Within a sorting layer, the Order in Layer determines the renderer’s priority in the rendering queue.

Go to EditProject SettingsTags and Layers and expand Sorting Layers.

Tags and Layers

Notice this project has three layers, excluding the Default layer: Landscape, Foreground and Darkness.

The topmost sorting layer is first in the render queue, followed by the rest. So, sprites associated with the Landscape layer render first, followed by Foreground and Darkness. Because they rendered first those sprites will be behind the ones rendered later.

Layer Sort Order

In the Main scene, the sprite renderer for the Background has its sorting set to Landscape and the one for Darkness GameObject is set to the Darkness layer. The rest of the sprite renderers, including the one for Character, have a sorting layer set to Foreground.

Note: The Darkness layer has more use in the Final scene inside the Final project. You can take a look at that once the tutorial is complete.

Even though the Foreground sprites share a sorting layer, you can change the order in layer for each at run-time based on the vertical distance from the camera’s top view. This helps you fake a 3D effect.

Fake 3D effect

To calculate this vertical distance, find the Y coordinate of the topmost point inside the camera view. Use the following formula:

Top Point Y Coordinate = Camera Y Coordinate + Camera Orthographic Size

Calculating vertical distance

Then set the order in layer for the sprite as a mathematical function of this top point Y coordinate and the sprite’s Y coordinate. The simplest way to do this is to take their absolute difference.

Adjusting Order in Layer

To implement the order in layer adjustment at runtime, go to RW/Scripts and open SetSortingOrder.cs. Paste the following code inside the class:

[SerializeField] private float accuracyFactor = 100;
private Camera cam;
private SpriteRenderer sprite;
private float topPoint;

private void Awake()
{
    sprite = GetComponent<SpriteRenderer>();
    cam = Camera.main;
}

This code declares, and later initializes in Awake, variables to store the scene’s Main Camera reference and the GameObject’s Sprite Renderer reference. It also declares variables topPoint and accuracyFactor. You’ll use them to calculate the order in layer.

After the Awake method body paste:

public void SetOrder()
{
    topPoint = cam.transform.position.y + Camera.main.orthographicSize;
    sprite.sortingOrder = (int)(Mathf.Abs(topPoint - transform.position.y) * accuracyFactor);
}

SetOrder first calculates topPoint based on the earlier formula. Then it sets the sortingOrder, or the order in layer. You set the sortingOrder by first taking the absolute difference of topPoint and the sprite Y coordinate, as discussed earlier.

Then you follow this operation by multiplying the difference to the accuracyFactor, which modifies the output range. Increasing this value results in more precise sorting changes as the character moves in the scene. But keep in mind, the value for the order in layer must be between -32768 and 32767.

Now, paste SetOrder(); as the final line inside Awake to ensure this method is called when the scene loads.

Save everything and go to RW/Scripts. Open SetRelativeSorting.cs and paste the following inside the class body:

public SpriteRenderer referenceSprite;
public int relativeOrder;

private void Start()
{
    GetComponent<SpriteRenderer>().sortingOrder =
        referenceSprite.sortingOrder + relativeOrder;
}

This script sets order in layer for a sprite relative to a referenceSprite. The order in layer of the sprite will shift up by relativeOrder from the order in the referenceSprite layer. This handles cases where sorting needs to be relative to another sprite rather than depend on the Y coordinate of the GameObject.

Save everything and go back to the Main scene. Attach the Set Sorting Order component to the following sprites:

  • Character
  • Lit
  • Unlit
  • Tree Sprite
  • well-sprite

Hierarchy selections for SetSortingOrder component
Hierarchy filtered for SetSortingOrder component

Now, attach the Set Relative Sorting component to the following sprites:

  • All child objects of Apples
  • AppleOnTree
  • bucket
  • bucketGlow
  • ropeInside

Hierarchy selections for SetRelativeSorting component
Hierarchy filtered for SetRelativeSorting component

For AppleOnTree and all child objects of Apples, set the Reference Sprite to Tree Sprite. Set the Relative Order to 1.

For bucket, bucketGlow and ropeInside, set the Reference Sprite to well-sprite. Then, for bucket and ropeInside set the Relative Order to 1. Finally, for bucketGlow set the Relative Order to 2.

Now, select any of the aforementioned sprites in the Hierarchy and keep your eyes on the Inspector. Save everything and press Play. Notice how the value for Order in Layer in the Sprite Renderer is updated on playing the scene.

If you try moving the character at this point, you’ll still find sorting issues because you’re calling SetOrder only in the Awake method of SetSortingOrder.cs.

To fix this, open up CharacterMovement.cs and replace [RequireComponent(typeof(Animator))] with [RequireComponent(typeof(Animator), typeof(SetSortingOrder))]. Declare the following variable at the top:

private SetSortingOrder sortingScript;

Then paste this line inside Awake:

sortingScript = GetComponent<SetSortingOrder>();

Finally, paste the following before yield return null inside MovementRoutine:

sortingScript.SetOrder();

This effectively keeps calling SetOrder as long as the character is moving. Save everything and head back to the main scene. Press Play and move the character around. Now it’ll work as intended.

Preview of finished movement

Note: For this to work properly, most of these sprites have their pivots set near their bottom. You can confirm this by looking at them inside the Unity Editor.

In the next section you’ll accept commands from the player and parse them.

Text Commands

Before you get started with coding the text command parsing for Wolf’s Quest, it’ll be good to learn a little bit of the theory behind the implementation you’ll perform.

Text Parsing and the Backus-Naur Form

Text parsing is a broad topic. For your purposes, it’s the idea of extracting useful information from a given string. In text-based games, parsing involves taking a text command from the player and extracting information the game can use while discarding the rest.

To do this you need to establish some form of grammar. This is where the Backus-Naur Form or BNF comes into picture.

Simply put, BNF provides a way to represent a syntax symbolically. For example, you can use it to specify a postal address format or describe a programming language’s rules.

Here are some key points about BNF specification:

  • It’s written as: < symbol > ::= __ expression __.
  • The ::= sign means the < symbol > on the left is equivalent to the __ expression __ on the right.
  • An expression consists of one or more sequences of symbols.
  • The vertical bar | indicates a choice, similar to the bitwise OR operator.
  • The symbols that never appear on the left are called terminals. The symbols on the right, enclosed between the pair <>, are called non-terminals.

Consider the following example BNF specification:

< whole number > ::= < digit > | < digit > < whole number >
< digit > ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

A < digit > symbol can take the values of zero through nine as described. The symbol < whole number > can be a single < digit >, such as 0, or can be represented recursively by combining a < digit > and a < whole number > such as 10 or 110.
In this case the 0 through 9 values are terminals whereas < whole number > and < digit > are non-terminals.

In addition to the standard rules, square brackets are used around optional items. This is a universally recognized extension to the BNF.

For the text parser you’ll implement, use the BNF to specify the grammar as follows:

< command > ::= < verb > [< preposition >] < noun > [< preposition > < noun >]
< verb > ::= get | look | pick | pull | push
< preposition > ::= to | at | up | into | using
< noun > ::= < article > < entity >
< article > ::= a | an | the

As you can see this doesn’t specify the < entity > symbol, but that’s OK. You’ll use this as the base and specify the entities inside Unity. Now it’s time to write the parser.

Implementing Command Parsing

Parsing involves extracting the verb and the entities from the command. Refer to the first entity in the specification as the primary entity and the last entity as the secondary entity.

Navigate to RW/Scripts and open CommandParser.cs. Paste the following above the CommandParser class body but within the namespace block:

public struct ParsedCommand
{
    public string verb;
    public string primaryEntity;
    public string secondaryEntity;
}

This struct acts as the container for the data you want to extract from the command.

Now, inside the CommandParser class body paste:

private static readonly string[] Verbs = { "get", "look", "pick", "pull", "push" };
private static readonly string[] Prepositions = { "to", "at", "up", "into", "using" };
private static readonly string[] Articles = { "a", "an", "the" };

These three string array variables have fixed sizes and are configured with the verbs, prepositions and articles that your parser will use as per your BNF specification. The Parse method that you’ll add next will use these.

Now add the following method to the CommandParser class:

//2
public static ParsedCommand Parse(string command)
{
    var pCmd = new ParsedCommand();
    var words = new Queue<string>(command.ToLowerInvariant().
        Split(new[] { ' ' }, System.StringSplitOptions.RemoveEmptyEntries));

    try
    {
        if (Verbs.Contains(words.Peek())) pCmd.verb = words.Dequeue();

        if (Prepositions.Contains(words.Peek())) words.Dequeue();

        if (Articles.Contains(words.Peek())) words.Dequeue();

        pCmd.primaryEntity = words.Dequeue();
        while (!Prepositions.Contains(words.Peek()))
            pCmd.primaryEntity = $"{pCmd.primaryEntity} {words.Dequeue()}";
        words.Dequeue();

        if (Articles.Contains(words.Peek())) words.Dequeue();

        pCmd.secondaryEntity = words.Dequeue();
        while (words.Count > 0)
            pCmd.secondaryEntity = $"{pCmd.secondaryEntity} {words.Dequeue()}";
    }
    catch (System.InvalidOperationException)
    {
        return pCmd;
    }

    return pCmd;
}

//1
public static bool Contains(this string[] array, string element)
{
    return System.Array.IndexOf(array, element) != -1;
}

There is quite a chunky method there, along with a helper extension method. Here’s a breakdown:

  1. The helper extension method Contains for string arrays returns true if the given string array contains the string element. If not, it returns false.
  2. The Parse method accepts a string command and returns a ParsedCommand struct. This is done as follows:
    • First you define a new ParsedCommand variable pCmd to store your results.
    • Then, after setting all the letters in command to lowercase, it’s split into individual words and stored inside words queue, while making sure any extra whitespaces in command aren’t included in the queue.
    • Then one by one, you look at the word at the top of the words queue. If the word is a verb, it’s dequeued and stored in pCmd.verb. If the word is a preposition you dequeue it without storing because you don’t need to extract it.

      Next, you look for and discard articles. Then you go through the words, concatenating them to the pCmd.primaryEntity until you find another preposition. If you find a preposition, you follow the same process and discard it.

      Then look for another article and discard that, too. Finally, you dequeue, concatenate and store words inside pCmd.secondaryEntity until there are no words queue items left.

    • You use the try-catch block to catch the InvalidOperationException which is thrown if a Peek operation is carried out on an empty Queue. This can happen if you run out of words.
    • Finally, pCmd is returned.

You can’t test this code yet. Open GameManager.cs from RW/Scripts and paste the following inside the class:

public void ExecuteCommand(string command)
{
    var parsedCommand = CommandParser.Parse(command);
    Debug.Log($"Verb: {parsedCommand.verb}");
    Debug.Log($"Primary: {parsedCommand.primaryEntity}");
    Debug.Log($"Secondary: {parsedCommand.secondaryEntity}");
}

private void Awake()
{
    ExecuteCommand("get to the chopper");
}

In this code you define ExecuteCommand which accepts a string command and then calls it inside Awake. The ExecuteCommand defines parsedCommand and initializes it to the ParsedCommand value returned by passing command to CommandParser.Parse.

For now, ExecuteCommand only logs the values of verb, primaryEntity and secondaryEntity to Unity’s Console.

Save everything, go back to the Unity editor and attach a Game Manager component to the Main Camera. Press Play and check the Console. You’ll see the following output:

Console output
Arnold staring

Now go back to GameManager.cs and replace ExecuteCommand("get to the chopper"); with ExecuteCommand("push the boulder using the wand");. Save and play to get the following output:

Console output with secondary entity

You won’t use the secondary entity in this tutorial. But feel free to use it to improve the project and make it your own later.

Next, you’ll implement interactions using the parsed commands.

Implementing Interactions

Before you implement the interactions, you need to understand the approach taken:

  • The game world has interactable objects, each in a certain state.
  • When you look at these objects in this interactable state, you get some textual response associated with the look action. Refer to that as the look dialogue.
  • This state can be associated with other interactions. These interactions are associated with a dialogue similar to the look dialogue, in addition to the associated in-game actions they trigger.
  • The same interaction can be associated with multiple verbs. For example, pick and get could do the same thing.
  • If an interaction requires the character to be near to the object, there should be a textual response if the character is far away. Call that away dialogue.

Now, brace yourself for a lot of coding. First, open InteractableObject.cs and paste the following at the top of the class:

[System.Serializable]
public struct InteractableState
{
    public string identifier;
    [TextArea] public string lookDialogue;
    public Interaction[] worldInteractions;
}

[System.Serializable]
public struct Interaction
{
    public string[] verbs;
    [TextArea] public string dialogue;
    [TextArea] public string awayDialogue;
    public UnityEngine.Events.UnityEvent actions;
}

This code defines InteractableState and Interaction which represent the approach you saw earlier. The actions variable triggers any in-game actions when it’s invoked. The identifier variable stores the ID for InteractableState so you can map the states using a dictionary.

Now, paste the following inside the InteractableObject class, below the code you just added:

[SerializeField] private float awayMinDistance = 1f;
[SerializeField] private string currentStateKey = "default";
[SerializeField] private InteractableState[] states = null;
[SerializeField] private bool isAvailable = true;
private Dictionary<string, InteractableState> stateDict =
    new Dictionary<string, InteractableState>();

public string LookDialogue => stateDict[currentStateKey].lookDialogue;
public bool IsAvailable { get => isAvailable; set => isAvailable = value; }

public void ChangeState(string newStateId)
{
    currentStateKey = newStateId;
}

public string ExecuteAction(string verb)
{
    return ExecuteActionOnState(stateDict[currentStateKey].worldInteractions, verb);

}

private void Awake()
{
    foreach (var state in states)
    {
        stateDict.Add(state.identifier.Trim(), state);
    }
}

private string ExecuteActionOnState(Interaction[] stateInteractions, string verb)
{
    foreach (var interaction in stateInteractions)
    {
        if (Array.IndexOf(interaction.verbs, verb) != -1)
        {
            if (interaction.awayDialogue != string.Empty
                && Vector2.Distance(
                GameObject.FindGameObjectWithTag("Player").transform.position,
                transform.position) >= awayMinDistance)
            {
                return interaction.awayDialogue;
            }
            else
            {
                interaction.actions?.Invoke();
                return interaction.dialogue;
            }
        }
    }

    return "You can't do that.";
}

That is a bucket load of methods you just added. Here’s a breakdown of the code:

  • The isAvailable boolean is a simple flag to determine if the object is available for any kind of interaction.
  • Awake populates the stateDict dictionary by mapping the identifier of InteractableState to itself. This is done for all the members of states and helps in quick retrieval.
  • ChangeState is a public helper method to update the value of the currentStateKey.
  • ExecuteAction accepts a verb and passes it on to ExecuteActionOnState along with the worldInteractions of the current state after retrieving the same from the stateDict using the currentStateKey.
  • ExecuteActionOnState finds the interaction with the associated verb. If no such interaction exists, then it returns a default response of "You can't do that.". However if it exists, then it returns the interaction.dialogue.

    If the awayDialogue is non-empty for the interaction and the distance between the character and the interactable object is greater or equal to awayMinDistance, it returns interaction.awayDialogue.

Save everything and head back to GameManager.cs.

Associate multiple name variations for a single interactable object to ensure a good player experience. Paste this struct above the GameManager class body:

[System.Serializable]
public struct InteractableObjectLink
{
    public string[] names;
    public InteractableObject interactableObject;
}

The words contained inside names are associated with the interactableObject.

Now replace all the code inside GameManager class with:

[SerializeField] private InteractableObjectLink[] objectArray = null;
private UIManager uiManager;
private Dictionary<string, InteractableObject> sceneDictionary;

public void ExecuteCommand(string command)
{
    var parsedCommand = CommandParser.Parse(command);

    //1
    if (string.IsNullOrEmpty(parsedCommand.verb))
    {
        uiManager.ShowPopup("Enter a valid command.");
        return;
    }

    if (string.IsNullOrEmpty(parsedCommand.primaryEntity))
    {
        uiManager.ShowPopup("You need to be more specific.");
        return;
    }

    if (sceneDictionary.ContainsKey(parsedCommand.primaryEntity))
    {
        //3
        var sceneObject = sceneDictionary[parsedCommand.primaryEntity];
        if (sceneObject.IsAvailable)
        {
            if (parsedCommand.verb == "look") uiManager.ShowPopup(sceneObject.LookDialogue);
            else uiManager.ShowPopup(sceneObject.ExecuteAction(parsedCommand.verb));
        }
        else
        {
            uiManager.ShowPopup("You can't do that - atleast not now.");
        }
    }
    else
    {
        //2
        uiManager.ShowPopup($"I don't understand '{parsedCommand.primaryEntity}'.");
    }
}

private void Awake()
{
    uiManager = GameManager.FindObjectOfType<UIManager>();
    sceneDictionary = new Dictionary<string, InteractableObject>();
    foreach (var item in objectArray)
    {
        foreach (var name in item.names)
        {
            sceneDictionary.Add(name.ToLowerInvariant().Trim(), item.interactableObject);
        }
    }
}

This code updates the definitions of Awake and ExecuteCommand from earlier and adds some instance variables.

Awake populates the sceneDictionary by iterating through objectArray and mapping the interactableObject of each member to its name. It also initializes uiManager to the UIManager in the scene.

You don’t need to know the inner workings of UIManager. For this tutorial understand that it contains ShowPopup which accepts a string and shows that string in the UI overlay you saw in the beginning.

ExecuteCommand first parses the command then works as follows:

  1. If the parsedCommand has an empty verb or an empty primaryEntity, it calls ShowPopup with some existing text indicating the same to the player.
  2. If the primaryEntity is non-empty, but isn’t present as a key inside sceneDictionary, it calls ShowPopup with some text telling the player the game doesn’t understand that word.
  3. If the InteractableObject is successfully retrieved from the sceneDictionary but isn’t available, ShowPopup is called with text conveying the same. If it’s available then the following happens:
    • If the verb is “look”, ShowPopup is called with the LookDialogue.
    • If the verb isn’t “look”, the ExecuteAction from before is called by passing the parsedCommand.verb to it. The returned string value is passed to ShowPopup.

You can relax now. You’re done coding for this tutorial. Save everything and head back to the Unity Editor.

Now the real fun begins. :]

Scene Interaction Setup

First, attach Interactable Object to the following GameObjects:

  • Apples
  • Bucket
  • Rope

Hierarchy selections for InteractableObject component

Then select InputField and add the GameManager.ExecuteCommand to the subscriber list for its On End Edit event as shown below:

adding executecommand to the on end edit event.

This ensures GameManager.ExecuteCommand is called when the player presses the Return key after typing the command.

Now select the Main Camera.

Set the size of Object Array of the Game Manager to 3.

You’ll need to set each element of Object Array‘s Interactable Object field to Apples, Bucket, and Rope respectively.

Each should then have it’s own Names array set with variations of what it could be called. For example Apples could be referred to as either “Apple” or “Apples”.

You should end up with a configuration as shown in the following screenshot:

Inspector for Main Camera

Next, select Bucket and set its Interactable Object properties as follows:

Set the Size of States to 1. Change Identifier to default and Look Dialogue to the string That’s one huge bucket..

Change Is Available to false (i.e. unchecked).

Here is how it should look:

Inspector for Bucket

Then select Apples and set its Interactable Object properties as follows:

  • Set Away Min Distance to 4.
  • Changes States Size to 1.
  • Under States, set the Identifier to default and Look Dialogue to the string “The ground near the tree is covered with lots of apples.”
  • Change the Size of World Interactions to 1.
  • Set the Size of Verbs under World Interactions to 2.
  • Enter the verbs pick and get for the two verb elements.
  • Now set the Dialogue and Away Dialogue to “You picked up a delicious apple.” and “I need to get near the apples first.” respectively.

Notice how you’re using get and pick for the same interaction. Also, notice the use of Away Min Distance to configure the difference between the normal dialogue response and the away dialogue response.

Inspector for apples

Finally, select Rope and set its Interactable Object properties to react to the inside and pull identifiers. The setup of this Interactable Object component is similar to how you configured Apples, except you’ll also be adding 5 Actions in this case.

The Rope Interactable Object component should end up configured like this:

Inpsector for rope

Notice you’re changing the state of the rope after the pull. Also notice how the Bucket is made available for interactions after the pull interaction.

Save everything and Play. Type the commands inside the input field and test them. For the existing setup try:

  • look at the rope
  • pull the rope
  • look at the apples
  • pick up an apple
  • look at the bucket

Preview of finished tutorial

You can also try some variations on these, such as:

  • look at rope
  • pull rope
  • get an apple
  • get the apples
  • look at the large bucket

That’s it!

Where to Go From Here?

You can download the final project by using the Download Materials button at the top and bottom of this tutorial.

Try adding your own interactions. There are a bunch of unused objects in the scene. You can create a story with these.

You can also explore the Final project and take a look at the Final scene inside RW/Scenes where a bit of a story has been added. You can see how the interactions are setup.

As you might have noticed, you’re currently using existing methods or properties inside the Unity Editor to set up interactions. You can program custom methods and also subscribe to the interaction events in other ways. You can also add more scenes and add scene transitions.

Look at the original manual for King’s Quest I and see if you can replicate all the game features.

Thanks for taking the time to read this tutorial. I hope you had fun and learned something new. Please feel free to join the forum below for any questions or comments.

Average Rating

5/5

Add a rating for this content

2 ratings

More like this

Contributors

Comments