Weekly Game Jam 1週目

一週間前に、

こんなTweetをしたので一週間でゲームを作りました。結果ですが、完成まではいけませんでした( ;∀;)
ですが、自分なりに学べたことがいくつかありましたので技術共有ということでブログにしたいと思います!

とりあえずこんなゲームを作りました。



お題
Game Jamなので、お題がなければ始まりません。そのお題ですが今回は FPS でした。
FPSの概念ですが、こちらを参照
ファーストパーソン・シューティングゲーム - Wikipedia


シンプルに、いつも作っているようなShooterでも良かったのですが、一週間も期間があるのでちょっと自分なりに挑戦してみたいと思い、いつもとは少し違うジャンルの制作に挑戦しました。
ちなみにいつも作ってたのはこういうのです。





それで、今回作ろうと思ったゲームはこんな感じのゲームです。
youtu.be

こちらのゲームはアーケードゲームとして有名な、The house of the dead シリーズです。
操作の概要ですが、移動は自動、プレイヤーが唯一しなければならないことと言えば
" 敵の弱点目掛けて鉛玉をぶち込む " だけです。このシンプルな操作 + 弾は無限というところから来る爽快感、また、敵は数で圧倒してくるので弱点をしっかりと狙い如何に非ダメージを減らすかという緊張感がウリのゲームとなっています。(僕はそう思ってますw)

では、The house of the dead (以下 HOD )をUE4で一週間で作るにあたって、どんなことを考えながらロジックを組み、HOD らしさを自分なりに出したのかを書いていきたいと思います。





プロトタイプ編
まずは、このゲームの面白さを確認するための " 土台 " を作らなければなりません。HOD の土台は一体何か?と考えた時、
私は「自動移動ロジック」だと考えました。
基本的にアーケードゲームとして遊ばれているこの HOD 。ゲームセンターに来るお客さんがシューティングゲームに慣れている方ばかりなんて想定はされていません。なので Input を削りに削って、プレイヤーがしなければいけないことが銃を撃つということだけになっているのだと思います。
ということは、" 敵を倒す為に銃を撃つ = 楽しい "が成立しなければ、ゲームとしては失敗です。
では、 " 敵を倒す為に銃を撃つ = 楽しい " をプロトタイプで達成する為にまず、必要な基本的なロジックは何か?
となると「自動移動ロジック」に落ち着くわけです
自動で移動してもらい、プレイヤーは射撃に集中する。この仕組みをプロトタイプで作ろうと決めました。


自動移動実装編①
このロジックを実装しようと考えた時、方法はいくつか浮かびましたが最善は Spline かなと確信していました。
docs.unrealengine.com
方法は浮かびましたが、このノードを使えば行けそう...とかは全く思い浮かびませんでした。
ですが Unreal Engine には、Content Example なるものが存在していることは知っていました。
docs.unrealengine.com
Content ExampleUE4 がアップデートされた際に追加された機能などをサンプルとして展示、使用例などを提示してくれるものです。
Spline 自体は4.7あたりから純粋な機能として入っていた気がしますので、サンプルは豊富だろう!と思い勉強させていただきました。




自動移動実装編②
この自動移動ロジックですが、大きく分けて三つに細分化出来ると考えました。

1 - 移動( Location )
2 - 回転( Rotation )
3 - イベント

です。一つずつ説明していきます。

1 - 移動
これは非常に単純で名前の通りですが、プレイヤーの移動です。
移動と言っても、何を、どれくらいの時間で、どれくらいの距離を移動させるかなど考えることは様々ですがこんな感じで取り合えず落ち着きました。
f:id:ikagamedev:20171204173533p:plain

1 - 移動はお馴染みの、SetActorLocation ノードを使っています。移動させる Target はシンプルに Player Pawn です。

2 - Get Location at Distance Along Spline ノードは Content Example の使用例で使われていたので使っています。恐らく、Enum Cordinate Spaceinput から、その座標系にある Spline の座標を返してくれるノードです。
今回の移動は Spline 上での移動を想定していますので、SplineLengthProgress Of Proceed 変数 ( 0 to 1 )を掛けています。

3 - Progress Of Proceed 変数ですが、ここの演算は元々 Timeline で行って、その 0 to 1 の返り値を Progress Of Proceed の代わりにしていたのですが、何秒で Spline の終点まで行くか。というパラメータが扱いにくかった為に変数での制御になっています。

4, 何秒で終点まで行くか。の変数ですが、GoalTime と名付けています。Progress Of ProceedGoalTime 秒で1になれば良いので毎フレーム、Progress Of Proceed = Progress Of Proceed + ( Delta / GoalTime ) をすることで何秒で終点に到達するか。を実現しています。


2 - 回転
回転は非常にシンプルです。Spline の直線方向を常に見させる...。というものだと思っています。(あまり理解せずに使ってましたw)
とりあえずこれで問題はなさそうだったのでOKということで
f:id:ikagamedev:20171204174636p:plain
ActorRotation を弄らず、ControlRotation を弄っているのは PlayerPawn のクラスが持つComponent の親がカメラだからです


