yu00’s blog

プログラミングに関する備忘録です

UnityのRigidbodyでCharacterController風動作を作る

はじめに

UnityのCharacterControllerは便利ですが、
リアルな挙動ができるなどRigidbodyを使う方が自由度が高く
利点がある場合もあります。
そこでRigidbodyを使いCharacterControllerの機能をいくつか実装することで
両方の利点を生かす方法を考えます

方針

  • プレイヤーの入力で水平移動・ジャンプ・y軸回転ができるようにする
    • 水平移動は以下実装にすることでリアルな挙動を目指す
      • AddForce, ForceMode.Forceを使う
      • PID制御のP制御を行い目標速度に追従するようにする
    • ジャンプは以下実装にすることでリアルな挙動を目指す
      • AddForce, ForceMode.Impulseを使う
      • 接触面の角度を調べて地面かどうか判定する
      • 地面に接触していない時はジャンプしない
    • y軸回転はtransform.Rotateを使い、リアルな挙動は諦める
    • RigidbodyのFreeze RotationのX・Y・ZをONにすることで壁にぶつかっても回転しないようにする
  • 以下機能を実装することで坂を登れる(落ちない)ようにする
    • 接触面の角度を調べて坂かどうかを判定する
    • 坂に接触している時は重力を反転する

ソースコード

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// プレイヤーの入力
/// </summary>
public class PlayerInputs : MonoBehaviour
{
    public float HorizontalAxis = 0;
    public float VerticalAxis = 0;
    public float RotateAxis = 0;
    public bool JumpButton = false;

    private void Update()
    {
        JumpButton = Input.GetButton("Jump");
        HorizontalAxis = Input.GetAxis("Horizontal");
        VerticalAxis = Input.GetAxis("Vertical");
        RotateAxis = GetRotateAxis();
    }

