Ref: https://github.com/o3de/sig-content/issues/112
Through the implementation of https://github.com/o3de/sig-content/issues/75 , support now exists in the editor behind a feature flag to author and manage prefab overrides. A brief tutorial explaining the current state of overrides behind the flag can be found here. In the current state, users can only see and revert overrides on entities rather than individual components and properties. Since an entity can have many components under it, it is very likely that users would only want to revert specific overrides on an entity rather than all of them at once. This is what other major game engines offer too. There have been attempts made in the past to add prefab override support to the current RPE based Entity Inspector. However, since prefabs work with DOM data and the RPE uses data generated using object pointers, the integration proved to be very difficult and complicated. With the introduction of DPE, which uses DOM data too, the systems are better placed to handle the integration now. At the time of writing this document, the work to make the CVar editor and AssetEditor use DPE is in its final stages. There has been some work also done to use make the entity inspector use DPE as the framework. This design proposes building on top of that work to add override viewing and managing support to the DPE based entity inspector. So, there is a hard dependency on the Entity Inspector to use DPE instead of RPE as its property editors in order to launch the features mentioned in this document.
At a high level, these are the changes that this design proposes:
This work is important because right now, users don't have visual indications in the Entity Inspector to see whether a property is overridden or not. They can only know that something is overridden by switching between the component editors of the default component and the overridden component. Even if they can find what's overridden in this manner, they cannot revert an individual property override on a component. Then can only revert all the overrides on an entity through the outliner, which is not the desired workflow always.
Once completed, this work will
There are multiple pieces that need to be finished in order to achieve prefab override management through the Entity Inspector. The major ones will be discussed here:
Entities in a prefab instance object are stored in an unordered map of entity alias(string) to entity objects . Given an entity alias string in a DOM, it's easy to map to the actual entity object in a prefab. Just like entity aliases, the JSON serializer also creates component aliases (strings) for each component. The system-generated component aliases look like 'Component_[17501195180351523199]' but can be changed at any time by the user. Unlike the entity aliases, given a component alias string in a DOM, there is no way to identify which component object it maps to. This mapping will be crucial in order to apply patches to an individual component within a prefab DOM. To bridge this gap, this design proposes injecting the serialized component alias string into the AzToolsFramework::Components::EditorComponentBase class, which is the base class for all editor components in O3DE. This injection will happen in the JsonEntitySerializer class that loads entities from DOM values. During the time of prefab patch generation we will be extracting the alias from the component and use it to construct a DOM path to be placed in the patch. Eg: .../PathToEntity/Components/SerializedComponentAliasString/AnyPropertyPath . The reason for injecting this alias into EditorComponentBase class instead of AZ::Component class is because prefab patch calculation only needs to happen at editor time and doesn't have any value at runtime and we can avoid allocating 1 string's worth of memory per component at runtime.
This does increase the editor memory usage but it is not a significant number for even a large level (a few MB). For a level with10k entities with 10 components each, the total number of editor components would be 100k. So the increase in editor memory usage would be 100k * size of AZStd::string. Based on whether a stateless allocator is used or not, the size for a string varies from 24-32 Bytes. So, we would be looking at a 2-3 MB increase in editor memory usage.
EditorComponentBase class additions
class AzToolsFramework::Components::EditorComponentBase // This is an existing class
{
public:
.... // Existing functions and members
void SetAlias(const AZStd::string& alias);
AZStd::string GetAlias();
private:
AZStd::string m_alias;
}
The key to indicating which properties are overridden is to create a custom handler for the override visualization. The MVP use case for override visualization is that an override icon will be added next to properties that are overridden. In order to support that, the proposal is to create a new 'OverrideIcon' node and register it with the PropertyEditorSystemInterface so that it is recognized as a valid node type in the DPE DOM. The node should also be accompanied with a handler class that inherits from the AzToolsFramework::PropertyHandlerWidget class. This handler class will be responsible for showing the override icon when an 'OverrideIcon' node is encountered in the DPE Dom.
Here is a rough image of what the override icon would look like next to overridden properties. In the image below, the EditorCommentComponent's 'configuration' field is modified and stored as an overrride, hence the blue icon (Ignore the weird spacing. It's a Qt setting that needs to be fixed in the prototype)
Once we have the OverrideIconHandler, adding a revert option is going to be easy because the handler inherits from the PropertyHandlerWidget, which at the end of the day is a QWidget. So, the widget's behavior will be extended to show a custom context menu using Qt 'customContextMenuRequested' signal. When the 'revert' option is clicked from the custom context menu, the call needs to be sent to AzToolsFramework:Prefab:PrefabOverridePublicInterface so that the prefab system can revert the override on the specified property of the component within the selected entity.
Here is a rough image of how that override menu option would look like (taken from the prototype):
The exact behavior of where and when the override icon will appear for different types of property hierarchies is subject to a UX review. You can find some of these different hierarchies in the 'Open Questions' section and the proposed places of where the override icons should show up. The rule of thumb for where the icon should show up that will be followed for initial development is 'Show it next to the row it appears in the entity inspector. If that is not possible, show it next to the 'group' it belongs to if any. If neither of them is possible, show it next to the component'. These type of UX decisions will be however discussed with sig-ux to reach a consensus on what's the best user experience for the editor user.
PropertyEditorNodes class
namespace AZ::DocumentPropertyEditor::Nodes // Existing namespace
{
.
.
. // Existing node types
struct OverrideIcon : PropertyEditorDefinition
{
static constexpr AZStd::string_view Name = "OverrideIcon";
};
}
OverrideIconHandler class
// New class
class AzToolsFramework::OverrideIconHandler
: public PropertyHandlerWidget<QToolButton>
{
public:
// Connect the customContextMenuRequested signal to the showContextMenu here.
OverrideIconHandler();
// This will be the function responsible for adding the actual icon in the inspector. Input is the OverrideIcon node in the DPE DOM
void SetValueFromDom(const AZ::Dom::Value& node);
static constexpr const AZStd::string_view GetHandlerName()
{
return AZ::DocumentPropertyEditor::Nodes::OverrideIcon::Name;
}
void showContextMenu(const QPoint&);
};
DPEComponentAdapter class additions
AZ::DocumentPropertyEditor::ComponentAdapter //Existing class
{
public:
// Checks whether an override exists on the prefab at the provided serializedPath and if it does, adds an OverrideIcon node into the DPE DOM
// Adds the entity and component aliases to the serializedPath so that the path to the component/property from the prefab correctly constructed.
void AddIconIfPropertyOverride(AdapterBuilder* adapterBuilder, const AZStd::string_view& serializedPath);
protected:
AZStd::string m_entityAlias;
AZStd::string m_componentAlias;
AZ::EntityId m_entityId;
}
There are multiple ways to generate a valid prefab patch, one of them being serializing the entities to JSON and comparing that JSON to the data stored in the in-memory prefab templates. This is what is currently being done in the editor to calculate prefab patches when a component property changes. However, DPE has its own patch generation mechanism that generates AZ:Dom:Patch based on the AZ:Dom data stored in DPE DOM. Since DPE generates patches at a more local level (individual component), the prefab system needn't serialize the entire entity just to know which components are modified. That information can be fetched from the DPE and a valid Prefab patch can be calculated from it.
The responsibility of creating the prefab patches and calling appropriate prefab APIs to accept those patches can all be delegated to a prefab adapter that listens to events from the ComponentAdapter.
Generate Prefab Patches
In order for the ComponentAdapter to be able to listen to property edits in the editor, the design proposes to add a handler to the ReflectionAdapter:PropertyChangeEvent as a class member and connect it to the event. When the event gets triggered, the ComponentAdapter class will generate a valid prefab patch. The generation of the prefab patch is a responsibility that can be delegated to a prefab adapter in the future.
DPEComponentAdapter additions for prefab patch generation
AZ::DocumentPropertyEditor::ComponentAdapter //Existing class
{
protected:
// This is the function that'll generate a valid prefab patch when a property is edited.
void GeneratePropertyEditPatch(const ReflectionAdapter::PropertyChangeInfo& propertyChangeInfo);
// This is the handler that'll listen to property change events and call the above function to generate a valid prefab patch.
// This handler will be connected to listen to the events during the class construction
ReflectionAdapter::PropertyChangeEvent::Handler m_propertyChangeHandler;
}
In order for the prefab system to easily ingest the AZ:Dom:Value the DPE produces, opaque types supported by AZ:Dom:Value add a slight wrinkle. An opaque type in AZ:Dom:Value is basically a shared pointer to an AZStd::any. It is used to store values in AZ:Dom:Value that can't be mapped to primitive types likes ints and strings(eg. AZ::Vector3). The data stored in those opaque values is not in the desired format for the prefab system as it expects a patch in jsonPatch format. So, the opaque values need to be serialized to produce JSON so that it can be put in a jsonPatch. This serialization to JSON will be done in the ReflectionAdapter while generating AZ:Dom from the instance pointers if a 'SerializedPath' attribute (AZ::Reflection::DescriptorAttributes::SerializedPath) can be found in the reflected data. If a serialized path attribute is not found (eg. ui related properties that doesn't affect prefab data), we will continue to store them as opaque types. Since the modification of such properties won't affect the prefab data in any manner, those opaque types will not cause an issue with prefab patch generation. By doing this, not only will we be making the process of generating prefab patches easier but we will also unblock other features like multi-editing in the DPE, where the changes to opaque values won't always be detected (see https://github.com/o3de/o3de/pull/12130#discussion_r977081325).
The challenging part of the design mentioned in this document would be to make the DPE send the correct patches/overrides to the prefab system. This alternative solution sidesteps the problem by making the prefab system calculate its own patches and ignoring the DPE patches, which is what is currently being done in the editor.
Behind the feature flag, modifying a component property correctly generates a prefab override in development right now. The way this works is because the undo system marks entities as dirty and uses that to correctly generate the prefab patches by serializing the entire entity. It's not efficient but it does the job of generating prefab patches and propagating them. With this alternate solution, the dpe backed inspector will only be used for reading and deleting overrides whereas the create and update will be handled by prefabs system itself.
The proposed solution is preferred because this alternative only achieves partial integration of prefabs with the DPE architecture. In order to take full advantage of the DPE's AZ:Dom and AZ:Dom:Patch formats and use them to replace rapidjson and jsonPatch in prefabs, it is better to spend time now to achieve a better integration now so that they are well placed for these future efforts.
One way to avoid serializing the entire entity just for a property change is to inspect the az:dom:patch that the DPE already produces and use that to generate a prefab json patch. This is what is proposed in the document above. But an alternative approach to this would be to serialize opaque values on an as-needed basis. During the inspection of DPE patches, if we encounter opaque values, we can then serialize them to generate valid json patches.
The same reason as not choosing the above alternative. In addition to that, this solution would also require a lot of redundant and repeated serializations of the same property for every single property edit. While we may find a caching solution to prevent that from happening, the complexity and maintenance requirements of such a cached solution can be avoided if we go with a better integration which is also simpler to maintain and expand over time.
MVP:
The work listed below is sufficient to be able to perform CRUD operations on overrides through the entity inspector. For the creation/updating part, we will be sidestepping the DPE and doing it ourselves. This is what is currently being done in development branch as well. This is the easier part of the design and relatively straightforward.
Next phase:
Once the MVP has been delivered, we can optimize the systems further and tackle the complex integration parts.
EnableOverridesUx
{
"Amazon": {
"Preferences": {
"EnablePrefabSystem": true
}
},
"O3DE": {
"Preferences": {
"Prefabs": {
"EnableOverridesUx": true
}
}
}
}