Unity.Properties

Not sure where to feedback on the package, so I’m writing it here.

I came across this:
https://docs.unity3d.com/Packages/com.unity.properties@2.1/manual/index.html#getting-started

Architectural Feedback
This might be a contextual misunderstanding, so possibly a more real-world example might alleviate this. My view might be overly simplistic or idealistic.

However - I don’t see the advantage at all over inheritance or code generation, especially since it seems to actually USE code generation. (or used to?)

There appears to be an improvement in API from Properties 1.8.x to 2.1.x, but the fundamental questions remain unanswered, also in the documentation.

I don’t understand and can think of absolutely no scenario where I’d write the following:

var value = PropertyContainer.GetValue<MyContainer, int>(ref m_Container, m_PathToX);

Instead, I would naturally write:

var value = m_container.X;

X even is a property already, which can run arbitrary code on get and set. In these getters and setters is where code like PropertyContainer.GetValue<> should be invoked, and only in code-generated implementations of these.

I also believe what com.unity.properties does (according to the scant documentation) could probably be done with just OOP, and seems to be one of the core use cases for OOP (lazy binding / contract enforcement / encapsulation).

It’s one of the few things OOP is really, undoubtedly, good at.

com.unity.properties also seems to use reflection at runtime, which proper code generation/inheritance obviates; and keeps its data in PropertyBags, which seem incarnatons of the data clump antipattern, usually a code smell.

Alternative Industry Approaches
It feels like the example on Properties | Properties | 2.1.0-exp.7 gives the user the a hammer, but we’re forced to hold it at its head. (i.e. we’re looking at the back side of MyContainer’s API, and the normally front-facing side, i.e. C# properties, are facing the compiler/codegen, not the developer, so the developer has to take the code generator’s path to access the property.

As a good counterexample, I’d say the data binding and object weaving in MongoDB’s Realm (an ORM with a local synced native database, works nicely with Unity, btw.) gets it quite right (also for UI - some examples included here). https://www.mongodb.com/docs/realm/sdk/dotnet/model-data/data-binding/

The user (synonyms: coders, us, unity customers) really only ever has to work with raw C# APIs and a few, often optional, attributes. The Roslyn codegen does all the heavy lifting of binding to native and live query objects, etc.

Then, that can directly be bound to, for example, user interface frameworks like .NET MAUI to feed one into the other, with low to minimal boilerplate, and more importantly, minimal verbosity.

Blue Sky Example
I believe an ideal world example could look kind of like this:

partial struct XContainer //NB: absence of even [Serializable], use only if desired
{
   [CreateProperty(/*optionalpath*/)] public int X {get; set;}
}

//Just get and set X as you always would.
XContainer a = new {X=42};

//Generated code resolves the path, which you may override via optionalpath
//In the default case the path is derived from its name by codegen
//X declares its path, not an outside constant in another class.

In a data binding scenario, the user-facing boilerplate should be limited to the type declarations, property attributes, and single UXML elements.

//UXML declaration for the associate UI element
<IntegerField name="X" label="Answer" value="0" />

// All "X" of type int in all bound containers are bound to this IntegerField
// matching by longest common suffix. If you had a ParentContainer with
//    [CreateProperty]XContainer child{get;set;}
//    [CreateProperty]XContainer friend{get;set;}
// then "X" would match child.x and friend.x
// while "child.X" and "friend.X" would only match the respective fields.

…and in C#-Land…

//2nd container is a surprise tool that will help us later
partial struct XYContainer
{
   [CreateProperty] public int X {get; set;} = 69;
   [CreateProperty] public int Y {get; private set;} //can't be set by visitor (UI)
}

//Binding some datas now! Finally. Ohhh baby!
XYContainer ab = default;

//The Visitor would also be automatically generated, and used by the binder
rootVisualElement.Bind(ref a, ref ab, ...);
//UI shows 69, as bindings apply in param order
//Should you prefer to take the initial value from UXML instead,
//imagine the appropriate overloads.

//User types "123" in UI
Debug.Log($"{a.x}=={ab.x}"}); // >"123==123"

//Code path executes this somewhere:
ab.x = 9000+1;
Debug.Log($"{a.x}"}); // >"9001"
// use style/property on IntegerField in UXML to limit backpropagation if desired
// accessors and propagation always synchronous, rendering always asynchronous

Tooling Feedback
Something about recent Unity Tech - DOTS, SRP, and UIElements in particular - seems to be adding massively to boilerplate and statement verbosity/complexity with each release.

It has strayed far from the clean simplicity of class Game:MonoBehaviour{public int X;} that is still definitive for most of Unity.

I believe com.unity.properties should serialize Schemas as YAML (naturally the Unity serialization format), not JSON. JSON has practically only drawbacks over YAML, plus YAML is what 99% of the Unity ecosystem seems to use.

Thoough in actuality, instead of schema files, the Code should be the Schema, i.e. using a schema from an external source just turns compile time errors into runtime errors. Especially for fast-changing project features, such as User Interfaces, I’ll take a 100% always compile time error over a 5% occasional runtime error any time.

That’s all. :slight_smile:

Hi @Thygrrr !

The documentation of the experimental package is very barebone. We purposefully didn’t advertise this package while it was being worked on. Now that it has been moved as a Unity module, the proper documentation is being worked on and should be released soon.

