Location: Samples/07 Sprites/03 Directional Character 3D
Recommended After: Directional Character
Learning Outcomes: in this sample you will learn:
How to use
Sprite
based animations in 3D space.
Summary
This sample reworks the 2D characters from the Directional Character sample to work in a 3D environment.
Overview
The code has the same general structure as the Directional Character sample with two differences:
- It uses 3D physics components instead of 2D ones.
DirectionalCharacter3D
references aDirectionalAnimations3D
component instead of directly referencing theAnimancerComponent
.
Other than that, DirectionalCharacter3D
looks very similar to DirectionalCharacter
.
using Animancer;
using UnityEngine;
public class DirectionalCharacter3D : MonoBehaviour
{
[Header("Physics")]
[SerializeField] private CapsuleCollider _Collider;
[SerializeField] private Rigidbody _Rigidbody;
[SerializeField, MetersPerSecond] private float _WalkSpeed = 1;
[SerializeField, MetersPerSecond] private float _RunSpeed = 2;
[Header("Animations")]
[SerializeField] private DirectionalAnimations3D _Animancer;
[SerializeField] private DirectionalAnimationSet _Idle;
[SerializeField] private DirectionalAnimationSet _Walk;
[SerializeField] private DirectionalAnimationSet _Run;
[SerializeField] private DirectionalAnimationSet _Push;
private Vector3 _Movement;
private bool _IsPushing;
public enum AnimationGroup
{
Other,
Movement,
}
protected virtual void Update()
{
Vector2 input = SampleInput.WASD;
if (input != Vector2.zero)
{
input = _Animancer.Animations.Snap(input);
_Movement = new Vector3(input.x, 0, input.y);
Transform camera = _Animancer.Camera;
_Movement = camera.TransformDirection(_Movement);
_Movement.y = 0;
_Movement.Normalize();
_Animancer.Forward = _Movement;
Play(GetMovementAnimations(), AnimationGroup.Movement);
}
else
{
_Movement = Vector3.zero;
Play(_Idle, AnimationGroup.Other);
}
}
private void Play(DirectionalAnimationSet animations, AnimationGroup group)
=> _Animancer.SetAnimations(animations, (int)group);
private DirectionalAnimationSet GetMovementAnimations()
{
if (_IsPushing)
return _Push;
else if (SampleInput.LeftShiftHold)
return _Run;
else
return _Walk;
}
protected virtual void OnCollisionEnter(Collision collision)
=> OnCollision(collision);
protected virtual void OnCollisionStay(Collision collision)
=> OnCollision(collision);
private void OnCollision(Collision collision)
{
if (_IsPushing)
return;
int contactCount = collision.contactCount;
for (int i = 0; i < contactCount; i++)
{
ContactPoint contact = collision.GetContact(i);
if (Vector3.Angle(contact.normal, _Movement) > 180 - 30)
{
_IsPushing = true;
return;
}
}
}
protected virtual void FixedUpdate()
{
_IsPushing = false;
float speed = _Animancer.Animations == _Run ? _RunSpeed : _WalkSpeed;
_Rigidbody.velocity = _Movement * speed;
}
#if UNITY_EDITOR
protected virtual void OnValidate()
{
if (_Animancer != null)
_Animancer.Animations = _Idle;
}
#endif}
Fields
The Serialized Fields are very similar to the fields in the Directional Character sample:
[Header("Physics")]
[SerializeField] private CapsuleCollider _Collider;// Changed from CapsuleCollider2D.
[SerializeField] private Rigidbody _Rigidbody;// Changed from Rigidbody2D.
[SerializeField, MetersPerSecond] private float _WalkSpeed = 1;
[SerializeField, MetersPerSecond] private float _RunSpeed = 2;
[Header("Animations")]
[SerializeField] private DirectionalAnimations3D _Animancer;// Changed from AnimancerComponent.
[SerializeField] private DirectionalAnimationSet _Idle;
[SerializeField] private DirectionalAnimationSet _Walk;
[SerializeField] private DirectionalAnimationSet _Run;
[SerializeField] private DirectionalAnimationSet _Push;
private Vector3 _Movement;// Changed from Vector2.
private bool _IsPushing;// New.
public enum AnimationGroup
{
Other,
Movement,
}
The _Facing
, _CurrentAnimationSet
, and TimeSynchronizer
are no longer needed because they're all managed by the DirectionalAnimations3D
.
- To set the facing direction we now use
_Animancer.Forward = _Movement;
. - And to play something we now use:
private void Play(DirectionalAnimationSet animations, AnimationGroup group)
=> _Animancer.SetAnimations(animations, (int)group);
That automatically takes care of the Synchronization and getting the appropriate animation for the current direction.
Movement
The Update
method is a bit more complicated than Movement in the Directional Character sample because it has to transform the player's 2D screen space inputs into a 3D world space direction:
protected virtual void Update()
{
Vector2 input = SampleInput.WASD;
if (input != Vector2.zero)
{
input = _Animancer.Animations.Snap(input);
Convert the input to 3D in the XZ plane:
_Movement = new Vector3(input.x, 0, input.y);
Apply the camera's rotation, flatten it on the Y axis, and set it as the forward direction:
Transform camera = _Animancer.Camera;
_Movement = camera.TransformDirection(_Movement);
_Movement.y = 0;
_Movement.Normalize();
_Animancer.Forward = _Movement;
And play the appropriate animations:
Play(GetMovementAnimations(), AnimationGroup.Movement);
}
else
{
_Movement = Vector3.zero;
Play(_Idle, AnimationGroup.Other);
}
}
Pushing
Unfortunately, there is no 3D equivalent of the _Collider.GetContacts
method used in the Directional Character sample to detect if the character is pushing against something so we need to use the OnCollisionEnter
and OnCollisionStay
MonoBehaviour Messages:
private bool _IsPushing;
protected virtual void OnCollisionEnter(Collision collision)
=> OnCollision(collision);
protected virtual void OnCollisionStay(Collision collision)
=> OnCollision(collision);
private void OnCollision(Collision collision)
{
if (_IsPushing)
return;
Once we have a collision
, we can process its contacts using the same angle calculation as the Directional Character sample (using Vector3.Angle
instead of Vector2.Angle
):
int contactCount = collision.contactCount;
for (int i = 0; i < contactCount; i++)
{
ContactPoint contact = collision.GetContact(i);
if (Vector3.Angle(contact.normal, _Movement) > 180 - 30)
{
Physics collisions aren't detected every Update
though, so instead of immediately playing animations here we need to remember if the character is pushing something:
_IsPushing = true;
return;
}
}
}
Unity calls FixedUpdate
before the OnCollision
events so in each physics update we can simply reset _IsPushing = false
in FixedUpdate
so that we don't have to worry about collision exit events or try to handle the possibility of a contact normal changing if an object rotates:
protected virtual void FixedUpdate()
{
_IsPushing = false;
The rest of FixedUpdate
is the same as the Directional Character sample
float speed = _Animancer.Animations == _Run ? _RunSpeed : _WalkSpeed;
_Rigidbody.velocity = _Movement * speed;
}
And now back in Update
we can use that field to determine if we need to use the _Push
animations:
protected virtual void Update()
{
...
Play(GetMovementAnimations(), AnimationGroup.Movement);
...
}
private DirectionalAnimationSet GetMovementAnimations()
{
if (_IsPushing)
return _Push;
else if (SampleInput.LeftShiftHold)
return _Run;
else
return _Walk;
}
Edit Mode
DirectionalAnimations3D
already takes care of showing the appropriate sprite for its assigned animations in Edit Move just like in the Directional Character sample, so all we need to do this time is make sure it has the right _Idle
animations:
#if UNITY_EDITOR
protected virtual void OnValidate()
{
if (_Animancer != null)
_Animancer.Animations = _Idle;
}
#endif