🎮

C++でゲーム用AssetManagerの設計を考えてみる

2023/08/03に公開

はじめに

ゲームにはModel、Texture、Audio、Effect、etc...、様々なAssetからできています。UnityなどのゲームエンジンではそういったAssetを管理しやすくしてくれる機能が備わっていますが、ネイティブ開発ではそういったものも一から作らなければなりません。その際に管理しやすい(と私が思っている)設計を紹介します。

設計

設計といってもそんなに複雑ではありません。この設計では大きく分けて4つのクラスを作ります。{AssetType}にはModelやEffectなど使うAsset名と置き換えてください。

  • {AssetType}Data
    ファイル一つ単位の情報を持ったクラス。持っている情報はロード時以外で情報が変更されることはない(してはならない)
  • {AssetType}Instance
    Assetをインスタンス化する際に生成するクラス。このクラスには{AssetType}Dataとゲーム中に変更したい値を持ち、このクラスの値は外から自由に変更できる。
  • {AssetType}Trasnfrom
    {AssetType}Instanceの値をまとめておくクラス。値が増えるとgetter/setterが増えて煩雑になるので、生成する際にこれをshared_ptrで返して共有する。
  • {AssetType}Manager
    {AssetType}Instanceを生成、監視するクラス。生成と削除はこのManagerを通して行う。

実際にゲーム開発をしている人はピンとくると思いますが、Assetが保存されているファイル自体は1つですが、ゲーム中(の同フレーム中)では重複することがあると思います。例えば、同じ3DModelやSEやEffectを同時に出し、それぞれで違うTrasnformを持ったり、違うパラメータを持ったりすることがあると思います。なので{AssetType}Dataを直接使うのではなく、それを複製した{AssetType}Instanceで管理する方が都合が良いため、こういった設計になっています。またManagerでは、AudioやEffectなどは自動で消えることがあるので生存時間などを監視して自動的に開放する機能を持たせています。今回はエフェクトを再生できるライブラリEffekseer for C++を例に実際にコードを書いていきます。

EffectData

変数として以下を持ちます。

// Effectファイルがあるパス
const std::string    m_filePath;
// エフェクトを`Effekseer::Effect::Create`で生成した際の参照
Effekseer::EffectRef m_effectRef;

関数として以下を持ちます。

// コンストラクタでEffectファイルがあるパスを受け取る
EffectData(std::string_view file_path);
// 実際にファイルからEffectをロードする
void Load(const Effekseer::ManagerRef& manager);
// ファイルパス取得
const std::string& GetFilePath();
// EffectRefを取得
const Effekseer::EffectRef& GetEffectRef();

実装

class EffectData
{
public:
    EffectData(std::string_view file_path)
        : m_filePath(file_path)
    {}
    void Load(const Effekseer::ManagerRef& manager) {
        m_effectRef = Effekseer::Effect::Create(manager, string::StrToUtf16(m_filePath).c_str());
    }
    const std::string& GetFilePath() const noexcept {
        return m_filePath;
    }
    const Effekseer::EffectRef& GetEffectRef() const noexcept {
        return m_effectRef;
    }
private:
    const std::string    m_filePath;
    Effekseer::EffectRef m_effectRef;
};

EffectTransform

変数として以下を持ちます。

// 姿勢用の行列(移動と回転)
DirectX::SimpleMath::Matrix matrix;
// ループするか
bool                        isLoop   = false;
// 再生速度
float                       speed    = 1.f;
// 再生フレームの上限
int                         maxFrame = 0;

これはInstance後に変更される値をまとめておく構造体になります。なので他にパラメータが必要ならここに記述していきます。今回はエフェクト再生時にこの構造体をshared_ptrで共有する形にしています。

実装

struct EffectTransform {
    DirectX::SimpleMath::Matrix matrix;
    bool                        isLoop   = false;
    float                       speed    = 1.f;
    int                         maxFrame = 0;
};

EffectInstance

変数として以下を持ちます。

// このクラスが生成されてからの経過時間
double                           elapsedTime = 0;
// `Effekseer::ManagerRef::Play`で再生した際のハンドル
Effekseer::Handle                handle      = 0;
// `EffectTransform`の共有ポインタ
std::shared_ptr<EffectTransform> effectTransform;
// `EffectData`の共有ポインタ
const std::shared_ptr<EffectData> m_spEffectData;

関数として以下を持ちます。

// コンストラクタでEffectDataを受け取る
EffectInstance(const std::shared_ptr<EffectData>& effect_data)
// EffectDataを取得
std::shared_ptr<const EffectData> GetEffectData()

実装

class EffectInstance
{
public:

    EffectInstance(const std::shared_ptr<EffectData>& effect_data)
        : m_spEffectData(effect_data)
    {}

    std::shared_ptr<const EffectData> GetEffectData() const noexcept {
        return m_spEffectData;
    }

    double                           elapsedTime = 0;
    Effekseer::Handle                handle      = 0;
    std::shared_ptr<EffectTransform> effectTransform;

private:

    const std::shared_ptr<EffectData> m_spEffectData;

};

EffectManager

変数として以下を持ちます。

// Set関数で登録した元データリスト
std::unordered_map<std::string, std::shared_ptr<effekseer_helper::EffectData>> m_spEffectData;
// Emit関数でインスタンス化したインスタンスリスト
std::unordered_multimap<std::string, std::unique_ptr<effekseer_helper::EffectInstance>> m_upEffectInstances;

関数として以下を持ちます。

// 毎フレーム呼び出し、監視し適宜解放も行う
void Update(double delta_time);
// 元データを登録する
void SetEffect(std::string_view effect_name, std::string_view file_path);
// インスタンス化を行う
std::shared_ptr<effekseer_helper::EffectTransform> Emit(std::string_view effect_name, const EffectTransform& effect_transform, bool is_unique = false);

実装

class EffekseerManager
{
public:

    void Update(double delta_time) {
        constexpr double effect_frame = 60.0;
        for (auto iter = m_upEffectInstances.begin(); iter != m_upEffectInstances.end();) {
            auto& data     = *iter->second;
            auto& handle   = iter->second->handle;
            auto& tranform = iter->second->effectTransform;

            if (data.elapsedTime == 0) {
                handle = m_managerRef->Play(data.GetEffectData()->GetEffectRef(), 0, 0, 0);
            }

            if (data.elapsedTime > (tranform->maxFrame / effect_frame)) {
                m_managerRef->StopEffect(handle);
                if (tranform->isLoop) {
                    data.elapsedTime = 0;
                }
                else {
                    iter = m_upEffectInstances.erase(iter);
                }
            }
            else {
                m_rendererRef->SetTime(static_cast<float>(data.elapsedTime));
                m_managerRef->SetMatrix(handle, effekseer_helper::ToMatrix43(tranform->matrix));
                m_managerRef->SetSpeed(handle, tranform->speed);
                data.elapsedTime += delta_time;
                ++iter;
            }
        }

        m_managerRef->Update(static_cast<float>(delta_time * effect_frame));
    }

    void SetEffect(std::string_view effect_name, std::string_view file_path) {
        m_spEffectData.emplace(effect_name, std::make_shared<effekseer_helper::EffectData>(m_managerRef, file_path));
    }
    
    std::shared_ptr<effekseer_helper::EffectTransform> Emit(std::string_view effect_name, const effekseer_helper::EffectTransform& effect_transform, bool is_unique = false) {
        if (is_unique) {
            if (auto iter = m_upEffectInstances.find(effect_name.data()); iter != m_upEffectInstances.end()) {
                return iter->second->effectTransform;
            }
        }
        if (auto iter = m_spEffectData.find(effect_name.data()); iter != m_spEffectData.end()) {
            auto effect_instance = std::make_unique<effekseer_helper::EffectInstance>(iter->second);
            effect_instance->effectTransform = std::make_shared<EffectTransform>(effect_transform);
            auto& sp_et = effect_instance->effectTransform;
            m_upEffectInstances.emplace(effect_name, std::move(effect_instance));
            return sp_et;
        }
        else {
            assert::ShowError(ASSERT_FILE_LINE, "EffectData is not found.");
        }
        return nullptr;
    }

private:

    std::unordered_map<std::string, std::shared_ptr<effekseer_helper::EffectData>>          m_spEffectData;
    std::unordered_multimap<std::string, std::unique_ptr<effekseer_helper::EffectInstance>> m_upEffectInstances;

};

さいごに

2023/08/03 誤字を修正

Assetの管理は初めてゲームの設計からする人に対しては難関だと思います(自分もそうでした)。はじめはこの設計さえ覚えていれば複数インスタンス化することもできますし、他のAssetに対しても、おおまかにはこの設計で問題ないはずです。ここからの発展の余地としては、Textureや3DModelなどのロード処理に時間がかかってしまうAssetに対する非同期ロードの実装や基底クラスを作る事でしょうか。

神戸電子専門学校ゲーム技術研究部

Discussion