Skip to content

Implement Prefab workflow#776

Open
Gopmyc wants to merge 17 commits intoOverload-Technologies:mainfrom
Gopmyc:25-beta
Open

Implement Prefab workflow#776
Gopmyc wants to merge 17 commits intoOverload-Technologies:mainfrom
Gopmyc:25-beta

Conversation

@Gopmyc
Copy link
Copy Markdown
Contributor

@Gopmyc Gopmyc commented Apr 24, 2026

Description

This PR delivers the prefab MVP workflow in the editor end-to-end: creating prefab assets from actors, instantiating prefabs, maintaining actor-to-prefab links ("source instance"), editing prefab instances (apply/revert), and propagating asset renames through scenes and prefabs.

Implemented:

  • Added dedicated .ovprefab file type support in asset typing/parsing.
  • Added prefab icon resource and prefab picker label support.
  • Added Save as Prefab... in actor contextual menu.
  • Added prefab serialization/instantiation utilities (PrefabOperations).
  • Added actor prefab_source support (API + serialization/deserialization).
  • Added drag & drop prefab instantiation into scene view.
  • Added Create > From prefab... in actor creation menu (hierarchy + menu bar).
  • Added Apply to Prefab and Revert to Prefab for prefab-linked actors.
  • Added hierarchy prefab visual state (prefab icon for linked instance root + children).
  • Extended rename propagation logic to include .ovprefab files.
  • Added prefab rename/delete propagation to current scene actor prefab references.
  • Extended script migration retargeting to include .ovprefab files.

Related Issue(s)

Fixes #25

Review Guidance

Please validate the prefab workflow in this order:

  1. Create an actor hierarchy and use Save as Prefab....
  2. Instantiate the prefab by:
    • Drag & drop into Scene View,
    • Create > From prefab... from hierarchy/menu bar.
  3. Confirm prefab icon appears in hierarchy for the instance root and its children.
  4. Modify instance and run Apply to Prefab; verify .ovprefab content updates.
  5. Modify instance and run Revert to Prefab; verify actor hierarchy is replaced correctly.
  6. Rename/move/delete prefab from Asset Browser; verify references are retargeted in:
    • .ovscene files,
    • .ovprefab files,
    • current scene actor prefab source references.
  7. Sanity-check existing retarget flows (scripts/models/materials/sounds) still work with prefab files included.

Key files:

  • Sources/OvEditor/include/OvEditor/Utils/PrefabOperations.h
  • Sources/OvEditor/src/OvEditor/Utils/PrefabOperations.cpp
  • Sources/OvEditor/include/OvEditor/Core/EditorActions.h
  • Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp
  • Sources/OvCore/include/OvCore/ECS/Actor.h
  • Sources/OvCore/src/OvCore/ECS/Actor.cpp
  • Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp
  • Sources/OvEditor/src/OvEditor/Utils/ActorCreationMenu.cpp
  • Sources/OvEditor/include/OvEditor/Panels/SceneView.h
  • Sources/OvEditor/src/OvEditor/Panels/SceneView.cpp
  • Sources/OvTools/include/OvTools/Utils/PathParser.h
  • Sources/OvTools/src/OvTools/Utils/PathParser.cpp
  • Sources/OvCore/src/OvCore/Helpers/GUIHelpers.cpp
  • Sources/OvEditor/src/OvEditor/Core/EditorResources.cpp
  • Sources/OvEditor/src/OvEditor/Core/Editor.cpp
  • Resources/Editor/Textures/Prefab.png

Screenshots/GIFs

1 2 3 4 5

AI Usage Disclosure

Generated new code / Refactored code

Checklist

  • My code follows the project's code style guidelines
  • When applicable, I have commented my code, particularly in hard-to-understand areas
  • When applicable, I have updated the documentation accordingly
  • My changes don't generate new warnings or errors
  • I have reviewed and take responsibility for all code in this PR (including any AI-assisted contributions)

@Gopmyc Gopmyc marked this pull request as ready for review April 24, 2026 00:34
Copy link
Copy Markdown
Member

@adriengivry adriengivry left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks really good!

