Rayと法線ベクトルを使った坂道移動の実装方法

坂道の処理は難しい

アクションゲームで特に悩んだのが「坂を上る処理」だったため、
今回あらためて有料アセットなどを調査し、その結果をまとめてみました。
単純に坂を上るだけであれば簡単ですが、物理演算を使用した場合に、坂を上っている最中に移動を止めてしまうと上方向に跳ねてしまう不自然な挙動が発生するので、そこで今回は、その挙動を回避する処理を書きました。

以下の方法以外にも対処方法はあると、思うので個人のゲームに合ったやり方を探すのがいいかもしれません。

Rayとは何か

ある一定の方向にビームのような線を飛ばして、その線が何に当たったのかを取得ができます。例えば、特定のレイヤーのオブジェクトがプレイヤーの右側にあるとするなら、右方向にRayを飛ばすことでそのオブジェクトを検知することができます。

Rayの使い方

引数は「開始点」「方向」「長さ」「判定したいレイヤー」の4つです。
以下のコードは、プレイヤーの中心から下方向にrayLengthの長さのRayを飛ばして、groundLayerMaskで指定したレイヤーとの衝突を判定しています。

RaycastHit2D hit = Physics2D.Raycast(center, Vector2.down, rayLength, groundLayerMask);

坂を上る処理の実装

この処理ではプレイヤはシンプルな横方向の移動のみできるようにしています。
そしてRayを使用して、坂道に到達した際に、法線(hit.normal)と上向きベクトル(上向きの垂直ベクトル)を取得して、その2つのなす角度(groundAngle)を取得して坂道かどうかを判定します。坂道なら法線に対して垂直の方向にプレイヤーを移動する処理を実行しています。

 

ペンギンさん
法線とは坂道に対して垂直なベクトルのことだよ!

各種ベクトルの表示

  1. 緑の線が法線のベクトル
  2. 青の線が上向きの垂直ベクトル
  3. 黄色の線が法線に対して垂直なベクトル

坂道用のスクリプト

using UnityEngine;

public class SimpleSlopeMover : MonoBehaviour
{
    [Header("Movement")]
    [SerializeField] private float moveSpeed = 5f;       // 移動スピード
    [SerializeField] private float maxSlopeAngle = 45f;  // 登れる坂道の最大角度

    [Header("Ground Detection")]
    [SerializeField] private float groundCheckDistance = 1.1f; // 地面チェックの距離
    [SerializeField] private LayerMask groundLayerMask = 1;    // 地面のレイヤー

    // コンポーネント参照
    private Rigidbody2D rb;
    private CapsuleCollider2D capsuleCollider;

    // 地面チェック用の変数
    private bool isGrounded = false;
    private Vector2 groundNormal = Vector2.up;
    private float groundAngle = 0f;
    private bool isOnSlope = false;

    // 入力
    private float horizontalInput = 0f;

    void Start()
    {
        rb = GetComponent();
        capsuleCollider = GetComponent();
    }

    void Update()
    {
        // 入力を取得
        horizontalInput = Input.GetAxis("Horizontal");
    }

    void FixedUpdate()
    {
        CheckGround();    // 地面チェック
        HandleMovement(); // 移動処理
    }

    // 1つのレイで地面をチェック
    void CheckGround()
    {
        Vector2 bounds = capsuleCollider.bounds.size;
        Vector2 center = capsuleCollider.bounds.center;

        // キャラクターの中心から下に向かってレイを出す
        float rayLength = bounds.y / 2 + groundCheckDistance;
        RaycastHit2D hit = Physics2D.Raycast(center, Vector2.down, rayLength, groundLayerMask);

        if (hit.collider != null)
        {
            isGrounded = true;
            groundNormal = hit.normal; //地面の法線ベクトル 
            groundAngle = Vector2.Angle(Vector2.up, groundNormal); //対象物の角度
            isOnSlope = groundAngle > 0.1f && groundAngle <= maxSlopeAngle;
        }
        else
        {
            isGrounded = false;
            groundNormal = Vector2.up;
            groundAngle = 0f;
            isOnSlope = false;
        }
    }

    // 移動処理
    void HandleMovement()
    {
        if (isGrounded)
        {
            if (isOnSlope)
            {
                // 坂道での移動:地面の法線に垂直な方向に移動
                Vector2 slopeDirection = new Vector2(groundNormal.y, -groundNormal.x).normalized;

                // 移動方向を決定(入力に応じて坂道に沿って移動)
                Vector2 moveDirection = slopeDirection * horizontalInput;

                // 坂道の角度に応じて速度を調整
                float slopeSpeedMultiplier = Mathf.Lerp(1f, 0.7f, groundAngle / maxSlopeAngle);

                rb.linearVelocity = new Vector2(
                    moveDirection.x * moveSpeed * slopeSpeedMultiplier,
                    moveDirection.y * moveSpeed * slopeSpeedMultiplier
                );
            }
            else
            {
                // 平地での移動:横方向のみ
                rb.linearVelocity = new Vector2(horizontalInput * moveSpeed, rb.linearVelocity.y);
            }
        }
    }
}

オブジェクトの各種設定

  • playerに上記のスクリプトをアタッチする
  • playerにCapsuleCollider2DとRigidbody2Dをアタッチする
  • playerのRigidbody2Dの回転の項目を固定にする
  • 地面のオブジェクトに地面用のレイヤー(Ground)などを設定

実行結果

プレイヤーは坂道に入ると、坂道に沿って移動をします。同様に坂道を下るときも坂道に沿って移動をします。

まとめ

今回はRayを使用して法線に対して垂直方向に移動するという処理を試してみました。
Rayの数を増やすとより正確に坂道の状態を把握できるので、個人のゲームの精度に応じて変えるとよいですね。
坂道の処理はかなり難しく感じていたので、この方法で一旦作りながら色々試していこうと思っています。

最新情報をチェックしよう!
>
CTR IMG