Unityで3D横スクロールの挙動を考える
Unityで3D横スクロールの挙動を作る
これはpepabo Advent Calendar 2016 - Qiitaの15日目の記事です。
昨日は id:yutokyokutyo の コードレビューするのが怖いと思っていたエンジニアが半年間コードレビューを経験して思った 10 のことでした。
ところでみなさんはロックマンXシリーズが好きですよね。
僕は大好きです。なのでみなさんも大好きです。
ロックマンXシリーズはX~X6まではドット絵の2D横スクロールアクションゲームでした。 X7から完全な3Dとなり、今までなかった手前や奥の概念が入りました。 それが不評だったかは定かではありませんが、X8は3Dだけども移動は旧来の左右のみの2D移動となりました。
こういうのを3D横スクロールと呼ぶらしいです。
3D横スクロールの良いところは前進後退の二択しかないので操作が簡単になる、且つ3D空間を動き回るので見栄えが良いという点だと思います。
今回はこの3D横スクロールのキャラの挙動を考えてみます。
説明はいいから使うぞという方はこちらのアセットをご利用ください。 github.com
後半もう一回リンク貼ります。
要件
- 3D空間である
- キャラクターは進むルートが決まっている
- 進む方向は前・後の二種類
ざっくり考えるとこんな感じ。
キャラクターは進むルートが決まっている
これを実現するには、歩いてほしいルートの要所要所の位置情報を持たせておいて、AからBまで直進、到着したらBからCまで直進、CからD... といった感じにするのが一番わかりやすいかなと思います。
いわゆる WayPoint
と呼ばれる位置情報の配列を用意して、それに沿って移動させるということです。
UnityだとWayPointを置く場合空のGameObjectを配置すると楽です。
上記のgifで見える赤い点がWayPointで、それらを線で繋いでます。 Boxがひたすら点の位置に向かって進む、通過したら次の点まで直進してるのがわかります。
AからBまで向かうのにどの方向を向けばいいかはベクトルの減算(B-A)を正規化すれば求められます。
Unityなら以下の感じ
Vector3 a = transform.position; Vector3 b = target.transform.position; Vector3 direction = (b - a).normalized;
transform.LookAt
でも同じことはできますがオブジェクトの向き自体が変わってしまうのでご注意を。
これで向きは定まったので、directionにスカラー値speedを乗算して目標地点に向かわせることができます。 で、接近したら次の目的地を渡して、directionの再計算。基本的にこれの繰り返しです。
- 現在位置Aから目的地Bの向きを求めて移動する
- 現在位置と目的地の距離がn以下になったら到着とみなす
- 次の目的地Cを参照する。以降繰り返し。
一方通行であれば配列一つでなんとかなるので楽です。
進む方向は前・後の二種類
実際のゲームになると前進だけじゃなくて後ろに戻るパターンもあります。
一方通行だとA->B、B->Cだけ考えればよかったのですがB->A、C->Bも考える必要があります。 欲しいのは現在の自分からの、前方の目的地、後方の目的地情報です。 この辺をクラス化してまとめていくとよさそう。
class WayPoint { public Vector3 position; WayPoint next, prev; }
自身の位置情報、その前後のWayPoint情報を持つシンプルなクラス。 ただ、個人的に理解しにくかったです。
というのも、A->Bのちょうど中間地点にいたときに自分はどこの点にいると言える?という点でもやってしまいました。
点から点へ移動するゲームであればWayPointの概念がわかりやすいのですが、ロックマンのようなゲームでは点はあくまで通過点に過ぎず、点と点を繋ぐ線の間を自由移動するようなイメージが強いです。
点と点の間で止まる可能性もあるし、点の間で反対方向のWayPointに向く場合もある。
というのを踏まえると、WayPointの概念をうまいこと包み込んで線で管理するクラスのがよいのでは。ということで下記のLineクラスを定義。
public class Line { public Line next, prev; public Vector3 front, back; public Line(Vector3 f, Vector3 b) { front = f; back = b; } /// <summary> /// 任意の向きの目的地を返す /// </summary> /// <param name="d"></param> /// <returns></returns> public Vector3 GetWayPointByDirection(Direction d) { return d == Direction.front ? front : back; } /// <summary> /// 任意の向きの次の線を返す /// </summary> /// <param name="d"></param> /// <returns></returns> public Line GetNextLineByDirection(Direction d) { return d == Direction.front ? next : prev; } } public enum Direction { front, back }
WayPoint情報をfront,backに格納した、二点間の線を表すクラスです。 このクラスの仕事は、自身のLine上のキャラクタが前進・後退する場合にどこを目指せばいいのかというのを教えることです。 もらったWayPointに接近したら次のLineを取得する。の繰り返し。
実際にできたアセット
こんなものができました。
WayPointを置いたら、その方向にオブジェクトを移動させるアセットです。
使い方
先にUniRxをインポートしてください https://www.assetstore.unity3d.com/jp/#!/content/17276
- LineManagerを適当なスクリプトに貼り付けて、インスペクタからWayPoints配列の数を設定します
- WayPointのための空のオブジェクトを生成して、インスペクタからWayPointsの配列に入れます。この時点でシーン上では線が表示されます。
- 動かしたいオブジェクトに
DirectionController2d
をアタッチします
- 実際に移動するスクリプトを書きます。 例としてはこんな感じ。
using UnityEngine; using LineTrace; public class Move : MonoBehaviour { public DirectionController2d controller; public float speed; void Update () { if (Input.GetKey(KeyCode.LeftArrow)) { // 向きを設定する controller.direction = Direction.back; transform.position += controller.forward*speed*Time.deltaTime; } else if (Input.GetKey(KeyCode.RightArrow)) { // 向きを設定する controller.direction = Direction.front; transform.position += controller.forward * speed * Time.deltaTime; } } }
キーボードの十字キーで右側、左側にいい感じに動いてくれます。
実装側は DirectionController2d
に向き direction
を与えることと、現在の向きベクトル forward
を取得する以外特に何もしなくていいです。
LineManager
がWayPointからよしなに Line
クラスを生成して、 DirectionController2d
クラスが現在自身が乗っている Line
を逐一チェックして更新してくれます。
ちなみに LineManager
のcycle
にチェックを入れるとWayPointの始点と終点を繋ぐので無限にぐるぐるできます。
DirectionController2d
のautoRotation
チェックを入れると、目的地の方向に自動で回転します。回転速度は rotateSpeed
の値を参照します。
欠点
このアセットは二点間の距離を調べるときに高さ(Y軸)を見ません。 なぜ高さを省いたかというと、高さまで考慮すると通過される可能性があったためです。
例えばWayPointAが次の目的地だったとして、Aを飛び越えるような感じで大きくジャンプした場合に、距離算出だけだと目的地にたどり着いてない判断をされる可能性があります。 そのためY軸を潰して、高さ情報抜きで接近したかを判別してます。
DirectionController2d
の一部を抜粋
this.UpdateAsObservable() // 目的のポイントに接近したかどうか .Where(_ => Mathf.Abs(Vector2.Distance(transform.position.XZ(), current.GetWayPointByDirection(mDirection).XZ())) < distance) .Subscribe(_ => { var next = current.GetNextLineByDirection(mDirection); current = next ?? current; direction = mDirection; });
transform.position.XZ()
はVector3クラスの拡張メソッドです。X,Zの値でVector2を作ります。
こんな感じでY軸を潰して距離算出、distance以下であれば次のLineを取得・・といったことをしています。
なので、このスクリプトは上に登っていくようなゲームには向いてないです。横スクロール用でお使いください。
その代わり、回転については処理が多少簡単になっています。
this.UpdateAsObservable() .Where(_ => forward != Vector3.zero) .Subscribe(_ => { var arrivedForward = forward.XZ(); var cross = transform.forward.XZ().Cross(arrivedForward); var dot = Vector2.Dot(transform.forward.XZ(), arrivedForward); // ほぼ目的の方向向いた場合に回転させない if (dot < 0.98F) { transform.Rotate(Vector3.down, rotateSpeed * Mathf.Sign(cross) * Time.deltaTime); } });
自身の現在の向きと向きたい方向ベクトルの外積 cross
を出します。
二次元ベクトルの外積は便利で、A x B の時、ベクトルAから見てベクトルBが右側にあるのか、左側にあるのかを正・負で算出できます。
これで回転すべき方向がわかります。が、これだと自身が目的の方向を向いているのかわからないので内積 dot
を出します。
正規化した二次元ベクトルの内積 A・B は二つのベクトルのなす角度を -1~1 で求められます。0が直交、1が並行、-1が真逆のベクトルということですね。
crossで向きはわかったので、dotが一定以下である場合に回転させる、ということをしています。
備考:カメラ
これで移動処理は概ね実装できますが、ゲームとなるとカメラもいい感じに追従して欲しくなります。 カメラも基本原理は同じで、WayPointを置いて動かすというスタンスで行けそうですが、キャラがどの位置にいるときにここにカメラがあってほしい、みたいなマッピングがあるので結構大変そうです。 また、アーティストが入ってくる部分でもあるので簡単に弄れるようにしたかったりもします。
割と大変な作業なので、僕はCameraPathAnimatorというアセットを使うことが多いです。
配置がかなりお手軽なのでかなりコスト削減できます。 対象を注視しながら移動ということもできるので横スクロール系だとこれがメインの使い方になりそう。 上記リンクもしくはここからアセットを購入すると僕に報酬が入るシステムなので皆様是非お買い求めください。
ちなみにWayPointシステムも、SimpleWayPointSystem という便利なアセットを使えばサクッと実装できます。こちらはルートをベジエ曲線とかスプライ曲線などで細かく設定できるのがポイントです。
じゃあこの記事はなんだったんでしょうね。
明日は @genkiroid の番です genkiroid.github.io