28
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ファイナル判定をやっつける(すり抜け、多段ヒット対策)

Last updated at Posted at 2021-12-20

はじめに

この記事は『Unity Advent Calendar 2021』の21日目の記事です。

完成品

Animation3.gif

すり抜け対策(当たり判定の補完)
Animation6.gif

ファイナル判定とは?

ここでは、下記のようなガバガバあたり判定を指します。

  • 攻撃がすり抜ける
  • 逆に1度の攻撃が何度もヒットする

特にすり抜けはUnityで素直に実装すると陥りがちな罠ですね。
Animation2.gif

今回は上記のような現象の対処方法をご紹介します。

ライセンス表示

image.png
ユニティちゃんライセンス条項の元、アセットを利用しました。

まずは素直に実装してみる

とりあえずファイナル判定を実装してみます。

武器を持たせる

武器を持たせるためには、アニメーションに合わせた位置に武器を持たせることが大切です。
アニメーションウィンドウから武器装備中のアニメーションを選択し、Previewボタンをクリックしてください。

下記のようにPreview状態になれば準備OKです。
image.png

右手を探して子オブジェクトとして武器を配置します。
image.png

これで大体一致しますが、おそらく少しずれているので微調整しておいてください。

攻撃スクリプトの実装

武器にコライダをアタッチし、トリガーとして設定します。
※スクショに写っていませんが、rigidbodyもアタッチしましょう
image.png

rigidbody設定
赤枠箇所の設定が大切です。
image.png

下記関数を含むスクリプトを実装します。

  • 攻撃処理開始処理(AnimationEventで呼び出す)
  • 攻撃命中時の処理
実装例
    /// <summary>
    /// 命中時エフェクト
    /// </summary>
    public GameObject hitEffect;

    /// <summary>
    /// コライダ
    /// </summary>
    private Collider col;

    void Start()
    {
        col = GetComponent<BoxCollider>();
    }

    /// <summary>
    /// 攻撃開始処理
    /// </summary>
    public void Attack(){
        // 当たり判定持続時間を雑に実装するためコルーチン化
        StartCoroutine("attack");
    }

    /// <summary>
    /// 攻撃開始実処理
    /// </summary>
    IEnumerator attack()
    {
        col.enabled = true;
        // 当たり判定持続時間を雑に実装
        yield return new WaitForSeconds(0.2f);
        col.enabled = false;
    }

    /// <summary>
    /// 命中時処理
    /// </summary>
    /// <param name="other"></param>
    void OnTriggerEnter(Collider other){ 
        Debug.Log("Hit!");
        // ヒット箇所を計算してエフェクトを表示する
        Vector3 hitPos = other.ClosestPointOnBounds(col.bounds.center);
        GameObject effectIstance = Instantiate(hitEffect, hitPos, Quaternion.identity);
        Destroy(effectIstance, 1);
    }

これで攻撃側はいったんOK。
続いてテスト用の敵を作成しましょう。

敵に判定を実装

Animationに応じて判定も動くように、ボーンごとに設定していきましょう。
また、Meshコライダーは動作が重いため、プリミティブコライダで設定していくことが推奨されます。

今回は「頭」「首」「体」「足」「脚」て感じで簡単に設定してみました。
image.png

実際に動かしてみる

ちゃんと動いているように見えます!
Animation.gif

しかし問題が…

1回の攻撃で複数回のHitを確認

image.png

すり抜けることも…

Animation2.gif

多段ヒット対策

原因

アニメーションに合わせるために部位を小分けにした結果、複数の部位に命中判定が発生しているためです。
image.png

このように「首」や「前脚」に判定が重なっていることが読み取れますね。

解決方法

共通する親を持つオブジェクトに対しては、命中時に処理しないようにしましょう。

攻撃受付スクリプト(敵用)の実装

親オブジェクト用
public class HitMaster : MonoBehaviour
{
    public void TakeDamage(){
        // ダメージを受ける処理とか
    }
}
子オブジェクト用
public class HitZone : MonoBehaviour
{
    /// <summary>
    /// 親スクリプト公開
    /// </summary>
    public HitMaster Master => master;
    HitMaster master;

    void Start()
    {
        master = GetComponentInParent<HitMaster>();
    }
}

敵のルートにHitMasterをアタッチします。
image.png

コライダーを設定した部位それぞれにHitZoneをアタッチします。
image.png

まとめて設定するにはタイプ検索を利用すると便利です。

image.png

攻撃側スクリプトの修正

武器スクリプトの修正:プロパティの追加
    /// <summary>
    /// 当たり判定処理済みを記録
    /// </summary>
    private Dictionary<int, bool> hitMasters { get; } = new Dictionary<int, bool>();
武器スクリプトの修正:攻撃開始実処理の修正
     IEnumerator attack()
    {
        hitMasters.Clear(); // 追加
        col.enabled = true;
        yield return new WaitForSeconds(0.2f);
        col.enabled = false;
    }
