Flinch

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:

  1. Call target.GetComponent<Damageable>() and if the returned component is null then that object cannot be damaged.
  2. Build a Damageable.DamageMessage containing details such as the amount of damage and the direction it is coming from.
  3. 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: