ゴマちゃんフロンティア

アザラシが大好きなエンジニアの開発日記です

【Unity】キャラクターをスクリプトで自動的に移動させる方法

time 2017/08/27

というわけで、今回はタイトル通り「キャラクターを自動移動させる」な話題になります。

キャラクターが勝手に動くシーンって、イベントのデモシーンとかでよくありますよね。もっとアクション寄りの話をすれば、エリアのつなぎ目 (通路やドアなど) に到達すると、自動的に次のエリアに移動するゲームもあります。
自作ゲームでも「カメラアングル切り替え時」や「ワープポイント」など、自動移動させたい場面がちらほらあるので、その実現方法について考えてみました。
その場しのぎのコーディングを晒してもブログ的につまらないので、なるべく汎用的に作るよう心掛けていきます。

移動先と移動用パラメータの定義

移動用パラメータクラスの作成

「移動先」「移動時間」「移動前の遅延時間」のパラメータを持つ専用のクラスを作成します。

[System.Serializable]
public class RouteParameter {
    // 移動先となる空オブジェクトのTransform
    public Transform movePoint;
    // 移動時間
    public float time;
    // 開始前の遅延時間
    public float delay;
}

クラスに属性「System.Serializable」を付けるとインスペクター上で編集できるようなので、指定しておきましょう。

自動移動制御用クラスに移動用パラメータを設定

自動移動を制御するクラス「AutoMovementPoint」を作成し、先ほどの移動用パラメータクラス型のListを定義します。

using System.Collections.Generic;
using UnityEngine;

public class AutoMovementPoint : MonoBehaviour {

    [SerializeField]
    protected List<RouteParameter> routes;
}

これでパラメータをインスペクター上で編集できるようになります。

エディタ上で移動先を編集しやすくする

スクリプトから位置を指定するのはアレなので、シーンビュー上の空オブジェクトの位置を移動先として使用します。
ただ、空オブジェクトはシーンビュー上で選択しにくいです。なのでOnDrawGizmos()を使い、DrawSphere()で球体を表示するようにしておきます。

自分の場合、Gizmoを描画するための汎用クラスを作成し、描画が必要なオブジェクトに設定しています。

using UnityEngine;

public class EditorObject : MonoBehaviour {

    // ギズモの描画フラグ
    public bool draw_sphere;

    // DrawSphereの大きさ
    public float sphere_radius;

    // DrawSphereの色
    public Color sphereColor = Color.blue;

    private void OnDrawGizmos() {
        if (draw_sphere) {
            Gizmos.color = sphereColor;
            Gizmos.DrawSphere(transform.position, sphere_radius);
        }
    }
}

色とか変えれてもしょうがない気もしますが、複数のオブジェクト位置を描画する場合に区別できるので、邪魔にはならないかと。
実際に描画すると以下のような感じです。

あとはシーンビュー上でGizmoをクリックすればオブジェクトが選択できます。

自動移動処理

自動移動用インタフェースの作成

「自動移動」と言っても、キャラクターによって移動方法が異なります。プレイヤーキャラクターはCharacterMotorPlatformInputControllerを使用しており、敵キャラクターはRigidBodyを使用しています。
その差を吸収するために、自動移動の対象となるオブジェクト用にインタフェースを作成し、各キャラクターに対応した物理コンポーネントのvelocityに対するセッタを実装します。
加えて自動移動の開始時と終了時に呼び出す関数も定義しておきます。これは自動移動中の移動入力を無効化したり、キャラクターに移動モーションを取らせたりするために使います。

using UnityEngine;

public interface IAutoMovement {
    void setMovementVelocity(Vector3 velocity);

    void onMovementStart();
    void onMovementEnd();
}

Playerクラス側の実装例です。
(charMotor変数はCharacterMotor型のインスタンスです)

public void setMovementVelocity(Vector3 velocity) {
    velocity = new Vector3(velocity.x, charMotor.movement.velocity.y, velocity.z);
    this.charMotor.movement.velocity = velocity;
}

