This page is part of the 3D Game Kit sample.
Touching an enemy causes the player to flinch away from them and become invulnerable for several seconds (regardless of whether the enemy is actually attacking or not):
Mecanim
Dealing damage to an object involves a fairly straightforward process:
- Call
target.GetComponent<Damageable>()
and if the returned component is null then that object cannot be damaged. - Build a
Damageable.DamageMessage
containing details such as the amount of damage and the direction it is coming from. - Pass that data into
Damageable.ApplyDamage
.
The player's Damageable
component has several events setup in the Inspector (pictured to the right):
- Update the UI health display when taking damage, dying, or being reset to full health.
- Show the blue glowing shield effect after taking damage to indicate that the player is invulnerable.
- Hide the shield after the
Invulnerability Time
passes or if the player's health is reset to full.
The main PlayerController
implements the IMessageReceiver
interface:
// MessageSystem.cs:
public enum MessageType
{
DAMAGED,
DEAD,
RESPAWN,
}
public interface IMessageReceiver
{
void OnReceiveMessage(
MessageType type,
object sender,
object msg);
}
Player Controller
This allows it to register itself in the Damageable
to receive damage messages as well, allowing it to play the Hurt
Blend Tree and set the HurtFromX
and HurtFromY
parameters to flinch away from whatever direction the damage came from:
protected Damageable m_Damageable;
void OnEnable()
{
m_Damageable = GetComponent<Damageable>();
m_Damageable.onDamageMessageReceivers.Add(this);
m_Damageable.isInvulnerable = true;
...
}
void OnDisable()
{
m_Damageable.onDamageMessageReceivers.Remove(this);
...
}
// The Damageable will call this when taking damage.
public void OnReceiveMessage(MessageType type, object sender, object data)
{
switch (type)
{
case MessageType.DAMAGED:
{
Damageable.DamageMessage damageData = (Damageable.DamageMessage)data;
Damaged(damageData);
}
break;
case MessageType.DEAD:
{
Damageable.DamageMessage damageData = (Damageable.DamageMessage)data;
Die(damageData);
}
break;
}
}
readonly int m_HashHurt = Animator.StringToHash("Hurt");
void Damaged(Damageable.DamageMessage damageMessage)
{
// Set the Hurt parameter of the animator.
m_Animator.SetTrigger(m_HashHurt);
// Find the direction of the damage.
Vector3 forward = damageMessage.damageSource - transform.position;
forward.y = 0f;
Vector3 localHurt = transform.InverseTransformDirection(forward);
// Set the HurtFromX and HurtFromY parameters of the animator based on the direction of the damage.
m_Animator.SetFloat(m_HashHurtFromX, localHurt.x);
m_Animator.SetFloat(m_HashHurtFromY, localHurt.z);
// Shake the camera.
CameraShake.Shake(CameraShake.k_PlayerHitShakeAmount, CameraShake.k_PlayerHitShakeTime);
// Play an audio clip of being hurt.
if (hurtAudioPlayer != null)
{
hurtAudioPlayer.PlayRandomClip();
}
}
Blend Tree
The Hurt
state is simply a 2D Freeform Cartesian Blend Tree based on the HurtFromX
and HurtFromY
parameters:
Animancer
The Animancer character has the same Damageable
events but also has On Damage Received
call FlinchState.OnDamageReceived
in the following script, which uses StateMachine.ForceSetState
to interrupt any other state and force it to enter this one:
using Animancer;
using Animancer.Units;
using UnityEngine;
public class FlinchState : CharacterState
{
[SerializeField] private MixerTransition2D _Animation;
[SerializeField] private LayerMask _EnemyLayers;
[SerializeField, Meters] private float _EnemyCheckRadius = 1;
protected virtual void Awake()
{
_Animation.Events.OnEnd = Character.ForceEnterIdleState;
}
public void OnDamageReceived()
{
Character.StateMachine.ForceSetState(this);
}
protected virtual void OnEnable()
{
Character.Parameters.ForwardSpeed = 0;
Character.Animancer.Play(_Animation);
Vector3 direction = DetermineHitDirection();
_Animation.State.Parameter = new Vector2(
Vector3.Dot(Character.Animancer.transform.right, direction),
Vector3.Dot(Character.Animancer.transform.forward, direction));
}
private Vector3 DetermineHitDirection()
{
Vector3 position = Character.transform.position;
float closestEnemySquaredDistance = float.PositiveInfinity;
Vector3 closestEnemyDirection = Vector3.zero;
Collider[] enemies = Physics.OverlapSphere(position, _EnemyCheckRadius, _EnemyLayers);
for (int i = 0; i < enemies.Length; i++)
{
Vector3 direction = enemies[i].transform.position - position;
float squaredDistance = direction.magnitude;
if (closestEnemySquaredDistance > squaredDistance)
{
closestEnemySquaredDistance = squaredDistance;
closestEnemyDirection = direction;
}
}
return closestEnemyDirection.normalized;
}
public override bool FullMovementControl => false;
public override bool CanExitState => false;
}
The _Animation
field is a Mixer which serves the same purpose as the Mecanim Blend Tree:
Unfortunately, the Script Referencing issue means that we cannot implement the IMessageReceiver
interface to receive the proper hit details to determine which direction it came from, so instead we just use Physics.OverlapSphere
to get all the enemies near the player and select the closest one:
[SerializeField] private LayerMask _EnemyLayers;
[SerializeField, Meters] private float _EnemyCheckRadius = 1;
private Vector3 DetermineHitDirection()
{
Vector3 position = Character.transform.position;
float closestEnemySquaredDistance = float.PositiveInfinity;
Vector3 closestEnemyDirection = Vector3.zero;
Collider[] enemies = Physics.OverlapSphere(position, _EnemyCheckRadius, _EnemyLayers);
for (int i = 0; i < enemies.Length; i++)
{
Vector3 direction = enemies[i].transform.position - position;
float squaredDistance = direction.magnitude;
if (closestEnemySquaredDistance > squaredDistance)
{
closestEnemySquaredDistance = squaredDistance;
closestEnemyDirection = direction;
}
}
return closestEnemyDirection.normalized;
}
So when we enter this state we can get that direction and use some Dot Products to determine how much of it lies along the right
and forward
axes of the character's model:
protected virtual void OnEnable()
{
Character.Parameters.ForwardSpeed = 0;
Character.Animancer.Play(_Animation);
Vector3 direction = DetermineHitDirection();
_Animation.State.Parameter = new Vector2(
Vector3.Dot(Character.Animancer.transform.right, direction),
Vector3.Dot(Character.Animancer.transform.forward, direction));
}
We could have used InverseTransformDirection
to convert the direction into local space like the Mecanim character did. Both approaches would achieve the same thing and have similar performance costs, so it is simply a matter of choosing which method you find easiest to understand.
AI
Unfortunately, the enemy AI is too tightly coupled to the PlayerController
script for us to be able to get enemies to attack the player so they will simply stand still at all times. In order to actually demonstrate the ability to take damage from any direction, the sample scene has a red capsule near the player's starting location that moves back and forth and will damage the player on contact just like an enemy: