■第9回UE4ぷちコン応募作の「Bullets Looped Around」の制作メモです。
はじめに
ぷちコンの参加も3回目になりまして、だいぶUE4にもなれてきたと思います。ノードをつなぐのに迷いがなくなりましたし、基本的な処理の流れならノードの図がぱっと浮かんでくるようになりました。自分はあいかわらずググりまくりですが、UE4の情報はだいぶ増えたのではないかと思いました。かなり助かりました。ありがたいです。
今回はTwinStickシューターのテンプレートを使って、弾がループするシューティングを作りました。使用したUE4のバージョンは、4.18.3です。
できてみたら、シューティングっぽさはなく、アクションゲームのようになってしまいましたが、ほぼ計画通りです問題ありません。「ループする回数でスコアが上がる」というのを思いついたおかげで、戦略みたいなものが生まれたので、良かったと思います。しかし、そのせいで「直接撃たない、当てない」というジレンマが生じたのでシューティングぽくなっているのですが。
●応募動画はこちら
第9回UE4ぷちコン応募作「Bullets looped Around」 - YouTube
●ゲームのダウンロードはこちら
テーマは「Loop」ということで、4,5日悩みました。なんだかんだ悩んでいたところで、「弾が画面の端っこでループして飛び続ける」っていいんじゃね? と思いつき、その方向で進めることにしました。
思いついたのはいいものの、今度はどうやってループを実現させるのか、という部分で悩みました。
「端と端の座標をとっておいて、その間の範囲に限定するとか?」や、「パスを作って、その上を移動させるとか?」などと考えてましたが、移動範囲が固定されてしまうのはなんかカッコ悪いのでどうにかできないかと考えてました。
まず「端から端へ瞬時に飛ぶ」ので、セットロケーションの、テレポートを使えばできそうと考えました。あとは飛ぶ先のロケーションをどのように取るかで、「まっすぐ飛びつつ、方向は変わらないのだから」ライントレースで取れるかもと思いついて、やってみました。
- 弾はプロジェクタイルで、発射時の方向のまま一定の速度で、まっすぐ飛び続ける。
- 進行方向の壁にぶつかったとき、弾のフォワードベクターの反対側へラインを伸ばして、
- 伸ばした先の壁にぶつかったら、その地点へテレポートする。
- テレポート後は速度と方向を保ったまま、そこからスタートして同じことを繰り返す。
という流れです。
ただ、実験中は、あれこれやってもLineがまっすぐに飛んでくれなかったので、この辺はあきらめました。
↓ ツイートしてたやつです。
https://twitter.com/mixz_waterisle/status/968108821359968257https://twitter.com/mixz_waterisle/status/968108821359968257https://twitter.com/mixz_waterisle/status/968108821359968257https://twitter.com/mixz_waterisle/status/968108821359968257https://twitter.com/mixz_waterisle/status/968108821359968257https://twitter.com/mixz_waterisle/status/968108821359968257
https://twitter.com/mixz_waterisle/status/968108821359968257
https://twitter.com/mixz_waterisle/status/968108821359968257hhttps://twitter.com/mixz_waterisle/status/968108821359968257ttps://twitter.com/mixz_waterisle/status/968108821359968257https://twitter.com/mixz_waterisle/status/968108821359968257以下で、BPの図なども入れていますが、おかしな処理を書いていた場合は申し訳ありません。
ご指摘いただけると幸いです。
https://twitter.com/mixz_waterisle/status/968108821359968257https://twitter.com/mixz_waterisle/status/96810882135996825
弾とかコリジョンとか設定
まず、最も重要な弾の挙動から着手しました。
弾が「ぶつかったときにテレポートする」だけだと、テレポートするまでは良かったのですが、自機や敵にラインがぶつかった場合、自機や敵の位置にテレポートしてしまいました。ダメージ判定したら確実に当たる弾になるのでダメだなと思いました。
デフォルトのコリジョンプリセットを使ってあれこれやるのはかえって面倒そうと思ったので、コリジョンプリセットをゲーム内に登場するモノすべてで個別に設定することにしました。図は弾用の壁のコリジョンです。
弾と壁の設定
ゲームプレイ中に、うっすら色がついている壁はただの「絵」です。プロトタイプ中はチェック柄でおなじみの床を置いていたので、どこまで動けるのかだいたいわかったのですが、背景を海にすると全く範囲がわからなくなったので突貫工事でつけました。先に行けそうなのに見えないものにぶつかって動けないというのは、かなりのストレスになります。
実際に弾がぶつかる壁は、この見える壁のさらに外側に置きました。
弾と壁とのライントレースは、LineTraceForObjectsを使って、壁だけに反応するようにしました。
オーバーラップイベントで開始しています。
オーバーラップしたところから、弾のフォワードベクターの反対側へRayLength分(10000くらい)
伸ばしています。
Lineがぶつかったら、SetActorRelativeLocationで、テレポートにチェックいれて、インパクトポイントへ飛ばします。ループの回数をちゃんと数えるので、ねんのためDoOnceをはさみました。
SetActorRelativeLocationのあとの処理は図のような流れで、回数を数えて、その回数ごとに弾の色を変えています。ループ回数が設定回数(5回)になったらデストロイします。
敵と自機のコリジョン設定
敵のコリジョンは大きめにして、弾は当たりやすくしています。(実際は弾とのヒットはSphereTraceでとりました)。敵はCharacterMovementを使いました。メッシュの回転はRotationgMovementコンポーネントを使っています。
自機のコリジョンは、敵や弾とのヒットをカプセルでとって(かなり小さくしてます)、壁との衝突はメッシュのコライダを使っています。
自機はツインスティックシューターのテンプレートからほとんど変えていないので、FloatingPawnMovementで動いています。
弾と敵の判定
弾があたった判定は、SphereTraceでとりました。
EventHitだと、見た目があたってるのに、あたったことになってないという状況が多発したので、方法を変えました。(何か設定をミスっていたと思うのですが、つきつめる時間がありませんでした)
SphereTraceを前方に30くらい伸ばして、
トレースに当たるとヒットってことにして、ApplyDamageします。それをきっかけにして、イベントディスパッチャで、弾のループ回数を敵に受け取ってもらいます。(敵側の処理は後述します)
(SpawnEffectは自作の関数ライブラリで、パーティクルの向きや大きさを設定できるようにしました)
自機の場合は「画面内で飛んでいるモノすべて敵」なので、ループ回数も関係ないですし「当たったらダメージ」という処理だけです。
子BPを使った、敵のバリエーションづくり
敵のバリエーションをつくるのに、子BPを使ってみました。
ベースとなる敵を作って、基本の処理は全部そこに入れました。
バリアがあるとか、子をスポーンするとか、敵を特徴づける機能を別に作って対応しました。
図はボスが子をスポーンする処理。
敵の動きは、AIコントローラーの中で、プレイヤーに向けてMoveToLocationしているだけです。
これだけですが、移動速度に変化をつけたり、耐久力に差をつけたりすることで、わりとバリエーション感は出せたと思います。
できるだけシンプルにする敵のスポーンと管理
エネミースポーナーというBPを作って敵を管理しました。
スポーン場所になるターゲットポイントには専用のタグをつけて、コンストラクションスクリプトで拾って、配列にセットする方法にしました。これならポイントが増えても対応できます。スポーンするときは、配列からロケーションをランダムで選択しています。
敵のスポーンする種類や数は、べた書きしていたのですが、3種類くらい書いたところでまとめられそうと思ったので、まとめてみました。
最終的には一個の流れで全部できるようになりました。
まず敵用の構造体を作って配列にしました。
「スポーン間隔秒」「敵ポーンの種類」「その敵のスポーン数」「次の敵が出るまでのインターバル秒」
という構成です。
この構造体の配列のWaveEnemySpawnDataを、それぞれの要素に分解する関数を作って読み取っていくことで、敵の流れを作りました。
まず、最初にスポーンする敵の数を設定してから、Tickで配列を処理していきます。
設定した数に達するまで、設定した種類の敵を出します。出し終わったら、インターバル時間だけ待って、次のインデックスにすすめて、次にスポーンする敵の数を設定します。
→
→→
→
このしくみだと、別の敵が同時にスポーンできませんが、それに対応するのを考えるのは今回のぷちコン応募ではあきらめました。
とりあえず、敵が倒されたかどうかは関係なく時間がきたらスポーンするので、一時的に種類が違う敵を存在させることはできます。
ブループリントの通信
今回、イベントディスパッチャやブループリントインターフェイスを使ってBP同士のやりとりをやってみることを最低限の学習目標にしました。過去にぷちコンで応募した作品ではBPインターフェイスは使ってなかった(わかってなかった)ので、無理やりになったとしても使ってみることにしました。
できるだけ、ゲームの流れはゲームモードに集約して、入力と表示はプレイヤーコントローラーに集約しました。主にゲームの「ステート」を切り替える時のイベントとしてBPインターフェイスを使いました。
このゲームではレベルの読み込みを行わず、全部一つのレベルの中でやってます。(再スタートは同じレベルを読み込んでます)
「ちょっとやってみたかったので・・・」という理由なのですが、次なにか作るときはレベルで分けると思います。その方が楽だと思いました。
ステートを区別するための列挙体を作って、ゲームモードでゲームの流れを管理しました。ゲームのスタート時、敵が全滅してクリアになったとき、自機がやられてゲームオーバーになったとき、などにBPインターフェイスのイベントを使っています。
スタートボタンが押されたら、タイトルのウィジェットからプレイヤーコントローラーのゲームスタートのイベントを呼びます。
ゲームスタートのイベントで、ゲームモードにも通知します。
プレイヤーコントローラーはメインのウィジェットの表示をして、自機のイベントを呼び、自機はそれを受け取って弾の発射をTrueにします。
ゲームモードは、MainGameのステートに切り替えたあと、エネミースポナーへスポーン開始のイベントを通知します。
あとはそれぞれ勝手に動きます。
弾のループ数の受け渡しとスコアの表示
敵は弾から「あたったぞコノヤロウ」と来るので、敵は「だれ?」ってことで弾にキャストして、弾のイベントディスパッチャを受け取ります。弾の方で、自分のループ数をCallの引数に設定しておきます。
敵が倒されたとき、弾からもらった値を元に、敵の中でスコアの計算などをして、処理した内容を一旦プレイヤーコントローラーに渡してから、
スコア表示用のウィジェットへ渡すためだけに変数に格納して、
→
→
スコア表示ウィジェットに表示しました。
直接ウィジェットに渡してもよかったのかもしれませんが、スコア表示用のウィジェットはプレイヤーコントローラーだけ見ている形にしたかったので、このような流れにしました。
ConvertWorldLocationToScreenLocationノードで、敵がやられた位置付近に表示しています。
→
→
スコア表示用のウィジェットは、受け取ったループ回数で文字の色と大きさを変えてます。
こんな処理をしているからなのか、敵はすぐにデストロイしてしまうとエラーがでたので、わずかに(0.05秒くらい)ディレイさせてからデストロイしました。
ゲームオーバー通知
プレイヤーのシールド(ライフ)が0になったらゲームオーバーになったとして、ゲームモードへ通知を出します。プレイヤーは、爆発とともに表示を消すだけにしています。(見えないだけで操作できます)
ゲームモードでは、イベントを開始すると、ゲームオーバーステートに変えて、ゲームモードからプレイヤーコントローラーを通して、非表示にしていたメインのウィジェットのゲームオーバーを表示させます。
ゲームクリア通知
エネミースポーナーから、「敵用の配列がラストインデックスです。かつ、現存のエネミー数が0です」という条件でゲームモードに通知を出します。(プレイヤーにも弾の発射を止める通知をします)
ゲームモードでは、ゲームクリアか、ゲームオーバーのイベント開始すると、それぞれのステートに変えてスコアを保持しておきます。
その後は、ゲームモードでリザルトのステートに切り替えて、プレイヤーコントローラーでリザルト用のウィジェットを表示をします。
→
→
その他
BGM
BGMは自作しました。といっても、ループ音源を組み合わせただけの簡単なものです。
iPadアプリの、「Launchpad」を使いました。
※類似品にご注意。Novationで検索したほうが良いかもしれません。
無料で使えます。アプリ内課金で機能を追加したり、音源を増やしたりできます。機能を購入すれば、オリジナルの音源を使えるようになります。使い方は、Youtubeの動画などで。
もともとは、同じ会社Novationが出してるハードウエアのLaunchpadのアプリ版です。そのハードウエアとiPadをUSB接続して連携するなんてこともできるようです。
BGMは、このゲームをつくりはじめた直後から、BPM180くらいが合いそうだな~と思っていて、それを基準で探したのですが、「コレだ」というのがなかったので自分の好み(Drumn' Bass)で作りました。
BGMにあわせて、弾の連射間隔は0.33秒に設定しました。
モデル
自機と敵ともに、MODOでモデリングしました。
「プログラミングに時間かけたいし、プリミティブのままにしてしまおうか」と、けっこうギリギリまで悩みましたが。背景を変えたらモデルの方も変えたくなってしまったので手をつけました。
敵はキューブをだして、頂点をベベルして、シリンダーぶっさしてブーリアンして、ポリゴンを三角化してクリーンナップ(エラーを消す)という簡単手順です。1個あたり10分かけてません。ブーリアン処理の直後から一気に三角化すると意図しない頂点同士が結線して変なポリゴンができてしまうことがあるので、それを作り直すのに時間がかかったくらいです。
自機はちょっと時間かけてモデリングしました。エッジ追加>面の押出>頂点の調整>エッジ追加....という感じでひたすら繰り返します。考えながらラフ感覚で作るならModoの「メッシュフュージョン」を使う方がよかったかもしれません。
ツインスティックシューターのデフォルトUFOモデルのシルエットからあまりはずれないように、大きさも合わせつつ、でもそれなりにカッコよく見えるように、といったところを注意してモデリングしました。
テクスチャをはらないつもりだったので、UVは適当です(たぶんデフォルトのキューブから増えた頂点の分だけぐちゃぐちゃになってるやつ)。
おわりに
今回のぷちコンは、ゲームの内容と見た目はシンプルにして、UE4のプログラミングでわかってないところを勉強をしようと思いやってみました。
主に、インターフェイスを使ってBP間で通信することに挑戦しました。自分としては、かなり勉強になったと思います。
まだまだ、わからないことがわからない状態ですが(やってみないと見えもしないというか、認識できないというか、わかりようがない事多数)今後も学習していきたいです。
このゲームは、出現する敵の数が倍のバージョンも作ってみましたが、全体が長くて飽きました。あと、弾よけの集中力が持続しませんでした。そういうところを、スクロール等の展開で変化させたりステージを分けたりして、なにか広げられる余地がありそうなので、広げてみるのもいいかもしれないなと思っています。