public void onMovementStart() {
    charMotor.movement.velocity = Vector3.zero;
    charMotor.canControl = false;
}

public void onMovementEnd() {
    charMotor.canControl = true;
}

上はCharacterMotor使用時の例ですが、RigidBodyの場合はrigidBody.velocityで良いかと思われます。
対象のオブジェクトやキャラクターに応じて実装しましょう。

DOTweenの使用と実装

普段はiTween愛好家ですが、連続でアニメーションを行う場合はDOTweenSequenceが便利なので、今回はDOTweenを使用してみます。特筆すべきはdelayで、「数秒待ってから実行」が簡単に行えます。
ただし、トゥイーン系ライブラリの移動はTransformpositionを直接変化させるものが多く、考えずに使うと (判定的な意味で) 悲惨なことになり得ます。

DOTweenのドキュメントを見た感じでは、RigidBodyがある場合に考慮してくれるようですが、CharacterControllerはノータッチなので考える必要があります。
そもそもフォーラムでは「キャラクターの移動制御にトゥイーン系ライブラリを使うな」的なことがコメントで載っており、やや手段を間違えてしまった感があります。ただ、それを差し置いてもDOTweenSequenceが便利なので、勢いで実装しちゃいます。
また、実際に使用する場合はOnTriggerEnter()あたりで判定することになるかと思うので、そのあたりも併せて作ってしまいます。

using DG.Tweening;
using System.Collections.Generic;
using UnityEngine;

public class AutoMovementPoint : MonoBehaviour {

    [SerializeField]
    protected List<RouteParameter> routes;

    protected void OnTriggerEnter(Collider c) {
        GameCharacter character = c.GetComponent<GameCharacter>();

        if (character is IAutoMovement) {
            doMovement(c.gameObject);
        }
    }

    private void doMovement(GameObject moveObject) {
        IAutoMovement autoMovement = moveObject.GetComponent<IAutoMovement>();

        // シーケンス生成
        Sequence sequence = DOTween.Sequence();

        // シーケンス開始時の処理登録
        sequence.OnStart(() => autoMovement.onMovementStart());

        // 各ポイントへの移動処理登録
        foreach (RouteParameter route in routes) {
            sequence.SetDelay(route.delay);
            sequence.Append(
                DOTween.To(
                    () => moveObject.transform.position,
                    v => {
                        Vector3 velocity = (v - moveObject.transform.position) * Time.deltaTime * 1000;
                        autoMovement.setMovementVelocity(velocity);
                    },
                    route.movePoint.position,
                    route.time
                )
            );
        }

        // シーケンス終了時の処理登録
        sequence.OnComplete(() => autoMovement.onMovementEnd());
    }
}

ポイントはDOTween.To()内の処理で、第2引数内でざっくりとvelocityっぽい値を計算し、その値をインタフェースを通して各キャラクターのvelocityにセットします。
ここでいうvには「第1引数から第3引数への移動を補完するVector3型」が入ります。言い換えると「次に移動する場所」になるので、その値から移動前のキャラクター位置を減算し、Time.deltaTimeを掛けることで求めることができます。
ただ、この計算結果だけではほとんど動かないため、適当に1000を掛けて動くレベルの値にしてしまいました。

あとは移動先パラメータに合わせ、foreach等を使ってシーケンスを組み立てます。ループ処理で容易に組み立てられるのはDOTweenの長所ですね。

本当は移動方法の定義やジャンプを入れたかったのですが、思った以上に実現が難しかったです。また、ポイント間の距離によって移動速度に違いが出るのも微妙な気がしてきましたが、今回はこれで妥協します。
というか上の移動処理を考えるだけで1週間くらい掛かってしまったので、ブログ的にも妥協してしまいました (小声

あとがき

そんなわけで、キャラクターの自動移動処理について考えてみました。
ゲームに自然に搭載されている機能ほど見落としがちですが、こういったコーディングもサクッとできるようになりたいですね。
今回のDOTweenでの実装もそうですが、ある程度難しい課題をこなした方が力になるので、今後もあれこれ考えてみます!

down

コメントする