Some feedback I have:

  • In Unity, prefabs generally kind-of ignore the transformations on the topmost actor. With your feature, if I make a prefab from an actor, then duplicate this actor many time and place them in a level, reverting them to the original prefab will also move them back to the center of the level. We probably want the "Revert to prefab" to never revert the topmost actor's transform data. The only case the prefab's topmost actor's transform data would be used is when creating a new instance.
  • It would be AWESOME if we could drag-and-drop any actor from the hierarchy to any folder in the asset browser to turn it into a prefab (using the actor's name as the prefab's name).
  • When instancing a prefab, the prefab name should be used for the topmost actor, instead of the actor's name.

Comment thread Sources/OvEditor/src/OvEditor/Core/Editor.cpp Outdated
Comment thread Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp
Comment thread Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp Outdated
Comment thread premake5.lua
@Gopmyc Gopmyc requested a review from adriengivry April 24, 2026 11:08
Copy link
Copy Markdown
Member

@adriengivry adriengivry left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works very well, I tested with a bunch of Cornell boxes!

Image

A few issues I found:

  • See comments I added in the code
  • It is hard to know when looking at the hierarchy which source prefab an actor might come from. We could add a field to the actor's inspector, below "GUID" that would show an asset field of type prefab, similar to the "Target" field in asset properties, but that would be fully disabled.
  • Reverting an actor to its source prefab overrides its name. In the case where you name your objects (e.g. Pillar 1, Pillar 2, Pillar 3, it will always go back to "Pillar" after reverting)
  • Reverting an actor to its source prefab also move it in the hierarchy:
Before Revert After Revert
Image Image

Comment thread Sources/OvCore/include/OvCore/SceneSystem/PrefabOperations.h
Comment thread Sources/OvEditor/include/OvEditor/Core/EditorActions.h Outdated
Copy link
Copy Markdown
Member

@adriengivry adriengivry left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A regression I also found is using the "Create" menu to add an actor as a child of another actor end up attaching the new actor to the same parent as the actor the contextual menu has been opened from.

Image
Before After
Image Image

@Gopmyc
Copy link
Copy Markdown
Contributor Author

Gopmyc commented Apr 24, 2026

A regression I also found is using the "Create" menu to add an actor as a child of another actor end up attaching the new actor to the same parent as the actor the contextual menu has been opened from.

Image Before After Image Image

Good catch !
Fixed the Create menu parenting regression by resolving the contextual parent at click time

@Gopmyc
Copy link
Copy Markdown
Contributor Author

Gopmyc commented Apr 24, 2026

Works very well, I tested with a bunch of Cornell boxes!

Image A few issues I found:
  • See comments I added in the code
  • It is hard to know when looking at the hierarchy which source prefab an actor might come from. We could add a field to the actor's inspector, below "GUID" that would show an asset field of type prefab, similar to the "Target" field in asset properties, but that would be fully disabled.
  • Reverting an actor to its source prefab overrides its name. In the case where you name your objects (e.g. Pillar 1, Pillar 2, Pillar 3, it will always go back to "Pillar" after reverting)
  • Reverting an actor to its source prefab also move it in the hierarchy:

Before Revert After Revert
Image Image

Thanks !
Added a disabled Prefab Source field below GUID in Inspector, and revert now preserves actor name and no longer changes hierarchy placement

Copy link
Copy Markdown
Member

@adriengivry adriengivry left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Didn't look too hard in the code, since there are a few major issues that prevented further testing, and the code is likely gonna change.

Incorrect contextual actions

After creating a scene, adding a prefab, and reloading the scene, the contextual menu doesn't show anything else that "Save as prefab..." anymore, even after creating one.

Image

[Regression] Broken contextual actions on loaded scenes

Whenever a scene is loaded (not new scene), contextual actions on any actor will be broken. e.g. cannot delete, duplicate, save as prefab, "Create..." menu will attach new actor to root instead...

Wrong GUID re-assignation

  1. Create a prefab with actor as children
Image
  1. Drag-and-drop first children onto parent
Image Image
  1. Notice A GUID is still correct
Image
  1. Revert to prefab
  2. Notice A GUID is now different, and C now has A's GUID
Image

@Gopmyc
Copy link
Copy Markdown
Contributor Author

Gopmyc commented Apr 24, 2026

Note

