Weekly Game Jam 1週目
一週間前に、
突然ですが、UE4 weekly gamejamを開催しようと思っています!
— Kai Yoshida (@UnrealYoshida) 2017年11月27日
UE4のテンプレートを左から順に一週間でゲームを作るものです。今週は[ FPS ]を題材としたゲームを作りたいと思っているのですが、参加してくれる方を募集中です!(一人は寂しい)#UE4 #UE4Study pic.twitter.com/mGvOYlORrN
こんなTweetをしたので一週間でゲームを作りました。結果ですが、完成まではいけませんでした( ;∀;)
ですが、自分なりに学べたことがいくつかありましたので技術共有ということでブログにしたいと思います!
とりあえずこんなゲームを作りました。
タイムクライシス風なゲームwip
— Kai Yoshida (@UnrealYoshida) 2017年12月1日
がなんと、ハウスオブザデッドに変わってしまいました...プレイヤーの目の前にしっかり陣取って攻撃してきます!!#ue4 #ue4_wgj #epicfriday pic.twitter.com/5vjxqXfUuM
お題
Game Jamなので、お題がなければ始まりません。そのお題ですが今回は FPS でした。
FPSの概念ですが、こちらを参照
ファーストパーソン・シューティングゲーム - Wikipedia
シンプルに、いつも作っているようなShooterでも良かったのですが、一週間も期間があるのでちょっと自分なりに挑戦してみたいと思い、いつもとは少し違うジャンルの制作に挑戦しました。
ちなみにいつも作ってたのはこういうのです。
FPSゲーでよくある武器を拾う処理を…
— Kai Yoshida (@UnrealYoshida) 2017年2月19日
MainとSideArmでわけてます\(^^)/#ue4 #ue4con pic.twitter.com/pZQirTquEM
昔作ったTPSのゲーム、久しぶりにやったら案外面白かったw#ue4 #indiegame pic.twitter.com/9BPib9tB1k
— Kai Yoshida (@UnrealYoshida) 2017年6月18日
3DWidgetぽいのと、最近のTPSゲーで主流の、銃口からレイ飛ばして着弾地点を予想してその場所にレティクル持ってくってのやってみた(mgsvを目指しました)#UE4 pic.twitter.com/MOmCz5kLIi
— Kai Yoshida (@UnrealYoshida) 2017年9月27日
それで、今回作ろうと思ったゲームはこんな感じのゲームです。
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 Example は UE4 がアップデートされた際に追加された機能などをサンプルとして展示、使用例などを提示してくれるものです。
Spline 自体は4.7あたりから純粋な機能として入っていた気がしますので、サンプルは豊富だろう!と思い勉強させていただきました。
自動移動実装編②
この自動移動ロジックですが、大きく分けて三つに細分化出来ると考えました。
1 - 移動( Location )
2 - 回転( Rotation )
3 - イベント
です。一つずつ説明していきます。
1 - 移動
これは非常に単純で名前の通りですが、プレイヤーの移動です。
移動と言っても、何を、どれくらいの時間で、どれくらいの距離を移動させるかなど考えることは様々ですがこんな感じで取り合えず落ち着きました。
1 - 移動はお馴染みの、SetActorLocation ノードを使っています。移動させる Target はシンプルに Player Pawn です。
2 - Get Location at Distance Along Spline ノードは Content Example の使用例で使われていたので使っています。恐らく、Enum Cordinate Space の input から、その座標系にある Spline の座標を返してくれるノードです。
今回の移動は Spline 上での移動を想定していますので、Spline の Length に Progress Of Proceed 変数 ( 0 to 1 )を掛けています。
3 - Progress Of Proceed 変数ですが、ここの演算は元々 Timeline で行って、その 0 to 1 の返り値を Progress Of Proceed の代わりにしていたのですが、何秒で Spline の終点まで行くか。というパラメータが扱いにくかった為に変数での制御になっています。
4, 何秒で終点まで行くか。の変数ですが、GoalTime と名付けています。Progress Of Proceed が GoalTime 秒で1になれば良いので毎フレーム、Progress Of Proceed = Progress Of Proceed + ( Delta / GoalTime ) をすることで何秒で終点に到達するか。を実現しています。
2 - 回転
回転は非常にシンプルです。Spline の直線方向を常に見させる...。というものだと思っています。(あまり理解せずに使ってましたw)
とりあえずこれで問題はなさそうだったのでOKということで
Actor の Rotation を弄らず、ControlRotation を弄っているのは PlayerPawn のクラスが持つComponent の親がカメラだからです
3 - イベント
ここがそこそこ難しかったです。そして、あまり綺麗な処理を組めていません...。ですが、晒していきます。
の前に、試作を一つ作ったので先ずはこちらから
左が EventGraph ,右が折りたたまれたノードです。
True になったら、移動を止め、何かしらの進展があり次第移動を再開する。というシンプルな構成になっています。
HasArrivedSplinePoint 内の処理について説明します。
Get Location at Spline Point ノードで Point Index を受け取り、その Point の Location を返り値として返します。その値 (Vector) とプレイヤーの Location(Vector) を比べ、誤差が1ユニット以下なら True に処理が流れます。
非常にシンプルです。これらの処理で出来たwipがこちら
タイムクライシス的なゲームを作り始めました
— Kai Yoshida (@UnrealYoshida) 2017年11月29日
モックです#ue4 #ue4_wgj pic.twitter.com/4q2fdq5sss
何となくそれっぽくなってるのがわかりますね!細かく説明すると、赤いキューブを規定以上倒すことによって、移動のロジックが再開される仕組みになっています。
ここらへんからテンションが上がってきてます。
ではここで少し脱線して、敵の Spawner クラスについて説明していきます。(これも試作です)
ex - 敵スポナー
変数、Component の説明をしていきます
Component
- Box 敵をスポーンする際に使用、こいつの Extent(大きさ) 内にランダムでスポーンさせています
- BillBoard 他の BP からスポイト参照する際に目立つ Component がなかった為追加
- TextRender Spline の Point で移動が止まる。というロジックを組んでいた為に、どの Point で止まるかを明示する為に追加
変数
- MaxEnemy 敵がスポーンできる最大数
- CurrentNumOfEnemy 現在ワールドにいる敵の数
- EnemiesRefe 敵のリファレンス配列
- DestroyedCound 敵が破壊された場合にインクリメント
- BoxExtent(Editable) BoxComponent の大きさ、エディタから編集できるようにしています
- Text(Editable) Spline の Point の何番目かにいるかを明示する為のText、上に同じくエディタから...(以下略)
ロジック
Construction Script
- SetBoxExtent 名前のまんまですが、BoxCollisionComponent の Extent を設定する関数です。Box Extent 変数で制御できます
- SetText 大体上と同じ説明...。
Begin Play
- Max Enemy Set Max Enemy 変数に 3~7 の数値を入れてあげてます。この数値を元に、スポーンできる最大数が決まってきます。
- SpawnEnemy 後で説明します。
Custom Event Spawn Enemy
- Delay このカスタムイベントは勝手に Max Enemy回 ループする仕様なので、Delay を挟んでいます。
- Spawn AI From Class 敵キャラをスポーンします。Random Point in Bounding Boxノード で、BoxCollision内 でランダムにスポーンさせています。
Rotation は単純にプレイヤーの方向を向かせているだけです。で、最初は Spawn Actor From Classノード で Spawn していた(赤い Cube が Actorクラス だった為)のですが、本番用の 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
- 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 Point が Integer , そして、止まる、止まらないが Bool ということは Integer の中にBoolean を入れることができる Map を使えば実装できるのでは!?と考えたわけです。
そして実装してみたものがこちら。
Construction Script BP_MovePath
ForLoop で、Spline の Point数 ループをします。Integer Boolean Map の要素数 = Spline の Point数 です。
そして、Bool値(止まるか止まらないか) ですが、Event Actors配列 の中に何かしらが入っている = イベントが起きる。ということなので is Validノード で True or False を指定しています。
そして、EventActorsの要素数 = SplineのPoint数 なので Spline の Point数 で Resize をしています。
Event Actors配列 ですが、リファレンスは BP_EventActor です。
BP_EventActor を親として、BP_Event_Door , BP_Event_EnemySpawner が生み出されてるので、Construction Script の is Valid に反応して True を返してくれる訳です。
ということは、BP_EventActor を親とすれば、どんなイベントでも実現可能というわけです!!!!うおおおおおおお!!
そして、BP_EventActor で、Do Some Event Custom Event とイベントディスパッチャーの LogicHasFinishedSuccessfully を作成しました。
この二つを子クラスで呼びだすことで、Spline のクラスと連携を取っているわけです。
フローで見ると
- プレイヤーが EventActors配列 の要素が NULL でない Spline Point に到達し、移動が止まる
- DoSomeEvent が呼ばれる
- BP_EventActor を親とするクラス内で諸々の処理が実行される
- 実行が終わり次第、イベントディスパッチャーである LogicHasFinishedSuccessfully が呼ばれる
- 移動が再開される
というわけです。
例として、BP_Event_Door を見ていきます。
Event Do Some Event が呼ばれると、bCanOpen 変数に True が入ります。
Tick で bCanOpen変数 が True であれば、ドアが開くロジックが実行されるわけです。
そして、左のドア(この際、どちらのドアでもいい)の Relative Rotation と決め打ちされた値を比べ誤差が 0.1以下 なら(つまり、開ききったら) bCanOpen に False を代入し、LogicHasFinishedSuccessfully が実行されるというわけです。
Event Graph BP_MovePath
これは非常に単純で、Integer Boolean Map の Spline Point Index要素 の Bool値 が True かつ、要素が存在するのであれば何かしらのイベントを起こさせる...。といったものです。
False であれば、素通りしてくれます。動画で取ってみましたので、どうぞ。
youtu.be
これで自動移動編は終わりです。最後にオマケで AI編 をどうぞ
AI
上の HOD のプレイ動画を見ていただいたら分かる通り、単純にプレイヤーに向かってくる AI ではありません。
何らかのパラメーターにより重みづけをされ、ウェイトが一定値を超えると攻撃してくる仕組みになっています。
流石に AI に着手したのが遅すぎたので、これらの動きを出来る限り近似して表現することになりました。
最初に敵 AI の特徴を見つけるとしたら、綺麗にプレイヤーの正面に立ち、一定の間隔で攻撃してくるということだと思います。ここで AI をそれっぽい動きにさせる為にプレイヤーの正面に BillBoard を三つ置き、EnemyStayPos と名付けました。
ここに三体の AI を移動させようと企てたわけです。
まあそこそこ上手くいって、それっぽく見えるので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
AttackTrace という名の関数を作成しました。
ForLoop で複数回実行されます。今回は、NumOfAttackTraceLoop回 実行されます。(今回は30回にしてます)
では左から解説していきます。まず、今回の思想として、AI から見て 180°方向 にだけ Trace をしたいと考えていました。なので先ず、180 / NumOfAttackTraceLoop をし、それと ForLoopのIndexを掛けたもの を 自身のRotationに足している だけです。途中で -90° しているのは、Mesh の Relative Rotation が -90° だからです。
だらだら書いていたら長くなってしまいました。最後までお付き合いいただきありがとうございました。