読者です 読者をやめる 読者になる 読者になる

GameProgrammar's Night

ゲームプログラム系の覚え書き

UE4 見下ろし2Dゲームの移動処理(TopViewMovement)を作る

 久々の2Dゲームネタです。タイトルの通り、見下ろしタイプの2Dゲームの移動処理を担うMovementComponentをつくってみます。

見下ろし移動とは?

 RPGや縦STGなどでよく見る真上からキャラクターやマップを見ていることを見下ろし視点と言います。
 2Dゲームでは上下左右の4方向、もしくはそれに斜めを追加して8方向に動かします。これは、コントローラの十字キーと一致させるためにそうなっています。
 もちろん、回転操作をいれて任意方向に進めるようにしてあるものもありますし、モデルを使っている場合は、レバーを倒した方向に移動させるものもあります(UE4 TopDownテンプレート)。
 ここでは見下ろし2Dゲームでは一番使われると思われる8方向移動させることにします。

f:id:katze_7514:20170317185818j:plain

 見下ろし視点は英語ではTopDownView/TopViewと言いますので、実装するMovementComponentのことをTopViewMovementComponentと呼ぶことにします。

TopViewMovementComponentの機能

以下の機能を実装します。

  • 8方向移動
  • 8方向移動するナビゲーション移動

 ナビゲーションが入っていますので、Pawn向けのMovementComponentとして実装します。

TopViewMovementComponentの実装方針

 TopViewMovement実装の一番の難点は、ナビゲーション移動を8方向に制限することです。ナビゲーション移動は最短距離を突き進みますので、移動中かなり自由な向きを取ります。
 以下の記事のようにすることで、移動すべき方向ベクトルを取得することができます。

katze.hatenablog.jp

 これを利用して、移動すべき方向ベクトルから8方向のどれに一番近いかを判定して、その方向に動かすように実装することで実現します。
 上記記事の通り、C++で実装しないと移動すべき方向ベクトルが取得できませんので、TopViewMovementもC++で実装することにします。
 また、MovementComponentは裏で色々と処理を行っているようなので、全部自作することはせずに、FloatingPawnMovementComponentを継承し、必要な部分だけカスタマイズすることにします。

UCLASS(ClassGroup=Movement, meta=(BlueprintSpawnableComponent))
class PROJECT_API UTopViewPawnMovement : public UFloatingPawnMovement
{ ... }

 なお、完全なコードは載せるのは長いですので、この記事では大まかな実装の紹介だけにします。 

MovementComponentでの移動処理

 まずは簡単にですが、MovementComponentでの移動の仕組みを紹介します。

TickComponentで移動する

 移動を行うタイミングはComponentのTickです。TickComponentメソッドの中で、SafeMoveUpdatedComponentメソッドを呼ぶことで移動が行われます。
 SafeMoveUpdatedComponentメソッドに、そのTickで移動する分のベクトルを渡します。

void UTopViewPawnMovement::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction * ThisTickFunction)
{
  FVector DeltaVel = /* ... このTickで移動するベクトルを計算 */ ;
  const FQuat Rotation = UpdatedComponent->GetComponentQuat();
  FHitResult Hit(1.f);
  // 移動(座標の変化)が行われる
  SafeMoveUpdatedComponent(DeltaVel, Rotation, true, Hit);
}

 第一引数の移動ベクトル分、座標が変化します。
 第三引数がSweep処理を行うかのbool値です。trueにするとSweep処理が行われ、Hitがあれば第四引数のHitResultクラスに結果が入ります。

 移動メソッドは移動するための情報をため、実際の移動処理はTickComponentで行う設計となっています。

Velocity変数に移動方向がためられる

 MovementComponentは、Velocityという変数を持っています。AddInputVectorなどの移動メソッドによる移動ベクトルの計算結果がためられていきます。
 このVelocityは、次のTickでの移動方向を現します。長さは1以下で、最大速度をスケールします。Velocity変数を元にして、SafeMovementComponetメソッドの第一引数に渡す移動ベクトルを計算します。FloatingPawnMovementComponentやCharacterMovementComponentの最大速度はunit/secですので、DeltaTimeを乗算するのが基本の求め方になります。

  FVector DeltaVel = Velocity * MaxSpeed * DeltaTime;

 自作の移動メソッドを作る時は、このVelocity変数と合成するのを忘れないようにしましょう。

8方向移動

 XY平面(TopView)上の8方向とします。X方向が右に正、Y方向が下に正の座標系です。
 8方向は上方向を1として時計回りに1ずつ増やして、1~8のByteで扱うことにします。0は方向なしとして扱います。
 方向を表すenumを作っても良いのですが、それはBP側に作ることにします。代わりにByteで表すことでenumと連動しやすくしておきます。

移動メソッド

