Why?
In O3DE, your components are separated into two: an editor component and a runtime component. When you save your level, the editor component is called to create a runtime version of that component.
This is abstracted away from you if you just create a normal component, but if you want some functionality that's only in the editor, then you will want to start sharing code between these types, and that's where Component Adapter comes in.
Using Component Adapter
You'll need a few classes for your component:
- Controller class for shared logic between your editor and runtime.
- Config class to share common config values
- Runtime Component
- Editor Component
Example
Let's make a component that manages a voxel world.
Config
class VoxelWorldComponentConfig
: public AZ::ComponentConfig
{
AZ_RTTI(VoxelWorldComponentConfig, AZ::ComponentConfig);
public:
static void Reflect(AZ::ReflectContext* context)
{
if (auto sc = azrtti_cast<AZ::SerializeContext*>(context))
{
sc->Class<VoxelWorldComponentConfig>()
->Version(0)
->Filed("ChunkSize", &VoxelWorldComponentConfig::chunkSize)
->Filed("WorldScale", &VoxelWorldComponentConfig::worldScale);
}
}
uint32_t chunkSize = 32;
float worldScale = 1.0f;
};
Shared Logic
class VoxelWorldController
: protected AZ::TickBus::Handler
{
public:
AZ_RTTI(VoxelWorldController, "UUID");
VoxelWorldController(){}
VoxelWorldController(const VoxelWorldComponentConfig& config)
{
m_config = config;
}
static void Reflect(AZ::ReflectContext* context)
{
VoxelWorldComponentConfig::Reflect(context);
if (auto sc = azrtti_cast<AZ::SerializeContext*>(context))
{
sc->Class<VoxelWorldController>()
->Version(0)
->Field("Config", &VoxelWorldController::m_config);
}
}
void SetConfiguration(const VoxelWorldComponentConfig& config)
{
m_config = config;
}
[[nodiscard]] const VoxelWorldComponentConfig& GetConfiguration() const
{
return m_config;
}
void Activate(const AZ::EntityComponentIdPair& entityComponentIdPair)
{
m_entityComponentIdPair = entityComponentIdPair;
AZ::TickBus::Handler::BusConnect();
}
void Deactivate()
{
AZ::TickBus::Handler::BusDisconnect();
}
private:
AZ::EntityComponentIdPair m_entityComponentIdPair;
VoxelWorldComponentConfig m_config;
};
Runtime component
#include <AzCore/Component/Component.h>
#include <AzFramework/Components/ComponentAdapter.h>
//Include your config and controller
class VoxelWorldComponent
: public AzFramework::Components::ComponentAdapter<VoxelWorldController, VoxelWorldComponentConfig>
{
using Super = AzFramework::Components::ComponentAdapter<VoxelWorldController, VoxelWorldComponentConfig>;
public:
AZ_COMPONENT(VoxelWorldComponent, "{UUID}", Super);
VoxelWorldComponent() = default;
explicit VoxelWorldComponent(const VoxelWorldComponentConfig& config)
: Super(config)
{
}
static void Reflect(AZ::ReflectContext* context)
{
Super::Reflect(context);
if (auto sc = azrtti_cast<AZ::SerializeContext*>(context))
{
sc->Class<VoxelWorldComponent, Super>()
->Version(0);
}
}
void Activate() override
{
Super::Activate();
//Do some runtime only stuff
}
};
Editor component
You'll want to put this in your gems tools and only compile it your editor target of course.
#include <AzCore/Component/Component.h>
#include <AzToolsFramework/ToolsComponents/EditorComponentBase.h>
//Include your runtime component and your components config
class EditorVoxelWorldComponent
: public AzToolsFramework::Components::EditorComponentAdapter
<VoxelWorldController, VoxelWorldComponent, VoxelWorldComponentConfig>
{
public:
//It should include the same template arguments as you did above
using Super = EditorComponentAdapter;
AZ_EDITOR_COMPONENT(EditorVoxelWorldComponent, "{UUID}", Super);
static void Reflect(AZ::ReflectContext* context)
{
Super::Reflect(context);
if (auto sc = azrtti_cast<AZ::SerializeContext*>(context))
{
sc->Class<EditorVoxelWorldComponent, Super>()
->Version(0);
using namespace AZ::Edit;
if (auto ec = sc->GetEditContext())
{
ec->Class<EditorVoxelWorldComponent>("Voxel World Component", "")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::Category, "Voxel")
->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Level"))
->Attribute(AZ::Edit::Attributes::AutoExpand, true);
ec->Class<VoxelWorldController>("VoxelWorldComponentController", "")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::AutoExpand, true)
->DataElement(AZ::Edit::UIHandlers::Default, &VoxelWorldController::m_config, "Configuration", "")
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly);
ec->Class<VoxelWorldComponentConfig>("VoxelWorldComponent Config", "")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Level"))
->Attribute(AZ::Edit::Attributes::AutoExpand, true)
->DataElement(UIHandlers::Default, &VoxelWorldComponentConfig::chunkSize, "Chunk Size",
"Size in voxels for each chunk")
->DataElement(UIHandlers::Default, &VoxelWorldComponentConfig::worldScale, "World Scale",
"Size of a single voxel");
}
}
}
EditorVoxelWorldComponent() = default;
explicit EditorVoxelWorldComponent(const VoxelWorldComponentConfig& config)
: Super(config)
{
}
void Activate() override
{
Super::Activate();
//Editor only activate
}
void Deactivate() override
{
}
AZ::u32 OnConfigurationChanged() override
{
return 0;
}
};