Attack on Zombies!
This update features two new features:
- (minor) A crouch before you jump
Attacking is a big part of the game, but I needed to prevent the player from moving or jumping while you're attacking. I ended up putting in a function, "is attacking", and a bunch of hacks to disable everything. Cool. Moving on, to the crouch jump...
I was watching some youtube videos, when I came across a comment someone made on the old NES Batman game. In it, before he jumps, he briefly ducks. I liked the idea, as it makes jumping feel like it has more weight, plus it makes it more of a tactical decision. This makes the jump have a delay, you're committing to it, but you rise VERY quickly, and fall a bit slower. I like the game feel of it, and I want to continue to embrace mechanics that force you to think more tactically, rather than run-and-spam attack. It's a reason I like the modern Castlevania games.
I couldn't easily hack in another animation-waiting event... and the code was looking pretty complex. So I refactored, and I sort of discovered a new pattern on Godot that I've quickly fallen in love with. It's a pretty common state machine pattern I've practiced outside of Godot, so it's nothing revolutionary, I just like how it was implemented in Godot.
First, a high level:
He's the player object. He's a kinematic body, he has a sprite, he has some animations... but what I want to focus on is the "StateMachine" node. This is the core of the pattern. The StateMachine manages the current state, and each child node is a different state with its own logic. Let's take a look at the StateMachine code:
extends Node onready var current_state:BaseState = $Idle; func _process(_delta): update_current_state(); var player = get_parent(); current_state.update(player, player.input); func update_current_state(): var player = get_parent(); var next = current_state.get_next_state(player, player.input); if (next == null): return; current_state = get_node(next); current_state.on_enter(player);
We have a single variable, "current_state", and initialize it to the $Idle state. Not much explanation needed here. You can initialize it to whatever you want... but I think a better pattern would be to initialize it to the first child.
Some other alternatives I saw included people making a mapping between state names and instances of objects, or state names and individual nodes... I really liked the mapping state names to nodes pattern, it feels very Godot, but I didn't like how they manually did it. Instead, our mapping of the name of a state is simply the name of the child. Adding a new state is as simple as adding a new child!
Now, the _process function... doing it this way means that our player object doesn't have any idea that our state machine exists... the player can do things, but the state machine drives him. This means, we could put the state machine outside of the player, and instead of calling get_parent() to get the player object, we could pass in any-old object that we want to control the state of.
Now, the first line of code in process is to update the current state. We ask the current state, "should we transition to a new state", by calling get_next_state. The function will decide if we should transition to a new state. If so, it returns the name of the new state, otherwise, it returns null. We then invoke an "on_enter" function if we do change state (this was needed to trigger one-off things that should happen immediately upon entering a state. If you want to trigger something when leaving a node, you could call the "on_exit" before updating the current_state.)
Lastly, we call update in the node... that's about it!
Let's look at an example of a state to recap. Here is the code for the Attack state:
extends BaseState func on_enter(player: Player): player.set_animation("Attack-Knife"); player.velocity.x = 0; func get_next_state(player: Player, input: PlayerInput): var animation = player.get_node("Animation") as AnimationPlayer; if (!animation.is_playing()): return "Idle"; return .get_next_state(player, input);
When you first enter the state, we need to trigger the "Attack-Knife" animation, which plays an animation, but also enables the appropriate hit-boxes to do damage. We then zero out the velocity. This prevents the player from sliding once they initiate an attack.
Then, when getting the next state, we need to see if the current animation is still playing. If it's done, then we go back to the idle state, otherwise, we call the base class (which just returns null).
If we had played the animation in the update, it was perpetually play the animation forever. This is why we need to do it just once.
I plan on using this pattern going forward. In the past, I've had implicit state machines, with many variables tracking them... it gets complex quickly. If I can make my state machine object more generic, then maybe I'll even export it as an asset that can be re-used between projects! If not... well... it's not a lot of code anyways.
Relevant source code for this tutorial can be found here:
Get Zombies Ate My Teacher
Leave a comment
Log in with itch.io to leave a comment.