1
\$\begingroup\$

I'm making a simple platformer game and was suggested to use ECS from a friend. It looked pretty interested and at least worth a try, so I did. I got ECS working and started work on the rendering system. But I have no idea how to do it, at least correctly, given that each enemy type will have multiple animation sequences that it'll switch to depending on what its doing. A zombie might have a walk, idle, and attack sequences, while a bat would only have the fly animation. This question also extends to AI systems, but rendering is the priority right now.

The way I see it there are 2 possible ways, but neither seem like the right way.

  1. create a different component and system for every type, like ZombieSpriteComponent, ZombieSpriteRenderSystem, BatSpriteComponent, BatSpriteRenderSystem, etc.
  2. shove all the states from the individual types into a giant component and system which switches depending on whether the given component has the state. hasWalkState, hasAttackState, hasFlyState, etc.

I'm using C++, SDL, and a custom ECS I wrote based on this, in case it helps.

\$\endgroup\$
1
\$\begingroup\$

I'd recommend taking a look at how this is handled in other component-based engines (not necessarily full ECS). That will give you some examples of how to attack this problem with reusable components, in a way that you know has successfully shipped other games.

Taking Unity for example, they separate out a reusable Animator component (/system) that just generically plays animation state machines provided to it as data.

The state machine (called an Animator Control Graph) is a collection of Animation Clips (timelines of keyframes & interpolation curves) which serve as nodes/states in the graph, and transitions which describe when to switch/blend between two clips, basically directed edges between nodes. It can also contain a list of properties to use in conditions that enable/disable various transitions or modulate the playback and blending.

An instance of this graph will have variables to keep track of the current state / transition, the playhead position in any playing clips, and something like a map containing the current values of its properties.

This helps decouple the visual/animated representation of a state from the player control / AI behaviour that triggers those states.

A movement component might get a reference to its Animator and send it data like:

  • SetFloat("horizontalSpeed", velocity.x)

  • SetBool("isGrounded", groundRaycast.hit)

...and the decision about how to translate those properties to animations lives completely in the data of the graph being navigated by a general-purpose animation system, rather than needing one-off code.

I could use the same movement and animation components/systems and their underlying code for a character that walks and a character that slithers, just by putting walking keyframe sequences into one graph and slithering keyframe sequences into the other, but using the same vocabulary of property keys to drive both state machines.

This pattern of "put the differences in data" repeats for the AI behaviour side of the equation too. I could make a movement component take parameters that describe its movement speed and acceleration to get many different movement behaviours out of one set of code. For their AI brains, I could use a reusable BehaviourTree component, and feed it different trees of behaviour states as data.

But to get the difference between flying enemies like bats and ground-based enemies like zombies, you'd probably want different code. To avoid creating new components/systems for every single object type, try to focus on composition: build up a complex behaviour out of reusable modular pieces.

Have a Ground Movement component and a Flying Movement component that can both expose a similar API to the brain logic in the behaviour component(s), and both communicate with the animation component. That way you can mix and match those components freely to get new behaviour combinations out of the same code (like a Vampire enemy that has both a FlyingMovement and GroundMovement, with only one enabled at a time as it switches between bat and humanoid forms...)

Overall, whenever you can, try to rephrase your problems from "is a" to "has a". It's not that a bat enemy "is a" bat. It "has a" flying animation, and a flying movement, etc. Those "has a" relationships clarify how you can represent the particular traits of a game entity with swappable data and reusable building blocks, rather than one-off code.

\$\endgroup\$
6
  • \$\begingroup\$ I get the whole "put the differences in data" thing, the issue is I'm not sure how to set up the state machine side of it in ECS. Do I just have the attack node on the ai behavior tree tell the AnimationSystem that it's attacking and to switch to the attack animation? (I just decided to use behavior trees >15 minutes ago) \$\endgroup\$ Oct 17 at 14:16
  • 1
    \$\begingroup\$ You tell it "set parameter 'isAttacking' to 'true'" (data). The next time the state machine evaluates its transitions from the current state (also data), it will find that the transition with the condition (also data) isAttacking == true is satisfied, and it will execute that transition. \$\endgroup\$
    – DMGregory
    Oct 17 at 14:19
  • \$\begingroup\$ So after the transition, it'll set a value in the SpriteComponent to let the SpriteRenderSystem to use the attack animation/sprite? \$\endgroup\$ Oct 17 at 14:41
  • \$\begingroup\$ The animation clip (more data) will consist of a set of keyframes that each specify a property path to modify when the playhead reaches that frame (like This.SpriteRenderer.CurrentSprite) and a value to set it to (ZombieAttackFrame0) \$\endgroup\$
    – DMGregory
    Oct 17 at 14:43
  • 1
    \$\begingroup\$ The more generic you want your animations, the more complex your systems will get. What I've described is how it's done in Unity, which is made to be very generic - for animation 3D characters with skeletal rigs, or material properties, etc. If animations in your game only ever need to set a frame index into a sprite sequence, then you can hard-code that path as something like a direct call to spriteRenderer.SetSpriteIndex() rather than navigating arbitrary property paths. \$\endgroup\$
    – DMGregory
    Oct 17 at 15:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.