I am creating a fluent interface to build user interfaces in the Unity game engine.
As a subcomponent of this library, I have a set of objects called Components (e.g. Panel, Button, etc) that I want to map to new instances of View objects that are created by cloning an original "prefab" object. This is converts the objects created in the fluent interface into the game-facing UI objects that Unity works with for UI purposes.
The PrefabFactory
clones prefab view objects using the Unity Engine's built-in Instantiate
method:
public class PrefabFactory<TComponent, TView> : IComponentFactory<TComponent>
where TComponent : Components.Component
where TView : UIView<TComponent>
{
readonly TView prefab;
public PrefabFactory(TView prefab)
{
this.prefab = prefab;
}
public GameObject BuildComponent(TComponent component, UIFactory uiFactory)
{
var view = Object.Instantiate(prefab);
view.Bind(component, uiFactory);
return view.gameObject;
}
public GameObject BuildComponent(TComponent component, UIFactory uiFactory, RectTransform parent)
{
var view = Object.Instantiate(prefab, parent);
view.Bind(component, uiFactory);
return view.gameObject;
}
public GameObject BuildComponent(Components.Component component, UIFactory uiFactory)
{
return BuildComponent(component as TComponent, uiFactory);
}
public GameObject BuildComponent(Components.Component component, UIFactory uiFactory, RectTransform parent)
{
return BuildComponent(component as TComponent, uiFactory, parent);
}
}
Based on the IComponentFactory interface:
public interface IComponentFactory
{
GameObject BuildComponent(UIBuilder.Components.Component component, UIFactory uiFactory);
GameObject BuildComponent(UIBuilder.Components.Component component, UIFactory uiFactory, RectTransform parent);
}
public interface IComponentFactory<in T> : IComponentFactory where T: UIBuilder.Components.Component
{
GameObject BuildComponent(T component, UIFactory uiFactory);
GameObject BuildComponent(T component, UIFactory uiFactory, RectTransform parent);
}
Where UIBuilder.Components.Component is a model component. Components can be any type of UI object such as buttons, labels, etc. It's defined as follows:
public class Component
{
public Positioning Positioning { get; set; }
public Margin Margin { get; set; }
}
But this is subclassed for Panels, Buttons, etc. For example, Button:
public class Button : Component
{
public Component Content { get; set; }
public Action ClickAction { get; set; }
public static ButtonBuilder Builder => new ButtonBuilder();
}
And the View scripts (a script attached to each cloned prefab for controlling binding and functionality) inherit from this base class:
public abstract class UIView<TModel> : MonoBehaviour where TModel : Components.Component
{
private void SetMargin(Margin margin, RectTransform rectTransform)
{
rectTransform.offsetMax = new Vector2(-margin.Right, -margin.Top);
rectTransform.offsetMin = new Vector2(margin.Left, margin.Bottom);
}
private void SetPosition(Positioning positioning, RectTransform rectTransform)
{
rectTransform.anchorMax = positioning.Anchors.TopRight;
rectTransform.anchorMax = positioning.Anchors.BottomLeft;
rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, positioning.Width);
rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, positioning.Height);
}
public void Bind(TModel model, UIFactory uiFactory)
{
var rectTransform = GetComponent<RectTransform>();
if (model.Positioning != null)
{
SetPosition(model.Positioning, rectTransform);
}
if (model.Margin != null)
{
SetMargin(model.Margin, rectTransform);
}
BindModel(model, uiFactory);
}
protected abstract void BindModel(TModel model, UIFactory uiFactory);
}
With one typical implementation being:
public class ButtonView : UIView<Components.Button>
{
[SerializeField]
UnityEngine.UI.Button button;
[SerializeField]
RectTransform body;
protected override void BindModel(Components.Button model, UIFactory uiFactory)
{
button.onClick.AddListener(() => model.ClickAction());
uiFactory.BuildUI(model.Content as dynamic, body);
}
}
To tie this all together, I have a UIFactory
class that takes a dictionary which maps Component
types to component factories (it's possible to use a ComponentFactory
that doesn't rely on the prefab system, although the typical use case would be the PrefabFactory
) and simply uses the correct factory for the type passed in.
public class UIFactory
{
readonly IDictionary<Type, IComponentFactory> componentFactories;
public UIFactory(IDictionary<Type, IComponentFactory> componentFactories)
{
this.componentFactories = componentFactories;
}
public UIFactory() : this(new Dictionary<Type, IComponentFactory>())
{
}
public void RegisterPrefabFactory<TComponent, TView>(TView prefab)
where TView : UIView<TComponent>
where TComponent : Components.Component
{
componentFactories.Add(typeof(TComponent), new PrefabFactory<TComponent, TView>(prefab));
}
public GameObject BuildUI<TComponent>(TComponent component) where TComponent : Components.Component
{
var factory = componentFactories[typeof(TComponent)];
return factory.BuildComponent(component, this);
}
public GameObject BuildUI<TComponent>(TComponent component, RectTransform parent) where TComponent : Components.Component
{
var factory = componentFactories[typeof(TComponent)];
return factory.BuildComponent(component, this, parent);
}
}
Here's a typical usage scenario showing the full API. It sets up the prefab factory by registering prefabs to different component types, and then builds a small panel with a vertical stack of buttons on it. Finally, it passes this stack of buttons into the UIBuilder
instance, which instantiates a number of prefabs for the panel, stack content holder and the buttons.
[SerializeField]
PanelView panelPrefab;
[SerializeField]
ButtonView buttonPrefab;
[SerializeField]
StackContentView stackPrefab;
// Use this for initialization
void Start () {
var builder = new UIBuilder.Views.Factories.UIFactory();
builder.RegisterPrefabFactory<Panel, PanelView>(panelPrefab);
builder.RegisterPrefabFactory<Button, ButtonView>(buttonPrefab);
builder.RegisterPrefabFactory<StackContent, StackContentView>(stackPrefab);
builder.BuildUI(CreatePanel(), GetComponent<RectTransform>());
}
(where CreatePanel()
returns a PanelComponent
containing a stack of buttons)
This is all working fine and well, but I wonder if there's a better to implement RegisterPrefabFactory
to reduce the need to explicitly specify the generics.
Additionally, I'd love a general review of the code shown here to see if there's room for improvement.
Please note that I am restricted to C# 6 for this project and cannot use Nuget packages. This is because the Unity engine requires specific subsets of the .NET framework that no Nuget packages support.
Prefab
. Is this some pattern or an abbreviation for something? It looks like prefab stands for some template but as I've said... not sure of that either. More details please;-) \$\endgroup\$