ゲームロジック・設計 / Game Logic & Architecture
ゲームロジック・設計
「ただ動く」から「メンテナンス可能な設計」へ。
Unityの密結合を打破するDDD(ドメイン駆動設計)の導入や、
ステートマシンによる厳密なゲームフロー制御。
コレクライト (Crows & Treasures)
UnityJamは、9人・1週間で制作したPC向け探索ゲームです。進行管理、セーブ、ショップ、ランキング、演出、unityroom連携をまとめました。
成果: 設計、統合、CI/CDに加え、セーブ、ショップ、ランキング、演出を本人実装中心でまとめ上げ、1週間でunityroom公開まで完了しました。
1週間・9人開発を前に進める設計と主実装
カラスを操作してダンジョン内の財宝を集めるアクションゲームです。9人チームでの1週間開発において、アーキテクチャ設計、各機能の統合、GitHub Actions を用いた CI/CD パイプライン構築に加え、セーブ、ショップ、ランキング、演出の実装を主担当として進めました。
9人の担当分担とCI/CDパイプライン
カラス操作・ダンジョン探索
GameManager ステート遷移(FSM)
9人が並行開発できるよう、GameManagerにステートマシンを実装し責務を明確に分割。タイトル・探索・ショップ・イベントの状態遷移を厳密に管理し、チームメンバーが設計意図を逸脱しにくい構造を構築した。
セーブ・ロード・ショップ・演出・ランキングは自分が中心となって実装し、他メンバー担当の機能と一つのゲームとして結合。インターフェース経由で依存を逆転させ、統合時の衝突を抑えながら公開可能な品質まで仕上げた。
GitHub Actions でビルド自動化・自動テスト・unityroom へのデプロイフローを整備。短期開発でもリグレッションを早期検出し、全員がメインブランチへ安心して push できる環境を提供した。
設計の意図
9人・1週間という制約では、機能を足す速さより先に、誰がどこを触るかを明確にする必要がありました。設計、統合、CI/CDを同時に整えつつ、自分が担当したセーブ、ショップ、ランキング、演出を基準実装として先に通したことで、並行開発の摩擦を減らしたまま公開まで持っていけました。
短期統合で詰まったこと
インベントリ追加、バッテリー調整、マップ初期化、感度変更、タイトルへ戻る挙動、セーブ周りの修正を短い間隔で実装した経験から、短期開発では「状態遷移の境界」と「Prefab・設定値の差し替え」が詰まりやすいと再確認しました。次回は、ゲームフローごとの初期化チェックリスト、保存データの互換確認、UIと入力設定のスモークテストをさらに早い段階で自動化し、終盤の細かな手戻りを減らしたいと考えています。
実装の深堀り:短期チーム開発を支えたFSMと視界判定
このプロジェクトでは、1週間の開発期間で9人が並行して作業する前提だったため、状態管理の整理と負荷の読める実装が重要でした。ゲーム全体の流れは
GameManager
に集め、探索の緊張感に関わる視界判定はGCが出にくい形で組み立てています。
1. State Machine による進行管理の一本化
タイトル、探索、ショップ、イベントなどの状態遷移を各画面に散らさず、GameManager
のFSMに一本化しました。責務を1か所にまとめたことで、担当者ごとの実装境界が明確になり、短期開発でも統合時の衝突を抑えやすくしています。
// GameManagerにおけるステート管理の簡略例
public void TransitionTo(GameState newState) {
currentState?.Exit();
currentState = newState;
currentState?.Enter();
// 状態変更をEventBus等でブロードキャストし、UI等が反応する
OnGameStateChanged?.Invoke(newState.Type);
}
この形にしておくと、ショップUI、敵ギミック、セーブ処理といった別担当の機能が、状態遷移の契約だけを共有して独立に進められます。開発終盤に全体をまとめる際も、どこでゲームフローが変わるのかを追えるようになりました。
2. 動的ライティングと NonAlloc Raycast の組み合わせ
探索中の「見える範囲」をゲーム性に使うため、光量の扱いと視界判定をロジック側から参照できる構成にしました。視界確認では毎フレーム複数回のレイキャストが発生するため、割り当てによるGCを避ける目的で
Physics.RaycastNonAlloc を使っています。
// ゼロ・アロケーションな視界判定
int hitCount = Physics.RaycastNonAlloc(lightSource, direction, results, maxDistance, layerMask);
for (int i = 0; i < hitCount; i++) {
if (results[i].collider.CompareTag("Player")) {
// 光源内にプレイヤーがいる場合の処理
}
}
この実装により、暗闇と光源の境界で判定が頻発する場面でもフレーム落ちしにくくなり、プレイヤーにとって重要な「見えるか、見えないか」のルールを安定して扱えるようになりました。
通常Raycast vs NonAllocのGC発生量比較
CI/CD
パイプラインと状態遷移・ライティングの統合アーキテクチャ
ONE MORE PLANK (Game Jam)
ONE MORE PLANKは、2日間で制作した一人称視点の板配置パズルアクションです。短期開発でも拡張しやすいよう、入力、レベル、UI、演出を分けて実装しました。
成果: 板配置、部屋進行、演出、ランキング連携を2日で実装しつつ、後から機能を足しやすい名前空間構成を維持しました。
2日間でも追加実装が崩れない構成にする
ゲームジャムで制作した、板を置いてキャラクターを誘導するパズルアクションゲームです。完成優先の短期開発であっても後から読めるコードを保つため、Core・Player・Level・UI・Visuals を名前空間で明確に分離しました。
板配置パズルのゲームループ
ゲームプレイ画面
名前空間による責務分割 (Core / Player / Level / UI /
Visuals)
GameManagerはStateHistoryをStackで保持し、部屋の前進・後退をイベント(OnTimeChanged / OnLevelLoaded 等)で通知する設計。MonoBehaviour間の直接参照を持たせず、購読によって依存を逆転させている。
GameJuice.cs に画面揺れ・SE・振動フィードバックをまとめ、他のクラスがロジックを汚染せずに演出を呼び出せる構成。ゲームジャムにおいてもJuiceを後付けにしない判断で実装した。
クリアタイムを UnityroomApiClient で送信し、外部ランキングと連携。API呼び出しはGameManager内の勝利判定フローに統合し、ゲームロジックと通信処理の責務を分けたまま接続している。
設計の意図
ジャムゲームでも「とりあえず動く」だけにせず、名前空間分離とイベント購読を最初から入れました。終盤の機能追加が既存コードを大きく崩さずに済んだ点が、この判断の強さでした。
2日制作で割り切ったこと
2日ジャムではゲームロジックだけでなくリポジトリ容量や素材管理も早めに整える必要があると学びました。次回は、演出用アセットの容量上限、差し替えやすいフォルダ規約、提出直前でも崩れにくい最小ビルド構成を先に決め、後半はゲームの調整に集中したいです。
実装の深堀り:イベント分離とヒットストップ中の演出制御
短期開発のゲームでも、ロジックと演出が密結合になると調整がすぐに崩れます。そこで、責務は名前空間とイベントで分けたうえで、手触りに関わる演出だけは時間の流れを分離して扱える構成にしました。
1. ヒットストップ中も止めない演出更新
ゲーム全体を一瞬止めるヒットストップは気持ちよさに効く一方で、通常の
deltaTime
に依存した演出まで止まると見た目が不自然になります。そこで、カメラシェイクなどの演出は
Time.unscaledDeltaTime
を基準に動かし、ゲーム進行とは別の時間軸で動いています。
// 物理時間が止まっても動き続けるシェイク演出
public IEnumerator ShakeCameraRealtime(float duration, float magnitude) {
float elapsed = 0f;
while (elapsed < duration) {
// Time.unscaledDeltaTime を使うことで Time.timeScale=0 の影響を受けない
Vector3 offset = Random.insideUnitSphere * magnitude;
cameraTransform.localPosition = originalPos + offset;
elapsed += Time.unscaledDeltaTime;
yield return null; // WaitForSecondsRealtimeの代わりにより細かく制御
}
cameraTransform.localPosition = originalPos;
}
この分離によって、プレイヤー操作は止めつつ、画面揺れやポストエフェクトは最後まで再生できます。演出の追加や調整を行っても、物理挙動に影響を出しにくい点も実装上の利点でした。
timeScale=0 でも演出を継続する独立時間軸
2. イベント駆動と名前空間による依存整理
Core、Player、Level、UI、Visuals
の名前空間を分け、通知はEventBus経由にしています。MonoBehaviour同士の直接参照を減らすことで、短期間でも追加機能を後から差し込みやすくしました。
-
UIや演出の追加がしやすい:
クリア判定などのロジック側はイベントを送るだけにして、見た目の反応は
Visuals側で受けています。 - 終盤の統合がしやすい: 依存の方向をそろえておくことで、ジャム終盤の追加実装でも競合箇所が絞れました。
イベント駆動と独立時間軸を用いたGame
Juiceパイプライン
factoria (Production Design)
factoriaは、工場シミュレーションをUnity依存から切り離して検証した経営ゲームプロトタイプです。生産・在庫・市場・UIを4層に分けて追えます。
成果: 製造ライン、Worker、KPI、市場、故障、セーブ/ロードを層分離し、Unityなしでも検証しやすいDomain/Simulationへ整理しました。実装の見た目よりも、数値調整や不具合調査をどこから追うかを先に決めたプロトタイプです。
Unity依存を外してシミュレーションを検証する
複雑な生産ラインの整合性を保ちながらシミュレーションを行う工場経営ゲームのプロトタイプです。Unity特有の「なんでもMonoBehaviourに依存してしまう」流れを避け、純粋なC#ロジックを独立させました。画面を作り込む前に、まず生産と市場の数字が追える状態を作りたかったためです。
Domain / Simulation / Infrastructure / Presentation
DDDに基づく4層アーキテクチャ
コードベースは Domain(純粋なビジネスロジック),
Simulation(実行フレームワーク),
Infrastructure(永続化と外部通信),
Presentation(Unityの描画と入力)の4層に分けています。Simulation層はUnityなしで動かせるので、数字の検証をUnityを起動せずに回せます。
生産施設の動的生成とロジック注入はファクトリーパターンで整理し、オブジェクト同士が直接参照し合わない構成にしました。
施設が数千になっても、ロジックと描画を分けているので処理の重い箇所をプロファイルで追いやすくなっています。
設計の意図
MonoBehaviourに責務を集めすぎないことで、シミュレーション単体で検証できる形を目指しました。後のチーム開発で、どこまでをUnity外へ出すべきか判断する基準にもなっています。
残った検証課題
開発を通じて実感したのは、まず「second prototype」としてドメイン分離の骨格を先に立てる重要性です。一方で、設計をきれいに分けることと、実際に遊びとして調整しやすいことは別問題でした。EventBusの流量がどこで増えるか、KPIの数字がプレイヤーの判断に十分つながるか、故障や市場変動を入れたときにデバッグしやすいかは、まだ検証が足りません。次回は、プロトタイプ段階から負荷計測用の観測点と、価格変更・在庫不足・故障復旧のような運用シナリオを用意し、DDDの形だけでなく調整速度まで含めて評価します。
実装の深堀り:ドメイン分離を保つEventBusと統計処理
factoria では、生産、在庫、市場、UIが同時に状態を変えるため、処理の順序と責務を崩さないことを重視しました。そこで、状態変化の通知は EventBus に集め、表示用の統計値はシミュレーション側で別管理しています。
1. EventBus を使った層間の疎結合化
生産完了、在庫更新、市場変動といった出来事は、ドメイン層から直接UIを触らずにイベントとして公開しています。GameBootstrap.cs
で依存関係を組み立て、Unity依存の処理は Presentation
側だけが受け持ちます。
// Domain層: 純粋なビジネスロジック。Unity(MonoBehaviour)に依存しない
public class ProductionFacility {
public void Produce() {
if (inventory.HasSufficientResources()) {
inventory.Consume();
var product = CreateProduct();
// 状態変化をEventBusでブロードキャスト
EventBus.Publish(new ProductManufacturedEvent(product));
}
}
}
// Presentation層: イベントを購読し、UIや3Dモデルの演出のみを担当
EventBus.Subscribe<ProductManufacturedEvent>(e => {
// 煙パーティクルの再生やUIの更新
visualizer.PlaySmokeEffect();
});
この構成により、ロジックの検証と見た目の調整を分けて進められます。施設数が増えたときも、どのイベントがどの層へ流れているかを追えます。シミュレーションの整合性も崩れにくくなりました。
2. 指数移動平均 (EMA) によるKPIの平滑化
KPI表示は瞬間値のままだと変動が激しく、プレイヤーが傾向を読み取りにくくなります。そこで、スループットや品質の表示には EMA を使い、短期的な揺れを残しつつ全体傾向が見える形にしました。
// Time.deltaTime に依存しない滑らかなトレンド分析
public void UpdateStatistics(float currentValue, float deltaTime) {
// アルファ値(減衰係数)をフレームレート変動に強くする
float alpha = 1f - Mathf.Exp(-smoothingFactor * deltaTime);
currentEMA = (currentValue * alpha) + (currentEMA * (1f - alpha));
}
この統計処理により、一時的な詰まりや急増に引きずられすぎず、設備投資や人員配置の効果が画面上で読みやすくなりました。シミュレーションの内部値と、プレイヤーへ見せる情報の粒度を分けたことがポイントです。
瞬間値(ジグザグ)とEMA平滑値の比較
DDD
4層アーキテクチャとEventBusを用いたイベント駆動システム
ゲームフリーク・プログラミングチャレンジ
「ピカチュウ 仲間と特訓チュウ!」をテーマに開催された、アルゴリズムの実装精度と速度を競うエンジニア向けコンテストです。
成果: データ構造の選択と探索ロジックの改善を繰り返し、複雑な制約がある問題で精度と速度を追いながら全テストケースを通しました。
ゲーム基盤開発で培った「最適化」をアルゴリズム問題に適用する
自作エンジンのECS構築や描画パスの整理で日常的に意識している「メモリレイアウト」と「計算コスト」へのこだわりを、純粋なアルゴリズム問題として出力しました。複雑な条件が絡む探索問題に対し、無駄な計算を省くための枝刈りや、ビット演算で無駄な計算を削り、正解率と実行時間を両方追いました。
競技を通じた知見
ゲーム開発では「フレームを保てる速さ」が目標になりますが、このチャレンジでは限界まで削ることを求められました。そこで考えたことが、エンジンのボトルネックを追うときの感覚につながっています。