3 - イベント
ここがそこそこ難しかったです。そして、あまり綺麗な処理を組めていません...。ですが、晒していきます。
の前に、試作を一つ作ったので先ずはこちらから
f:id:ikagamedev:20171204180054p:plain
左が EventGraph ,右が折りたたまれたノードです。
True になったら、移動を止め、何かしらの進展があり次第移動を再開する。というシンプルな構成になっています。

HasArrivedSplinePoint 内の処理について説明します。
Get Location at Spline Point ノードで Point Index を受け取り、その PointLocation を返り値として返します。その値 (Vector) とプレイヤーの Location(Vector) を比べ、誤差が1ユニット以下なら True に処理が流れます。
非常にシンプルです。これらの処理で出来たwipがこちら


何となくそれっぽくなってるのがわかりますね!細かく説明すると、赤いキューブを規定以上倒すことによって、移動のロジックが再開される仕組みになっています。
ここらへんからテンションが上がってきてます。



ではここで少し脱線して、敵の Spawner クラスについて説明していきます。(これも試作です)


ex - 敵スポナー
f:id:ikagamedev:20171204182459p:plain
変数、Component の説明をしていきます

Component

  • Box 敵をスポーンする際に使用、こいつの Extent(大きさ) 内にランダムでスポーンさせています
  • BillBoard 他の BP からスポイト参照する際に目立つ Component がなかった為追加
  • TextRender SplinePoint で移動が止まる。というロジックを組んでいた為に、どの Point で止まるかを明示する為に追加


変数

  • MaxEnemy 敵がスポーンできる最大数
  • CurrentNumOfEnemy 現在ワールドにいる敵の数
  • EnemiesRefe 敵のリファレンス配列
  • DestroyedCound 敵が破壊された場合にインクリメント
  • BoxExtent(Editable) BoxComponent の大きさ、エディタから編集できるようにしています
  • Text(Editable) SplinePoint の何番目かにいるかを明示する為のText、上に同じくエディタから...(以下略)



ロジック

Construction Script
f:id:ikagamedev:20171204210850p:plain

  • SetBoxExtent 名前のまんまですが、BoxCollisionComponentExtent を設定する関数です。Box Extent 変数で制御できます
  • SetText 大体上と同じ説明...。

Begin Play
f:id:ikagamedev:20171204210801p:plain

  • Max Enemy Set Max Enemy 変数に 3~7 の数値を入れてあげてます。この数値を元に、スポーンできる最大数が決まってきます。
  • SpawnEnemy 後で説明します。

Custom Event Spawn Enemy
f:id:ikagamedev:20171204212259p:plain

  • Delay このカスタムイベントは勝手に Max Enemy回 ループする仕様なので、Delay を挟んでいます。
  • Spawn AI From Class 敵キャラをスポーンします。Random Point in Bounding Boxノード で、BoxCollision内 でランダムにスポーンさせています。

Rotation は単純にプレイヤーの方向を向かせているだけです。で、最初は Spawn Actor From Classノード Spawn していた(赤い CubeActorクラス だった為)のですが、本番用の Mesh に切り替え、Characterクラス を継承した敵キャラを Spawn Actor From Classノード を使ってスポーンした際に、AI Controller がスポーンしていないことに気づき、急遽このノードに変えました。

  • Add EnemiesRefe配列 にスポーンされた Enemy のリファレンスを追加しているだけです
  • Current Num Of Enemy Increment スポーンする = 敵キャラが増える。ということなのでインクリメントしてます。
  • Branch 非常にシンプルで、Current Num Of Enemy <= Max Enemy の間はループです。False になったら別ロジックが1秒起きに動きます。


Enemy Check
f:id:ikagamedev:20171204212243p:plain

  • ForEachLoop この処理は、EnemiesRefe に入っている値が Null かどうか?を調べ、NULL であった場合は DestroyedCount変数 をインクリメントします。この処理を毎秒やるのは気が引けましたが、しょうがなく....。(For loopでIndex指定したループの方がパフォーマンス的にいいのでしょうか?教えてください!)

ループが終わった時の Current Num Of Enemy DestroyedCount が等しいのであれば、全ての敵が NULL 、すなわち倒されたということになるので True に処理が抜け、イベントディスパッチャーが呼ばれる仕組みです。そして、イベントディスパッチャーが呼ばれることで自動移動が再開されます。




ここまで書いてきましたが、この子たちはあくまで試作です。(僕は何度も作っては消して、作っては消してを繰り返すタイプです)
では本番用のロジックはどうなったのかを見ていきます。





3 - イベント
上の HasArrivedSplinePoint はそのままです。ですが、この Bool値 による分岐だけだと Spline Point に到達する度 " 必ず " 移動が止まり、何かしらをしなければならなくなります。
では、Spline Point を出来る限りなく少なくして、イベントが起きる位置にだけ Spline Point を置けばいい。
ということをすれば、レベルデザイナーは苦しみ、プログラマーを呪い、僕はその念によって体調を崩すかもしれません。
それは避けたいので、特定の Spline Point にだけ止まれるような仕組みを作らなければなりませんでした。


