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. ![]()