Didn't look too hard in the code, since there are a few major issues that prevented further testing, and the code is likely gonna change.

Incorrect contextual actions

After creating a scene, adding a prefab, and reloading the scene, the contextual menu doesn't show anything else that "Save as prefab..." anymore, even after creating one.

Image ## [Regression] Broken contextual actions on loaded scenes Whenever a scene is loaded (not new scene), contextual actions on any actor will be broken. e.g. cannot delete, duplicate, save as prefab, "Create..." menu will attach new actor to root instead...

Wrong GUID re-assignation

  1. Create a prefab with actor as children
Image 2. Drag-and-drop first children onto parent

Image Image
3. Notice A GUID is still correct

Image 4. Revert to prefab 5. Notice A GUID is now different, and C now has A's GUID Image

Thanks for the detailed feedback, this is now addressed.

I fixed the blocking issues in d05bfd1:

  • Contextual actions no longer break/disappear after scene reload or on loaded scenes: the contextual menu now resolves the target from a live actor pointer validated against the current scene (instead of relying on a stale captured GUID).
  • The Create... submenu now uses the same live target resolution, so child creation is attached to the correct actor.
  • Revert GUID reassignment is fixed: child matching during revert now uses name-based matching first (with positional fallback), preventing GUID swaps when sibling order changed.

Related previous fixes are still in place (6e47f8d, fb1ee23).

@Gopmyc Gopmyc requested a review from adriengivry April 24, 2026 21:05
Copy link
Copy Markdown
Member

@adriengivry adriengivry left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functionality-wise it seems to be nearly feature-complete. There are a few bugs and issues I found that are listed below.

Otherwise great work!

std::vector<size_t> unmatchedTemplateChildren;
};

ChildMatchPlan BuildChildMatchPlan(OvCore::ECS::Actor& p_targetActor, OvCore::ECS::Actor& p_templateActor)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole matching strategy is still very fragile. We should never rely on actor names, or position in the hierarchy.

Here is an example case that fail:

Initial setup:

Stack (GUID 233...)
- A (GUID D72...)
- B (GUID 20B...)
- C (GUID C88...)

Moved A below C. And renamed all 3 to Foo:

Stack (GUID 233...)
- Foo (previously B) (GUID 20B...)
- Foo (previously C) (GUID C88...)
- Foo (previously A) (GUID D72...)

Revert to prefab:

Stack (GUID 233...)
- A (GUID 20B...)
- B (GUID C88...)
- C (GUID D72...)

As you can see, it doesn't match the initial setup anymore, A is now B, B is now C, and C is now A.

Instead of relying on Actor names and GUIDs, we should add new metadata to actors, alongside m_prefabSource, we should have m_prefabNodeGUID. Each actor inside of a prefab would have a unique GUID, that would be carried over when instanced. This way, we can easily track back which Actor in a scene correspond to which actor in a prefab.

* @param p_onItemClicked
*/
static void GenerateActorCreationMenu(OvUI::Widgets::Menu::MenuList& p_menuList, OvCore::ECS::Actor* p_parent = nullptr, std::optional<std::function<void()>> p_onItemClicked = {});
static void GenerateActorCreationMenu(OvUI::Widgets::Menu::MenuList& p_menuList, ActorParentProvider p_parentProvider = {}, std::optional<std::function<void()>> p_onItemClicked = {});
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I understand the point of this p_parentProvider. It worked well before without it, with only a pointer to the parent. Why do we suddenly need to patch this?