The basic idea of Unity.Properties is to create a customizable property bag and be able to run generic algorithms on it through visitors. These property bags can be created by reflection or by code generation mostly through the use of attributes or they can be crafted manually. Obviously, the algorithms/visitors are manually written.

Some real world examples would be:

  • The serialization package can be used to save/load data at runtime in the JSON or binary format.
  • The experimental Properties.UI package can be used to generate UI hierarchies based on data and automatically bind to it (think PropertyField and InspectorElement). While this package is no longer being worked on, it could rather easily be ported to be usable at runtime as well, which would be very useful for generic debugging tools.
  • The runtime bindings feature in UI Toolkit is also built on top of it.

Aside from instrumenting the types with the [CreateProperty] attribute, most of these examples can be used without any knowledge of Unity.Properties. It’s a foundational layer and not something we expect users to know about or directly use.

In your code, you probably should never write this kind of code. If you know the container and you know the path, just use plain C# to do it. It’s more direct, more efficient and more understandable.

These APIs from the Unity.Properties module are meant for generic code of higher-level features. For example, we’re using this in the runtime binding feature to extract the value of a data source at a given path and then set the UI at a given path. We don’t know ahead of time what the data or UI types will be, so these can become useful in that scenario.

It uses reflection at runtime to generate the property bag of a type for convenience. We also offer a code generation approach so that it wouldn’t use reflection.

The property bags will keep the information about the properties of a type and won’t keep other data from the type. This is necessary to be able to visit the properties of a given instance. The same property bag is used for all instance of a given type. This is akin to Type, FieldInfo, PropertyInfo, etc.

Here, I think the confusion is that your counter examples are not at the same level as this library. Unity.Properties is the boiler plate that other higher level features are built on. We’re perfectly happy if users don’t know anything about Unity.Properties other than the attributes.

Our philosophy for these libraries is that we’re giving you access to the low-level code so that you could write your on higher-level features. Don’t like the way the serialization is handled in the serialization package mentioned above? That’s fine, you can create your own using the same underlying library. Still want to use JSON though? We’re giving you access to the low-level JSON parser and tokenizer. However, we expect 95% of the package users to use JsonSerialization.ToJson(myObject); instead or re-rolling their own solution.

I am probably missing something here. This is very similar to what we’ve done for the runtime bindings feature in UI Toolkit, though with different design choices.

With your own code generator, you could even achieve most of what you want by building on top of what we have.

I don’t know for the other features, but UI Toolkit is actually removing the boilerplate. In the latest tech stream, you don’t need to define a factory and a traits method anymore, you can use attribute instead and we’ll use code generation to do the boilerplate for you.

I think you are thinking of the com.unity.serialization package here. At the time, our mandate was to write a Json de-serializer where the parsing / tokenizing could be done in burst. A Yaml de-serializer could also be done on top of the Unity.Properties APIs and the good news is that, it can be done completely in user land too.

Hope this helps!

Thank you for the detailed reply. I can understand.

I will read up on the new Attributes thing you mentioned, I haven’t seen that yet.

I don’t yet understand what serialization has to do with it (i.e. what is the life cycle of the PropertyBags? Who usually uses them? When do they serialize and de-serialize?)

Regarding the Blue Sky binding example - I still believe the amount of boilerplate in UI Toolkit is extremely high, for instance on the data binding in Unity 2023 (I was honestly taken aback).

Comparing this to UGUI, where I wrote a single [Auto(…)] attribute that just binds serialized Component references from the hierarchy to my code, optionally using tags, paths, etc., in OnPostProcessScene, and that’s literally all the boilerplate I need)

I write a lot of custom inspectors as well as game UI, and would love to graduate to UI Elements for that - so far, it has always been significantly more work for a significantly thinner feature set compared to IMGUI and UGUI, respectively. (IMGUI for game UI is of course the worst, it’s quite broken by now - I treat it as fully Deprecated, no problem)

I’ll look into Roslyn and other code generation to just bind UXML, as well. Of course my use case is “needs to work for myself and the studio I work in”, vs. “needs to work for every Unity user on the planet” - I have it a lot easier here.

With the attributes, it looks like this:

// Allow to use the element from Uxml
[UxmlElement]
public partial class MyElement : VisualElement
{
    private int m_Value;

    // Expose this property to uxml
    [UxmlAttribute]
    // Expose this property to bindings
    [CreateProperty]
    public int value
    {
        get => m_Value;
        set
        {
            if (value == m_Value)
                return;

            m_Value = value;
            // Notify that the property changed, this is done for performance reasons.
            NotifyPropertyChanged(nameof(value));
        }
    }
}

To use this element with bindings, you would do something like this:

var myElement = new MyElement();
myElement.dataSource = /* some objects */;

myElement.SetBinding("value", new DataBinding { dataSourcePath = "pathToValue" } );

I wouldn’t consider this extremely high boilerplate. Additional code can be added for performance reasons, but it is optional.

For these use cases, we also give the option to define your own bindings, so that you can make a dedicated and specialized workflow if you need to. These will also be supported in the uxml format.

We also need it to work on all supported platforms, not add too much to the domain reload times in the editor, work with existing tech, support for the authoring format, etc.

It was a lot easier writing custom tools to do data binding for a given project/studio. :slight_smile: