【UE4】ゲーム制作事例（The Primitive Shooter）

今まで
[EQSを使ってInfluence Map](https://qiita.com/Dv7Pavilion/items/c1fbd53db44be117d38b)
であったり
[アニメーションに合わせてパーティクルの生成](https://qiita.com/Dv7Pavilion/items/0e69b92fa2ba963d4750)
など、**「ある仕様を実現するための方法」**をUE4を用いてQiitaに投稿していましたが、**「UE4（ゲームエンジン）なのにゲームほとんど作ったこと無い...」**ということに気づいてしまったので「とにかく何かゲームを作らないと...」と思い立ったことが今回解説するゲームを作り始めたきっかけです。

以下ゲームのプレイ動画です。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">このゲームはこれで完成。今まで検証レベルでUE4を使っていたので、今回のような「ゲーム」を作るのはとても久しぶり。<br>次は何を作ろう。１週間で出来る規模がいいな。今回のもエタりそうだったから。<a href="https://twitter.com/hashtag/UE4?src=hash&amp;ref_src=twsrc%5Etfw">#UE4</a> <a href="https://twitter.com/hashtag/EpicFriday?src=hash&amp;ref_src=twsrc%5Etfw">#EpicFriday</a> <a href="https://t.co/r2F6LJbEmB">pic.twitter.com/r2F6LJbEmB</a></p>&mdash; PavilionDV7 (@Dv7Pavilion) <a href="https://twitter.com/Dv7Pavilion/status/918810739145457664?ref_src=twsrc%5Etfw">2017年10月13日</a></blockquote>

以下ゲームへのリンクです。ダウンロードして展開してexeを開けば遊べます。
マウスとキーボードのみ対応しています。パッドは対応していません。
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">先程のEpicFridayで載せたゲームはこちらで公開しました。<a href="https://twitter.com/hashtag/UE4?src=hash&amp;ref_src=twsrc%5Etfw">#UE4</a> <a href="https://twitter.com/hashtag/indiedev?src=hash&amp;ref_src=twsrc%5Etfw">#indiedev</a> <a href="https://t.co/pmrCrqpi4F">https://t.co/pmrCrqpi4F</a></p>&mdash; PavilionDV7 (@Dv7Pavilion) <a href="https://twitter.com/Dv7Pavilion/status/918818579285291008?ref_src=twsrc%5Etfw">2017年10月13日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

#敵管理システム
敵のスポーニング、ボスが撃破された等のお知らせを通知したり受け取ったりするクラスとして、Actorクラスを継承したアクタを作成しました。

##敵の出現はウェーブ制
横スクロールや縦スクロールのシューティングでは「敵はあるタイミングで出現し、全てを倒さずともボスへ到達できる」というパターンが多いのですが、このゲームでは「１ウェーブ分の敵を決まった敵数のみフィールド上に出現させる」というちょっと変わったウェーブ制を採用しています。

あるウェーブに15体の敵が登録されておりフィールド上にいるべき敵数が3体だった場合、フィールド上には3体の敵がいる状態をキープするように敵を出現させます。
これを1ウェーブ分の敵を出し切るまで（この例だと15体）続けます。

![2017-10-21_21h24_27.png](https://qiita-image-store.s3.amazonaws.com/0/104467/53f8d0ac-b27c-891c-08dc-c3e6edc6be48.png)

敵のスポーニングは上の通りです。再帰処理にしてみました。
しかしこの処理方法では一度既定数スポーンさせた後は敵が一切出現しません。再帰処理は条件を満たし処理を終了させると外部から呼ばれない限り処理を開始することはありません。

![2017-10-21_22h21_51.png](https://qiita-image-store.s3.amazonaws.com/0/104467/905daf54-9063-f3a9-f198-29e6dd8a7a7d.png)

そこで敵が撃破された時に呼び出されるイベントでFieldEnemyのデクリメントと同時に再度SpawnWaveを呼び出すことで解決しています。

![2017-10-21_22h29_37.png](https://qiita-image-store.s3.amazonaws.com/0/104467/8be9974f-48fa-a23e-b79b-90e41d288f9d.png)

敵は死亡時にSpawnerを検索し該当するイベントを呼び出します。
いちいち死亡する度にGet All Actors Of Classを呼ぶようにしているのですが、この理由は「スポーン時に引数としてSpawnerを渡してもアクセスエラーが出てしまった」からです。
出現した瞬間に撃破されると、なぜか引数として渡したはずのSpawnerの参照がNONEになっている事がありました。何故このようなことになったのか最終的な原因は不明ですが、とりあえず死亡時に検索するようにすればOKなのでこれでよしとしました。

![2017-10-21_21h42_23.png](https://qiita-image-store.s3.amazonaws.com/0/104467/6978579e-7f81-da9d-efce-5c89b14ff1a2.png)

Sequenceの下部分です。こちらではウェーブのインクリメントと全ウェーブを出し切った時にボスをスポーンさせています。


#ゲーム開始時とボス撃破時のカメラアニメーション
ステージを選択してゲームが始まる時、いきなりパッと始まるのは味気ないので以前から使ってみたかった**シーケンサー**を使ってカメラアニメーションを作ってみました。

<img src="https://qiita-image-store.s3.amazonaws.com/0/104467/ca01e2ef-6eda-76f4-c6ca-f9d83983302a.png" width=80%>
上のシーケンサーはゲーム開始時のものです。動かしているのはキャラクター自身とキャラクターが持つSpring Armコンポーネントです。色々試しながらでも10分程でそれっぽく動かせたので「スゲーッ！便利！」と感動してしまいました。

<img src="https://qiita-image-store.s3.amazonaws.com/0/104467/2180bfc9-8b29-4293-6c4c-54314cbc57aa.png" width=80%>
ボス撃破時のシーケンサは開始時のものとほとんど変わりません。動かしているのはSpring Armとカメラコンポーネントです。異なる点はそれぞれのTransformをローカル座標で動かしているという点です。
ローカル座標で動かした理由は**「プレイヤーがどの位置にいても同じアニメーションをさせるため」**です。ボス撃破後に既定の位置にワープさせるより、どの場所にプレイヤーが居ても滑らかにカメラアニメーションに繋がる方が気持ちいいのでこの方法を採用しました。

![2017-10-22_01h09_40.png](https://qiita-image-store.s3.amazonaws.com/0/104467/51b8cc75-016e-55dc-8024-a781939be0fa.png)

上の画像はゲーム開始時のシーケンサー再生部分です。これはレベルブループリントで行っています。
[ゲームプレイからシーケンスをトリガーする](https://docs.unrealengine.com/latest/JPN/Engine/Sequencer/HowTo/TriggeringSequences/index.html)
を参考にしました。

ボスが撃破されるとボス撃破シーケンスが再生されるのですが、これは
![2017-10-22_01h19_23.png](https://qiita-image-store.s3.amazonaws.com/0/104467/78182852-5ad0-6945-fa18-dad5f0215f7b.png)
EnemySpawnerで宣言されている**OnBossDestroyedディスパッチャー**をレベルブループリントで定義することで実現しています。

![2017-10-22_01h21_29.png](https://qiita-image-store.s3.amazonaws.com/0/104467/5fc4f708-7711-f960-e910-ced4cb89625a.png)
上の画像はEnemySpawnerでOnBossDestoyedディスパッチャーを呼び出している部分です。
DestroyedBossはボスブループリントから撃破時に呼び出されます。


#オフスクリーンインジケータ
このゲームでは敵が画面外に出た時には敵のいる方向にインジケーター（画像）を表示しています。これを**offscreen indicator（オフスクリーンインジケーター）**と呼ぶそうです。

<img src="https://qiita-image-store.s3.amazonaws.com/0/104467/4f57114d-3e42-a2f4-50ed-43dda927fb5a.png" width=80%>
インジケータそのものはウィジェットブループリントで実装しています。デザイナービューではImageパネルを中心に配置して、サイズを調整しているだけです。

![2017-10-22_17h15_38.png](https://qiita-image-store.s3.amazonaws.com/0/104467/72c22d40-9b2e-5898-194f-af6d5a4f438a.png)
先程のウィジェットブループリントのグラフです。OffScreenイベントやOnScreenイベントは、このウィジェットを生成する敵クラスから呼び出されます。
敵が画面外に出た場合にインジケーターを表示したいので、OffScreenイベントで可視性を「Visible」にしています。
OnScreenはOffScreenとは正反対の事をしています。画面内の場合はインジケーターを表示の表示は不要なので可視性を「Hidden」としています。

<img src="https://qiita-image-store.s3.amazonaws.com/0/104467/f7ad04d2-7708-b17e-9055-d865d9e2e37b.png" width=80%>
インジケータの更新部分ですが、結構シンプルに出来たなぁと思います。
Project World To Screenで敵キャラの座標をスクリーン座標に変換します。（Ownerはこのウィジェットを生成した敵の参照です）
変換した座標をビューポートサイズから画像サイズの半分（128x128なので半分は64x64）を引いた数でクランプします。この処理は2Dでよくある「画面外に出ないように制限する処理」と一緒ですね。Set Position In Viewportでウィジェットの位置を設定することでインジケータを実現しています。
ただしこの方法は**向き**までは設定出来ません。矢印の画像を使ってインジケーターを作ろうとしても向きまで取得するようには出来ていないのでその場合は別の方法が必要となります。

![2017-10-22_18h53_52.png](https://qiita-image-store.s3.amazonaws.com/0/104467/08636c75-64c2-30d0-d543-13026b988122.png)
これは敵のベースクラスで記述しているインジケーターウィジェットの呼び出し部分です。

![2017-10-22_18h57_58.png](https://qiita-image-store.s3.amazonaws.com/0/104467/49dd9c60-a62a-a631-aa08-c75f8e51f815.png)
これもシンプルな実装だと思います。インジケーターウィジェットのUpdate Indicator関数とやっていることは同じです。
このような実装になる前に**「Was Recently Rendered」**を用いて「画面外にいるか」を判定していたのですが、この関数は指定した時間内にアクタがレンダリングされたかを真偽で返すので画面外に出てからインジケーターが表示されるまでのラグと画面内に入った時の表示までのラグがはっきり感じ取れてしまったのでラグの少ないこの方法を採用しました。


#スローモー
プレイヤー以外の時間を遅くするというアイテムを実装したのですが結構面倒くさかった印象です。というのもSet Global Time Dilationを用いてゲーム内の時間を遅くしているのですがこれによりSet Timer by Eventで指定した時間も影響を受けてしまいリアル時間とのズレが生じてしまいました。（SetGlobalTimeDilationに0.5を指定し、SetTimerByEventに5秒後を指定した場合Eventが呼び出されるのはリアル時間で10秒後となってしまう）
[UE4小ネタ　：　制限時間の実装方法を色々と](http://pafuhana1213.hatenablog.com/entry/2015/11/21/014427)
こちらではSetTimerByEventは影響を受けないと書かれていたのですが自分の勘違いの可能性もあります。
ここの問題の解決を含めてアイテムがどのような処理をしているか紹介します。

<img src="https://qiita-image-store.s3.amazonaws.com/0/104467/78f15282-b296-32b1-8be4-bb41439c92ac.png" width=80%>
ゲーム内時間の変化によりSetTimerByEventで指定した時間と大きくズレる問題は**GetGlobalTimeDilation X スローモーが解除される時間**とすることで解決しました。
この計算方法によりスローモーが発動してリアル時間できっかり5秒後にスローモーが解除されるようになります。

GetPlayerCharacterから伸びるOnBeginSlomoはプレイヤーのCustom Time Dilationを指定の速度にしています。

![2017-10-22_22h29_45.png](https://qiita-image-store.s3.amazonaws.com/0/104467/1cb02533-bb2b-1f99-c962-58a656d8a6c8.png)

上の画像はプレイヤーのOnBeginSlomoとスローモーが解除された時に呼ばれるOnEndSlomo部分です。

##スローモー発動時のエフェクト
スローモーが発動した時画面には色収差（色ズレ）を起こすことで「プレイヤーの時間だけが加速している感」を出しています。（これは上手く表現できたと思います。出来てるよね...?）
このエフェクトはポストプロセスの**Chromatic Aberation**を弄っています。
[Scene Fringe (Chromatic Aberration)](https://docs.unrealengine.com/latest/INT/Engine/Rendering/PostProcessEffects/SceneFringe/index.html)
そしてスローモーの発動時と非発動時のエフェクト切り替えは**レベル上に配置したPost Process VolumeとCameraコンポーネントのBlend Weight**を切り替えて実現しています。

![2017-10-22_22h42_25.png](https://qiita-image-store.s3.amazonaws.com/0/104467/f1047628-65a7-8ba7-b399-69c8c933c5e5.png)
![2017-10-22_22h43_53.png](https://qiita-image-store.s3.amazonaws.com/0/104467/568f9bea-f670-be40-0529-fbb8529963fa.png)
プレイヤーのOnBeginSlomoとOnEndSlomoが呼び出されたタイミングでそれぞれWeightを1.0～0.0、0.0～1.0に変化させています。


#おわりに
「もしもこのゲームを誰かが作って自分がプレイしたときに、どういう事を聞きたい（知りたい）だろうか」というのを考えて書いてみました。主に「ある機能を実現するための手法」を紹介するような記事になりましたが、いかがでしたでしょうか？この記事を読んでいただいている方の参考になれば幸いです。
記事に書いてある事以外にも「これどうやってんの？」等の質問があれば
[Twitter](https://twitter.com/Dv7Pavilion?lang=ja)やコメントに書いていただければと思います。