ぐーるらいふ

底辺。

【Unity】攻撃判定タイミングをグラフィカルに作りたい 【Editor】

いきなりですが

GitHubにコード一式あげてます。ちょっと長いので、面倒な方はソースを見ると早いと思います。

github.com

格闘ゲームを作ろうとしてます。
格闘ゲームに限らずアクション要素のあるゲームには攻撃判定というものが大抵あります。

http://www.capcom.co.jp/blog/sf4/img_upload/t11151JRI_02.jpg

この攻撃判定ですが、「常に出続けてOK」なゲームもあれば、
「一時的にだけ表示してほしい」というゲームもあります。
アクションゲームの攻撃で「剣を振る」なども一時的に攻撃判定が出てほしいアクションだと思います。

攻撃判定の制御

パンチ Animation

start ---(攻撃判定発生中) ----->> end

キック Animation

start -------------(攻撃判定発生中) ----->> end

格闘ゲームで考えるとパンチならパンチ、キックならキックのアニメーションがあり、
攻撃判定が発生するタイミングとその長さをそれぞれ設定しなければなりません。

  • アニメーションの数が少ない
  • 一人で開発している、

などといった場合は
一つ一つ再生をスクリプトで拾って

10frame後に30frame間だけ判定を出す

などといったコードを埋め込んでいく事でも実現は可能です。
が、多人数で作っていたり、大規模になったりするとその限りではありません。

グラフィカルに当たり判定を制御したい!

Unity使ってるぞ!Editorだって作れちゃうんだ。
せっかくだからEditorで攻撃判定を制御するツールを作っちゃおうじゃないか。

事前準備

今回もまた以下のAssetsを使用して説明します。


f:id:ghoul_life:20170711171702p:plain
https://www.assetstore.unity3d.com/jp/#!/content/33478

f:id:ghoul_life:20170711171719p:plain
https://www.assetstore.unity3d.com/jp/#!/content/33083

以上のお二方は前回から引き続き。
また、Animationの共通化については以下の記事を参考にしてください。

ghoul-life.hatenablog.com

キャラクターに攻撃判定コライダーをつけよう

f:id:ghoul_life:20170720192725p:plain

f:id:ghoul_life:20170720192810p:plain

こんな感じで手足にCapsuleColliderと以下のスクリプトを付与します。
このCapsuleColliderが攻撃判定の大きさになります。

プログラマー作業ではなく、
ゲームデザイナーに位置や大きさなどの調整を任せる
といった分業も出来るでしょう。

また、タックルやヘッドバットといった別の箇所にも攻撃判定を付ける
といった事も可能です。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(CapsuleCollider))]
public class HitCollider : MonoBehaviour {

    public HitType hitType;
    private CapsuleCollider collider;

    public void Disable()
    {
        if (collider == null)
        {
            collider = GetComponent<CapsuleCollider>();
        }
        collider.enabled = false;
    }

    public void Enable()
    {
        if (collider == null)
        {
            collider = GetComponent<CapsuleCollider>();
        }
        collider.enabled = true;
    }

    // colliderをSceneビューで表示してみる…。多少強引な計算があるので、
    // colliderの正確な位置とサイズを取る方法があれば…。
    void OnDrawGizmos()
    {
        if (collider && collider.enabled)
        {

            Vector3 offset = 
                transform.right * (collider.center.x / collider.height / 2) + 
                transform.up * (collider.center.y / collider.height / 2) + 
                transform.forward * (collider.center.z / collider.height / 2);

            Vector3 size = new Vector3(
                (collider.radius / 0.5f) * collider.transform.lossyScale.x,
                (collider.height / 2) * collider.transform.lossyScale.y, 
                (collider.radius / 0.5f) * collider.transform.lossyScale.z
                );

            Quaternion dir;
            switch (collider.direction)
            {
                case 0:
                    dir = Quaternion.Euler(Vector3.forward * 90);
                    break;
                case 1:
                    dir = Quaternion.Euler(Vector3.up * 90);
                    break;
                case 2:
                    dir = Quaternion.Euler(Vector3.right * 90);
                    break;
                default:
                    dir = Quaternion.Euler(Vector3.zero);
                    break;
            }
            Gizmos.color = new Color(1, 0, 0, 0.5f);
            Gizmos.DrawMesh(GetPrimitiveMesh(PrimitiveType.Capsule), collider.transform.position + offset, transform.rotation * dir, size);

        }
    }

