Component Reflect custom attributes?

So I’m dealing with a library that when you load a file with it, it has a list of parameters that are separated as followed: id, name, min, max, value.

These parameters are dynamic in size depending on the file, i.e. one file can have 2 parameters and another can have 10.

What i am wondering is how I can add in an attribute list to the editor through the component’s reflect system to have a list of these parameters with a name of the parameter and a ui slider for it.

I.E.
[NAME][0.0][-----|----]

or something similar to this.

Up. Very interesting subject indeed.

Hey @alatnet, I’ve filed a ticket to get you an answer for this.

Well dang, sorry that answer didn’t. I’ve updated the ticket.

So to get that correct:
You are asking how you can reflect variables from your component?

Currently you are trying to write an adapter or interface for your library and want to have some variables in the editor over a component to do something with the libraries interface.

So I am not sure if you are asking for this:
http://docs.aws.amazon.com/lumberyard/latest/developerguide/component-entity-system-reflect-component.html

Where that part would be particularly interesting for you:

    /*static*/ 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.
serialize->Class<MyComponent>()
->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.")
;
}
}
}

Nope, this is for a dynamic reflection.

In essence, reflect currently allows for static reflection where all the edit components are already defined. The only dynamic sections is where you use, i believe, vectors variables for the data element sections which allow for a range of a specific type of variable, i.e. integer, string, etc.

What I am trying to figure out is dynamic reflection to where you can have a vector of stucts or something similar to where you can define how the data element looks.

So for the example above:
[NAME][0.0][----|----]
broken down:
[NAME] -> string
[0.0] -> float
[----|----] -> UISlider

so what would be useful is an attribute to define how a data element looks like.

maybe like:
->Attribute(AZ::Edit::DataView, “string, float, UISlider”);

and to use it, the data element variable would need to pass an array to display it’s variables or receive an array to change it’s variables.
auto[] GetElements();
void SetElements(auto[] elements);

this kind of system would allow for a wider range of options concerning editing.

Which would be find if it was a no parameter function.
But sadly, it’s has a ReflectContext parameter and that’s something that’s probably deep code in there for it to work and would be like pulling hairs to even get something like that working.

Thanks for the great answer @Herpaderp!

Not sure if you are able to reflect again during run time, you could try it easily though.

Your program could adapt between pre-defined (hard coded) reflections, so if your interface underneath changes, call reflect again, and inside the reflect function you have have a switch statement which reflects differently depending on which state your library interface has currently, if that makes sense?

I am afraid, that sounds quite impossible out of the box. I thought about it again and I think even calling reflect during RT would not work.

But does it need to be a component?
You could write an editor extension that controls the library.

So, I’ve been looking into the EditorContext.h file and found “SetDynamicEditDataProvider”.
Is there any references that use that. I dont know if it’s something that can be used for what i was thinking of.

EDIT: The script editor component for attaching script properties to the editor!
This might be it!

EDIT2: The more i read ScriptEditorComponent, ScriptComponent, and ScriptProperty, the less i understand it… @.@

EDIT3: so… from my understanding, you define each dynamic type with a Class and a ClassElement(EditorData). Then using the DynamicEditDataProvider, it looks up from a reflected list of dynamic entries and provides it’s ElementData of it, which is how the dynamic type should look like, it’s name, description, and the class element to reflect the data from.

The entirety of it is programmed as a component for usage in LyShine.
Dont think i can migrate it to an individual editor extension if it is bound to LyShine pretty tightly…

Ok, so here’s how i did it:
Header:


#pragma once
#include <AzCore/Component/Component.h>
#include <AzCore/Serialization/SerializeContext.h>
class Parameter {
public:
AZ_RTTI(Parameter, "<UUID>");
public:
virtual ~Parameter() {}
AZStd::string name;
float val;
public: //editor stuff
AZ::Edit::ElementData ed;
void InitEdit();
public: //RTTI stuff
static void Reflect(AZ::SerializeContext* serializeContext);
};
class ParameterGroup {
public:
AZ_TYPE_INFO(ParameterGroup, "<UUID>");
public:
AZStd::string m_name;
AZStd::vector<Parameter*> m_params;
void Clear();
ParameterGroup() : m_name("Parameters") {}
~ParameterGroup() { Clear(); }
// Disallow copying, only moving
ParameterGroup(const ParameterGroup& rhs) = delete;
ParameterGroup& operator=(ParameterGroup&) = delete;
ParameterGroup(ParameterGroup&& rhs) { *this = AZStd::move(rhs); }
ParameterGroup& operator=(ParameterGroup&& rhs);
public:
static void Reflect(AZ::SerializeContext* serializeContext);
};
class ParameterComponent
: public AZ::Component
{
public:
AZ_COMPONENT(ParameterComponent, "<UUID>", AZ::Component)
public:
ParameterComponent();
~ParameterComponent() override;
protected: // member functions
// AZ::Component
void Init() override;
void Activate() override;
void Deactivate() override;
// ~AZ::Component
public: // static member functions
static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) {
(void)provided
}
static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) {
(void)incompatible
}
static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required) {
(void)required
}
static void Reflect(AZ::ReflectContext* context);
private: //dynamic editor listing
static const AZ::Edit::ElementData* GetEditData(const void* handlerPtr, const void* elementPtr, const AZ::Uuid& elementType);
const AZ::Edit::ElementData* GetDataElement(const void* element, const AZ::Uuid& typeUuid) const;
struct ElementInfo {
AZ::Uuid m_uuid; // Type uuid for the class field that should use this edit data.
AZ::Edit::ElementData m_editData; // Edit metadata (name, description, attribs, etc).
};
AZStd::unordered_map<const void*, ElementInfo> m_dataElements;
private:
void LoadParams();
void FreeParams();
private:
ParameterGroup group;
};

