Animation meets StateMachine - Grisgram/gml-raptor GitHub Wiki
When you organize the behavior of your game object in StateMachines (which you should do!), you will quickly notice that the on_enter
function for a state is by far the most important one. This is where the magic happens.
- A monster entering the "hunting" state will likely draw its weapon (an
Animation
!), make some groaning sound, and start running towards the player. - Any living object getting "hit" will also play a Hit-
Animation
. - Anything spawning or dying might also,
- and a chest that is opened will likely play its Open-
Animation
.
So, you have a very good reason to design many animations which are sometimes simply other sprites. Other times, like the spawn event, these are simple modifications of the idle version of the game object and can be covered with an Animation
and a good AnimCurve
quite well.
I want to show you a bit more of the source code from One (K)night alone with some states and how they utilize the Animation
class in their on_enter
event.
In this game, when the player enters a field, the neighboring fields become revealed (the "fog" disappears).
This is the on_enter
of the "unfold" state of a map field in the game.
.add_state("unfold",
function(sdata) {
GAME.player_data.gain_xp(1);
animation_run(self, 0, 30, acFogDisappear,, "open") // enter the "open" state, when animation is finished
.add_finished_trigger(function() { // .add_finished_trigger gets invoked when animation finishes
image_alpha = 0;
if (contents != undefined)
with(contents) event_user(game_event.field_unfolded); // game-internal -- ignore for this example
});
)
When spawned, the Bat simply fades in from alpha 0 to 1 and switches into the "idle" state when spawning is complete.
.add_state("spawn",
function(sdata) {
sprite_index = sprVampireBat;
image_index = 0;
image_speed = 1;
image_alpha = 0;
animation_run_ex(self, 0, 90, acLinearAlpha,, "idle");
// Yes, you have seen right! It is only ONE SINGLE LINE of code!
// What happens here:
// * animation_run_ex is used ("run EXclusive"), which stops ALL other animations on this object
// before starting the alpha animation
// * It runs raptor's default Alpha curve
// * It will enter "idle" state, when spawning is finished
)
With these two examples you can see the pattern for most game objects:
- When entering a state, the matching
Animation
for that state is played - Normally you'd want to add a finished trigger (more on Triggers in Animation Triggers) to start the next step after the animation
Many events can happen while the game runs, and it may be necessary to pause some (or all) of your running Animations
whether it is because a full-screen overlay has been opened or simply the Pause Key has been pressed in your game.
To allow you to pause and later resume an Animation
in the exact frame where it was paused, Animation
has functions available which cover this scenario:
pause()
resume()
set_paused(paused)
is_paused()
As mentioned in then StateMachine and Animation main pages, both of these rely on the ListPool and are globally managed by the RoomController
. Both behave identically.
Call scoping means that the ListPool controls who is self
when invoking a callback.
In both cases, inside an on_enter
, step
and on_leave
event of a State
, as well as in any trigger of an Animation
, the self
pointer always points to the owner
of the object. The 'owner' is the game object you supplied as the first parameter to the constructor - the object that owns the State
or Animation
respectively. For example, in the code above, the owner is The Bat object.
NOTE: There is no limit on the amount of parallel Animations
running for one object. The RoomController
manages all active animations, and if you have 2 independent Animations
running on the same object (like one for fading in/out and another that rotates the object or changes the blend_color), this is perfectly ok!
In Version 1.3
, some shortcuts have been added to improve communication and control-sync between these two major elements of raptor
.
First, the Animation
class constructor (and the animation_run_...
functions) have been extended by an optional _finished_state
parameter.
This one works for every StatefulObject
and for all objects that own a StateMachine
named states
(which is the name of the StateMachine in a StatefulObject).
If you supply this parameter, you can omit the ever recurring finished_trigger that just sets a new state when an Animation
has finished.
So, with the new parameter, an Animation
like this
animation_run(self, 0, 30, acLinearMove)
.add_finished_trigger(function() {
states.set_state("idle");
});
can now be greatly simplified as
animation_run(self, 0, 30, acLinearMove,, "idle");
This (on first sight) very simple addition can simplify your code greatly and will ensure correct timing when transitioning from one state to another through an animation.
The new method lock_animation
in the StateMachine
class will prevent state changes to any other state while the specified Animation
is running. In addition to that, while animation-locked, the StateMachine will buffer the first request to a state change while it is locked. When the Animation
finishes, the buffered state will be entered.
While animation locking in general is not always what you want in games, there are many situations where you will want this sync. Spawning & Dying of objects are good examples here or a phase-change in a boss fight where the boss is invulnerable while it changes shape.
"Why is only the first state change request bufferd?"
Because in the logical workflow of the game, there is normally only one correct follow-up state to the current state (from a workflow view).
Locking on an Animation
is simple:
states.lock_animation(animation_run(self, 0, 25, acLinearScale));
With the code above, the StateMachine will be locked in its current state until that animation is finished.
...and now imagine, you already know, that you want the state after the animation to be "idle". To ensure, nothing can come in between, you can simply write this:
states.lock_animation(animation_run(self, 0, 25, acLinearScale));
states.set_state("idle");
Due to the animation lock, nothing will happen by calling set_state
, but this request will be the first request after the animation lock and it will be buffered. So, nothing can prevent this workflow from happening. The Animation will play, then the object will enter the "idle" state, no matter what other modules of your game might try to do with that object.
Caution
Using lock_animation
on a LOOP_INFINITE
animation will freeze the StateMachine
forever, until the animation is stopped using .abort()
!
Buffering the next state change request is optional (but enabled by default). To turn it off (all state change requests will be ignored then while animating), you may supply false
as the optional second parameter to lock_animation
:
states.lock_animation(animation_run(self, 0, 25, acLinearScale), false);
Next to read: Animation Triggers