TPS型のキャラ移動を作る【ゲーム開発日記】

今回の記事はUnityを使ってTPS型で物理演算のキャラ移動を作った際の覚え書きです。
まずこの記事では、壁と床を区分して床の角度に応じた移動方向を決めるまでの過程についてまとめてみました。

なおこちらは前の開発日記の続きなので、よろしければそちらも見てみてください!↓

実装方法を決める

まずキャラクターオブジェクトの移動をどんなものにするか決めます。
前はUnityのCharacter Controllerを使っていましたが、今回は物理演算を使うことにしました。
物理演算は以前「慣性がつくとキャラを動かしづらい」って思って使わなかったんですが、スプラトゥーン3を始めてから「慣性は制御次第」「キャラが物に乗って移動したり衝突する挙動は物理演算じゃないとしんどい」って思って今回挑戦することに。

…実は、物理演算の効くCharacter Controllerアセットを持っています。

Kinematic Character Controller | Unity Asset Store

Get the Kinematic Character Controller package from Philippe St-Amand and speed up your game development process.

ただ何度かやってみてどうしても自分の操作するイメージと合わせることができなかったので、僕はとりあえず使うのを保留しています…。
TPSカメラもついて複雑な移動にも対応しているアセットなので、とりあえずシンプルに実装だけしてしまいたい方はこういうものを使ってみるのもオススメです。(他にもキャラ移動系のアセットは有料無料問わずいろいろあります)

欲しい機能をまとめる

まずキャラ移動に自分が欲しい機能を考えてみます。
前のプロジェクトではPCとスマホ両方で操作できるようにしていてまとまりきらず微妙だったので、今回はコントローラー限定と割り切ってしまいます。

  • コントローラーのスティックの入力で移動する。入力の強さで速さが変わる。
  • カメラはTPS型でキャラを追う。カメラの回転とキャラの回転は独立している。(常に背後視点じゃなくキャラに手前を向かせることもできる)
  • 空中では落下する。
  • ジャンプ入力した時や攻撃を受けた時などに指定した方向・勢いで飛ぶ。
  • 滞空中にどのくらい入力を受け付けるかは指定できる。(敵から殴られて飛ぶ場合はプレイヤーが動かせないとか)

他にもゲームの仕様によって「足場が悪いところを歩く」とか「水中を泳ぐ」「空を飛ぶ」「二段ジャンプ」とか色々ありそうですが、今回そこらへんは考慮しないものにしようと思います。

移動に必要な情報

物理演算で移動するにしろCharacter Controllerや他のアセット等で移動するにしろ、移動方向をVector3で生成する必要があります。

TPSでカメラの向きとキャラの向きが独立して動くようにする場合、移動方向を算出するのに必要な情報はこんな感じです。

  • プレイヤーの入力方向(Vector2で受け取ってyが0のVector3として扱う)
  • カメラの向き(Quaternion)
  • キャラが立っている場所の傾斜(法線で取得)

移動方向のVector3はカメラのQuaternion * 入力方向のVector3で求められます。
カメラのQuaternionとはTransform.rotationのことです。y軸回転のみのものが必要です。

僕の場合、カメラはEmptyのオブジェクトを親にしてその中に作成することにしました。
この親は常にプレイヤーキャラと同じ位置に移動するようにし、y軸での回転しかしないようにします。子オブジェクトとしてプレイヤーキャラの後方にCameraを置き、上下回転とズームを担当させます。

これで親のTransform.rotationをそのまま移動の情報として使い、平面上での移動方向を算出することができます。

床の上を移動する

まず床に乗っている時の移動から考えていきます。
床の上で移動する場合は、床の面に沿って移動方向を決めなければいけないので少し複雑です。

今乗っている床を決める

プレイヤーキャラは床と壁などに同時に接触することができるので、どれが床なのかを判別しないと移動ができません。
今回は以下のようにしてみることにしました。

  • 床として歩ける角度の最大値を設定し、越えていないものだけを床とする
  • 同時に触れた中で一番水平に近い床を現在乗っている床とする

床や壁等のオブジェクトはOnCollisionEnterやOnCollisionStayで接触の情報が取れるので、こちらを使います。
ただし複数のオブジェクトに触れた場合はこれらが同じフレームの中で複数回呼ばれ、どれが現在の床なのかは分からないです。

今回は、同じフレームの中で角度が範囲内だったもののうち一番角度が上向きの水平だったものを取得するように考えます。

水平かどうかを確認するには、床・壁等の接触した点の法線が必要です。
接触した情報はOnCollisionEnter等のCollisionのGetContact(int index)からContactPointとして取得することができます。このContactPointに法線の情報が含まれており、ContactPoint.normalで取得できます。

ただし、これも複数接触の判定があります。
接触した壁・床等のオブジェクトに複数の物理演算オブジェクトが接触している場合、全部を配列で取得するにはGetContacts(ContactPoint[] contacts)関数を使う必要があります。
まず十分な長さ(ゲーム内で同時に接触する可能性のある数)の空のContactPoint配列を作ります。
これをGetContacts()の引数にして実行すると、その配列に現在のContactPointを全部入れてくれます(戻り値は入れて返された個数なので、for文で処理できます)。

