Coding a Console for Vacuous
With Vacuous I have entered the dark zone of game development: the final 10% of work. All the gameplay elements have been programmed, most of the level design and game structure is finished, and the vast majority of the final assets are done. At this point, most of the work revolves around fixing the elements that do not work, testing for bugs, and polishing the experience. In order to execute these tasks seriously the proper tool is needed. That is why I decided to add a developer console to Vacuous.
I need to make it clear how odd this is. Usually, the developer console is an early addition to any game project and assists in testing systems as they get implemented. In this case, however, for much of the lifespan of Vacuous the Game Maker Studio IDE has served this role. Even odder is the fact that the last type of game to be expected to have a console would be one made with a tool like Game Maker. Consoles are associated with programming/systems heavy projects like idTech, Source, and Unreal Engine. Projects are made in Game Maker with the intention of avoiding this type of work. However, Vacuous is a large enough, code heavy enough project to warrant the addition of a console.
In Vacuous, the console allows the user to switch levels, activate no-clip, change the game's settings, manipulate progression/save data, and test music and sounds, amongst other features. All of these functions aid in giving the developer more immediate access to everything in the game, making it easy to check the landscape of the game's systems for issues and changes. When the game is released, the console also acts as a fun cheat system for players to mess around with once they've beaten the game.
Honestly, if you told me to implement a console in a Game Maker project five years ago I would have thought you were crazy. But after some years of significant training in programming, it only took me a few hours to build the one that appears in Vacuous. Here is how I did it.
In the Game Maker IDE I created an object "obj_console" that contains almost all of the console's functionality (the functions the console calls are external script files, as will be explained later). The console object is created by the initialization object at the very moment the game starts and exists from room to room until the game ends (it is what Game Maker calls a "persistent object"). The console is the object on the highest layer in the game so that it renders above everything else, even menus. There exists a special draw call for this type of thing (Draw GUI?), but it is a newer addition so my design does not account for it. The console object responds to four events: create, step, game end, and draw. I will be focusing on the contents of create, step, and draw (the game end event contains a single line of code unnecessarily clearing a data structure from memory; it is just there to keep my conscious clear).
Here is the script for the create event. Everything being done here is initialization. The unlocked and unlockState variables are used to track state for unlocking the console. Essentially, the console can only be opened after a specific sequence of keys is pressed. The blurred comment in the code documents this sequence, but of course I am not going to reveal it here. After this is the variable that tracks whether or not the console is currently on: enabled. The counter and count variables here, as well as elsewhere, are used to throttle events. In this case, the console can only be enabled and disabled at a certain pace (in this case, every 9/60 seconds).
The commandBuffer variable is a string that holds the command being typed. It also has a defined limit on how large these typed commands can be. When allowing the user to input text strings, a powerfully expressive form of interaction, limits are important to avoid major problems. The history buffer holds a history of the previous command results. This is a list data structure, and again, it has a clear limit (in this case, to prevent the user from filling memory with a list of commands too big). After this, there are several counters for throttling backspacing and command submission. At the end, we have the variables controlling the marker which lets the user know where in the string there next inputted character will be added. These include the character the marker appears as and the rate at which that marker flickers (1/3 second).
The most important event is the step event; it manages the functionality of the console. Here is the code used in checking the unlock sequence (again, the sensitive information is blurred). If a new key being hit is detected, a switch statement takes the current position in the unlock sequence and checks if the new key corresponds to the next character. If it does, it moves to the next position in the unlock sequence. If it doesn't, the unlock sequence is restarted to the beginning. The final switch case unlocks the console for the rest of the game's duration.
Here we have the if statement that executes the behavior of the console disabled or enabled and the counters (they count down to 0 when they are not 0, pretty simple). The console can only be enabled if it is unlocked. I use a number code to check the "~" key (the classic choice) and enable the console if it is pressed and input is allowed (input in Vacuous is not allowed during room transitions).
All the following code executes when the console is enabled. Here, typed characters are added to the command buffer. A lot of key input is filtered out. While Game Maker has functions that filter out everything but alpha-numeric characters, these leave out characters the console needs such as decimal points and underscores. I am not certain this list filters out everything unwanted; additional inputs might need to be listed after more testing reveals them. New line characters are tricky, and thus require a special filtering mechanism that truncates them off the list once they are added. The same method is used to keep the command buffer from exceeding its limit. Once a key is registered, the record of that last key is replaced with the value "-1." This means that when a new key value enters the record of last key, it can be detected by the difference.
Here backspacing is handled. If the backspace key is detected, the command buffer is truncated like before and a period of pause is had before the next character can be erased. This paces backspacing so that the user does not accidentally erase too many characters.
When the enter key is detected, the command buffer is submitted for execution. In order to know what the command buffer means, it must be parsed. First, a 1D array of length 8 for holding the command's arguments is initialized. If I wanted this system to be more flexible, a list data structure might be better here, but I decided to just go with an array for simplicity's sake. The command is parsed with the read state and the current character. The while loop goes through each character in the command buffer one by one. If the character is not a space, it is added to the current string held in the argument array index corresponding to the current state. If the character is a space, the state is incremented and the next argument is read. So, the command "resolution 1920 1080" takes three arguments: arg = resolution, arg = 1920, and arg = 1080.
Here is a large sample of how the array of arguments is then processed and used to execute a command. A large if statement is used to sort the command by its first argument, the function call. The rest of the arguments are passed to the Game Maker script associated with that function name and then the results of what that script did are added to the history buffer. This doubles as error checking. If the arguments are unusable or the command cannot be executed at the given time, the console history states this. The results are handled by the return value of the scripts, which drive the switch/if statements determining which results to report. Once the command has been executed, the history buffer is truncated if it is too long, the command buffer is reset, and the submission counter is set to avoid another command being submitted immediately after.
There is nothing too complicated at the end of the step event for the enabled console. The marker is cycled in and out of appearance each time its counter counts down and disabling of the console is handled just like enabling.
The final part of the console is how it is rendered in the draw event. The console only needs to be rendered when it is enabled. First, a transparent black box for making the console text easy to read is rendered over the rest of the game. Its drawing coordinates keep it always placed in the screen even when it scrolls. The box height is dynamically tied to the maximum size of the history. This means that if I want the history buffer to contain more or less result history, the console is rendered to match this change automatically. After drawing the box, the console text is drawn. The font and color are chosen and then a while loop goes through each result in the history and draws it in its proper location in the box (some math here). I chose a monospace font, as is appropriate for the application. Finally, the command buffer is drawn at the bottom. If the buffer has maxed out, the marker is no longer drawn, indicating to the user that they have reached the maximum command size. Otherwise, the command buffer is drawn normally.
Here are the results in game. I hope that this post can act as a resource for those who want to add consoles to their own Game Maker projects.
- J. M. Stark