今回はカプセルコライダとRigidbodyのAddForceメソッドを使ってキャラクター移動スクリプトを作成していきます。
以前、カプセルコライダとRigidbodyを使ったキャラクター移動スクリプトを作成しましたが、そちらの場合はMovePositionを使って移動処理をしていたので、厳密には小刻みにワープをして移動していました。
今回はRigidbodyのAddForceを使って移動時になるべくすり抜け等が起きないようにします。
今回の機能を作成すると以下のような感じになります。
ただ色々改善の必要はあるかもしれません。(´Д`)
キャラクターの設定
まずは人型キャラクターモデルを用意してヒエラルキーに配置し、RigidbodyコンポーネントとCapsuleColliderコンポーネントを取り付け、Rigidbodyの設定とCapsuleColliderのコライダのサイズを調整します。
AnimatorControllerを作成し、AnimatorコンポーネントのControllerに設定します。
アニメーションパラメーターにはFloat型のSpeedとJumpPower、Bool型のIsGrounded、Trigger型のJumpを作成します。
Idle状態、Walk状態、Jump状態を作成します。
Jump状態はブレンドツリーで作成し、JumpPowerの値によってジャンプアニメーションを変更するようにします。
Idle→WalkはSpeedがGreaterで0
Walk→IdleはSpeedがLessで0.0001
Idle→JumpはJumpがトリガーされた時
Jump→IdleはIsGroundedがtrueの時
Walk→JumpはJumpがトリガーされた時
で各遷移条件のHas Exit Timeのチェックを外しておきます。
アニメーターコントローラーについては
Jumpのブレンドツリーについては
を参照してください。
設定をしたキャラクターゲームオブジェクトのインスペクタで以下のように設定しました。
キャラクター用のスクリプトを作成
キャラクター用のスクリプトを作成していきます。
MakeRigidAddForceCharaスクリプトを作成し、先ほどヒエラルキーに配置したキャラクターに取り付けます。
一気に作ると大変なので、徐々に作成していきます。
基本的な移動部分の作成
まずはキャラクターの基本的な移動と接地の確認が出来るようにします。
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 | using System.Collections; using UnityEngine; public class MakeRigidAddForceChara: MonoBehaviour { private Rigidbody rigidBody; // 移動速度 private Vector3 velocity; // 入力値 private Vector3 input; // アニメーター private Animator animator; // 地面と判断するレイヤーマスク [SerializeField] private LayerMask groundLayers; // 速さ [SerializeField] private float walkSpeed = 4f; // 接地しているかどうか [SerializeField] private bool isGrounded; // 接地確認のコライダの位置のオフセット [SerializeField] private Vector3 groundPositionOffset = new Vector3(0f, 0.02f, 0f); // 接地確認の球のコライダの半径 [SerializeField] private float groundColliderRadius = 0.29f; void Start() { animator = GetComponent<Animator>(); rigidBody = GetComponent<Rigidbody>(); } void FixedUpdate() { // 接地確認 CheckGround(); // 入力値 input = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical")); // 移動速度計算 var clampedInput = Vector3.ClampMagnitude(input, 1f); velocity = clampedInput * walkSpeed; transform.LookAt(rigidBody.position + input); // 接地時の処理 if (isGrounded) { if (clampedInput.magnitude > 0f) { animator.SetFloat("Speed", clampedInput.magnitude); } else { animator.SetFloat("Speed", 0f); } } // 移動処理 rigidBody.AddForce(rigidBody.mass * velocity / Time.fixedDeltaTime, ForceMode.Force); } // 地面のチェック private void CheckGround() { // groundLayersに接触したら地面に接地 if (Physics.CheckSphere(rigidBody.position + groundPositionOffset, groundColliderRadius, groundLayers) ) { isGrounded = true; } else { isGrounded = false; } animator.SetBool("IsGrounded", isGrounded); } } |
rigidBodyには自身に取り付けたRigidbodyを取得し、入れます。
velocityには移動速度を入れます。
inputには入力値を入れます。
animatorには自身のAnimatorコンポーネントを取得し、入れます。
groundLayersには地面として認識させるゲームオブジェクトのレイヤーマスクを設定します。
walkSpeedにはキャラクターの速さを設定します。
isGroundedは地面と接地しているかどうかの判定に使います。
collisionPositionOffsetは接地確認のコライダの位置のオフセット
collisionColliderRadiusは接地確認の球の半径です。
Startメソッドでは自身のゲームオブジェクトに取り付けられたコンポーネントを取得しています。
Rigidbodyを使った処理はUpdateメソッドよりも固定フレームレートで実行されるFixedUpdateメソッドの方が適しているので、こちらに移動処理を記述していきます。
CheckGroundメソッドを呼び出して、まず地面と接地しているかどうかの確認をします。CheckGroundメソッドは後で作ります。
移動の入力値を取得しinputに入れます。
Vector3.ClampMagnitudeメソッドを使ってinputの入力値を第2引数で指定した1までの値に制限し、それをclampedInputに入れます。
これは斜め方向に移動する場合に縦と横を同時に押しますが、最大で1.41・・・・と1以上の値が入ってきてしまうので、値を制限する為に使っています。
input.normalizedをつかう事も出来ますが、こちらの場合は正規化した値になるので0~1の間の小数点の値が取れない為、今回は使用していません。
今回は移動する時にアナログスティックの小さな傾きで少しの移動をさせたいので小数点の値を使います。
velocityには制限した入力値に速さをかけたものを入れます。
transform.LookAtメソッドを使ってキャラクターの向きを現在地+入力値の方に向けています。
isGroundedがtrueの時は接地している時で、制限した入力値が0より大きい時はアニメーションパラメーターのSpeedに制限した入力値の大きさを渡して歩行アニメーションを再生し、0以下の時は0を渡して立っているアニメーションへと遷移するようにします。
アニメーションパラメーターのSpeedに制限した入力値(小数点)を渡していますが、今回はWalk状態は一つのアニメーションクリップのみを再生するようにしているので特に意味はありません。ジャンプ状態のようにブレンドツリーを使ってSpeedの値によってアニメーションクリップを変更するとより細かい歩きアニメーションの遷移が出来ます。
その後、rigidBody.AddForceメソッドで質量に速度をかけたものをTime.fixedDeltaTimeを掛けて1フレーム当たりに加える力を計算しています。
AddForceで与える力に関しては以下を参照してください。
次にCheckGroundメソッドを見ていきます。
キャラクターの現在位置+groundPositionOffsetにgroundColliderRadiusの半径の球を作成し、groundLayersで設定したレイヤーを持つコライダと接触した場合は接地とします。
条件が成立したらisGroundedをtrue、それ以外はfalseとし、その後アニメーションパラメーターのIsGroundedをisGroundedに設定しています。
ヒエラルキーで右クリックから3D Object→Planeを選択して名前をFloorとし、LayerにGroundを作り設定します。
キャラクターを動かしてみると床の上を移動します・・・・が、ここでキャラクターは加速度的に移動し、瞬く間に床を外れてしまいます。(^_^;)
ずっと押していても一定値に制限する
今回はRigidbodyのAddForceメソッドを使って力を加えてキャラクターを動かしますが、CharacterControllerの時のように毎フレーム力を加えると加速度的に移動してしまいます。
これはRigidbodyに一度力を加えると抵抗する力が働かない限りずっと力が加わってしまう為です。
そこでAddForceに加える力をwalkSpeedで設定した速さ以上にはならないようにします。
そこで計算したvelocity(速度)から現在のRigidbodyの速度を引いたものを改めてvelocityに代入し、さらにそのvelocityがwalkSpeedを超えないように制限します。
transform.LookAtメソッドの下に以下のスクリプトを挿入します。
1 2 3 4 5 6 | // 今入力から計算した速度から現在のRigidbodyの速度を引く velocity = velocity - rigidBody.velocity; // 速度のXZを-walkSpeedとwalkSpeed内に収めて再設定 velocity = new Vector3(Mathf.Clamp(velocity.x, -walkSpeed, walkSpeed), 0f, Mathf.Clamp(velocity.z, -walkSpeed, walkSpeed)); |
最初に計算した速度から現在の速度を引いてAddForceに与える力を制限しています。
例えば現在キャラクターが左に(4, 0, 0)で移動しているとして
今入力をして、velocityが(4, 0, 0)で左側に押しっぱなしの状態にすると
1 2 3 | velocity = velocity - rigidBody.velocity; |
は
(4, 0, 0) – (4, 0, 0)
を計算するのでvelocityは(0, 0, 0)となり、AddForceで与える力は0になります。
今入力して計算した速度velocityが(2, 0, 0)だとしたら
(2, 0, 0) – (4, 0, 0)
となるので、velocityは(-2, 0, 0)となり右側に移動する速度を使ってAddForceで力を加える事になります。
つまり現在のRigidbodyの速度に応じて与える力を変更します。
velocityのxとzの値をMathf.Clampを使って-walkSpeedとwalkSpeedの範囲内に制限しています。
これで移動キーを押しっぱなしにしても一定速度でしか移動しないようになりました。
ジャンプ機能の作成
基本的な移動機能が出来たのでジャンプ機能を取り付けます。
まずはフィールドを追加します。
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 | // ジャンプボタンを押したかどうか private bool pushJumpButton; // ジャンプする高さ [SerializeField] private float jumpHeight = 5f; // 初期のジャンプ値 private float initJumpHeightValue; // 現在のジャンプ値 private float jumpValue; // ジャンプ中かどうか [SerializeField] private bool isJump; // ジャンプ後に接地確認するまでの遅延時間 [SerializeField] private float jumpWaitTime = 0.5f; // ジャンプ後の時間 [SerializeField] private float jumpTime = 0f; // 衝突したかどうか [SerializeField] private bool isCollision; // 衝突確認のコライダの位置のオフセット [SerializeField] private Vector3 collisionPositionOffset = new Vector3(0f, 0.5f, 0.1f); // 衝突確認の球のコライダの半径 [SerializeField] private float collisionColliderRadius = 0.3f; // 天井の衝突確認用コライダのオフセット値 [SerializeField] private Vector3 collisionCeilingOffset = new Vector3(0f, 1.8f, 0f); // 天井の衝突確認用コライダの球の半径 [SerializeField] private float collisionCeillingColliderRadius = 0.29f; |
pushJumpButtonはジャンプボタンを押したかどうか
jumpHeightはジャンプする高さ
initJumpHeightValueは初期のジャンプ値
jumpValueは現在のジャンプ値
isCollisionは天井にぶつかっているかどうか
collisionPositionOffsetは衝突確認の球の位置のオフセット値
collisionColliderRadiusは衝突確認の球の半径
collisionCeilingOffsetは天井衝突確認用の球の位置のオフセット値
collisionCeilingColliderRadiusは天井衝突確認の球の半径
です。
次にUpdateメソッドにJumpボタンが押された時の処理を追加します。
1 2 3 4 5 6 7 8 9 | private void Update() { if(Input.GetButtonDown("Jump") && isGrounded ) { pushJumpButton = true; } } |
Jumpボタンを押した時、かつisGroundedがtrueの時にpushJumpButtonをtrueにします。
FixedUpdateメソッド内でジャンプボタンを押したかどうかを判定してもいいのですが、FixedUpdateメソッドの場合はデフォルトで0.02秒毎に実行するので、Updateメソッドよりも実行する回数が少なくなっています。
その為、Jumpボタンを押してもその処理を捉えられずジャンプ出来ないことがあります。
今回は移動値の計算をFixedUpdateメソッドで行っていますが、こちらもUpdateメソッドで移動値の計算をし、FixedUpdateメソッドではその値を使った移動処理を行う方がより細かい値を使えます。
接地時の処理にジャンプボタンを押した時の処理を追加します。
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 | // 接地時の処理 if (isGrounded) { if (clampedInput.magnitude > 0f) { animator.SetFloat("Speed", clampedInput.magnitude); } else { animator.SetFloat("Speed", 0f); } // ジャンプ if (pushJumpButton) { pushJumpButton = false; isGrounded = false; isJump = true; jumpTime = 0f; animator.SetTrigger("Jump"); // 初期のジャンプ値を計算 2ax = v²-v₀² initJumpHeightValue = Mathf.Sqrt(-2 * Physics.gravity.y * jumpHeight); jumpValue = initJumpHeightValue; animator.SetFloat("JumpPower", initJumpHeightValue); // 即座にジャンプさせる rigidBody.velocity = new Vector3(rigidBody.velocity.x, initJumpHeightValue, rigidBody.velocity.z); } } |
移動値によりアニメーションを変更した後にpushJumpButtonがtrueどうかを判定しています。
pushJumpButtonがtrueの時はpushJumpButtonをfalse、ジャンプするのでisGroundedをfalse、isJumpをtrueにします。
jumpTimeを0に初期化します。
アニメーションパラメーターのJumpをトリガーします。
initJumpHeightValueはジャンプをした時の最高到達点を入れます。
ジャンプ値(ジャンプの速さ)は
$$2ax = v^2-{v_0}^2$$
で求めています。
aは加速度、xは距離、vは現在の速度、v₀は初速度です。
jumpValueは現在のジャンプ値を更新していくので、まずは初期値であるinitJumpHeightValueを入れます。
アニメーションパラメーターのJumpPowerにinitJumpHeightValueを入れます。
ジャンプの移動を即座に反映させる為、RigidbodyのvelocityのYを直接変更します。
接地時の処理が終わった所で以下の処理を追加します。
1 2 3 4 5 6 7 8 9 10 11 | // 現在のジャンプ値の計算 if (jumpValue > -initJumpHeightValue) { jumpValue += Physics.gravity.y * Time.fixedDeltaTime; animator.SetFloat("JumpPower", jumpValue); } // ジャンプ中はジャンプ時間を計測する if (isJump && jumpTime < jumpWaitTime) { jumpTime += Time.deltaTime; } |
現在のジャンプ値がマイナスのジャンプの初期値以上の時は現在のジャンプ値を更新します。
jumpValueはPhysics.gravity.y(重力加速度)にTime.fixedDeltaTimeを掛けて1フレームでの落下速度分をjumpValueに足します。
これで重力分(Physics.gravity.yは初期値で-9.81なので下向きです)のジャンプ値が現在のジャンプ値から引かれます。
計算が終わったらジャンプ値をアニメーションパラメーターのJumpPowerに入れます。
次にジャンプ中でジャンプ時間がジャンプの待ち時間以下の場合はjumpTimeに時間を足していきます。
このjumpTimeは何かというと、ジャンプ後に直ぐに接地と判断されるのを避けるための時間の計算で、jumpWaitTimeを超えたジャンプ時間にならない限りは接地をさせないようにします。
この処理をする為にCheckGroundメソッドを変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // 地面のチェック private void CheckGround() { // アニメーションパラメータのRigidbodyのYが0.1以下でGroundまたはEnemyレイヤーと球のコライダがぶつかった場合は地面に接地 if (Physics.CheckSphere(rigidBody.position + groundPositionOffset, groundColliderRadius, groundLayers)) { // ジャンプ中 if (isJump) { if (jumpTime >= jumpWaitTime) { isGrounded = true; isJump = false; } else { isGrounded = false; } } else { isGrounded = true; } } else { isGrounded = false; } animator.SetBool("IsGrounded", isGrounded); } |
接地確認の為の球にgroundLayersに指定したレイヤーが接触している時にジャンプ中であればジャンプの経過時間がjumpWaitTime以上になった時は接地とし、isJumpをfalseにします。
ジャンプの経過時間がjumpWaitTimeより下の場合はisGroundedはfalseにします。
そもそもジャンプ中でない場合はisGroundedをtrueにし、接地しているとします。
球がgroundLayersのレイヤーと接触していなければisGroundedはfalseにします。
天井にぶつかった時の処理
天井を作ります。
ヒエラルキーで右クリックから3D Object→Cubeを選択し、名前をCeilingとします。
CeilingのインスペクタでLayerをGroundにします。
Ceilingゲームオブジェクトはキャラクターのジャンプの最高到達点よりも低い位置に置き、キャラクターを天井に向けてジャンプさせて確認します。
ジャンプは出来ましたが、ジャンプアニメーションはアニメーションパラメーターのJumpPowerの値によって、
ジャンプアップ→最高点付近→落下
というアニメーションの切り替えを行っています。
これはジャンプしてぶつからずに重力で落下することを前提としていて、JumpPowerに渡すJumpValueの値の更新も重力に合わせた変化なので、頭の上に天井があってぶつかった場合もアニメーションの変化は天井にぶつかっていない時と同じなのでジャンプアップ→最高点付近→落下というアニメーションの遷移がされてしまいます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | private void OnCollisionStay(Collision collision) { // 天井に衝突したらジャンプ値の調整 if (jumpValue > 0f && Physics.CheckSphere(rigidBody.position + collisionCeilingOffset, collisionCeillingColliderRadius, groundLayers )) { Debug.Log("天井に衝突"); jumpValue = 0f; animator.SetFloat("JumpPower", jumpValue); } } private void OnCollisionExit(Collision collision) { // 地面とするレイヤーから離れた if ((groundLayers & 1 << collision.gameObject.layer) == 1 << collision.gameObject.layer) { isCollision = false; } } private void OnDrawGizmos() { Gizmos.DrawWireSphere(transform.position + Vector3.up * 1.5f, 0.3f); } |
OnCollisionStayは他のコライダと衝突している間に呼ばれるメソッドなのでこのメソッドの中に処理を書いています。
groundLayersが地面とするレイヤー群で、1をcollision.gameObject.layer分左シフトし、&で論理積を求めそれがそのままの値として得られたら地面とするレイヤー群の中にそのレイヤーが含まれているということなので、衝突とします。
if文でjumpValueが0より大きい時で、かつPhysics.CheckSphereを使ってキャラクターの頭部付近に球を作り、その球とgroundLayersに指定したレイヤーが衝突するかどうかを判定します。
jumpValueが0より大きい時という条件は、何度もjumpValueを0に設定するのを防ぐためです。
衝突していたらjumpValueを0にし、アニメーションパラメーターのJumpPowerに値を設定します。
OnCollisionExitは衝突したゲームオブジェクトのコライダから離れた時に呼ばれるので、ここで離れた相手のレイヤーがgroundLayersに含まれている場合はisCollisionをfalseにして衝突していないとします。
OnDrawGizmosメソッドはギズモを表示するメソッドで、Gizmos.DrawWireSphereを使って先ほどのPhysics.CheckSphereでチェックする球をシーンビュー等で視覚的に確認出来るようにしています。
ただし現在地で使っているrigidBodyはStartメソッドで取得しているコンポーネントなので、OnDrawGizmos内では使えません(ギズモはUnity実行中でも有効になる為)。
なので、計算した値をそのまま入れています。
これで天井に衝突時の処理が出来たので試してみます。
ジャンプ時や落下時に壁と衝突した時の問題
ジャンプ時や落下時に壁と接触してそのままその方向に移動キーを押していると空中でそのまま浮遊してしまいます。
この原因はジャンプ中も移動値を反映している為で、壁の方向に向かって移動させ続けるとずっと摩擦力で空中浮遊した状態になってしまいます。
そこで、空中にいる時で壁等に接触している場合でキャラクターが壁の方を向いている場合は移動値を反映しないようにします。
まずはcollisionDirectionフィールドを作成します。
1 2 3 4 | // 衝突した面の方向 private Vector3 collisionDirection; |
OnCollisionStayメソッド内で衝突した相手の面の方向をcollisionDirectionに入れます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | private void OnCollisionStay(Collision collision) { // 地面とするレイヤーと接触している if ((groundLayers & 1 << collision.gameObject.layer) == 1 << collision.gameObject.layer) { isCollision = true; // 衝突した最初の面の向きを入れる collisionDirection = collision.contacts[0].normal; } if (jumpValue > 0f && Physics.CheckSphere(rigidBody.position + collisionCeilingOffset, collisionCeilingColliderRadius, groundLayers) ) { jumpValue = 0f; animator.SetFloat("JumpPower", jumpValue); } } |
衝突した相手のcollisionから衝突した複数個所の0番目だけを取得し、その法線(面の表向き)をcollisionDirectionに入れます。
これは衝突した面とキャラクターの向きからどの程度の角度がついているかを計算するのに使います。
OnCollisionExitメソッドに処理を追加します。
1 2 3 4 5 6 7 8 9 | private void OnCollisionExit(Collision collision) { // 地面とするレイヤーから離れた if ((groundLayers & 1 << collision.gameObject.layer) == 1 << collision.gameObject.layer) { isCollision = false; collisionDirection = Vector3.zero; } } |
衝突から抜け出した時にcollisionDirectionに0を入れます。
FixedUpdateメソッドの接地時の処理ブロックが終わったすぐ後に以下の処理を追加します。
1 2 3 4 5 6 7 8 9 10 | // ジャンプ中等に横壁と接触している時、または階段を上っている時は重力以外の移動値を0にする if ((!isGrounded && isCollision)) { //Debug.Log(Vector3.Dot(transform.forward, collisionDirection)); // 衝突した壁方向を向いている時だけ重力以外の移動値を0にする if (Vector3.Dot(transform.forward, collisionDirection) <= 0.5f) { velocity = new Vector3(0f, velocity.y, 0f); } } |
isGroundedがfalseで、かつisCollisionがtrueの時は空中でGroundレイヤーを設定したゲームオブジェクトと接触しているのでその時にVector3.Dotメソッドを使ってキャラクターの向いている向きと衝突した相手の面の方向からベクトルの内積を求めます。
内積は1であればまったく同じ方向を向き、-1であれば反対方向を向いている状態です。
今回の場合は0.5以下の時にvelocityのXとZを0にして上下の速度だけを反映させます。
0.5以下なのでキャラクターが衝突面に対して真横から衝突面の真正面の間の場合は移動値を制限するということになります。
これでジャンプ中や落下時に空中浮遊することがなくなりました。
Terrainの地面や階段が登れない問題
次にゲームの舞台にTerrainを使ってデコボコの地面を作ったり、階段を作った場合の移動の問題があります(滑らかな坂は上れます)。
ジャンプ機能を搭載したので段差がある場所はジャンプで進ませるというのもありですが、いちおうこれにも対応しておきます。
基本的には以前の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 | // 階段を上に上る速さ [SerializeField] private float stepUpwardSpeed = 2.1f; // 前方に段差があるか調べるレイを飛ばすオフセット位置 [SerializeField] private Vector3 stepRayOffset = new Vector3(0f, 0.05f, 0f); // レイを飛ばす距離 [SerializeField] private float stepDistance = 0.6f; // 昇れる段差 [SerializeField] private float stepOffset = 0.4f; // 昇れる角度 [SerializeField] private float slopeLimit = 45f; // 昇れる段差の位置から飛ばすレイの距離 [SerializeField] private float slopeDistance = 0.65f; // 階段を上っているかどうか [SerializeField] private bool isClimbed; |
stepUpwardSpeedは階段を上る時の上向きの速さを設定します。
stepRayOffsetは前方に段差があるかどうかを調べるレイを飛ばすキャラクター位置からのオフセット値を設定します。
stepDistanceは前方に段差があるかどうかを調べるレイを飛ばす距離を設定します。
stepOffsetは登れる段差を設定します。
slopeLimitは登れる坂の角度を設定します。
slopeDistanceは登れる段差の位置から飛ばすレイの距離を設定します。
isClimbedは階段を上っている最中かどうかのフラグです。
次に階段や坂を上る処理を行うClimbTheStairsメソッドを作成します。
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 | private void ClimbTheStairs() { var stepRayPosition = rigidBody.position + stepRayOffset; // ステップ用のレイが地面に接触しているかどうか if (Physics.Linecast(stepRayPosition, stepRayPosition + rigidBody.transform.forward * stepDistance, out var stepHit, groundLayers)) { var angle = Vector3.Angle(transform.up, stepHit.normal); var clampedInput = Vector3.ClampMagnitude(input, 1f); // 進行方向の地面の角度が指定以下、または昇れる段差より下だった場合の移動処理 if (angle <= slopeLimit) { // 指定角度以下の場合は普通の力のまま Debug.Log("坂: " + angle); velocity = Quaternion.FromToRotation(Vector3.up, stepHit.normal) * clampedInput * walkSpeed; velocity = velocity - rigidBody.velocity; velocity = Vector3.ClampMagnitude(velocity, walkSpeed); } else if (angle > slopeLimit && !Physics.Linecast(rigidBody.position + new Vector3(0f, stepOffset, 0f), rigidBody.position + new Vector3(0f, stepOffset, 0f) + rigidBody.transform.forward * slopeDistance, groundLayers) ) { // 角度確認 Debug.Log("階段: " + angle); velocity = Quaternion.FromToRotation(Vector3.up, stepHit.normal) * clampedInput * walkSpeed; velocity = Vector3.ClampMagnitude(velocity, stepUpwardSpeed); velocity = Vector3.ClampMagnitude(velocity - rigidBody.velocity, stepUpwardSpeed); isClimbed = true; } else { // 完全に速度を0または強制で下向きの速度にする velocity = Vector3.zero; } } else { // 前方に坂や階段がない場合 isClimbed = false; } } |
最初にキャラクターの現在位置にstepRayOffsetの値を足して、レイを飛ばす位置をstepRayPositionに入れます。
stepRayPositionからレイを飛ばしgroundLayersに設定したレイヤーと接触したらその情報をstepHitに入れます。
キャラクターの上方と衝突した相手のゲームオブジェクトの前面の角度を計算しangleに入れます。
入力値に制限を加えた値をclampedInputに入れます。
アングルがslopeLimit以下であれば登れる坂なので、上方と前方の速度を計算し通常の移動時と同じように速度に制限を加えています。
アングルがslopeLimitより大きく、かつキャラクターの位置にstepOffsetを足した位置から前方にレイを飛ばしgroundLayersと接触していなければ上れる段差になるので、速度を計算します。
前方の階段が90度の場合は上向きの速さが大きくなってしまうので、別途stepUpwardSpeedの値を使って制限をしています。
階段を上る場合はisClimbedにtrueを入れ、段差を昇っている最中だとします。
angleが指定した条件に引っかからなかった場合はvelocityに0を入れています。
ステップ用のレイがそもそも壁等に接触しなかった場合はisClimbedをfalseにします。
FixedUpdateメソッドを変更します。
途中に色々処理を入れているのでFixedUpdateメソッドの全部を載せています。
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 | void FixedUpdate() { // 接地確認 CheckGround(); // 入力値 input = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical")); // 移動速度計算 var clampedInput = Vector3.ClampMagnitude(input, 1f); velocity = clampedInput * walkSpeed; transform.LookAt(rigidBody.position + input); // 今入力から計算した速度から現在のRigidbodyの速度を引く velocity = velocity - rigidBody.velocity; // 速度のXZを-walkSpeedとwalkSpeed内に収めて再設定 velocity = new Vector3(Mathf.Clamp(velocity.x, -walkSpeed, walkSpeed), 0f, Mathf.Clamp(velocity.z, -walkSpeed, walkSpeed)); // 接地時の処理 if (isGrounded || isClimbed) { //velocity.y = 0f; if (clampedInput.magnitude > 0f) { // 前方の坂や階段を上るための処理 ClimbTheStairs(); animator.SetFloat("Speed", clampedInput.magnitude); } else { animator.SetFloat("Speed", 0f); isClimbed = false; velocity = new Vector3(0f, velocity.y, 0f); } // ジャンプ if (pushJumpButton) { pushJumpButton = false; isClimbed = false; Debug.Log("Jump3"); isGrounded = false; isJump = true; jumpTime = 0f; animator.SetTrigger("Jump"); // 初期のジャンプ値を計算 2ax = v²-v₀² initJumpHeightValue = Mathf.Sqrt(-2 * Physics.gravity.y * jumpHeight); jumpValue = initJumpHeightValue; animator.SetFloat("JumpPower", initJumpHeightValue); // 即座にジャンプさせる rigidBody.velocity = new Vector3(rigidBody.velocity.x, initJumpHeightValue, rigidBody.velocity.z); } } // ジャンプ中等に横壁と接触している時、または階段を上っている時は重力以外の移動値を0にする if ((!isGrounded && isCollision) || isClimbed ) { //Debug.Log(Vector3.Dot(transform.forward, collisionDirection)); // 衝突した壁方向を向いている時だけ重力以外の移動値を0にする if (Vector3.Dot(transform.forward, collisionDirection) <= 0.5f) { velocity = new Vector3(0f, velocity.y, 0f); } } // 現在のジャンプ値の計算 if (jumpValue > -initJumpHeightValue) { jumpValue += Physics.gravity.y * Time.fixedDeltaTime; animator.SetFloat("JumpPower", jumpValue); } // ジャンプ中はジャンプ時間を計測する if (isJump && jumpTime < jumpWaitTime) { jumpTime += Time.deltaTime; } // 移動処理 rigidBody.AddForce(rigidBody.mass * velocity / Time.fixedDeltaTime, ForceMode.Force); } |
接地時のif文でisGroundedだけでなくisClimbedの時も条件に加えます。
制限した移動値の大きさが0より大きければClimbTheStairsメソッドを呼び出します。
そうでない場合はisClimbedをfalseにし移動値がない場合は階段にも上っていない状態にします。
ジャンプ中等に横壁と接触している時の処理にisClimbedの条件も加えます。
これは段差を昇る時に重力と反対方向に上っていく速度が必要な為です。
OnDrawGizmosメソッドに処理を追加します。
1 2 3 4 5 6 7 8 9 10 11 | private void OnDrawGizmos() { Gizmos.DrawWireSphere(transform.position + Vector3.up * 1.5f, 0.3f); var stepRayPosition = transform.position + stepRayOffset; Gizmos.color = Color.blue; Gizmos.DrawLine(stepRayPosition, stepRayPosition + transform.forward * stepDistance); Gizmos.color = Color.green; Gizmos.DrawLine(transform.position + new Vector3(0f, stepOffset, 0f), transform.position + new Vector3(0f, stepOffset, 0f) + transform.forward * slopeDistance); Gizmos.DrawWireSphere(transform.position, 0.29f); } |
前方に地面があるかどうかを調べるレイのギズモと登れる段差なのかどうかを調べる時に使うレイのギズモを表示しています。
キャラクターのゲームオブジェクトに取り付けたMakeRigidAddForceCharaのインスペクタは以下のようにしました。
キャラクターに弾を当てて確認してみる
RigidbodyのAddForceを使ったキャラクタースクリプトが出来たので、四方から弾が飛んでくるようにし、キャラクターがどのように動くか確認してみます。
ヒエラルキーで右クリックから3D Object→Sphereを選択し、名前をBulletにします。
BulletのインスペクタのAdd ComponentからPhysics→Rigidbodyを取り付けます。
BulletゲームオブジェクトのLayerには新しくBulletレイヤーを作成し設定します。
Scaleを0.3にし少し小さくします。
RigidbodyのMassを5にし、Use Gravityのチェックを外し重力が働かないようにします。
ここまで出来たらAssetsフォルダ内にドラッグ&ドロップしてプレハブにします。
ヒエラルキーのBulletは削除します。
Bulletレイヤー同士は衝突させたくないので、UnityメニューのEdit→Project Settings→Physicsを選択し、Layer Collision MatrixのBullet同士のチェックを外します。
ヒエラルキーで右クリックしてCreate Emptyを選択し、名前をBulletAttackとします。
BulletAttackゲームオブジェクトには新しくBulletAttackスクリプトを作成し取り付けます。
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class BulletAttack : MonoBehaviour { [SerializeField] private GameObject bulletObj; [SerializeField] private float range = 10f; // Start is called before the first frame update void Start() { StartCoroutine(Shot()); } IEnumerator Shot() { while (true) { var bulletIns = Instantiate(bulletObj); var bulletRigidbody = bulletIns.GetComponent<Rigidbody>(); bulletIns.transform.position = new Vector3(Random.Range(-range, range), Random.Range(0f, 4.5f), -range); bulletRigidbody.AddForce(bulletRigidbody.mass * Vector3.forward * 1000f); Destroy(bulletIns, 5f); var bulletIns2 = Instantiate(bulletObj); var bulletRigidbody2 = bulletIns2.GetComponent<Rigidbody>(); bulletIns2.transform.position = new Vector3(-range, Random.Range(0f, 4.5f), Random.Range(-range, range)); bulletRigidbody2.AddForce(bulletRigidbody2.mass * Vector3.right * 1000f); Destroy(bulletIns2, 5f); var bulletIns3 = Instantiate(bulletObj); var bulletRigidbody3 = bulletIns3.GetComponent<Rigidbody>(); bulletIns3.transform.position = new Vector3(range, Random.Range(0f, 4.5f), Random.Range(-range, range)); bulletRigidbody3.AddForce(bulletRigidbody3.mass * Vector3.left * 1000f); Destroy(bulletIns3, 5f); var bulletIns4 = Instantiate(bulletObj); var bulletRigidbody4 = bulletIns4.GetComponent<Rigidbody>(); bulletIns4.transform.position = new Vector3(Random.Range(-range, range), Random.Range(0f, 4.5f), range); bulletRigidbody4.AddForce(bulletRigidbody4.mass * Vector3.back * 1000f); Destroy(bulletIns4, 5f); yield return new WaitForSeconds(0.01f); } } } |
bulletObjは先ほど作ったBulletプレハブをインスペクタで設定します。
rangeは弾が飛んでくる範囲として使用します。
コルーチンを使って0.01秒毎に四方向から弾のプレハブからインスタンスを生成し飛ばしています。
BulletAttackゲームオブジェクトのインスペクタにAssetsフォルダにあるBulletプレハブをドラッグ&ドロップして設定します。
実行してみると以下のようになりました。
地面とするレイヤーを設定しない
今回は地面とする全てのレイヤーをレイヤーマスクに指定して処理をしていますが、設定をしなくても全てのゲームオブジェクトを地面としてもいいかもしれません。
そうすると各処理は以下のようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // 地面のチェック private void CheckGround() { // キャラクターに設定したPlayerレイヤー以外のものと接触したら地面に接地 if (Physics.CheckSphere(rigidBody.position + groundPositionOffset, groundColliderRadius, ~LayerMask.GetMask("Player"))) { // ジャンプ中 if (isJump) { if (jumpTime >= jumpWaitTime) { isGrounded = true; isJump = false; } else { isGrounded = false; } } else { isGrounded = true; } } else { isGrounded = false; } animator.SetBool("IsGrounded", isGrounded); } |
CheckGroundメソッドでどのレイヤーと接触したかを調べる時に~LayerMask.GetMask(“Player”)を使ってPlayerレイヤーのレイヤーマスクを作りそれにチルダ~を付けて反転させたもの(Playerレイヤー以外のレイヤーマスク)を指定します。
これはキャラクター自身のコライダと接触したのが判定されるのを防ぐ為です。
キャラクターにはPlayerレイヤーを設定していることとします。
次にOnCollisionStayメソッド内を修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | private void OnCollisionStay(Collision collision) { // 地面と接触している isCollision = true; // 衝突した最初の面の向きを入れる collisionDirection = collision.contacts[0].normal; if (jumpValue > 0f && Physics.CheckSphere(rigidBody.position + collisionCeilingOffset, collisionCeillingColliderRadius, ~LayerMask.GetMask("Player")) ) { jumpValue = 0f; animator.SetFloat("JumpPower", jumpValue); } } |
レイヤー判定をしていたif文を削除し、そのまま処理をします。
天井にぶつかったときの処理で~LayerMask.GetMask(“Player”)を指定します。
次にOnCollisionExitメソッド内を修正します。
1 2 3 4 5 6 7 | private void OnCollisionExit(Collision collision) { // 地面とするレイヤーから離れた isCollision = false; collisionDirection = Vector3.zero; } |
レイヤー判定を削除します。
次にClimbTheStairsメソッド内を修正します。
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 | private void ClimbTheStairs() { var stepRayPosition = rigidBody.position + stepRayOffset; // ステップ用のレイが地面に接触しているかどうか if (Physics.Linecast(stepRayPosition, stepRayPosition + rigidBody.transform.forward * stepDistance, out var stepHit)) { var angle = Vector3.Angle(transform.up, stepHit.normal); var clampedInput = Vector3.ClampMagnitude(input, 1f); // 進行方向の地面の角度が指定以下、または昇れる段差より下だった場合の移動処理 if (angle <= slopeLimit) { // 指定角度以下の場合は普通の力のまま //Debug.Log("坂: " + angle); velocity = Quaternion.FromToRotation(Vector3.up, stepHit.normal) * clampedInput * walkSpeed; velocity = velocity - rigidBody.velocity; velocity = Vector3.ClampMagnitude(velocity, walkSpeed); } else if (angle > slopeLimit && !Physics.Linecast(rigidBody.position + new Vector3(0f, stepOffset, 0f), rigidBody.position + new Vector3(0f, stepOffset, 0f) + rigidBody.transform.forward * slopeDistance) ) { // 角度確認 //Debug.Log("階段: " + angle); velocity = Quaternion.FromToRotation(Vector3.up, stepHit.normal) * clampedInput * walkSpeed; velocity = Vector3.ClampMagnitude(velocity, stepUpwardSpeed); velocity = Vector3.ClampMagnitude(velocity - rigidBody.velocity, stepUpwardSpeed); isClimbed = true; } else { // 完全に速度を0または強制で下向きの速度にする velocity = Vector3.zero; } } else { // 前方に坂や階段がない場合 //velocity.y = 0f; isClimbed = false; } } |
レイヤー判定していた部分でレイヤーを指定しないようにします。
地面とするレイヤーを使わないのでgroudLayersフィールドは削除します。
終わりに
RigidbodyのAddForceを使ったキャラクター移動スクリプトは難しいですね。
もっとうまいやり方がありそうです・・・・・(´Д`)