BB – Deep Dive: Builders
Posted: October 6, 2024
Last Updated: October 6, 2024
For each new kind of thing we want to make (bullet, firing point, enemy, etc.), I want a builder to act as the easier-to-use interface between the UI and ECS. That was the idea when I started, and it’s mostly stayed unchanged, it’s just been a lot more painful than I expected it to be.
At this point in development, I would say this idea generally worked more than it didn’t work, so that’s good. On one hand, it took a helluva lot of bugfixing and frustration interfacing with Unity’s ECS, but on the other hand, I do think it is a convenient setup for adding different builders ad naseam until I run out of features to add. In other words, I think I’m in a good place.
PROBLEMS:
- Setup: Setting up the builder.
- Interaction: How do we actually plan to dynamically change a ton of values on an entity in our ECS world?
- User Input: Linking UI Input elements to values on our subject. When users input a value, update it correspondingly on the subject in ECS.
- Feedback: Linking values on our subject to UI elements. When a value on the subject changes in ECS, all relevant UI elements associated with that value change to match.
SETUP:
First, each builder must have a subject entity which has on it the components required for functioning in ECS. The builder must find and track their associated subject entity as the main entry point into ECS.
This turned out to be more involved than I initially expected considering I don’t really have any guarantees as to when the OOP Builders and the ECS entities have loaded relative to one another. So, I can’t be sure that a subject exists before the builder looks for them. This requires me to do some funny business waiting in coroutines until I can find the subject singleton for each builder.
// WaitForSubjectCoroutine for FiringPointBuilder.
// Basically just looks in the ECS world for the needed entity.
protected override IEnumerator WaitForSubjectCoroutine() {
// Create Query for FiringPointBuilderSubjectTag
EntityQuery queryTag = _entityManager.CreateEntityQuery(new EntityQueryDesc {
All = new ComponentType[] {typeof(FiringPointBuilderSubjectTag)},
Options = EntityQueryOptions.IncludeDisabledEntities
});
// Keep looking for the subject entity
bool found = false;
while (!found) {
found = found || queryTag.TryGetSingletonEntity<FiringPointBuilderSubjectTag>(out subject);
yield return null;
}
// If things are done setting up, then trigger other things
_waitingForSubject = false;
TrySetWaiting();
if (!_waiting) { DoneLoading(); }
}
Subject found. Now we have a good basis for doing whatever we want in the ECS world by just modifying these local components and setting their data back onto the subject entity!…
INTERACTION:
Unfortunately, it’s not quite that simple, as there’s one more catch: our data/pointers in the ECS world are constantly being invalidated, so anytime we want to change DynamicBuffer components, we have to re-retrieve those components from the subject the same frame of the change.
Let’s add an abstract function that each new InGameBuilder implements to fetch the proper component and buffer data from the subject:
// Each InGameBuilder must implement a method to
// fetch information from the subject Entity in
// the ECS World and read/modify that information
// in locally-kept copies of those components.
public abstract bool TryGetSubjectComponents(bool readOnlyBuffers = false);
// Here is an example of TryGetSubjectComponents
// for the FiringPointBuilder
public override bool TryGetSubjectComponents(bool readOnlyBuffers = false) {
// Returns true if able to get all components
// Returns false if unable to get all components/buffers
if (subject == Entity.Null) { return false; }
if (!_entityManager.HasComponent<FiringPointComponent>(subject) ||
!_entityManager.HasBuffer<FiringPointElement>(subject)) {
Debug.Log($"FPComponent {_entityManager.HasComponent<FiringPointComponent>(subject)}");
Debug.Log($"FPBuffer {_entityManager.HasBuffer<FiringPointElement>(subject)}");
return false;
}
// Gets the components if they all exist
firingPointBuffer = _entityManager.GetBuffer<FiringPointElement>(subject, readOnlyBuffers);
firingPointComponent = _entityManager.GetComponentData<FiringPointComponent>(subject);
return true;
}
For basic IComponentData-derived components, we have to explicitly set the data back, but for DynamicBuffers, we should just be able to properly set the data back by changing the returned DynamicBuffer as long as we are NOT using read-only buffers.
With this out of the way, it should be super easy to create the specific interactions for changing things in the ECS world. For example, we could make a couple functions to set the number of projectiles the firing point shoots and a function to set whether or not it is firing at all:
// Set this to call when a UI element (like a slider) is changed.
// (I never implemented this code because I moved on to
// a different way, but this is how it would work)
public void SetNumBullets(int numBullets) {
if (!TryGetSubjectComponents(false)) { return; }
// Get, change, and set back current firing point element
FiringPointElement fpe = firingPointBuffer[currElement];
fpe.OriginalPattern.numBullets = numBullets;
firingPointBuffer[currElement] = fpe;
}
// Set this to call when a UI element (like a toggle) is changed.
public void SetIsFiring(bool isFiring) {
// Not using DynamicBuffer, so we can do read-only on those
if (!TryGetSubjectComponents(true)) { return; }
// Change the value in local copy of struct
firingPointComponent.Firing = isFiring;
// Send data back into ECS world
_entityManager.SetComponentData<FiringPointComponent>(subject, firingPointComponent);
}
Everything is now solved and my problems are over 🙂 🙂 🙂 🙂 🙂 🙂 🙂 🙂 🙂 🙂 🙂 🙂 🙂 🙂 🙂 🙂 🙂 🙂 🙂 🙂 🙂 🙂 🙂 🙂 🙂 🙂 🙂 🙂 🙂 🙂 🙂 🙂 🙂 🙂 🙂 .
But, what if we wanted a couple different UI elements or widgets that linked to the same field? We would need a way to update all the other associated UI elements. The first thing that comes to mind is just to track each UI element with a List<> and either assign them in the inspector or have some naming scheme so we can search for relevant UI elements.
Also what happens if we want something like a float that represents an angle to be interpreted as a 2D vector direction for one of our fields? We would need to make a new function for the field that takes in a float instead of a Vector2 and do some processing on it.
Now take all these things, multiply it by a few hundred fields, plus the knowledge of the hundreds of fields that I might add in the future, and you have a recipe for me not wanting to have to copy-paste and edit multiple hundreds of functions that are all slightly different.
// Note: Again, I didn't actually end up using this approach.
private bool StringToFloat2(string input, out float2 output) {
// Convert the input string into a valid vector
string[] components = ((string)value).Split(',');
if (components.Count != 2) { return false; }
float xVal;
float yVal;
bool success = float.TryParse(components[0], out xVal);
success = float.TryParse(components[1], out yVal) && success;
if (success) {
output = new float2(xVal, yVal);
return true;
}
else { return false; }
}
// Set this to call when a UI element (like an input box) is changed.
// Input string should have format of "x_float_val,y_float_val"
public void SetBaseAim(string baseAimString) {
float2 vec;
if (!StringToFloat2(baseAimString, out vec)) { return; }
SetBaseAim(vec);
}
// Set this to call when a UI element (like slider) is changed.
// Input value represents angle from the +x axis in 2D.
public void SetBaseAim(float angleDegrees) {
float2 vec = (new float2(1f,0f)).RotateDegrees(angleDegrees);
SetBaseAim(vec);
}
// Actually performing the data update
private void SetBaseAim(float2 baseAim) {
if (!TryGetSubjectComponents(false)) { return; }
// Get, change, and set back current firing point element
FiringPointElement fpe = firingPointBuffer[currElement];
fpe.OriginalPattern.baseAim = vec;
firingPointBuffer[currElement] = fpe;
// Now also check for all the UI elements associated with this field.
// fpbBaseAimElems is a List<base ui-kinda type>
// (This part is PSEUDOCODE)
// We could also abstract this out and pass in the List
// of relevant UIElements.
foreach (UIElement elem in fpbBaseAimElems) {
if (elem is Slider) { ((Slider)elem).value = baseAim.Angle(); }
else if (elem is InputBox) { ((InputBox)elem).value = $"{baseAim.x},{baseAim.y}"; }
else if (elem is TranslationWidget2D) { ((TranslationWidget2D)elem).position = baseAim; }
// ...
}
}
// Now repeat this a hundred times - once for each field ಠ_à²
No thanks. Aside from being a major pain, it was also a major pain. After some trial-and-error, I settled on using reflection to solve my problems. Sometimes this solution makes me feel like the smartest being alive, and other times it makes me feel like my dumdum brain has made a massive mistake. Regardless, it mostly works with only a few hitches now.
“Great!” I thought. “Now I can just give each UI element an extra MonoBehaviour and assign them a field by name, then whenever a UI element activates, it just needs to trigger an event that updates all elements with the same field”. Or something like that.
So that’s… exactly what I did. What I ended up with was a BuilderBinding – a data class that just relates a field name to a specific UI element, and a BuilderBindingGroup – a MonoBehaviour that I would place to automatically create BuilderBindings using other components on the GameObject.
// A BuilderBinding is simply used to link a field name ('name')
// to a UI object ('bindingObj') among a few other details.
public class BuilderBinding {
public string name;
public GameObject bindingObj;
[Tooltip("The type of value of the bound field.")]
public BindingValueType valueType;
public Binder parentBinder;
// Optional index that maps binding to one or more list indices in binding object
public List<int> indices;
/// <summary>
/// UI or Widget MonoBehaviour type (e.g. 'Slider' or 'RegistryPrefabSelectionWidget')
/// </summary>
public Type behaviourType {
get {
// Check for interactive slider first
InteractiveSlider iSlider = bindingObj.GetComponent<InteractiveSlider>();
if (iSlider != null) { return typeof(InteractiveSlider); }
Slider slider = bindingObj.GetComponent<Slider>();
if (slider != null) { return typeof(Slider); }
TMP_InputField inputField = bindingObj.GetComponent<TMP_InputField>();
if (inputField != null) { return typeof(TMP_InputField); }
// ... it continues
return null;
}
}
/// <summary>
/// UI or Widget MonoBehaviour itself (a class)
/// </summary>
public object behaviour {
get {
InteractiveSlider iSlider = bindingObj.GetComponent<InteractiveSlider>();
if (iSlider != null) { return iSlider; }
Slider slider = bindingObj.GetComponent<Slider>();
if (slider != null) { return slider; }
TMP_InputField inputField = bindingObj.GetComponent<TMP_InputField>();
if (inputField != null) { return inputField; }
// ... it continues
return null;
}
}
}
// Simple way to group some bindings for a single parameter
public class BuilderBindingGroup : MonoBehaviour {
[Tooltip("Replaces the name for all bindings in the group.")]
public string bindingField;
[Tooltip("Replaces the valueType for all bindings in the group.")]
public BindingValueType bindingType;
[Tooltip("List of interactive objects relating to the same binding field.")]
public List<GameObject> bindingObjects;
public List<BuilderBinding> bindings;
public bool bindingEnabled = true;
public bool doomed { get; private set; }
public bool built { get; private set; }
void Awake() {
doomed = false;
built = false;
if (bindings == null) {
BuildBindings();
}
}
// If no BuilderBindings are explicitly set, then
// just look at the UI GameObjects in my bindingObjects
// and create a new BuilderBinding using them by associating
// them with the single field name 'bindingField'.
public void BuildBindings() {
if (built) { return; }
if (bindings == null) { bindings = new List<BuilderBinding>(); }
foreach (GameObject bObj in bindingObjects) {
bindings.Add( new BuilderBinding{
name = bindingField,
bindingObj = bObj,
valueType = bindingType
});
}
built = true;
}
public void SetDoomed() {
doomed = true;
}
public void SetEnabled(bool enabled) {
bindingEnabled = enabled;
}
}
Fabulous! Now I just stick a BuilderBindingGroup onto a GameObject, type in a field name (e.g. ‘fpe.OriginalPattern.numProjectiles’), set the value type (‘INT’), and drag in the UI elements I want to attach into the bindingObjects list. Then the BuilderBindingGroup automatically makes all the BuilderBindings for that field using the provided binding objects.

