- Home /
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:
Label {color: red}; Sprite1 {enabled}; Sprite2 {disabled}; Sprite3 {disabled};
Label {color: blue}; Sprite1 {disabled}; Sprite2 {enabled}; Sprite3 {disabled};
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.
Collapsed - Label Only
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:
Compose Prefab (create all elements)
Add VisualStateManager component to prefabs' root game object
Set color to label
Hide unused sprites
Leave used sprites enabled
Click "Save as new state" button in a VisualStateManager inspector.
Enter style name
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.
Answer by lassade · Mar 24, 2016 at 07:53 PM
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("\t{");
if (string.IsNullOrEmpty(path))
{
code.AppendLine("\t\tvar child = go.transform;");
}
else
{
// ignore the frist / in the path
code.AppendLine(string.Format("\t\tvar child = go.transform.Find(\"{0}\");", path.Substring(1)));
}
edit1 = true;
}
if (!edit2)
{
code.AppendLine("\t\t{");
code.AppendLine(string.Format("\t\t\tvar c = child.GetComponent<{0}>();", type.Name));
edit2 = true;
}
code.AppendLine(
string.Format("\t\t\tc.{0} = {1};", prop.Name, cast(rval))
);
}
}
}
if (edit2)
{
code.AppendLine("\t\t}");
}
}
if (edit1)
{
code.AppendLine("\t}");
}
}
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;
}
}
Reflection is really slow on mobile devices in case of applying saved states in runtime for 100 prefabs ( Even if you do something like type chache.
The reflection is just to check for differences between your gameObjects you can then generate a c# script with the difference info that sets all values if needed.
I just finish a proof of concept of what i said above, im posting here.
I made a nice project with a sample and improved code. Now code generation is working with a press of a button. Each diff generate a static function inside a static partial class that can be called from anyhere.
Sorry the flood of messages, I am with insomnia.
Check my repo https://github.com/lassade/Unity3D-Godiff pull requests are welcome.