Update:
Now does not need to edit the packages, but uses Harmony to do the changes it previously needed to edit packages for.
Probably not gonna update this anymore.(probably)
Download::UIToolkitExtensions.zip
Everything besides not needing to edit packages is mostly unchanged, biggest exception being the AddManipulator
class needing the namespace to be usable and it not showing up in the editor. So you need to manually add it to the UXML (<Unity.UI.BuilderExtension.AddManipulator/>
) and then you can just use the editor to configure it.
Iâve been using this new version for about two months now, so it should be reasonably bug free. And I hopefully didnât forget to package anything with it either.
Old Description with explanations:
Data flow:
Entity â Component â Field(optional) â Conversion(optional) â Operaton
Entity â has Component â Conversion (optional) â Operaton
Do not select âNoneâ for the conversion field if the Component output and Operation input are not of the same type.
Has Component(bool) field:
A new âfieldâ option is âHas Elementâ, rather than getting the component or a field of it, it returns whenever the Entity has or has not said Component.
Operation field:
It lets you do whatever you want with the VisualElement rather than being stuck only being able to set text. It requires two generic types, the input value and the VisualElement subtype. It is checked during runtime if the VisualElement subtype matches to what it was attached to, but it seems not possible to check during edit time. A Error will display if a UpdateOperation is not valid for the VisualElement the manipulator was attached to, remember to name your VisualElement or you will have a hard time to find which one is causing the issue.
UpdateOperation Examples:
public class SetTextOperation : UpdateOperation<string, TextElement> {
public override void PerformOperation(string value, TextElement target) {
target.text = value;
}
}
public class SetDisplayOperation : UpdateOperation<bool, VisualElement> {
public override void PerformOperation(bool value, VisualElement target) {
target.style.display = value ? DisplayStyle.Flex : DisplayStyle.None;
}
}
public class SetVisibleOperation : UpdateOperation<bool, VisualElement> {
public override void PerformOperation(bool value, VisualElement target) {
target.visible = value;
}
}
public class SetEntityData : UpdateOperation<Entity, VisualElement> {
public override void PerformOperation(Entity value, VisualElement target) {
Utils.UIElementUserData(target, new SourceEntity() { source = value });
}
}
Old Old Description and more indepth explanation:
Iâve made a tool to enable you to add manipulators straight from UI Builder and even made so you can automagically take ECS data and replace Text fieldsâ text with said values. I am sure there are more optimizations to further mitigate the whole lot of allocations and reflection used, but I am not really good at that and need to focus on other issues in my project, so those will be as is unless someone else want to keep extending this.
Video: (OBS did not capture the pop up menus, and I really donât want to remake the video) (I really wish this forum would support uploading webm)
https://www.youtube.com/watch?v=NPBxIpn3ncw
How it works:
The Manipulator Type field will use reflection to find any manipulator on your assembly, it currently have a solution specific limitation on which assemblies it searches for.
The manipulators can declare their own âInspectorâ to be used. Unfortunately I couldnât get the inspector to both create the inspector fields and generate the manipulator to be added, since adding a manipulator have to work on the built version that lacks editor code and the âadding of fieldsâ require editor code, so I had to split declaring a manipulator inspector in two parts, one is declaring the inspector much alike how one declares a custon inspector, and another is making sure your manipulator class implements a interface for initializing itâs fields from a string (since UXML stores data in strings).
Currently I have only one âMiniInspectorâ implemented, which is for the SetTextFromEntityDataManipulator
shown above.
Data Source Type: Lets you select if the source is a IComponentData
, ISharedComponentData
or a Component
Object
Data Type Filter: Is just a string field that lets you filter, because ECS projects will tend to have a whole lot of any of the above to fill your whole monitor height 3 times over. If any part of the type name contains any part of the field text, itâs considered found.
Data Type: A popup field that lets you pick a type to be searched for in the entity (more on this later)
Data Field: Which field/property of the type you want to supply the conversor or .ToString() it.
Conversor: Lets you pick a class to convert your data to string, in case it needs special treatment. It will filter for matching types, so a DataConversion<PlantData>
will only show in the list if you select PlantData
and field as None to supply the whole PlantData
as is (in case you need more than one field from it to properly display the information.
Currently there is no way to supply two or more components for a conversor. But the same tech used to get conversor could be used for a different Manipulator
that supply a Entity
and EntityManager
to a different type of conversor.
Supplying the Entity:
Those manipulators are all and good, but there needs to be something that supplies them the Entity to take the data from. There is basically no generic way to accomplish this, so each project needs itâs own way of doing it. Currently my project sets a static variable âWorldTooltipâ with whichever Entity the center of the screen is âlookingâ at. This is done by this Manipulator:
SetEntityManipulator.cs
using UnityEngine.UIElements;
public class SetEntityManipulator : Manipulator {
public enum EntitySourceMethod {
WorldTooltip
}
private bool Disabled;
public EntitySourceMethod method;
public SetEntityManipulator() {//I will eventually make a mini inspector for this when I have more than one
method = EntitySourceMethod.WorldTooltip;//way of supplying a Entity to the UI Toolkit
}
protected override void RegisterCallbacksOnTarget() {
Disabled = false;
target.schedule.Execute(() => Utils.UIElementUserData(target, GetEntitySource())).Until(() => Disabled);
}
private SourceEntity GetEntitySource() {
if (method == EntitySourceMethod.WorldTooltip) {
if (UIKeyListener.WorldTooltip != null) {//Solution specific
return new SourceEntity() { source = UIKeyListener.WorldTooltip.Entity };
}
}
return new SourceEntity();
}
protected override void UnregisterCallbacksFromTarget() {
Disabled = true;
}
}
Whatâs important is a UIElement
having a userData
of SourceEntity
type, so itâs child can look for it and then know which Entity to use. This probably can be optimized using Events. Utils.UIElementUserData
uses a Dictionary to let me have multiple user datas in the same object, much like a GameObject
can have multiple Behaviors
Getting and converting the data to string:
This class is heavily dependent on Vexe.Runtime.Extensions (GitHub - vexe/Fast.Reflection: Extension methods library emitting IL to generate delegates for fast reflection) and uses Newtonsoft.Json to serialize the data
There is not much to explain besides displaying the code. The most important methods are RegisterCallbackOnTarget
which starts the scheduling for updating the text field and ConfigureManipulator
which enables this to work on the Standalone Player by taking the Manipulator configuration out of the Editor-only MiniInspector
SetTextFromEntityDataManipulator.cs
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Reflection;
using Unity.Entities;
using UnityEngine;
using UnityEngine.UIElements;
using Vexe.Runtime.Extensions;
public class SetTextFromEntityDataManipulator : Manipulator, ConfigurableManipulator {
public class SetTextFromEntityDataManipulatorData {
public Type DataSourceType;
public Type DataType;
public MemberInfo Member;
public Type Conversor;
[JsonIgnore]
public List<Type> DataTypeTypes = new List<Type>();
[JsonIgnore]
public string TypeFilter = "";
[JsonIgnore]
public List<MemberInfo> MemberInfoList = new List<MemberInfo>();
[JsonIgnore]
public List<Type> ConversorList = new List<Type>();
}
private bool Disabled;
private bool HasEntityManager;
private string OldValue = "";
public Func<EntityManager, Entity, object> GetEntityDataFunction;
public Func<object, string> TransformationFunction;
public SetTextFromEntityDataManipulator() {
}
protected override void RegisterCallbacksOnTarget() {
Disabled = false;
HasEntityManager = false;
if (typeof(TextElement).IsAssignableFrom(target.GetType())) {
target.schedule.Execute(() => {
EntityManager em;//World.DefaultGameObjectInjectionWorld is not a thing when the first schedule runs
if (World.DefaultGameObjectInjectionWorld != null) {
em = World.DefaultGameObjectInjectionWorld.EntityManager;
} else {
return;
}
target.schedule.Execute(() => {
//Navigates the hierarchy until it finds a UIElement having SourceEntity, probably can be optimized with events
SourceEntity sourent = Utils.UIElementUserDataInParent<SourceEntity>(target);
if (sourent != null && sourent.source != default) {
Entity ent = sourent.source;
if (em.Exists(ent)) {
//boxing, there might be a way to optimize this, but I don't know of it
object data = GetEntityDataFunction(em, ent);
if (data != null) {
string newval = TransformationFunction(data);
if (OldValue != newval) {
OldValue = newval;
TextElement eltarg = target as TextElement;
eltarg.text = newval;
}
}
}
}
}).Until(() => Disabled);
}).Until(() => HasEntityManager);
}
}
protected override void UnregisterCallbacksFromTarget() {
Disabled = true;
}
public void ConfigureManipulator(string CurrentValue) {
SetTextFromEntityDataManipulatorData data = JsonConvert.DeserializeObject<SetTextFromEntityDataManipulatorData>(CurrentValue, new MemberInfoConverter());
if (data.DataSourceType != null && data.DataType != null) {
MethodCaller<object, object> methodCaller = null;
MemberGetter<object, object> memberGetter = null;
Type EMType = typeof(EntityManager);
MethodCaller<EntityManager, bool> hasMethod = EMType.GetMethod("HasComponent", new Type[] { typeof(Entity) }).
MakeGenericMethod(data.DataType).DelegateForCall<EntityManager, bool>();
if (data.DataSourceType == typeof(IComponentData)) {
methodCaller = EMType.GetMethod("GetComponentData").
MakeGenericMethod(data.DataType).DelegateForCall<object, object>();
}
if (data.DataSourceType == typeof(ISharedComponentData)) {
methodCaller = EMType.GetMethod("GetSharedComponentData", new Type[] { typeof(Entity) }).
MakeGenericMethod(data.DataType).DelegateForCall<object, object>();
}
if (data.DataSourceType == typeof(Component)) {
methodCaller = EMType.GetMethod("GetComponentObject").
MakeGenericMethod(data.DataType).DelegateForCall<object, object>();
}
Type fieldType = null;
if (data.Member is FieldInfo) {
memberGetter = (data.Member as FieldInfo).DelegateForGet();
fieldType = (data.Member as FieldInfo).FieldType;
} else if (data.Member is PropertyInfo) {
memberGetter = (data.Member as PropertyInfo).DelegateForGet();
fieldType = (data.Member as PropertyInfo).PropertyType;
}
if (data.Member == null || data.Member.GetType() == typeof(EmptyMemberInfo)) {
fieldType = data.DataType;
}
if (methodCaller != null) {
GetEntityDataFunction = (EM, E) => {
object[] ENT = new object[] { E };
return hasMethod(EM, ENT) ? methodCaller(EM, ENT) : null;
};
}
if (memberGetter != null) {
if (data.Conversor != null && data.Conversor != typeof(EmptyConversor) && fieldType != null) {
ConvertDelegate convertMethod = DataConversion.GetConvertMethod(data.Conversor, fieldType);
if (convertMethod != null) {
TransformationFunction = (O) => convertMethod(memberGetter(O));
} else {
TransformationFunction = (O) => memberGetter(O).ToString();
}
} else {
TransformationFunction = (O) => memberGetter(O).ToString();
}
} else {
if (data.Conversor != null && data.Conversor != typeof(EmptyConversor) && fieldType != null) {
ConvertDelegate convertMethod = DataConversion.GetConvertMethod(data.Conversor, fieldType);
if (convertMethod != null) {
TransformationFunction = (O) => convertMethod(O);
} else {
TransformationFunction = (O) => O.ToString();
}
} else {
TransformationFunction = (O) => O.ToString();
}
}
}
}
}
Custom MiniInspectors:
Create a class extending ManipulatorMiniInspector
and with the CustomMiniInspector
attribute much alike a CustomEditor
. The only method you need to implement is BindableElement CreateInspector(string CurrentValue, EventCallback<string> OnChangeCallback)
The biggest issue is that even though SetTextFromEntityDataManipulatorMiniInspecotor
have multiple fields but can only write to one UXML attribute without a major rewrite of the BuilderInspectorAttributes
class, and to keep changes to the packages at a minimum, I just use Json to serialize whatever I need to string.
SetTextFromEntityDataManipulatorMiniInspecotor.cs
using System.Collections.Generic;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
using System.Reflection;
using Newtonsoft.Json;
using Unity.Entities;
using System.Linq;
using UnityEngine;
using System;
using static SetTextFromEntityDataManipulator;
[CustomMiniInspector(typeof(SetTextFromEntityDataManipulator))]
public class SetTextFromEntityDataManipulatorMiniInspecotor : ManipulatorMiniInspector {
public override BindableElement CreateInspector(string CurrentValue, EventCallback<string> OnChangeCallback) {
SetTextFromEntityDataManipulatorData data = null;
try { data = JsonConvert.DeserializeObject<SetTextFromEntityDataManipulatorData>(CurrentValue, new MemberInfoConverter()); } catch (Exception e) { Debug.LogError(e); };
if (data == null) {
data = new SetTextFromEntityDataManipulatorData() { DataSourceType = typeof(IComponentData) };
}
BindableElement container = new BindableElement();//
Func<Type, string> func = (T) => (T.Name);
Type[] values = new Type[] { typeof(IComponentData), typeof(ISharedComponentData), typeof(Component) };
PopupField<Type> datasourcetype = new PopupField<Type>("Data Source Type", values.ToList(), values[0], func, func);
if (values.Contains(data.DataSourceType)) {
datasourcetype.value = data.DataSourceType;
}
container.Add(datasourcetype);
TextField dataTypeFilter = new TextField("Data Type Filter");
container.Add(dataTypeFilter);
Type[] types = UpdateTypes(data);//
PopupField<Type> datatype = new PopupField<Type>("Data Type", data.DataTypeTypes, data.DataTypeTypes[0], func, func);
if (types.Contains(data.DataType)) {
datatype.value = data.DataType;
}
container.Add(datatype);
MemberInfo[] members = GetMembers(data);//
Func<MemberInfo, string> func2 = (T) => (T.Name);
PopupField<MemberInfo> member = new PopupField<MemberInfo>("Data Field", data.MemberInfoList, data.MemberInfoList[0], func2, func2);
if (members.Contains(data.Member)) {
member.value = data.Member;
}
member.SetEnabled(members.Length != 1);
container.Add(member);
Type[] conversors = GetConversors(data);//
Func<Type, string> func3 = (T) => (T == typeof(EmptyConversor) ? "None" : T.Name);
PopupField<Type> conve = new PopupField<Type>("Conversor", data.ConversorList, data.ConversorList[0], func3, func3);
if (conversors.Contains(data.Conversor)) {
conve.value = data.Conversor;
}
conve.SetEnabled(conversors.Length != 1);
container.Add(conve);
datasourcetype.RegisterValueChangedCallback((T) => {
data.DataSourceType = T.newValue;
UpdateTypePopupField(data, datatype);
UpdateTypePopupField(data, member);
UpdateConversorPopupField(data, conve);
OnChangeCallback(JsonConvert.SerializeObject(data, new MemberInfoConverter()));
});
dataTypeFilter.RegisterValueChangedCallback((T) => {
data.TypeFilter = T.newValue;
UpdateTypePopupField(data, datatype);
UpdateTypePopupField(data, member);
UpdateConversorPopupField(data, conve);
});
datatype.RegisterValueChangedCallback((T) => {
data.DataType = T.newValue;
UpdateTypePopupField(data, member);
UpdateConversorPopupField(data, conve);
OnChangeCallback(JsonConvert.SerializeObject(data, new MemberInfoConverter()));
});
member.RegisterValueChangedCallback((T) => {
data.Member = T.newValue;
UpdateConversorPopupField(data, conve);
OnChangeCallback(JsonConvert.SerializeObject(data, new MemberInfoConverter()));
});
conve.RegisterValueChangedCallback((T) => {
data.Conversor = T.newValue;
OnChangeCallback(JsonConvert.SerializeObject(data, new MemberInfoConverter()));
});
return container;
}
private static void UpdateTypePopupField(SetTextFromEntityDataManipulatorData data, PopupField<Type> datatype) {
Type[] types2 = UpdateTypes(data);
datatype.MarkDirtyRepaint();
if (types2.Length > 0) {
datatype.SetValueWithoutNotify(types2[0]);
datatype.SetEnabled(true);
} else {
datatype.SetEnabled(false);
}
}
private static void UpdateTypePopupField(SetTextFromEntityDataManipulatorData data, PopupField<MemberInfo> members) {
MemberInfo[] membersInfos = GetMembers(data);//
members.MarkDirtyRepaint();
if (membersInfos.Length > 0) {
members.SetValueWithoutNotify(membersInfos[0]);
}
members.SetEnabled(membersInfos.Length != 1);
}
private static void UpdateConversorPopupField(SetTextFromEntityDataManipulatorData data, PopupField<Type> members) {
Type[] conversors = GetConversors(data);//
members.MarkDirtyRepaint();
if (conversors.Length > 0) {
members.SetValueWithoutNotify(conversors[0]);
}
members.SetEnabled(conversors.Length != 1);
}
private static Type[] UpdateTypes(SetTextFromEntityDataManipulatorData data) {
Type[] types2 = Utils.GetAllClassesImplementing(data.DataSourceType, "Unity", "Microsoft", "System", "Mono", "UMotion");
Array.Sort(types2, (T1, T2) => T1.FullName.CompareTo(T2.FullName));
data.DataTypeTypes.Clear();
string lower = data.TypeFilter.ToLower();
types2 = types2.Where(T => T.FullName.ToLower().Contains(lower)).
Where(T => GetFields(T).Length > 0 || GetProperties(T).Length > 0).ToArray();
data.DataTypeTypes.AddRange(types2);
return types2;
}
private static Type[] GetConversors(SetTextFromEntityDataManipulatorData data) {
Type[] types2 = new Type[0];
Type fieldType = null;
if (data.Member is FieldInfo) {
fieldType = (data.Member as FieldInfo).FieldType;
} else if (data.Member is PropertyInfo) {
fieldType = (data.Member as PropertyInfo).PropertyType;
}
if (data.Member == null || data.Member.GetType() == typeof(EmptyMemberInfo)) {
fieldType = data.DataType;
}
if (fieldType != null) {
types2 = DataConversion.GetConversors(fieldType).Cast((T) => T.GetType()).ToArray();
Array.Sort(types2, (T1, T2) => T1.GetType().Name.CompareTo(T2.GetType().Name));
}
data.ConversorList.Clear();
IEnumerable<Type> collection = new Type[] { typeof(EmptyConversor) }.Concat(types2);
data.ConversorList.AddRange(collection);
return collection.ToArray();
}
private static MemberInfo[] GetMembers(SetTextFromEntityDataManipulatorData data) {
MemberInfo[] types2 = new MemberInfo[0];
if (data.DataSourceType != null && data.DataType != null) {
types2 = types2.Concat(GetFields(data.DataType)).Concat(GetProperties(data.DataType)).ToArray();
Array.Sort(types2, (T1, T2) => T1.Name.CompareTo(T2.Name));
}
data.MemberInfoList.Clear();
IEnumerable<MemberInfo> collection = new MemberInfo[] { new EmptyMemberInfo() }.Concat(types2);
data.MemberInfoList.AddRange(collection);
return collection.ToArray();
}
private static PropertyInfo[] GetProperties(Type data) {
return data.GetProperties();
}
private static FieldInfo[] GetFields(Type data) {
return data.GetFields();
}
}
Custom Conversor:
Some examples:
public class PlantDataTestConversor : DataConversion<PlantData> {
public override string Convert(PlantData value) {
return "PlantData";
}
}
public class StringTestConversor : DataConversion<string> {
public override string Convert(string value) {
return "" + value.Length;
}
}
Installation:
Just drop anywhere in the project.
Requires Harmony and Newtonsoft.Json-for-Unity (insert "jillejr.newtonsoft.json-for-unity": "https://github.com/jilleJr/Newtonsoft.Json-for-Unity.git#upm"
into your manifest.json)
Was tested against:
"com.unity.ui.builder": "1.0.0-preview.13"```
and should at least compile like that.
[6207564--927914--UIToolkitExtensions.zip|attachment](upload://5gBVn0dCysezSb9hZsVYyGI55Ig.zip) (624 KB)