Unityで3D横スクロールの挙動を作る
これはpepabo Advent Calendar 2016 - Qiita の15日目の記事です。
昨日は id:yutokyokutyo の コードレビューするのが怖いと思っていたエンジニアが半年間コードレビューを経験して思った 10 のこと でした。
ところでみなさんはロックマンX シリーズが好きですよね。
VIDEO www.youtube.com
僕は大好きです。なのでみなさんも大好きです。
ロックマン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を取得する。の繰り返し。
実際にできたアセット
こんなものができました。
github.com
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 というアセットを使うことが多いです。
VIDEO www.youtube.com
配置がかなりお手軽なのでかなりコスト削減できます。
対象を注視しながら移動ということもできるので横スクロール系だとこれがメインの使い方になりそう。
上記リンクもしくはここ からアセットを購入すると僕に報酬が入るシステムなので皆様是非お買い求めください。
ちなみにWayPointシステムも、SimpleWayPointSystem という便利なアセットを使えばサクッと実装できます。こちらはルートをベジエ曲線 とかスプライ曲線などで細かく設定できるのがポイントです。
じゃあこの記事はなんだったんでしょうね。
明日は @genkiroid の番です
genkiroid.github.io