Animations

Model Entities, which are entities that have a modelUri when created, can have their in-game animation state completely controlled and blended using the Entity animation methods.

Animations do not work with Block Entities.

GLTF Animations

The HYTOPIA SDK expects all possible animations of a model to be included in a model's .gltf file. Model animations are referenced by their name. For example, if your .gltf file has an animation called idle that you want to play and loop as soon as an entity spawns, you'd do that as follows:

const myEntity = new Entity({
  modelUri: 'models/cow.gltf',
  modelLoopedAnimations: [ 'idle' ], // animations here will play looped when entity spawns
  // .. the rest of your entity options
});

Types Of Animations

HYTOPIA currently supports two types of animations.

  • Looped Animations - These are animations that will contiue to play over and over again from start to finish in a smooth manner until they are explicitly stopped.

  • One-shot Animations - These are animations that will play from start to finish only once, or until they are explicitly stopped or restarted.

Animation Blending & Playback

At this time, all played animations for a model entity are equally blended in the visual playback state of the entity in game. This means you can play multiple GLTF model animations at the same time and they will blend equally between the same model node that may be involved in multiple playing animations.

Starting And Stopping Animations

For model entities, we can easily start, stop and filter the played animations of an entity. Here's an example of how our DefaultCharacterController internally handles animation state changes based on player inputs.

Animation state is internally tracked - so invoking start or stop of animations that are in an irrelevant state won't cause unusual animation states or performance overhead.

// tickPlayerMovement is a method from BaseCharacterController
// it is called each tick when associated with an entity
// controlled by a player (PlayerEntity)
public tickPlayerMovement(inputState: PlayerInputState, orientationState: PlayerOrientationState, deltaTimeMs: number) {
  // current pressed keystate of the controlling 
  // player of the entity  has pressed this tick.
  const { w, a, s, d, sp, sh, ml } = inputState;
  
  // ... Other character controller logic

  if (this.isGrounded && (w || a || s || d)) {
    if (isRunning) {
      // Stop all animations that aren't run
      this.entity.stopModelAnimations(Array.from(this.entity.modelLoopedAnimations).filter(v => v !== 'run'));
      // Play run animation, if it's already playing it won't be restarted since it's looped
      this.entity.startModelLoopedAnimations([ 'run' ]);
    } else {
      // Stop all animations that aren't walk
      this.entity.stopModelAnimations(Array.from(this.entity.modelLoopedAnimations).filter(v => v !== 'walk'));
      // Play walk animation, if it's already playing it won't be restarted since it's looped
      this.entity.startModelLoopedAnimations([ 'walk' ]);
    }
  } else {
    // Stop all animations that aren't idle
    this.entity.stopModelAnimations(Array.from(this.entity.modelLoopedAnimations).filter(v => v !== 'idle'));
    // Play idle animation, if it's already playing it won't be restarted since it's looped
    this.entity.startModelLoopedAnimations([ 'idle' ]);
  }
  
  if (ml) { // player pressed the left mouse button
    // Play our animation named 'simple_interact' once./
    this.entity.startModelOneshotAnimations([ 'simple_interact' ]);
  }
  
  // ... Other character controller logic
}

One-shot Animations

One-shot animations are exactly what they sound like. An animation that plays only once. This is great for things like a gun-shooting effect when the player presses a button in game to fire a weapon, or a movement of a player's hand each time they click to interact with something, etc.

Let's assume our PlayerEntity model assigned to a player when they join a game has a dance animation, and a hand waving animation called wave. We'll play then as a one-shot when a player joins.

world.onPlayerJoin = player => {
  const playerEntity = new PlayerEntity({
    player,
    name: 'Player',
    modelUri: 'models/my-player.gltf',
    modelLoopedAnimations: [ 'idle' ],
    modelScale: 0.5,
  });
  
  playerEntity.spawn(world, { x: 0, y: 10, z: 0 });
  
  // Note: An entity must be spawned to start/stop animations,
  // otherwise the invoked animation methods will throw an error.
  
  // dance and wave animation is assumed to exist in the my-player.gltf
  // file. Both animations will play and blend together.
  playerEntity.startModelOneshotAnimations([ 'dance', 'wave' ]);
}

Animations & Entity Colliders

It's important to note that the animation state of model entities is entirely visual. Played animations will not automatically alter or move the collider(s) of an entity even if the entity is visually moving relative to it's collider because of the animation.

If you need your collider to move along with the animation state played, you'll need to implement this manually with your own timings and logic to alter the translation and rotation of the entity collider(s) relative to the animation played.

Last updated