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;
    }
};