ゲームエンジン・描画基盤 / Game Engine & Graphics
ゲームエンジン・描画基盤
DirectX 11/12による描画基盤と、
データ指向設計に基づいた独自ECSの実装。
WikiGOLF
Wikipediaの記事構造を、DirectX製の3Dゴルフゲームへ変換した作品です。API取得、SQLite、探索、地形生成、物理を自作ECS上でつなぎました。
成果: API取得、SQLiteキャッシュ、経路計算、地形生成、物理挙動を自作ECSへ統合し、取得・解析・描画・物理が連携する3Dコース生成基盤として完成させました。
記事構造をそのまま遊べる3Dコースへ変換する
Wikipediaの記事構造を読み取り、見出し・画像・リンクを3Dコースの起伏やハザード、遷移先へ変換するゲームです。既存エンジンを使わず、描画・物理・非同期API・DBを自作ECSで一貫して扱えるかを検証することを主眼に置きました。
WikiGOLF — マップ俯瞰
WikiGOLF — 通常視点
自作ECS — World / ComponentPool / Pipeline
4コンポーネント構成 — WikiClient / Path / Terrain /
Physics
Wikipedia APIから取得した数千のテキストノードを自作ECSへ動的に投入。キャッシュ効率を重視したメモリレイアウトにより、大量オブジェクトを遅延なく処理できる設計にした。
スピンによる揚力・空気抵抗・摩擦係数をフルスクラッチで実装。test_physics_friction.cpp
で挙動を固めながら、地形ごとに転がり方の差が出るよう調整しています。
地形別摩擦係数と挙動差 (Fairway / Green / Rough /
Bunker)
API通信の待ち時間が体験を損なわないよう、グラフ構造をSQLiteにキャッシュ。最短経路アルゴリズムで「リンク数=難易度」をゲーム開始前に算出する。
BFSによる最短経路 → SQLiteキャッシュ → 難易度算出
設計の意図
既存エンジンを使わない前提にしたのは、描画・物理・非同期API・DBという異なる関心事を、ECSという一つの構造で最後まで運べるか確かめたかったからです。変換パイプラインをドキュメントに整理しながら、地形生成、シェーダ、物理衝突、全文取得、CMake対応までを段階的に積み上げ、実験で終わらせず再現できる状態を目指しました。
苦労した点と判断の変更
最初は固定ステージとして組む想定でしたが、それではWikipediaをたどる偶発性が弱く、題材の面白さが出ませんでした。そこで、ランダムワードピックで入口と目的地を選び、記事リンクをたどる遊びにしました。
技術的には、日本語Wikipedia特有の表記ゆれ、リダイレクト、カテゴリの揺れ、英語版リンクの混在が厄介でした。Six Degrees of Wikipedia の考え方をそのまま持ち込まず、ゲームに必要な情報だけを絞り込み、失敗時は再抽選できる流れにして、探索が止まらない形にしました。
残った課題はUIと初見導線です。探索ルールや操作説明を画面上で十分に伝え切れず、技術的な成立に寄りすぎたため、次に作るならチュートリアルと見た目の設計を早い段階から同時に進めます。
記事データをゲームにする難しさ
地形生成、terrain shader、height-field衝突、全文取得、ビルド修正、CMake対応まで、成立条件を一段ずつ増やしながら前進しました。記事取得・構造解析・描画・物理が密結合になりやすく、どこで不整合が起きているか切り分けるのに時間を要しました。
特に詰まりやすかったのは、Wikipedia側の揺らぎをゲーム側の前提に合わせ込みすぎると、コース生成の面白さより例外処理の比重が上がる点です。失敗時の再抽選やSQLiteキャッシュで遊びを止めない工夫は効きましたが、次回は取得失敗率や再抽選回数も計測し、どのルールが体験改善に効いたかを定量で残したいです。
後半にCMake対応やビルド修正が集中したことで、手元で動く状態を他環境へ広げる整備は早めに着手すべきだと学びました。今作り直すなら、記事変換パイプラインの各段階をログとテストで独立検証できるようにし、導線UIとビルド再現性を序盤から同じ優先度で進めます。
実装の深堀り:Wikipediaの記事構造を3Dコースに落とし込む変換処理
このプロジェクトでは、Wikipediaの記事をそのまま表示するのではなく、記事の構造をゲームプレイに使える形へ変換するところを中心に実装しました。API取得、テキスト描画、構造解析、コース生成を1本の流れとしてつなぎ、記事ごとに異なる地形が自然に立ち上がります。
1. 記事構造をコース要素へ変換するマッピング
IDWriteFactory と
ID2D1Factory1 を使って記事をオフスクリーンの
ID3D11Texture2D
に描画し、その途中でレイアウト情報を拾っています。見た目を作るための描画処理を、そのまま地形生成の入力にも使う設計にしたことで、HTML解析用の別経路を増やさずに済みました。
- HeadingRegion(見出し)による起伏生成: H1〜H6 の位置を拾ってハイトマップへ反映し、見出しの密度が高い記事ほど地形の変化が増えます。記事の構成差がそのままコースの難しさに出ます。
- ImageRegion(画像)によるハザード配置: 記事中の画像領域を障害物の候補として扱い、バンカーやウォーターハザードに割り当てています。テキスト以外の要素もコースに反映されるため、地形のパターンが単調になりにくくなりました。
-
LinkRegion(リンク)による遷移先生成:
<a>タグはカップや次ステージの入口として扱い、リンク先の記事へ動的に遷移できます。記事のリンク構造をそのままステージ遷移に使うことで、ゲームの進行とWikipediaの探索が切り離されない構成になっています。
H1〜H6タグ → 起伏 / img → ハザード / a → ポータル
のマッピング構造
2. 地形ごとの差を出すための摩擦計算
コース生成だけではゲームとしての差が出にくいため、ボールの転がり方も独自に実装しました。test_physics_friction.cpp
で挙動を固定しながら、摩擦、重力、斜面上での停止判定を順に検証しています。
// 摩擦と重力の影響を計算し、ボールの減速をシミュレート
void ApplyRollingFriction(Entity e, float deltaTime) {
auto& phys = registry.get<PhysicsComponent>(e);
float gravity = 9.8f;
// 地形材質に応じた摩擦係数 (Fairway: 0.35, Green: 0.15)
float friction = GetSurfaceFriction(phys.position);
float deceleration = gravity * friction;
// 静止摩擦判定 (微小斜面での安定化)
if (CanStaticFrictionHold(phys.velocity, phys.slopeAngle)) {
phys.velocity = Vector3::Zero;
} else {
phys.velocity -= phys.velocity.Normalized() * deceleration * deltaTime;
}
}
この計算を入れたことで、平坦な地面と傾斜地で止まり方が変わり、生成コースごとの手触りを出せるようになりました。あわせて
test_tangent_generator.cpp
で法線マップ計算も固めており、見た目の起伏と物理挙動がずれにくい状態を保っています。
Wikipedia
セマンティック・トポロジー・マッピング構造と描画パイプライン
フリップエスケイプ (Engine Foundation)
フリップエスケイプは、12人チームで開発したDirectX 11製の3Dアクションゲームです。HEW_ECSの基盤、描画補助、デバッグ支援、開発運用を担当しました。
成果: チーム総コミット1,089の開発を支えるECS、DebugDraw、ビルダーAPI、Clang-Format CIを整備し、複数メンバーが並行実装できる基盤として運用しました。
12人開発を前進させる基盤をリードプログラマとして組む
12人チームが並行して実装を進められるように整えた、DirectX 11ベースのゲーム基盤です。私はリードプログラマとしてECS、描画補助、ビルダーAPI、開発ルールの整備を担当し、各メンバーがゲームロジック実装へ集中できる環境を支えました。
ライティング実装
ECS / Graphics / Components 層構造とビルダーAPI
アクションフロー — チャージ / 弾き / 壁衝突 / ゴール
DirectXの詳細を隠すビルダーAPI
world.Create().With<Transform>().With<MeshRenderer>().Build()
のような直感的なAPIを設計し、他のプログラマーがDirectXの細部を追わずにロジック実装へ集中できる環境を用意しました。
1000行近いドキュメントで仕様・使い方を文書化。PCHとClang-Format CIを組み合わせてコードスタイルを自動強制し、12人チーム開発でもコード品質を揃えやすい状態を維持した。
当たり判定やレイキャストを画面上に可視化するデバッグ描画システムをServiceLocatorパターンで実装。コードのどこからでも呼び出せる設計にし、デバッグ工数を削減した。
当たり判定と空間分割グリッドのデバッグ描画
Gamepadの振動プロファイルやポストエフェクトのパラメータを個別に調整し、数学的な処理に体感としての応答性を持たせた。
設計の意図
自分だけが使いやすい基盤ではなく、チーム全員が判断に迷わず使えることを優先しました。後半は他メンバーの実装支援や統合確認に多くの時間を回し、リードプログラマとして基盤整備で開発全体を支えました。運用ルールをドキュメントにまとめ、描画、入力、UI、演出、振動、フルスクリーン、ビデオシーン修正まで継続して手を入れたため、基盤は単なる抽象化ではなく、終盤まで全体の調整を支えられる形にすることが目標でした。
チーム開発で見えた反省
一番大きかった壁は描画です。DirectXでHLSLを書くこと、Assimpからアニメーションや描画データを分解すること、ライティングをゲーム内の見た目として成立させることは、ほとんど初めての状態から調査して組み込みました。
チーム面では、基盤を作るだけでは足りませんでした。APIや仕様を変えた後の共有が遅れ、古い使い方のまま実装されて引数漏れ、初期化不足、ビルドエラーにつながった場面があります。その場では修正と統合を巻き取りましたが、リードとして必要だったのは、変更内容をチーム全員が追える仕組みを先に作ることでした。
開発後半ではマージ作業やUI・入力・演出調整が集中し、実際に統合負荷が高くなりました。特にゲームパッド振動、BT対応、ステセレ調整、ポーズUI、デス数表示のような「小さく見えるがプレイ感に関わる修正」が後半へ集中しており、基盤側で先に吸収できる余地がありました。
この反省から、ドキュメント、Issue、PR、レビューで変更理由と影響範囲を残す運用を重く見ています。今作り直すなら、UIとゲームパッド対応も序盤から基盤化し、終盤に操作説明や入力対応が集中しない進め方にします。加えて、マージ前提の開発では「実装が正しいか」だけでなく「統合後に誰が困るか」を毎回確認する運用を、最初からルールとして固定します。
実装の深堀り:多人数開発を支えた空間分割と描画パイプライン
このタイトルでは、プレイ感の調整とチーム開発の両方を成立させるために、ゲームロジック側の処理負荷とエンジン側の使いやすさを同時に詰めました。特に衝突判定、設定反映、描画パス分離の3点は、手触りと開発速度の両方で実際に差が出ています。
1. Spatial Hash Grid による衝突候補の絞り込み
SpatialHashGrid.h では、3D空間を
cellSize 単位のグリッドに分け、std::unordered_map<int64_t,
std::vector<Entity>>
にエンティティを登録しています。全組み合わせを毎フレーム調べるのではなく、近傍セルだけを見る形にしたことで、ギミックや弾が増えても処理量が急に跳ねにくくなりました。
この構成にしておくと、部屋ごとの密度が高いステージでもロジック側の負荷を読みやすく、レベルデザインの自由度を下げずに済みます。
O(n²) 全ペア判定 → Spatial Hash Grid
による近傍セルのみ判定
2. ビルダーAPIと Config Manager による調整の回しやすさ
12人規模の開発では、基盤の都合でゲームロジック実装が止まる状態を避けたかったので、エンティティ生成はビルダーAPIで統一し、設定値は
ConfigManager.cpp
でTOMLからホットリロードできます。
// クライアント(プランナー・プログラマ)が直感的にエンティティを構築できるAPI
auto player = world.Create()
.With<Transform>(Vector3(0, 5, 0))
.With<MeshRenderer>(ModelCache::Get("Player.gltf"))
.With<Collider>(BoundingSphere(1.0f))
.With<PlayerController>()
.Build();
保存監視には
fs::last_write_time
を使い、移動速度や演出パラメータを再起動なしで反映できます。本番時は
--bake で
config.bin
にまとめることで、運用時は読み込み負荷を抑え、開発時は調整を速く回せる構成にしました。
config.toml 変更検知 → パース →
ゲームパラメータ即時反映
3. 役割ごとに分けた描画パス
描画は単一の関数にまとめず、ECSのSystemとしてパス単位に分離しています。描画順と責務を分けておくことで、演出追加の影響範囲を追いやすくし、デバッグの見通しも上がりました。
- ShadowRenderSystem: シャドウマップ生成を独立させ、ライティング調整時に影響範囲を切り分けやすくしました。
-
EmissiveRenderSystem:
Emissiveを持つエンティティだけを集めてブルーム用ソースを作り、発光表現を通常描画から分離しています。 - PostProcessSystem: 被ダメージ時の色収差や画面揺れなど、画面全体にかかる演出を最後段へ集約しました。
多階層レンダリング・空間分割および実行パイプラインの統合アーキテクチャ
DX_EnTT / ECS_BACE
DX_EnTTは、DirectX基盤でEnTTを採用できるか検証したC++のR&Dです。データ配置、参照安全性、描画責務を小さく切って確認しました。
成果: EnTTと自作ECSの設計差を整理し、後続基盤で採るべきデータ構造と責務分割の判断材料を明確にしました。
採用判断のためにECSの内部構造まで読む
汎用ECSライブラリ「EnTT」を表面上の使い勝手だけでなく、内部のメモリ管理や更新特性まで見て評価したR&Dプロジェクトです。ここで得た知見が、後続のフリップエスケイプの設計方針に直接つながっています。
AoS → SoA 変換によるキャッシュヒット率の改善
AoS(配列内の構造体)からSoA(構造体の配列)へメモリレイアウトを転換。CPUキャッシュのヒット率を高め、コンポーネントの反復処理を高速化した。
EnTT採用の判断材料を得るために、内部のメモリ管理とアーキテクチャを自分で読んで把握した。このR&Dを通じてフリップエスケイプの設計方針を固めた。
設計の意図
外部ライブラリをそのまま信頼するのではなく、内部実装を理解したうえで採用可否を決めたいと考えました。EnTTの構造を把握したことで、自作ECSとのトレードオフが言語化でき、何を次の基盤へ持ち込むか判断できるようになりました。DX_EnTT自体の実装だけでなく、前段のECS_BACEで向き合ったメモリリーク修正、パフォーマンス改善、入力統合、文書構成の見直しも含めて、採用判断を机上比較ではなく実装の痛みから行った点がこのR&Dの価値です。
R&Dで見えた課題
DX_EnTT自体の作業期間は短く、前段のECS_BACEからメモリリーク修正、パフォーマンス問題の再修正、メッシュ描画不具合、FBX取り込み、Unicode移行、入力統合、文書構成の大幅な見直しまで、基盤化の前に何度も土台を整え直しました。EnTT採用の評価で本当に時間を使ったのはAPIの見た目ではなく、周辺の実装を含めて拡張に耐える最小構成を探る工程でした。
特に学びとして大きかったのは、R&Dでも説明文書とサンプルの整備を後回しにすると、比較実験の前提自体がぶれやすいことです。文書構成を何度も見直したことで、判断材料を残す文書化が技術検証の一部だと分かりました。
次回改善するなら、性能計測・入力系・アセット導入を分離した小さな検証テーマごとに成功条件を先に決め、結果をログと文章で残します。そうすれば、採用可否の判断が「なんとなく使いやすい」ではなく、どの制約に対して何が効いたかまで説明できる基盤比較になります。
実装の深堀り:SoAとGenerational IDを検証した基盤実験
このプロジェクトでは、後続のゲーム基盤で採用するデータ構造を判断するために、更新効率と安全性を分けて検証しました。焦点を置いたのは、反復処理の速さを左右するメモリレイアウトと、エンティティ再利用時も整合性を保てるID管理です。
1. AoSからSoAへ切り替えた反復処理の検証
従来のAoSでは、更新に不要なデータまで一緒に読み込むため、コンポーネント数が増えるほどキャッシュ効率が落ちやすくなります。そこでEnTTの view を使い、必要なコンポーネントだけを連続アクセスする形に切り替えて、反復処理の特性を確認しました。
// EnTTによるSoAアプローチのイテレーション
auto view = registry.view<const Transform, const Velocity>();
view.each([&](const auto entity, const auto& transform, const auto& velocity) {
// 必要なデータだけがメモリ上で連続しているため、
// CPUキャッシュに乗りやすく、反復処理の効率が安定する
UpdatePosition(transform, velocity);
});
この検証により、更新系システムではデータ配置がそのまま処理時間に効くことを確認できました。後続の自作ECSで、どのコンポーネントを連続配置すべきか判断する材料になっています。
2. Generational ID による参照の安全性確保
生成と破棄が頻繁なエンティティでは、古い参照が残ると不具合の追跡が難しくなります。なので、単純な数値IDではなく世代情報を含む Generational ID を使い、再利用されたスロットへの古いアクセスを弾けます。
32bit ID — 上位16bit 世代番号 / 下位16bit
インデックス。再利用時は世代が上がり古い参照を無効化
3. 描画側で確認した転送コストの扱い
描画ではHLSL
5.0での法線マップ計算やライティングに加え、定数バッファ更新時の
Map/Unmap
の使い方も整理しました。CPU側の更新頻度とGPU転送量のバランスを見ながら、どこで更新をまとめるべきかを確認しています。
- オブジェクトごとの行列データはできるだけまとめ、転送単位を必要最小限に抑えました。
- ImGui による統計表示を併設し、描画とロジックの各フェーズでどこに時間がかかっているかを確認できます。
SoAアーキテクチャによるCPUキャッシュ最適化とGenerational
IDのパイプライン