In games made of connected rooms like The Legend of Zelda, the more recent The Binding of Isaac, or any type of Roguelike or even Metroidvania-like, doors play an essential part in the navigation and progress of the player.
Doors allow the player to travel from one room or level to another, and so have an important place in the navigation and the connection of the different rooms one to another, and in defining the map as an open world or a dungeon floor. They may also act as temporary roadblocks that the player will need to unlock through a specific mechanic (such as obtaining a key, or activating a switch).
In this tutorial, I'll demonstrate several lock mechanics, and propose ways to implement them in your games. These are in no way meant to be the only or best implementations; they are practical examples.
The interactive demos in this tutorial were made with the HTML5 game maker tool Construct 2 and should be compatible with its free version. (The CAPX files are available in the source download.) However, this tutorial should help you learn how to implement the doors and locks logic in whatever engine you fancy. Once you get the idea behind the logic, it's all down to your own knowledge of your coding tool/language and the way you want to adapt it to the game you are currently making.
Let's dive in!
The Basic Mechanic
A door is basically a block of scenery that cannot be passed, preventing the player's character to go through until it is unlocked. The door can have different states: locked or unlocked, closed or open.
There must be an obvious representation of the latter; the player must be able to tell that the door is actually a door and whether it is in its locked or unlocked state.
In the following demos, the doors are presented through two graphics :
I've also used different colours to represent the different materials that the doors might be made of—but, to be honest, the graphical aspect is up to you, your game, and its universe. The most important part is that the door should be clearly identifiable as being a door, and it should be obvious whether it's going to block the player's progression or open and leading to the rest of the level or world.
When closed or locked, the door should be a block of solid state. When opened, the solid state should be disabled, allowing for characters to go through it. Be sure that whatever your collision engine is, it allows you to modify this state on the fly pretty easily.
From a programming perspective, the door object should contain or be linked to an
is_locked Boolean variable. Depending on the value of this variable, you can determine what sprite to display and whether the block should be solid or not.
To unlock the door, the character should contain a
has_key Boolean variable itself when the player has picked up a key:
true if they have it,
false if they don't.
In this basic mechanic, the key acts as a part of the inventory of the character, and one key opens all doors. Using the key on a door doesn't consume it; the key stays in the character's inventory.
To visualize it, we can simply display a picture of the key in the HUD to let the player know he "owns" a key that could open the doors once the character has picked it up (by moving the character over the key in the room).
Consider the following basic example:
Click the demo to give it focus, then control the character using the arrow keys of your keyboard and perform actions with the space bar. (In this example, the action is "open a door".)
Walls are solid blocks that don't allow the character to go through when colliding with them. Closed doors are also solid.
To open a door, the character must be within 64 pixels of the door and possess a key (that is, the
has_key Boolean variable that determines whether the character has the key in its inventory must be
With those conditions, when the player presses the space bar, the appropriate door's state is changed. Its Boolean variable
locked is set to
false, and its "solid" state is disabled.
In pseudocode, this would look something like :
Door.Locked = True Door.AnimationFrame = 0 //The animation frame that displays the door as locked Door.Solid = Enabled //The solid state of the door is enabled Door.Locked = False Door.AnimationFrame = 1 //The animation frame that displays the door as opened Door.Solid = Disabled //The solid state of the door is disabled Keyboard Key "Space" is pressed and Distance(Character,Door) <= 64px and Door.Locked = True and Character.Has_Key = True //The player has a key Door.Locked = False Keyboard Key "Space" is pressed and Distance(Character,Door) <= 64px and Door.Locked = True and Character.Has_Key = False //The player does not have a key Text.text = "You don't have a key for that door"
Reminder: This code does not represent a specific language; you should be able to implement it in any language you want to.
You can also note that we do a check for when the player does not have the expected key, and display a feedback message explaining why the door wasn't unlocked. You can handle checks like those however best suits your game—just be aware that it's always nice to give feedback to your player that his action was registered, and explain the reason why it wasn't completed.
This is a very basic door and lock logic and how to implement it. In the rest of the tutorial, we'll look at other lock systems that are variants of this basic system.
Different Lock Systems
We've seen the basic system where the key is a whole part of the inventory of the character and one key opens all doors and can be reused to open up several doors. Let's build on this.
In the next example, the character will have a stack of keys in its inventory. Although there are several different colours of door, the difference is strictly graphical here—the door object is logically the same as in the basic example, and one type of key can open any of them. However, this time, whenever you use a key to open a door, that key is removed from the stack.
As far as coding is concerned, this modification is mostly on the character's level. Instead of having a
has_key Boolean variable, you will want to have a numeric variable that will hold the number of keys the character has "in stock".
Each time the character picks up a key, add
1 to this variable to represent the stack going up. Each time the character opens a door, subtract
1 from this variable to represent the use of a key. (In video game land, keys are destroyed as soon as they are used once.)
Another modification is to when the player presses the space bar: instead of checking that a
has_key Boolean variable is
true, we actually want to check that the value of
KeyStack is more than zero, so that we can consume a key after opening the door.
In pseudocode, this looks something like:
Doors mechanics = same as in the basic example above. Keyboard Key "Space" is pressed and Character.KeyStack > 0 and Distance(Character, Door) <= 64 and Door.Locked = True Character.KeyStack = Character.KeyStack - 1 Door.Locked = False
In this new example, we will consider a scenario where different type of doors require different types of keys to be unlocked.
Here, as in the first basic example, keys will be part of the character's inventory. We will go back to using Boolean variables to determine whether the character has picked up the requisite keys. And since we will have different keys, we will also have different types of doors (black door, red door, gold door) which will also require an appropriate key to allow them to be opened (black key, red key, gold key, respectively).
The door objects will use different sprites to show their material, and will contain a numeric variable named
WhichKey that will indicate the kind of key that is expected as well as the kind of graphic it should display. The different key values are contained as constant variables, for better readability.
CONSTANT BLACK_KEY = 0 CONSTANT RED_KEY = 1 CONSTANT GOLD_KEY = 2 Door mechanics are the same as in the basic example. Keyboard Key "Space" is pressed //The door requires a black key but the character doesn't have one If Door.Locked = True and Door.WhichKey = BLACK_KEY and Character.Has_Black_Key = False and Distance(Door,Character) <= 64 Text.text="You need a black key for this door" //The door requires a red key but the character doesn't have one Else If Door.Locked = True and Door.WhichKey = RED_KEY and Character.Has_Red_Key = False and Distance(Door,Character) <= 64 Text.text="You need a red key for this door" //The door requires a gold key but the character doesn't have one Else If Door.Locked = True and Door.WhichKey = GOLD_KEY and Character.Has_Gold_Key = False and Distance(Door,Character) <= 64 Text.text="You need a red key for this door" //The door requires a black key and the character has one Else If Door.Locked = True and Door.WhichKey = BLACK_KEY and Character.Has_Black_Key = True and Distance(Door,Character) <= 64 Door.Locked = False //The door requires a red key and the character has one Else If Door.Locked = True and Door.WhichKey = RED_KEY and Character.Has_Red_Key = True and Distance(Door,Character) <= 64 Door.Locked = False //The door requires a gold key and the character has one Else If Door.Locked = True and Door.WhichKey = GOLD_KEY and Character.Has_Gold_Key = True and Distance(Door,Character) <= 64 Door.Locked = False
This is a variation on the basic example that allows several types of keys and doors and that doesn't consume keys to open doors. Once you have the key, it's part of your inventory—part of the character's "statistics".
This time, instead of acting directly on doors, the player has to activate a specific switch to open or close a specific door.
The doors here are essentially the same object as in the basic example. They could display different graphics, but the logic of the object is still the same. There is an addition though: we add two numeric variables
SwitchID, which we use to know which switch is tied to which door.
Switches are a new type of objects that I have chosen to make solid (but you don't have to). They contain a Boolean variable,
Activated, and numeric variables
SwitchID which, as you can guess, we use to determine which switch is tied to which door.
So when a switch has
Activated: True, the "linked" door is set to have
Locked: False. Our action with the space bar is then going to happen when next to a switch, rather than next to a door. Note the absence of a key in this example, since the switches acts as keys:
We could just use simple code that checks for the door-switch links in the same room (since this example displays three doors and switches in the same room), but later, we will see that we may have switches acting on doors which are in another room, and so their action won't occur at the exact moment that the player activates the switch; it will occur later on, when the new room is loaded.
For this reason, we need persistence. One option for this is to use arrays to keep track of data like the state of the switches (that is, whether each switch is activated or not).
CONSTANT SWITCH_DOORID = 0 CONSTANT SWITCH_ACTIVATION = 1 //Those constants will allow us to keep a readable reminder of the array coordinates <br>//Define some array //The X coordinate of the array will correspond to the SwitchID value //The Y-0 coordinate will be the DoorID //The Y-1 coordinate will be the activation state aSwitch(number of switches,2) //2 is the height (Y) number, often 0-based. <br>Run some association of the SwitchIDs with DoorIDs Door mechanic is still the same as in the basic example. //Displaying the correct switch graphic according to their activation state Switch.Activated = True Display the animation frame Switch_ON Switch.Activated = False Display the animation frame Switch_OFF Keyboard Key "Space" is pressed and Distance(Character, Switch) <= 64 Switch.Toggle(Activated) //A function that will set the value to either True or False) aSwitch(Switch.SwitchID,SWITCH_ACTIVATION) = Switch.Activated //It can depend on your coding language, but the idea is to set the value in the array where X is the SwitchID and where Y is the state of activation of the switch. The value itself is supposed to be the equivalent of the Switch.Activated boolean value. Door.DoorID = aSwitch(Switch.SwitchID,SWITCH.DOORID) //Allows us to make sure we're applying/selecting the correct door instance //Now according to the activation value, we lock or unlock the door aSwitch(Switch.SwitchID,SWITCH.ACTIVATION) = True Door.Locked = False aSwitch(Switch.SwitchID,SWITCH.ACTIVATION) = False Door.Locked = True
For this specific example, where the switches are in the same room as the doors they are linked to, using the array technique is overkill. If your game is set up in such a way that each switch acting on a door is going to be positioned in the same room, then by all means go for the simpler method, get rid of the array, and check for objects which are on screen only.
Plate-switches are similar to switches, in the sense that they are either activated or not, and that we can link them to doors to lock or unlock them. The difference lies in how a plate-switch is activated, which is through pressure.
In this top-down view example, the plate-switch will be activated whenever the character is overlapping it. You can press the space bar to drop a rock on the plate-switch, leaving it activated even when the character is not standing on it.
The implementation of this is similar to the previous example, with two little modifications:
- You have to activate the plate-switch when a character or rock is on top of it.
- You have to make the space bar drop a rock (from the inventory) on to the plate-switch.
//Most of the implementation is the same as the previous example replace the Switch object with PlateSwitch object //Plate-Switch Mechanic Character OR Rock is NOT overlapping PlateSwitch PlateSwitch.Activated = False aSwitch(PlateSwitch.SwitchID,SWITCH_ACTIVATION) = PlateSwitch.Activated Door.DoorID = aSwitch(PlateSwitch.SwitchID,SWITCH.DOORID) //Allows us to make sure we're applying/selecting the correct door instance Door.Locked = True Character OR Rock is overlapping PlateSwitch PlateSwitch.Activated = True aSwitch(PlateSwitch.SwitchID,SWITCH_ACTIVATION) = PlateSwitch.Activated Door.DoorID = aSwitch(PlateSwitch.SwitchID,SWITCH.DOORID) //Allows us to make sure we're applying/selecting the correct door instance Door.Locked = False Keyboard Key "Space" is pressed And Character is overlapping PlateSwitch Spawn Rock at PlateSwitch.position
Another possible lock mechanic is to require the player to get rid of all enemies (also known as mobs) in a room or area to trigger the unlocking of the doors.
For this example, I've made a few areas in a single room; each area has a door and several mobs (although those enemies don't move and do not deal damage).
Each area has its own colour.
The space bar will make the character fire some projectiles; three projectiles will kill a mob.
This kind of mechanic is used in The Legend of Zelda and The Binding of Isaac, and revolves around a function checking the number of living enemies in the room or area. In this example, each coloured area holds a count of the living mobs, initiated when the room loads, and is tied to the door. The death of each mob subtracts
1 from this counter; once it drops to
0, the door's
Locked state is changed to
//On start of the game For each Area For each Mob overlapping Area Area.AliveMobs = Area.AliveMobs + 1 <br>Door mechanic is the same as in the basic example<br> Keyboard Key "Space" is pressed Spawn a Projectile from Character's position Projectile collides with Mob Mob.HP = Mob.HP - 1 Destroy Projectile Mob.HP <=0 //Mob is dead and Mob is overlapping Area Destroy Mob Area.AliveMobs = Area.AliveMobs - 1 Area.AliveMobs <= 0 and Door is linked to Area //By means of an ID, a pointer or whatever Door.Locked = False
In this example, an
Area is a coloured sprite with an associated numeric variable,
AliveMobs, which counts the number of mobs overlapping the area. Once all mobs in an area are defeated, the corresponding door is unlocked (using the same mechanic as we've seen since the basic example).
As I mentioned in the introduction, doors can act as blocking obstacles, but can also be used to allow the player's character to navigate from a room to another.
In this example, doors will be unlocked by default since we're more interested in the navigation aspect.
The mechanic depends a great deal on the game you are making, as well as the way you handle the data structure for your floors. I won't go into the details of how my implementation works here, as it's very specific to Construct 2, but you can find it in the source files if you wish.
Throughout this article, we've seen how doors are temporary obstacles that require keys or unlocking mechanics such as switches, plate-switches, or even mobs' death. We've also seen how they can act as "bridges", allowing navigation through different areas of the game's world.
As a quick reminder, here are some possible lock mechanics :
- One key for all doors, as part of the inventory.
- Consumable keys: each time you open a door, one key gets taken away from your key stack.
- Different doors require different keys.
- Switches, or plate-switches, where you don't act directly on the door to unlock it but through a separate, linked device.
- Killing all the mobs of an area automatically unlocks a door.
If you mixed all those mechanics in a game, you could end up with something like this:
Here we have a nice selection of different door and lock mechanics, requiring the player to go through several rooms to unlock the various doors. For learning purposes, you might want to reproduce this in your own programming environment, using all the previous implementations we've gone through.
I hope you enjoyed this article and that it was useful for you, and I'd like to remind that you can find the source to all the demos on Github. You can open and edit them in Construct 2's free version (version r164.2 or above).