UnityのRigidbodyのAddForceを使ったキャラクターの移動スクリプト

記事内に広告が含まれています。

今回はカプセルコライダとRigidbodyのAddForceメソッドを使ってキャラクター移動スクリプトを作成していきます。

以前、カプセルコライダとRigidbodyを使ったキャラクター移動スクリプトを作成しましたが、そちらの場合はMovePositionを使って移動処理をしていたので、厳密には小刻みにワープをして移動していました。

キャラ操作をCharacterControllerからRigidbody+コライダに変更する
Unityでキャラクターを操作する時CharacterControllerを使ってきましたが、物理的な影響を得る為にRigidbody+CapsuleColliderでキャラクターの移動、ジャンプ、坂や段差を登れるようにしてみます。

今回はRigidbodyのAddForceを使って移動時になるべくすり抜け等が起きないようにします。

今回の機能を作成すると以下のような感じになります。

ただ色々改善の必要はあるかもしれません。(´Д`)

処理をだいぶ変更しました(2021/09/07)。
スポンサーリンク

キャラクターの設定

まずは人型キャラクターモデルを用意してヒエラルキーに配置し、RigidbodyコンポーネントとCapsuleColliderコンポーネントを取り付け、Rigidbodyの設定とCapsuleColliderのコライダのサイズを調整します。

AnimatorControllerを作成し、AnimatorコンポーネントのControllerに設定します。

アニメーションパラメーターにはFloat型のSpeedとJumpPower、Bool型のIsGrounded、Trigger型のJumpを作成します。

Idle状態、Walk状態、Jump状態を作成します。

RigidbodyのAddForceキャラクターのアニメーターコントローラー

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のチェックを外しておきます。

アニメーターコントローラーについては

Unityのアニメーションの切り替えシステムとスクリプト
Unityのアニメーションの切り替えシステムであるAnimatorControllerの設定とスクリプトからアニメーションを制御していきます。

Jumpのブレンドツリーについては

Unityのキャラクターでジャンプアップから着地までのアニメーションを変更する
UnityのCharacterControllerコンポーネントを使ったキャラクターでジャンプアップから着地までのアニメーションを変更するようにします。

を参照してください。

設定をしたキャラクターゲームオブジェクトのインスペクタで以下のように設定しました。

RigidbodyのAddForceで移動するキャラクターゲームオブジェクトのインスペクタの設定

キャラクター用のスクリプトを作成

キャラクター用のスクリプトを作成していきます。

MakeRigidAddForceCharaスクリプトを作成し、先ほどヒエラルキーに配置したキャラクターに取り付けます。

一気に作ると大変なので、徐々に作成していきます。

基本的な移動部分の作成

まずはキャラクターの基本的な移動と接地の確認が出来るようにします。

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で与える力に関しては以下を参照してください。

UnityのRigidbodyのAddForceに加える力とオプション
UnityのRigidbodyのAddForceメソッドの第1引数に指定する力と、第2引数に指定するオプションについて見ていき、理解を深めます。

次にCheckGroundメソッドを見ていきます。

キャラクターの現在位置+groundPositionOffsetにgroundColliderRadiusの半径の球を作成し、groundLayersで設定したレイヤーを持つコライダと接触した場合は接地とします。

条件が成立したらisGroundedをtrue、それ以外はfalseとし、その後アニメーションパラメーターのIsGroundedをisGroundedに設定しています。

ヒエラルキーで右クリックから3D Object→Planeを選択して名前をFloorとし、LayerにGroundを作り設定します。

AddForceで動くキャラクターが移動する床の作成

キャラクターを動かしてみると床の上を移動します・・・・が、ここでキャラクターは加速度的に移動し、瞬く間に床を外れてしまいます。(^_^;)

RigidbodyのAddForceで力を加えると加速度的に移動してしまう。

ずっと押していても一定値に制限する

今回はRigidbodyのAddForceメソッドを使って力を加えてキャラクターを動かしますが、CharacterControllerの時のように毎フレーム力を加えると加速度的に移動してしまいます。

これはRigidbodyに一度力を加えると抵抗する力が働かない限りずっと力が加わってしまう為です。

そこでAddForceに加える力をwalkSpeedで設定した速さ以上にはならないようにします。

そこで計算したvelocity(速度)から現在のRigidbodyの速度を引いたものを改めてvelocityに代入し、さらにそのvelocityがwalkSpeedを超えないように制限します。

transform.LookAtメソッドの下に以下のスクリプトを挿入します。

最初に計算した速度から現在の速度を引いてAddForceに与える力を制限しています。

例えば現在キャラクターが左に(4, 0, 0)で移動しているとして

今入力をして、velocityが(4, 0, 0)で左側に押しっぱなしの状態にすると

(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の範囲内に制限しています。

これで移動キーを押しっぱなしにしても一定速度でしか移動しないようになりました。

ジャンプ機能の作成

基本的な移動機能が出来たのでジャンプ機能を取り付けます。

まずはフィールドを追加します。

pushJumpButtonはジャンプボタンを押したかどうか
jumpHeightはジャンプする高さ
initJumpHeightValueは初期のジャンプ値
jumpValueは現在のジャンプ値
isCollisionは天井にぶつかっているかどうか
collisionPositionOffsetは衝突確認の球の位置のオフセット値
collisionColliderRadiusは衝突確認の球の半径
collisionCeilingOffsetは天井衝突確認用の球の位置のオフセット値
collisionCeilingColliderRadiusは天井衝突確認の球の半径

です。

次にUpdateメソッドにJumpボタンが押された時の処理を追加します。

Jumpボタンを押した時、かつisGroundedがtrueの時にpushJumpButtonをtrueにします。

FixedUpdateメソッド内でジャンプボタンを押したかどうかを判定してもいいのですが、FixedUpdateメソッドの場合はデフォルトで0.02秒毎に実行するので、Updateメソッドよりも実行する回数が少なくなっています。

その為、Jumpボタンを押してもその処理を捉えられずジャンプ出来ないことがあります。

今回は移動値の計算をFixedUpdateメソッドで行っていますが、こちらもUpdateメソッドで移動値の計算をし、FixedUpdateメソッドではその値を使った移動処理を行う方がより細かい値を使えます。

接地時の処理にジャンプボタンを押した時の処理を追加します。

移動値によりアニメーションを変更した後に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を直接変更します。

接地時の処理が終わった所で以下の処理を追加します。

現在のジャンプ値がマイナスのジャンプの初期値以上の時は現在のジャンプ値を更新します。

jumpValueはPhysics.gravity.y(重力加速度)にTime.fixedDeltaTimeを掛けて1フレームでの落下速度分をjumpValueに足します。

これで重力分(Physics.gravity.yは初期値で-9.81なので下向きです)のジャンプ値が現在のジャンプ値から引かれます。

計算が終わったらジャンプ値をアニメーションパラメーターのJumpPowerに入れます。

次にジャンプ中でジャンプ時間がジャンプの待ち時間以下の場合はjumpTimeに時間を足していきます。

このjumpTimeは何かというと、ジャンプ後に直ぐに接地と判断されるのを避けるための時間の計算で、jumpWaitTimeを超えたジャンプ時間にならない限りは接地をさせないようにします。

この処理をする為にCheckGroundメソッドを変更します。

接地確認の為の球にgroundLayersに指定したレイヤーが接触している時にジャンプ中であればジャンプの経過時間がjumpWaitTime以上になった時は接地とし、isJumpをfalseにします。

ジャンプの経過時間がjumpWaitTimeより下の場合はisGroundedはfalseにします。

そもそもジャンプ中でない場合はisGroundedをtrueにし、接地しているとします。

球がgroundLayersのレイヤーと接触していなければisGroundedはfalseにします。

天井にぶつかった時の処理

天井を作ります。

ヒエラルキーで右クリックから3D Object→Cubeを選択し、名前をCeilingとします。

CeilingのインスペクタでLayerをGroundにします。

Ceilingゲームオブジェクトはキャラクターのジャンプの最高到達点よりも低い位置に置き、キャラクターを天井に向けてジャンプさせて確認します。

ジャンプは出来ましたが、ジャンプアニメーションはアニメーションパラメーターのJumpPowerの値によって、

ジャンプアップ→最高点付近→落下

というアニメーションの切り替えを行っています。

これはジャンプしてぶつからずに重力で落下することを前提としていて、JumpPowerに渡すJumpValueの値の更新も重力に合わせた変化なので、頭の上に天井があってぶつかった場合もアニメーションの変化は天井にぶつかっていない時と同じなのでジャンプアップ→最高点付近→落下というアニメーションの遷移がされてしまいます。

天井にぶつかってもジャンプアニメーションが最後まで再生される

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フィールドを作成します。

OnCollisionStayメソッド内で衝突した相手の面の方向をcollisionDirectionに入れます。

衝突した相手のcollisionから衝突した複数個所の0番目だけを取得し、その法線(面の表向き)をcollisionDirectionに入れます。

これは衝突した面とキャラクターの向きからどの程度の角度がついているかを計算するのに使います。

OnCollisionExitメソッドに処理を追加します。

衝突から抜け出した時にcollisionDirectionに0を入れます。

FixedUpdateメソッドの接地時の処理ブロックが終わったすぐ後に以下の処理を追加します。

isGroundedがfalseで、かつisCollisionがtrueの時は空中でGroundレイヤーを設定したゲームオブジェクトと接触しているのでその時にVector3.Dotメソッドを使ってキャラクターの向いている向きと衝突した相手の面の方向からベクトルの内積を求めます。

内積は1であればまったく同じ方向を向き、-1であれば反対方向を向いている状態です。

今回の場合は0.5以下の時にvelocityのXとZを0にして上下の速度だけを反映させます。

0.5以下なのでキャラクターが衝突面に対して真横から衝突面の真正面の間の場合は移動値を制限するということになります。

壁と接触している間に移動しても移動値を反映しないようにする

これでジャンプ中や落下時に空中浮遊することがなくなりました。

Terrainの地面や階段が登れない問題

次にゲームの舞台にTerrainを使ってデコボコの地面を作ったり、階段を作った場合の移動の問題があります(滑らかな坂は上れます)。

ジャンプ機能を搭載したので段差がある場所はジャンプで進ませるというのもありですが、いちおうこれにも対応しておきます。

基本的には以前のRigidbody+CapsuleColliderでキャラクターを動かした記事でやっていたことと同じなので詳細はそちらを参照してみてください。

キャラ操作をCharacterControllerからRigidbody+コライダに変更する
Unityでキャラクターを操作する時CharacterControllerを使ってきましたが、物理的な影響を得る為にRigidbody+CapsuleColliderでキャラクターの移動、ジャンプ、坂や段差を登れるようにしてみます。

フィールドを追加します。

stepUpwardSpeedは階段を上る時の上向きの速さを設定します。
stepRayOffsetは前方に段差があるかどうかを調べるレイを飛ばすキャラクター位置からのオフセット値を設定します。
stepDistanceは前方に段差があるかどうかを調べるレイを飛ばす距離を設定します。
stepOffsetは登れる段差を設定します。
slopeLimitは登れる坂の角度を設定します。
slopeDistanceは登れる段差の位置から飛ばすレイの距離を設定します。
isClimbedは階段を上っている最中かどうかのフラグです。

次に階段や坂を上る処理を行うClimbTheStairsメソッドを作成します。

最初にキャラクターの現在位置にstepRayOffsetの値を足して、レイを飛ばす位置をstepRayPositionに入れます。

stepRayPositionからレイを飛ばしgroundLayersに設定したレイヤーと接触したらその情報をstepHitに入れます。

キャラクターの上方と衝突した相手のゲームオブジェクトの前面の角度を計算しangleに入れます。

入力値に制限を加えた値をclampedInputに入れます。

アングルがslopeLimit以下であれば登れる坂なので、上方と前方の速度を計算し通常の移動時と同じように速度に制限を加えています。

アングルがslopeLimitより大きく、かつキャラクターの位置にstepOffsetを足した位置から前方にレイを飛ばしgroundLayersと接触していなければ上れる段差になるので、速度を計算します。

前方の階段が90度の場合は上向きの速さが大きくなってしまうので、別途stepUpwardSpeedの値を使って制限をしています。

階段を上る場合はisClimbedにtrueを入れ、段差を昇っている最中だとします。

angleが指定した条件に引っかからなかった場合はvelocityに0を入れています。

ステップ用のレイがそもそも壁等に接触しなかった場合はisClimbedをfalseにします。

FixedUpdateメソッドを変更します。

途中に色々処理を入れているのでFixedUpdateメソッドの全部を載せています。

接地時のif文でisGroundedだけでなくisClimbedの時も条件に加えます。

制限した移動値の大きさが0より大きければClimbTheStairsメソッドを呼び出します。

そうでない場合はisClimbedをfalseにし移動値がない場合は階段にも上っていない状態にします。

ジャンプ中等に横壁と接触している時の処理にisClimbedの条件も加えます。

これは段差を昇る時に重力と反対方向に上っていく速度が必要な為です。

OnDrawGizmosメソッドに処理を追加します。

前方に地面があるかどうかを調べるレイのギズモと登れる段差なのかどうかを調べる時に使うレイのギズモを表示しています。

キャラクターのゲームオブジェクトに取り付けたMakeRigidAddForceCharaのインスペクタは以下のようにしました。

MakeRigidAddForceCharaの最終的なインスペクタの設定

キャラクターに弾を当てて確認してみる

RigidbodyのAddForceを使ったキャラクタースクリプトが出来たので、四方から弾が飛んでくるようにし、キャラクターがどのように動くか確認してみます。

ヒエラルキーで右クリックから3D Object→Sphereを選択し、名前をBulletにします。

BulletのインスペクタのAdd ComponentからPhysics→Rigidbodyを取り付けます。

AddForceで動かすキャラクターのサンプルで使うBulletの設定

BulletゲームオブジェクトのLayerには新しくBulletレイヤーを作成し設定します。

Scaleを0.3にし少し小さくします。

RigidbodyのMassを5にし、Use Gravityのチェックを外し重力が働かないようにします。

ここまで出来たらAssetsフォルダ内にドラッグ&ドロップしてプレハブにします。

ヒエラルキーのBulletは削除します。

Bulletレイヤー同士は衝突させたくないので、UnityメニューのEdit→Project Settings→Physicsを選択し、Layer Collision MatrixのBullet同士のチェックを外します。

AddForceで動かすキャラクターのサンプルで使用するBulletのレイヤー同士が衝突しないようにする

ヒエラルキーで右クリックしてCreate Emptyを選択し、名前をBulletAttackとします。

BulletAttackゲームオブジェクトには新しくBulletAttackスクリプトを作成し取り付けます。

bulletObjは先ほど作ったBulletプレハブをインスペクタで設定します。

rangeは弾が飛んでくる範囲として使用します。

コルーチンを使って0.01秒毎に四方向から弾のプレハブからインスタンスを生成し飛ばしています。

BulletAttackゲームオブジェクトのインスペクタにAssetsフォルダにあるBulletプレハブをドラッグ&ドロップして設定します。

AddForceで動くキャラクターのサンプルで使用するBulletAttackゲームオブジェクトのインスペクタの設定

実行してみると以下のようになりました。

地面とするレイヤーを設定しない

今回は地面とする全てのレイヤーをレイヤーマスクに指定して処理をしていますが、設定をしなくても全てのゲームオブジェクトを地面としてもいいかもしれません。

そうすると各処理は以下のようにします。

CheckGroundメソッドでどのレイヤーと接触したかを調べる時に~LayerMask.GetMask(“Player”)を使ってPlayerレイヤーのレイヤーマスクを作りそれにチルダ~を付けて反転させたもの(Playerレイヤー以外のレイヤーマスク)を指定します。

これはキャラクター自身のコライダと接触したのが判定されるのを防ぐ為です。

キャラクターにはPlayerレイヤーを設定していることとします。

次にOnCollisionStayメソッド内を修正します。

レイヤー判定をしていたif文を削除し、そのまま処理をします。

天井にぶつかったときの処理で~LayerMask.GetMask(“Player”)を指定します。

次にOnCollisionExitメソッド内を修正します。

レイヤー判定を削除します。

次にClimbTheStairsメソッド内を修正します。

レイヤー判定していた部分でレイヤーを指定しないようにします。

地面とするレイヤーを使わないのでgroudLayersフィールドは削除します。

終わりに

RigidbodyのAddForceを使ったキャラクター移動スクリプトは難しいですね。

もっとうまいやり方がありそうです・・・・・(´Д`)

参考サイト

Unity Community-RigidbodyFPSWalker-

タイトルとURLをコピーしました