Custom Asset Type
Add a new asset class from a native addon — registered with the engine factory, importable from the editor, version-gated for serialization, and exposed in the asset browser. The VideoPlayer addon's VideoClip asset is a fully-worked production example.
What this gives you
- A new asset class that lives alongside built-in assets like
Texture,StaticMesh,SoundWave. - Editor-side import: drop a file with your registered extension into the asset browser and the editor calls your
Import(). - Inspector properties via
GatherProperties. - Round-tripped serialization (
SaveStream/LoadStream) with version gates so older saves keep loading.
Files to add
Packages/com.example.myaddon/
Source/
Assets/
MyAsset.h
MyAsset.cpp
MyAddon.cpp (existing entry point)
Asset header
// Source/Assets/MyAsset.h
#pragma once
#include "Asset.h"
class MyAsset : public Asset
{
public:
DECLARE_ASSET(MyAsset, Asset);
MyAsset();
virtual ~MyAsset();
virtual void Create() override;
virtual void Destroy() override;
virtual void SaveStream(Stream& stream, Platform platform) override;
virtual void LoadStream(Stream& stream, Platform platform) override;
virtual void GatherProperties(std::vector<Property>& outProps) override;
#if EDITOR
virtual void Import(const std::string& path, ImportOptions* options) override;
#endif
float GetSpeed() const { return mSpeed; }
void SetSpeed(float v) { mSpeed = v; }
protected:
static const uint32_t kVersionInitial = 1;
static const uint32_t kVersionWithColor = 2;
static const uint32_t kVersionCurrent = kVersionWithColor;
float mSpeed = 1.0f;
glm::vec4 mColor = glm::vec4(1.0f);
std::vector<uint8_t> mSourceData; // raw imported bytes (cooked formats live in subclasses)
};
DECLARE_ASSET(MyAsset, Asset) registers the class with the engine's asset factory at static-init time when the addon DLL loads. You do not need to touch any engine factory files.
Asset implementation
// Source/Assets/MyAsset.cpp
#include "Assets/MyAsset.h"
#include "Stream.h"
#include "Property.h"
FORCE_LINK_DEF(MyAsset);
DEFINE_ASSET(MyAsset);
MyAsset::MyAsset() {}
MyAsset::~MyAsset() {}
void MyAsset::Create() { Asset::Create(); }
void MyAsset::Destroy() { Asset::Destroy(); }
void MyAsset::SaveStream(Stream& stream, Platform platform)
{
Asset::SaveStream(stream, platform);
stream.WriteUint32(kVersionCurrent); // version your own format
stream.WriteFloat(mSpeed);
stream.WriteVec4(mColor);
stream.WriteUint32((uint32_t)mSourceData.size());
if (!mSourceData.empty())
{
stream.WriteBytes(mSourceData.data(), (uint32_t)mSourceData.size());
}
}
void MyAsset::LoadStream(Stream& stream, Platform platform)
{
Asset::LoadStream(stream, platform);
uint32_t version = stream.ReadUint32();
mSpeed = stream.ReadFloat();
if (version >= kVersionWithColor)
{
mColor = stream.ReadVec4();
}
uint32_t numBytes = stream.ReadUint32();
mSourceData.resize(numBytes);
if (numBytes > 0)
{
stream.ReadBytes(mSourceData.data(), numBytes);
}
}
void MyAsset::GatherProperties(std::vector<Property>& outProps)
{
Asset::GatherProperties(outProps);
outProps.push_back(Property("Speed", &mSpeed, PropertyType::Float));
outProps.push_back(Property("Color", &mColor, PropertyType::Color));
}
#if EDITOR
void MyAsset::Import(const std::string& path, ImportOptions* options)
{
// Read the source file into mSourceData. Real cookers parse the format
// here, populate fields, and either store cooked bytes inline or write a
// sidecar file (see VideoClip's THP/N3MV pipeline for the sidecar pattern).
FILE* fp = fopen(path.c_str(), "rb");
if (!fp) return;
fseek(fp, 0, SEEK_END);
size_t size = (size_t)ftell(fp);
fseek(fp, 0, SEEK_SET);
mSourceData.resize(size);
if (size > 0) fread(mSourceData.data(), 1, size, fp);
fclose(fp);
SetDirtyFlag(); // editor-only — see "Save dirty state" gotcha below
}
#endif
FORCE_LINK_DEF(MyAsset) plus FORCE_LINK_CALL(MyAsset) in the addon's OnLoad keep the linker from dropping the translation unit when no other addon code references it.
Wire the addon entry point
In your existing MyAddon.cpp:
#include "Plugins/PolyphasePluginAPI.h"
#include "Plugins/PolyphaseEngineAPI.h"
#include "Assets/MyAsset.h"
#if EDITOR
#include "AssetManager.h" // RegisterImportExtension lives here
#endif
static int OnLoad(PolyphaseEngineAPI* api)
{
FORCE_LINK_CALL(MyAsset);
#if EDITOR
TypeId t = MyAsset::GetStaticType();
RegisterImportExtension(".myext", t);
RegisterImportExtension(".myx", t);
#endif
return 0;
}
RegisterImportExtension teaches the editor's import dispatcher to instantiate MyAsset when a file with the matching extension is imported via the asset browser, drag-drop, or Reimport. It's editor-only.
Save dirty state
If your asset is mutated programmatically (e.g. from a custom editor action), call SetDirtyFlag() on the asset so the editor's unsaved-changes flow picks it up. SetDirtyFlag only exists in #if EDITOR builds, so wrap call sites accordingly.
Verification
- Place a
foo.myextfile inside the project'sAssets/directory. - In the editor, the asset browser should pick it up automatically and show a
MyAssetinstance. - Open the inspector —
SpeedandColorshould be editable. - Edit the values, save the project, restart the editor — the values should round-trip.
- To test version migration: bump
kVersionCurrentto a new constant, add a new field gated behind it, and confirm the asset still loads from a save written before the bump.
See also
Engine/Source/Engine/Asset.h— base class,ASSET_VERSION_CURRENT,StreamAPI.- VideoPlayer's
Source/Assets/VideoClip.cpp— production-grade asset withImport(), sidecar cook output, and inspector cook-time knobs. - External Library Integration — pair this with FFmpeg-style import if your asset wraps third-party data.