4 - イベント本実装編 BP_MovePath
特定の Spline Point で止まるということは、結局は Bool値制御 になるだろうと考えていました。
そこで思いついたのが、Map です。
docs.unrealengine.com
Spline PointInteger , そして、止まる、止まらないが Bool ということは Integer の中にBoolean を入れることができる Map を使えば実装できるのでは!?と考えたわけです。
そして実装してみたものがこちら。


Construction Script BP_MovePath

f:id:ikagamedev:20171204230745p:plain

ForLoop で、SplinePoint数 ループをします。Integer Boolean Map の要素数 = SplinePoint数 です。
そして、Bool値(止まるか止まらないか) ですが、Event Actors配列 の中に何かしらが入っている = イベントが起きる。ということなので is ValidノードTrue or False を指定しています。
そして、EventActorsの要素数 = SplineのPoint数 なので SplinePoint数Resize をしています。

Event Actors配列 ですが、リファレンスは BP_EventActor です。
f:id:ikagamedev:20171204222652p:plain


BP_EventActor を親として、BP_Event_Door , BP_Event_EnemySpawner が生み出されてるので、Construction Scriptis Valid に反応して True を返してくれる訳です。
f:id:ikagamedev:20171204222728p:plain



ということは、BP_EventActor を親とすれば、どんなイベントでも実現可能というわけです!!!!うおおおおおおお!!


そして、BP_EventActor で、Do Some Event Custom Event とイベントディスパッチャーの LogicHasFinishedSuccessfully を作成しました。
f:id:ikagamedev:20171204224559p:plain



この二つを子クラスで呼びだすことで、Spline のクラスと連携を取っているわけです。
フローで見ると

  • プレイヤーが EventActors配列 の要素が NULL でない Spline Point に到達し、移動が止まる
  • DoSomeEvent が呼ばれる
  • BP_EventActor を親とするクラス内で諸々の処理が実行される
  • 実行が終わり次第、イベントディスパッチャーである LogicHasFinishedSuccessfully が呼ばれる
  • 移動が再開される

というわけです。



例として、BP_Event_Door を見ていきます。
f:id:ikagamedev:20171204224906p:plain

Event Do Some Event が呼ばれると、bCanOpen 変数に True が入ります。
TickbCanOpen変数True であれば、ドアが開くロジックが実行されるわけです。


f:id:ikagamedev:20171204225319p:plain

そして、左のドア(この際、どちらのドアでもいい)の Relative Rotation と決め打ちされた値を比べ誤差が 0.1以下 なら(つまり、開ききったら) bCanOpenFalse を代入し、LogicHasFinishedSuccessfully が実行されるというわけです。




Event Graph BP_MovePath
f:id:ikagamedev:20171204220101p:plain

これは非常に単純で、Integer Boolean MapSpline Point Index要素Bool値True かつ、要素が存在するのであれば何かしらのイベントを起こさせる...。といったものです。
False であれば、素通りしてくれます。動画で取ってみましたので、どうぞ。
youtu.be

これで自動移動編は終わりです。最後にオマケで AI編 をどうぞ




AI
上の HOD のプレイ動画を見ていただいたら分かる通り、単純にプレイヤーに向かってくる AI ではありません。
何らかのパラメーターにより重みづけをされ、ウェイトが一定値を超えると攻撃してくる仕組みになっています。
流石に AI に着手したのが遅すぎたので、これらの動きを出来る限り近似して表現することになりました。

最初に敵 AI の特徴を見つけるとしたら、綺麗にプレイヤーの正面に立ち、一定の間隔で攻撃してくるということだと思います。ここで AI をそれっぽい動きにさせる為にプレイヤーの正面に BillBoard を三つ置き、EnemyStayPos と名付けました。
ここに三体の AI を移動させようと企てたわけです。
f:id:ikagamedev:20171204233404p:plain

まあそこそこ上手くいって、それっぽく見えるのでOKということにしました。三体以外の AIプレイヤーの前方向ベクトル * Random Float( 200 ~ 500) + プレイヤーの横方向ベクトル * Random Float( -200 ~ 200) 付近で待機してもらっています。攻撃してくる三体がやられ次第、適当なところにいる AI が正面にまた来るような形です。また、AI 同士の衝突を避ける為に RVOAvoidance を設定しています。Avoidance Consideration Radius 100 , Avoidance Weight 0.5です。



攻撃当たり判定
Trace の進み具合はこんな感じです。
www.youtube.com

f:id:ikagamedev:20171204235622p:plain
AttackTrace という名の関数を作成しました。
ForLoop で複数回実行されます。今回は、NumOfAttackTraceLoop回 実行されます。(今回は30回にしてます)
では左から解説していきます。まず、今回の思想として、AI から見て 180°方向 にだけ Trace をしたいと考えていました。なので先ず、180 / NumOfAttackTraceLoop をし、それと ForLoopのIndexを掛けたもの自身のRotationに足している だけです。途中で -90° しているのは、MeshRelative Rotation-90° だからです。

だらだら書いていたら長くなってしまいました。最後までお付き合いいただきありがとうございました。