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

GameProgrammar's Night

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

UnrealEngine4で2Dゲームを作ろう! その8 UE4でゲームを実装していく方法

 作ってるゲームの最低限の検証が大体終わったので、その辺りから来るお話。

 さて、ゲームとして成立させていくにはキャラが表示されて動くだけではダメで、敵を認識して攻撃できたり、HPが0になったらゲームオーバーにしたりしないといけません。
 今回は、そういうゲームロジック的なものをUE上でどう実装するかという自分なりのメモです。

ゲームプレイフレームワーク

 UE4でゲームロジックをどう実装するのかというのを公式ドキュメントのゲームプレイの項にまとめられています。
 その中でも、特に重要なのがゲームプレイフレームワークです。

 UEは元々アンリアルトーメントというオンライン対戦型FPSゲームのためのエンジンだということを念頭においておくと、なぜそうなっているかが想像しやすいので頭にいれておくと良いと思います。

PawnとController

 最重要要素は、PawnとControllerです。
 PawnとControllerは1対1で対応し、ゲーム上のキャラクター(Characterクラスのことではない)を表現します。
 Pawnはキャラの身体、Controllerは頭脳を担当します。つまり、モデル・スプライト・コリジョンなどはPawnが持ち、ユーザー入力による行動実行やAI処理はControllerが担います。

 公式ドキュメントでは「キャラが死んで復活する時に分かれてた方が便利でしょ」(超要約)と解説されてます。これは対戦型FPSをやってる人にはなるほどと思えますが、やってない人はピンと来ないと思いますので追記すると「見た目は大体同じだけど行動が違うキャラ作るのに便利だよね。もしくは、行動は同じだけど見た目が違うキャラ作るのに便利」ということでもあります。

 PawnとControllerの対応づけは、Pawnが生成された時に、設定したControllerも一緒に作成するのとControllerは別途して生成してから対応付けるのと、どちらでもできます。下図のとこに設定項目があります。

f:id:katze_7514:20150706153438j:plain

 また、対応づけられたPawnとControllerはお互いに取得しあうことができますので、直接変数を読んだり、イベントを飛ばしたり、関数を呼ぶことができます。

Pawn

 素のPawnは位置・回転・スケールなどといった最低限の描画情報しか持っていませんので、Pawnにコリジョン・モデル・スプライトといったComponentを追加して、欲しい機能を持つPawnにします。
 描画だけでなく移動などの運動系、HPゲージやダメージ表示などそのPawnに直接関係するUI系などを担当させます。

 なお、UE4が用意しているCharacterクラスは、カプセルコリジョン・メッシュ・移動コンポーネントがあらかじめ設定されたPawnです。

Controller

 Controllerには、UE4が用意している基本クラスとしてPlayerControllerとAIControllerがあります。名前の通り、ユーザーが操作することを前提にしたのがPlayerController、AIが処理することを前提にしたのがAIControllerです。これらのクラスを継承したControllerクラスを作り実装します。

 頭脳であるControllerは、見た目が関係ない概念的な情報を持ちます。HPやMPの実際の数値、もっているアイテム、使えるスキルなど。

PlayerController

 PlayerControllerの大きな特徴は、一度作られるとLevelが終了するまでPlayerControllerのインスタンスは保持されることです。そのため、Level実行中ならいつでもPlayerControllerのインスタンスはBP/C++から取得することができます。
 Levelが終了すると一度消えるので、Levelをまたぐような情報は別途持つ必要があります。

AIController

 自力で実装しても良いですし、NavigationやBehaviorTreeを扱う仕組みが用意されていますので、それらを利用することもできます。
 こちらは対応しているPawnが破壊されると一緒に破壊されます。

GameMode

 1人用、対戦、協力など、大枠のルールを実装する部分です。ゲームスタートやゲームオーバーなど、どのLevelでも使うようなロジックもGameModeに実装します。
 GameModeは、いつでも取得できますので、全Pawnが共有するデータ(今Levelに存在しているPawnの数とか)などを保持しておくと良いと思います。

 ライフタイムはPlayerControllerと同じで、Levelの生成破壊に連動します。そのため、Levelをまたぐようなデータを持たすことはできません。

Level

 日本ではマップという方がなじみがあると思います。アクションゲームのステージ1・ステージ2、ダンジョンRPGの1F・2Fなどに対応するものです。
 UEにおけるゲーム構築の基本単位になります。

 ある場所まで来たら敵を出現させる、ダメージを受ける床を作る、Actorの出し入れなどマップ上での出来事はLevelで実装します。
 現在、Levelのロジック実装にはBPしか使えないようです。C++で実装した関数などをBPから呼ぶという形でC++は使えますが、すべてをC++実装することはできません。

 どうLevelを切り分けるかは自由です。作業のしやすい範囲で切り分けると良いと思います。Levelにあらかじめ配置されているActorはLevelを切り替える時に、全部ロードされてからゲームが始まりますのでロード単位と考えても良いかもしれません。ただ、ロード範囲はLevelStreamingVolumeを使うことで制御することも可能ですので、1つのLevelで賄うことも可能です。

GameInstance

 UEは大体のものがLevelと一緒に終わります。それでは、いろいろと不便ですので、Levelをまたいでデータを保持する仕組みとしてGameInstanceがあります。GameInstanceはゲームが起動した時点で生成され、ゲームが終了するまで生きています。
 ゲーム全体の進行情報を持ったり、Levelを切り替える時の一時データ置き場に使います。また、GameInstanceのメソッドをオーバーライドすることで、生成時や終了時に処理を挟むことができます。

C++を使う場合は、各フレームワークC++層を挟んでおくと良い

 その5でも書きましたが、C++からBPにアクセスする手段が乏しいため、ControllerはC++で実装してPawnはBPで実装するなんてことをすると、ControllerからPawnのBPを直接呼ぶことはできませんので、結局、PawnにC++層をいれるはめになります。
 これは、BlueprintImmpletaionEvent宣言メソッドを呼ぶ時の仕様のため、回避することができません。

 BlueprintImmpletaionEvent宣言されたメソッドC++から呼ぶ際は以下のように

class AMyPawn : public APawn{
  UFUNCTION(BlueprintImmpletaionEvent)
  void Foo();

  void FooEvent(){ this->Foo(); }
};

のように、必ずthisを使用して呼ばないといけません。つまり、自分自身しかBluprintImmpletaionEvent宣言メソッド(Fooメソッド)を呼ぶことができないということです。外から呼ぶ時は、例のようにBluprintImmpletaionEvent宣言メソッドを呼ぶメソッド(FooEventメソッド)を介すことになります。

 よって、どこかでC++を使ったら空で良いのでC++層を1枚いれておくと、のちのちやりとりが発生した際にスムーズに実装ができます。途中から挟むとベースクラスの変更になりますので、関連するBPの修正がめんどいです。