    private Mesh GetPrimitiveMesh(PrimitiveType type)
    {

        GameObject gameObject = GameObject.CreatePrimitive(type);
        Mesh mesh = gameObject.GetComponent<MeshFilter>().sharedMesh;
        DestroyImmediate(gameObject);

        return mesh;

    }

}

スクリプトはコライダーのON/OFFを制御するのに使います。
HitTypeは後述。

攻撃判定種別を付けよう

今回は格闘ゲームを題材にしているため、
攻撃判定は複数あることにします。

  • 左手
  • 右手
  • 左足
  • 右足

の4つとします。
ゲームによっては、ひじやひざ、または頭突きや肩なんてものもあるかもしれません。
が、今回はシンプルにこの4つにします。
これを定義しているのが、HitTypeです。

public enum HitType
{
    LEFT_HAND = 1,
    RIGHT_HAND = 2,
    LEFT_LEG = 4,
    RIGHT_LEG = 8
}

こんなただのenumです。
値はbit演算のためにこのようにしてます。
その1で対応したHitColliderに左手なら左手、右足なら右足といったHitTypeをセットしておきます。

まずはAnimationClipを再生するEditorを作る

Editor沼に足を踏み入れたような気がします。
AnimationClipなんて、Inspectorの再生窓使えば見れるやんけ!
という声が聞こえてきそうですが、それをあえて自分で作ってみる。

(EditorWindowの作り方の説明などはいくらでも出てくるので割愛)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

// 単純なサンプルエディタ
public class AnimClipSampleEditor : EditorWindow
{

    // アニメーションクリップを再生させるオブジェクト
    private Animator animObject;
    // 再生したいアニメーションクリップ
    private AnimationClip animClip;

    [MenuItem("Window/AnimClipSampleEditor")]
    static void Open()
    {
        GetWindow<AnimClipSampleEditor>();
    }

    private float value;

    void OnGUI()
    {
        GUILayout.BeginHorizontal();
        GUILayout.Label("character : ", GUILayout.Width(110));
        animObject = (Animator)EditorGUILayout.ObjectField(animObject, typeof(UnityEngine.Animator), true);
        GUILayout.EndHorizontal();
        EditorGUILayout.Space();

        GUILayout.BeginHorizontal();
        GUILayout.Label("animation clip : ", GUILayout.Width(110));
        animClip = (AnimationClip)EditorGUILayout.ObjectField(animClip, typeof(UnityEngine.AnimationClip), true);
        GUILayout.EndHorizontal();
        EditorGUILayout.Space();

        if (animObject == null || animClip == null)
        {
            GUILayout.Label("Please Setting character and animation clip");
            EditorGUILayout.Space();
            EditorGUI.BeginDisabledGroup(true);

        }

        GUILayout.BeginHorizontal();
        value = EditorGUILayout.Slider(new GUIContent("TimeLine"), value, 0, 1, GUILayout.Width(300));
        GUILayout.EndHorizontal();

        if (animObject == null || animClip == null)
        {
            EditorGUI.EndDisabledGroup();
        }

        //--------------------------------------------------------------------

        if (animObject != null && animClip != null)
        {
            animClip.SampleAnimation(animObject.gameObject, value);
        }

        Repaint();
    }

}

そして適当にキャラクターをシーンに配置して、
AnimationClipをセットすれば、
スライダーでアニメーションを動かすことが出来ます。

f:id:ghoul_life:20170720193802p:plain

攻撃判定を付与するには

色々なアプローチがあります。

  • 自力Scriptアプローチ、
  • AnimatorからStateBehaviorでUpdateのframeを見る

などなんでも良いのですが、
今回はAnimationClipからEventを呼び出すアプローチで行きたいと思います。

AnimationClipにはEventを付与することが出来ます。

f:id:ghoul_life:20170720195028p:plain

これによって、AnimationClipの最後にEventを付与して、アニメーションの終了判定を行ったり
していた過去もあるかもしれません。

