Unityでインディゲームを作る!

Unityでのゲーム制作を目指し、それに関わる話題についてのブログ

動く足場とプレイヤーが一緒に動くためには!Rigidbodyによる対処法検証!その1

 "Rigidbody"を使った動く足場、床の作り方、それに乗ったプレイヤーと足場を同期させる方法について。現時点での解決法のまとめ!

 この行ったり来たりする、宙に浮く足場というものは、マリオをはじめたとしたアクションゲーム(海外で言うところの"Platformer")において、なくてはならないものです。

 早い段階から作りたかったのですが、まともな形で実現できたのは最近になります。改善の余地はあるかと思いますが、とりあえず、経緯含め書き出してみたいと思います。

 

動く足場の課題

 アクションゲームにおいて、動く足場というのは、なくてはならない仕掛けだと思います。上下、左右に往復する足場、これを踏み外せばゲームオーバー!!

よりダイナミックで、スリリングなレベルデザインが可能になるわけですが、その実現には、解決しなければいけない、ふたつの課題があります。 

  • 決まった幅を決まった周期で往復する。

 ただ動けばいいわけではありません。コントロールされた反復運動である必要があります。ゲームが詰んでしまいますからね。

  • 乗ったプレイヤーが、足場と一緒に動く

 慣性の法則により、そこに乗ったプレイヤーも、足場と同期して動く必要があります。 

いやそもそも宙に浮く足場って・・・、っていう突っ込みはナシで。

 

動く足場

 まずUnityの場合、ゲームオブジェクトを動かすのに、二つの移動方法があります。TransformによるものとRigidbodyによるものです。これらを使って、まず上下左右を行ったりきたりする足場を作るわけです。

 

 最初に思いついたのは、Transform由来の方法です。例えばVector3.Lerpを使い、あらかじめ設定した2つの地点を行ったり来たりする方法があります。ただしこの方法は、いちいち左右の地点を指定したり、ワンループごとに進行方向をif文などで切り替えないといけないんですよね。(違う方法あったらすいません。)これだけだと少しスマートではないな、と。

 

 で、思いついたのがMathf.Sinを使う方法です。三角関数なんてすっかり忘れてますが、要はサイン波であり、反復運動なわけです。これを上手く使えれば、シンプルなコードに出来るんでは?と思いました。

f:id:miur-us:20170709010202p:plain

行ったり来たり・・・、波です。

 

Mathf.Sinについて

 Mathf.Sinというメソッドがあります。引数はfloatによるラジアン(radian)になります。ラジアンとは、その半径と同じだけの円周を取ったときの角度です。1ラジアンはおよそ57.3度です。

 当然、円は360度で一周ですので、つまりおよそ6.283ラジアンをもって、サイン波でいうところの上がって下がって戻ってきたというワン・ループを達成することになります。要は、この運動を延々ループすればいいわけです。

 具体的には、Mathf.Sin(0.0f) ~ Mathf.Sin(6.283f)までですね。先ほどの図を拡大したものですが、ワン・ループ内を時間ごとに区切っていくと、値が-1の間を行き来しているのが分かります。 

f:id:miur-us:20170709010247p:plain 

返り値としては、時間軸毎に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が効いている間Interpolationnoneにするという手段をとっています。

  

ひとまず、まとめ

 というわけで結構なゴリ押しで、いろいろ試してみた結果、よくわからんけど上手くいった!という流れなので、これでいいのか?という気もします。

 実際、何かしら問題もあるのかもしれませんが、表面的な動作はかなり自然です。まだまだ検証は必要かと思いますが、しかしまともなゲームを作るためのピースはそろってきたように思います!動く足場があれば、実際ゲームとしての幅は、かなり広がりますからね!Unityかなり面白くなってきました!!!