Save changes (delta) made to prefab in editor as serialized data and apply it later in runtime.

INPUT:

We need to show 100 instances of a prefab on a mobile screen.
Prefab consists of (just for example, the real one is a complex UI prefab):

  • Label
  • Sprite1
  • Sprite2
  • Sprite3

Each prefab instance can have 3 base-states:

  1. Label {color: red}; Sprite1 {enabled}; Sprite2 {disabled}; Sprite3 {disabled};
  2. Label {color: blue}; Sprite1 {disabled}; Sprite2 {enabled}; Sprite3 {disabled};
  3. Label {color: green}; Sprite1 {disabled}; Sprite2 {disabled}; Sprite3 {enabled};

Also there is “sub-states” managed by Animator and AnimationClip. This states applicable to all base-states of original prefab because it only animates the process of Collapsing/Expanding of UI Prefab.

  1. Collapsed - Label Only
  2. Expanded - Lablel and Sprite

WHAT WE NEED

Because we have 3 base-states we have to create 3 pools with 100 prefab istances in each. Each pool instantiates own prefab wich represent a base-state but inside they are pretty all same.
As you see there is a vast optimisation possibility.
We can describe each base-state by code and apply it so we whould have only one pool of 100 prefab instances but it’s not handy for production pipeline.

We need to be able to serialize current prefab state and save it in any format and then apply it in runtime.
So the workflow of UI programmer and designer should be:

  1. Compose Prefab (create all elements)
  2. Add VisualStateManager component to prefabs’ root game object
  3. Set color to label
  4. Hide unused sprites
  5. Leave used sprites enabled
  6. Click “Save as new state” button in a VisualStateManager inspector.
  7. Enter style name
  8. GO TO 3. repeat needed number of times.

Later at runtime programmer will just have to do something like:

Animator animator =  GetComponent<Animator>();
VisualStateManager VSM = GetComponent<VisualStateManager>();
VSM.SetState("Collapsed");
if(_currentMapZoomLevel > 0.3) {
    animator.Play("CollapsedState")
}else {
    animator.Play("ExpandedState")
}

WHAT WE ALREADY TRIED

We tried to create a second layer for animator and 1-frame AnimationClip which could set all needed values in a first frame but mechanim needs avatar so layer could work… This could be a workaround
if only there were a way to make layers work without avatars.

I was trying to do the same thing by reading the prefab as yaml (Asset serialization changed to text) then cheking for changed attributes base on other template prefab. Btw is far from working … I can post more if you are interested.

But i guess the ease way is doing a search in the prefab transform tree using:

void Do (Transform t) 
{
	var components = t.GetComponents<Component>(); // Gets all components
	Debug.Log (t.name + " (" + components.Length + ")");

	// Use some reflection to get all attributes foreach component
}

void ForAllChilds(Transform t) {
	Do(t);
	for (int i = 0; i < t.childCount; i++) {
		ForAllChilds(t.GetChild(i));
	}
}

Then using some reflection magic to compare and save the results.


BIG EDIT
i just got some thing work ! If i have this moths ago it would have saved me a lot of time…

The way this works you need to put two Objects in your scene one the m_Left is the template and the m_Rigth is the changed then run Execute from the context menu and it will give a function that changes the m_Left object into the m_Rigth. For now the generated code can be optimized a lot, and only attributes changes are allowed, like color, text and so on.

For more support add extra functions on m_SaveAs to support Vector3 and Vector2 for instance.

    using UnityEngine;

using System.Text;
using System.Linq;
using System.Reflection;
using System.Collections;
using System.Collections.Generic;

public class SimplerDiff : MonoBehaviour {

	public Transform m_Left;
	public Transform m_Right;

	static List<string> m_IgnoreComponents = new List<string> () {"Transform", "RectTransform", "CanvasRenderer"};
	static List<string> m_IgnorePropsNames = new List<string> () {"name", "onCullStateChanged"};

	static Dictionary<System.Type, System.Func<object, string>> m_SaveAs;

	StringBuilder code;

	static SimplerDiff()
	{
		m_SaveAs = new Dictionary<System.Type, System.Func<object, string>>();
		m_SaveAs.Add(typeof(string), (x) => "\""+x.ToString()+"\"");
		m_SaveAs.Add(typeof(int), (x) => x.ToString());
		m_SaveAs.Add(typeof(float), (x) => x.ToString() + "f");

		m_SaveAs.Add(typeof(Color),
			(x) => {
				var c = (Color)x;
				return string.Format("new Color({0}f, {1}f, {2}f, {3}f)", c.r, c.g, c.b, c.a);
			}
		);

		m_SaveAs.Add(typeof(Color32),
			(x) => {
				var c = (Color32)x;
				return string.Format("new Color32({0}, {1}, {2}, {3})", c.r, c.g, c.b, c.a);
			}
		);
	}

	[ContextMenu("Execute")]
	void Execute ()
	{
		code = new StringBuilder();
		code.AppendLine("void ChangeTo_" + m_Right.name + "(GameObject go) {");
		ForAllChilds(m_Left, Do);
		code.AppendLine("}");

		Debug.Log(code.ToString());
	}

	void Do (Transform left)
	{
		bool edit1 = false;
		var path = GetGameObjectPath(left, m_Left);
		var right = GameObject.Find(m_Right.name + path).transform;
		foreach(var lmono in left.GetComponents<Component>())
		{
			var type = lmono.GetType();

			if (m_IgnoreComponents.FindIndex(x => type.Name.Equals(x)) != -1) continue;

			var rmono = right.GetComponent(type);

			if (rmono == null)
				continue;

			var props = type.GetProperties(BindingFlags.Instance|BindingFlags.Public)
				.Where(x => x.CanRead && x.CanWrite).ToArray();

			bool edit2 = false;
			foreach (var prop in props)
			{
				if (m_IgnorePropsNames.FindIndex(x => prop.Name.Equals(x)) != -1) continue;
				
				var lval = prop.GetValue(lmono, null);
				var rval = prop.GetValue(rmono, null);

				if (!lval.Equals(rval))
				{
					System.Func<object, string> cast;
					if (m_SaveAs.TryGetValue(prop.PropertyType, out cast))
					{
						if (!edit1)
						{
							code.AppendLine("	{");
							if (string.IsNullOrEmpty(path))
							{
								code.AppendLine("		var child = go.transform;");
							}
							else
							{
								// ignore the frist / in the path
								code.AppendLine(string.Format("		var child = go.transform.Find(\"{0}\");", path.Substring(1)));
							}
							edit1 = true;
						}
						
						if (!edit2)
						{
							code.AppendLine("		{");
							code.AppendLine(string.Format("			var c = child.GetComponent<{0}>();", type.Name));
							edit2 = true;
						}
						
						code.AppendLine(
							string.Format("			c.{0} = {1};", prop.Name, cast(rval))
						);
					}
				}
			}

			if (edit2)
			{
				code.AppendLine("		}");
			}
		}

		if (edit1)
		{
			code.AppendLine("	}");
		}
	}

	void ForAllChilds(Transform transform, System.Action<Transform> map) {
		map(transform);
		for (int i = 0; i < transform.childCount; i++) {
			ForAllChilds(transform.GetChild(i), map);
		}
	}

	private string GetGameObjectPath(Transform transform, Transform top)
	{
		string path = "";

		while (top != transform && transform.parent != null)
		{
			path = "/" + transform.name + path;
			transform = transform.parent;
		}

		return path;
	}
}