Source:


#include "StdAfx.h"
#include <AzCore/Math/Crc.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/Component/ComponentApplicationBus.h>
#include "DynamicAttributes.h"
ParameterComponent::ParameterComponent() {
}
ParameterComponent::~ParameterComponent() {
}
void ParameterComponent::Init() {
}
void ParameterComponent::Activate() {
}
void ParameterComponent::Deactivate() {
}
void ParameterComponent::Reflect(AZ::ReflectContext* context) {
AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
if (serializeContext) {
Parameter::Reflect(serializeContext);
ParameterGroup::Reflect(serializeContext);
serializeContext->Class<ParameterComponent, AZ::Component>()
->Version(1)
->Field("Params", &ParameterComponent::params)
AZ::EditContext* ec = serializeContext->GetEditContext();
if (ec) {
//basic editor data
auto editInfo = ec->Class<ParameterComponent>("ParameterComponent", "Example of how to list custom parameters");
editInfo->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::Icon, "Editor/Icons/Components/CharacterPhysics.png")
->Attribute(AZ::Edit::Attributes::ViewportIcon, "Editor/Icons/Components/Viewport/CharacterPhysics.png")
->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("UI", 0x27ff46b0))
->Attribute(AZ::Edit::Attributes::AutoExpand, true);
//set the dynamic edit data provider
editInfo->SetDynamicEditDataProvider(&ParameterComponent::GetEditData);
//parameters group
{
editInfo->DataElement(0, &ParameterComponent::params, "Parameters", "List of parameters.");
ec->Class<ParameterGroup>("Parameter Group", "The group of parameters")
->ClassElement(AZ::Edit::ClassElements::EditorData, "ParameterGroup's class attributes.")
->Attribute(AZ::Edit::Attributes::NameLabelOverride, &ParameterGroup::m_name)
->Attribute(AZ::Edit::Attributes::AutoExpand, true)
->DataElement(0, &ParameterGroup::m_params, "m_params", "Parameters in this property group")
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly);
ec->Class<Parameter>("Parameter", "A Parameter")
->ClassElement(AZ::Edit::ClassElements::EditorData, "Parameter Attribute.")
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
->DataElement(AZ::Edit::UIHandlers::Slider, &Parameter::val, "val", "A float");
}
}
}
}
const AZ::Edit::ElementData* ParameterComponent::GetEditData(const void* handlerPtr, const void* elementPtr, const AZ::Uuid& elementType) {
const ParameterComponent* owner = reinterpret_cast<const ParameterComponent*>(handlerPtr);
return owner->GetDataElement(elementPtr, elementType);
}
const AZ::Edit::ElementData* ParameterComponent::GetDataElement(const void* element, const AZ::Uuid& typeUuid) const {
auto it = m_dataElements.find(element);
if (it != m_dataElements.end()) {
if (it->second.m_uuid == typeUuid) {
return &it->second.m_editData;
}
}
return nullptr;
}
void ParameterComponent::LoadParams() {
for (int i = 0; i < 10; i++) {
Parameter * p = new Parameter;
p->name = AZStd::string(itoa(i));
p->val = 1.0f;
this->params.m_params.push_back(p);
//editor data
p->InitEdit();
ElementInfo ei;
ei.m_editData = p->ed;
ei.m_uuid = AZ::SerializeTypeInfo<float>::GetUuid();
this->m_dataElements.insert(AZStd::make_pair(&p->val, ei));
}
this->group.m_params.shrink_to_fit(); //free up unused memory
}
void ParameterComponent::FreeParams() {
this->group.Clear();
this->m_dataElements.clear(); //clear all editor data
}
void Parameter::InitEdit() {
this->ed.m_elementId = AZ::Edit::UIHandlers::SpinBox;
this->ed.m_name = this->name.c_str();
this->ed.m_description = this->name.c_str();
}
void Parameter::Reflect(AZ::SerializeContext* serializeContext) {
serializeContext->Class<Parameter>()->
Version(1)
->Field("name", &Parameter::name)
->Field("value", &Parameter::val);
}
void ParameterGroup::Clear() {
if (this->m_params.size() != 0) {
this->m_params.clear(); //clear the parameters vector
}
}
void ParameterGroup::Reflect(AZ::SerializeContext* serializeContext) {
serializeContext->Class<ParameterGroup>()
->Field("Name", &ParameterGroup::m_name)
->Field("Params", &ParameterGroup::m_params);
}

I F**KING DID IT!

That was the right way of doing it!

Freaking hell yea!

The important thing you need to to is that after you modify the m_dataElements, you need to refresh the entire tree.

You can either refresh the tree via ebuses, ala “EBUS_EVENT(ToolsApplicationEvents::Bus, InvalidatePropertyDisplay, Refresh_EntireTree);” or by having “->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ::Edit::PropertyRefreshLevels::EntireTree);” for one of the “DataElement”.

The only drawback that I seem to have found is that for LyShine, any data changed in the dynamic list does not seem to work with the UI Animation system.

To see more about this or where I figure this out from, check in “AzToolsFramework/ToolsComponents/ScriptEditorComponent”

Please can you post a sample source of the correct answer?

uh… i did… 2 days ago…