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
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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.