    private float GetRotateAxis()
    {
        float ret = 0;
        if (Input.GetKey(KeyCode.E)) ret = 1;
        else if (Input.GetKey(KeyCode.Q)) ret = -1;
        return ret;
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// キャラクターに重力を与える
/// </summary>
public class GravityCharacterController : MonoBehaviour
{
    public float Gravity = -15; // 重力
    public bool IsEnabledGravity; // 重力有効かどうか
    private Rigidbody CharacterRigidbody;
    private void Start()
    {
        CharacterRigidbody = GetComponent<Rigidbody>();
    }
    void FixedUpdate()
    {
        if (IsEnabledGravity)
        {
            CharacterRigidbody.AddForce(0, Gravity, 0, ForceMode.Acceleration);
        }
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// プレイヤーの入力をもとにキャラクターを動かす
/// </summary>
public class PlayerCharacterController : MonoBehaviour
{
    public float JumpForce; // ジャンプ力
    public float MoveSpeed; // 水平移動の目標速度
    public float RotateSpeed; // 回転速度
    public float Kp; // P項係数

    public Vector3 Speed;

    PlayerInputs PlayerInputs;
    GroundChecker GroundChecker;
    Rigidbody CharacterRigidbody;

    Vector3 JumpForceVec = Vector3.zero;
    Vector3 MoveDirectionVec = Vector3.zero;
    Vector3 JumpDirectionVec = Vector3.zero;
    Vector3 MoveSpeedErr = Vector3.zero;

    private void Start()
    {
        PlayerInputs = GetComponent<PlayerInputs>();
        CharacterRigidbody = GetComponent<Rigidbody>();
        GroundChecker = GetComponent<GroundChecker>();
    }

    void FixedUpdate()
    {
        Move();
        Jump();
    }

    void Jump()
    {
        if (PlayerInputs.JumpButton && GroundChecker.IsGrounded)
        {
            JumpDirectionVec.x = MoveDirectionVec.x;
            JumpDirectionVec.z = MoveDirectionVec.z;
            JumpDirectionVec.y = 1.0f - (Mathf.Abs(JumpDirectionVec.x) + Mathf.Abs(JumpDirectionVec.z));
            JumpForceVec = JumpDirectionVec * JumpForce;
            CharacterRigidbody.AddForce(JumpForceVec, ForceMode.Impulse);
        }
    }

    void Move()
    {
        MoveDirectionVec = transform.forward * PlayerInputs.VerticalAxis + transform.right * PlayerInputs.HorizontalAxis;        
        Vector3 tgtMoveSpeed = MoveDirectionVec * MoveSpeed;
        MoveSpeedErr = tgtMoveSpeed - CharacterRigidbody.velocity;
        MoveSpeedErr.y = 0;
        Vector3 force = MoveSpeedErr * Kp;
        CharacterRigidbody.AddForce(force, ForceMode.Force);
        float rotateSpeed = RotateSpeed * PlayerInputs.RotateAxis;
        CharacterRigidbody.transform.Rotate(0, rotateSpeed * Time.fixedDeltaTime, 0);

        Speed = CharacterRigidbody.velocity;
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// キャラクターの当たり判定を行う
/// </summary>
public class CharacterCollider : MonoBehaviour
{
    SlopeChecker SlopeChecker;
    GroundChecker GroundChecker;
    private void Start()
    {
        SlopeChecker = GetComponent<SlopeChecker>();
        GroundChecker = GetComponent<GroundChecker>();
    }
    private void OnCollisionStay(Collision collision)
    {
        Vector3 collidedNormal = collision.contacts[0].normal;

        Vector3 v = Vector3.zero;
        v.x = collidedNormal.x;
        v.z = collidedNormal.z;
        v = v.normalized;
        float cosTheta = Vector3.Dot(collidedNormal, v);

        SlopeChecker.CosTheta = cosTheta;
        GroundChecker.CosTheta = cosTheta;
        SlopeChecker.IsCollided = true;
        GroundChecker.IsCollided = true;
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 坂のチェックを行う
/// </summary>
public class SlopeChecker : MonoBehaviour
{
    public float MinSlopeCos = 0.1f; // 坂と判定する最小のcos
    public float MaxSlopeCos = 0.9f; // 坂と判定する最大のcos
    public float GravityGain = 1e-3f; // 反転重力に掛ける係数
    public bool IsCollided; // 接触しているかどうか
    public float CosTheta; // 接触面の角度cos
    Rigidbody CharacterRigidbody;
    GravityCharacterController GravityCharacterController;
    Vector3 Force;

    private void Start()
    {
        CharacterRigidbody = GetComponent<Rigidbody>();
        GravityCharacterController = GetComponent<GravityCharacterController>();
    }

    void FixedUpdate()
    {
        if (!IsCollided)
        {
            GravityCharacterController.IsEnabledGravity = true;
            return;
        }
        IsCollided = false;
        
        float absCosTheta = Mathf.Abs(CosTheta);
        if ((absCosTheta >= MinSlopeCos) && (absCosTheta <= MaxSlopeCos))
        {
            Force.y = -GravityCharacterController.Gravity * GravityGain;
            CharacterRigidbody.AddForce(Force, ForceMode.Acceleration);
            GravityCharacterController.IsEnabledGravity = false;
        }
        else
        {
            GravityCharacterController.IsEnabledGravity = true;
        }
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 地面のチェックを行う
/// </summary>
public class GroundChecker : MonoBehaviour
{
    public float MaxGroundCos = 0.5f; // 地面と判定する最大のcos
    public bool IsCollided; // 接触しているかどうか
    public float CosTheta; // 接触面の角度cos
    public bool IsGrounded; // 地面に接触しているかどうか

    void FixedUpdate()
    {
        if (!IsCollided)
        {
            IsGrounded = false;
            return;
        }
        IsCollided = false;
        float absCosTheta = Mathf.Abs(CosTheta);
        if (absCosTheta <= MaxGroundCos) IsGrounded = true;
        else IsGrounded = false;
    }
}

Unityプロジェクトは以下にあります
https://github.com/hide00310/Unity_RigidbodyCharacterController

バージョン

Unity : 2019.4.31f1