攻撃判定を開始するタイミングでEventを発行して、終了タイミングで再度Eventを発行すれば良さそうです。

HitTypeの判定について

攻撃判定を開始した時

  • 「左手の攻撃判定開始」 -> 「判定終了」
  • 「右足の攻撃判定開始」 -> 「判定終了」
  • 「左手と右手の攻撃判定開始」 -> 「判定終了」

といった命令が送られてくると考えます。

こうしました。

イベントを送る側(Animation Clip)

    private int CreateHitEvent()
    {
        int result = 0;

        if (toggleLeftHand) result = (result | (int)HitType.LEFT_HAND);
        if (toggleRightHand) result = (result | (int)HitType.RIGHT_HAND);
        if (toggleLeftLeg) result = (result | (int)HitType.LEFT_LEG);
        if (toggleRightLeg) result = (result | (int)HitType.RIGHT_LEG);

        return result;
    }
    
    void OnGUI(){
        //~~
        if (GUILayout.Button("Add Hit Event")){
            var serialied = new SerializedObject(animClip);
            serialied.Update();

            var events = new List<AnimationEvent>();

            onHitEventStart = new AnimationEvent();
            onHitEventStart.time = startTime;
            onHitEventStart.functionName = "OnHitEventStart";
            onHitEventStart.intParameter = CreateHitEvent();
            events.Add(onHitEventStart);

            onHitEventEnd = new AnimationEvent();
            onHitEventEnd.time = endTime;
            onHitEventEnd.functionName = "OnHitEventEnd";
            events.Add(onHitEventEnd);

            // Animation ClipにEventを付与します
            AnimationUtility.SetAnimationEvents((AnimationClip)serialied.targetObject , events.ToArray());
            EditorUtility.SetDirty(serialied.targetObject);

            serialied.ApplyModifiedProperties();
        }
        //~~
    }

イベントを受ける側(Character)

    // hitEvtValue = 0x1111 , 0x1001 といった値が来る
    public void OnHitEventStart(int hitEvtValue)
    {
        // 該当するcolliderをON
        foreach (var hit in hitColliders)
        {
            if (((int)hit.hitType & hitEvtValue) != 0)
            {
                hit.Enable(); // HitColliderをON
            }
        }

    }

ビット演算を使い、そのフラグが立っていたら判定をONというシンプルな作りです。
他にも渡したいものがあればObjectに情報をまとめて渡すなんてのもアリなんですが、
とりあえず思いつかなかったので最小限にしてます。

あとはこれをグラフィカルにEditor化すればOK

必要なパーツは大体揃ったので、後はウィンドウにまとめて値をEditorから操作します。

f:id:ghoul_life:20170721103214p:plain

どうしてもGUI系はコードだらっと長いので、githubのコードを参考にしてください。

github.com

MinMaxSliderの範囲が攻撃判定の範囲ということにしました。
ハンドルが持ちにくく、もう少し見た目が変えられるといいんですが、
GUIStyleとか頑張って作ればいけるのかな。

攻撃判定発生タイミングはAnimationClip側に紐付いているため、
キャラクターを変えても、AnimationClipに攻撃判定発生タイミングは変わらず、
共通で扱うことが出来ます。

f:id:ghoul_life:20170721124224g:plain

そして、攻撃判定の大きさや位置はキャラクター側に紐付いているため、
キャラクター個別に設定することが出来ます。


f:id:ghoul_life:20170721123414g:plain

こうしてEditor沼へと落ちていくのだった

これで作業の分離が出来ました。(?)
出来たのか?

ゲームデザイナーやキャラクターデザイナー
攻撃判定位置や発生フレームを丸投げして、
自分はロジックを組む…という分担が出来ます。

「困難は群れで分け合え」

と、かばんさんも言っていました。

自分は完全に個人なので、自分で作って自分で使う!
という無駄な作業かもしれないのですが、勉強だと思って…。

注意点として

fbxの内側にあるAnimationClipをそのまま使おうとすると、
Eventを上手く付与出来ないかもしれません。(保存したのに反映されないなど)
その場合はfbxからAnimationClipを取り出すと、Editが自由になるため
Eventの付与も出来るはずです。
こちらも参考に!神。

baba-s.hatenablog.com