Galacean Logo
English
English
Physics Series Part 4: Character Controller Driving Character Animation

Physics Series Part 4: Character Controller Driving Character Animation

technical

Introduction

In the previous three articles, we introduced the basic design of physical components around PhysX.

  1. The First Physics Article: Cross-Platform Compilation of PhysX Based on WebAssembly and PVD Joint Debugging
  2. The Second Physics Article: Multi-Backend Physics and Component Architecture Design
  3. The Third Physics Article: Implementing a Lightweight Collision Detection Algorithm Package

In this article, we will delve into the CharacterController component, which is essentially an advanced encapsulation of the collider component. This component makes it easier to implement motion control and event triggering related to character control. For example, the character controller can control the character's ability to climb slopes, overcome obstacles, and detect states after object movement to trigger different animations or user logic.

Sep-16-2022 10-29-09.gif

Basic Usage of the Character Controller

As mentioned earlier, the character controller is actually a type of collider component, an advanced encapsulation of the collider component. Therefore, in the component design, it is also treated as a subclass of the collider Collider:

/**
* The character controllers.
*/
export class CharacterController extends Collider {}

However, unlike other collider components, the character controller only supports one ColliderShape, and it only supports:

  1. BoxColliderShape
  2. CapsuleColliderShape (most commonly used)

With the character controller component, you can configure the controller's capabilities through a series of properties, such as setting the height of obstacles that can be overcome. During the program's runtime, user operations are converted into calls to the move function to control the movement of the character controller. During the movement, the character controller can determine whether it can climb over obstacles or be blocked by overly steep slopes based on the configuration parameters.

/**
* Moves the character using a "collide-and-slide" algorithm.
* @param disp - Displacement vector
* @param minDist - The minimum travelled distance to consider.
* @param elapsedTime - Time elapsed since last call
* @return flags - The ControllerCollisionFlag
*/
move(disp: Vector3, minDist: number, elapsedTime: number): number;

The most important parameter in this function is disp, which specifies the displacement of the controller and returns a value used to determine whether the controller has encountered any obstacles after moving:

/**
 * The up axis of the collider shape.
 */
export enum ControllerCollisionFlag {
  /** Character is colliding to the sides. */
  Sides = 1,
  /** Character has collision above. */
  Up = 2,
  /** Character has collision below. */
  Down = 4
}

Synchronization of Physical Components and Character Entities

The character controller is somewhat similar to a dynamic collider. However, a dynamic collider, such as a small ball, moves according to physical laws when subjected to physical feedback. The character controller, on the other hand, delegates the logic of controlling movement to the user, controlled through the aforementioned move function. Therefore, the synchronization method between physical components and character entities is consistent with that of dynamic colliders:

  1. Before the physical update, the _onUpdate function is called to synchronize other user operations that change the controller to the physical component. This step is executed by checking the dirty flag in the Transform.
  2. After completing the physical update, the _onLateUpdate function is called to synchronize the position of the physical component back to the character entity.

Therefore, if the user logic calls the move function, the physical system will determine whether the character will encounter other obstacles after moving and decide whether to execute the move command. This way, all user operations are filtered through the physical system, making the character's behavior appear more "physical."

A Simple State Machine

The move function returns the state of the character controller after displacement. Checking whether it is Down can determine if the character has landed, and checking whether it is Sides can determine if the character has hit a side object, etc. The character controller has multiple composite states, and these states are often related to different animation clips. For example, a character performing a jump involves a jump animation, a falling animation, a landing animation, and finally returning to the character's idle state. The animation state machine can be written using AnimatorStateScript to build complex animation logic. For simplicity and to highlight the use of the character controller, we will try to write a simple state machine.

 
type State = "Run" | "Idle" | "Jump_In" | "Fall" | "Landing";
 
class AnimationState {
  private _state: State = "Idle";
  private _lastKey: Keys = null;
 
  get state(): State {
    return this._state;
  }
 
  setFallKey() {
    this._state = "Fall";
  }
 
  setIdleKey() {
    if (this._state == "Jump_In") {
      return;
    }
 
    if (this._state === "Fall") {
      this._state = "Landing";
    }
 
    if (this._state === "Landing") {
      this._state = "Idle";
    }
  }
}

Assuming we have five animation states, AnimationState maintains the current state and can switch between different states by calling its internal functions. Next, we need to move the character controller in the onPhysicsUpdate script function. It is worth noting that this function is called at the same frequency as the physical system's update, so it may be called once or multiple times per rendering frame.

  onPhysicsUpdate() {
    const physicsManager = this.engine.physicsManager;
    const gravity = physicsManager.gravity;
    const fixedTimeStep = physicsManager.fixedTimeStep;
    this._fallAccumulateTime += fixedTimeStep;
    const character = this._controller;
    character.move(this._displacement, 0.0001, fixedTimeStep);
 
    const flag = character.move(new Vector3(0, gravity.y * fixedTimeStep * this._fallAccumulateTime, 0), 0.0001, fixedTimeStep)
    if (flag & ControllerCollisionFlag.Down) {
      this._fallAccumulateTime = 0;
      this._animationState.setIdleKey();
    } else {
      this._animationState.setFallKey();
    }
    this._playAnimation();
  }
 
  _playAnimation() {
    if (this._animationName !== this._animationState.state) {
      this._animator.crossFade(this._animationState.state, 0.1);
      this._animationName = this._animationState.state;
    }
  }

In this function, we first move the character controller in the _displacement direction based on user input. Since the user's operation is only moving on the ground, the return value of the function is not important at this time. To make the character controller automatically fall back to the ground, we attempt to move the character a specific distance in the y direction and determine whether it has landed based on the return value, while setting a specific animation state. The _playAnimation function will check if the animation state has changed, and if so, use the crossFade function to automatically blend the two animations and switch to the next state.

Summary

The character controller is an advanced encapsulation of the collider component. It allows you to link components and systems such as InputManager, Animator, etc., making it very easy to switch the character's animation logic by judging the physical state. As mentioned above, you will also notice that combining the character controller with ray detection can easily implement the logic of shooting games. In addition to the character controller, the 0.8 milestone also introduced basic physical constraint components, including HingeJoint, SpringJoint, and FixedJoint. In the next milestone, we will continue to focus on enhancing physics-based animation capabilities, including ragdolls, elastic bones, etc. These components are also implemented using physical constraints. By combining the character controller with various physical animation effects, the playability and realism of interactive projects can be greatly improved.

If you have any physics-related technical points you want to understand or urgently need, feel free to leave us a message.

Galacean Logo
Make fantastic web apps with the prospective
technologies and tools.
Copyright © 2025 Galacean
All rights reserved.