Menu
Lumberyard
Developer Guide (Version 1.11)

Reflecting a Component for Serialization and Editing

Components use AZ reflection to describe the data they serialize and how content creators interact with them.

The following example reflects a component for serialization and editing:

Copy
class MyComponent : public AZ::Component { // ... AZ_COMPONENT, Activate(), Deactivate(), etc, ... static void Reflect(AZ::ReflectContext* context); enum class SomeEnum { EnumValue1, EnumValue2, } float m_someFloatField; AZStd::string m_someStringField; SomeEnum m_someEnumField; AZStd::vector<SomeClassThatSomeoneHasReflected> m_things; int m_runtimeStateNoSerialize; } void MyComponent::Reflect(AZ::ReflectContext* context) { AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context); if (serialize) { // Reflect the class fields that you want to serialize. // In this example, m_runtimeStateNoSerialize is not reflected for serialization. // Base classes with serialized data should be listed as additional template // arguments to the Class< T, ... >() function. serialize->Class<MyComponent, AZ::Component>() ->Version(1) ->Field("SomeFloat", &MyComponent::m_someFloatField) ->Field("SomeString", &MyComponent::m_someStringField) ->Field("Things", &MyComponent::m_things) ->Field("SomeEnum", &MyComponent::m_someEnumField) ; AZ::EditContext* edit = serialize->GetEditContext(); if (edit) { edit->Class<MyComponent>("My Component", "The World's Most Clever Component") ->DataElement(AZ::Edit::DefaultHandler, &MyComponent::m_someFloatField, "Some Float", "This is a float that means X.") ->DataElement(AZ::Edit::DefaultHandler, &MyComponent::m_someStringField, "Some String", "This is a string that means Y.") ->DataElement("ComboBox", &MyComponent::m_someEnumField, "Choose an Enum", "Pick an option among a set of enum values.") ->EnumAttribute(MyComponent::SomeEnum::EnumValue1, "Value 1") ->EnumAttribute(MyComponent::SomeEnum::EnumValue2, "Value 2") ->DataElement(AZ::Edit::DefaultHandler, &MyComponent::m_things, "Bunch of Things", "A list of things for doing Z.") ; } } }

The preceding example adds five data members to MyComponent. The first four data members will be serialized. The last data member will not be serialized because it contains only the run-time state. This is typical; components commonly contain some members that are serialized and others that are not.

It is common for fields to be reflected for serialization, but not for editing, when using advanced reflection features such as change callbacks. In these cases, components may conduct complex internal calculations based on user property changes. The result of these calculations must be serialized but not exposed for editing. In such a case, you reflect the field to SerializeContext but do not add an entry in EditContext. An example follows:

Copy
serialize->Class<MyComponent>() ->Version(1) ... ->Field("SomeFloat", &MyComponent::m_someFloatField) ->Field("MoreData", &MyComponent::m_moreData) ... ; ... AZ::EditContext* edit = serialize->GetEditContext(); if (edit) { edit->Class<MyComponent>("My Component", "The World's Most Clever Component") ->DataElement(AZ::Edit::DefaultHandler, &MyComponent::m_someFloatField, "Some Float", "This is a float that means X.") ->EnumAttribute("ChangeNotify", &MyComponent::CalculateMoreData) // m_moreData is not reflected for editing directly. ; }

Lumberyard has reflection contexts for different purposes, including the following:

  • SerializeContext – Contains reflection data for serialization and construction of objects.

  • EditContext – Contains reflection data for visual editing of objects.

  • BehaviorContext – Contains reflection for run-time manipulation of objects from Lua, flow graph, or other external sources.

  • NetworkContext – Contains reflection for networking purposes, including marshaling, quantization, and extrapolation.

Note

This guide covers only SerializeContext and EditContext.

All of Lumberyard's reflection API operations are designed to be simple, human readable, and human writable, with no forced dependency on code generation.

A component's Reflect() function is invoked automatically for all relevant contexts.

The following code dynamically casts the anonymous context provided to a serialize context, which is how components discern the type of context that Reflect() is being called for.

Copy
AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context);

Serialization

Reflecting a class for serialization involves a builder pattern style markup in C++, as follows:

Copy
serialize->Class<TestAsset>() ->Version(1) ->Field("SomeFloat", &MyComponent::m_someFloatField) ->Field("SomeString", &MyComponent::m_someStringField) ->Field("Things", &MyComponent::m_things) ->Field("SomeEnum", &MyComponent::m_someEnumField) ;

The example specifies that m_someFloatField, m_someStringField, m_things, and m_someEnumField should all be serialized with the component. Field names must be unique and are not user facing.

Tip

We recommend that you keep field names simple for future proofing. If your component undergoes significant changes and you want to write a data converter to maintain backward data compatibility, you must reference the field names directly.

The preceding example reflects two primitive types—a float, and a string—as well as a container (vector) of some structure. AZ reflection, serialization, and editing natively support a wide variety of types:

