Developer Comments

なぜこの設計なのか ― 実装思想と技術的判断の背景

Overview

Hybrid ECSは、「ECSかつ実用重視」をテーマに構築されました。 Pure ECSの学習を0からするには学習コストが高くまた、エディタとの連携を簡易的にするため今回はこの設計を採用しました。
そして、オブジェクト指向やコンポーネント指向は触れる機会があったため、まだないデータ指向についての知見を深めるために挑戦の意味を込めました。

Design

Production Costs

今回、開発したFalcon IDEでの削除できた工数について。

コードファイルの作成

開発にMSVSを使うとして1つのクラスを実装するには、まず追加したいフィルター上で右クリックし、 「追加」→「新しい項目」を選択し、 表示されるテンプレートから「ヘッダファイル」を選択し、ファイル名を入力して追加したディレクトリを選択し「追加」をクリックします。 その後、同様にC++ファイルも追加します。

プロジェクトの作成

同じくMSVSでプロジェクトを作成したい場合、ソリューションの上で右クリックし、 「追加」→「新しいプロジェクト」を選択し、 表示されるテンプレートからC++の適切なプロジェクトを選択し、プロジェクト名を入力して置きたいディレクトリを選択。

初期設定

プロジェクトを生成した後に、プリコンパイルヘッダやパッケージの設定、フィルタの設定などを行う必要がある。

コードの記述と制約

コードを書きますが、ある程度決まっている内容をまたコピペもしくは書かなくてはいけません。 そして、DLLである場合ランタイムとのInterface Boundaryを気にしなくてはいけません。

  • ソリューションエクスプローラー上でクリック→追加→新しいプロジェクト→プロジェクトテンプレート選択→プロジェクト名入力→ディレクトリ選択→追加
    →プリコンパイルヘッダの設定→言語のバージョン設定→インクルードパスの設定→アウトプット設定→その他設定→フィルタ設定
    →新しい項目→ヘッダファイル→名前入力→ディレクトリ選択→追加→新しい項目→ソースファイル→名前入力→追加
    →プロジェクトに合わせた制約を守ったコードを記述
  • Falcon IDE上でボタンをクリック→名前入力→Createボタンをクリック
  • Past

    Pragmatic(実用主義)

    Editor Layout Diagram
    図1. 旧設計思想図
    Hybrid ECSよりもさらに実用性を持たせた設計思想を過去には採用していました。 それをPragmatic Hybrid ECSと呼びpure ECSをかなり崩し、 学習コストと実装コストを大きく減らしたものでした。 実際には、Componentは純粋なデータ構造体ではなく、補助メソッドを備えたものなっており、 ゲッター、セッター、保存、読込、エディタのレンダーなどの関数を持っていました。 Systemはその他のロジックのみとなっており。 Componentには規定したメソッド以外を持たせないことを厳守し、開発していました。 しかし、この設計ではあまりにもECSからかけ離れており、また実用例がかなりまれであることから変更しました。

    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

    今後の展望は尽きることなく、上記以外にも興味のある機能や現時点での不満点を解決していきます。

    Closing

    このエンジンの開発方針は「理想を知ったうえで現実を選ぶ」です。 ECSの完全性を追うのではなく、エディタとトレードオフによる実用性を追求しています。 それが「Hybrid ECS」の名に込めた意図です。

    Contents