【開発メモ】敵キャラクターとロジックの作成に関する考察 その2

というわけで、前回に引き続き、敵キャラクターとロジックに関するお話です!
出張が多かったので開発が滞っていますが、やっとロジッククラスが作成できたので記載します!

shot2ss20170205204248559

前回の記事はこちら!

【開発メモ】敵キャラクター作成とロジックに関する考察

今回は上記の記事内で考察した中の、移動ロジックと攻撃ロジックについて考えてみます。

ロジッククラス作成の準備

ロジッククラス作成の前に下準備をしておきます。
まずはロジッククラスを共通化させるためのインタフェースを定義します。

【IEnemyMoveLogic】

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

public interface IEnemyMoveLogic {
    /// <summary>
    /// 移動する速度を取得する
    /// </summary>
    Vector3 getMoveVelocity();
}

【IEnemyAttackLogic】

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

public interface IEnemyAttackLogic {

    /// <summary>
    /// 攻撃を行うか判断する
    /// </summary>
    bool attackDetermine();

    /// <summary>
    /// 攻撃判定を取得する
    /// </summary>
    GameObject getAttackHitObject();

    /// <summary>
    /// 攻撃の種類を取得する
    /// </summary>
    int getAttackType();

    /// <summary>
    /// 攻撃時に再生するモーションを取得する
    /// </summary>
    string getAttackMotion();
}

次に各ロジッククラスのスーパークラスとなる抽象クラスを作成します。
以下は大元となる「AbstractLogic」クラスです。

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

namespace EnemyLogic {
    public abstract class AbstractLogic : MonoBehaviour {

        protected Enemy enemy;

        protected virtual void Start () {
            enemy = this.GetComponent<Enemy>();
        }
    }
}

更に、移動・攻撃ロジック用に AbstractLogic を継承した抽象クラスを作成します。
共通処理として、Enemy クラスにロジックを設定します。

【AbstractMoveLogic】

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

namespace EnemyLogic.Move {
    public abstract class AbstractMoveLogic : AbstractLogic, IEnemyMoveLogic {
        public float move_speed;

        protected override void Start () {
            base.Start();

            enemy.setMoveLogic(this);
        }

        public abstract Vector3 getMoveVelocity();
    }
}

【AbstractAttackLogic】

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

namespace EnemyLogic.Attack {
    public abstract class AbstractAttackLogic : AbstractLogic, IEnemyAttackLogic {
        public const int CLOSE_RANGE_HIT_TYPE = 1;
        public const int RANGE_HIT_TYPE = 2;

        public GameObject attackHitObject;
        public int attack_type;
        public string attack_motion;

        protected override void Start () {
            base.Start();

            enemy.addAttackLogic(this);
        }

        public abstract bool attackDetermine();
        public abstract GameObject getAttackHitObject();
        public abstract int getAttackType();
        public abstract string getAttackMotion();
    }
}

移動用クラスは IEnemyMoveLogic、攻撃用クラスは IEnemyAttackLogic を実装させます。
これらのインタフェースを介した実装は、「特定のロジックに依存させない」ために重要です。

ロジッククラスの作成

いきなり複雑なロジックは厳しいので、今回は簡単な移動クラスと攻撃クラスを作成してみます。

以下は移動ロジックの、「自分のいる地点から左右に往復移動」のクラスです。
(横スクロール型のアクションで使用することを想定しています)

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

namespace EnemyLogic.Move {
    public class RoundTripMoveLogic : AbstractMoveLogic {

        // 基準点から往復するまでの距離
        public float repeat_dist;

        private Vector3 defaultPosition;

        protected override void Start () {
            base.Start();

            defaultPosition = enemy.transform.position;
        }

        public override Vector3 getMoveVelocity() {
            // 振り向き処理
            if (repeat_dist < Mathf.Abs(enemy.transform.position.x - defaultPosition.x)) {
                Vector3 angle = new Vector3(defaultPosition.x, enemy.transform.position.y, defaultPosition.z);
                enemy.lockRotate(angle);
            }

            float forward_x = enemy.transform.forward.x * move_speed;
            Vector3 newPosition = new Vector3(forward_x, enemy.rb.velocity.y, enemy.rb.velocity.z);

            return newPosition;
        }
    }
}

ロジックに関する深い説明は割愛しますが、「往復する距離まで移動したら逆側に振り向かせる」こと、「敵キャラクターは Rigidbody で動かしているので、Rigidbody.velocity を変更する」ことに注意します。

次は攻撃ロジックの、「一定間隔で攻撃」のクラスです。

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

namespace EnemyLogic.Attack {

    public class AttackIntervalLogic : AbstractAttackLogic {

        // 攻撃を行う間隔 (フレーム)
        public int attack_interval;

        private int attack_count;

        protected override void Start () {
            base.Start();

            enemy.addAttackLogic(this);
            attack_count = 0;
        }

        public override bool attackDetermine() {
            if (attack_interval < attack_count) {
                attack_count = 0;
                return true;
            }
            attack_count += 1;
            return false;
        }

        public override GameObject getAttackHitObject() {
            return this.attackHitObject;
        }

        public override int getAttackType() {
            return this.attack_type;
        }

        public override string getAttackMotion() {
            return this.attack_motion;
        }
    }
}

主に使用するのは attackDetermine() で、これを Enemy クラスの Update() 内で呼び出し、攻撃を行うかの判定をしています。
その他、インタフェースで定義された関数を実装していますが、ロジック上で必要なパラメータを取得するためのものです。
このあたりはゲームによって実装が異なると思います。

