Unityで三人称視点のキャラクターコントローラを作ってみよう(第2弾)

前のチュートリアルで三人称視点ゲームのキャラクターの準備を行いました。ここではそのキャラクターに追加した「Character」スクリプトの編集をして簡単に操作できるようにします。

スクリプトの準備

Unityのスクリプトを書くときによくある工程ですが、必要なコンポーネントの参照を取得します。

public class Character : MonoBehaviour
{
    Animator animator;
    Rigidbody rb;
    CapsuleCollider capsuleCollider;

    // Start is called before the first frame update
    void Start()
    {
        animator = GetComponent<Animator>();
        rb = GetComponent<Rigidbody>();
        capsuleCollider = GetComponent<CapsuleCollider>();
    }

これでは問題はないですが、念のためにこの3つコンポーネントを「必須」にしたいです。クラス宣言の上に記述を追加します。

// 必須コンポーネントを設定する
[RequireComponent(typeof(Animator))] 
[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(CapsuleCollider))]
public class Character : MonoBehaviour

アクションを受け取る

UnityのInput Systemではプレーヤーのアクションを受け取る方法がいろいろあります。Player Inputコンポーネントの「Behavior」でその方法を変えますが、ここではデフォルトの「Send Messages」にします。

「Send Messages」にすると、入力設定アセットで登録されているアクションが起こると、Player Inputが付いているオブジェクトのすべてのスクリプトが特定のメソッドを実装することによってそのアクションに反応することができます。

ここにそのメソッド名が表示されます。すべて、「On」+アクション名になっています。キャラクターコントローラで実装が必要なのは「OnMove」と「OnJump」です。(「OnFire」もゲームによって必要ですが、このチュートリアルでは実装しません。)

これらに反応する前に、スクリプトにInput Systemの機能を読み込む必要があります。スクリプトの冒頭にusing宣言を追加します。

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

Characterクラスに二つの新しいメソッドを追加します。引数の型は「InputValue」になります。

void OnMove(InputValue inputValue)
{

}

void OnJump(InputValue inputValue)
{

}

このスクリプトはAIキャラクターも使えるようにしたいですが、独自のInputValueを生成することができないので、InputValueから必要な情報だけを取り出して、AIからも呼び出せるメソッドに渡します。

public void Move(Vector2 input)
{
    
}

void OnMove(InputValue inputValue)
{
    Move(inputValue.Get<Vector2>());
}

public void Jump(bool state)
{

}

void OnJump(InputValue inputValue)
{
    Jump(inputValue.isPressed);
}

ジャンプの実装

移動よりもジャンプの実装が簡単なので、先に行いましょう。先ほど作った「Jump」メソッドはプレーヤーまたはAIがボタンを押したか離したか、state引数で分かります。ジャンプする時は、Rigidbodyのy速度を一瞬で上に上げます。このジャンプの勢いを調整するためにパラメータを追加します。

