敵のダメージリアクション・バリエーション

バトルがあるゲームの爽快感の重要な要素といえば、やっぱり敵のリアクションです（断言）
3Dゲームにおける敵のダメージリアクションといえば、ダメージモーションを再生するのが一般的・・・と思いきや、**ダメージモーションって地味に取り回しが悪いですよね**。

確かにリアクションとしては一番丁寧な演出なのですが、モーションを流す以上、攻撃や移動などのモーションを中断しないといけないので演出の充実として実装するにしては**ゲーム仕様側への影響が大きく、気軽に入れるわけにはいけません**。
そんなわけで一般的にはダメージモーションは大技を当てた時や、部位破壊的な特殊な状態になったときなど、「仕様に伴う特別な演出」で再生されることが多い印象です。

とはいえ一番頻度の高い「小さなダメージを与えた時」の演出が無いと爽快感がないどころか、攻撃が効いてるのかどうか分からない、なんてことになりかねません。

https://twitter.com/flankids/status/1402483371712778247

↑効いてる？弾かれてる？

市場のゲームでは「細かいダメージを与えた時の汎用演出」が用意されてて、いろいろあるなぁと思ったので自分が知ってるものを引き出しとして実装してみました。

### ■備考
本記事では演出の組み立てにDOTweenを使ってますが、同じ演出をAnimationでもベタ書きでも作れます

# 点滅によるダメージ表現

https://twitter.com/flankids/status/1402484163266039808

まずは点滅。もはや古典と言える演出ですが、攻撃が当たった結果何かが起きたことの分かりやすさはバッチリ。
`Renderer`のON/OFFで完結するので負荷も低い（というかなんなら一時的に軽くすらなるのでは）ので、その辺は古くからの演出ならではですね。

これを汎用演出とすると他の演習も合わせてレトロなものにしないと異物感が大きいので、あえてそういう路線を攻めたりしない限りは他の演出を使ったほうが良さそう。

```Enemy.cs
using UnityEngine;
using DG.Tweening;

public class Enemy : MonoBehaviour
{
    /// <summary> モデルのRenderer </summary>
    [SerializeField]
    private Renderer _renderer;

    private Sequence _seq;

    private void OnTriggerEnter(Collider other)
    {
        HitBlink();
    }

    /// <summary> 点滅によるダメージ演出再生 </summary>
    private void HitBlink()
    {
        _seq?.Kill();
        _seq = DOTween.Sequence();
        _seq.AppendCallback(() => _renderer.enabled = false);
        _seq.AppendInterval(0.07f);
        _seq.AppendCallback(() => _renderer.enabled = true);
        _seq.AppendInterval(0.07f);
        _seq.SetLoops(2);
        _seq.Play();
    }
}
```

# 赤のカラー乗算によるダメージ表現

https://twitter.com/flankids/status/1402484730751160323

3Dゲームの黎明期に主流だったような印象のある演出。
古臭さも薄く、実装も簡単でわかりやすいので今でも一定数見られるものだと思います。

難点としては、色の乗算なのでモデル自体が黒に近かったり、暗めの雰囲気を作っているゲームでは変化が目立ちづらいこと。
場合によっては汚れてるような印象にもなりうるので、時間が許すならもう少し踏み込んだ演出を作りたいところ。

```Enemy.cs
using UnityEngine;
using DG.Tweening;

public class Enemy : MonoBehaviour
{
    /// <summary> マテリアルの色パラメータのID </summary>
    private static readonly int PROPERTY_COLOR = Shader.PropertyToID("_Color");

    /// <summary> モデルのRenderer </summary>
    [SerializeField]
    private Renderer _renderer;

    /// <summary> モデルのマテリアルの複製 </summary>
    private Material _material;

    private Sequence _seq;

    private void Awake()
    {
        // materialにアクセスして自動生成されるマテリアルを保持
        _material = _renderer.material;
    }

    private void OnTriggerEnter(Collider other)
    {
        HitFadeBlink(Color.red);
    }

    /// <summary> カラー乗算によるダメージ演出再生 </summary>
    private void HitFadeBlink(Color color)
    {
        _seq?.Kill();
        _seq = DOTween.Sequence();
        _seq.Append(DOTween.To(() => Color.white, c => _material.SetColor(PROPERTY_COLOR, c), color, 0.1f));
        _seq.Append(DOTween.To(() => color, c => _material.SetColor(PROPERTY_COLOR, c), Color.white, 0.1f));
        _seq.Play();
    }
}
```

# 白のカラー`加算`によるダメージ表現

https://twitter.com/flankids/status/1402485274358083586

最近よく見る演出。
加算で色を与えるのでモデルや画面の色味に左右されづらく、綺麗でわかりやすいです。
特にこだわりがなければこれが一番安定だと思います。

サンプルコードでは、**[Create] > [Shader] > [Standard Surface Shader]** で加算カラーを与えられるシェーダーを作っています。
プロパティ名は **`_AdditiveColor`** です。

```StandardSurface-Additive.shader
Shader "Custom/StandardSurface-Additive"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _AdditiveColor ("Additive Color", Color) = (0,0,0) // 加算カラーのプロパティ追加
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        #pragma surface surf Standard fullforwardshadows

        #pragma target 3.0

        sampler2D _MainTex;

        struct Input
        {
            float2 uv_MainTex;
        };

        half _Glossiness;
        half _Metallic;
        fixed4 _Color;
        fixed3 _AdditiveColor; // 加算カラーのメンバ定義

        UNITY_INSTANCING_BUFFER_START(Props)
        UNITY_INSTANCING_BUFFER_END(Props)

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb + _AdditiveColor.rgb; // 加算カラーを与える
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}
```

