Using clj-statecharts to Manage Character Animations
I’ve been spending my spare time doing some game development in ClojureScript, and recently I needed to solve the problem of dealing with animations for the player character. Having used the Unity game engine in the past, I knew that finite state machines (FSM) were a great way to manage transitioning between different animations for a character, based on underlying state changes.
For my game, I am currently dealing with 4 animations:
idle- When standing stillrun- When movingjump- When jumpingattack- When attacking
These animations come from the Kenney Character Assets bundle
So, I spent some time building out a very poor FSM implementation for managing the animation states and transitioning the animations when the character’s state changes due to player input.
I soon learned that my FSM implementation was not going to cut it as my animation needs were a bit more advanced than I originally thought. I found two issues:
- The
idle,run, andjumpanimations were mutually exclusive, but theattackanimation actually needs to be blended with the other animations. - For the
attackanimation, I also wanted to emit an event at a particular time in the animation to signal that the “hit” procedure should execute, as the sword-swing doesn’t really land until about 1/2 second into the animation.
I was going to start researching the different FSM libraries available for Clojure, when, literally the next day, I saw a post on the Clojure subreddit about a great library called clj-statecharts by Lucy Wang.
After reading through the documentation, I decided to try using it to replace my terrible FSM implementation.
Here’s a simplified version of what my character animation FSM looks like in clj-statecharts:
(def lower-body-fsm {:initial :grounded
:states {:airborne {:on {:tick {:target :grounded
:guard (complement airborne?)}}
:entry #(play-animation! :character/jump)}
:grounded {:initial :idle
:on {:tick {:target :airborne
:guard airborne?}}
:states {:idle {:on {:tick {:target :run
:guard moving?}}
:entry #(play-animation! :character/idle)}
:run {:on {:tick {:target :idle
:guard (complement moving?)}}
:entry #(play-animation! :character/run)}}}}})
(def upper-body-fsm {:initial :attack-idle
:states {:attack-idle {:on {:tick {:target :attack-start
:guard attacking?}}}
:attack-start {:after [{:delay 450
:target :attack-end
:actions #(emit-event! :attack-hit)}]
:entry #(play-animation! :character/attack)}
:attack-end {:on {:tick {:target :attack-idle
:guard #(animation-complete? :character/attack)}}}}})
(def full-fsm {:id :character-animation
:type :parallel
:regions {:lower-body lower-body-fsm
:upper-body upper-body-fsm}})
(defn tick! [service]
(fsm/send service :tick)
(defn init! []
(let [machine (fsm/machine full-fsm)
service (fsm/service machine)]
(fsm/start service)))
A few things to note about this FSM:
- The only event ever sent is the
:tickevent, which is fired on every frame. I make extensive use of guarded transitions to determine if a transition should be made based on the underlying game-state. This is represented by all of the predicates (airborne?,moving?,attacking?etc) - The undisclosed
play-animation!function deals with playing the animation via theAnimationMixerin Three.js. - The undisclosed
emit-event!function simply emits an event to character’s event-system - I’m using parallel states of
:upper-bodyand:lower-bodyto separate the animation layers. So the character can be in both a:runstate for the:lower-bodyand:attack-startstate for the:upper-body - I’m using delayed transitions to handle emitting the event after 450ms have passed since the attack animation started.
- I’m using a nested state machine for the
:groundedstate to toggle between the:groundedand:airbornestates
Overall, I’m really happy with how well clj-statecharts worked for this. It feels much easier to manage than my previous hacky FSM system, and it will be a very useful tool to have for other aspects of my game.
Here’s what the result looks like in-game:
