第14回ぷちコン応募作品の実装メモと振り返り
はじめに
2020年7月18日~8月31まで開催されていた第14回ぷちコンに作品を応募しました。
この記事では箇条書きですがプロジェクトの実装メモを紹介していきます。
プレイヤー
- プレイヤーはこのプロジェクトで主に「レベル内の移動」を担当する
プレイヤーの移動
- プレイヤーは慣性を伴った動きをする
- あの動きはトルクをルートコンポーネントに掛け続けることで実現している
- プレイヤーのベースはRolling Templateに登場する球体
Rolling Templateのプレイヤーはルートとなるメッシュコンポーネントにトルクを掛けることで動いている
物理運動による移動のを採用したのは怒り狂うスイカの猛烈な突進攻撃感というのを出したかったから
- 外力を加えるAdd Forceも悪くはなかったが少し操作性が良すぎたので見送った。(パラメータ次第ではどうにでもなりそう)
コンポーネント
- プレイヤーを構成するコンポーネントは少し複雑
ルートコンポーネントのメッシュはゲーム中に表示されることはなくトルクを掛けられるために存在している
ルートコンポーネントの子であるSphereコリジョンはルートコンポーネントの回転の影響を受けないように回転をAbsolute Rotationに設定している。
- Absolute Rotationは世界からみた相対値として解釈される
Rolling Templateのプレイヤーに任意のスケルタルメッシュを設定したい時は次の記事を参照
プレイヤーの向きと移動の関係
- 基本的にはトルクによる物理運動で動く
それに加えて操作性向上のためプレイヤーの向きと連動した減速や加速を取り入れた
プレイヤーの向き(正確にはコリジョン)と運動している向きとの差で掛かるトルクの大きさを調整している
- 向きの差はベクトルの内積で取得
プレイヤーの向きと運動の向きが一致していれば1となり正反対であれば-1
Curve Assetを使って入力された内積の結果に応じたトルクの乗数を決定している
- ちょっと曲がるときや真横に曲がる時は最大で20倍のトルクが掛かるようになっている
- これにより幾分、曲がりやすくなった
- 山なりになっているのは正反対の方向に進みたいときに慣性によって引っ張られる感じを出したかっため
コンボ時のオーラ
- コンボが発生するとオーラが見える
- 「コンボの発生」タイミングを知るため、GameStateブループリント側にEvent Dispatchを定義
- プレイヤーはEvent Dispatchにオーラ発生のカスタムイベントをバインド
- GameStateがコンボ発生のEvent Dispatchを呼び出すたびにプレイヤー側は自動的にオーラ発生のイベントを呼び出す
- GameState側にはコンボが終了したときのEvent Dispatchも定義
- プレイヤー側はオーラを停止するカスタムイベントをバインドする
インジケーター
「敵グループの方向とスイカを割るまでの時間を知らせる」ことを担当する
インジケーターは3Dで表現されている
- UIで表示するより楽だったため
インジケーターはプレイヤー側で初期化時にAdd Child Actor Componentノードで生成される
- インジケーター毎に向くべき敵グループが指定される
- 初期化時にプレイヤーを中心とした距離が指定され、ゲーム中はこの距離が維持される
あとはTickでルートコンポーネントを回転させるとプレイヤーを中心にインジケーターが円周上にぐるぐる回るようになる
視認性を高めるためピッチ方向に少し傾けるようにしている
- ピッチ方向に傾けることでカメラ方向へインジケーターを大きく見せることができるので視認性を大きく高める事ができた
敵グループ
- 「スイカを割る」ことを担当する
プレイヤーとの衝突
- プレイヤーと衝突するとまず最初に速度のチェックが行われる
- 速度が不十分だった場合、プレイヤーを進行方向とは反対へと吹き飛ばす
速度が十分だった場合、敵グループ自身がプレイヤーの進行方向+斜め上へと吹き飛ぶ
吹き飛ぶ処理はカスタムイベントで定義しSet Timer by Eventで毎フレーム呼ぶ
- 少し遅延を挟んで各所へ吹き飛ばされたことを知らせたり必要な処理を呼んだりする
- 遅延を挟んだのは色々な音や画面の変化が同時に発生することによるプレイヤーの混乱を防ぐため
レベルブループリント
- ゲーム内時間の計測、レベル上のギミックを担当
敵グループの自動配置
リプレイ性を高めるため、敵グループの自動配置を採用
スポーンする敵グループの数はデータテーブルに定義してあるので将来レベルが増えたときでもテーブルの行を追加すれば簡単に対応できる
海の中やマップ外にスポーンしないよう、NavMeshが敷かれているエリアにのみスポーンエリアを限定した
スポーンエリア内にスポーンさせるための手順は次の通り
- NavMeshBoundsVolume内のランダムな位置を取得(Random Point in Bounding Box)
- Project Point To NavigationでNavMeshが敷かれている位置に修正する
今回のプロジェクトではNavMesh上にスポーン位置を取るがProject Point To Navigationが失敗と判断されるときもあった。そのためProject Point To Navigationが成功する(もしくはループ回数が限界に達する)までスポーン位置を模索するようにしている
NavMesh上にスポーン出来たとしても敵グループが重なってしまうこともあったので敵グループ側で一定距離内に別の敵グループがいれば離れた位置に再移動するようにしている
制限時間
制限時間はレベルブループリントで計算
- 理由としてUI側には画面に表示することだけを徹底させたかったため
- UIが制限時間の操作&管理をするのはおかしいんじゃない?という考え
UIに制限時間を反映させることを簡単にするためここでUIを生成しキャッシュを保持している
Tick毎に残り時間を表す「Remaining Time」をデルタ秒減らして、UI側に残り時間を反映させる処理を呼ぶ
Remaining Timeが0以下になったらゲームオーバー処理を呼ぶ
敵が追い出されたときや砂時計を獲得したときの残り時間増加処理もレベルブループリントで実装
- 追い出されたときと砂時計が獲得されたことは両者に定義しているEvent Dispatcherをこちらでバインドすることで知覚する
Game State
- ゲームの進行に必要な数字や状態の記録・増減を担当
スコア計算とコンボ
敵が追い出されるたびに1000点ずつゲームスコアに加算され、コンボ数が1つずつ加算される
コンボ数が増えるとRetriggerable Delayが呼ばれコンボ消滅までのカウントダウンが始まる
- Retriggerable Delayは呼び出されるたびにDelay時間が初期化されるのでDelay時間が0になるまでコンボ時のボーナススコアの計算は実行されない
Game Mode
- ゲームの進行を担当
シーン遷移
敵グループが追い出されるたび、ゲームクリア条件をクリアしているかチェックする
ゲームクリアと判断された場合は次の手順を取る
- コンボボーナススコアの計算を指示
- Game Instanceへのリザルト画面表示に必要なデータのコピーをする
- シーン遷移をする
リザルト画面は別のレベルに構築しているのでレベル間でデータをやりとりするためGame Instanceに一旦必要なデータをコピーしている
シーン遷移時にはパッと画面が変わらないようにPlayer Camera ManagerにあるStart Camera Fadeを用いて簡易なフェードアウトを実装している
- フェードインは各レベルのBegin Playで同じくStart Camera Fadeを使って実装している
- Start Camera Fadeは簡単にフェードイン・アウトをするのに便利だが画面全体を暗くしたり明るくしたりしてしまうので右から左へとフェードアウトするといったことは出来ない
- かっこいいフェードイン・アウトをしたい時はウィジェットブループリントを使ったほうがいい
ゲームUI
- ゲーム中の各種情報の表示を担当
カウントダウン
- ゲームの開始前に「3,2,1,START」というカウントダウンがある
このUIが生成されたタイミングでSetTimerEventから1秒毎にテキストのアニメーションと内容を書き換えるイベントを呼び出す単純なもの
カウントダウン中は操作出来ないようにレベルBPのBeginPlayでPlayer Controllerの入力を無効にしている
カウントダウンが完了するとゲームUIは定義したEventDispatcherを呼ぶ
- EventDispatcherはレベルBP側でバインドされており、呼び出しを検知すると敵グループや砂時計アイテム、プレイヤー等に対して初期化の指示を出す
スコア表示
- スコアの更新はGameStateのEventDispatcherの呼び出しを検知して実行される
スコアに限らずUIに表示される数字の殆どはGameStateのEventDispatcherを通じて更新する
スコア更新イベントの引数にスコアの増分が渡される
- 渡される増分値は正負どちらの値でも問わない。
「表示されるべき値」であるTargetScoreに増分値を加えて更新する
実際にUIに反映させる処理をデルタ秒間隔(ほぼ毎フレーム)呼び出すようにSetTimerEventを設定
「現在表示中のスコア値」であるGameScoreとTargetScoreが一致するまで増分値の10分の1をGameScoreに加えてUIに反映する(カウントアップアニメーションのため)
- 増分値をそのまま加えると1フレームで書き換わってしまうのでプレイヤーに画面の何が変化したのかわかりづらい
- 10分の1という数字を使えたのはこのゲームのスコアの増分値はすべて10で割り切れる数のため
- 端数が出るようなスコアであれば今回の方法は使えない(その場合はカウントアップアニメーションの終了条件を「GameScore > TargetScore」とし、条件が満たされると GameScore = TargetScore とすれば良いはず)
スコアを0埋めする方法については次の記事を参照
プロジェクトの総評
良かった点
- ゲームクリアに必要なデータをデータテーブルに定義した
- レベル名と紐付けることで現在のレベルの目標ターゲット数を簡単に得ることが出来、UI表示の際にとても楽だった
GameMode、GameState、GameInstanceの活用がうまく出来た
- GameModeはゲームの遷移ルール、GameStateはゲーム内に登場する各データ(スコアや救出数等)、GameInstanceはレベル間のデータのやり取り
- それぞれの役割に適した処理を書けたことで見通しの良いプロジェクトに出来た
マケプレアセットの活用
- 各種モデルや配置ツール、音楽はマケプレのアセットを活用することで省力化出来、より短時間で完成まで持っていくことが出来た
悪かった点
EventDispatcherの多用
- EventDispatcherは1対多の関係であれば非常に強力
- ただし今回のプロジェクトではほとんど1対1なので無駄遣い感が強い
- 1対1なら直接呼び出してしまったほうが見通しが良い(関係がはっきりしている)
- 依存を強めたくないのならインターフェースを使うべき
- ただしレベルブループリントとのやりとりにはEventDispatcherを使う方が楽
マジックナンバーが多い
- 頻出するマジックナンバーはマクロライブラリを利用して定数として定義する
- 20年オヤジのUnreal Engine 4 TIPS - SEGA TECH Blog
アセットを自分で用意しようとしすぎた
- 個人的にぷちコンは「ゲームを作る訓練」という感覚で参加しているためDCCツールやテクスチャ作成ツールとの連携も重視している
- そのため音楽以外のアセットを何でもかんでも自分で用意しようとしてしまい不要な時間を使ってしまった
- 所持しているマケプレアセット等をうまいこと自分のプロジェクトに馴染むように使うことも良い訓練であるはず
- それに気づいたのは開発後半だがここを改められたおかげでより良く出来た気がする