  • Primitive types, including integers (signed and unsigned, all sizes), floats, and strings

  • Enums

  • AZStd containers (flat and associative), including AZStd::vector, AZStd::list, AZStd::map, AZStd::unordered_map, AZStd::set, AZStd::unordered_set, AZStd:pair, AZStd::bitset, AZStd::array, fixed C-style arrays, and others.

  • Pointers, including AZStd::smart_ptr, AZStd::intrusive_ptr, and raw native pointers.

  • Any class or structure that has also been reflected.

Note

The example omits the reflection code for SomeClassThatSomeoneHasReflected. However, you need only reflect the class. After that, you can freely reflect members or containers of that class in other classes.

For C++ API reference documentation on the serialize context, see the SerializeContext Class Reference in the Amazon Lumberyard C++ API Reference.

Editing

When you run Lumberyard tools such as Lumberyard Editor, an EditContext and a SerializeContext are provided. You can use the robust facilities in these contexts to expose your fields to content creators.

The following code demonstrates basic edit context reflection:

Copy
AZ::EditContext* edit = serialize->GetEditContext(); if (edit) { edit->Class<TestAsset>("My Component", "The World's Most Clever Component") ->DataElement(AZ::Edit::DefaultHandler, &MyComponent::m_someFloatField, "Some Float", "This is a float that means X.") ->DataElement(AZ::Edit::DefaultHandler, &MyComponent::m_someStringField, "Some String", "This is a string that means Y.") ->DataElement("ComboBox", &MyComponent::m_someEnumField, "Choose an Enum", "Pick an option among a set of enum values.") ->EnumAttribute(MyComponent::SomeEnum::EnumValue1, "Value 1") ->EnumAttribute(MyComponent::SomeEnum::EnumValue2, "Value 2") ->DataElement(AZ::Edit::DefaultHandler, &MyComponent::m_things, "Bunch of Things", "A list of things for doing Z.") ; }

Although this example demonstrates the simplest usage, many features and options are available when you reflect structures (including components) to the edit context. For the fields to be exposed directly to content creators, the example provides a friendly name and a description (tooltip) as the third and fourth parameters of DataElement. For three fields, the first parameter of DataElement is the default UI handler AZ::Edit::DefaultHandler. The property system's architecture supports the ability to add any number of UI handlers, each valid for one or more field types. A given type can have multiple available handlers, with one handler designated as the default. For example, floats by default use the SpinBox handler, but a Slider handler is also available.

An example of binding a float to a slider follows:

Copy
->DataElement("Slider", &MyComponent::m_someFloatField, "Some Float", "This is a float that means X.") ->Attribute("Min", 0.f) ->Attribute("Max", 10.f) ->Attribute("Step", 0.1f)

The Slider UI handler expects Min and Max attributes. Optionally, a value for Step may also be provided. The example provides incremental increases of 0.1. If no Step value is provided, a default stepping of 1.0 is used.

Note

The property system supports external UI handlers, so you can implement your own UI handlers in your own modules. You can customize the behavior of the field, the Qt control that it uses, and the attributes that it observes.

For C++ API reference documentation on the edit context, see the EditContext Class Reference in the Amazon Lumberyard C++ API Reference.

Attributes

The example also demonstrates the use of attributes. Attributes are a generic construct on the edit context that allows the binding of literals, or functions that return values, to a named attribute. UI handlers can retrieve this data and use it to drive their functionality.

Attribute values can be bound to the following:

Literal values

Attribute("Min", 0.f)

Static or global variables

Attribute("Min", &g_globalMin)

Member variables

Attribute("Min", &MyComponent::m_min)

Static or global functions

Attribute(AZ::Edit::Attributes::ChangeNotify, &SomeGlobalFunction)

Member functions

Attribute(AZ::Edit::Attributes::ChangeNotify, &MyComponent::SomeMemberFunction)

Change Notification Callbacks

Another commonly used feature of the edit context is its ability to bind a change notification callback:

Copy
->DataElement(AZ::Edit::DefaultHandler, &MyComponent::m_someStringField, "Some String", "This is a string that means Y.") ->Attribute("ChangeNotify", &MyComponent::OnStringFieldChanged)

The example binds a member function to be invoked when this property is changed, which allows the component to conduct other logic. The ChangeNotify attribute also looks for an optional returned value that tells the property system if it needs to refresh aspects of its state. For example, if your change callback modifies other internal data that affects the property system, you can request a value refresh. If your callback modifies data that requires attributes be reevaluated (and any bound functions be reinvoked), you can request a refresh of attributes and values. Finally, if your callback conducts work that requires a full refresh (this is not typical), you can refresh the entire state.

The following example causes the property grid to refresh values when m_someStringField is modified through the property grid. RefreshValues signals the property grid to update the GUI with changes to the underlying data.

Copy
->DataElement(AZ::Edit::DefaultHandler, &MyComponent::m_someStringField, "Some String", "This is a string that means Y.") ->Attribute("ChangeNotify", &MyComponent::OnStringFieldChanged) ... AZ::u32 MyComponent::OnStringFieldChanged() { m_someFloatField = 10.0f; // We've internally changed displayed data, so tell the property grid to refresh values (cheap). return AZ_CRC("RefreshValues"); }

RefreshValues is one of three refresh modes that you can use:

  • RefreshValues – Refreshes only values. The property grid updates the GUI to reflect changes to underlying data that may have occurred in the change callback.

  • RefreshAttributesAndValues – Refreshes values but also reevaluates attributes. Since attributes can be bound to data members, member functions, global functions, or static variables, it's sometimes necessary to ask the property grid to reevaluate them. Doing so might include reinvoking bound functions.

  • RefreshAll – Completely reevaluates the property grid. This is seldom needed, as RefreshAttributesAndValues should cover all requirements for rich dynamic editor reflection.

The following more complex example binds a list of strings as options for a combo box. The list of strings is attached to a string field Property A. Suppose you want to modify the options available in the combo box for Property A with the values from another Property B. In that case you can bind the combo box StringList attribute to a member function that computes and returns the list of options. In the ChangeNotify attribute for Property B, you tell the system to reevaluate attributes, which in turn reinvokes the function that computes the list of options.

Copy
... bool m_enableAdvancedOptions; AZStd::string m_useOption; ... ->DataElement(AZ::Edit::DefaultHandler, &MyComponent::m_enableAdvancedOptions, "Enable Advanced Options", "If set, advanced options will be shown.") ->Attribute("ChangeNotify", AZ_CRC("RefreshAttributesAndValues")) ->DataElement("ComboBox", &MyComponent::m_useOption, "Options", "Available options.") ->Attribute("StringList", &MyComponent::GetEnabledOptions) ... AZStd::vector<const char*> MyComponent::GetEnabledOptions() { AZStd::vector<const char*> options; options.reserve(16); options.push_back("Basic option"); options.push_back("Another basic option"); if (m_enableAdvancedOptions) { options.push_back("Advanced option"); options.push_back("Another advanced option"); } return options; }