わたくしは今までキャラクターの当たり判定や移動の機能に関してCharacterControllerを使用してきました。
CharacterControllerコンポーネントを使用するとコライダ+移動機能が付いているので簡単にキャラクターの移動を実現出来たからです。
ですが、

や、

等の機能を作成しているうちに相手方のゲームオブジェクトの物理的な影響を受けないと実装したい機能が不十分になる事があります。
細かいアクションが売りのゲームを作るとなると物理的な影響を受けないというのはかなり不便です。
衝突の影響をそれほど考慮しなくていい時はCharacterControllerの方が便利で楽です。
作成するゲームによって変更するといいかもしれません。
CharacterControllerを削除しRigidbodyとCapsuleColliderを取りつける
CharacterControllerでキャラクターを動かしていたものをRigidbody+CapsuleColliderに変更するのは大変なんじゃないか?
と思ったんですが、やってみたらそれほどでもありませんでしたので、紹介したいと思います。
と思っていたんですが、制御がめちゃくちゃ大変でした・・・(T_T)
移動だけならともかくジャンプ機能を搭載したら全然うまくいかなくなり四苦八苦しておりました・・・・(^_^;)
今でも何らかの不具合発生しそうで怖いですが、とりあえず動くようになったので・・・・(-.-)
キャラクターのCharacterControllerを削除し、
Add Component→Physic→RigidbodyとCapsuleColliderを取りつけます。
↑のようにキャラクターのLayerをPlayerに設定します。
RigidbodyのMassに60を入れ質量を60にします。
Use Gravityにチェックを入れキャラクターに重力を働かせます。
ConstraintsでFreeze Rotationにすべてチェックを入れます。
これは外部からの物理的な影響で回転しないようにするためです。
Freeze Positionはチェックを外し、物理的な影響でキャラクターが移動するようにします。
これは外部からの影響もありますが、キーを押して自分でキャラクターを動かす時にもここでチェックがされてると動かなくなる為チェックを外す必要があります。
実際のコライダのサイズは
↑のような感じにします。
最小限のキャラクター操作スクリプト
次に最小限のCharacterControllerを使ったキャラクターの移動スクリプトを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | using UnityEngine; using System.Collections; public class RigidChara : MonoBehaviour { private Animator animator; private CharacterController cCon; private Vector3 velocity; [SerializeField] private float jumpPower = 5f; // 入力値 private Vector3 input; // 歩く速さ [SerializeField] private float walkSpeed = 1.5f; void Start () { animator = GetComponent<Animator>(); cCon = GetComponent<CharacterController>(); } void Update () { // キャラクターコライダが接地、またはレイが地面に到達している場合 if(cCon.isGrounded) { velocity = Vector3.zero; // 着地していたらアニメーションパラメータと2段階ジャンプフラグをfalse animator.SetBool("Jump", false); input = new Vector3(Input.GetAxis("Horizontal"), 0f, Input.GetAxis("Vertical")); // 方向キーが多少押されている if(input.magnitude > 0f) { animator.SetFloat("Speed", input.magnitude); transform.LookAt(transform.position + input); velocity += input.normalized * walkSpeed; // キーの押しが小さすぎる場合は移動しない } else { animator.SetFloat("Speed", 0f); } // ジャンプ if(Input.GetButtonDown("Jump") && !animator.GetCurrentAnimatorStateInfo(0).IsName("Jump") && !animator.IsInTransition(0) // 遷移途中にジャンプさせない条件 ) { animator.SetBool("Jump", true); velocity.y += jumpPower; } } velocity.y += Physics.gravity.y * Time.deltaTime; cCon.Move(velocity * Time.deltaTime); } } |
キャラクターの移動とジャンプ機能だけを搭載したスクリプトです。
スクリプトの変更点を考える
CharacterControllerを使った移動で使っていたCharacterControllerのisGroundedプロパティが使えなくなるのでCapsuleColliderが地面と接地しているかを別の個所で調べるようにします。
また、移動の処理をCharacterControllerのMoveからRigidbodyのMovePositionに変更します。
MovePositionで指定する引数は移動先のVector3を指定する必要があるので、現在地+入力値で計算します。
Rigidbodyでの移動処理はUpdateメソッドではなくFixedUpdateメソッドで行う必要があるので、移動処理だけFixedUpdateメソッド内に記述するようにします。
Rigidbodyを使った物理的な移動等はFixedUpdateメソッドで行った方がいいようです。
スクリプトで計算していたキャラクターにかける重力はRigidbodyで働かせるようにします。
RigidbodyとCapsuleColliderを使った移動に切り替えたスクリプト
それではさきほどの変更点を考慮してスクリプトを修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | using UnityEngine; using System.Collections; public class RigidChara : MonoBehaviour { private Animator animator; private Vector3 velocity; [SerializeField] private float jumpPower = 5f; // 地面に接地しているかどうか [SerializeField] private bool isGrounded; // 入力値 private Vector3 input; // 歩く速さ [SerializeField] private float walkSpeed = 1.5f; // rigidbody private Rigidbody rigid; // Playerレイヤー以外のレイヤーマスク int layerMask; void Start() { animator = GetComponent<Animator>(); rigid = GetComponent<Rigidbody>(); layerMask = ~(1 << LayerMask.NameToLayer("Player")); } void Update() { // キャラクターが接地している場合 if (isGrounded) { // 接地したので移動速度を0にする velocity = Vector3.zero; input = new Vector3(Input.GetAxis("Horizontal"), 0f, Input.GetAxis("Vertical")); // 方向キーが多少押されている if (input.magnitude > 0f) { animator.SetFloat("Speed", input.magnitude); transform.LookAt(transform.position + input); velocity += transform.forward * walkSpeed; // キーの押しが小さすぎる場合は移動しない } else { animator.SetFloat("Speed", 0f); } // ジャンプ if (Input.GetButtonDown("Jump") && !animator.GetCurrentAnimatorStateInfo(0).IsName("Jump") && !animator.IsInTransition(0) // 遷移途中にジャンプさせない条件 ) { animator.SetBool("Jump", true); // ジャンプしたら接地していない状態にする isGrounded = false; velocity.y += jumpPower; } } } void FixedUpdate() { // キャラクターを移動させる処理 rigid.MovePosition(transform.position + velocity * Time.fixedDeltaTime); } private void OnCollisionEnter(Collision collision) { // 地面に着地したかどうかの判定 if (Physics.CheckSphere(transform.position, 0.3f, layerMask)) { isGrounded = true; animator.SetBool("Jump", false); velocity.y = 0f; } } private void OnCollisionExit(Collision collision) { // 地面から降りた時の処理 // Fieldレイヤーのゲームオブジェクトから離れた時 if (1 << collision.gameObject.layer != layerMask) { // 下向きにレイヤーを飛ばしFieldレイヤーと接触しなければ地面から離れたと判定する if (!Physics.Linecast(transform.position + Vector3.up * 0.2f, transform.position + Vector3.down * 0.1f, LayerMask.GetMask("Field", "Block"))) { isGrounded = false; } } } } |
↑が変更を加えた結果です。
ほとんど変わっていないですね。
うそつけーーー(-_-)/~~~ピシー!ピシー!
Updateメソッドでは移動とジャンプの速度を計算し、FixedUpdateメソッドでRigidbodyのMovePositionを使ってキャラクターを移動させています。
Rigidbodyに力を加える場合はUpdateよりもFixedUpdateメソッドで行うようにします。
またTime.deltaTimeをTime.fixedDeltaTimeに変更します。
FixedUpdateメソッド内でTime.deltaTimeを使うとTime.fixedUpdateと同じ効果になるとどこかで見たような気もしますが、変更しておきます。
コライダが地面と接触しているかどうかはOnCollisionEnterで他のコライダとの接触を検知し、さらにキャラクターの下に地面として使うコライダがあるかどうかを
レイを飛ばして確認します。
ジャンプをした時にisGroudedをfalseにし、OnCollisionEnterでPhysics.CheckSphereを使ってキャラクターの地面の位置に球の判定範囲を作成し、Playerレイヤー以外のレイヤーと接触していたらisGroundedをtrueにし着地したと判定しています。
Playerレイヤー以外は1をPlayerレイヤーのレイヤー番号分を左にシフトしてビットフィールドを作成し、それを~(チルダ)を使って反転して作成しています。
1 | layerMask = ~(1 << LayerMask.NameToLayer("Player")); |
主人公にはPlayerレイヤーを設定し、地面などはFieldレイヤー等を設定しておきます。
OnCollisionExitはキャラクターのコライダが他のコライダとの衝突から抜け出した時に呼ばれるので、キャラクターが地面から普通に落下した時の処理を記述します。
衝突していたコライダのレイヤーがPlayerレイヤー以外の時にさらに下に短いレイを飛ばし、Fieldレイヤーと接触していない時にisGroundedにfalseを入れます。
1 2 3 4 5 6 7 8 9 10 | private void OnCollisionExit(Collision collision) { // 地面から降りた時の処理 // Fieldレイヤーのゲームオブジェクトから離れた時 if (1 << collision.gameObject.layer != layerMask) { // 下向きにレイヤーを飛ばしFieldレイヤーと接触しなければ地面から離れたと判定する if (!Physics.Linecast(transform.position + Vector3.up * 0.2f, transform.position + Vector3.down * 0.1f, LayerMask.GetMask("Field", "Block"))) { isGrounded = false; } } } |
敵のキャラクターや地面以外のレイヤーからの落下も判断する場合は別の処理が必要になります。
キャラクターが動くかどうか確認する
CharacterControllerで動かしていた時と違いが出てないか確認してみましょう。
基本的な移動も出来、他のゲームオブジェクトにも自動的に物理的な力が加わっています。
他のゲームオブジェクトからの影響も受けるのでシーソーの床に乗ると少しずつ坂を落ちていきます。
青い坂の方にはPhysic Materialを設定し摩擦を大きくしたのでキャラクターが落ちていく事がありません。
また動画内にはありませんが、最初に紹介した記事でキャラクターがジャンプや移動した時にすり抜けてしまう現象が回避されるようになりました。
やったね(^^)v
これで問題が出なければ自分のゲームでキャラクターを使う時はRigidbody+CapsuleColliderで作ろうかな・・・・(-.-)
ひとつ問題があるとすれば、Rigidbody+CapsuleColliderでキャラクターを動かす時に段差を越えられないという事でしょうか。
ちょっとした段差でもコライダが引っかかって動けません(低い段差なら大丈夫)。
CharacterControllerとRigidbody+CapsuleColliderのキャラクターの動きを比較する
最後にCharacterControllerとRigidbodyで動かすキャラクターを横に並べて確認してみましょう。
左側がCharacterControllerで右側がRigidbodyで動かしているキャラクターです。
最後にどちらがどちらか確認するのでよく見てください!!(ウソです)
制御が違うので多少違いが出てますね。
Rigidbody君の方は接触している床の摩擦を受けるので坂を登るスピード等は遅くなりますね。
CharacterControllerと同じように段差を越えたり、坂を登れるようにする
Rigidbody+コライダのセットでもCharacterControllerと同じような設定をして坂等も昇れるようにしていきます。
やり方としてはキャラクターの前に坂や壁があるかどうかを調べるレイを飛ばし(足元あたりから)キャラクターとレイが衝突した壁の角度が指定した角度以下だったときに
坂を登れるようにし上向きの移動値を計算します。
昇れる段差であれば角度を関係なく上向きの移動値を計算します。
キャラクターから前の坂や壁を検出出来るようにレイを飛ばす場所StepRayPointをキャラクターの子要素に空オブジェクトで作成し位置を調整します。
↑のようにキャラクターの子要素に作成します。
↑がStepRayPointの位置です。
Local表示での青矢印がキャラクターの前向きになるようにRayPointを回転させます。
それでは先ほど言及した事を考慮してスクリプトを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 | using UnityEngine; using System.Collections; public class RigidChara : MonoBehaviour { private Animator animator; private Vector3 velocity; [SerializeField] private float jumpPower = 5f; // 地面に接地しているかどうか [SerializeField] private bool isGrounded; // 入力値 private Vector3 input; // 歩く速さ [SerializeField] private float walkSpeed = 1.5f; // rigidbody private Rigidbody rigid; // Playerレイヤー以外のレイヤーマスク int layerMask; // 段差を昇る為のレイを飛ばす位置 [SerializeField] private Transform stepRay; // レイを飛ばす距離 [SerializeField] private float stepDistance = 0.5f; // 昇れる段差 [SerializeField] private float stepOffset = 0.3f; // 昇れる角度 [SerializeField] private float slopeLimit = 65f; // 昇れる段差の位置から飛ばすレイの距離 [SerializeField] private float slopeDistance = 1f; void Start() { animator = GetComponent<Animator>(); rigid = GetComponent<Rigidbody>(); layerMask = ~(1 << LayerMask.NameToLayer("Player")); } void Update() { // キャラクターが接地している場合 if (isGrounded) { // 接地したので移動速度を0にする velocity = Vector3.zero; input = new Vector3(Input.GetAxis("Horizontal"), 0f, Input.GetAxis("Vertical")); // 方向キーが多少押されている if (input.magnitude > 0f) { animator.SetFloat("Speed", input.magnitude); transform.LookAt(transform.position + input); // 昇れる段差を表示 Debug.DrawLine(transform.position + new Vector3(0f, stepOffset, 0f), transform.position + new Vector3(0f, stepOffset, 0f) + transform.forward * slopeDistance, Color.green); // ステップ用のレイが地面に接触しているかどうか if (Physics.Linecast(stepRay.position, stepRay.position + stepRay.forward * stepDistance, out var stepHit, LayerMask.GetMask("Field", "Block"))) { // 進行方向の地面の角度が指定以下、または昇れる段差より下だった場合の移動処理 if (Vector3.Angle(transform.up, stepHit.normal) <= slopeLimit || (Vector3.Angle(transform.up, stepHit.normal) > slopeLimit && !Physics.Linecast(transform.position + new Vector3(0f, stepOffset, 0f), transform.position + new Vector3(0f, stepOffset, 0f) + transform.forward * slopeDistance, LayerMask.GetMask("Field", "Block"))) ) { velocity = new Vector3(0f, (Quaternion.FromToRotation(Vector3.up, stepHit.normal) * transform.forward * walkSpeed).y, 0f) + transform.forward * walkSpeed; Debug.Log(Vector3.Angle(transform.up, stepHit.normal)); } else { velocity += transform.forward * walkSpeed; } Debug.Log(Vector3.Angle(Vector3.up, stepHit.normal)); // 地面に接触していなければ } else { velocity += transform.forward * walkSpeed; } // キーの押しが小さすぎる場合は移動しない } else { animator.SetFloat("Speed", 0f); } // ジャンプ if (Input.GetButtonDown("Jump") && !animator.GetCurrentAnimatorStateInfo(0).IsName("Jump") && !animator.IsInTransition(0) // 遷移途中にジャンプさせない条件 ) { animator.SetBool("Jump", true); // ジャンプしたら接地していない状態にする isGrounded = false; velocity.y += jumpPower; } } } void FixedUpdate() { // キャラクターを移動させる処理 rigid.MovePosition(transform.position + velocity * Time.fixedDeltaTime); } private void OnCollisionEnter(Collision collision) { // 地面に着地したかどうかの判定 if (Physics.CheckSphere(transform.position, 0.3f, layerMask)) { isGrounded = true; animator.SetBool("Jump", false); velocity.y = 0f; } } private void OnCollisionExit(Collision collision) { // 地面から降りた時の処理 // Fieldレイヤーのゲームオブジェクトから離れた時 if (1 << collision.gameObject.layer != layerMask) { // 下向きにレイヤーを飛ばしFieldレイヤーと接触しなければ地面から離れたと判定する if (!Physics.Linecast(transform.position + Vector3.up * 0.2f, transform.position + Vector3.down * 0.1f, LayerMask.GetMask("Field", "Block"))) { isGrounded = false; } } } } |
レイが地面や壁と接触しているかどうかはPhysics.Linecastで調べています。
Physics.Linecastは始点から終点に向けてレイを飛ばしてぶつかった相手をstepHit変数に入れます。
接触相手は『Field』、『Block』というレイヤーに設定されているゲームオブジェクトのみを調べています。
その為地面や壁などはこの2つのどちらかのレイヤーを設定している必要があります。
レイの使い方は

を参照してください。
またキャラクターの上方向(Vector3.up)とレイが接触した方向(stepHit.normal)の角度をVector3.Angleで求めています。
Vector3.Angleで2つの方向の角度を求める事が出来ます。
この角度がインスペクタで設定したslopeLimit以下の時、または昇れる段差(stepOffset)の高さから前方にレイを飛ばし接触していなければ
上方向と押したキーの方向のvelocityを計算しています。
その部分が
1 2 3 | velocity = new Vector3(0f, (Quaternion.FromToRotation(Vector3.up, stepHit.normal) * transform.forward * walkSpeed).y, 0f) + transform.forward * walkSpeed; |
ですね。
Quaternion.FromToRotation(Vector3.up, stepHit.normal)で上方向とレイの接触面の間の角度を計算しそれにtransform.forwardをかける事で坂の角度方向へのベクトルを作成しています(坂の下から坂の上への方向ベクトル)。
接触面から計算した角度方向はy座標だけを使ってx、y座標の移動はキャラクターの向いている向きで計算します。
図にすると↑のような感じです。
角度を求めたらキャラクターの進行方向をかけることで坂の上の方向のベクトルが計算出来ます。
ここら辺はちょっと難しいですね・・・。

ここら辺の記事を参照して頂くと衝突面の角度辺りの事に触れています。
キャラクターの向いている方向×キャラクターの移動値
の計算に変更しました。
キャラクターの移動する速さであるwalkSpeedの値が高いと急な坂も制限を超えて登れます。
なので、あくまでslopeLimitはおまけ程度の扱いです。(^_^;)
坂を上る移動速度を単純な方法に変える
先ほどの場合坂の角度からキャラクターの進む方向を求めましたが、
1 2 3 | velocity = new Vector3(0f, (Quaternion.FromToRotation(Vector3.up, stepHit.normal) * transform.forward * walkSpeed).y, 0f) + transform.forward * walkSpeed; |
上のように坂の角度方向のY軸にキャラクターの移動スピードから上方向の移動値を計算し、それにキャラクターの前方方向のベクトルも加えています。
キャラクターの移動スピードが上がれば上がるほど上方向のベクトルの長さが長くなっていきます。
単純に坂を昇らせるだけならば
1 2 3 | velocity = Vector3.up * 2f + transform.forward * walkSpeed; |
上のように上向きの適当な力とキャラクターの前方に移動スピードをかけるだけでも出来ます。
1 2 3 4 5 | // ステップアップする力 [SerializeField] private float stepUpPower = 1.5f; |
上のように上向きの力をフィールドで設定出来るようにして処理を作ってみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // ステップ用のレイが地面に接触しているかどうか if (Physics.Linecast(stepRay.position, stepRay.position + stepRay.forward * stepDistance, out stepHit, LayerMask.GetMask("Field", "Block"))) { // ステップアップ用のレイが地面と接触していない if (!Physics.Linecast(transform.position + new Vector3(0f, stepOffset, 0f), transform.position + new Vector3(0f, stepOffset, 0f) + transform.forward * slopeDistance, LayerMask.GetMask("Field", "Block"))) { velocity = Vector3.up * stepUpPower + transform.forward * walkSpeed; // 接触した地面が登れる坂の角度以下 } else if (Vector3.Angle(transform.up, stepHit.normal) <= slopeLimit) { velocity = Quaternion.FromToRotation(Vector3.up, stepHit.normal) * transform.forward * walkSpeed; // それ以外は前進のみ } else { velocity += transform.forward * walkSpeed; } // ステップ用のレイが地面に接触していなければ } else { velocity += transform.forward * walkSpeed; } |
最初に地面確認用のレイを飛ばして接触していたら、
次に飛び越えられる段差を確認します。
段差が越えられる物であれば上向きの力と前方に進む力を速度に入れます。この場合は確実に上向きの力を加えないと前の障害物が90度の角度のような階段だった場合に登れなくなる可能性があるからです。
それ以外で坂が上れる角度であれば坂の角度を計算し、その方向にキャラクターの移動速度をかけます。この場合は登れる坂なので坂の方向を求めてそちらに移動速度をかけています。
登れる段差と角度でない場合は前方方向のみの移動にします。
↑がインスペクタで設定した値です。
設定値によってはうまく動作しない可能性もあるのである程度動かして確認する必要があります。
RigidbodyのUse Gravityにチェックを入れると重力が働き、他のゲームオブジェクトにも力を与える事が出来ますが、自身にも重力が働いており斜めの坂等では滑り落ちていきます。
これに対処するには地面にPhysicマテリアルを設定し滑らないようにするか、もしくはRigidobodyの重力を使わず自前で重力を計算すると滑らないようになります。
ただしRigidbodyの重力を使わないという事はシーソー等の床を作成した時に力を加えないのでシーソーしないようになります。
これではCharacterControllerとの違いがほとんどないのでRigidbodyにした意味は薄れます。
しかしシーソーの下からジャンプした時にすり抜けてシーソーの上に飛び乗ってしまうということは発生しないのでこれはRigidbodyの恩恵と言えるかもしれません。
ここら辺りの問題点は

でも触れているので参照してください。
設定値の角度より高い坂も昇れたり、キャラクターに重力が働いている為、低い坂が昇れなくなったりします。
そこら辺はRigidobodyの重力が働いていたりするので厳密な判定は難しいところですね・・・。
これでRigidbodyを使ったキャラクターの移動とジャンプの処理、坂や段差を登る事が出来ました。
キャラクターの移動に関してはRigidbodyのMovePositionではなくvelocityの値を変える事で移動させる事も出来ますが、Rigidbodyのvelocityを直接操作するのは
非現実的な物理シミュレーションに繋がるらしいのでやめておきました。