kedzkiestの日記

普段の発見や開発の進捗について書いていきます。

Unityでキャラクターにトレイル上を移動させる

トレイルの上を走るユニティちゃん。

はじめに

3Dのゲームで、プレイヤーに思う存分フィールドを動き回ってもらうのではなく、決められたルートの上だけを動いてほしい場合があります。

例えばスーパーマリオブラザーズシリーズのステージ選択画面や、ヘブンバーンズレッドの散策フェーズでは、プレイヤーはあらかじめ決められた道(トレイル)の上だけを好きに移動できます。
youtu.be
youtu.be

今回はUnityを使って、トレイル上でのみプレイヤーが移動できるシステムを実装してみました。

完成したもの

github.com
youtu.be
シーンビューとゲームビューを同時に見せているため、キャラクターがトレイル上を移動している様子が分かりやすいと思います。カメラの挙動はヘブバンをちょっと意識しました。

動画の途中でトレイルをループさせる設定に切り替えましたが、始点と終点が同じであっても上手く動作しています。強いて言えば経路点(waypoint)のつなぎ目でキャラクターがカクついているかもしれません。

どのように実現したか

CinemachineのDolly Track with Cartを使用しました。Dolly Trackでルートを指定するとCartがその上を移動できるようになりますが、キャラクターにCartを追従させることで、キャラクターがトレイル上を動いているように見えます。基本的な方針はこちらの記事で紹介されていますので、ぜひご覧ください。
tsubakit1.hateblo.jp
上の記事と異なる点として、今回はプレイヤーがキャラクターを操作することを想定しているため、TimelineではなくC#スクリプトを使用しています。また、トレイル上を移動させたいキャラクターをCartの子オブジェクトにしていません。

使用したC#スクリプト

using UnityEngine;
using Cinemachine;

public class PlayerController : MonoBehaviour
{
    [SerializeField] private CinemachinePath path;
    [SerializeField] private CinemachineDollyCart cart;

    [SerializeField] private float moveSpeed;
    [SerializeField] private float rotateSpeed;
    [SerializeField] private float howCloseCanReachPathEnd;
    private Animator anim;

    void Start()
    {
        anim = GetComponent<Animator>();
    }

    void LateUpdate()
    {
        if (Input.GetKey(KeyCode.LeftArrow) && Input.GetKey(KeyCode.RightArrow))
        {
            anim.SetBool("isRunning", false);
            return;
        }

        Rotate();
        Move();
    }

    [HideInInspector]
    public Quaternion rot;

    private void Rotate()
    {
        // active character rotation using keyinput
        if (Input.GetKey(KeyCode.RightArrow))
        {
            rot = Quaternion.Euler(0, cart.transform.rotation.eulerAngles.y, 0);
            transform.rotation = Quaternion.Slerp(transform.rotation, rot, rotateSpeed * Time.deltaTime);
        }

        if (Input.GetKey(KeyCode.LeftArrow))
        {
            rot = Quaternion.Euler(0, cart.transform.rotation.eulerAngles.y, 0) * Quaternion.Euler(0, 180, 0);
            transform.rotation = Quaternion.Slerp(transform.rotation, rot, rotateSpeed * Time.deltaTime);
        }
    }

    private void Move()
    {
        // passive character moving along the path(cart)
        transform.position = cart.transform.position;

        // active character moving using keyinput
        if (Input.GetKey(KeyCode.RightArrow))
        {
            if (!path.m_Looped && Mathf.Abs(path.PathLength - cart.m_Position) < howCloseCanReachPathEnd)
            {
                anim.SetBool("isRunning", false);
                return;
            }

            cart.m_Position += moveSpeed * Time.deltaTime;
            anim.SetBool("isRunning", true);
            return;
        }

        if (Input.GetKey(KeyCode.LeftArrow))
        {
            if (!path.m_Looped && Mathf.Abs(cart.m_Position) < howCloseCanReachPathEnd)
            {
                anim.SetBool("isRunning", false);
                return;
            }

            cart.m_Position -= moveSpeed * Time.deltaTime;
            anim.SetBool("isRunning", true);
            return;
        }

        anim.SetBool("isRunning", false);
    }
}

色々書いてありますが、アニメーションの制御やキャラクターの回転のための記述も含まれているだけで、特に大事なのはMove関数の中のこの部分です。

// このスクリプトがアタッチされているキャラクターにCartを追従させる
transform.position = cart.transform.position;
// Cartの位置を更新する
cart.m_Position += moveSpeed * Time.deltaTime;

キャラクターをCartの子オブジェクトにしている場合は一行目が不要です。cart.m_PositionはDolly Trackによって指定されたパスのどの位置にCartがあるかを示しており、0からパスの長さの間で変動します。キー入力によってCartがパス上を動く->キャラクターがそれを追従する->キャラクターがパス上を動いているように見える、というような仕組みです。

おわりに

トレイル上を動くキャラクターの挙動を実現できました。当初の方針としてはシーン上にwaypointとなるゲームオブジェクトを配置し、それらを配列に格納してインデックスを更新しながらそれらを経由していくことを考えていましたが、Cinemachineに便利な機能があって良かったです。

ライセンス表記
© Unity Technologies Japan/UCL


おまけ:ユニティちゃん軍団

youtu.be
(ちゃんと隊列みたいに移動するのも再現してみたい)

2022/11/30 追記

Unity 2022.1からはSpline機能が公式パッケージになります。
youtu.be
そこで、Dolly Cartよりも簡単にキャラクターにトレイル上を移動させられそうだったので試してみたのですが、Dolly CartにおけるCartのようにパス上の特定の点を示すものがないので、パスに沿ってキャラクターをインタラクティブに動かすことが出来ませんでした。プレイヤーが操作する必要がないなら(例えば経路上を移動するNPC)、上の動画で紹介されている「スプラインに沿ったアニメーション」を使うことで実現できそうです。

せっかくなので以前作ったシーンに少しだけ手を加えてみました。
youtu.be
Unityのバージョンは2022.1.23f1です。

実際にキャラクターが移動する経路はCinemachineのDolly Cartで作成されていますが、CinemachinePathに限りなく近い経路をスプラインでも作成し、スプラインに沿って石のオブジェクトを配置した後、周囲の環境を全く描画しないことで、石を渡ってキャラクターが空中を移動しているように見せています。浮いている石はプレイヤーが近くに来ると少し沈むようにC#コードを書きました。

Spline機能はまだ登場したばかりでドキュメントもあまり充実していないようなので、「Dolly CartにおけるCartのようにパス上の特定の点を示すものがない」と書きましたが、後からそれが間違いだったと分かり、プレイヤー操作によるトレイル上の移動もSplineを使って実現できるようになるかもしれません。今後に期待ですね!