Comment on lines +89 to +91
if (auto* target = GetTargetActor())
{
EDITOR_EXEC(MoveToTarget(*target));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need that? It worked fine before.
This look like a workaround on a problem that hasn't been properly root-caused.

Same for all the actions below.

Comment on lines +258 to +276
OvCore::ECS::Actor* GetTargetActor() const
{
if (!m_targetActor)
{
return nullptr;
}

auto* currentScene = EDITOR_CONTEXT(sceneManager).GetCurrentScene();
if (!currentScene)
{
return nullptr;
}

const auto& actors = currentScene->GetActors();
const bool actorIsStillInScene =
std::find(actors.begin(), actors.end(), m_targetActor) != actors.end();

return actorIsStillInScene ? m_targetActor : nullptr;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't we just use OvCore::ECS::Actor* m_target; like before? I don't understand the problem this is trying to solve.

Comment on lines +119 to 182
std::function<void()> ActorWithComponentCreationHandler(ActorParentProvider p_parentProvider, std::optional<std::function<void()>> p_onItemClicked)
{
return Combine(EDITOR_BIND(CreateMonoComponentActor<T>, true, p_parent), p_onItemClicked);
return [p_parentProvider = std::move(p_parentProvider), p_onItemClicked]()
{
EDITOR_EXEC(CreateMonoComponentActor<T>(true, ResolveParent(p_parentProvider)));

if (p_onItemClicked.has_value())
{
p_onItemClicked.value()();
}
};
}

std::function<void()> ActorWithModelComponentCreationHandler(ActorParentProvider p_parentProvider, const std::string& p_modelName, std::optional<std::function<void()>> p_onItemClicked)
{
return [p_parentProvider = std::move(p_parentProvider), p_modelName, p_onItemClicked]()
{
EDITOR_EXEC(CreateActorWithModel(":Models\\" + p_modelName + ".fbx", true, ResolveParent(p_parentProvider), p_modelName));

if (p_onItemClicked.has_value())
{
p_onItemClicked.value()();
}
};
}

std::function<void()> ActorWithModelComponentCreationHandler(OvCore::ECS::Actor* p_parent, const std::string& p_modelName, std::optional<std::function<void()>> p_onItemClicked)
std::function<void()> CreateSkysphereHandler(ActorParentProvider p_parentProvider, std::optional<std::function<void()>> p_onItemClicked)
{
return Combine(EDITOR_BIND(CreateActorWithModel, ":Models\\" + p_modelName + ".fbx", true, p_parent, p_modelName), p_onItemClicked);
return [p_parentProvider = std::move(p_parentProvider), p_onItemClicked]()
{
CreateSkysphere(ResolveParent(p_parentProvider));

if (p_onItemClicked.has_value())
{
p_onItemClicked.value()();
}
};
}

std::function<void()> CreateSkysphereHandler(OvCore::ECS::Actor* p_parent, std::optional<std::function<void()>> p_onItemClicked)
std::function<void()> CreateAtmosphereHandler(ActorParentProvider p_parentProvider, std::optional<std::function<void()>> p_onItemClicked)
{
return Combine(std::bind(CreateSkysphere, p_parent), p_onItemClicked);
return [p_parentProvider = std::move(p_parentProvider), p_onItemClicked]()
{
CreateAtmosphere(ResolveParent(p_parentProvider));

if (p_onItemClicked.has_value())
{
p_onItemClicked.value()();
}
};
}

std::function<void()> CreateAtmosphereHandler(OvCore::ECS::Actor* p_parent, std::optional<std::function<void()>> p_onItemClicked)
std::function<void()> CreateCharacterHandler(ActorParentProvider p_parentProvider, std::optional<std::function<void()>> p_onItemClicked)
{
return Combine(std::bind(CreateAtmosphere, p_parent), p_onItemClicked);
return [p_parentProvider = std::move(p_parentProvider), p_onItemClicked]()
{
CreateCharacter(ResolveParent(p_parentProvider));

if (p_onItemClicked.has_value())
{
p_onItemClicked.value()();
}
};
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't need to change these, they are working perfectly fine. Same as above, looks like a patch on an issue that hasn't been properly root-caused.

Comment on lines +242 to +267
primitives.CreateWidget<MenuItem>("Cube").ClickedEvent += ActorWithModelComponentCreationHandler(p_parentProvider, "Cube", p_onItemClicked);
primitives.CreateWidget<MenuItem>("Sphere").ClickedEvent += ActorWithModelComponentCreationHandler(p_parentProvider, "Sphere", p_onItemClicked);
primitives.CreateWidget<MenuItem>("Cone").ClickedEvent += ActorWithModelComponentCreationHandler(p_parentProvider, "Cone", p_onItemClicked);
primitives.CreateWidget<MenuItem>("Cylinder").ClickedEvent += ActorWithModelComponentCreationHandler(p_parentProvider, "Cylinder", p_onItemClicked);
primitives.CreateWidget<MenuItem>("Plane").ClickedEvent += ActorWithModelComponentCreationHandler(p_parentProvider, "Plane", p_onItemClicked);
primitives.CreateWidget<MenuItem>("Gear").ClickedEvent += ActorWithModelComponentCreationHandler(p_parentProvider, "Gear", p_onItemClicked);
primitives.CreateWidget<MenuItem>("Helix").ClickedEvent += ActorWithModelComponentCreationHandler(p_parentProvider, "Helix", p_onItemClicked);
primitives.CreateWidget<MenuItem>("Pipe").ClickedEvent += ActorWithModelComponentCreationHandler(p_parentProvider, "Pipe", p_onItemClicked);
primitives.CreateWidget<MenuItem>("Pyramid").ClickedEvent += ActorWithModelComponentCreationHandler(p_parentProvider, "Pyramid", p_onItemClicked);
primitives.CreateWidget<MenuItem>("Torus").ClickedEvent += ActorWithModelComponentCreationHandler(p_parentProvider, "Torus", p_onItemClicked);
physicals.CreateWidget<MenuItem>("Physical Box").ClickedEvent += ActorWithComponentCreationHandler<CPhysicalBox>(p_parentProvider, p_onItemClicked);
physicals.CreateWidget<MenuItem>("Physical Sphere").ClickedEvent += ActorWithComponentCreationHandler<CPhysicalSphere>(p_parentProvider, p_onItemClicked);
physicals.CreateWidget<MenuItem>("Physical Capsule").ClickedEvent += ActorWithComponentCreationHandler<CPhysicalCapsule>(p_parentProvider, p_onItemClicked);
lights.CreateWidget<MenuItem>("Point").ClickedEvent += ActorWithComponentCreationHandler<CPointLight>(p_parentProvider, p_onItemClicked);
lights.CreateWidget<MenuItem>("Directional").ClickedEvent += ActorWithComponentCreationHandler<CDirectionalLight>(p_parentProvider, p_onItemClicked);
lights.CreateWidget<MenuItem>("Spot").ClickedEvent += ActorWithComponentCreationHandler<CSpotLight>(p_parentProvider, p_onItemClicked);
lights.CreateWidget<MenuItem>("Ambient Box").ClickedEvent += ActorWithComponentCreationHandler<CAmbientBoxLight>(p_parentProvider, p_onItemClicked);
lights.CreateWidget<MenuItem>("Ambient Sphere").ClickedEvent += ActorWithComponentCreationHandler<CAmbientSphereLight>(p_parentProvider, p_onItemClicked);
audio.CreateWidget<MenuItem>("Audio Source").ClickedEvent += ActorWithComponentCreationHandler<CAudioSource>(p_parentProvider, p_onItemClicked);
audio.CreateWidget<MenuItem>("Audio Listener").ClickedEvent += ActorWithComponentCreationHandler<CAudioListener>(p_parentProvider, p_onItemClicked);
others.CreateWidget<MenuItem>("Camera").ClickedEvent += ActorWithComponentCreationHandler<CCamera>(p_parentProvider, p_onItemClicked);
others.CreateWidget<MenuItem>("Post Process Stack").ClickedEvent += ActorWithComponentCreationHandler<CPostProcessStack>(p_parentProvider, p_onItemClicked);
others.CreateWidget<MenuItem>("Reflection Probe").ClickedEvent += ActorWithComponentCreationHandler<CReflectionProbe>(p_parentProvider, p_onItemClicked);
others.CreateWidget<MenuItem>("Skysphere").ClickedEvent += CreateSkysphereHandler(p_parentProvider, p_onItemClicked);
others.CreateWidget<MenuItem>("Atmosphere").ClickedEvent += CreateAtmosphereHandler(p_parentProvider, p_onItemClicked);
others.CreateWidget<MenuItem>("Character").ClickedEvent += CreateCharacterHandler(p_parentProvider, p_onItemClicked);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't be needed, see comment above.

if (!prefabName.empty())
instantiatedRoot->SetName(prefabName);

OVLOG_INFO("Prefab instantiated: " + realPath.string());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed. We recently removed the "Actor created" log, this one should follow the same treatment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Implement a prefab system

2 participants