Unity: Rigidbody と CapsuleCollider を使った移動とジャンプ

Unity入門 - Project Bonfire

今回は、CaracterController を使っていた移動などの処理を、Rigidbody と CapsuleCollider を使って実現する方法を紹介します。

はじめに

Rigidbody を使って移動とジャンプを実装します。最終的にはCharacterControllerを使うため、この記事の内容はなかったことにして元に戻します。完全に私の備忘録です。

ジャンプの処理に特徴があるため、簡単なジャンプの処理を追加しています。今回実装するジャンプは、2Dのアクションゲームで飛ぶような高いジャンプです。面倒なので、アニメーションなどはつけていません。

Rigidbody 版を作るにあたり、以下の2つのサイトを参考にしました。

キャラ操作をCharacterControllerからRigidbody+コライダに変更する
Unityでキャラクターを操作する時CharacterControllerを使ってきましたが、物理的な影響を得る為にRigidbody+CapsuleColli...

https://qiita.com/tomopiro/items/e691524c3c0eef16c089

詳しい説明は上記サイトにあるので、これまでの実装をどう変えればいいかに絞って書いていきます。

最終的に CharactorController にした理由は書いておきます。

  • 特にリアリティがある物理制御がしたいわけではない
  • Rigidbodyだと角度がある傾斜を登れてしまう
  • 物理的にあり得ないことをしている部分があるので、かえって変な動きになる

一言でいうなら、私がRigidbodyをうまく制御できないからです。

Rigidbody、CapsuleCollider、スクリプトの取り付け方

今回使ったスクリプトのファイルは、末尾につけておきます。

まずは、Kinght のプレハブを開きます。以下の画像のオレンジの部分をクリックしてください。

つづいて、Inspector で、CharactoController を無効化し、PlayerMove スクリプトは Remove Component で削除します。

Add Component で、Rigidbody と CapsuleCollider を追加します。

それと今回使用するスクリプト PlayerMoveRigidbody.cs も追加してください。

Rigidbody, CapsuleCollider, PlayerMoveRigidbodyを追加したところが以下の画像になります。

上記のようにパラメータを設定していきます。

Rigidbody については、Constraints の Freeze Rotaion に全部チェックを入れます。これを入れておかないとキャラクターが何かに衝突すると、くるくる回ってしまいます。

Capsule Collider の値は、CharactorController で設定していた時と同じ値を入れます。Center の Y だけ微調整しています。

PlayerMoveRigidbody のDebugText とLookTargetは、プレハブではなくインスタンスの方に設定しますので、あとから設定します。

設定が終わったら、Hirarchy の以下の四角で囲んだ部分をクリックして、プレハブを閉じます。

最後に、PlayerMoveRigidbody のDebugText と LookTarget を設定します。

以上で、Rigidbody, CapsuleCollider, PlayerMoveRigidbody の取り付けが完了しました。

動かすためには、PlayerCtrl.cs の書き換えも必要になります。添付ファイルと差し替えてもらえば動くようになります。(ジャンプしないなら、そのままでも動くかもしれません。)

スクリプトの説明

PlayerMoveRigidbody.cs

一番大きな変更点は、Update() 内で書いていた移動処理を FixedUpdate() 内で書かなければならない点です。Rigidbodyではそうするものらしいです。

そのため変数の一部の定義の場所が変わっています。

        // RIGIDBODY
        Rigidbody rbody = null;
        // 移動速度
        Vector3 velocity = Vector3.zero;
        float moveSpeed = 0f;
        bool isJumpInputted = false;

Rigidbody の操作用の変数 rbody を用意します。また、移動速度 velocity と moveSpeed がこの部分に移動しています。

bool isJumpInputted は、ジャンプのためのボタンが入力されたかどうかの判定に使います。

        void Start()
        {
            mainCamera = FindObjectOfType<Camera>();
            animator = GetComponent<Animator>();
            playerCtrl = GetComponent<PlayerCtrl>();
            status = GetComponent<PlayerStatus>();
            characterController = GetComponent<CharacterController>();

            // RIGIDBODY
            rbody = GetComponent<Rigidbody>();
        }

Start() 内で Rigidbody コンポーネントを取得します。

以下は、独自の接地判定の処理です。これは、CharacterController のときに使っていたものと全く同じです。CharacterController の isGrounded がひまひとつ安定しなかったためにつけていた処理です。

            // 独自の着地判定
            Ray ray = new Ray(transform.position + 0.18f * transform.up, -transform.up);
            isGrounded = Physics.SphereCast(ray, 0.13f, isGroundedTestRayDistance);
            status.isGrounded = isGrounded;

次は、プレイヤー向きやカメラの向きの処理部分です。

            if (playerCtrl.IsInputStickL())
            {
                // プレイヤーの向き
                Vector3 playerDirection = playerCtrl.GetPlayerDirection();


                // カメラの向きに、スティックの向きを掛け合わせ、キャラクターをスティックの方向に向かせる

<中略>

                // スティックの傾きによる速さの変化
                speedByStick = playerCtrl.GetMagunitudeStickL();

                moveSpeed = speedByStick * walkSpeed * runSpeedUp * sprintSpeedUp;
                // RIGIDBODY
                // velocity = transform.forward.normalized * moveSpeed;
            }
            else
            {
                moveSpeed = 0f;
            }

