"Rigidbody"を使った動く足場、床の作り方、それに乗ったプレイヤーと足場を同期させる方法について。現時点での解決法のまとめ!
この行ったり来たりする、宙に浮く足場というものは、マリオをはじめたとしたアクションゲーム(海外で言うところの"Platformer")において、なくてはならないものです。
早い段階から作りたかったのですが、まともな形で実現できたのは最近になります。改善の余地はあるかと思いますが、とりあえず、経緯含め書き出してみたいと思います。
動く足場の課題
アクションゲームにおいて、動く足場というのは、なくてはならない仕掛けだと思います。上下、左右に往復する足場、これを踏み外せばゲームオーバー!!
よりダイナミックで、スリリングなレベルデザインが可能になるわけですが、その実現には、解決しなければいけない、ふたつの課題があります。
- 決まった幅を決まった周期で往復する。
ただ動けばいいわけではありません。コントロールされた反復運動である必要があります。ゲームが詰んでしまいますからね。
- 乗ったプレイヤーが、足場と一緒に動く
慣性の法則により、そこに乗ったプレイヤーも、足場と同期して動く必要があります。
いやそもそも宙に浮く足場って・・・、っていう突っ込みはナシで。
動く足場
まずUnityの場合、ゲームオブジェクトを動かすのに、二つの移動方法があります。TransformによるものとRigidbodyによるものです。これらを使って、まず上下左右を行ったりきたりする足場を作るわけです。
最初に思いついたのは、Transform由来の方法です。例えばVector3.Lerpを使い、あらかじめ設定した2つの地点を行ったり来たりする方法があります。ただしこの方法は、いちいち左右の地点を指定したり、ワンループごとに進行方向をif文などで切り替えないといけないんですよね。(違う方法あったらすいません。)これだけだと少しスマートではないな、と。
で、思いついたのがMathf.Sinを使う方法です。三角関数なんてすっかり忘れてますが、要はサイン波であり、反復運動なわけです。これを上手く使えれば、シンプルなコードに出来るんでは?と思いました。
行ったり来たり・・・、波です。
Mathf.Sinについて
Mathf.Sinというメソッドがあります。引数はfloatによるラジアン(radian)になります。ラジアンとは、その円の半径と同じだけの円周を取ったときの角度です。1ラジアンはおよそ57.3度です。
当然、円は360度で一周ですので、つまりおよそ6.283ラジアンをもって、サイン波でいうところの上がって下がって戻ってきたというワン・ループを達成することになります。要は、この運動を延々ループすればいいわけです。
具体的には、Mathf.Sin(0.0f) ~ Mathf.Sin(6.283f)までですね。先ほどの図を拡大したものですが、ワン・ループ内を時間ごとに区切っていくと、値が1と-1の間を行き来しているのが分かります。
返り値としては、時間軸毎に0 -> 1 -> 0 -> -1 -> 0
という感じで、図の交点が示すfloat値が帰ってきます。つまりゼロを中心とした反復運動になっているわけです。図で言えば、時間軸、つまり横軸をなくせば、単純な上下運動になる事がわかると思います。これを利用すれば、Unity内のオブジェクトの反復運動を再現できます。float "speed"を代入演算子でドンドン足していって、6.283以上になったら、0,0fにリセットして・・・という感じでループさせます。
初期位置をVector3 centerとして、そこを基点に、このMathf.Sinによる値を利用して、Transformを計算し、Update()を使い、transform.positionを随時指定していく、という方法になります。
これは見た動きの感じもスムーズですし、かなり満足行く結果になりました。しかし、実際にプレイヤーを操作して、そこに乗せてみると二番目の課題が!
プレイヤーが一緒に動かない!
プレイヤーキャラは、Sphereつまりボールを使っているのですが、実際乗せてみると、足場と同期せずに、そのままだとドンドンずれていきます。
そういう足場である、と言い通せば(仕様です、という魔法の言葉)いいレベルではあるかもしれませんが、やはり不自然ではあります。
ググッタ結果、コリジョン時にプレイヤーのparent(Transform.SetParentというメソッドもあります。)を足場に設定する、という方法を見つけました。それを試してみたんですけども、動きは鈍くなり、かなり不自然な動作になってしまいました。足場側のScaleの影響で乗った瞬間、形がおかしくなったりします。より細かい設定が必要なのかもしれませんが、この方法はあきらめました。
ということで、とりあえずこの問題は保留に。
Rigidbodyによる解決
この際、Rigidbodyを使って動かして、プレイヤーのRigidbodyと同期させたらどうなんだろう?と思ったんですが、まず足場が上手く動かせなかったんですよね。
で後日、Rigidbody.MovePositionを使えばいいことに気がつきました。先ほどのMathf.Sinと、このMovePositonを使って足場を動かします。この際、足場のRigidbodyの設定として、rotation.yはfreezeにしてクルクルしないように設定。
そして足場のOnCollisionStayに接触したプレイヤーから、Rigidbodyの参照を得るコードを書きます。でそのRigidbody、ここではplayerRbとしてます。
追記! 大事なことをかき忘れてましたが、この方法では操作キャラと足場のRigidbodyのMassが同じであることが必須です。違うとおかしなことになります。
そのplayerRb.positionを足場の移動先のVector3である、movePositionを代入するとどうなるかというと、足場の中央に埋まったボールが、足場と一緒に動きます。これは使えるな、と思いました。それからいろいろ試したんですが、結果はシンプルなコードになりました。
playerRb.position = movePos + playerRb.collider.transform + transform.position;
こんな感じになりました。どうなんですかね、よくわかんないっす。
movePosというのは足場の動く位置ですね、これはあくまで相対的な位置、足場内部での位置関係なので、プレイヤーと足場のワールド座標も足して、調整してるという感じになると思います。とりあえずこれでひとまずそれっぽくはなりました。
ただし、この方法には明らかな問題が見つかっています。Interpolationの設定です。通常、プレイヤーのInterplation設定は、Interpolationに設定し、それ以外のRigidbodyに関しては、オフにすべきだということが推奨されています。
しかし、この方法において、プレイヤのInterpolationがオンになっていると、乗った瞬間からガクガクになります。解決法としては、乗ってOnCollisionEnterが効いている間Interpolationをnoneにするという手段をとっています。
ひとまず、まとめ
というわけで結構なゴリ押しで、いろいろ試してみた結果、よくわからんけど上手くいった!という流れなので、これでいいのか?という気もします。
実際、何かしら問題もあるのかもしれませんが、表面的な動作はかなり自然です。まだまだ検証は必要かと思いますが、しかしまともなゲームを作るためのピースはそろってきたように思います!動く足場があれば、実際ゲームとしての幅は、かなり広がりますからね!Unityかなり面白くなってきました!!!