武器スクリプトの修正:攻撃命中時の処理修正
    void OnTriggerEnter(Collider other){ 
        // 追加
        // 攻撃対象部位ならHitZoneが取得できる
        var hitZone = other.GetComponent<HitZone>();
        if(hitZone == null) return;

        // 攻撃対象部位の親のインスタンスIDで重複した攻撃を判定
        int masterId = hitZone.Master.GetInstanceID();
        if(hitMasters.ContainsKey(masterId)) return;
        hitMasters[masterId] = true;

        Debug.Log("Hit!");
        // ダメージ計算とかこのへんで実装できますねぇ
        hitZone.Master.TakeDamage();
        // ヒット箇所を計算してエフェクトを表示する(前回から特に変更なし)
        Vector3 hitPos = other.ClosestPointOnBounds(col.bounds.center);
        GameObject effectIstance = Instantiate(hitEffect, hitPos, Quaternion.identity);
        Destroy(effectIstance, 1);
    }

実行結果

1回の攻撃につき、最初に触れた1部位に1度だけ命中するようになりました!
Animation3.gif

すり抜け対策

原因

実行速度を1/10に落として、当たり判定を可視化してみました。
image.png

Animation4.gif

物理演算は30fpsと控えめの設定とはいえ…
なるほど。これはすり抜ける訳だ。

※アニメーションが滑らかなのは、スロー再生により実質600fpsで更新されているため。
 同設定で「Animator」の「UpdateMode」を「Animate Physics」にするとカクカクになります。

解決方法

物理演算の処理間隔を短くするという方法もありますが、60fpsでもすり抜けはゼロになりません。
また、設定に比例して処理が重くなってしまいます。

そこで、中間部分のあたり判定を動的に生成して補完してあげることにしました。

準備

まずは武器に直接アタッチしていた攻撃用スクリプト、コライダーを無効化します。
image.png

兄弟オブジェクト「WeaponCollider」を生成して、
攻撃用スクリプト、コライダー、rigidbodyをコピペ、有効化してあげましょう。
image.png

次に「WeaponCollider」の子オブジェクトとして切っ先の位置を表す「TipPosition」を生成します。
image.png

これで準備OKです。
スクリプトを実装しましょう。

動的コライダ生成の実装

当たり判定補完スクリプト
using UnityEngine;
[RequireComponent(typeof(MeshFilter), typeof(MeshCollider), typeof(Rigidbody))]
public class ComplementCollider : MonoBehaviour {
    /// <summary>
    /// 切っ先部分の位置追跡用
    /// </summary>
    public Transform TipPosition;

    /// <summary>
    /// メッシュ生成用
    /// </summary>
    Mesh mesh;

    /// <summary>
    /// メッシュコライダ
    /// </summary>
    MeshCollider meshCollider;

    /// <summary>
    /// 前フレーム切っ先位置
    /// </summary>
    Vector3 lastTipPosition;

    /// <summary>
    /// 前フレーム
    /// </summary>
    Vector3 lastPosition;

    void Start () {
        mesh = GetComponent<MeshFilter>().mesh;
        meshCollider = GetComponent<MeshCollider>();
        // トリガーとして設定する
        meshCollider.convex = true;
        meshCollider.isTrigger = true;
        // メッシュを指定
        meshCollider.sharedMesh = mesh;
        // 前回位置の初期化
        lastTipPosition = TipPosition.position;
        lastPosition = transform.position;
    }

    void FixedUpdate () {
        // 前回位置と同じ時は処理しない(エラーになる)
        if(lastPosition == transform.position)
            return;
        if(lastTipPosition == TipPosition.position)
            return;

        // 毎フレームメッシュを作り直す
        mesh.Clear ();

        // 前回位置と今回位置の中間位置を補完する(こうしないと中間部分の判定が短くなる)
        var colliderLength = (TipPosition.position - transform.position).magnitude;
        var startMidPoint = Vector3.Lerp(transform.position, lastPosition, 0.5f);
        var endMidPoint = Vector3.Lerp(TipPosition.position, lastTipPosition, 0.5f);
        var assumedMidEndPoint = startMidPoint + (endMidPoint - startMidPoint).normalized * colliderLength;

        // 頂点(ローカル座標に変換して設定)
        mesh.vertices = new Vector3[] {
            transform.InverseTransformPoint(lastPosition), transform.InverseTransformPoint(lastTipPosition),
            transform.InverseTransformPoint(startMidPoint), transform.InverseTransformPoint(assumedMidEndPoint),
            transform.InverseTransformPoint(transform.position), transform.InverseTransformPoint(TipPosition.position),
        };

        // さっきの頂点を順に指定して三角形をつくる
        mesh.triangles = new int[]{
            0, 1, 2,
            1, 2, 3,
            2, 3, 4,
            3, 4, 5,
        };

        // 反映
        meshCollider.enabled = false;
        meshCollider.enabled = true;

        // 前回位置の記憶
        lastTipPosition = TipPosition.position;
        lastPosition = transform.position;
    }
}

スクリプトをアタッチ

上記スクリプトを「WeaponCollider」オブジェクトにアタッチし先ほど生成した「TipPosition」を指定します。
image.png

実行結果

Animation6.gif

前回の位置から今回の位置までが補完されるようになりました!
これで30FPS設定でもすり抜けることはないでしょう。

さいごに

いざ自分でゲームを作ってみると、簡単そうに見える部分も実は難しくて、時間がいくらあっても足りません。
最後まで作り上げた某ファイナルソードは、(品質はともかく)とてもスゴいと思います。

分かりづらい点、ソースコードの改善提案などあればコメントください!
思ったより長い記事になってしまいましたが、最後までご覧いただきありがとうございました。

使用アセット(一部有料)

※アフィ含みません

環境

下記の環境で撮影しました。
Unity:2020.3.20f1

28
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?