Developer Comments
なぜこの設計なのか ― 実装思想と技術的判断の背景
Overview
Hybrid ECSは、「ECSかつ実用重視」をテーマに構築されました。
Pure ECSの学習を0からするには学習コストが高くまた、エディタとの連携を簡易的にするため今回はこの設計を採用しました。
そして、オブジェクト指向やコンポーネント指向は触れる機会があったため、まだないデータ指向についての知見を深めるために挑戦の意味を込めました。
Design
Production Costs
今回、開発したFalcon IDEでの削除できた工数について。
コードファイルの作成
開発にMSVSを使うとして1つのクラスを実装するには、まず追加したいフィルター上で右クリックし、 「追加」→「新しい項目」を選択し、 表示されるテンプレートから「ヘッダファイル」を選択し、ファイル名を入力して追加したディレクトリを選択し「追加」をクリックします。 その後、同様にC++ファイルも追加します。
プロジェクトの作成
同じくMSVSでプロジェクトを作成したい場合、ソリューションの上で右クリックし、 「追加」→「新しいプロジェクト」を選択し、 表示されるテンプレートからC++の適切なプロジェクトを選択し、プロジェクト名を入力して置きたいディレクトリを選択。
初期設定
プロジェクトを生成した後に、プリコンパイルヘッダやパッケージの設定、フィルタの設定などを行う必要がある。
コードの記述と制約
コードを書きますが、ある程度決まっている内容をまたコピペもしくは書かなくてはいけません。 そして、DLLである場合ランタイムとのInterface Boundaryを気にしなくてはいけません。
→プリコンパイルヘッダの設定→言語のバージョン設定→インクルードパスの設定→アウトプット設定→その他設定→フィルタ設定
→新しい項目→ヘッダファイル→名前入力→ディレクトリ選択→追加→新しい項目→ソースファイル→名前入力→追加
→プロジェクトに合わせた制約を守ったコードを記述
Past
Pragmatic(実用主義)
Native Component
今回のプロジェクトで初めて私はHot Reloadを試みました。 C ABI互換性やInterface Boundaryなど知らないことだらけでしたが、特に後々になって響いたのがNative Componentの存在でした。 それまではすべてのコンポーネントを Script Componentとして実装していましたが、RuntimeとDLL間での制約によりかなり不便になっていきコードが乱れていきました。 必要最低限のものはNative Componentとして実装する、それ以外はScript Componentとして実装するという デザイン思想を採用しました。
Entity Update
ECSも今回のプロジェクトから初めて採用した設計でした。 それまではオブジェクト指向でGame Objectを既存の親から継承しリストを回す方法でUpdateやDrawなどを行ってきました。 そのため、ECSになったときどのようにUpdateですべて回すのか、 どうやって当たり判定処理、移動処理、描画処理を順番づけてするのか、 そのところがあやふやでありかなり無理やりな形で行っていました。 設計のテストのような段階でしたが、今見るとかなりひどい出来です。
void FlScene::UpdateCameraEntity(const std::weak_ptr<Entity>& entity, float deltaTime)
{
if (auto sp{ entity.lock() })
{
for (auto& sys : m_systems) {
auto supportedType{ m_dllController->GetSupportedComponentType(sys) };
for (auto& component : sp->components) {
if (component->GetType() != supportedType) continue;
auto cameraComp{ dynamic_cast<BaseCameraComponent>(component) };
if (cameraComp)
{
sys->Update(cameraComp, sp->id, deltaTime);
...
}
}
}
for (auto& child : sp->children)
UpdateCameraEntity(child, deltaTime);
}
}
void FlScene::UpdateEntityRecursive(const std::weak_ptr<Entity>& entity, float deltaTime)
{
if (auto sp{ entity.lock() })
{
for (auto& sys : m_systems) {
auto supportedType{ m_dllController->GetSupportedComponentType(sys) };
for (auto& component : sp->components) {
if (component->GetType() == supportedType)
{
sys->Update(component, sp->id, deltaTime);
auto render{ dynamic_cast<BaseModelRenderComponent*>(component) };
if (render && render->GetModelRenderData().isLoading_)
{
...
}
}
}
auto collisionSys{ dynamic_cast<BaseCollisionSystem*>(sys) };
if (collisionSys)
{
for (const auto& [id, entity] : m_entityMap) {
for (auto& comp : entity->components) {
if (comp->GetType() == collisionSys->GetSupportedComponentType())
collisionSys->UpdateCollision(comp, id, deltaTime);
}
}
}
}
for (auto& child : sp->children)
UpdateEntityRecursive(child, deltaTime);
}
}
ここで抽象化を強く意識し、再設計していきました。一つのUpdateですべてのEntityを更新し、NativeでもScriptでも構わずComponentを付けれるようにするように改善しました。
Future
- 🔧 DirectX12対応によるGPUバッチング最適化
- 📜 CI/CDの導入
- 🧩 Assetsのサムネイル機能
- 🪶 Asset Importerの拡張(PMX, EXR, USD対応)
- 💡 暗号化の強化
- 🚀 RuntimeとEditorの切り離し
今後の展望は尽きることなく、上記以外にも興味のある機能や現時点での不満点を解決していきます。
Closing
このエンジンの開発方針は「理想を知ったうえで現実を選ぶ」です。 ECSの完全性を追うのではなく、エディタとトレードオフによる実用性を追求しています。 それが「Hybrid ECS」の名に込めた意図です。