public class Character : MonoBehaviour
{
    public float jumpSpeed = 5;

Jumpメソッドの実装はこうなります。

public void Jump(bool state)
{
    if (state)
    {
        rb.velocity += Vector3.up * jumpSpeed;
    }
}

動作を確認してみるとキャラクターが見事にジャンプしてくれます。

が、ダブルジャンプどころか、空中でも何回でもジャンプできてしまいます。まあ、こういうゲームはありかも知れませんが、普通なら地面に立っていなければジャンプはできません。

地面のチェック

2Dでも3Dでもキャラクターを制御するスクリプトを書く時は、キャラクターが地面に立っているかどうかのチェックが極めて重要です。ゲームの種類によって判定方法も若干違います。例えば、1ピクセル単位の細かな操作を要するアスレチック系のプラットフォーマーゲームなら非常に慎重に判定しなければなりません。しかし、足元が見えない一人称視点ゲームならもう少し簡単に処理してもいいでしょう。

まあ、方法は基本的に同じです。キャラクターの足元からレイキャストを行って、一定の短い距離以内なら何かに衝突すれば地面に立っていると判定します。「レイキャスト」は、ある点からレーザー光線のようにゲーム空間で直線を引いて、どこかのコライダに当たるかどうかを行う操作です。

この判定は毎フレーム行う必要がありますが、Updateを使いません。衝突判定は物理演算の一種で、Unityでは物理演算をすべて「FixedUpdate」というメソッドで行わなければなりません。こういう風に準備します。

bool isOnGround = false; // 地面に立っているかどうか

bool CheckForGround()
{
    return false;
}

void FixedUpdate()
{
    isOnGround = CheckForGround();
}

CheckForGroundメソッドで地面をレイキャストで探しますが、足元の位置を計算する必要があります。ここでは、カプセルコライダーの下の位置を足元に定義します。正しい計算方法はこうです。

bool CheckForGround()
{
    Vector3 capsuleBottom = capsuleCollider.center + Vector3.down * capsuleCollider.height * 0.5f;
    Vector3 feetPosition = transform.TransformPoint(capsuleBottom);

    return false;
}

「capsuleBottom」はオブジェクトに対して、相対的な足元の位置です。これを世界の絶対的な位置の「feetPosition」に変換します。この位置から地面を探しますが、閾値をパラメータとして追加しましょう。

public class Character : MonoBehaviour
{
    public float jumpSpeed = 10;
    public float groundDistance = 0.01f;

レイキャストはこう行います。

bool CheckForGround()
{
    Vector3 capsuleBottom = capsuleCollider.center + Vector3.down * capsuleCollider.height * 0.5f;
    Vector3 feetPosition = transform.TransformPoint(capsuleBottom);

    bool raycastHit = Physics.Raycast(feetPosition, Vector3.down, groundDistance);

    return raycastHit;
}

しかし、これでうまくいきません。誤差があるので、レイキャストは足元よりも少し上の位置から始める必要があります。Unityではレイキャストの開始点がコライダの中に入っているならそのコライダが無視されます。これで、自分自身を地面として間違えることはありません。

bool CheckForGround()
{
    Vector3 capsuleBottom = capsuleCollider.center + Vector3.down * capsuleCollider.height * 0.5f;
    Vector3 feetPosition = transform.TransformPoint(capsuleBottom) + Vector3.up * groundDistance;

    bool raycastHit = Physics.Raycast(feetPosition, Vector3.down, groundDistance * 2f);

    return raycastHit;
}

試してみると、ダブルジャンプができなくなります。キャラクターが地面に立っている時だけジャンプを開始する事ができます。

地面検出のレイキャストのイメージです。カプセルコライダの中から下へレイキャストを行って、この図の赤い矢印が何かに衝突すれば地面に立っていると判定します。レイキャストの距離(groundDistance × 2)で判定の誤差を調整します。

次の工夫は、地面として認識しないコライダの設定を可能にすることです。Unityのレイヤーを使えば簡単にできます。まず、LayerMaskパラメータを追加します。

public class Character : MonoBehaviour
{
    public float jumpSpeed = 10;
    public float groundDistance = 0.01f;
    public LayerMask groundMask = ~0;

ここの「~0」は「全てのレイヤー」を意味します。

インスペクタではこうなります。

チェックが入っているレイヤーは地面として判定されます。オブジェクトのレイヤーの設定はインスペクタの上部で行います。必要に応じて新しいレイヤーを追加する事もできます。

LayerMaskを使うようにレイキャストを少し修正します。

bool raycastHit = Physics.Raycast(feetPosition, Vector3.down, groundDistance * 2f, groundMask);

多くのゲームはこの単純なレイキャストで地面検出を行っても大丈夫ですが、アスレチック系のゲームなどでこういうシチュエーションが問題になる可能性もあります。

崖っぷち状態では地面にぎりぎり立っているのにレイキャストが地面を外れてしまうことがあります。

こういう問題が起こりそうなレベルデザインでは、別の方法で地面を検出しなければなりません。直線のレイキャストは計算が比較的軽いのでゲーム開発でよく使いますが、直線以外の形状で衝突判定を行うことができます。ここで便利なのは球体を使って判定するスフィアキャストです。レイキャストと同様に足元より少し高い位置から球体を下へ移動したら何かにぶつかるかという計算です。レイキャスト同様に、最初から重なるコライダが無視され、自分自身を地面として誤認しません。

地面判定のコードがこういう風に変わります。

bool CheckForGround()
{
    float extent = Mathf.Max(0, capsuleCollider.height * 0.5f - capsuleCollider.radius);
    Vector3 origin = transform.TransformPoint(capsuleCollider.center + Vector3.down * extent) + Vector3.up * groundDistance;

    Ray sphereCastRay = new Ray(origin, Vector3.down);
    bool raycastHit = Physics.SphereCast(sphereCastRay, capsuleCollider.radius, groundDistance * 2f, groundMask);

    return raycastHit;
}

ジャンプの高さを調整する

ジャンプの高さが固定の3Dゲームが多いですが、細かなジャンプの操作が必要な場合、2Dゲームでよく採用される仕組みを実装すると便利です。これは元々「スーカーマリオブラザーズ」で登場した賢いアプローチです。プレーヤーがジャンプボタンを押し続ける間、重力を軽減します。現実世界ではありえない現象ですが、これでプレーヤーがボタンを強く押した時に高くジャンプするという感覚になります。実際はボタンを押す強さを感知していませんが、強く押すと必然的に押す時間が長くなります。

この機能を実装するために、地面に立っているかどうかだけでなく、ジャンプしているかどうかというプライベートのプロパティが必要になります。この機能がいらない場合も、ジャンプしているかどうかの判定がアニメーションの演出に必要ですので、追加してください。

bool isOnGround = false; // 地面に立っているかどうか
bool isJumping = false; // ジャンプしているかどうか

ジャンプをしているかどうかの判定はこうです:

  1. ジャンプを開始する時にtrueに設定する
  2. プレーヤーがジャンプボタンを離した時にfalseにする
  3. キャラクターが下へ移動したら(落下している時)falseにする

まず、Jumpメソッドがこうなります。

public void Jump(bool state)
{
    if (state && isOnGround)
    {
        rb.velocity += Vector3.up * jumpSpeed;
        isJumping = true;
    }
    if (!state)
    {
        isJumping = false;
    }
}

FixedUpdateは:

void FixedUpdate()
{
    isOnGround = CheckForGround();
    if (rb.velocity.y < 0)
    {
        isJumping = false;
    }
}

ジャンプしている間の重力の変更を調整できるパラメータを追加します。

public class Character : MonoBehaviour
{
    public float jumpSpeed = 10;
    public float jumpGravityScale = 0.6f;

ここでは、ジャンプボタンを押し続けると重力が0.6倍になります。

FixedUpdateでは、ジャンプしている場合、重力と反対側の力を加えます。Rigidbodyの重量を忘れずに計算に入れておきましょう。

void FixedUpdate()
{
    isOnGround = CheckForGround();
    if (rb.velocity.y < 0)
    {
        isJumping = false;
    }

    // ジャンプの高さ調節
    if (isJumping)
    {
        rb.AddForce(Physics.gravity * rb.mass * (gravityScale - 1f));
    }
}

これで一度動作を確認して、ジャンプの高さが調整できるかどうかを確かめます。もし、ゲームでジャンプの高さ調節が不要ならjumpGravityScaleを1にします。

次のチュートリアルでキャラクターを動かします。

Leave a Reply

Your email address will not be published. Required fields are marked *

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください