• entries
    21
  • comments
    28
  • views
    33699

Assets: Creation & data processing

Sign in to follow this  
Juliean

1555 views

Last entry:

https://www.gamedev.net/blog/1930/entry-2261068-advanced-asset-handling/

So after I've talked about the theory behind the new asset system, I'll now go on to show some of the implementation details. I'll start with the more interesting stuff, namely the creation of the actual assets.

The asset class:

To understand whats going on, we first need to look at how the actual assets are setup. First major point, assets and their respective data are seperate entities, so they are setup using composition rather than inheritance. For this case, there are two asset base classes:// The generic asset base classclass BaseAsset{public: // .... const std::wstring& GetName(void) const; const std::wstring& GetFile(void) const; const std::wstring& GetAssetFile(void) const; const std::string& GetFileData(void) const; unsigned int GetType(void) const; const core::Variable& GetAttribute(const std::wstring& stName) const; const VariableMap& GetAttributes(void) const; bool HasAttribute(const std::wstring& stName) const; // ....}
It stores the generic asset information like name, file, attributes etc... . Then, we have a derived class simply called asset. This class is templated, because... I really like the CRTP, and it makes our lifes much easier (as it usually does):templateclass Asset final : public BaseAsset{public: typedef sys::Pointer DataPointer; Type* GetData(void) { return m_pAsset; } private: DataPointer m_data;}
Type equals to the actual asset type. So an asset of a specific type is defined as asset::Asset, for example. Whenever we need to process assets generically, we can use the BaseAsset, which can be safely casted down to the Asset<> type using a specific templated Cast<> method. Well,
its almost safe, we assume in this case that there is no other class derived from BaseAsset. Its a price I'm willing to pay for not having to use another virtual method, but it can be easily made more safe anytime.

Regarding to why composition over inheritance, its not just because "well everyone says its better, but there are some implicit benefits to this:

- Reloading of assets is much easier. We can change the internal data of the asset by well, simply changed its data. No need to recreate it and fix all external references, or provide a "Reload"-method, or some such shenanigans, just straight-forward replacement of the interior data.
- Seperation of asset declaration data and the actual implementation data. This is mostly for memory layout concerns. If we had the actual asset derive from BaseAsset (say gfx::ITexture), then all the Data members of the BaseAsset-class would be part of the memory the ITexture-class, and
therefore had to be accessed whenever we want to use any data member of the ITexture class. Of course this depends on whether the parent members are in front or behind the actual classes members, but you get the idea - whenver we only want to work with the asset implementation, we only work with the
implementation, and nothing else. To counter the cost of an additional dereferencation, we would normally use a special memory allocator which would place the content of the data pointer right behind the asset class, which would give us the best of both worlds.

The IO-class:

Okay, enough reasoning about why its done this way. Lets look at how this data pointer is loaded. For this, a specific base class has to be derived:class BaseAssetDataIO{public: virtual ~BaseAssetDataIO(void) = default; virtual BaseAsset* CreateAsset(const std::wstring& stName, const std::wstring& stFile, const std::wstring& stAssetFile, const std::string& stFileData, VariableMap&& mVariables, bool loadData) const = 0; virtual BaseAsset* CreateAssetEmpty(const std::wstring& stName) const = 0; virtual void SetAssetData(BaseAsset& asset, const std::wstring& stFile, const std::wstring& stAssetFile, const std::string& stFileData, VariableMap&& mVariables) const = 0; virtual void LoadAssetData(BaseAsset& asset) const = 0; virtual bool SaveAsset(const BaseAsset& asset, std::string& stDataOut) const = 0; virtual void HandleImport(BaseAsset& asset) const; virtual void HandleFinishLoad(void); virtual void OnUnloadAsset(const BaseAsset& asset) const = 0;};
This one has a lot of virtual methods, but most of them are only needed for certain special cases. For example, the OnUnloadAsset-method is only there if the asset data is registered somewhere and require deregistration, like a script class would. Also, this class is actually not what a user would themself use -
there is yet another templated class in between:templateclass AssetDataIO : public BaseAssetDataIO{public: virtual sys::Pointer LoadAssetData(const std::string& stFileData, const VariableMap& mVariables) const = 0; virtual bool SaveAssetData(const Type& data, std::string& stData) const { return false; }; virtual sys::Pointer OnCreateDefault(const std::wstring& stName) const { return sys::Pointer(); }; virtual void OnUnloadAssetData(const Type& data) const { }}
It overrides most of the base classes methods (and declares them final), but offers equivalents to them, just that now we are not dealing with the abstract BaseAsset-class, but with the concrete type. Also, this allows for greater generalization - CreateAsset, SetAssetData and LoadAssetData of
BaseAssetDataIO all use the "AssetDataIO::LoadAssetData" method to generate their data member.

Okay, I lied, there is yet another class before the user actually generates their IO class. Well, it isn't actually required, but let me explain...

See the "mVariables" parameter in the LoadAssetData-method? This is a map of variant instances, which represent the parameters required for data processing, like texture size, etc... Of course, one could now do this:sys::Pointer TextureAssetIO::LoadAssetData(const std::string& stFileData, const VariableMap& mVariables) const{ const auto vSize = mVariables[L"Size"].GetValue(); const auto loadFlags = mVariables["LoadFlags"].GetValue(); const auto format = mVariables[L"Format"].GetValue(); // load data here}
but I can tell from experience that this starts to suck ass quite fast, especially since you'd have to write a declaration for this too:asset::LoaderEntryVector TextureAssetLoader::GenerateDeclaration(void){ return { { L"Size", core::generateTypeId(), false }, // bool parameter is "isArray" { L"LoadFlags", core::generateTypeId(), false }, { L"Format", core::generateTypeID(), false } };}
So I finally wanted to be able to 1) generate the asset attribute declaration from a method and 2) auto-expanding the parameter map. I'm not going to show you the actual code for this, because its an ungodly horrible template mindfuck (I might make a coding horror entry one day...), but it lets you declare
your asset loader like this:#pragma once#include "ITextureLoader.h"#include "ITexture.h"#include "..\Asset\AssetIO.h"#include "..\Math\Vector.h"namespace acl{ namespace gfx { class Screen; class TextureAssetLoader : public asset::AssetDataBindingIO { public: TextureAssetLoader(const Screen& screen, const ITextureLoader& loader); sys::Pointer OnLoad(std::string stData, math::Vector2 vSize, LoadFlags loadFlags, TextureFormats format); static asset::LoaderEntryVector GenerateDeclaration(void); private: const ITextureLoader* m_pLoader; const Screen* m_pScreen; }; }}// TextureAssetLoader.cpp#include "TextureAssetLoader.h"#include "Screen.h"namespace acl{ namespace gfx { TextureAssetLoader::TextureAssetLoader(const Screen& screen, const ITextureLoader& loader) : m_pLoader(&loader), m_pScreen(&screen) { } sys::Pointer TextureAssetLoader::OnLoad(std::string stData, math::Vector2 vSize, LoadFlags loadFlags, TextureFormats format) { if(stData.empty()) { if(vSize.x == 0 || vSize.y == 0) vSize = m_pScreen->GetSize(); return m_pLoader->Create(vSize, format, loadFlags); } else return m_pLoader->Load(stData, loadFlags, format); } asset::LoaderEntryVector TextureAssetLoader::GenerateDeclaration(void) { return { { L"Size" }, { L"LoadFlags", LoadFlags::NONE }, { L"Format", TextureFormats::UNKNOWN } }; } }}
Note that you still have to write a declaration, since I don't have a way of reflecting parameter names and default values. But its already way easier than it was before, and once I'll go about implementing a parser/compiler for my custom reflection data, even writing this declaration would be passe.
Now the load function takes the expanded parameters, as well as the data loaded by eigther the or tag, and can use this information to create the asset.

And, belive it or not, but thats actually all you have to do add a new asset type. Obviously, the implementation inside ITextureLoader is a bit more complication (standard texture loading procedure, you know), but one tiny registration-function call and you have added an asset which is fully integrated
into the asset pipeline with (almost) all features. Of course, you also have to implement out the saving routine as well as the loading of a default resource (in case at some point the actual asset is missing). Also, you obviously have to implement an editor interface for this type of asset, which I'm
going to show maybe another time. But in comparison to what I had to do before to add a new asset type, minus the new functionalty every asset gets, this is just so much easier.

So yeah, thats it for this time. Next time, I'm going to talk a bit more about the behind-the-scenes implementation of the asset system, but I hope you've got a good idea how easy it is to deal with specific asset types in a void here. Thanks for reading, and until next time!
Sign in to follow this  


0 Comments


Recommended Comments

There are no comments to display.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now