// 指定した方向に移動する。DirectionはTOP(1)から時計回りに1ずつ増える。0は停止
UFUNCTION(BlueprintCallable, Category="TopViewPawnMovement")
void UTopViewPawnMovement::MoveToToward(uint8 Direction)
{
   FVector Vel = DirectionToVector(Direction);
   AddInputVector(Vel);
}

 方向に応じた移動ベクトルを計算して、AddInputVectorを呼びVelocityに設定しています。TickComponentでVelocityに応じた通常の移動処理を行います。

 方向からベクトルを計算する関数は以下の通りです。

// 方向を単位ベクトルに変換する
FVector DirectionToVector(uint8 Direction)
{
  float sin=0, cos=0;

  switch(Direction)
  {
  case 1:  // 上
      return FVector(0.f, 1.0f, 0.f);
   
  case 2: // 右上
      FMath::SinCos(&sin, &cos, FMath::DegreesToRadians(315.f));
      return FVector(cos, sin, 0.f);

  case 3: // 右
      return FVector(1.0f, 0.f, 0.f);

  case 4: // 右下
      FMath::SinCos(&sin, &cos, FMath::DegreesToRadians(45.f));
      return FVector(cos, sin, 0.f);

  /* ... 以下続く ... */
  }
}

8方向移動するナビゲーション

 ナビゲーション移動を開始するメソッドは、AIMoveToなどFloatingPawnMovementComponentに実装されているものをそのまま使えます。
 紹介した記事に従って、まずはナビゲーションによって計算された移動方向をインターセプトします。

void UTopViewPawnMovement::RequestDirectMove(const FVector& MoveVelocity, bool bForceMaxSpeed)
{
   NavVelocity = MoveVelocity;
}

 RequestDirectMoveメソッドをオーバーライドして、ナビゲーション移動方向であるMoveVelocityを保存しておきます。

TickComponent

 先に書きました通り、実際の移動処理は、TickComponent内で行います。
 ナビゲーション移動状態かは、以下のコードで判定します。

const AControlloer Controller = PawnOwner->GetController();
if(Controller
&& Controller->IsLocalController()
&& !(Controller->IsLocalPlayerController())
&& Controller->IsFollowingAPath())
{
   /* ... ナビゲーションの移動処理を行う ... */
}

 MovementComponentを持っているPawnのControllerが「ローカルのコントローラであり、PlayerControllerでなく、パスフォロー状態である」という判定になります。
 ナビゲーション移動処理は、以下の通りです。

 if(NavVelocity.SizeSquared2D()>0.f)
  {
     const uint8 direction = VectorToDirection(NavVelocity);
     MoveToDirection(direction);
  }

 ナビゲーション移動ベクトルが存在しているかをチェックし、ベクトルを方向に変換して、先ほど実装した8方向移動メソッドに渡します。

 ベクトルを方向に変換する関数は以下の通りです。

uint8 VectorToDirection(const FVector Vel)
{
   float angle = FMath::RagiansToDegrees(FMath::Atan2(Vel.Y, Vel.X));
   if(angle<0.f) angle += 360.f;

   if(22.5f<=angle && angle<67.5f)        return 4; // 右下
   else if(67.5f<=angle && angle<112.5f)  return 5; // 下
   else if(112.5f<=angle && angle<157.5f) return 6; // 左下
 /* ... 以下続く ... */ 
}

 360度を8分割し、どの分割範囲に入っているのかを判定しています。分割する範囲の中央には8方向がそれぞれ入るような分割にしています。

 なお、元々、任意の方向が取れる前提のナビゲーション移動を無理矢理8方向に制限していますので、通常のナビゲーション移動とは違う動きをする可能性があります。方向の切り替え角度をまたぐような移動が連続してしまいカクカクした移動になるなどです。これは仕様上どうしもないですので、目的地設定を変えるなど、上手く対応してください。

まとめ

 8方向移動をするTopViewMovementを作る指針を紹介しました。自作のMovementComponentを作るヒントになればと思います。
 TopViewMovementを完成させるには、ここで紹介する以外の所にも手を入れないといけないのですが、それは実際のコードを見てください。

 移動を伴うゲームに取って、移動処理というのは肝になる部分です。UE4が用意しているMovementComponentは、必要最小限ですので手をいれる必要がどうしてもあります。
 例えば自分の場合は、これらの処理を基本として、

  • 指定方向に指定時間だけ移動する
  • 指定方向に指定距離だけ移動する

なども実装し、簡単に移動演出を行えるようにしています。

TopViewMovementComponentのソース

 完全な実装は、以下のgithubに公開しておきますので好きに改造しておつかいください。
 実際に桃子と夢のシールで使っているものを、公開用に少し手直ししたものになります。使い方などは、READEME.mdをご覧ください。

github.com