上記の`StandardSurface-Additive.shader`を敵モデルのマテリアルに与え、下記コードで明滅させます。

```Enemy.cs
using UnityEngine;
using DG.Tweening;

public class Enemy : MonoBehaviour
{
    /// <summary> マテリアルの加算色パラメータのID </summary>
    private static readonly int PROPERTY_ADDITIVE_COLOR = Shader.PropertyToID("_AdditiveColor");

    /// <summary> モデルのRenderer </summary>
    [SerializeField]
    private Renderer _renderer;

    /// <summary> モデルのマテリアルの複製 </summary>
    private Material _material;

    private Sequence _seq;

    private void Awake()
    {
        // materialにアクセスして自動生成されるマテリアルを保持
        _material = _renderer.material;
    }

    private void OnTriggerEnter(Collider other)
    {
        HitFadeBlink(Color.white);
    }

    /// <summary> カラー加算によるダメージ演出再生 </summary>
    private void HitFadeBlink(Color color)
    {
        _seq?.Kill();
        _seq = DOTween.Sequence();
        _seq.Append(DOTween.To(() => Color.black, c => _material.SetColor(PROPERTY_ADDITIVE_COLOR, c), color, 0.1f));
        _seq.Append(DOTween.To(() => color, c => _material.SetColor(PROPERTY_ADDITIVE_COLOR, c), Color.black, 0.1f));
        _seq.Play();
    }
}
```

# スケールの拡大・縮小によるダメージ表現

https://twitter.com/flankids/status/1402485707092889601

色ではなく**動き**で表現するダメージ演出。
カラー乗算・加算と組み合わせで使われることが多いかも。

大きめの敵で使うと画面上の変化量が大きすぎて、大げさに見えたり違和感があったりするので相性は悪いですが、１〜３頭身くらいのキャラ向けには向いてる演出だと思います。

```Enemy.cs
using UnityEngine;
using DG.Tweening;

public class Enemy : MonoBehaviour
{
    /// <summary> 拡大・縮小するTransform </summary>
    [SerializeField]
    private Transform _model;

    private Sequence _seq;

    private void OnTriggerEnter(Collider other)
    {
        HitScaleBound();
    }

    /// <summary> スケールの拡大・縮小によるダメージ演出再生 </summary>
    private void HitScaleBound()
    {
        _seq?.Kill();
        _seq = DOTween.Sequence();
        _seq.Append(DOTween.To(() => Vector3.one, scale => _model.localScale = scale, Vector3.one * 1.05f, 0.07f));
        _seq.Append(_model.DOScale(1f, 0.07f));
        _seq.Play();
    }
}
```

# のけぞり（ボーン回転）によるダメージ表現

https://twitter.com/flankids/status/1402486433919635460

本記事で紹介したかった本命のダメージ演出！
これ実は `FF7 REMAKE` で使われてた表現で・・・早い話がパクったネタです。
（バレットで敵に連射してるときにいい感じにガクガクしてて気持ちよかった）

攻撃によって特定のボーン（人型なら腰あたりが良い）を傾け、のけぞるような演出を与えています。
使用するボーンの選定や傾けかたをモデルごとに決めたり、量産時に規則を作ったりとやや配慮が必要ですが、**変にモデルの色や大きさを変えない上、攻撃方向に応じたのけぞり方**をするので、3Dゲームらしさのある自然な見た目になります。

```Enemy.cs
using UnityEngine;
using DG.Tweening;

public class Enemy : MonoBehaviour
{
    /// <summary> のけぞりによるダメージ演出で回転するボーン </summary>
    [SerializeField]
    private Transform _waistBone;

    /// <summary> のけぞりによるダメージ演出によるボーンの回転角度 </summary>
    private Vector3 _offsetAnglesWaist;

    private Sequence _seq;

    private void OnTriggerEnter(Collider other)
    {
        // ヒットした攻撃判定の進行方向に向けて体を倒すため、角度を取得
        var bulletAngles = other.transform.eulerAngles;
        // X角度は無視
        bulletAngles.x = 0f;
        HitTiltWaist(Quaternion.Euler(bulletAngles) * Vector3.forward);
    }

    /// <summary> のけぞり（ボーン回転）によるダメージ演出を再生 </summary>
    /// <param name="vector">ヒットした攻撃判定の進行方向</param>
    private void HitTiltWaist(Vector3 vector)
    {
        _seq?.Kill();
        _seq = DOTween.Sequence();
        // 攻撃判定の進行方向を自身のTransformのローカル座標系に変換
        vector = transform.InverseTransformVector(vector);
        // 攻撃判定の進行方向に向けて10度傾ける
        // FIXME: Vector3のどの要素に攻撃判定の進行方向ベクトルの各要素を使うか、正/負解釈をどうするかは、モデルのボーン構造に合わせて適宜変更する
        var tiltAngles = new Vector3(0f, -vector.x, -vector.z).normalized * 10f;
        _seq.Append(DOTween.To(() => Vector3.zero, angles => _offsetAnglesWaist = angles, tiltAngles, 0.1f));
        _seq.Append(DOTween.To(() => tiltAngles, angles => _offsetAnglesWaist = angles, Vector3.zero, 0.2f));
        _seq.Play();
    }

    private void LateUpdate()
    {
        // Animatorによる今フレームのボーンの角度が決まった後、のけぞりによる回転角度を与える
        _waistBone.localEulerAngles += _offsetAnglesWaist;
    }
}
```

# まとめ

汎用のダメージ演出はゲーム中に一番多く見ることになる演出だと思うので、いかに自然かつ小気味良いものにするか地味に重要な要素だな〜と思います。
「他にこういうのあるよ！」ってのがあったら教えてください！