アプリケーションフレームワークのインターフェース設計
現代のゲームエンジンにおいて、アプリケーションフレームワークはエンジン全体の挙動を制御する背骨のような役割を果たします。これはエンジンのブートストラップ(起動)、メインループの実行、そして終了処理を定義し、開発者が各プラットフォームの差異を意識せずにロジックを記述できる抽象化レイヤーを提供します。優れたフレームワークは、モジュール性、拡張性、およびプラットフォーム非依存性を備えている必要があります。
フレームワークのコア構造
まず、アプリケーションのライフサイクルを管理するための基底クラスを定義します。以下の実装では、エンジンの状態遷移と基本構成を管理します。
// Runtime/ApplicationCore.h
#pragma once
#include "../Base/ModuleInterface.h"
#include "../Base/EventSystem.h"
#include <memory>
#include <string>
#include <chrono>
namespace EngineRuntime
{
// アプリケーションの実行フェーズ
enum class AppPhase : uint8_t
{
None = 0,
Bootstrapping, // 起動処理中
Active, // メインループ実行中
InActive, // 一時停止状態
Hibernating, // バックグラウンド待機
Terminating // 終了処理中
};
// エンジン起動設定
struct LaunchSettings
{
std::string title = "Generic Engine App";
uint32_t screenWidth = 1920;
uint32_t screenHeight = 1080;
bool useVSync = true;
float fixedTickRate = 60.0f;
std::string logFileName = "runtime.log";
#if defined(PLATFORM_WIN32)
bool preferD3D12 = true;
#endif
};
// 抽象アプリケーションインターフェース
class IEngineApp : public IEngineModule
{
public:
IEngineApp()
: currentPhase_(AppPhase::None)
, totalTime_(0.0f)
, frameCounter_(0)
{}
virtual ~IEngineApp() = default;
// ライフサイクルメソッド
virtual bool Boot(const LaunchSettings& settings) = 0;
virtual int Execute() = 0;
virtual void Exit() = 0;
AppPhase GetPhase() const { return currentPhase_; }
const LaunchSettings& GetSettings() const { return settings_; }
protected:
void TransitionTo(AppPhase nextPhase)
{
currentPhase_ = nextPhase;
}
AppPhase currentPhase_;
LaunchSettings settings_;
uint64_t frameCounter_;
float totalTime_;
float lastDeltaTime_;
};
}
プラットフォーム抽象化レイヤー (PAL)
エンジンのポータビリティを確保するため、OS固有のウィンドウ処理やイベントループをPALとして分離します。以下は、Windows環境を想定したウィンドウ管理の実装例です。
// Platform/Win32/Win32Surface.h
#pragma once
#ifdef PLATFORM_WIN32
#include "../../Runtime/ApplicationCore.h"
#include <windows.h>
namespace EngineRuntime
{
class Win32Surface
{
public:
struct Desc
{
std::string label;
int width, height;
bool isFullscreen;
};
bool Initialize(const Desc& desc);
void ProcessSystemMessages();
void* GetInternalHandle() const { return hWnd_; }
private:
HWND hWnd_ = nullptr;
HINSTANCE hInstance_ = nullptr;
static LRESULT CALLBACK WindowMsgRouter(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
};
class Win32AppHost : public IEngineApp
{
public:
bool Boot(const LaunchSettings& settings) override
{
TransitionTo(AppPhase::Bootstrapping);
Win32Surface::Desc surfaceDesc;
surfaceDesc.label = settings.title;
surfaceDesc.width = settings.screenWidth;
surfaceDesc.height = settings.screenHeight;
if (!surface_.Initialize(surfaceDesc)) return false;
TransitionTo(AppPhase::Active);
return true;
}
int Execute() override
{
auto prevTime = std::chrono::high_resolution_clock::now();
while (currentPhase_ == AppPhase::Active)
{
surface_.ProcessSystemMessages();
auto now = std::chrono::high_resolution_clock::now();
float deltaTime = std::chrono::duration<float>(now - prevTime).count();
prevTime = now;
OnUpdate(deltaTime);
OnDraw();
frameCounter_++;
}
return 0;
}
virtual void OnUpdate(float dt) {}
virtual void OnDraw() {}
void Exit() override { TransitionTo(AppPhase::Terminating); }
private:
Win32Surface surface_;
};
}
#endif
入力および出力マッピングシステム
入力システムは、ハードウェア(キーボード、マウス、コントローラー)からの生データを、ゲーム内アクション(「ジャンプ」「射撃」など)に変換する役割を担います。これにより、コードを修正することなく操作方法のカスタマイズが可能になります。
入力デバイスの抽象化
まず、すべての入力ソースを統一されたインターフェースで扱えるようにします。
// Input/InputDevice.h
#pragma once
#include <vector>
#include <string>
namespace EngineRuntime
{
enum class InputSourceType { Digital, Analog };
// デバイスごとの入力状態
struct RawInputState
{
uint32_t inputId;
float value; // 0.0 or 1.0 for Digital, -1.0 to 1.0 for Analog
bool isTriggered; // このフレームで押されたか
};
class IInputProvider
{
public:
virtual ~IInputProvider() = default;
virtual void Poll(std::vector<RawInputState>& outStates) = 0;
virtual const char* GetDeviceName() const = 0;
};
}
アクションマッピングの設計
生入力をゲームのアクションに紐付けるマッピングレイヤーを構築します。これにより、同じ「移動」アクションを、キーボードのWASDとコントローラーのスティックの両方に割り当てることができます。
// Input/ActionMapper.h
#pragma once
#include "InputDevice.h"
#include <unordered_map>
namespace EngineRuntime
{
// アクションの定義
enum class GameActionType { Button, Axis };
struct ActionBinding
{
std::string actionName;
InputSourceType srcType;
uint32_t hardwareCode;
float sensitivity = 1.0f;
float deadzone = 0.1f;
};
class ActionMap
{
public:
void Bind(const std::string& name, const ActionBinding& binding)
{
bindings_[name].push_back(binding);
}
float EvaluateAction(const std::string& name, const std::vector<RawInputState>& currentStates)
{
float result = 0.0f;
auto it = bindings_.find(name);
if (it == bindings_.end()) return result;
for (const auto& binding : it->second)
{
for (const auto& state : currentStates)
{
if (state.inputId == binding.hardwareCode)
{
float val = state.value;
// デッドゾーン処理
if (std::abs(val) < binding.deadzone) val = 0.0f;
result += val * binding.sensitivity;
}
}
}
return std::clamp(result, -1.0f, 1.0f);
}
private:
std::unordered_map<std::string, std::vector<ActionBinding>> bindings_;
};
}
ジェスチャ認識の統合
モバイルデバイスやタッチ入力向けに、単一のタッチポイントを「タップ」「スワイプ」「ピンチ」などの高レベルなジェスチャとして認識するロジックを組み込みます。
// Input/GestureProcessor.h
#pragma once
namespace EngineRuntime
{
struct TouchData
{
float x, y;
float timestamp;
bool isDown;
};
class GestureInterpreter
{
public:
void Update(const TouchData& data)
{
if (data.isDown && !wasDown_)
{
startPos_ = { data.x, data.y };
startTime_ = data.timestamp;
}
else if (!data.isDown && wasDown_)
{
AnalyzeGesture(data);
}
wasDown_ = data.isDown;
}
private:
void AnalyzeGesture(const TouchData& endData)
{
float dx = endData.x - startPos_.x;
float dy = endData.y - startPos_.y;
float dt = endData.timestamp - startTime_;
if (std::sqrt(dx*dx + dy*dy) < 10.0f && dt < 0.2f)
{
// タップとして認識
BroadcastGesture("OnTap");
}
else if (std::abs(dx) > 50.0f && dt < 0.5f)
{
// スワイプとして認識
BroadcastGesture(dx > 0 ? "SwipeRight" : "SwipeLeft");
}
}
void BroadcastGesture(const std::string& eventName) { /* イベント発火 */ }
struct { float x, y; } startPos_;
float startTime_;
bool wasDown_ = false;
};
}
これらのシステムを統合することで、アプリケーションの基盤は、異なるプラットフォームや入力デバイスに対しても一貫した動作を維持できるようになります。フレームワークがライフサイクルを管理し、入力マッピングシステムが抽象化された操作感を提供することで、開発者はゲームプレイの本質的なロジックに集中することが可能になります。