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. By Najmm Shora.

Leave a rating/review
Download materials
Save for later
Share

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.