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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How to Make an Adventure Game Like King’s Quest
35 mins
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 Edit ▸ Project Settings ▸ Tags and Layers and expand Sorting 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.
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.
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.
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
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
Now, attach the Set Relative Sorting component to the following sprites:
- All child objects of Apples
- AppleOnTree
- bucket
- bucketGlow
- ropeInside
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.
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.