The Character class serves as the foundational element for all game avatars, whether under player control or managed by AI. This class employs the CharacterMovement component as its character controller / motor, seamlessly integrating typical modes of movement, such as walking, falling, flying, and swimming. Additionally, it boasts fully configurable jump and crouch mechanics.
The Character class dictates its movement based on its current mode, delegating the execution of this movement to the CharacterMovement component, which ultimately performs the physical motion.
How Does a Character Work?
By default, a Character implements an auto-simulation, taking on the responsibility of invoking its Simulate method. This method undertakes the heavy lifting of the character's movement, including updating its current movement mode, adjusting rotation based on its current rotation mode, handling jump and crouch actions, triggering events, and more.
When auto-simulation is enabled (the default operation), the Character implements a LateFixedUpdate through a coroutine. This ensures that the Character is simulated after Unity's internal physics update, a crucial step to deliver seamless physical interactions.
Utilize the enableAutoSimulation property of the Character to enable or disable its auto-simulation.
In essence, a Character interprets input vectors and events, including keyboard inputs used to control the character. It translates these inputs into updates for the character's position and rotation. This process occurs through its different movement modes, such as walking, flying, swimming or any custom mode you may create. Each mode defines how inputs are processed and influences the resulting outputs.
All of this occurs within the Simulate method, with the following execution order:
This is called before any movement mode is updated. It automatically switches between the Walking and Falling movement modes based on the CharacterMovement grounding status and handles the jump and crouch mechanics.
Finally, it triggers the OnBeforeSimulationUpdate event, allowing us to expand its functionality.
Utilize the OnBeforeSimulationUpdate method/event to externally update the character's state.
privatevoidSimulationUpdate(float deltaTime)
This utilizes the given movement direction vector (SetMovementDirection) to calculate a desired velocity vector. Subsequently, this information is handed over to the current movement mode, which, in turn, adjusts the character's velocity according to its defined rules.
Additionally, it performs the character's rotation according to its current rotation mode.
privatevoidAfterSimulationUpdate(float deltaTime)
This method is responsible for triggering the ReachedJumpApex and AfterSimulationUpdated events. At this point, the character's rotation and velocity are updated according to its current modes, but the movement has not been applied yet. This occurs in the subsequent CharacterMovementUpdate method.
This final simulation method passes the character's velocity to the CharacterMovement Move method, which subsequently executes the requested movement. Finally, it triggers the CharacterMovementUpdated event. At this point, the character's state is up to date.
Character Movement Modes
The Character class introduces the concept of Movement Modes, such as Walking, Falling, Flying, Swimming, and Custom. Each movement mode has a set of predefined rules and properties that dictate how the character moves through the world. While this concept is somewhat related to character states (e.g., logical states like Jumping, Attacking, Dying, etc.), it should not be confused. The primary purpose of a Movement Mode is to determine how the character should move through the world.
For instance, the Flying Movement Mode, while implying that the character is in a flying logical state, specifically defines how the character is moved. For example, it allows the character to move through the air without being affected by gravity, giving it the freedom to remain unbound to the ground, with its vertical velocity preserved.
The movement modes are defined as follows:
publicenumMovementMode{ /// <summary> /// Disables movement clearing velocity and any pending forces / impulsed on Character. /// </summary> None, /// <summary> /// Walking on a surface, under the effects of friction, and able to "step up" barriers. /// Vertical velocity is zero. /// </summary> Walking, /// <summary> /// Falling under the effects of gravity, after jumping or walking off the edge of a surface. /// </summary> Falling, /// <summary> /// Flying, ignoring the effects of gravity. /// </summary> Flying, /// <summary> /// Swimming through a fluid volume, under the effects of gravity and buoyancy. /// </summary> Swimming, /// <summary> /// User-defined custom movement mode, including many possible sub-modes. /// </summary> Custom}
You can change the character's movement mode by using its SetMovementMode function. This action automatically invokes the OnMovementModeChanged method and triggers the MovementModeChanged event. The OnMovementModeChanged method is specifically designed to handle the initiation of particular modes, such as enabling or disabling ground constraints, stopping jumps, resetting jump counts, and more.
It's important to note that Walking and Falling modes are automatically managed based on the character’s grounding status. When the character is constrained to the ground on a walk-able surface, the Walking movement mode is enabled. Conversely, if the character is not on the ground or is on a non-walkable surface, it will switch to the Falling movement mode.
For the Flying movement mode, you need to explicitly enable and disable it as necessary. Exiting the Flying state is safely achieved by transitioning to the Falling movement mode, which, in turn, automatically leads to the Walking mode. This principle also applies to Custom movement modes.
// Enter flying modeSetMovementMode(MovementMode.Flying);..// Exits flying modeSetMovementMode(MovementMode.Falling);
While you can manually enable / disable the Swimming movement mode, this is automatically managed when using aPhysicsVolume.
Moving a Character
A Character comprises a set of methods specifically crafted to streamline the execution of actions, usually in response to input events like on-down, on-up, and so forth.
The methods incorporated are:
/// <summary>/// Assigns the Character's movement direction (in world space),/// eg: our desired movement direction vector./// </summary>publicvoidSetMovementDirection(Vector3 movementDirection)/// <summary>/// Request the Character to jump./// The request is processed on the next simulation update./// Call this from an input event (such as a button 'down' event)./// </summary>public virtual void Jump()/// <summary>/// Request the Character to end a jump./// The request is processed on the next simulation update./// Call this from an input event (such as a button 'down' event)./// </summary>public virtual void StopJumping()/// <summary>/// Request the Character to crouch./// The request is processed on the next simulation update./// Call this from an input event (such as a button 'down' event)./// </summary>public virtual void Crouch()/// <summary>/// Request the Character to stop crouching./// The request is processed on the next simulation update./// Call this from an input event (such as a button 'up' event)./// </summary>public virtual void UnCrouch()
Movement Relative to Camera
It is often necessary to align our character's movement with the camera's orientation, ensuring consistent motion regardless of the camera's viewing direction. To simplify this process, ECM2 incorporates a helpful Vector3 extension method:
/// <summary>/// Transforms a vector to be relative to given transform./// If isPlanar == true, the transform will be applied on the plane defined by world up axis./// </summary>publicstaticVector3relativeTo(thisVector3 vector3,Transform relativeToThis,bool isPlanar =true)
This can be employed to align our movement direction with the character's follow camera as follows:
// Poll inputVector2 inputMove =newVector2(){ x =Input.GetAxisRaw("Horizontal"), y =Input.GetAxisRaw("Vertical")};// World space movement directionVector3 movementDirection =Vector3.zero;movementDirection +=Vector3.right*inputMove.x;movementDirection +=Vector3.forward*inputMove.y;// Make movement direction relative to a given TransformmovementDirection =movementDirection.relativeTo(_character.cameraTransform);// Assign camera's relative movement direction to character_character.SetMovementDirection(movementDirection);
An alternative approach, without utilizing the relativeTo extension method, is demonstrated below:
// Pool inputVector2 inputMove =newVector2(){ x =Input.GetAxisRaw("Horizontal"), y =Input.GetAxisRaw("Vertical")};// Movement relative to a camera's Transform (cameraTransform)Vector3 movementDirection =Vector3.zero;movementDirection +=cameraTransform.right*inputMove.x;movementDirection +=cameraTransform.forward*inputMove.y;// Assign camera's relative movement direction to character_character.SetMovementDirection(movementDirection);
The primary distinction lies in the relativeTo extension method, allowing us to execute the transformation on the plane defined by the given world-up axis. This becomes crucial when working with custom world-up directions, as seen in games like Mario Galaxy.
Character Rotation Modes
A Character, although it can be rotated as desired by directly modifying its rotation property, also incorporates a set of predefined Rotation Modes, similar to the Movement Modes.
The rotation modes are defined as follows:
publicenumRotationMode{ /// <summary> /// Disable Character's rotation. /// </summary> None, /// <summary> /// Smoothly rotate the Character toward the direction of acceleration, /// using rotationRate as the rate of rotation change. /// </summary> OrientRotationToMovement, /// <summary> /// Smoothly rotate the Character toward camera's view direction, /// using rotationRate as the rate of rotation change. /// </summary> OrientRotationToViewDirection, /// <summary> /// Let root motion handle Character rotation. /// </summary> OrientWithRootMotion, /// <summary> /// User-defined custom rotation mode. /// </summary> Custom}
The Character incorporates the following functions designed for alter its rotation:
/// <summary>/// Sets the Character's current rotation mode./// </summary>publicvoidSetRotationMode(RotationMode rotationMode)/// <summary>/// Sets the yaw value./// This will reset current pitch and roll values./// </summary>public virtual void SetYaw(float value)/// <summary>/// Amount to add to Yaw (up axis)./// </summary>public virtual void AddYawInput(float value)/// <summary>/// Amount to add to Pitch (right axis)./// </summary>public virtual void AddPitchInput(float value)/// <summary>/// Amount to add to Roll (forward axis)./// </summary>public virtual void AddRollInput(float value)
This are processed on the next simulation update.
Ground Constraint
The CharacterMovement component, serving as our character controller / motor, introduces a crucial feature absent in Unity's built-in character controller—the GroundConstraint. This feature plays a pivotal role in ensuring the character remains grounded on walk-able surfaces, preventing unintended launches when moving at higher speeds. However, this enhancement necessitates informing the system when the character is permitted to disengage from the ground, for instance, when engaging in activities like climbing, flying, swimming, or jumping.
To achieve this, we leverage the constrainToGround property of the CharacterMovement component, allowing us to explicitly enable or disable it. Alternatively, the Character PauseGroundConstraint method can be employed to temporarily suspend the ground constraint for a specified duration (N seconds),
For example, a basic jump:
// Temporarily disable the ground constraint to allow the character to leave the groundPauseGroundConstraint(0.1f);// Apply a vertical impulse to the characterLaunchCharacter(Vector3.up*10.0f);
Events
The Character class provides a diverse array of events and event handlers that can be utilized for responsive actions either within the local context (e.g., within a derived class of Character) or externally.
The included events are:
/// <summary>/// Event triggered when a character enter or leaves a PhysicsVolume./// </summary>public event PhysicsVolumeChangedEventHandler PhysicsVolumeChanged;/// <summary>/// Event triggered when a MovementMode change./// </summary>public event MovementModeChangedEventHandler MovementModeChanged;/// <summary>/// Event for implementing custom character movement mode./// Called if MovementMode is set to Custom./// </summary>public event CustomMovementModeUpdateEventHandler CustomMovementModeUpdated;/// <summary>/// Event for implementing custom character rotation mode./// Called when RotationMode is set to Custom./// </summary>public event CustomRotationModeUpdateEventHandler CustomRotationModeUpdated;/// <summary>/// Event called before character simulation updates./// This 'hook' lets you externally update the character 'state'./// </summary>public event BeforeSimulationUpdateEventHandler BeforeSimulationUpdated;/// <summary>/// Event called after character simulation updates./// This 'hook' lets you externally update the character 'state'./// </summary>public event AfterSimulationUpdateEventHandler AfterSimulationUpdated;/// <summary>/// Event called when CharacterMovement component is updated (ie. Move call)./// At this point the character movement has completed and its state is current. /// This 'hook' lets you externally update the character 'state'./// </summary>public event CharacterMovementUpdateEventHandler CharacterMovementUpdated;/// <summary>/// Event triggered when characters collides with other during a Move./// Can be called multiple times./// </summary>public event CollidedEventHandler Collided;/// <summary>/// Event triggered when a character finds ground (walkable or non-walkable)/// as a result of a downcast sweep (eg: FindGround method)./// </summary>public event FoundGroundEventHandler FoundGround;/// <summary>/// Event triggered when a character is falling and finds walkable ground/// as a result of a downcast sweep (eg: FindGround method)./// </summary>public event LandedEventHandled Landed;/// <summary>/// Event triggered when Character enters crouching state./// </summary>public event CrouchedEventHandler Crouched;/// <summary>/// Event triggered when character exits crouching state./// </summary>public event UnCrouchedEventHandler UnCrouched;/// <summary>/// Event triggered when character jumps./// </summary>public event JumpedEventHandler Jumped;/// <summary>/// Triggered when Character reaches jump apex (eg: change in vertical speed/// from positive to negative)./// Only triggered if notifyJumpApex == true./// </summary>public event ReachedJumpApexEventHandler ReachedJumpApex;
And its corresponding virtual methods that raises the events are:
/// <summary>/// Called when this Character's PhysicsVolume has been changed./// </summary>protectedvirtualvoidOnPhysicsVolumeChanged(PhysicsVolume newPhysicsVolume)/// <summary>/// Called after MovementMode has changed./// Does special handling for starting certain modes, eg:/// enable / disable ground constraint, etc./// If overridden, base method MUST be called./// </summary>protected virtual void OnMovementModeChanged(MovementMode prevMovementMode,int prevCustomMode)/// <summary>/// Event for implementing custom character movement mode./// Called if MovementMode is set to Custom./// Derived Character classes should override CustomMovementMode method instead. /// </summary>protected virtual void OnCustomMovementMode(float deltaTime)/// <summary>/// Event for implementing custom character rotation mode./// Called if RotationMode is set to Custom./// Derived Character classes should override CustomRotationMode method instead. /// </summary>protected virtual void OnCustomRotationMode(float deltaTime)/// <summary>/// Called at the beginning of the Character Simulation, before current movement mode update./// This 'hook' lets you externally update the character 'state'./// </summary>protected virtual void OnBeforeSimulationUpdate(float deltaTime)/// <summary>/// Called after current movement mode update./// This 'hook' lets you externally update the character 'state'. /// </summary>protected virtual void OnAfterSimulationUpdate(float deltaTime)/// <summary>/// Event called when CharacterMovement component is updated (ie. Move call)./// At this point the character movement has been applied and its state is current. /// This 'hook' lets you externally update the character 'state'./// </summary>protected virtual void OnCharacterMovementUpdated(float deltaTime)/// <summary>/// Event triggered when characters collides with other during a CharacterMovement Move call./// Can be called multiple times./// </summary>protected virtual void OnCollided(refCollisionResult collisionResult)/// <summary>/// Event triggered when a character find ground (walkable or non-walkable)/// as a result of a downcast sweep (eg: FindGround method)./// </summary>protected virtual void OnFoundGround(refFindGroundResult foundGround)/// <summary>/// Event triggered when character enter Walking movement mode/// (ie: isOnWalkableGround AND isConstrainedToGround)./// </summary>protected virtual void OnLanded(Vector3 landingVelocity)/// <summary>/// Called when character crouches./// </summary>protected virtual void OnCrouched()/// <summary>/// Called when character un crouches./// </summary>protected virtual void OnUnCrouched()/// <summary>/// Called when a jump has been successfully triggered./// </summary>protected virtual void OnJumped()/// <summary>/// Called when Character reaches jump apex/// (eg: change in vertical speed from positive to negative)./// Only triggered if notifyJumpApex == true./// </summary>protected virtual void OnReachedJumpApex()
Character-derived classes are encouraged to extend the 'On' methods instead of subscribing to the event for enhanced customization and seamless integration.
In order to receive the ReachedJumpApex event, it is imperative to set the notifyJumpApex property to true beforehand; otherwise, this event will not be triggered.
For example:
protectedoverridevoidOnJumped(){ // Call base method implementation base.OnJumped(); // Add your code here...Debug.Log("Jumped!"); // Enable apex notification event notifyJumpApex =true;}protectedoverridevoidOnReachedJumpApex(){ // Call base method implementation base.OnReachedJumpApex(); // Add your code here...Debug.Log($"Apex reached {GetVelocity():F4}");}
Collisions
While you have the flexibility to utilize Unity's OnTriggerXXX and OnEnterXXX events, the Character class (just like the Unity built-in cc) encompasses dedicated methods and events specifically designed to manage character collisions detected during its most recent CharacterMovement.Move invocation.
/// <summary>/// Event triggered when character collides during a Move./// Can be called multiple times./// </summary>public event CollidedEventHandler Collided;/// <summary>/// Event triggered when character collides during a CharacterMovement Move call./// Can be called multiple times./// </summary>protectedvirtualvoidOnCollided(refCollisionResult collisionResult)
Character-derived classes are encouraged to extend the OnCollided method rather than subscribing to the Collided event for enhanced customization and seamless integration.
Custom Character Class Responding to Collision Events:
publicclassPlayerCharacter:Character{protectedoverridevoidOnCollided(refCollisionResult collisionResult) { // Call base method implementation base.OnCollided(ref collisionResult); // Handle collision here...Debug.Log($"Collided with {collisionResult.collider.name}"); }}
Moreover, you can conveniently access all collisions found during the last movement in an iterative manner, as demonstrated below:
for (int i =0, c =characterMovement.GetCollisionCount(); i < c; i++){CollisionResult collisionResult =characterMovement.GetCollisionResult(i); // Handle collision here...}
Listening to Character Collision Events
Non Character-based classes should subscribe to the CharacterCollided event, for example:
publicclassOtherClass:MonoBehaviour{publicCharacter character;protectedvoidOnCollided(refCollisionResult collisionResult) {Debug.Log($"Collided with {collisionResult.collider.name}"); }privatevoidOnEnable() { // Subscribe to Character eventscharacter.Collided+= OnCollided; }privatevoidOnDisable() { // Un-subscribe from Character eventscharacter.Collided-= OnCollided; }}
Collision Filtering
In many game scenarios, there is a need to ignore collisions with another character, a specific collider, or even all colliders attached to a rigid body. To address this, the CharacterMovement component incorporates a set of functions that allow you to conveniently define your desired interaction.
/// <summary>/// Makes the character's collider (eg: CapsuleCollider) to ignore all collisions/// vs otherCollider./// NOTE: The character can still collide with other during a Move call/// if otherCollider is in its collisionLayers mask./// </summary>publicvoidCapsuleIgnoreCollision(Collider otherCollider,bool ignore =true)/// <summary>/// Makes the character to ignore all collisions vs otherCollider./// </summary>public void IgnoreCollision(Collider otherCollider,bool ignore =true)/// <summary>/// Makes the character to ignore collisions vs all colliders attached/// to the otherRigidbody./// </summary>public void IgnoreCollision(Rigidbody otherRigidbody,bool ignore =true)
When finer control is necessary, the CharacterMovement ColliderFilterCallback comes into play. This allows you to selectively ignore specific colliders based on your game criteria.
/// <summary>/// Let you define if the character should collide with given collider./// Return true to filter (ignore) collider, false otherwise./// </summary>public ColliderFilterCallback colliderFilterCallback { get; set; }
For example, to ignore collisions against other characters using the CharacterMovement component:
privateboolColliderFilterCallback(Collider collider){ // If collider is a character (e.g. using CharacterMovement component) // ignore collisions with it (e.g. filter it)if (collider.TryGetComponent(outCharacterMovement _))returntrue;returnfalse;}
Physics Interactions
When enabled, a Character can interact with other rigidbodies—pushing or being pushed—and other characters, applying forces, explosion forces, downward forces, etc. These interactions are managed by the CharacterMovement component, and by default, the resulting behavior is influenced by the masses of the characters, where larger mass will push others more easily, and vice versa.
This default behavior can be modified using the CharacterMovement collisionResponseCallback function. This allows us to adjust the computed collision response impulses in any manner that our game may require.
The callback function prototype is declared as follows:
/// <summary>/// Let you modify the collision response vs dynamic objects,/// eg: compute resultant impulse and / or application point (CollisionResult.point)./// </summary>publicdelegatevoidCollisionResponseCallback(refCollisionResult inCollisionResult,refVector3 characterImpulse,refVector3 otherImpulse);
Where 'impulses' represent the computed resulting collision response impulses, and this can be modified as needed.
Additionally, the Character class includes a set of functions to externally influence the character. These functions are similar to Unity's Rigidbody functions, such as AddForce, AddExplosionForce, and a custom one called LaunchCharacter.
/// <summary>/// Adds a force to the Character./// This forces will be accumulated and applied during Move method call./// </summary>publicvoidAddForce(Vector3 force,ForceMode forceMode =ForceMode.Force)/// <summary>/// Applies a force to a rigidbody that simulates explosion effects./// The explosion is modeled as a sphere with a certain centre position and radius in world space;/// normally, anything outside the sphere is not affected by the explosion and the force decreases in proportion to distance from the centre./// However, if a value of zero is passed for the radius then the full force will be applied regardless of how far the centre is from the rigidbody./// </summary>public void AddExplosionForce(float forceMagnitude,Vector3 origin,float explosionRadius,ForceMode forceMode =ForceMode.Force)/// <summary>/// Set a pending launch velocity on the Character. This velocity will be processed/// the next Move call./// If overrideVerticalVelocity is true replace the vertical component of the Character's velocity instead of adding to it./// If overrideLateralVelocity is true replace the XY part of the Character's velocity instead of adding to it./// </summary>public void LaunchCharacter(Vector3 launchVelocity,bool overrideVerticalVelocity =false,bool overrideLateralVelocity =false)
Animating a Character
When animating a character, you should query the character's state and/or subscribe to its numerous events, feeding this information to your AnimationController parameters to ensure your animation stays perfectly in sync with the character's state, such as whether it is grounded, falling, jumping, etc.
ECM2 does not require the use of animation or any specific animation techniques. You have the freedom to animate your characters using 'plain Unity code' or your preferred method.
Querying Character State
The Character class provides an extensive array of methods, events, and delegates that you can leverage to access the character's information, including GetPosition, GetRotation, GetVelocity, IsWalking, IsFalling, IsOnWalkableGround, etc. This should be utilized to maintain seamless synchronization in your animations.
In addition to the information provided by the Character class, you can access further details through the CharacterMovement component. This includes ground-related information, retrieval and adjustment of capsule collider dimensions, access to collision detection functions, and even the ability to compute a new set of ground-related details (e.g., is walk-able, is a step, distance to the ground, etc.).
The following example illustrates how to animate the 'UnityCharacter' using the information provided by the Character to feed parameters into its Animator.
privatevoidUpdate(){float deltaTime =Time.deltaTime; // Get Character animatorAnimator animator =_character.GetAnimator(); // Compute input move vector in local spaceVector3 move =transform.InverseTransformDirection(_character.GetMovementDirection()); // Update the animator parametersfloat forwardAmount =_character.useRootMotion&&_character.GetRootMotionController()?move.z:Mathf.InverseLerp(0.0f,_character.GetMaxSpeed(),_character.GetSpeed());animator.SetFloat(Forward, forwardAmount,0.1f, deltaTime);animator.SetFloat(Turn,Mathf.Atan2(move.x,move.z),0.1f, deltaTime);animator.SetBool(Ground,_character.IsGrounded());animator.SetBool(Crouch,_character.IsCrouched());if (_character.IsFalling())animator.SetFloat(Jump,_character.GetVelocity().y,0.1f, deltaTime); // Calculate which leg is behind, so as to leave that leg trailing in the jump animation // (This code is reliant on the specific run cycle offset in our animations, // and assumes one leg passes the other at the normalized clip times of 0.0 and 0.5)float runCycle =Mathf.Repeat(animator.GetCurrentAnimatorStateInfo(0).normalizedTime+0.2f,1.0f);float jumpLeg = (runCycle <0.5f?1.0f:-1.0f) * forwardAmount;if (_character.IsGrounded())animator.SetFloat(JumpLeg, jumpLeg);}
As you can see, it updates the Animator parameters based on the information provided by the Character component.
Utilizing Root Motion
Root motion refers to motion integrated directly into the animation, where the animation itself dictates how far an object moves rather than relying on code.
To incorporate root motion in a Character-derived class, follow these steps:
Add the RootMotionController component to your model's GameObject. This RootMotionController is responsible for providing the animation's velocity, rotation, etc., to the Character.
Enable the useRootMotion property in the Character. This property can be toggled as needed.
Once a character is moved using root motion, the animation assumes complete control over the character's movement. This replaces all procedural movement, rendering properties such as maxWalkSpeed, maxFallSpeed, etc., irrelevant, as the character is entirely driven by the animation.
Worth noting, the character's ground constraint still applies when root motion is enabled. This implies that any vertical movement in your root motion-based animation won't function unless you explicitly disable this constraint. In such cases, it's advisable to assign the flying movement mode, as it automatically disables the ground constraint and allows for full vertical movement.
To enable vertical root motion movement, the character's movement mode must be set to Flying.