Closed srikappa-amzn closed 1 year ago
The approach outlined here looks overall quite solid! I'd suggest ideating a bit more on the testing story for this - right now manual testing is called out but a lot of this work can be covered quite well by unit / integration tests and I think it would be extremely beneficial to scope testing the adapters and their interaction with the prefab system; I personally focused on fast iteration on the ReflectionAdapter
and let test coverage slide to rapidly iterate there and I'd nominate that as my biggest misstep in my initial DPE work. I have a few thoughts on some details, in no particular order.
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.
I'd suggest moving the business logic for this into the adapter layer, it'll make testing easier (no Qt required) and we could probably use a general purpose context menu thing for the DPE. Maybe something like:
// An entry in a menu, as described by an adapter
struct MenuEntry
{
bool m_enabled;
bbol m_visible;
AZStd::string_view m_text;
Dom::Value SerializeToDom() const;
static MenuEntry DeserializeFromDom(const AZ::Dom::Value& value);
};
// An attribute that contains the context of a menu
class MenuAttributeDefinition final : public AttributeDefinition<AZStd::vector<ContextMenuEntry>>
{
public:
explicit constexpr TypeIdAttributeDefinition(AZStd::string_view name);
Dom::Value ValueToDom(const AZ::TypeId& attribute) const override;
AZStd::optional<AZStd::vector<MenuEntry>> DomToValue(const Dom::Value& value) const override;
};
// ... inside struct PropertyEditor in PropertyEditorNodes
//! The contents of this editor's context menu, if any
static constexpr auto ContextMenu = MenuAttributeDefinition("ContextMenu");
// Callback that gets invoked when a menu entry gets selected
static constexpr auto OnContextMenuInvoked = CallbackAttribute<void(size_t)>("OnContextMenuInvoked");
Then the adapter can be the authority about the override logic and unit tests can trigger menu actions directly from the adapter.
Reusing DPE patches instead of making the prefab system calculate them again
The approach actually outlined here looks solid to me, but I'd frame this more as "generating Prefab patches from the DPE adapter" than "reusing DPE patches" since there's two fully orthogonal patching mechanisms at work here:
The DPE Adapter patch system is for the adapters (models) to let the DPE (view) know that their contents have changed. This is typically triggered by a user interaction with a property editor widget, but it's dispatched by the adapter, and there are scenarios where it doesn't map back from user intent (e.g. for two inspectors pointing at the same component, the second inspector should emit patches when it sees the value updated from underneath it, even though it authored no change).
The Prefab patch system is for ingesting user-authored changes into the prefab system - these would be convenient (and likely much more performant!) to author at the same time that DPE patches are authored, because a user entered a number into a text box or whatever, but they'd be tied to the actual user intent (a DPE editor firing OnChanged
to its adapter) rather than by the DPE's patches (which are just a heads up to the view that may be a side effect of this).
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
I don't have a strong opinion on this, but I think it would simplify the handler logic if we just prescriptively said "all DPE values shall be JSON-format serialized" so that handlers never need to worry about opaque types - this is a minority of properties in most DPEs, so I wouldn't expect performance to be all that relevant vs the benefits of narrowing the testing matrix of possible acceptable DPE values. Bias on the table, I spent a lot of time fighting opaque types when optimizing the DPE (hence the bespoke pointer marshal path etc) so I'm very happy to see a directional phase out regardless.
Overall I'm extremely excited to see override DPE integration come to be and will be happy to follow progress here :)
PS- Out of scope for this RFC, but since it's related I'd like to solicit some feedback from folks working on prefabs and the DPE on the ergonomics of O3DE's usage of JSON patch. What I've personally found working with JSON patch is that it's a perfectly capable format for serializing and applying changes, but a relatively poor abstraction for systems introspecting those changes. The JSON patch is extremely permissive of destructive changes that can make it difficult to reason about side-effects in the strict context of a patch.
In a prefab, you could replace the contents of an entire entity or (in theory) do really weird stuff like move a property's value into another entity - any reasonable UX that handles this is probably just going to have to say "uh, I bail, this is not an override the UI can handle." Which is... probably fine?
The DPE arguably has it worse, because the UI has to update itself based on patches and those can be wild things like "this SpinBox became a new Row that contains 10 color pickers, all you get is a Replace patch, figure it out." It makes me think that there might be some merit in looking into a more bespoke command-based patching system that actually maps to intents that our systems pretty well, like for prefabs you might have "add entity" / "remove entity" / "override property" and for the DPE you might have "add row" / "remove rows" / "update row properties" - I at least wonder if that wouldn't make some of the patch introspection a lot easier without a significant loss of power.
Thanks for taking a look @NicholasVanSickle. Overall, I agree with the suggestions you've made above. I'll address them below in the order they appear:
This RFC is now in the final comment period. Please submit any remaining feedback by Tuesday, February 14.
Mitigation strategy: A lot of user testing needs to be done. We can reach out to individual teams within O3DE, ask them what are the common type of components they mostly interact with and work with them to create custom test strategies. This approach worked in the past when we migrated all the slices in AutomatedTesting project to use prefabs instead.
It mentioned user testing. Do we also have a testing plan (unit tests + automated tests) for the new features and use cases introduced by this RFC?
EDIT: Oh nvm. I just noticed it was discussed in the comments above.
Closing as accepted
Summary
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:
What is the relevance of this feature?
Why is this important?
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.
What are the use cases?
What will it do once completed?
Once completed, this work will
Glossary:
Feature and Technical Design Description
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:
Injecting serialized component alias string into AzToolsFramework::Components::EditorComponentBase objects
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.
API changes
EditorComponentBase class additions
Identifying and reverting overridden properties/components
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.
API Changes/Additions
PropertyEditorNodes class
OverrideIconHandler class
DPEComponentAdapter class additions
Generating prefab patches from DPE patches instead of serializing entities to generate patches
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
API Changes
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
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).
Are there any alternatives to this feature?
Alternative 1 : Keep calculating patches on the entire entity for a single property edit
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.
Pros:
Cons:
Why the proposed solution is preferred to this alternative?
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.
Alternative 2 : Keep storing opaque types in the DPE DOM and only serialize them during the construction of prefab patch
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.
Pros:
Cons:
Why the proposed solution is preferred to this alternative?
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.
What are the advantages of the feature?
What are the disadvantages of the feature?
What are the risks associated with this feature?
How will this be implemented or integrated into the O3DE environment?
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.
How will users learn this feature?
Are there any open questions?
EnableOverridesUx