Starting Point
There’s something exciting about starting from zero. Whilst I have a good amount of software development experience, I have never worked on games before. I also hate tutorials with a passion, preferring to read and then reference the documentation as I work.
I started this prototype having completed only the Getting Started and Your First 2D Game.
With that done, I was itching to try something a bit more stateful. In my professional life we spend a lot of time trying to factor state out of our code, so whilst I know what a state machine is, I very rarely implement anything that looks like one. As state machines are fundamental to game development, I felt like it was important to hack around with as soon as possible.
I thought back to the games I loved playing as a kid, and how we would break them at every opportunity. Whether it was jumping over the end of the underground level to the warp pipes in Super Mario, or entering some arcane sequence into the sound test menu of Sonic 2, there was something wildly playful about completely breaking the rules of the game.
What better way tp prove I can track a specific sequence of inputs than to implement a certain cheat code?
Go ahead, try it out :)
I’ve also published the code for this project on github.
Design
State machines are used primarily to orchestrate transitions between states that an entity can be in. Running, walking, ducking, blocking, etc. These states (or modes) determine what actions the entity can take and what effect they might have at a given time.
The easiest example is transitioning between sprite animations. So I decided I would start by simply showing what was being input on the screen.
The Art
I would need some minimal symbols for arrows/d-pad inputs, A and B buttons, and a Start button (gamers of a certain age will already know what code we are implementing).
I drew some sprites for these in Krita. This is the first time I have done any kind of sprite art myself, and whilst they are extremley rough, I really enjoyed the workflow. Using my laptop stylus in Krita had a great flow to it and it really did make me feel like this is something I could spend the time to get better at.
I decided to use the Godot logo (because what’s an example project without it!) as the “idle” animation and made a very simple edit for when the code is entered successfully.
Input Feedback
I also wanted the player to be able to see feedback on whether their inputs were correct, so I put a text lable at the top of the screen and planned to have it change based on how the code entry was going. I thought about doing sounds, but decided it wasn’t necessary for this POC.
Implementation
Now to the meat and potatoes. This was a good opportunity to get a proper feel for GDScript, both the syntax as well as the common types.
Overall I quite like it. It is extremely similar to Python, so much so that a lot of it just works™️ how you expect.
I did have some gripes (if you can call them such) was the actual API provided by the engine.
I’ve seen versions of state machines that use a whole bunch of nodes to orchestrate the whole thing. This felt a bit complicated for what I was trying to do, so in the sprit of YAGNI I decided to use an array index to track the different states instead.
I set up the array values to exactly match the mapped input action labels so I can do direct equality checks with any index of the array.
var konami_code: Array[String] = [
'input_up',
'input_up',
'input_down',
'input_down',
'input_left',
'input_right',
'input_left',
'input_right',
'input_b',
'input_a',
'input_start' ] # The konami code
var code_point: int = 0 # This will represent the current state of the code input
As you can see, I like my type annotations. I also would normally gravitate to an enum (does GDScript even have enum’s?) for something like this, however I could not find any way to constrain both sides of the comparison, so there seemed to be no point.
Another important thing about entering cheat codes is that you have to do it quickly. I didn’t want to make it too hard, but it shouldn’t wait forever. I created a Timer node and connected its signal to the main node. If it elapses, the position in the sequence is reset.
func _on_input_timer_timeout() -> void:
code_point = 0
I added signals for the code advancing, breaking, and completing:
signal code_failed(message)
signal code_completed(message)
signal code_advanced(message)
And now I was ready to handle the user inputs. This is where I hit my first obstacle, I was seeing all inputs come through twice! It turns out the _unhandled_input() signal sends both presses AND release events. When I think on it, this does make sense, as many game states might rely on a button remaining pressed (ducking?), so I can see how you’d want to listen for that event. While I was working on this though, it was annoying!
On top of that, there does not appear to be any signal that only emits mapped input actions, you have to first check if the input maps to an action, and then decide what to do with it.
I ended up with the below code. I was not totally happy with the need for the guard clause, but this is not a problem with GDScript itself, rather the API that the InputEvent class provides.
As long as the input is something we care about, we check if it matches the next expected code value.
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_type() && event.is_pressed():
if event.is_action_pressed(konami_code[code_point]):
$InputTimer.stop()
code_point += 1
if code_point < konami_code.size():
code_advanced.emit('Getting There...')
$InputTimer.start(input_time_limit)
else:
code_point = 0
code_completed.emit('SUCCESS!')
else:
code_point = 0
code_failed.emit('Wrong!')
This is pretty much it for the main node - everything else will be a sub-node and either do its own thing or react to these signals (or both).
Changing The Sprite Animation
I decided that the AnimatedSprite2D may as well react directly to the user inputs, so then the player can see exactly what they did even if it was wrong.
I spent a bit of time here messing with the implementation because it just felt messy. There is no apparent way to yield the string representation of a mapped action, you can only use .is_action_pressed() or similar which yields a bool. This means if you have a long list of possible actions, you are stuck with if/elif/else blocks for this (and more guard clauses).
This is what I ended up with, a bit nasty in my opinion:
func _unhandled_input(event: InputEvent) -> void:
if event.is_released():
pass
elif event.is_action_pressed('input_up'):
animation = 'up'
elif event.is_action_pressed('input_down'):
animation = 'down'
elif event.is_action_pressed('input_left'):
animation = 'left'
elif event.is_action_pressed('input_right'):
animation = 'right'
elif event.is_action_pressed('input_a'):
animation = 'A'
elif event.is_action_pressed('input_b'):
animation = 'B'
elif event.is_action_pressed('input_start'):
animation = 'start'
else:
animation = 'idle'
What I would really love is something like this:
func _unhandled_input(event: InputEvent) -> void:
if event.is_released():
pass
elif event.is_action():
match event.get_action_pressed():
'input_up': animation = 'up'
'input_down': animation = 'down'
'input_left': animation = 'left'
'input_right': animation = 'right'
'input_a': animation = 'A'
'input_b': animation = 'B'
'input_start': animation = 'start'
_: animation = 'idle'
else:
animation = 'idle'
This would however risk errors if you tried to get the action from the event and it just didn’t map to one. I feel this is a little inconsistent, as the engine happily sends you all inputs even if you don’t want them, necessitating guard clauses everywhere, but does not do me the same courtesy when I do want something.
Summary
This little POC was good fun. It introduced me to a lot of the engine primitives, as well as fundamentals of GDScript. It was also nice to see some of my prior programming knowledge come in handy for really simplifying things with the static array. There is a commonly taught Godot state machine pattern which uses a whole bunch of nodes to do basically the same thing - and this might be useful if your needs are more complex, it would not perform as well.
I am looking forward to doing more with Godot and finding opportunities to test what those performance notes from the documentation really mean in real world applications.