この部分は、velocity の計算を FixedUpdate() に移したのと、左スティックの入力がない場合に移動速度を 0 にしてる以外は全く同じです。velocity をFixedUpdate() に移したため、 moveSpeed を明示的に 0 にする必要があります。これをやらないと、キャラが歩き続けます。

あとは、これに続く 重力関係や、移動の処理がなくなっています。重力関係の処理は、Rigidbodyがやってくれます。移動処理は FixedUpdate()に移しました。

次は、本題の FixedUpdate() の中身です。velocity = transform.forward * moveSpeed; とやりたかったので、y方向の velocity を一時退避するために、currentVelocity を使っています。ついでに y方向のVelocity の下限を決めています。

        void FixedUpdate()
        {
            currentVelocity = rbody.velocity;
            currentVelocity.y = Mathf.Max(currentVelocity.y, maxFallSpeed);

            velocity = transform.forward * moveSpeed;
            velocity.y = currentVelocity.y;

            // 1回だけジャンプが呼ばれるようにする
            if (isJumpInputted)
            {
                isJumpInputted = false;
                velocity.y = jumpPower;
            }

            // 着地判定
            if (status.isGrounded && velocity.y < 0.1f)
            {
                status.isJumping = false;
            }

            // --- MovePosition を使う場合 ---
            // 以下の Time.deltaTime は、Time.fixDeltaTime ではなく Time.deltaTime でいいらしい
            //(同じ値が返ってくるとのこと)
            // rbody.MovePosition(transform.position + transform.forward * moveSpeed * Time.deltaTime);
            // --- verocity を直接変更する場合 ---
            rbody.velocity = velocity;
<中略>

        }

ジャンプの処理ですが、ジャンプの入力があったら1回だけ上向きの力を加えています。このコードでは、入力をUpdate() で受け付けて、ジャンプや移動の処理を FixedUpdate() で行っています。 FixedUpdate() が1回呼ばれる間に Update()が複数回呼ばれることがあり、入力が安定せず、ジャンプしたりしなかったりするのでこういう処理をしています。

次に移動の部分です。

Rigidbody を使った移動方法はいくつかあるようです。上品なのは AddForce() のような気がします。しかし上手に制御できなかったため諦めました。

実際に使ってみたのは MovePosition を使う方法と直接 velocity を書き換える方法です。どこかで、velocityを直接書き換えると物理的にあり得ない動きになるからあまり使うなって記述を見たので、最初はMovePosition を使っていました。MovePositionの書き方はコメントアウトして残しておきました。

高速で移動すると壁を突き抜けることが多々あるので、最終的には velocity を直接書き換える方法に落ち着きました。

一応、調べたことについて。

MovePosition を使う場合、Time.deltatime を使うことになります。FixedUpdate 内なので Time.fixDeltaTime でなくていいのか?と思ったのですが、どうやらTime.deltatimeが推奨らしいです。Time.fixDeltaTime と同じ値が返ってくるようです。Time.fixDeltaTimeの存在意義は不明です。(確かUnityのマニュアルに書いてあった気がするのですが、違ったかも)

壁のすり抜けについては、Rigidbody の Collision Detection を Discrete から Continuous や Continuous Dynamic にしたらいいという記述を見かけたのやってみたのですが、スピードが速いとどうやってもすり抜けが回避できませんでした。

すり抜けは、別のプロトタイプでもっと高速に移動できるようにしていた時の話で、このゲームの速度ならそのままでも問題ないと思います。ダメな時でも、Collision Detection の設定変更で何とかなるような気がします。

        public void StartJump()
        {
            isJumpInputted = true;
        }

最後に PlayerCtrl から呼び出されるジャンプ処理です。PlayerMove では、直接 velocity に上向きの力を加えていましたが、先に書いた 入力のタイミング問題を回避するため、入力があったというフラグを設定するだけになっています。

PlayerCtrl.cs

        //RIGIDBODY
        PlayerMoveRigidbody playerMoveRigidbody = null;

        void Start()
        {
            status = GetComponent<PlayerStatus>();
            playerMove = GetComponent<PlayerMove>();
            hudController = FindObjectOfType<HudController>();

            playerMoveRigidbody = GetComponent<PlayerMoveRigidbody>();
        }

クラス名を PlayerMoveRigidbody にしたので、それ用の定義を追加。PlayerMove はそのまま残してります。Knight から PlayerMove のコンポーネントを外しているので、null が入るだけです。

            if (Input.GetButtonDown("Fire_0"))
            {
                if (state != State.Jump)
                {
                    nextState = State.Jump;
                }
            }

ボタンの処理にジャンプの処理を追加。ジャンプの処理自体は最初から書いてあるので、ボタンが押されたときに nextState = State.Jump にするだけです。

        private void StartJump(State prevState)
        {
            StateStartCommon();
            status.isJumping = true;
            nextState = State.Walking;

            // PlayerMoveRigidbody の StartJump なので注意!!
            playerMoveRigidbody.StartJump();
        }

最後にジャンプの処理。playerMove.AddJumpPower() から playerMoveRigidbody.StartJump() に変更しています。

ファイル

おわりに

Rigidboy でやってみたことは、ひととおり書きました。今回書いた変更は元に戻して、次に進みます。

次回は、アニメーションをつけて遊びます。攻撃とローリングの予定です。