We’ve stumbled at an inconvenient design in a game and wondering, whether there is nothing simpler. The background needs some explaining so I’m sorry for the wall of text.
We have a player class. The player knows how to walk around in the world. At certain occasions, we want to disable the player from being able to walk around.
At first, we just wanted to stop our player from walking when they open the inventory menu.
The player can also walk around the world and inspect objects. This also is supposed to disable walking for the player.
Here is the problem however: You can open the inventory while inspecting an item. There was a bug: the inventory code did not know about the inspection code. As such, if you closed the inventory during inspection, the inventory code allowed the player to walk again.
Naturally, to fix this problem we came up with a layer of indirection. It is somewhat similar to semaphores. In a nutshell our indirection is this class:
class ActionBlocker: void Block(EBlockableAction) void Unblock(EBlockableAction) Map<EBlockableAction, Array<Actions>>
The EBlockableAction describes the type in-game action. For example, we have EBlockableAction::Inventory and EBlockableAction::Inspection.
When the inventory is opened, its code simply calls Block(EBlockableAction::Inventory). Likewise, when the inventory is closed it calls Unblock(EBlockableAction::Inventory). It is similar for the inspection code.
Each enum is assigned to an array of actions: each action can be performed and reversed. In the example above, one action is you can enable walking (perform) and disable walking (reverse).
The Block function goes through each action assigned to the enum and performs the action. The Unblock function tells each action to reverse.
Internally, each action has a counter of how many times it was requested to be performed. The action is only performed initially, i.e. when that counter goes from 0 to 1. Likewise, the action is only reversed, once every code that requested for that action to be performed requests for it to be reversed, i.e. when the counter goes from 1 to 0.
I hope the way the system works is clear now (if not I can further explain it).
The problem with our solution
We expanded this system across the game. The number of enum entries has grown. Each enum also has multiple actions now, e.g. there is an action to toggle mouse cursor visibility, another action for ignoring certain types of keyboard inputs, and more.
What we won: all gameplay code is independent from each other. They do not need to talk to each other to sync blocking certain types of things in-game.
What we lost: It is difficult to debug debug the overall system’s behaviour. When multiple features interact and block the same things, it is hard to know what is really blocking what.
The system is configured in a separate file. Sometimes it is not obvious that a certain gameplay action will block certain other features in the game.
It has happened on multiple occasions that we were left wondering why a certain input key was being ignored by our game. It was then often traced back to a Block call not getting made. This is difficult to debug.
We are looking for a more simple system than this. Do you have feedback / suggestions? Maybe you have encountered a similar scenario in the past and can enlighten your solution.
We’re left wondering whether this solution really is the way to go and it is just the nature of inter-feature dependencies that makes the system so complex, and not the code design itself…