したかみ ぶろぐ

Unity成分多め

ReorderableListを使って敵の動きを編集する

f:id:vxd-naoshi-19961205-maro:20190625023316p:plain

はじめに

これまでの記事です。

aizu-vr.hatenablog.com

shitakami.hatenablog.com

shitakami.hatenablog.com


ReorderableListを使って敵の動きを簡単に編集したいなーと思いながらエディタ拡張に苦労して時間をかけすぎました。まずは取り合えず完成させることを目標にして作成したものをまとめたいと思います。

加えて今回作成したものは後の開発でも使えると思うので自分用でもあります。


出来たもの

エディタ拡張を使ってモンスターのアクションを編集しやすくしました。 (すいません!ちっさくなってしまいました)

f:id:vxd-naoshi-19961205-maro:20190625013527g:plain

このように簡単に要素の追加、削除、入れ替えが出来ます。


ここでInspectorを次のようにします。

f:id:vxd-naoshi-19961205-maro:20190625014205p:plain

設定した内容です。

  • 歩く速さを 3
  • 振り向くスピードを 5.6
  • アクションを次のように設定
    • (5, 0, 10)に移動
    • 2秒待機
    • プレイヤー方向を見る
    • 2回弾を投げる
  • 要素3までのアクションを実行したら要素1に戻る


結果

f:id:vxd-naoshi-19961205-maro:20190625015841g:plain

指定された通りのアクションを実行しています。


概要

やりたいことの内容は上の一番目のリンクに大体書いてありますが、ここでも簡単な説明をします。

今まではモンスターの取るアクション、行動、AIを一つのプログラムに書いていました。また、それによりすべてのモンスターが同じ行動をするのでゲームが飽きやすい。

このことを改善すべく、モンスターのアクション一つ一つを別々のプログラムに書き、そのアクションをUnityのInspectorから簡単に編集することを目標にしています。


基盤を作る

色々試行錯誤したところ、複数の派生クラスをReorderableListに保存することが難しいことがわかりました。 その反省から、保存するのは単一の構造体のみとしました。

構造体ActionData

以下のプログラムはモンスターの行動を表す構造体ActionDataです。

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

[System.Serializable]
public struct ActionData {

    [SerializeField]
    public ActionType type;

    [SerializeField]
    public int actionCount;
    [SerializeField]
    public float carryoutTime;

    [SerializeField]
    public Vector3 destination;

}


public enum ActionType {
    MoveAction,
    LookPlayerAction,
    ShotBallAction,
    ....
}

この構造体はモンスターの様々なアクションで使われる値を保持します。 ActionTypeは動きの種類を表します。例えば、指定されたところまで移動するMoveAction、プレイヤーの方向を向くLookPlayerAction、ボールを投げるShotBallActionという感じです。


MonsterActionクラス

次にMonsterActionクラスを定義します。

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

public abstract class MonsterAction {

    protected Monster monster;
    protected ActionData data;

    public MonsterAction(Monster monster, ActionData data) {
        this.monster = monster;
        this.data = data;
    }

    public void Update() {

        UpdateAction();

        if (CheckFinishAction())
            monster.ChangeMonsterAction();

    }

    protected abstract void UpdateAction();

    protected abstract bool CheckFinishAction();
    
}

このクラスは抽象メソッド UpdateActionCheckFinishAction を持ち、Updateメソッドでこの二つを使用しています。

UpdateActionではモンスターのアクションを、CheckFinishActionではアクションが終了したか確かめる関数を派生クラスで定義します。 後程解説しますが、アクションが終了したらMonster.ChangeMonsterActionにより次のMonsterActionに変更します。

コンストラクタではこのインスタンスを使うMonsterと行うアクションに必要なActionDataを受け取ります。


Monsterクラス

次にMonsterクラスを定義します。

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

public class Monster : MonoBehaviour {

    [SerializeField]
    private float velocity;
    [SerializeField]
    private float rotateSpeed;

    [SerializeField]
    private int beginLoopIndex;
    [SerializeField]
    private int endLoopIndex;


    #region Getter

    public float Velocity { get { return velocity; } }
    public float RotateSpeed { get { return rotateSpeed; } }

    #endregion

    [SerializeField]
    private List<ActionData> actions;

    [SerializeField]
    private ActionData appearAction;
    [SerializeField]
    private ActionData deathAction;

    private MonsterAction nowMonsterAction;

    private int actionsIndex = 0;

    public Animator animator;

    // Use this for initialization
    void Start() {

        animator = GetComponent<Animator>();
        nowMonsterAction = MonsterActionFactory.MakeAction(this, appearAction);
    }

    // Update is called once per frame
    void Update() {

        if (nowMonsterAction != null)
            nowMonsterAction.Update();

    }

    public void ChangeMonsterAction() {

        nowMonsterAction = MonsterActionFactory.MakeAction(this, actions[actionsIndex]);

        if (actionsIndex == endLoopIndex) {
            actionsIndex = beginLoopIndex;
        }
        else {
            actionsIndex = Mathf.Min(actionsIndex + 1, actions.Count);
        }

    }


    public void ChangeDeathAction() {

        nowMonsterAction = MonsterActionFactory.MakeAction(this, deathAction);
    }
    

}

少し長いので、要点を説明します。所々に出てくるMonsterActionFactoryクラスは後程。


まず、モンスターが行うアクションを次のようにメンバ変数に持ちます。

    [SerializeField]
    private List<ActionData> actions;

    [SerializeField]
    private ActionData appearAction;
    [SerializeField]
    private ActionData deathAction;

appearActionはモンスターが生成された際に行うアクション、deathActionではモンスターが倒された際に行うアクションを指定します。