A BuilderBindingGroup. This would make 1 BuilderBinding tied to the field named ‘fpe.OriginalPattern.numProjectiles’, which is an integer (INT), using the TMP_InputField component on the BuilderInputField object as a bindingObject. We could add other objects to the BindingObjects list, such as a slider, or anything which has a defined type mapping to integer.
However, this in and of itself does not do anything we care about. It’s just a setup for the actual connections happening behind the scenes via reflection.
There will be a whole post about the Binder, which is the meat of the field editing and UI connection process, but for now, just imagine the Binder as a place to throw in a field name, an object to edit, and a desired value. The Binder takes this information and, using reflection, changes the provided field in the provided editing object to the provided assignment value, doing necessary conversions along the way. The Binder’s other job is to search out all of the fields related to the changed field, and then set all of the related UI objects to the newly changed value.
For now, though, there are a few more things to consider in the InGameBuilder.
USER INPUT:
First, let’s reiterate – I need a way to use existing BuilderBindings to alter some value on the builder’s subject Entity when I change a value on a UI element. Because I’m using reflection to accomplish this, I need some kind of free-form object that will always have the fields I want to edit. From this comes the concept of a BindingObject. The BindingObject just acts as an interface between field names and the builder’s subject in the ECS world. It typically contains fields of various IComponentData and lists of IBufferElementData mirroring the unmanaged Components and DynamicBuffers on the subject entity.
At the very simplest, BindingObjects are just data containers, as the following code shows:
public abstract partial class InGameBuilder : MonoBehaviour {
// ...
public class BaseBindingObject {
// A few fields and methods exist in the
// BaseBindingObject class, but they are not relevant
// to our FiringPointBuilder example right now.
}
// ...
}
public class FiringPointBuilder : InGameBuilder {
// ...
// Each subclass of InGameBuilder should implement it's own
// subclass of BaseBindingObject
public class BindingObject : BaseBindingObject {
public FiringPointElement fpe;
public FiringPointComponent fpc;
// A few more methods are
// omitted for brevity
}
// ...
}
That’s great, but there is no connection here between our BindingObject and the component data on our subject. In that case, let’s make that connection. Each InGameBuilder also needs to implement a SetupBindingObject function that returns an instance of that builder’s BindingObject filled with all the relevant data from our current subject.
In the following code, note how the final structure of our BindingObject for the FiringPointBuilder relates to the BuilderBinding examples from earlier. Here, the builder has a BindingObject with a FiringPointElement field called ‘fpe’, and within a FiringPointElement, there is a FiringPattern called ‘OriginalPattern’ which in turn has an integer field called ‘numProjectiles’. Thus, we can see the chain of nested fields/properties that our BuilderBinding name uses: ‘fpe.OriginalPattern.numProjectiles’.
public class FiringPointBuilder : InGameBuilder {
// ...
/// <summary>
/// Creates a binding object matching FiringPointComponent and current FiringPointElement
/// to same data object to be filled in by interactive bindings.
/// </summary>
/// <returns>BindingObject subclassing BaseBindingObject that contains fields specific to a FiringPoint.</returns>
public override BaseBindingObject SetupBindingObject() {
// Do not allow this to be called while in the middle of matching interactive
// elements to firing point values
if (_waitingOnParamMatch) { return null; }
if (_waiting) { return null; }
// Remember TryGetSubjectComponents?
// It reads data from our subject into the
// InGameBuilder's instance variables.
if (!this.TryGetSubjectComponents()) { return null; }
// Sets the values of the firing point to match
// the UI elements
FiringPointComponent fpc = firingPointComponent;
FiringPointElement fpe = firingPointBuffer[currElement];
// Here we set up the BindingObject for the
// FiringPointBuilder
BindingObject bo = new BindingObject {
fpe = fpe,
fpc = fpc,
// The following is for other stuff
flatTriggerBindings = triggerMap.GetFlatBindings(), // Gets the trigger bindings from the trigger map
mounts = HasMounts() ? mounts.ToList<BaseMountElement>() : null
};
return bo;
}
// ...
}
So we can get the data from our subject into our binding object, but can we get data from our binding object into our subject? The answer, luckily, is yes. Our SetSubjectValues function is basically the foil to SetupBindingObject. It takes a binding object as input and sets the proper Component/DynamicBuffer values on the subject to the corresponding values in the binding object. That’s all! Our binding object is just a vehicle for data changes.
public class FiringPointBuilder : InGameBuilder {
// ...
/// <summary>
/// Sets the internal values of subject FiringPointComponent and current FiringPointElement
/// to the values contained within BaseBindingObject bbo.
/// Used to override subject component values with other values from binding object.
/// </summary>
/// <param name="bbo">BaseBindingObject casted to BindingObject (FP specific) that is used to replace current FPC and FPE structures in subject.</param>
public override void SetSubjectValues(BaseBindingObject bbo) {
if (!TryGetSubjectComponents(false)) { return; }
BindingObject bo = (BindingObject)bbo;
// Some funny business with triggers to map trigger
// data to trigger IDs, but that's irrelevant here
bo = (BindingObject)FillBindingObjectTriggerValues((BaseBindingObject)bo);
FiringPointElement fpe = bo.fpe;
FiringPointComponent fpc = bo.fpc;
// This is where the magic happens!
// We change an element of our writable DynamicBuffer
// with new FiringPointElement data from the binding object.
// Firing Group Element
if (firingPointBuffer.Length > 0) {
fpe.PreviewReset();
Debug.LogWarning($"FiringPointBuilder setting fpe element bullet prefab entity: {fpe.OriginalPattern.projectilePrefab}");
firingPointBuffer[currElement] = fpe;
}
// And here, we set the main FiringPointComponent value
// to that in our binding object and set that component data back on the subject.
// Firing Point Component
_entityManager.SetComponentData<FiringPointComponent>(subject, fpc);
}
// ...
}
Cool, so now we can call SetupBindingObject to get data from our subject, and we can call SetSubjectValues to set data into our subject’s components, but where does the data changing occur?
We just need a new function that sandwiches a little (a lot) of logic between our calls to SetupBindingObject and SetSubjectValues. We can reuse a single method in the InGameBuilder base class to handle all of this for us! It’s as simple as the following ModifySubjectToSingleParameter function.
/// <summary>
/// Same as ModifySubjectToParameters except on a specific binding.
/// Used to modify just one particular part of the subject to match interactive input.
/// </summary>
/// <param name="binding">Specific BuilderBinding to change in subject.</param>
public void ModifySubjectToSingleParameter(BuilderBinding binding) {
StopCoroutine("ModifySubjectToSingleParameterCoroutine");
StartCoroutine(ModifySubjectToSingleParameterCoroutine(binding));
}
IEnumerator ModifySubjectToSingleParameterCoroutine(BuilderBinding binding) {
// Get a BindingObject that defines the current state of
// the builder's subject.
BaseBindingObject bo = null;
do {
// Here's our fun SetupBindingObject function
bo = SetupBindingObject();
if (bo == null) { yield return null; }
} while(bo == null);
Debug.Log($"{this.GetType()} ModifySubjectToSingleParameter {binding.name} on binding object {bo}");
// Call Binder magic to edit the relevant fields of the
// BindingObject based on the provided BuilderBinding
bo = (BaseBindingObject)binder.ModifyBindingObjectToSingleBinding(bo, binding);
// Set modified BindingObject values back
// into the subject Entity.
SetSubjectValues(bo);
onModify?.Invoke();
// Helps us update any interactable elements where multiple
// ui elements map to the same subject field
ModifySingleParameterToSubject(binding);
ResetPreview();
}
FEEDBACK:
Now pretty much everything is connected up as it should be. No more having to write a new function to change a specific field on our subject entity. We just need to type a valid field path in the BuilderBindingGroup, and watch our process do the rest.
All that’s left is just one more problem: how do we change the elements of other UI elements that map to the same (or related) fields in the binding object?
As it turns out, because we are using reflection, we can reuse most of the actual value-setting logic in the Binder to basically go in the opposite direction. We know that most of the interactive UI elements/MonoBehaviours rely on specific fields or properties to get and set their own values. So, for example, we could just know that if a BuilderBinding has a UI ‘Slider’ as its behaviourType, we can just set the slider.value property to update the UI element automatically with a new value.
/// <summary>
/// Changes a single interactive components/BuilderBindings to match subject's internal values.
/// Changes an internal (display or interaction) field to match that of the subject.
/// E.g. changes the 'value' of a slider binding or the 'text' field of an TMP_InputField binding.
/// </summary>
public virtual void ModifySingleParameterToSubject(BuilderBinding binding) {
StopCoroutine("ModifySingleParameterToSubjectCoroutine");
StartCoroutine(ModifySingleParameterToSubjectCoroutine(binding));
}
IEnumerator ModifySingleParameterToSubjectCoroutine(BuilderBinding binding) {
Debug.Log($"{this.GetType()} ModifySingleParametersToSubject");
BaseBindingObject bo = null;
do {
bo = SetupBindingObject();
if (bo == null) { yield return null; }
} while(bo == null);
binder.ModifySingleInteractorToSubject(bo, binding);
_waitingOnParamMatch = false;
TrySetWaiting();
ResetPreview();
}
This way, if we change the number of projectiles from a text box to 3, let’s say, then after ModifySubjectToSingleParameter is called, the subject’s corresponding number of projectiles will be set to 3, but also the ModifySingleParameterToSubject call at the end will read this new value and modify the internal values of all the UI elements connected to the ‘fpe.OriginalPattern.numProjectiles’ field to 3. The actual logic related to finding related binding fields is hidden within the call to ‘binder.ModifySingleInteractorToSubject’, but that is for another time.
Well that was fun, wasn’t it? This DOTS framework has provided me with a bunch of unique challenges, and designing the InGameBuilders as an intermediary between the UI and the ECS world has certainly been one of them. As always, there are more problems to tackle and complain about, so keep a lookout for other long-form deep dives and updates, and I hope you found this interesting.