共通して気を付けることは、「ロジッククラスから敵キャラクターを直接操作しない」ことです。
複数のロジッククラスからあれこれ敵キャラクターを操作すると訳が分からなくなります。
ロジッククラスは条件判定後に結果を Enemy クラスへ返したり、Enemy クラスの関数を呼び出して間接的に操作したりします。

敵キャラクター共通のクラス作成

敵キャラクター共通のスーパークラス「Enemy」を作成します。
既にダメージ処理等が入ってしまっていて長いので、ロジックに関する部分のみ紹介します。

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

[RequireComponent (typeof (Rigidbody))]

public abstract class Enemy : MonoBehaviour, ICharacter {

    protected bool isAttack;

    // 攻撃判定の出現位置
    public GameObject attackHitOffset;
    public GameObject bulletHitOffset;

    protected Animator animator;
    protected AnimatorStateInfo stateInfo;

    public Rigidbody rb;
    protected Collider bodyCollider;

    // ロジック制御クラス
    protected IEnemyMoveLogic moveLogic;
    protected List<IEnemyAttackLogic> attackLogics;

    protected virtual void Awake() {
        // ロジック初期化
        attackLogics = new List<IEnemyAttackLogic>();
    }

    protected virtual void Start() {
        isAttack = false;

        // 各コンポーネント取得
        animator = this.GetComponent<Animator>();
        rb = this.GetComponent<Rigidbody>();
        bodyCollider = this.GetComponent<Collider>();

    }

    /// <summary>
    /// 毎フレーム実行する処理
    /// </summary>
    protected virtual void Update() {
        if (animator && canAction) {
            stateInfo = animator.GetCurrentAnimatorStateInfo(0);

            // 移動ロジック
            if (moveLogic != null && !isAttack) {
                movement(moveLogic.getMoveVelocity());
            }

            // 攻撃ロジック
            foreach (IEnemyAttackLogic logic in attackLogics) {
                if (logic.attackDetermine()) {
                    GameObject attackHitObject = logic.getAttackHitObject();
                    string attack_motion = logic.getAttackMotion();

                    switch (logic.getAttackType()) {
                        case EnemyLogic.Attack.AbstractAttackLogic.CLOSE_RANGE_HIT_TYPE:
                            // 近距離攻撃処理
                            break;
                        case EnemyLogic.Attack.AbstractAttackLogic.RANGE_HIT_TYPE:
                            // 遠距離攻撃処理
                            break;
                    }
                }
            }
        }
    }

    /// <summary>
    /// 移動処理を行う
    /// </summary>
    public void movement(Vector3 velocity) {
        rb.velocity = velocity;
    }

    /// <summary>
    /// 指定した座標へ振り向かせる
    /// </summary>
    /// <param name="angle"></param>
    public void lockRotate(Vector3 angle) {
        transform.LookAt(angle);
    }

    public void setMoveLogic(IEnemyMoveLogic moveLogic) {
        this.moveLogic = moveLogic;
    }

    public void addAttackLogic(IEnemyAttackLogic attackLogic) {
        this.attackLogics.Add(attackLogic);
    }
}

今までは Update() 内にごりごりロジックを書いていましたが、今回は各ロジッククラスを制御する命令のみに留めます。

キモとなるのは、ロジッククラスの型をインタフェースにしておくことです。
インタフェースに記載された関数の実装が約束されるため、どのロジッククラスにも依存せず共通化できること、Enemy クラスからロジッククラスを見た際に最小限の機能のみを提供するためです。

攻撃クラスのみ List<> で管理しています。
例えば「近距離攻撃と遠距離攻撃を行うキャラクター」や「2種類の近距離攻撃を持つキャラクター」を作成する際、複数のロジックを作成して対応させるためです。
また List<> ならロジックが1つでも複数あっても対応でき、特定のロジックのみを無効化しても問題なく動きます。

実際に使用する際はキャラクターごとに専用のサブクラスを作成し、実際の敵キャラクター用ゲームオブジェクトのコンポーネントとして設定します。
中身はキャラクター固有の特殊能力がある場合に実装し、特にない場合はスーパークラスの関数を呼び出すだけです。

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

public class Raidran : Enemy {

    protected override void Start () {
        base.Start();
    }

    protected override void Update() {
        base.Update();
    }
}

今回の敵キャラクター「レイドラン」は特に考えていないため、何も考えずに継承しているだけです。

敵キャラクターへの設定とプレハブ化

上で作った移動ロジックと攻撃ロジックを敵キャラクターのオブジェクトに設定し、その動作を確認してみます。

20170205_01

うーん・・・左右を往復するだけの雑魚っぽいにはなったでしょうか。
本ゲームにはアクションでよくある「接触被ダメージ」がないので微妙かもしれませんが・・・。

ちなみに、Unityのベストプラクティス的にはオブジェクトの種別ごとにプレハブを作るのが良いらしいです。
動的にコンポーネントを足すのもアレなので、ロジックを設定したキャラクター毎にプレハブ化します。
プレハブが多くなって管理が面倒ですが、動的にインスタンスすることを考えればこちらの方が良いです。

まとめ

そんなわけで、ロジック系クラス作成の基盤と簡単なロジックの作成をしてみました!
敵キャラクター制御が使いまわせるようになり、コンポーネント毎に付け替えできるのでいい感じです!
索敵やジャンプのロジックも作れば、更に幅広いアクションを取らせることができそうですね。

ただやっていて思うのは、かなり手探りで自分なりの設計をしているということです。
このあたりのベストプラクティスとかってあるのでしょうか。
ゲームによって異なるとは思いますが、他の方がどのように実装しているのか気になります。
そのあたりのお話をUnity道場等でやっていたらぜひ参加したいところです。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

*