Hacking Stronger Enums Into GML
GameMaker users might be familiar with enums as numerical constants. They are nothing more than a “find and replace” trick the compiler does for us so that we can use words as identifiers in our code, instead of memorizing numbers.
WHAT ARE ENUMS?
GameMaker users might be familiar with enums as numerical constants. They are nothing more than a “find and replace” trick the compiler does for us so that we can use words as identifiers in our code, instead of memorizing numbers. Here’s an example for a player’s state machine.
enum PlayerState { Idle, Attack, Jump, } state = PlayerState.Idle; // this will be compiled as state = 0 state = PlayerState.Attack; // ...and state = 1 state = PlayerState.Jump; // ...and state = 2
Let’s add a state for holding an item. We need to store what that item is somewhere. Here’s one way we can do that.
enum PlayerState { Idle, Attack, Jump, HoldItem, } // Make the player hold an item... state = PlayerState.HoldItem; item_held = Item.Apple; // Now, somewhere in the step event... show_debug_message("I am holding this item: " + string(item_held));
Cool! Now our player can hold an item, which in our example just means they will spam the output with that debug message every frame.
But what about when we go back to our idle state?
state = PlayerState.Idle;
We’re still printing our debug message every frame! I forgot to clear the item_held variable when I swapped states. Whoops!
This is a small example, and an easy bug to spot and fix, but illustrates an important issue: the HoldItem state and item_held variable are linked together. One does not make sense without the other. By keeping them apart, we leave the door open to bugs like these.
But how can we avoid this without creating an enum variant for every single item the player could hold (PlayerState.HoldPotatoItem, PlayerState.HoldBeanItem…)? And what if we want to hold even more data as well?
…what if we could store information in our enums?
state = PlayerState.HoldItem(ItemId.Apple);
It might look crazy! But this resembles how some other languages work, where enums act more like mini namespaces for constructors. or said more simply, they can be a handy way to construct data that is related – like our player states!
This is not possible with GML enums. They are something else entirely, mostly related by name alone. However, we can do some neat tricks so that we can create these for ourselves, and make the code above real.
HOW DO THEY WORK?
Before we dive into how we can make these, let’s explore how they work. The easiest way to understand them is to view them as normal constructors.
function IdleState() constructor { tag = PlayerState.Idle; } function HoldItemState(item_id) constructor { tag = PlayerState.HoldItem; item_held = item_id; // here is where we store our item! } // Set us to Idle... state = new IdleState(); var tag = state.tag; // tag == PlayerState.Idle // Set us to HoldItem state = new HoldItemState(ItemId.Apple); var tag = state.tag; // tag == PlayerState.HoldItem var item = state.item_held; // item == ItemId.Apple
This works great! In fact, this is how some people approach their state machines, and that’s perfectly fine. This solves our previous problem because item_held property now only exists in the HoldItem state. Reading it at the wrong time will always give us a helpful error now, whereas before it produced a bug.
Our hacky enums are going to work like this too, but we’ll be able to organize them together better.
// Hacky enum version! state = PlayerState.Idle; state = PlayerState.HoldItem(ItemId.Apple); var item = state.item_held; // item == ItemId.Apple, still works!
HOW DO YOU CREATE THEM?
The first step is to create your backing enum. All of our hacky enums will have a real enum associated with them used to communicate their tag.
enum PlayerStateId { Idle, HoldItem, }
After this, we make a “factory” for our hacky enums. It’s the secret source of the structs we produce.
function PlayerStateFactory() constructor { Idle = { tag: PlayerStateId.Idle, }; HoldItem = function(item_id) { return { tag: PlayerStateId.HoldItem, item_held: item_id, }; } }
Our Idle state can just be a raw struct since we don’t need to pass it any information. However, our HoldItem state does need information passed to it, so we need to make it a function instead. That function takes our information, builds a new struct with all our information inside it, and returns it to us.
Function or no function, we must always make sure that our output is the same: a struct with the tag property. This means that we can still reliably handle our enums.
// No need to worry about what this state actually is -- we guarantee that // it has a `tag` property that we can read. if state.tag == PlayerStateId.HoldItem { // ... } // We can also still use switch... switch state.tag { case PlayerStateId.Idle: break; case PlayerStateId.HoldItem: var item = state.item_held; break; }
Finally, the actual “hack”. We want to be able to use the factory with the actual enum syntax. In order to do this, we place this somewhere at the bootup of our game.
global.__player_state_factory = new PlayerStateFactory(); #macro PlayerState global.__player_state_factory
First, we create a new PlayerStateFactory and store it in the global variable __player_state_factory. I’ve named it with two underscores as a prefix to notate that it should never be used directly. This PlayerStateFactory is where all of our enums will be created moving forward.
Next, we declare a macro, PlayerState, which is simply an alias for our global. Every time we type PlayerState from now on, GameMaker will interpret it as global.__player_state_factory.
And that’s it, it now works!
var state = PlayerState.HoldItem(Item.Apple); // yay!
WHAT ARE THE DOWNSIDES?
There’s two main downsides to consider here.
MEMORY ALLOCATION
For our function-based members (ie: PlayerState.HoldItem), we are allocating a new struct every time we access the enum (meaning we are creating a brand new struct each time in memory). In most contexts (like our example), this is perfectly fine, and not really something to worry about. However, it is still not the same as traditional enums which are completely free. If you are using these in a situation that will call them often (many times a frame), then it is worth keeping in mind.
PERSISTENT STATE
As for our non-function-based members (ie: PlayerState.Idle), they do not allocate. This is nice, but keep in mind that it’s only because you’re referencing the same property every time. If you did some really silly things, you could really break your game:
var state = PlayerState.Idle; state.tag = "gml rulez"; // now everyone who uses PlayerState.Idle is screwed
This is an unlikely scenario, but one to keep in mind!
WHAT ARE SOME OTHER COOL EXAMPLES?
To close off I want to provide a few other interesting use cases to show how universally useful these can be. If you find any cool uses in your own projects, feel free to share them with me!
NETWORK PACKETS
var ping = Packet.Ping; var chat_message = Packet.ChatMessage("Hello, world!"); var login = Packet.Login(global.player_id);
TYPE CHECKING FOR SERIALIZATION
var data = load_data(); // Is data[0] a Real? var type_needed = Type.Real; type_needed.check(data[0]); // we can put methods on our enum members! neat! // We don't *need* to declare a variable -- now they feel like a cool namespace! Type.String.check(data[1]); // What about a more complicated type? var type_needed = Type.Struct({ foo: Type.Real, bar: Type.String, }); type_needed.check(data[2]);
COLOR MANAGEMENT
var purple = Color.Rgb(255, 0, 255); var also_purple = Color.Bgr(255, 0, 255); var still_purple = Color.Hsv(300, 100, 100); var purple_but_rgb_again = still_purple.to_rgb();
If you’ve enjoyed this, you’ll also love this incredible article by Kat, which shows a whole bunch of other ways customize the GML syntax using macros.
ABOUT THE AUTHOR
Gabe Weiner (also known as “lazyeye”) is a professional GameMaker developer who has worked on the 2019 hit Forager and now works an upcoming title called Fields of Mistria. You may also know him from his time managing the GameMaker Discord, as well as hosting obj_podcast. You can find more of his work on his Twitter.
Read more about:
BlogsAbout the Author
You May Also Like