徳島ゲーム開発ごっこ 技術ブログ

ゲームを作るために役に立ったり立たなかったりする技術を学んでいきます!

【Unity】ExampleProjectから見る地面の判定

 こんにちは、うら干物です。

 Unityでゲームを作り始めようとする際に、頭の中でこういうものを作りたいなという想像図はあるのですが、Unityではどのように作るのかわからないことがあります。
 その際、情報をネット上から探すこともありますが、UnityのExampleProjectを参考にすることもあります。
 今回はアクションゲームでよく使う地面の判定部分をExampleProjectから見ていきたいと思います。

f:id:urahimono:20160829105559p:plain


この記事にはUnity5.4.0f3を使用しています。

CharacterFirstPersonシーン

 今回はCharacterFirstPersonシーンを参考にします。

f:id:urahimono:20160829105646p:plain

 ExampleProjectの各シーンを見ていく際に、どのGameObjectから調べていけば迷うことがありますが、CharacterFirstPersonシーンの場合はHierarchyのルート上にあるGameObjectは5つあり、各GameObjectの内容は以下のようになります。

  • RigidBodyFPSController
    プレイヤーとカメラの制御オブジェクト群。
  • Helpers
    Exampleシーン用のUI制御オブジェクト群。
  • GeometryStatic
    地面や障害物オブジェクト群
  • Lights
    ライトオブジェクト群。
  • UI
    モバイル用のコントローラーオブジェクト群。

 そのため、スクリプトとして見ていきたいのはRigidBodyFPSControllerのGameObjectの中を調査していけばよさそうです。
 その中でもRigidbodyFirstPersonControllerコンポーネントを見ていきます。

f:id:urahimono:20160829105652p:plain

Physics.SphereCast()について

 RigidbodyFirstPersonControllerコンポーネントの中を見ていきましょう。
 その中でGroundCheck()関数が地面の判定を行っているようですね。

private void GroundCheck()
{
    m_PreviouslyGrounded = m_IsGrounded;
    RaycastHit hitInfo;
    if (Physics.SphereCast(transform.position, m_Capsule.radius * (1.0f - advancedSettings.shellOffset), Vector3.down, out hitInfo,
                           ((m_Capsule.height/2f) - m_Capsule.radius) + advancedSettings.groundCheckDistance, Physics.AllLayers, QueryTriggerInteraction.Ignore))
    {
        m_IsGrounded = true;
        m_GroundContactNormal = hitInfo.normal;
    }
    else
    {
        m_IsGrounded = false;
        m_GroundContactNormal = Vector3.up;
    }
    if (!m_PreviouslyGrounded && m_IsGrounded && m_Jumping)
    {
        m_Jumping = false;
    }
}

 処理を見る限り、下向きのレイを飛ばしてレイがヒットすれば地面についている、着地している扱いにしているようです。
 ではレイを飛ばす処理をしている、Physics.SphereCast()の部分について調べていきましょう。

docs.unity3d.com

 引数が多くて大変ですが、どれぐらいの大きさの球を、どれぐらいの距離に、どれと判定をとるの、的なことを渡します。
 上記のGroundCheck()では引数にどんな値が割り当てられているかを見ていきます。