次にactionsではモンスターが順に行うアクションを保持します。この部分にReorderableListを適用し、Inspectorからモンスターが行うアクションを設定できます。(後でその部分に触れます)


ChangeMonsterAction関数について少し解説します。

 public void ChangeMonsterAction() {

        nowMonsterAction = MonsterActionFactory.MakeAction(this, actions[actionsIndex]);

        if (actionsIndex == endLoopIndex) {
            actionsIndex = beginLoopIndex;
        }
        else {
            actionsIndex = Mathf.Min(actionsIndex + 1, actions.Count - 1);
        }

    }

この関数が呼ばれた際は指定した次のアクションを実行します。

また、beginLoopIndexとendLoopIndexを使ってアクションをループするようにしています。


次にUpdate関数について

 void Update() {

        if (nowMonsterAction != null)
            nowMonsterAction.Update();

    }

ここでは現在指定されているMonsterActionのUpdate関数を実行します。前述した通り、Update関数ではモンスターが行うアクションとアクションが終了したか判定します。


MonsterActionFactoryクラス

指定された型のMonsterActionを生成して返すメソッドを持つクラスです。

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

public static class MonsterActionFactory  {

    public static MonsterAction MakeAction(Monster monster, ActionData actionData) {

        
        switch (actionData.type) {

            // Titan専用
            case ActionType.ShotBallAction:
                return new ShotBallAction(monster, actionData);
                
                
            case ActionType.AppearAction:
                return new AppearAction(monster, actionData);
                

            case ActionType.DeathAction:
                return new DeathAction(monster, actionData);
                

            case ActionType.IdleAction:
                return new IdleAction(monster, actionData);
                

            case ActionType.MoveAction:
                return new MoveAction(monster, actionData);
                

            case ActionType.LookPlayerAction:
                return new LookPlayerAction(monster, actionData);

            .......

        }

        return null;

    }


}

ActionDataのActionTypeを比較し対応するインスタンスを生成しています。


MonsterActionを継承したクラス

ここで3つほど例として継承クラスを紹介します。


IdleActionクラス

ただ待つだけのクラスです。

public class IdleAction : MonsterAction {

    private float time;
    private float finishTime;

    public IdleAction(Monster monster, ActionData data) : base(monster, data) {
        time = 0;
        finishTime = data.carryoutTime;
        monster.animator.SetTrigger("Idle");
    }

    protected override void UpdateAction() {

        time += Time.deltaTime;

    }

    protected override bool CheckFinishAction() {
        return time > finishTime;
    }

}

解説することがあるとすればコンストラクタでアニメーションをIdleに変更していること、CheckFinishActionで指定された時間が経過したかを判断しています。


AppearActionクラス

生成時のアニメーションが終了するのを待ちます。

public class AppearAction : MonsterAction {

    public AppearAction(Monster monster, ActionData data) : base(monster, data) {

    }

    protected override void UpdateAction() {
        
    }

    protected override bool CheckFinishAction() {

        AnimatorStateInfo stateInfo = monster.animator.GetCurrentAnimatorStateInfo(0);
        return stateInfo.normalizedTime > 0.95f;

    }

}

CheckFinishAction関数内でアニメーションの時間を取得し、ほぼ終わりに近い状態であるかを判定しています。このクラスは後々シェーダーなどを使用してもう少し派手にする予定です。


MoveActionクラス

地上にいるモンスターに適用できるアクションです。

public class MoveAction : MonsterAction {

    private float velocity;
    private float rotateSpeed;
    private Vector3 destination;
    private Transform transform;

    private readonly float stopDistance = 0.3f;
    private readonly float power = 3f;

    public MoveAction(Monster monster, ActionData data) : base(monster, data) {

        velocity = monster.Velocity;
        rotateSpeed = monster.RotateSpeed;
        destination = data.destination;
        transform = monster.transform;
        monster.animator.SetTrigger("Move");
    }

    protected override void UpdateAction() {

        Vector3 walkDirection = destination - monster.transform.position;
        walkDirection = walkDirection.normalized;
        Quaternion q = Quaternion.LookRotation(walkDirection.normalized, Vector3.up);
        transform.rotation = Quaternion.Slerp(transform.rotation, q, Time.deltaTime * rotateSpeed);
        float moveSpeed = velocity * Mathf.Pow(Mathf.Max(0, Vector3.Dot(transform.forward, walkDirection)), power);
        transform.position += Time.deltaTime * transform.forward * moveSpeed;

    }


    protected override bool CheckFinishAction() {

        float distance = (destination - transform.position).sqrMagnitude;

        return distance < stopDistance;

    }

}

このプログラムは過去に解説したものになります。それをここで使い回しました。

shitakami.hatenablog.com



作っての感想

ここでは基盤となるプログラムを解説しました。エディタ拡張については別の記事でまとめたいと思います。

モンスター一体だけだとあまり効果を感じにくいかもしれませんが、

  • アクション一つ一つを丁寧に作成できる
  • アクションが他のアクションに干渉することがほぼない
  • 別々の動きをするモンスターを簡単に作れる
  • 難易度調整がやり易い
  • 別のモンスターにもアクションを使い回せる
  • 別のプロジェクトにも使える

と感じました。


欠点としては

  • 決まった手順の行動しか行わない
  • ActionDataをすべてのアクションに対応できるようにする
  • MonsterActionFactoryのswitch文が巨大化する

次の目標としてはノードベースのエディタ拡張とか出来たら完璧だな~って思っています。
付け加えて、Twitter上でBehavior Designerというものを見つけたのでいつか触って勉強したい。