キャラクターの移動にはCharacterControllerのMove関数を使っていましたが、その移動の条件として、キャラクターが地面に接地している事という条件を設定していました。
ジャンプをするにも接地している必要があります。(空中でジャンプOKなら別ですが)
またCharacterControllerではなくRigidbody+コライダでキャラクターを動かしている場合の接地の判断としても今回の記事は有効です。
ジャンプ機能に関しては
を参考にしてください。
キャラクターの接地はCharacterControllerのisGroundedプロパティを調べればCharacterControllerのコライダが地面に接地しているかどうかが簡単に調べられます。
ジャンプの機能を実装するまでは全然気づかなかったんですが、平たんな道や坂を登る時は問題ないんですが、坂を下りながらジャンプボタンを押すとジャンプが出来ないんです。
上の動画のように坂を下りながらジャンプボタンを押してもジャンプしてくれません。
その原因は坂を下る時にコライダが接地面から外れisGroundedプロパティがfalseになってしまっているせいです。
どんな具合に接地面から外れるか確認してみます。
CharacterControllerのisGroundedプロパティで接地が確認出来ない理由
坂を登る時はコライダが矢印の方向に進むのでisGroundedプロパティはtrueになります。
登る時は問題ないですね。
次に坂を下る時です。
コライダが矢印の方向に進むので移動している間はisGroundedプロパティはfalseになってしまいます。
ジャンプ機能を使用しなければこの問題を考えなくてもいいんですが、ジャンプ機能を使ったアクションゲームを作る場合、これはかなり大きな問題です。
というわけで、CharacterControllerのisGrounded以外の接地を確認する方法が必要になります。
レイを使わないパターンを追記しました(2018年07月27日)。レイを使わないパターンの方が簡単です。
Rayを飛ばしキャラクターの接地を確認する
まずはキャラクターからRayを飛ばし、地面に接地しているかどうかを調べる方法をやってみます。
具体的にはPhysics.Linecast関数を使いキャラクターの体の一部をスタート地点にして、下方向に指定した距離のレイを飛ばします。
isGroundedだけで接地条件としていた個所にレイを飛ばして地面と当たっていた場合も含める事にします。
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 | using UnityEngine; using System.Collections; public class RayGroundChara : MonoBehaviour { private CharacterController characterController; private Animator animator; private Vector3 velocity = Vector3.zero; [SerializeField] private float walkSpeed = 1.5f; [SerializeField] private float jumpPower = 5f; // レイを飛ばす位置 [SerializeField] private Transform rayPosition; // レイの距離 [SerializeField] private float rayRange = 0.85f; // レイが地面に到達しているかどうか private bool isGround = false; // Use this for initialization void Start() { characterController = GetComponent<CharacterController>(); animator = GetComponent<Animator>(); } // Update is called once per frame void Update() { // CharacterControllerのコライダで接地が確認出来ない場合 if (!characterController.isGrounded) { if (Physics.Linecast(rayPosition.position, (rayPosition.position - transform.up * rayRange))) { isGround = true; } else { isGround = false; } // 接地確認用にレイを視覚化 Debug.DrawLine(rayPosition.position, (rayPosition.position - transform.up * rayRange), Color.red); } if (characterController.isGrounded || isGround) { // 地面に接地してる時は速度を初期化 if (characterController.isGrounded) { velocity = Vector3.zero; // レイを飛ばして接地確認の場合は重力だけは働かせておく、前後左右は初期化 } else { velocity = new Vector3(0f, velocity.y, 0f); } var 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 * walkSpeed; } else { animator.SetFloat("Speed", 0f); } if (Input.GetButtonDown("Jump") && !animator.GetCurrentAnimatorStateInfo(0).IsName("Jump") ) { animator.SetBool("Jump", true); velocity.y += jumpPower; } } velocity.y += Physics.gravity.y * Time.deltaTime; characterController.Move(velocity * Time.deltaTime); } } |
少しづつ見ていきます。
1 2 3 4 5 6 7 8 9 10 | // レイを飛ばす位置 [SerializeField] private Transform rayPosition; // レイの距離 [SerializeField] private float rayRange = 0.85f; // レイが地面に到達しているかどうか private bool isGround = false; |
rayPositionはレイを飛ばす位置、rayRangeはレイの距離でインスペクタで設定出来るようにします。
isGroundはレイが地面に到達したかどうかのフラグに使用します。
次にUpdateメソッドの最初でCharacterControllerのisGroundがfalseの時にレイを飛ばして接地を確認します。
1 2 3 4 5 6 7 8 9 10 11 12 | // CharacterControllerのコライダで接地が確認出来ない場合 if (!characterController.isGrounded) { if (Physics.Linecast (rayPosition.position, (rayPosition.position - transform.up * rayRange))) { isGround = true; } else { isGround = false; } // 接地確認用にレイを視覚化 Debug.DrawLine (rayPosition.position, (rayPosition.position - transform.up * rayRange), Color.red); } |
CharacterControllerのisGroudedプロパティがfalseの時だけレイを飛ばすようにします。
isGroundedプロパティがtrueの時は接地しているのがわかっているのでレイを飛ばす必要がない為です。
Physics.Linecastは
1 2 3 | Physics.Linecast(開始地点, 終了地点) |
なのでレイを飛ばす開始地点と終了地点を指定します。
開始地点はインスペクタでキャラクターの体の一部を指定します、今回はStandardAssetsのキャラクターであるEthanのEthanHipsを指定します。
終了地点はレイを飛ばす開始地点+飛ばす先で計算した位置を指定します。
飛ばす先は
-transform.upでキャラクターの上向きの逆、なのでキャラクターは地面と垂直にしているので、下向きであるVector3(0, -1, 0)になります。
それにrayRangeをかける事でVector3(0, -charaRayRange, 0)になります。
rayPosition.positionにその値を足す事で開始地点に飛ばす先の位置を足したので終了地点となります。
rayPosition.position + (-transform.up * charaRayRange)
↓
rayPosition.position – transform.up * charaRayRange
となります。
レイが地面と接触していればisGroundにtrueを入れます。
接触していなければfalseを入れます。
今回はレイを飛ばして接触した相手を指定していないので、地面じゃない物にレイが接触した場合もisGroundにtrueが入ってしまいます。
通常は地面にするゲームオブジェクトにレイヤーを指定し、そのレイヤーと接触していたかどうかで判断します。
地面に設定するレイヤー名をFieldとしていた場合
1 2 3 | Physics.Linecast(charaRay.position, (charaRay.position - transform.up * charaRayRange), LayerMask.GetMask("Field")) |
とレイヤーを指定します。
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 | if (characterController.isGrounded || isGround) { // 地面に接地してる時は速度を初期化 if (characterController.isGrounded) { velocity = Vector3.zero; // レイを飛ばして接地確認の場合は重力だけは働かせておく、前後左右は初期化 } else { velocity = new Vector3(0f, velocity.y, 0f); } var 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 * walkSpeed; } else { animator.SetFloat("Speed", 0f); } if (Input.GetButtonDown("Jump") && !animator.GetCurrentAnimatorStateInfo(0).IsName("Jump") ) { animator.SetBool("Jump", true); velocity.y += jumpPower; } } |
isGroudedだけで判断していた接地にisGroundがtrueである時も加えます。
接地していた時はvelocityにVector3.zeroを入れていましたが、レイを飛ばして当たった場合は方向キーの値だけを初期化し、重力の値はそのままにします。
レイが地面に当たっていた時のisGroundはあくまでジャンプする時に使用するものなので、重力値の初期化はisGroudedプロパティがtrueであった時だけにします。
重力の処理以外は通常のキャラクターの移動と同じです。
下り坂を移動中にジャンプ出来るかどうかを確認
それでは機能が出来上がったので設定をしてUnityの実行ボタンを押して確認してみます。
MoveTest(キャラクターを移動させるスクリプト)のインスペクタでcharaRayにEthanHips、charaRayRangeに0.85を設定します。
キャラクターによって設定する位置や距離は異なります。
上のような感じで、レイが地面より少し下に到達するような感じで指定します。
上の動画のように坂を下っている途中でもジャンプが出来るようになりました。
もしジャンプが出来ないようであればレイの距離を少し伸ばしてみてください。
上のようにCharacterControllerのコライダが接地している時はレイを飛ばさず、接地していない時にレイを飛ばしています。
前述したようにコライダは坂を下る時は接地判定されません(動いている時)
レイを使わないで坂を下っている時にジャンプする方法
2018年07月27日に追記した内容になります。
レイを使わずとも坂道を下っている時にジャンプする事が出来たのでそちらの方法もやってみましょう。
接地が確認出来ないのは横に移動した時に空中にいる状態になってCharacterControllerのisGroundedがfalseになってしまう事でジャンプが出来ませんでした。
そこでisGroundedがtrueになった時に下向きのオフセットの移動値を加えることによって空中状態をなくします。
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 | using UnityEngine; using System.Collections; public class CharacterControllerCheckGround : MonoBehaviour { private CharacterController characterController; private Animator animator; private Vector3 velocity = Vector3.zero; [SerializeField] private float walkSpeed = 1.5f; [SerializeField] private float jumpPower = 5f; // 下方向に強制的に加える力 [SerializeField] private Vector3 addForceDownPower = Vector3.down; // Use this for initialization void Start () { characterController = GetComponent <CharacterController> (); animator = GetComponent <Animator> (); } // Update is called once per frame void Update () { if (characterController.isGrounded) { velocity = Vector3.zero; var 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 * walkSpeed; } else { animator.SetFloat ("Speed", 0f); } if (Input.GetButtonDown ("Jump") && !animator.GetCurrentAnimatorStateInfo(0).IsName("Jump") ) { animator.SetBool ("Jump", true); velocity.y += jumpPower; } else { // ジャンプキーを押していない時は下向きに力を加える velocity += addForceDownPower; } } velocity.y += Physics.gravity.y * Time.deltaTime; // 下向きのオフセット値を足して動かす characterController.Move (velocity * Time.deltaTime); } } |
addForceDownPowerが強制的に加える下向きの力になります。
ジャンプボタンを押していない時は移動速度の下向きにaddForceDownPowerを足します。
これで坂を下っている時もジャンプ出来るようになりました。
レイを使うよりスクリプトの流れもわかりやすくなって処理もうまくいっているようなのでこちらの方がいいかも?
これでUnityのCharacterControllerの接地をスクリプトで判断する事が出来るようになりました。