Physics.SphereCast(
    transform.position,
    m_Capsule.radius * (1.0f - advancedSettings.shellOffset),
    Vector3.down,
    out hitInfo,
    ( ( m_Capsule.height / 2f ) - m_Capsule.radius ) + advancedSettings.groundCheckDistance,
    Physics.AllLayers,
    QueryTriggerInteraction.Ignore )
  • origin ... 球形が通過を開始する地点の中心
    レイを飛ばす地点にはtransform.positionを渡しています。
    これはわかりやすいですね。
  • radius ... 球形の半径
    なんか複雑な計算式の値が渡されていますね。
    m_CapsuleはGameObjectについているCapsuleColliderです。
    CapsuleColliderradiusをそのまま使わずに、advancedSettings.shellOffsetで球の大きさを調節できるようにしているようです。
    値を足し引きとして使うのではなく、値の割合で拡縮するように設定するみたいです。
    1.0f - advancedSettings.shellOffsetと記述されているので、小さくするのを前提でしょうか。
    エディタ上でadvancedSettings.shellOffsetに負数を与えることもできちゃうようですけど。
    シーン上では0が指定されています。
  • direction ... 球を通過させる方向
    レイと飛ばす方向にはVector3.downを渡しています。
    地面との当たり判定を調べたいわけですから、下方向に飛ばせばいいわけですね。
  • hitInfo ... もし true が返されると hitInfo にはコライダーのヒットに関する詳細情報が含まれるようになります。
    これは返り値と使用しますので、RaycastHitクラスとして宣言した変数を渡してあげればOKです。
    のちに当たったオブジェクトの法線を取得するのに使用します。
  • maxDistance ... キャストの最大の長さ
    これまた複雑な計算式を渡していますね。
    1つ1つ順番に見ていきましょう。
    m_Capsule.heightCapsuleColliderの高さです。そのまま渡すとこうなります。

    • m_Capsule.height
      f:id:urahimono:20160829105730p:plain CapsuleCollidercenterは(0,0,0)になっていますので、カプセルの基準点が中心になっています。
      そのためそのままm_Capsule.height渡すと半分ずれてしまうため、渡す値を2で割ります。

    • m_Capsule.height / 2f
      f:id:urahimono:20160829105736p:plain まだ球半分ずれています。
      レイとして判断する球の部分は、maxDistanceとして指定した位置を中心に円を作ります。
      そのため球の半径分引いてあげる必要があります。

    • ( m_Capsule.height / 2f ) - m_Capsule.radius
      f:id:urahimono:20160829105744p:plain advancedSettings.groundCheckDistanceはエディタ上で調整する値です。
      シーンでは0.1が設定されています。

  • layerMask ... レイヤーマスク はレイキャストするときに選択的に衝突を無視するために使用します。
    衝突するレイヤーをマスクで指定します。
    今回指定されているのはPhysics.AllLayersなのですべてのレイヤーを判定します。

  • queryTriggerInteraction ... トリガーに設定されているものも検索対象にするか
    トリガータイプのコリジョンを対象とするかです。
    QueryTriggerInteraction.Ignoreを指定しているのでトリガーは対象にしません。

 作るゲームによってのパラメータの渡し方は異なると思いますが、上記の渡し方が基本となると思います。

 ちなみにUnity技術者のバイブル、テラシュールブログでもPhysics.SphereCast()についての記事もあります。是非一読を。

tsubakit1.hateblo.jp

Vector3.ProjectOnPlane()

 地面に着地しているかを判定をPhysics.SphereCast()で取得することができました。
 これで地面にいる間のみジャンプができるなど、地上・空中で処理を分けることが出来るようになったと思います。

 これに加えて、RigidbodyFirstPersonControllerコンポーネントでは坂道の対応も行われています。
 坂道を進む際に移動の進行方向を坂道に合わせておかないと、登る際はコリジョンの判定任せで登ことになり、降りる際は空中方向に移動してしまうため、ジャンプしたような挙動になってしまいます。

 RigidbodyFirstPersonControllerコンポーネントではVector3.ProjectOnPlane()を利用して対応しています。

private void FixedUpdate()
{
    GroundCheck();
    Vector2 input = GetInput();

    if( ( Mathf.Abs( input.x ) > float.Epsilon || Mathf.Abs( input.y ) > float.Epsilon ) && ( advancedSettings.airControl || m_IsGrounded ) )
    {
        // always move along the camera forward as it is the direction that it being aimed at
        Vector3 desiredMove = cam.transform.forward * input.y + cam.transform.right * input.x;
        desiredMove = Vector3.ProjectOnPlane( desiredMove, m_GroundContactNormal ).normalized;

        desiredMove.x = desiredMove.x * movementSettings.CurrentTargetSpeed;
        desiredMove.z = desiredMove.z * movementSettings.CurrentTargetSpeed;
        desiredMove.y = desiredMove.y * movementSettings.CurrentTargetSpeed;
        if( m_RigidBody.velocity.sqrMagnitude <
            ( movementSettings.CurrentTargetSpeed * movementSettings.CurrentTargetSpeed ) )
        {
            m_RigidBody.AddForce( desiredMove * SlopeMultiplier(), ForceMode.Impulse );
        }
    }
}

docs.unity3d.com

 desiredMoveは入力された移動の進行方向です。
 m_GroundContactNormalには、Physics.SphereCast()で取得したhitInfonormal(法線)を利用しています。地面にいないときはVector3.upを使って上方向の法線を渡しています。
 この2つの値をVector3.ProjectOnPlane()に渡すことで返される向きを使うことで地面の方向に合わせた移動方向に変換されます。
 上記ではnormalizedを使って正規化も行われているようです。

 Unityでアクションゲームを作る際に役に立つかもしれません。