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;
}

地面に立っているかどうかが分かれば、Jumpを開始する条件を工夫する事が出来ます。

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

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

地面検出のレイキャストのイメージです。カプセルコライダの中から下へレイキャストを行って、この図の赤い矢印が何かに衝突すれば地面に立っていると判定します。レイキャストの距離(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にします。

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

14 Comments

  • Jump関数のif文の条件を(state && isOnGround)にしない限り、レイキャストを行ったとしてもダブルジャンプが解消されないと思います。また直していただけると幸いでございます。あとすべてコーディングしましたが、スペースキーを押してもジャンプしないという結果になりましたので、レイキャストを見直してくださるとありがたいです。宜しくお願い致します。

    • jmp says:

      確かに、isOnGroundを使うジャンプの実装はチュートリアルの後半に登場しますが、使い方の説明がありませんでした。この辺の文章とコードを加えておきました。

      ジャンプができない原因はいろいろ考えられます。以下を確認していただければジャンプできるはずです。

      • 1回目のチュートリアル通りにJumpアクションが正しく設定されているか?
      • PlayerInputコンポーネントのBehaviorが「SendMessages」または「BroadcastMessages」になっているか?
      • 2つのOnJumpメソッドが正しく実装されているか?
      • レイキャストの実装を追加した時点で飛べなくなったら、実装が正しくて、地面が正しく検出されているか?
      • Unity初心者 says:

        ご返答ありがとうございます。プロジェクトを再起動したところ、しっかり1回ジャンプになりました。Unity側のバグがあったのかもしれないです。引き続き移動についてチュートリアルを進めていこうと思います。ありがとうございました!

  • Uniy新米 says:

    grabityScaleが定義されていないというエラーですが、どこで定義しているか教えてもらってもいいですか?
    見逃してるだけだと思うのですが、自分では見つけられないので教えてください。

  • Unity新米 says:

    gravityScaleが定義されていないというエラーが出ているのですが、どこで定義されているか教えてもらってもいいでしょうか。
    見逃しているだけだと思うのですが、自分ではどうしても見つけられないので教えてほしいです。

    • jmp says:

      gravityScaleはCharacterクラスのパラメータで、このチュートリアルではクラス定義の冒頭にあります。「ジャンプしている間の重力の変更を調整できるパラメータを追加します。」という説明があるところのコードで追加しています。

  • unity beginner says:

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

    「isJumping」ではなく、「isOnGround」ではないでしょうか?
    初めてたてなので自分が間違ってるかもしれないですが、、
    見直してもらえませんか?

    • jmp says:

      「isJumping」はジャンプをしているかどうかの状態を記憶しています。ここでジャンプの開始が成功したのでtrueにします。「isOnGround」は地面に立っているかどうかの情報で、その判定が別のタイミングで行われていて、ここでは読み取りますが、値を変えません。

  • beginner says:

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

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

    個々の部分で「重大度レベル コード 説明 プロジェクト ファイル 行 抑制状態
    エラー CS0103 現在のコンテキストに 'gravityScale' という名前は存在しません Assembly-CSharp D:\unity\TPS Sample\Assets\ThirdPersonController\Character.cs 79 アクティブ
    」と表示されます

    解決策を教えてください

  • tanaka says:

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

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

    個々の部分で「重大度レベル コード 説明 プロジェクト ファイル 行 抑制状態
    エラー CS0103 現在のコンテキストに 'gravityScale' という名前は存在しません Assembly-CSharp D:\unity\TPS Sample\Assets\ThirdPersonController\Character.cs 79 アクティブ
    」と表示されます

    解決策を教えてください

  • Unity初心者 says:

    はじめまして。
    約1年前の内容へのコメントで申し訳ありません。
    この6月からゆっくりとUnityの学習を進めております初心者です。
    ここまでは順調に学習できました、たいへん有難いと思っております。
    最後の重量を計算に入れるくだりで実行すると、gravityScaleが無い、という旨のエラーが出てしまいます。
    たしかにコードの中ではどこにも設定していないように見受けられますが、エラーの回避方法などありますでしょうか?
    Unityのバージョンは 2020.3.3of1 を使用しております。

  • pimen says:

    なぜbool CheckForGroundの
    floot extentで Mathf.Max を使うのですか?

  • unity player says:

    gravityScaleがありません。とエラーが出るのですがjumpGravityScaleと同じものでしょうか?

Leave a Reply

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

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