ここで少々詰まったのですが、複数接触している点を配列で取得することはできてもプレイヤーキャラのものがどれなのかは単純には分かりません。
Vector3.Distanceで全部の距離を算出してプレイヤーキャラに一番近い点を探してもいいのですが、オブジェクトの数が多い場合に毎フレームやるのはしんどそうな気がします…。

そこで今回僕は「レイヤーを使って多重構造にする」という方法でやってみることにしました。
プレイヤーキャラ・プレイヤーキャラ用ステージ・他物理オブジェクト・他物理オブジェクト用ステージの4つのレイヤーを用意し、それぞれのオブジェクトに設定します。
プレイヤーキャラは専用のステージの中を移動し、他の物理オブジェクトは別の専用のステージの中を動きます。
2つのステージは全く同じ形で同じ位置に設置します。
そしてプレイヤーキャラと他の物理オブジェクト同士は接触できるようにします。

これでスクリプトでの判定をしなくてもプレイヤーキャラの壁や床の接触地点は常に必ず1つという状態になります。
(このためにレイヤーを4つ使うのが良いのか同時接触した全オブジェクトの距離を毎フレーム判定するのが良いのかは正直分からないんですが…)

プレイヤーキャラの接触地点が必ず1つと決まれば、法線はCollision.GetContact(0).normalで取得できます。
法線が取得できたら床の角度を算出します。
角度はVector3.Angle(基準方向, 対象の方向)で0~180度の範囲で分かるので、Vector3.upとの間で角度を算出すれば床の角度が分かります。

一番角度が小さいものを現在の床とします。

坂の傾斜を反映させる

当初は物理演算って坂の傾斜を考慮しなくても重力とか摩擦とかで何とかしてくれるのでは?と思っていましたが、そんなことはなく下り坂では空中に飛び出してしまいます。
上り坂に押し込まれてしんどそうな感じはある意味リアルでしたが、ゲームとしてはどちらも駄目なので坂に沿って移動できるようにします。

Unityで移動方向に床の角度を反映させるには、専用の関数があるのでそのまま使えばOKです。 Vector3.ProjectOnPlane(水平での移動方向, 床との接触地点の法線)です。
引数の2つのベクトルを入れれば平面に沿うように投影された状態のベクトルを返してくれます。

水平での移動方向はすでに決まっており接触している位置の法線のベクトルも床の判定で取れているので、これを両方そのまま使います。
注意点として、上記の図のような算出方法なので坂の角度が大きい場合は投影後のベクトルが短くなってしまいます。ベクトルの長さ=移動速度になることが多いと思うので、気になる場合はVector3.Normalizeなどを使って必要な長さになるよう調節が必要です。

以上で床に接している時の移動方向のベクトルが生成できます!
これを移動系アセットや物理演算のRigidbody.AddForce・Rigidbody.velocityなどに入れれば床に沿って移動ができるようになります。

スクリプトに処理を書く

物理演算をきちんとやるのが初めてだったせいで色々なところに詰まりポイントがありました。
特に詰まったのが以下の部分です。

  • 処理の実行順はFixedUpdate→OnTrigger系→OnCollision系→コルーチン(WaitForFixedUpdate)
  • OnCollision系などの物理演算は一定時間オブジェクトが動かなければスリープする

(参考)
https://docs.unity3d.com/ja/2023.2/Manual/ExecutionOrder.html
https://docs.unity3d.com/ja/2023.2/Manual/class-PhysicsManager.html

スリープのことを知らなくて、OnCollisionStayは接触していたらずっと呼ばれるんだと思っていたのでしばらく考え込みました。
スリープに入るまでの時間は設定で変更可能ではあるけれど負荷を軽減するためのものなので大幅な変更は望ましくない…とのこと。

「床に触れている間は移動入力を受け付ける」という処理をしたい場合でも、OnCollisionStayの中に移動処理を書いては駄目ということになります。(最初からキーを押しっぱなしなら動きますが、止めてスリープに入ったら動けなくなります)

そんなわけで、1つのフレーム内を以下のような実行順にしてみてとりあえずはうまく動くようになりました。

① OnCollision以外の判定処理(Rayとか)

② ①と前フレーム⑤の物理演算の判定に基づく壁・床接触などの状況の更新

③ 動作の停止・終了・開始などの処理

④ 移動のAddForceなどの処理

※ここで物理演算の更新処理が行われる

⑤ Collisionに関わる判定処理

⑥ 「指定フレーム数が経過したら別のモードに切り替え」のような処理
※yield return WaitForFixedUpdate(); を使うように気を付けます。

これで正しいのかは謎ですが、動作が詰まることはないし1フレームの遅延を感じることもないのでとりあえずこれでいこうかなと…。

ここまでの仕上がり

とりあえずはこんな感じです。まだ「床移動は出来るようになった」というだけなのでもっといろんな動きを作り込む必要があります。

記事が長くなってしまったので、「空中での移動」「ジャンプ」などは次の記事に分けました。

続きも見ていただければ嬉しいです!

シェアする

「Unity」の記事

もっと読む>>

最新記事

もっと読む>>