Browse Source

Merge branch '3.7-beta' into 3.7-beta-cpp

badlogic 6 years ago
parent
commit
cdabc333fa
30 changed files with 576 additions and 463 deletions
  1. 2 0
      spine-monogame/example/ExampleGame.cs
  2. 6 6
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/AssetDatabaseAvailabilityDetector.cs
  3. 14 2
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/BoneFollowerInspector.cs
  4. 15 17
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/SkeletonAnimationInspector.cs
  5. 2 1
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/SkeletonDataAssetInspector.cs
  6. 5 5
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/SkeletonDebugWindow.cs
  7. 150 112
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/SkeletonRendererInspector.cs
  8. 9 3
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/SpineAtlasAssetInspector.cs
  9. 70 52
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/SpineAttributeDrawers.cs
  10. 44 38
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/SpineEditorUtilities.cs
  11. 3 8
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/SpineInspectorUtility.cs
  12. 6 8
      spine-unity/Assets/Spine/Editor/spine-unity/SkeletonUtility/Editor/SkeletonUtilityBoneInspector.cs
  13. 30 27
      spine-unity/Assets/Spine/Editor/spine-unity/SkeletonUtility/Editor/SkeletonUtilityInspector.cs
  14. 61 51
      spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/BlendModeMaterialsAsset.cs
  15. 19 19
      spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SkeletonDataAsset.cs
  16. 11 1
      spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SkeletonDataAsset.cs.meta
  17. 0 2
      spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SpineAtlasAsset.cs
  18. 1 3
      spine-unity/Assets/Spine/Runtime/spine-unity/Components/BoneFollower.cs
  19. 3 1
      spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonRenderer.cs
  20. 0 2
      spine-unity/Assets/Spine/Runtime/spine-unity/ISkeletonAnimation.cs
  21. 7 0
      spine-unity/Assets/Spine/Runtime/spine-unity/Modules/AttachmentTools/AttachmentTools.cs
  22. 3 10
      spine-unity/Assets/Spine/Runtime/spine-unity/Modules/SkeletonUtility Modules/SkeletonUtilityEyeConstraint.cs
  23. 2 2
      spine-unity/Assets/Spine/Runtime/spine-unity/Modules/SkeletonUtility Modules/SkeletonUtilityGroundConstraint.cs
  24. 24 4
      spine-unity/Assets/Spine/Runtime/spine-unity/Modules/Timeline/SpineAnimationState/SpineAnimationStateMixerBehaviour.cs
  25. 16 9
      spine-unity/Assets/Spine/Runtime/spine-unity/Modules/Timeline/SpineSkeletonFlip/SpineSkeletonFlipMixerBehaviour.cs
  26. 44 53
      spine-unity/Assets/Spine/Runtime/spine-unity/SkeletonUtility/SkeletonUtility.cs
  27. 14 18
      spine-unity/Assets/Spine/Runtime/spine-unity/SkeletonUtility/SkeletonUtilityBone.cs
  28. 6 8
      spine-unity/Assets/Spine/Runtime/spine-unity/SkeletonUtility/SkeletonUtilityConstraint.cs
  29. 7 1
      spine-unity/Assets/Spine/Runtime/spine-unity/SpineAttributes.cs
  30. 2 0
      spine-xna/example/src/ExampleGame.cs

+ 2 - 0
spine-monogame/example/ExampleGame.cs

@@ -98,6 +98,8 @@ namespace Spine {
 			state = new AnimationState(stateData);
 
 			if (name == "spineboy-ess") {
+				skeleton.SetAttachment("head-bb", "head"); // Activate the head BoundingBoxAttachment.
+
 				stateData.SetMix("run", "jump", 0.2f);
 				stateData.SetMix("jump", "run", 0.4f);
 

+ 6 - 6
spine-unity/Assets/Spine/Editor/spine-unity/Editor/AssetDatabaseAvailabilityDetector.cs

@@ -32,20 +32,20 @@ using UnityEngine;
 
 namespace Spine.Unity.Editor {
 	public static class AssetDatabaseAvailabilityDetector {
-		const string MARKER_RESOURCE_NAME = "SpineAssetDatabaseMarker";
-		private static bool _isMarkerLoaded;
+		const string MarkerResourceName = "SpineAssetDatabaseMarker";
+		private static bool isMarkerLoaded;
 
 		public static bool IsAssetDatabaseAvailable (bool forceCheck = false) {
-			if (!forceCheck && _isMarkerLoaded)
+			if (!forceCheck && isMarkerLoaded)
 				return true;
 
-			TextAsset markerTextAsset = Resources.Load<TextAsset>(MARKER_RESOURCE_NAME);
-			_isMarkerLoaded = markerTextAsset != null;
+			TextAsset markerTextAsset = Resources.Load<TextAsset>(AssetDatabaseAvailabilityDetector.MarkerResourceName);
+			isMarkerLoaded = markerTextAsset != null;
 			if (markerTextAsset != null) {
 				Resources.UnloadAsset(markerTextAsset);
 			}
 
-			return _isMarkerLoaded;
+			return isMarkerLoaded;
 		}
 	}
 }

+ 14 - 2
spine-unity/Assets/Spine/Editor/spine-unity/Editor/BoneFollowerInspector.cs

@@ -46,7 +46,7 @@ namespace Spine.Unity.Editor {
 		[MenuItem ("CONTEXT/SkeletonRenderer/Add BoneFollower GameObject")]
 		static void AddBoneFollowerGameObject (MenuCommand cmd) {
 			var skeletonRenderer = cmd.context as SkeletonRenderer;
-			var go = new GameObject("BoneFollower");
+			var go = new GameObject("New BoneFollower");
 			var t = go.transform;
 			t.SetParent(skeletonRenderer.transform);
 			t.localPosition = Vector3.zero;
@@ -65,8 +65,20 @@ namespace Spine.Unity.Editor {
 			var skeletonRenderer = cmd.context as SkeletonRenderer;
 			return skeletonRenderer.valid;
 		}
+
+		[MenuItem("CONTEXT/BoneFollower/Rename BoneFollower GameObject")]
+		static void RenameGameObject (MenuCommand cmd) {
+			AutonameGameObject(cmd.context as BoneFollower);
+		}
 		#endregion
 
+		static void AutonameGameObject (BoneFollower boneFollower) {
+			if (boneFollower == null) return;
+
+			string boneName = boneFollower.boneName;
+			boneFollower.gameObject.name = string.IsNullOrEmpty(boneName) ? "BoneFollower" : string.Format("{0} (BoneFollower)", boneName);
+		}
+
 		void OnEnable () {
 			skeletonRenderer = serializedObject.FindProperty("skeletonRenderer");
 			boneName = serializedObject.FindProperty("boneName");
@@ -195,7 +207,7 @@ namespace Spine.Unity.Editor {
 			bool missingRigidBody = (hasCollider2D && component.GetComponent<Rigidbody2D>() == null) || (hasCollider3D && component.GetComponent<Rigidbody>() == null);
 			if (missingRigidBody) {
 				using (new SpineInspectorUtility.BoxScope()) {
-					EditorGUILayout.HelpBox("Collider detected. Unity recommends adding a Rigidbody to the parent Transforms of any colliders that are intended to be dynamically repositioned and rotated.", MessageType.Warning);
+					EditorGUILayout.HelpBox("Collider detected. Unity recommends adding a Rigidbody to the Transforms of any colliders that are intended to be dynamically repositioned and rotated.", MessageType.Warning);
 					var rbType = hasCollider2D ? typeof(Rigidbody2D) : typeof(Rigidbody);
 					string rbLabel = string.Format("Add {0}", rbType.Name);
 					var rbContent = SpineInspectorUtility.TempContent(rbLabel, SpineInspectorUtility.UnityIcon(rbType), "Add a rigidbody to this GameObject to be the Physics body parent of the attached collider.");

+ 15 - 17
spine-unity/Assets/Spine/Editor/spine-unity/Editor/SkeletonAnimationInspector.cs

@@ -56,16 +56,12 @@ namespace Spine.Unity.Editor {
 			bool sameData = SpineInspectorUtility.TargetsUseSameData(serializedObject);
 
 			if (multi) {
-				foreach (var o in targets)		
-					TrySetAnimation(o, multi);
+				foreach (var o in targets)
+					TrySetAnimation(o as SkeletonAnimation, multi);
 				
 				EditorGUILayout.Space();
 				if (!sameData) {
-					#if UNITY_5_3_OR_NEWER
 					EditorGUILayout.DelayedTextField(animationName);
-					#else
-					animationName.stringValue = EditorGUILayout.TextField(animationName.displayName, animationName.stringValue);
-					#endif
 				} else {
 					EditorGUI.BeginChangeCheck();
 					EditorGUILayout.PropertyField(animationName);
@@ -78,7 +74,7 @@ namespace Spine.Unity.Editor {
 					component.timeScale = Mathf.Max(component.timeScale, 0);
 				}
 			} else {
-				TrySetAnimation(target, multi);
+				TrySetAnimation(target as SkeletonAnimation, multi);
 
 				EditorGUILayout.Space();
 				EditorGUI.BeginChangeCheck();
@@ -99,31 +95,33 @@ namespace Spine.Unity.Editor {
 			}
 		}
 
-		protected void TrySetAnimation (Object o, bool multi) {
-			var skeletonAnimation = o as SkeletonAnimation;
+		protected void TrySetAnimation (SkeletonAnimation skeletonAnimation, bool multi) {
 			if (skeletonAnimation == null) return;
 			if (!skeletonAnimation.valid)
 				return;
 
 			if (!isInspectingPrefab) {
 				if (wasAnimationNameChanged) {
+					var skeleton = skeletonAnimation.Skeleton;
+					var state = skeletonAnimation.AnimationState;
+
 					if (!Application.isPlaying) {
-						if (skeletonAnimation.state != null) skeletonAnimation.state.ClearTrack(0);
-						skeletonAnimation.skeleton.SetToSetupPose();
+						if (state != null) state.ClearTrack(0);
+						skeleton.SetToSetupPose();
 					}
 
-					Spine.Animation animationToUse = skeletonAnimation.skeleton.Data.FindAnimation(animationName.stringValue);
+					Spine.Animation animationToUse = skeleton.Data.FindAnimation(animationName.stringValue);
 
 					if (!Application.isPlaying) {
-						if (animationToUse != null) animationToUse.PoseSkeleton(skeletonAnimation.Skeleton, 0f);
-						skeletonAnimation.Update(0);
+						if (animationToUse != null) animationToUse.PoseSkeleton(skeleton, 0f);
+						skeleton.UpdateWorldTransform();
 						skeletonAnimation.LateUpdate();
 						requireRepaint = true;
 					} else {
 						if (animationToUse != null)
-							skeletonAnimation.state.SetAnimation(0, animationToUse, loop.boolValue);
+							state.SetAnimation(0, animationToUse, loop.boolValue);
 						else
-							skeletonAnimation.state.ClearTrack(0);
+							state.ClearTrack(0);
 					}
 
 					wasAnimationNameChanged = false;
@@ -131,7 +129,7 @@ namespace Spine.Unity.Editor {
 
 				// Reflect animationName serialized property in the inspector even if SetAnimation API was used.
 				if (!multi && Application.isPlaying) {
-					TrackEntry current = skeletonAnimation.state.GetCurrent(0);
+					TrackEntry current = skeletonAnimation.AnimationState.GetCurrent(0);
 					if (current != null) {
 						if (skeletonAnimation.AnimationName != animationName.stringValue)
 							animationName.stringValue = current.Animation.Name;

+ 2 - 1
spine-unity/Assets/Spine/Editor/spine-unity/Editor/SkeletonDataAssetInspector.cs

@@ -451,7 +451,8 @@ namespace Spine.Unity.Editor {
 					}
 					//string frameCountString = (fps > 0) ? ("(" + (Mathf.RoundToInt(animation.Duration * fps)) + ")").PadLeft(12, ' ') : string.Empty;
 					//EditorGUILayout.LabelField(new GUIContent(animation.Name, Icons.animation), SpineInspectorUtility.TempContent(animation.Duration.ToString("f3") + "s" + frameCountString));
-					EditorGUILayout.LabelField(new GUIContent(animation.Name, Icons.animation), SpineInspectorUtility.TempContent(animation.Duration.ToString("f3") + "s"));
+					string durationString = animation.Duration.ToString("f3");
+					EditorGUILayout.LabelField(new GUIContent(animation.Name, Icons.animation), SpineInspectorUtility.TempContent(durationString + "s", tooltip: string.Format("{0} seconds\n{1} timelines", durationString, animation.Timelines.Count)));
 				}
 			}
 		}

+ 5 - 5
spine-unity/Assets/Spine/Editor/spine-unity/Editor/SkeletonDebugWindow.cs

@@ -228,11 +228,11 @@ namespace Spine.Unity.Editor {
 							EditorGUI.EndDisabledGroup();
 
 							// Flip
-							EditorGUILayout.BeginHorizontal(GUILayout.MaxWidth(160f));
-							EditorGUILayout.LabelField("Scale", GUILayout.MaxWidth(EditorGUIUtility.labelWidth - 20f));
-							skeleton.ScaleX = EditorGUILayout.DelayedFloatField(".ScaleX", skeleton.ScaleX, GUILayout.MaxWidth(70f));
-							skeleton.ScaleY = EditorGUILayout.DelayedFloatField(".ScaleY", skeleton.ScaleY, GUILayout.MaxWidth(70f));
-							GUILayout.EndHorizontal();
+							skeleton.ScaleX = EditorGUILayout.DelayedFloatField(".ScaleX", skeleton.ScaleX);
+							skeleton.ScaleY = EditorGUILayout.DelayedFloatField(".ScaleY", skeleton.ScaleY);
+							//EditorGUILayout.BeginHorizontal(GUILayout.MaxWidth(160f));
+							////EditorGUILayout.LabelField("Scale", GUILayout.Width(EditorGUIUtility.labelWidth - 20f));
+							//GUILayout.EndHorizontal();
 
 							// Color
 							skeleton.SetColor(EditorGUILayout.ColorField(".R .G .B .A", skeleton.GetColor()));

+ 150 - 112
spine-unity/Assets/Spine/Editor/spine-unity/Editor/SkeletonRendererInspector.cs

@@ -49,14 +49,20 @@ namespace Spine.Unity.Editor {
 		protected SerializedProperty skeletonDataAsset, initialSkinName;
 		protected SerializedProperty initialFlipX, initialFlipY;
 		protected SerializedProperty singleSubmesh, separatorSlotNames, clearStateOnDisable, immutableTriangles;
-		protected SerializedProperty normals, tangents, meshes, zSpacing, pmaVertexColors, tintBlack; // MeshGenerator settings
+		protected SerializedProperty normals, tangents, zSpacing, pmaVertexColors, tintBlack; // MeshGenerator settings
 		protected SpineInspectorUtility.SerializedSortingProperties sortingProperties;
+
 		protected bool isInspectingPrefab;
+		protected bool forceReloadQueued = false;
 
 		protected GUIContent SkeletonDataAssetLabel, SkeletonUtilityButtonContent;
-		protected GUIContent PMAVertexColorsLabel, ClearStateOnDisableLabel, ZSpacingLabel, MeshesLabel, ImmubleTrianglesLabel, TintBlackLabel, SingleSubmeshLabel;
+		protected GUIContent PMAVertexColorsLabel, ClearStateOnDisableLabel, ZSpacingLabel, ImmubleTrianglesLabel, TintBlackLabel, SingleSubmeshLabel;
 		protected GUIContent NormalsLabel, TangentsLabel;
-		const string ReloadButtonLabel = "Reload";
+		
+		const string ReloadButtonString = "Reload";
+		static GUILayoutOption reloadButtonWidth;
+		static GUILayoutOption ReloadButtonWidth { get { return reloadButtonWidth = reloadButtonWidth ?? GUILayout.Width(GUI.skin.label.CalcSize(new GUIContent(ReloadButtonString)).x + 20); } }
+		static GUIStyle ReloadButtonStyle { get { return EditorStyles.miniButtonRight; } }
 
 		protected bool TargetIsValid {
 			get {
@@ -76,13 +82,12 @@ namespace Spine.Unity.Editor {
 
 		protected virtual void OnEnable () {
 			isInspectingPrefab = (PrefabUtility.GetPrefabType(target) == PrefabType.Prefab);
-			
+
 			SpineEditorUtilities.ConfirmInitialization();
 
 			// Labels
 			SkeletonDataAssetLabel = new GUIContent("SkeletonData Asset", Icons.spine);
 			SkeletonUtilityButtonContent = new GUIContent("Add Skeleton Utility", Icons.skeletonUtility);
-			MeshesLabel = new GUIContent("Render MeshAttachments", "Disable to optimize rendering for skeletons that don't use Mesh Attachments");
 			ImmubleTrianglesLabel = new GUIContent("Immutable Triangles", "Enable to optimize rendering for skeletons that never change attachment visbility");
 			PMAVertexColorsLabel = new GUIContent("PMA Vertex Colors", "Use this if you are using the default Spine/Skeleton shader or any premultiply-alpha shader.");
 			ClearStateOnDisableLabel = new GUIContent("Clear State On Disable", "Use this if you are pooling or enabling/disabling your Spine GameObject.");
@@ -90,7 +95,7 @@ namespace Spine.Unity.Editor {
 			NormalsLabel = new GUIContent("Add Normals", "Use this if your shader requires vertex normals. A more efficient solution for 2D setups is to modify the shader to assume a single normal value for the whole mesh.");
 			TangentsLabel = new GUIContent("Solve Tangents", "Calculates the tangents per frame. Use this if you are using lit shaders (usually with normal maps) that require vertex tangents.");
 			TintBlackLabel = new GUIContent("Tint Black (!)", "Adds black tint vertex data to the mesh as UV2 and UV3. Black tinting requires that the shader interpret UV2 and UV3 as black tint colors for this effect to work. You may also use the default [Spine/Skeleton Tint Black] shader.\n\nIf you only need to tint the whole skeleton and not individual parts, the [Spine/Skeleton Tint] shader is recommended for better efficiency and changing/animating the _Black material property via MaterialPropertyBlock.");
-			SingleSubmeshLabel = new GUIContent("Use Single Submesh", "Simplifies submesh determination by assuming you are only using one Material and need only one submesh. This is will disable render separation and custom slot materials.");
+			SingleSubmeshLabel = new GUIContent("Use Single Submesh", "Simplifies submesh generation by assuming you are only using one Material and need only one submesh. This is will disable multiple materials, render separation, and custom slot materials.");
 
 			var so = this.serializedObject;
 			skeletonDataAsset = so.FindProperty("skeletonDataAsset");
@@ -99,7 +104,6 @@ namespace Spine.Unity.Editor {
 			initialFlipY = so.FindProperty("initialFlipY");
 			normals = so.FindProperty("addNormals");
 			tangents = so.FindProperty("calculateTangents");
-			meshes = so.FindProperty("renderMeshes");
 			immutableTriangles = so.FindProperty("immutableTriangles");
 			pmaVertexColors = so.FindProperty("pmaVertexColors");
 			clearStateOnDisable = so.FindProperty("clearStateOnDisable");
@@ -111,80 +115,115 @@ namespace Spine.Unity.Editor {
 
 			zSpacing = so.FindProperty("zSpacing");
 
-			SerializedObject rso = SpineInspectorUtility.GetRenderersSerializedObject(serializedObject);
-			sortingProperties = new SpineInspectorUtility.SerializedSortingProperties(rso);
+			SerializedObject renderersSerializedObject = SpineInspectorUtility.GetRenderersSerializedObject(serializedObject); // Allows proper multi-edit behavior.
+			sortingProperties = new SpineInspectorUtility.SerializedSortingProperties(renderersSerializedObject);
 		}
 
-		GUIContent[] skins;
-		ExposedList<Skin> loadedSkinList;
+		public void OnSceneGUI () {
+			var skeletonRenderer = (SkeletonRenderer)target;
+			var skeleton = skeletonRenderer.Skeleton;
+			var transform = skeletonRenderer.transform;
+			if (skeleton == null) return;
 
-		protected virtual void DrawInspectorGUI (bool multi) {
-			bool valid = TargetIsValid;
-			var reloadWidth = GUILayout.Width(GUI.skin.label.CalcSize(new GUIContent(ReloadButtonLabel)).x + 20);
-			var reloadButtonStyle = EditorStyles.miniButtonRight;
+			SpineHandles.DrawBones(transform, skeleton);
+		}
 
-			if (multi) {
-				using (new EditorGUILayout.HorizontalScope(EditorStyles.helpBox)) {
-					SpineInspectorUtility.PropertyFieldFitLabel(skeletonDataAsset, SkeletonDataAssetLabel);
-					if (GUILayout.Button(ReloadButtonLabel, reloadButtonStyle, reloadWidth)) {
+		override public void OnInspectorGUI () {
+			bool multi = serializedObject.isEditingMultipleObjects;
+			DrawInspectorGUI(multi);
+			if (serializedObject.ApplyModifiedProperties() || SpineInspectorUtility.UndoRedoPerformed(Event.current)) {
+				if (!Application.isPlaying) {
+					if (multi) {
+						foreach (var o in targets) EditorForceInitializeComponent((SkeletonRenderer)o);
+					} else {
+						EditorForceInitializeComponent((SkeletonRenderer)target);
+					}
+					SceneView.RepaintAll();
+				}
+			}
+
+			if (!Application.isPlaying && Event.current.type == EventType.Layout) {
+				bool mismatchDetected = false;
+				if (multi) {
+					foreach (var o in targets)
+						mismatchDetected |= UpdateIfSkinMismatch((SkeletonRenderer)o);
+				} else {
+					mismatchDetected |= UpdateIfSkinMismatch(target as SkeletonRenderer);
+				}
+
+				if (mismatchDetected) {
+					mismatchDetected = false;
+					SceneView.RepaintAll();
+				}
+			}
+		}
+
+		protected virtual void DrawInspectorGUI (bool multi) {
+			// Initialize.
+			if (Event.current.type == EventType.Layout) {
+				if (forceReloadQueued) {
+					forceReloadQueued = false;
+					if (multi) {
+						foreach (var c in targets)
+							EditorForceReloadSkeletonDataAssetAndComponent(c as SkeletonRenderer);
+					} else {
+						EditorForceReloadSkeletonDataAssetAndComponent(target as SkeletonRenderer);
+					}
+				} else {
+					if (multi) {
 						foreach (var c in targets) {
 							var component = c as SkeletonRenderer;
-							if (component.skeletonDataAsset != null) {
-								foreach (AtlasAssetBase aa in component.skeletonDataAsset.atlasAssets) {
-									if (aa != null)
-										aa.Clear();
-								}
-								component.skeletonDataAsset.Clear();
+							if (!component.valid) {
+								EditorForceInitializeComponent(component);
+								if (!component.valid) continue;
 							}
-							component.Initialize(true);
 						}
+					} else {
+						var component = (SkeletonRenderer)target;
+						if (!component.valid)
+							EditorForceInitializeComponent(component);
 					}
 				}
 
-				foreach (var c in targets) {
-					var component = c as SkeletonRenderer;
-					if (!component.valid) {
-						if (Event.current.type == EventType.Layout) {
-							component.Initialize(true);
-							component.LateUpdate();
+				#if NO_PREFAB_MESH
+				if (isInspectingPrefab) {
+					if (multi) {
+						foreach (var c in targets) {
+							var component = (SkeletonRenderer)c;
+							MeshFilter meshFilter = component.GetComponent<MeshFilter>();
+							if (meshFilter != null && meshFilter.sharedMesh != null)
+								meshFilter.sharedMesh = null;
 						}
-						if (!component.valid)
-							continue;
-					}
-
-					#if NO_PREFAB_MESH
-					if (isInspectingPrefab) {
+					} else {
+						var component = (SkeletonRenderer)target;
 						MeshFilter meshFilter = component.GetComponent<MeshFilter>();
 						if (meshFilter != null && meshFilter.sharedMesh != null)
 							meshFilter.sharedMesh = null;
 					}
-					#endif
 				}
-					
-				if (valid)
-					EditorGUILayout.PropertyField(initialSkinName);					
+				#endif
+			}
 
-			} else {
-				var component = (SkeletonRenderer)target;
+			bool valid = TargetIsValid;
 
-				if (!component.valid && Event.current.type == EventType.Layout) {
-					component.Initialize(true);
-					component.LateUpdate();
+			// Fields.
+			if (multi) {
+				using (new EditorGUILayout.HorizontalScope(EditorStyles.helpBox)) {
+					SpineInspectorUtility.PropertyFieldFitLabel(skeletonDataAsset, SkeletonDataAssetLabel);
+					if (GUILayout.Button(ReloadButtonString, ReloadButtonStyle, ReloadButtonWidth))
+						forceReloadQueued = true;
 				}
 
+				if (valid) EditorGUILayout.PropertyField(initialSkinName, SpineInspectorUtility.TempContent("Initial Skin"));
+
+			} else {
+				var component = (SkeletonRenderer)target;
+
 				using (new EditorGUILayout.HorizontalScope(EditorStyles.helpBox)) {
 					SpineInspectorUtility.PropertyFieldFitLabel(skeletonDataAsset, SkeletonDataAssetLabel);
 					if (component.valid) {
-						if (GUILayout.Button(ReloadButtonLabel, reloadButtonStyle, reloadWidth)) {
-							if (component.skeletonDataAsset != null) {
-								foreach (AtlasAssetBase aa in component.skeletonDataAsset.atlasAssets) {
-									if (aa != null)
-										aa.Clear();
-								}
-								component.skeletonDataAsset.Clear();
-							}
-							component.Initialize(true);
-						}
+						if (GUILayout.Button(ReloadButtonString, ReloadButtonStyle, ReloadButtonWidth))
+							forceReloadQueued = true;
 					}
 				}
 
@@ -193,38 +232,14 @@ namespace Spine.Unity.Editor {
 					return;
 				}
 
-				#if NO_PREFAB_MESH
-				if (isInspectingPrefab) {
-					MeshFilter meshFilter = component.GetComponent<MeshFilter>();
-					if (meshFilter != null && meshFilter.sharedMesh != null)
-						meshFilter.sharedMesh = null;
+				if (!SkeletonDataAssetIsValid(component.skeletonDataAsset)) {
+					EditorGUILayout.HelpBox("Skeleton Data Asset error. Please check Skeleton Data Asset.", MessageType.Error);
+					return;
 				}
-				#endif
-
-				// Initial skin name.
-				if (component.valid) {
-					var skeletonDataSkins = component.skeleton.Data.Skins;
-					int skinCount = skeletonDataSkins.Count;
-					if (loadedSkinList != skeletonDataSkins) {
-						skins = new GUIContent[skinCount];
-						loadedSkinList = skeletonDataSkins;
-						for (int i = 0; i < skins.Length; i++) {
-							string skinNameString = skeletonDataSkins.Items[i].Name;
-							skins[i] = new GUIContent(skinNameString, Icons.skin);
-						}
-					}
-					
-					int skinIndex = 0;
-					for (int i = 0; i < skins.Length; i++) {
-						string skinNameString = skeletonDataSkins.Items[i].Name;
-						if (skinNameString == initialSkinName.stringValue)
-							skinIndex = i;
-					}
 
-					skinIndex = EditorGUILayout.Popup(SpineInspectorUtility.TempContent("Initial Skin"), skinIndex, skins);
-					if (skins.Length > 0) // Support attachmentless/skinless SkeletonData.
-						initialSkinName.stringValue = skins[skinIndex].text;
-				}
+				if (valid)
+					EditorGUILayout.PropertyField(initialSkinName, SpineInspectorUtility.TempContent("Initial Skin"));
+				
 			}
 
 			EditorGUILayout.Space();
@@ -232,8 +247,9 @@ namespace Spine.Unity.Editor {
 			// Sorting Layers
 			SpineInspectorUtility.SortingPropertyFields(sortingProperties, applyModifiedProperties: true);
 
-			if (!TargetIsValid) return;
-			
+			if (!valid)
+				return;
+
 			// More Render Options...
 			using (new SpineInspectorUtility.BoxScope()) {
 				EditorGUI.BeginChangeCheck();
@@ -250,11 +266,11 @@ namespace Spine.Unity.Editor {
 				EditorGUILayout.EndHorizontal();
 
 				if (advancedFoldout) {
-					
+
 					using (new SpineInspectorUtility.IndentScope()) {
 						using (new EditorGUILayout.HorizontalScope()) {
 							SpineInspectorUtility.ToggleLeftLayout(initialFlipX);
-							SpineInspectorUtility.ToggleLeftLayout(initialFlipY);							
+							SpineInspectorUtility.ToggleLeftLayout(initialFlipY);
 							EditorGUILayout.Space();
 						}
 
@@ -263,7 +279,6 @@ namespace Spine.Unity.Editor {
 						using (new SpineInspectorUtility.LabelWidthScope()) {
 							// Optimization options
 							if (singleSubmesh != null) EditorGUILayout.PropertyField(singleSubmesh, SingleSubmeshLabel);
-							//if (meshes != null) EditorGUILayout.PropertyField(meshes, MeshesLabel);
 							if (immutableTriangles != null) EditorGUILayout.PropertyField(immutableTriangles, ImmubleTrianglesLabel);
 							EditorGUILayout.PropertyField(clearStateOnDisable, ClearStateOnDisableLabel);
 							EditorGUILayout.Space();
@@ -290,7 +305,7 @@ namespace Spine.Unity.Editor {
 
 						EditorGUILayout.Space();
 
-						if (TargetIsValid && !isInspectingPrefab) {
+						if (valid && !isInspectingPrefab) {
 							if (multi) {
 								// Support multi-edit SkeletonUtility button.
 								//	EditorGUILayout.Space();
@@ -302,7 +317,7 @@ namespace Spine.Unity.Editor {
 								//	}
 							} else {
 								var component = (Component)target;
-								if (component.GetComponent<SkeletonUtility>() == null) {						
+								if (component.GetComponent<SkeletonUtility>() == null) {
 									if (SpineInspectorUtility.CenteredButton(SkeletonUtilityButtonContent, 21, true, 200f))
 										component.gameObject.AddComponent<SkeletonUtility>();
 								}
@@ -330,7 +345,7 @@ namespace Spine.Unity.Editor {
 				var sr = separatorSlotNames.serializedObject.targetObject as ISkeletonComponent;
 				var skeleton = sr.Skeleton;
 				int lastSlot = skeleton.Slots.Count - 1;
-				if (skeleton != null) {					
+				if (skeleton != null) {
 					for (int i = 0, n = separatorSlotNames.arraySize; i < n; i++) {
 						int index = skeleton.FindSlotIndex(separatorSlotNames.GetArrayElementAtIndex(i).stringValue);
 						if (index == 0 || index == lastSlot) {
@@ -360,27 +375,50 @@ namespace Spine.Unity.Editor {
 			}
 		}
 
-		public void OnSceneGUI () {
-			var skeletonRenderer = (SkeletonRenderer)target;
-			var skeleton = skeletonRenderer.skeleton;
-			var transform = skeletonRenderer.transform;
-			if (skeleton == null) return;
-
-			SpineHandles.DrawBones(transform, skeleton);
+		static bool UpdateIfSkinMismatch (SkeletonRenderer skeletonRenderer) {
+			if (!skeletonRenderer.valid) return false;
+
+			var skin = skeletonRenderer.Skeleton.Skin;
+			string skeletonSkinName = skin != null ? skin.Name : null;
+			string componentSkinName = skeletonRenderer.initialSkinName;
+			bool defaultCase = skin == null && string.IsNullOrEmpty(componentSkinName);
+			bool fieldMatchesSkin = defaultCase || string.Equals(componentSkinName, skeletonSkinName, System.StringComparison.Ordinal);
+
+			if (!fieldMatchesSkin) {
+				Skin skinToSet = string.IsNullOrEmpty(componentSkinName) ? null : skeletonRenderer.Skeleton.Data.FindSkin(componentSkinName);
+				skeletonRenderer.Skeleton.Skin = skinToSet;
+				skeletonRenderer.Skeleton.SetSlotsToSetupPose();
+				skeletonRenderer.LateUpdate();
+				return true;
+			}
+			return false;
 		}
 
-		override public void OnInspectorGUI () {
-			bool multi = serializedObject.isEditingMultipleObjects;
-			DrawInspectorGUI(multi);
-			if (serializedObject.ApplyModifiedProperties() || SpineInspectorUtility.UndoRedoPerformed(Event.current)) {
-				if (!Application.isPlaying) {
-					if (multi)
-						foreach (var o in targets)
-							((SkeletonRenderer)o).Initialize(true);
-					else
-						((SkeletonRenderer)target).Initialize(true);
+		static void EditorForceReloadSkeletonDataAssetAndComponent (SkeletonRenderer component) {
+			if (component == null) return;
+
+			// Clear all and reload.
+			if (component.skeletonDataAsset != null) {
+				foreach (AtlasAssetBase aa in component.skeletonDataAsset.atlasAssets) {
+					if (aa != null) aa.Clear();
 				}
+				component.skeletonDataAsset.Clear();
 			}
+			component.skeletonDataAsset.GetSkeletonData(true);
+
+			// Reinitialize.
+			EditorForceInitializeComponent(component);
+		}
+
+		static void EditorForceInitializeComponent (SkeletonRenderer component) {
+			if (component == null) return;
+			if (!SkeletonDataAssetIsValid(component.SkeletonDataAsset)) return;
+			component.Initialize(true);
+			component.LateUpdate();
+		}
+
+		static bool SkeletonDataAssetIsValid (SkeletonDataAsset asset) {
+			return asset != null && asset.GetSkeletonData(quiet: true) != null;
 		}
 
 	}

+ 9 - 3
spine-unity/Assets/Spine/Editor/spine-unity/Editor/SpineAtlasAssetInspector.cs

@@ -134,7 +134,7 @@ namespace Spine.Unity.Editor {
 			}
 
 			EditorGUILayout.Space();
-			if (SpineInspectorUtility.LargeCenteredButton(SpineInspectorUtility.TempContent("Set Mipmap Bias to " + SpineEditorUtilities.Preferences.DEFAULT_MIPMAPBIAS))) {
+			if (SpineInspectorUtility.LargeCenteredButton(SpineInspectorUtility.TempContent("Set Mipmap Bias to " + SpineEditorUtilities.Preferences.DEFAULT_MIPMAPBIAS, tooltip: "This may help textures with mipmaps be less blurry when used for 2D sprites."))) {
 				foreach (var m in atlasAsset.materials) {
 					var texture = m.mainTexture;
 					texture.mipMapBias = SpineEditorUtilities.Preferences.DEFAULT_MIPMAPBIAS;
@@ -261,12 +261,18 @@ namespace Spine.Unity.Editor {
 			}
 			#else
 			if (atlasFile.objectReferenceValue != null) {
-				EditorGUILayout.LabelField("Atlas Regions", EditorStyles.boldLabel);
+				
+				
 				int baseIndent = EditorGUI.indentLevel;
 
 				var regions = SpineAtlasAssetInspector.GetRegions(atlasAsset.GetAtlas());
+				int regionsCount = regions.Count;
+				using (new EditorGUILayout.HorizontalScope()) {
+					EditorGUILayout.LabelField("Atlas Regions", EditorStyles.boldLabel);
+					EditorGUILayout.LabelField(string.Format("{0} regions total", regionsCount));
+				}
 				AtlasPage lastPage = null;
-				for (int i = 0; i < regions.Count; i++) {
+				for (int i = 0; i < regionsCount; i++) {
 					if (lastPage != regions[i].page) {
 						if (lastPage != null) {
 							EditorGUILayout.Separator();

+ 70 - 52
spine-unity/Assets/Spine/Editor/spine-unity/Editor/SpineAttributeDrawers.cs

@@ -39,24 +39,24 @@ using Spine;
 
 namespace Spine.Unity.Editor {
 	public struct SpineDrawerValuePair {
-		public string str;
+		public string stringValue;
 		public SerializedProperty property;
 
 		public SpineDrawerValuePair (string val, SerializedProperty property) {
-			this.str = val;
+			this.stringValue = val;
 			this.property = property;
 		}
 	}
 
 	public abstract class SpineTreeItemDrawerBase<T> : PropertyDrawer where T:SpineAttributeBase {
 		protected SkeletonDataAsset skeletonDataAsset;
-		internal const string NoneString = "<None>";
+		internal const string NoneStringConstant = "<None>";
 
-		// Analysis disable once StaticFieldInGenericType
-		static GUIContent noneLabel;
-		static GUIContent NoneLabel (Texture2D image = null) {
-			if (noneLabel == null)
-				noneLabel = new GUIContent(NoneString);
+		internal virtual string NoneString { get { return NoneStringConstant; } }
+
+		GUIContent noneLabel;
+		GUIContent NoneLabel (Texture2D image = null) {
+			if (noneLabel == null) noneLabel = new GUIContent(NoneString);
 			noneLabel.image = image;
 			return noneLabel;
 		}
@@ -74,6 +74,12 @@ namespace Spine.Unity.Editor {
 				return;
 			}
 
+			// Handle multi-editing when instances don't use the same SkeletonDataAsset.
+			if (!SpineInspectorUtility.TargetsUseSameData(property.serializedObject)) { 
+				EditorGUI.DelayedTextField(position, property, label);
+				return;
+			}
+
 			SerializedProperty dataField = property.FindBaseOrSiblingProperty(TargetAttribute.dataField);
 
 			if (dataField != null) {
@@ -116,8 +122,8 @@ namespace Spine.Unity.Editor {
 				
 			position = EditorGUI.PrefixLabel(position, label);
 
-			var image = Icon;
-			var propertyStringValue = (property.hasMultipleDifferentValues) ? SpineInspectorUtility.EmDash : property.stringValue;
+			Texture2D image = Icon;
+			string propertyStringValue = (property.hasMultipleDifferentValues) ? SpineInspectorUtility.EmDash : property.stringValue;
 			if (GUI.Button(position, string.IsNullOrEmpty(propertyStringValue) ? NoneLabel(image) : SpineInspectorUtility.TempContent(propertyStringValue, image), EditorStyles.popup))
 				Selector(property);
 		}
@@ -151,8 +157,10 @@ namespace Spine.Unity.Editor {
 
 		protected virtual void HandleSelect (object menuItemObject) {
 			var clickedItem = (SpineDrawerValuePair)menuItemObject;
-			clickedItem.property.stringValue = clickedItem.str;
-			clickedItem.property.serializedObject.ApplyModifiedProperties();
+			var serializedProperty = clickedItem.property;
+			if (serializedProperty.serializedObject.isEditingMultipleObjects) serializedProperty.stringValue = "oaifnoiasf°ñ123526"; // HACK: to trigger change on multi-editing.
+			serializedProperty.stringValue = clickedItem.stringValue;
+			serializedProperty.serializedObject.ApplyModifiedProperties();
 		}
 
 		public override float GetPropertyHeight (SerializedProperty property, GUIContent label) {
@@ -167,9 +175,8 @@ namespace Spine.Unity.Editor {
 		protected override Texture2D Icon {	get { return SpineEditorUtilities.Icons.slot; } }
 
 		protected override void PopulateMenu (GenericMenu menu, SerializedProperty property, SpineSlot targetAttribute, SkeletonData data) {
-
 			if (TargetAttribute.includeNone)
-				menu.AddItem(new GUIContent(NoneString), string.IsNullOrEmpty(property.stringValue), HandleSelect, new SpineDrawerValuePair(string.Empty, property));
+				menu.AddItem(new GUIContent(NoneString), !property.hasMultipleDifferentValues && string.IsNullOrEmpty(property.stringValue), HandleSelect, new SpineDrawerValuePair(string.Empty, property));
 
 			for (int i = 0; i < data.Slots.Count; i++) {
 				string name = data.Slots.Items[i].Name;
@@ -186,7 +193,7 @@ namespace Spine.Unity.Editor {
 							var bbAttachment = attachment as BoundingBoxAttachment;
 							if (bbAttachment != null) {
 								string menuLabel = bbAttachment.IsWeighted() ? name + " (!)" : name;
-								menu.AddItem(new GUIContent(menuLabel), name == property.stringValue, HandleSelect, new SpineDrawerValuePair(name, property));
+								menu.AddItem(new GUIContent(menuLabel), !property.hasMultipleDifferentValues && name == property.stringValue, HandleSelect, new SpineDrawerValuePair(name, property));
 								hasBoundingBox = true;
 								break;
 							}
@@ -196,7 +203,7 @@ namespace Spine.Unity.Editor {
 							menu.AddDisabledItem(new GUIContent(name));
 
 					} else {
-						menu.AddItem(new GUIContent(name), name == property.stringValue, HandleSelect, new SpineDrawerValuePair(name, property));
+						menu.AddItem(new GUIContent(name), !property.hasMultipleDifferentValues && name == property.stringValue, HandleSelect, new SpineDrawerValuePair(name, property));
 					}
 
 				}
@@ -208,28 +215,33 @@ namespace Spine.Unity.Editor {
 
 	[CustomPropertyDrawer(typeof(SpineSkin))]
 	public class SpineSkinDrawer : SpineTreeItemDrawerBase<SpineSkin> {
+		const string DefaultSkinName = "default";
 
 		protected override Texture2D Icon {	get { return SpineEditorUtilities.Icons.skin; } }
 
-		public static void GetSkinMenuItems (SkeletonData data, List<string> animationNames, List<GUIContent> menuItems, bool includeNone = true) {
+		internal override string NoneString { get { return TargetAttribute.defaultAsEmptyString ? DefaultSkinName : NoneStringConstant; } }
+
+		public static void GetSkinMenuItems (SkeletonData data, List<string> outputNames, List<GUIContent> outputMenuItems, bool includeNone = true) {
 			if (data == null) return;
+			if (outputNames == null) return;
+			if (outputMenuItems == null) return;
 
 			var skins = data.Skins;
 
-			animationNames.Clear();
-			menuItems.Clear();
+			outputNames.Clear();
+			outputMenuItems.Clear();
 
 			var icon = SpineEditorUtilities.Icons.skin;
 
 			if (includeNone) {
-				animationNames.Add("");
-				menuItems.Add(new GUIContent(NoneString, icon));
+				outputNames.Add("");
+				outputMenuItems.Add(new GUIContent(NoneStringConstant, icon));
 			}
 
 			foreach (var s in skins) {
-				var skinName = s.Name;
-				animationNames.Add(skinName);
-				menuItems.Add(new GUIContent(skinName, icon));
+				string skinName = s.Name;
+				outputNames.Add(skinName);
+				outputMenuItems.Add(new GUIContent(skinName, icon));
 			}
 		}
 
@@ -239,9 +251,13 @@ namespace Spine.Unity.Editor {
 
 			for (int i = 0; i < data.Skins.Count; i++) {
 				string name = data.Skins.Items[i].Name;
-				if (name.StartsWith(targetAttribute.startsWith, StringComparison.Ordinal))
-					menu.AddItem(new GUIContent(name), name == property.stringValue, HandleSelect, new SpineDrawerValuePair(name, property));
-			}
+				if (name.StartsWith(targetAttribute.startsWith, StringComparison.Ordinal)) {
+					bool isDefault = string.Equals(name, DefaultSkinName, StringComparison.Ordinal);
+					string choiceValue = TargetAttribute.defaultAsEmptyString && isDefault ? string.Empty : name;
+					menu.AddItem(new GUIContent(name), !property.hasMultipleDifferentValues && choiceValue == property.stringValue, HandleSelect, new SpineDrawerValuePair(choiceValue, property));
+				}
+					
+			}			
 		}
 
 	}
@@ -251,23 +267,25 @@ namespace Spine.Unity.Editor {
 
 		protected override Texture2D Icon {	get { return SpineEditorUtilities.Icons.animation; } }
 
-		public static void GetAnimationMenuItems (SkeletonData data, List<string> animationNames, List<GUIContent> menuItems, bool includeNone = true) {
+		public static void GetAnimationMenuItems (SkeletonData data, List<string> outputNames, List<GUIContent> outputMenuItems, bool includeNone = true) {
 			if (data == null) return;
+			if (outputNames == null) return;
+			if (outputMenuItems == null) return;
 
 			var animations = data.Animations;
 
-			animationNames.Clear();
-			menuItems.Clear();
+			outputNames.Clear();
+			outputMenuItems.Clear();
 
 			if (includeNone) {
-				animationNames.Add("");
-				menuItems.Add(new GUIContent(NoneString, SpineEditorUtilities.Icons.animation));
+				outputNames.Add("");
+				outputMenuItems.Add(new GUIContent(NoneStringConstant, SpineEditorUtilities.Icons.animation));
 			}
 
 			foreach (var a in animations) {
-				var animationName = a.Name;
-				animationNames.Add(animationName);
-				menuItems.Add(new GUIContent(animationName, SpineEditorUtilities.Icons.animation));
+				string animationName = a.Name;
+				outputNames.Add(animationName);
+				outputMenuItems.Add(new GUIContent(animationName, SpineEditorUtilities.Icons.animation));
 			}
 		}
 
@@ -275,12 +293,12 @@ namespace Spine.Unity.Editor {
 			var animations = skeletonDataAsset.GetAnimationStateData().SkeletonData.Animations;
 
 			if (TargetAttribute.includeNone)
-				menu.AddItem(new GUIContent(NoneString), string.IsNullOrEmpty(property.stringValue), HandleSelect, new SpineDrawerValuePair(string.Empty, property));
+				menu.AddItem(new GUIContent(NoneString), !property.hasMultipleDifferentValues && string.IsNullOrEmpty(property.stringValue), HandleSelect, new SpineDrawerValuePair(string.Empty, property));
 
 			for (int i = 0; i < animations.Count; i++) {
 				string name = animations.Items[i].Name;
 				if (name.StartsWith(targetAttribute.startsWith, StringComparison.Ordinal))
-					menu.AddItem(new GUIContent(name), name == property.stringValue, HandleSelect, new SpineDrawerValuePair(name, property));
+					menu.AddItem(new GUIContent(name), !property.hasMultipleDifferentValues && name == property.stringValue, HandleSelect, new SpineDrawerValuePair(name, property));
 			}
 		}
 
@@ -301,7 +319,7 @@ namespace Spine.Unity.Editor {
 
 			if (includeNone) {
 				eventNames.Add("");
-				menuItems.Add(new GUIContent(NoneString, SpineEditorUtilities.Icons.userEvent));
+				menuItems.Add(new GUIContent(NoneStringConstant, SpineEditorUtilities.Icons.userEvent));
 			}
 
 			foreach (var a in animations) {
@@ -315,12 +333,12 @@ namespace Spine.Unity.Editor {
 			var events = skeletonDataAsset.GetSkeletonData(false).Events;
 
 			if (TargetAttribute.includeNone)
-				menu.AddItem(new GUIContent(NoneString), string.IsNullOrEmpty(property.stringValue), HandleSelect, new SpineDrawerValuePair(string.Empty, property));
+				menu.AddItem(new GUIContent(NoneString), !property.hasMultipleDifferentValues && string.IsNullOrEmpty(property.stringValue), HandleSelect, new SpineDrawerValuePair(string.Empty, property));
 
 			for (int i = 0; i < events.Count; i++) {
 				string name = events.Items[i].Name;
 				if (name.StartsWith(targetAttribute.startsWith, StringComparison.Ordinal))
-					menu.AddItem(new GUIContent(name), name == property.stringValue, HandleSelect, new SpineDrawerValuePair(name, property));
+					menu.AddItem(new GUIContent(name), !property.hasMultipleDifferentValues && name == property.stringValue, HandleSelect, new SpineDrawerValuePair(name, property));
 			}
 		}
 
@@ -335,12 +353,12 @@ namespace Spine.Unity.Editor {
 			var constraints = skeletonDataAsset.GetSkeletonData(false).IkConstraints;
 
 			if (TargetAttribute.includeNone)
-				menu.AddItem(new GUIContent(NoneString), string.IsNullOrEmpty(property.stringValue), HandleSelect, new SpineDrawerValuePair(string.Empty, property));
+				menu.AddItem(new GUIContent(NoneString), !property.hasMultipleDifferentValues && string.IsNullOrEmpty(property.stringValue), HandleSelect, new SpineDrawerValuePair(string.Empty, property));
 
 			for (int i = 0; i < constraints.Count; i++) {
 				string name = constraints.Items[i].Name;
 				if (name.StartsWith(targetAttribute.startsWith, StringComparison.Ordinal))
-					menu.AddItem(new GUIContent(name), name == property.stringValue, HandleSelect, new SpineDrawerValuePair(name, property));
+					menu.AddItem(new GUIContent(name), !property.hasMultipleDifferentValues && name == property.stringValue, HandleSelect, new SpineDrawerValuePair(name, property));
 			}
 		}
 
@@ -355,12 +373,12 @@ namespace Spine.Unity.Editor {
 			var constraints = skeletonDataAsset.GetSkeletonData(false).TransformConstraints;
 
 			if (TargetAttribute.includeNone)
-				menu.AddItem(new GUIContent(NoneString), string.IsNullOrEmpty(property.stringValue), HandleSelect, new SpineDrawerValuePair(string.Empty, property));
+				menu.AddItem(new GUIContent(NoneString), !property.hasMultipleDifferentValues && string.IsNullOrEmpty(property.stringValue), HandleSelect, new SpineDrawerValuePair(string.Empty, property));
 
 			for (int i = 0; i < constraints.Count; i++) {
 				string name = constraints.Items[i].Name;
 				if (name.StartsWith(targetAttribute.startsWith, StringComparison.Ordinal))
-					menu.AddItem(new GUIContent(name), name == property.stringValue, HandleSelect, new SpineDrawerValuePair(name, property));
+					menu.AddItem(new GUIContent(name), !property.hasMultipleDifferentValues && name == property.stringValue, HandleSelect, new SpineDrawerValuePair(name, property));
 			}
 		}
 	}
@@ -374,12 +392,12 @@ namespace Spine.Unity.Editor {
 			var constraints = skeletonDataAsset.GetSkeletonData(false).PathConstraints;
 
 			if (TargetAttribute.includeNone)
-				menu.AddItem(new GUIContent(NoneString), string.IsNullOrEmpty(property.stringValue), HandleSelect, new SpineDrawerValuePair(string.Empty, property));
+				menu.AddItem(new GUIContent(NoneString), !property.hasMultipleDifferentValues && string.IsNullOrEmpty(property.stringValue), HandleSelect, new SpineDrawerValuePair(string.Empty, property));
 
 			for (int i = 0; i < constraints.Count; i++) {
 				string name = constraints.Items[i].Name;
 				if (name.StartsWith(targetAttribute.startsWith, StringComparison.Ordinal))
-					menu.AddItem(new GUIContent(name), name == property.stringValue, HandleSelect, new SpineDrawerValuePair(name, property));
+					menu.AddItem(new GUIContent(name), !property.hasMultipleDifferentValues && name == property.stringValue, HandleSelect, new SpineDrawerValuePair(name, property));
 			}
 		}
 	}
@@ -422,7 +440,7 @@ namespace Spine.Unity.Editor {
 			menu.AddSeparator("");
 			if (TargetAttribute.includeNone) {
 				const string NullAttachmentName = "";
-				menu.AddItem(new GUIContent("Null"), property.stringValue == NullAttachmentName, HandleSelect, new SpineDrawerValuePair(NullAttachmentName, property));
+				menu.AddItem(new GUIContent("Null"), !property.hasMultipleDifferentValues && property.stringValue == NullAttachmentName, HandleSelect, new SpineDrawerValuePair(NullAttachmentName, property));
 				menu.AddSeparator("");
 			}
 
@@ -465,7 +483,7 @@ namespace Spine.Unity.Editor {
 						if (targetAttribute.placeholdersOnly && !placeholderNames.Contains(attachmentPath)) {
 							menu.AddDisabledItem(new GUIContent(menuPath));
 						} else {
-							menu.AddItem(new GUIContent(menuPath), name == property.stringValue, HandleSelect, new SpineDrawerValuePair(name, property));
+							menu.AddItem(new GUIContent(menuPath), !property.hasMultipleDifferentValues && name == property.stringValue, HandleSelect, new SpineDrawerValuePair(name, property));
 						}
 					}
 
@@ -485,12 +503,12 @@ namespace Spine.Unity.Editor {
 			menu.AddSeparator("");
 
 			if (TargetAttribute.includeNone)
-				menu.AddItem(new GUIContent(NoneString), string.IsNullOrEmpty(property.stringValue), HandleSelect, new SpineDrawerValuePair(string.Empty, property));
+				menu.AddItem(new GUIContent(NoneString), !property.hasMultipleDifferentValues && string.IsNullOrEmpty(property.stringValue), HandleSelect, new SpineDrawerValuePair(string.Empty, property));
 
 			for (int i = 0; i < data.Bones.Count; i++) {
 				string name = data.Bones.Items[i].Name;
 				if (name.StartsWith(targetAttribute.startsWith, StringComparison.Ordinal))
-					menu.AddItem(new GUIContent(name), name == property.stringValue, HandleSelect, new SpineDrawerValuePair(name, property));
+					menu.AddItem(new GUIContent(name), !property.hasMultipleDifferentValues && name == property.stringValue, HandleSelect, new SpineDrawerValuePair(name, property));
 			}
 		}
 
@@ -540,7 +558,7 @@ namespace Spine.Unity.Editor {
 
 			for (int i = 0; i < regions.Count; i++) {
 				string name = regions[i].name;
-				menu.AddItem(new GUIContent(name), name == property.stringValue, HandleSelect, new SpineDrawerValuePair(name, property));
+				menu.AddItem(new GUIContent(name), !property.hasMultipleDifferentValues && name == property.stringValue, HandleSelect, new SpineDrawerValuePair(name, property));
 			}
 
 			menu.ShowAsContext();
@@ -548,7 +566,7 @@ namespace Spine.Unity.Editor {
 
 		static void HandleSelect (object val) {
 			var pair = (SpineDrawerValuePair)val;
-			pair.property.stringValue = pair.str;
+			pair.property.stringValue = pair.stringValue;
 			pair.property.serializedObject.ApplyModifiedProperties();
 		}
 

+ 44 - 38
spine-unity/Assets/Spine/Editor/spine-unity/Editor/SpineEditorUtilities.cs

@@ -1165,15 +1165,13 @@ namespace Spine.Unity.Editor {
 
 			internal static readonly List<SkeletonComponentSpawnType> additionalSpawnTypes = new List<SkeletonComponentSpawnType>();
 
-			public static void IngestAdvancedRenderSettings (SkeletonRenderer skeletonRenderer) {
+			public static void TryInitializeSkeletonRendererSettings (SkeletonRenderer skeletonRenderer, Skin skin = null) {
 				const string PMAShaderQuery = "Spine/Skeleton";
 				const string TintBlackShaderQuery = "Tint Black";
 
-				if (skeletonRenderer == null)
-					return;
+				if (skeletonRenderer == null) return;
 				var skeletonDataAsset = skeletonRenderer.skeletonDataAsset;
-				if (skeletonDataAsset == null)
-					return;
+				if (skeletonDataAsset == null) return;
 
 				bool pmaVertexColors = false;
 				bool tintBlack = false;
@@ -1199,6 +1197,14 @@ namespace Spine.Unity.Editor {
 
 				skeletonRenderer.pmaVertexColors = pmaVertexColors;
 				skeletonRenderer.tintBlack = tintBlack;
+				skeletonRenderer.zSpacing = SpineEditorUtilities.Preferences.defaultZSpacing;
+
+				var data = skeletonDataAsset.GetSkeletonData(false);
+				bool noSkins = data.DefaultSkin == null && (data.Skins == null || data.Skins.Count == 0); // Support attachmentless/skinless SkeletonData.
+				skin = skin ?? data.DefaultSkin ?? (noSkins ? null : data.Skins.Items[0]);
+				if (skin != null && skin != data.DefaultSkin) {
+					skeletonRenderer.initialSkinName = skin.Name;
+				}
 			}
 
 			public static SkeletonAnimation InstantiateSkeletonAnimation (SkeletonDataAsset skeletonDataAsset, string skinName, bool destroyInvalid = true) {
@@ -1227,8 +1233,9 @@ namespace Spine.Unity.Editor {
 				GameObject go = new GameObject(spineGameObjectName, typeof(MeshFilter), typeof(MeshRenderer), typeof(SkeletonAnimation));
 				SkeletonAnimation newSkeletonAnimation = go.GetComponent<SkeletonAnimation>();
 				newSkeletonAnimation.skeletonDataAsset = skeletonDataAsset;
-				IngestAdvancedRenderSettings(newSkeletonAnimation);
+				TryInitializeSkeletonRendererSettings(newSkeletonAnimation, skin);
 
+				// Initialize
 				try {
 					newSkeletonAnimation.Initialize(false);
 				} catch (System.Exception e) {
@@ -1239,16 +1246,6 @@ namespace Spine.Unity.Editor {
 					throw e;
 				}
 
-				// Set Defaults
-				bool noSkins = data.DefaultSkin == null && (data.Skins == null || data.Skins.Count == 0); // Support attachmentless/skinless SkeletonData.
-				skin = skin ?? data.DefaultSkin ?? (noSkins ? null : data.Skins.Items[0]);
-				if (skin != null) {
-					newSkeletonAnimation.initialSkinName = skin.Name;
-					newSkeletonAnimation.skeleton.SetSkin(skin);
-				}
-
-				newSkeletonAnimation.zSpacing = SpineEditorUtilities.Preferences.defaultZSpacing;
-
 				newSkeletonAnimation.skeleton.Update(0);
 				newSkeletonAnimation.state.Update(0);
 				newSkeletonAnimation.state.Apply(newSkeletonAnimation.skeleton);
@@ -1274,7 +1271,22 @@ namespace Spine.Unity.Editor {
 				return InstantiateSkeletonMecanim(skeletonDataAsset, skeletonDataAsset.GetSkeletonData(true).FindSkin(skinName));
 			}
 
-			public static SkeletonMecanim InstantiateSkeletonMecanim (SkeletonDataAsset skeletonDataAsset, Skin skin = null) {
+			public static SkeletonMecanim InstantiateSkeletonMecanim (SkeletonDataAsset skeletonDataAsset, Skin skin = null, bool destroyInvalid = true) {
+				SkeletonData data = skeletonDataAsset.GetSkeletonData(true);
+
+				if (data == null) {
+					for (int i = 0; i < skeletonDataAsset.atlasAssets.Length; i++) {
+						string reloadAtlasPath = AssetDatabase.GetAssetPath(skeletonDataAsset.atlasAssets[i]);
+						skeletonDataAsset.atlasAssets[i] = (AtlasAssetBase)AssetDatabase.LoadAssetAtPath(reloadAtlasPath, typeof(AtlasAssetBase));
+					}
+					data = skeletonDataAsset.GetSkeletonData(false);
+				}
+
+				if (data == null) {
+					Debug.LogWarning("InstantiateSkeletonMecanim tried to instantiate a skeleton from an invalid SkeletonDataAsset.");
+					return null;
+				}
+
 				string spineGameObjectName = string.Format("Spine Mecanim GameObject ({0})", skeletonDataAsset.name.Replace("_SkeletonData", ""));
 				GameObject go = new GameObject(spineGameObjectName, typeof(MeshFilter), typeof(MeshRenderer), typeof(Animator), typeof(SkeletonMecanim));
 
@@ -1285,32 +1297,26 @@ namespace Spine.Unity.Editor {
 
 				go.GetComponent<Animator>().runtimeAnimatorController = skeletonDataAsset.controller;
 
-				SkeletonMecanim anim = go.GetComponent<SkeletonMecanim>();
-				anim.skeletonDataAsset = skeletonDataAsset;
-				IngestAdvancedRenderSettings(anim);
+				SkeletonMecanim newSkeletonMecanim = go.GetComponent<SkeletonMecanim>();
+				newSkeletonMecanim.skeletonDataAsset = skeletonDataAsset;
+				TryInitializeSkeletonRendererSettings(newSkeletonMecanim, skin);
 
-				SkeletonData data = skeletonDataAsset.GetSkeletonData(true);
-				if (data == null) {
-					for (int i = 0; i < skeletonDataAsset.atlasAssets.Length; i++) {
-						string reloadAtlasPath = AssetDatabase.GetAssetPath(skeletonDataAsset.atlasAssets[i]);
-						skeletonDataAsset.atlasAssets[i] = (AtlasAssetBase)AssetDatabase.LoadAssetAtPath(reloadAtlasPath, typeof(AtlasAssetBase));
+				// Initialize
+				try {
+					newSkeletonMecanim.Initialize(false);
+				} catch (System.Exception e) {
+					if (destroyInvalid) {
+						Debug.LogWarning("Editor-instantiated SkeletonAnimation threw an Exception. Destroying GameObject to prevent orphaned GameObject.");
+						GameObject.DestroyImmediate(go);
 					}
-					data = skeletonDataAsset.GetSkeletonData(true);
+					throw e;
 				}
 
-				// Set defaults
-				skin = skin ?? data.DefaultSkin ?? data.Skins.Items[0];
-				anim.zSpacing = SpineEditorUtilities.Preferences.defaultZSpacing;
-
-				anim.Initialize(false);
-				anim.skeleton.SetSkin(skin);
-				anim.initialSkinName = skin.Name;
-
-				anim.skeleton.Update(0);
-				anim.skeleton.UpdateWorldTransform();
-				anim.LateUpdate();
+				newSkeletonMecanim.skeleton.Update(0);
+				newSkeletonMecanim.skeleton.UpdateWorldTransform();
+				newSkeletonMecanim.LateUpdate();
 
-				return anim;
+				return newSkeletonMecanim;
 			}
 			#endif
 			#endregion

+ 3 - 8
spine-unity/Assets/Spine/Editor/spine-unity/Editor/SpineInspectorUtility.cs

@@ -165,8 +165,9 @@ namespace Spine.Unity.Editor {
 		public static GUIStyle GrayMiniLabel {
 			get {
 				if (grayMiniLabel == null) {
-					grayMiniLabel = new GUIStyle(EditorStyles.centeredGreyMiniLabel);
-					grayMiniLabel.alignment = TextAnchor.UpperLeft;
+					grayMiniLabel = new GUIStyle(EditorStyles.centeredGreyMiniLabel) {
+						alignment = TextAnchor.UpperLeft
+					};
 				}
 				return grayMiniLabel;
 			}
@@ -340,12 +341,6 @@ namespace Spine.Unity.Editor {
 			public SerializedSortingProperties (Renderer r) : this(new SerializedObject(r)) {}
 			public SerializedSortingProperties (Object[] renderers) : this(new SerializedObject(renderers)) {}
 
-			/// <summary>
-			/// Initializes a new instance of the
-			/// <see cref="Spine.Unity.Editor.SpineInspectorUtility.SerializedSortingProperties"/> struct.
-			/// </summary>
-			/// <param name="rendererSerializedObject">SerializedObject of the renderer. Use 
-			/// <see cref="Spine.Unity.Editor.SpineInspectorUtility.GetRenderersSerializedObject"/> to easily generate this.</param>
 			public SerializedSortingProperties (SerializedObject rendererSerializedObject) {
 				renderer = rendererSerializedObject;
 				sortingLayerID = renderer.FindProperty("m_SortingLayerID");

+ 6 - 8
spine-unity/Assets/Spine/Editor/spine-unity/SkeletonUtility/Editor/SkeletonUtilityBoneInspector.cs

@@ -53,7 +53,6 @@ namespace Spine.Unity.Editor {
 		bool canCreateHingeChain = false;
 
 		Dictionary<Slot, List<BoundingBoxAttachment>> boundingBoxTable = new Dictionary<Slot, List<BoundingBoxAttachment>>();
-		//string currentSkinName = "";
 
 		void OnEnable () {
 			mode = this.serializedObject.FindProperty("mode");
@@ -81,7 +80,6 @@ namespace Spine.Unity.Editor {
 			if (skeleton.Skin == null)
 				skin = skeleton.Data.DefaultSkin;
 
-			//currentSkinName = skin.Name;
 			for(int i = 0; i < slotCount; i++){
 				Slot slot = skeletonUtility.skeletonRenderer.skeleton.Slots.Items[i];
 				if (slot.Bone == utilityBone.bone) {
@@ -103,7 +101,7 @@ namespace Spine.Unity.Editor {
 
 		void EvaluateFlags () {
 			utilityBone = (SkeletonUtilityBone)target;
-			skeletonUtility = utilityBone.skeletonUtility;
+			skeletonUtility = utilityBone.hierarchy;
 
 			if (Selection.objects.Length == 1) {
 				containsFollows = utilityBone.mode == SkeletonUtilityBone.Mode.Follow;
@@ -146,7 +144,7 @@ namespace Spine.Unity.Editor {
 				using (new GUILayout.HorizontalScope()) {
 					EditorGUILayout.PrefixLabel("Bone");
 					if (GUILayout.Button(str, EditorStyles.popup)) {
-						BoneSelectorContextMenu(str, ((SkeletonUtilityBone)target).skeletonUtility.skeletonRenderer.skeleton.Bones, "<None>", TargetBoneSelected);
+						BoneSelectorContextMenu(str, ((SkeletonUtilityBone)target).hierarchy.skeletonRenderer.skeleton.Bones, "<None>", TargetBoneSelected);
 					}
 				}
 			}
@@ -166,7 +164,7 @@ namespace Spine.Unity.Editor {
 			using (new GUILayout.HorizontalScope()) {
 				EditorGUILayout.Space();
 				using (new EditorGUI.DisabledGroupScope(multiObject || !utilityBone.valid || utilityBone.bone == null || utilityBone.bone.Children.Count == 0)) {
-					if (GUILayout.Button(SpineInspectorUtility.TempContent("Add Child", Icons.bone), GUILayout.MinWidth(120), GUILayout.Height(24)))
+					if (GUILayout.Button(SpineInspectorUtility.TempContent("Add Child Bone", Icons.bone), GUILayout.MinWidth(120), GUILayout.Height(24)))
 						BoneSelectorContextMenu("", utilityBone.bone.Children, "<Recursively>", SpawnChildBoneSelected);
 				}
 				using (new EditorGUI.DisabledGroupScope(multiObject || !utilityBone.valid || utilityBone.bone == null || containsOverrides)) {
@@ -260,12 +258,12 @@ namespace Spine.Unity.Editor {
 					GameObject go = skeletonUtility.SpawnBoneRecursively(bone, utilityBone.transform, utilityBone.mode, utilityBone.position, utilityBone.rotation, utilityBone.scale);
 					SkeletonUtilityBone[] newUtilityBones = go.GetComponentsInChildren<SkeletonUtilityBone>();
 					foreach (SkeletonUtilityBone utilBone in newUtilityBones)
-						SkeletonUtilityInspector.AttachIcon(utilBone);
+						SkeletonGameObjectsInspector.AttachIcon(utilBone);
 				}
 			} else {
 				var bone = (Bone)obj;
 				GameObject go = skeletonUtility.SpawnBone(bone, utilityBone.transform, utilityBone.mode, utilityBone.position, utilityBone.rotation, utilityBone.scale);
-				SkeletonUtilityInspector.AttachIcon(go.GetComponent<SkeletonUtilityBone>());
+				SkeletonGameObjectsInspector.AttachIcon(go.GetComponent<SkeletonUtilityBone>());
 				Selection.activeGameObject = go;
 				EditorGUIUtility.PingObject(go);
 			}
@@ -274,7 +272,7 @@ namespace Spine.Unity.Editor {
 		void SpawnOverride () {
 			GameObject go = skeletonUtility.SpawnBone(utilityBone.bone, utilityBone.transform.parent, SkeletonUtilityBone.Mode.Override, utilityBone.position, utilityBone.rotation, utilityBone.scale);
 			go.name = go.name + " [Override]";
-			SkeletonUtilityInspector.AttachIcon(go.GetComponent<SkeletonUtilityBone>());
+			SkeletonGameObjectsInspector.AttachIcon(go.GetComponent<SkeletonUtilityBone>());
 			Selection.activeGameObject = go;
 			EditorGUIUtility.PingObject(go);
 		}

+ 30 - 27
spine-unity/Assets/Spine/Editor/spine-unity/SkeletonUtility/Editor/SkeletonUtilityInspector.cs

@@ -41,18 +41,17 @@ namespace Spine.Unity.Editor {
 	using Icons = SpineEditorUtilities.Icons;
 
 	[CustomEditor(typeof(SkeletonUtility))]
-	public class SkeletonUtilityInspector : UnityEditor.Editor {
+	public class SkeletonGameObjectsInspector : UnityEditor.Editor {
 
-		SkeletonUtility skeletonUtility;
+		SkeletonUtility skeletonGameObjects;
 		Skeleton skeleton;
 		SkeletonRenderer skeletonRenderer;
 		bool isPrefab;
-
-		GUIContent SpawnHierarchyButtonLabel = new GUIContent("Spawn Hierarchy", Icons.skeleton);
+		readonly GUIContent SpawnHierarchyButtonLabel = new GUIContent("Spawn Hierarchy", Icons.skeleton);
 
 		void OnEnable () {
-			skeletonUtility = (SkeletonUtility)target;
-			skeletonRenderer = skeletonUtility.GetComponent<SkeletonRenderer>();
+			skeletonGameObjects = (SkeletonUtility)target;
+			skeletonRenderer = skeletonGameObjects.GetComponent<SkeletonRenderer>();
 			skeleton = skeletonRenderer.Skeleton;
 
 			if (skeleton == null) {
@@ -77,9 +76,13 @@ namespace Spine.Unity.Editor {
 				return;	
 			}
 
-			skeletonUtility.boneRoot = (Transform)EditorGUILayout.ObjectField("Bone Root", skeletonUtility.boneRoot, typeof(Transform), true);
+			EditorGUILayout.PropertyField(serializedObject.FindProperty("boneRoot"), SpineInspectorUtility.TempContent("Skeleton Root"));
+
+			bool hasRootBone = skeletonGameObjects.boneRoot != null;
+
+			if (!hasRootBone)
+				EditorGUILayout.HelpBox("No hierarchy found. Use Spawn Hierarchy to generate GameObjects for bones.", MessageType.Info);
 
-			bool hasRootBone = skeletonUtility.boneRoot != null;
 			using (new EditorGUI.DisabledGroupScope(hasRootBone)) {
 				if (SpineInspectorUtility.LargeCenteredButton(SpawnHierarchyButtonLabel))
 					SpawnHierarchyContextMenu();
@@ -87,37 +90,37 @@ namespace Spine.Unity.Editor {
 
 			if (hasRootBone) {
 				if (SpineInspectorUtility.CenteredButton(new GUIContent("Remove Hierarchy"))) {
-					Undo.RegisterCompleteObjectUndo(skeletonUtility, "Remove Hierarchy");
-					Undo.DestroyObjectImmediate(skeletonUtility.boneRoot.gameObject);
-					skeletonUtility.boneRoot = null;
+					Undo.RegisterCompleteObjectUndo(skeletonGameObjects, "Remove Hierarchy");
+					Undo.DestroyObjectImmediate(skeletonGameObjects.boneRoot.gameObject);
+					skeletonGameObjects.boneRoot = null;
 				}
 			}
 		}
 
 		void SpawnHierarchyContextMenu () {
-			GenericMenu menu = new GenericMenu();
+			var menu = new GenericMenu();
 
-			menu.AddItem(new GUIContent("Follow"), false, SpawnFollowHierarchy);
+			menu.AddItem(new GUIContent("Follow all bones"), false, SpawnFollowHierarchy);
 			menu.AddItem(new GUIContent("Follow (Root Only)"), false, SpawnFollowHierarchyRootOnly);
 			menu.AddSeparator("");
-			menu.AddItem(new GUIContent("Override"), false, SpawnOverrideHierarchy);
+			menu.AddItem(new GUIContent("Override all bones"), false, SpawnOverrideHierarchy);
 			menu.AddItem(new GUIContent("Override (Root Only)"), false, SpawnOverrideHierarchyRootOnly);
 
 			menu.ShowAsContext();
 		}
 
-		public static void AttachIcon (SkeletonUtilityBone utilityBone) {
-			Skeleton skeleton = utilityBone.skeletonUtility.skeletonRenderer.skeleton;
-			Texture2D icon = utilityBone.bone.Data.Length == 0 ? Icons.nullBone : Icons.boneNib;
+		public static void AttachIcon (SkeletonUtilityBone boneComponent) {
+			Skeleton skeleton = boneComponent.hierarchy.skeletonRenderer.skeleton;
+			Texture2D icon = boneComponent.bone.Data.Length == 0 ? Icons.nullBone : Icons.boneNib;
 
 			foreach (IkConstraint c in skeleton.IkConstraints)
-				if (c.Target == utilityBone.bone) {
+				if (c.Target == boneComponent.bone) {
 					icon = Icons.constraintNib;
 					break;
 				}
 
 			typeof(EditorGUIUtility).InvokeMember("SetIconForObject", BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.NonPublic, null, null, new object[2] {
-				utilityBone.gameObject,
+				boneComponent.gameObject,
 				icon
 			});
 		}
@@ -131,23 +134,23 @@ namespace Spine.Unity.Editor {
 		}
 
 		void SpawnFollowHierarchy () {
-			Selection.activeGameObject = skeletonUtility.SpawnHierarchy(SkeletonUtilityBone.Mode.Follow, true, true, true);
-			AttachIconsToChildren(skeletonUtility.boneRoot);
+			Selection.activeGameObject = skeletonGameObjects.SpawnHierarchy(SkeletonUtilityBone.Mode.Follow, true, true, true);
+			AttachIconsToChildren(skeletonGameObjects.boneRoot);
 		}
 
 		void SpawnFollowHierarchyRootOnly () {
-			Selection.activeGameObject = skeletonUtility.SpawnRoot(SkeletonUtilityBone.Mode.Follow, true, true, true);
-			AttachIconsToChildren(skeletonUtility.boneRoot);
+			Selection.activeGameObject = skeletonGameObjects.SpawnRoot(SkeletonUtilityBone.Mode.Follow, true, true, true);
+			AttachIconsToChildren(skeletonGameObjects.boneRoot);
 		}
 
 		void SpawnOverrideHierarchy () {
-			Selection.activeGameObject = skeletonUtility.SpawnHierarchy(SkeletonUtilityBone.Mode.Override, true, true, true);
-			AttachIconsToChildren(skeletonUtility.boneRoot);
+			Selection.activeGameObject = skeletonGameObjects.SpawnHierarchy(SkeletonUtilityBone.Mode.Override, true, true, true);
+			AttachIconsToChildren(skeletonGameObjects.boneRoot);
 		}
 
 		void SpawnOverrideHierarchyRootOnly () {
-			Selection.activeGameObject = skeletonUtility.SpawnRoot(SkeletonUtilityBone.Mode.Override, true, true, true);
-			AttachIconsToChildren(skeletonUtility.boneRoot);
+			Selection.activeGameObject = skeletonGameObjects.SpawnRoot(SkeletonUtilityBone.Mode.Override, true, true, true);
+			AttachIconsToChildren(skeletonGameObjects.boneRoot);
 		}
 	}
 

+ 61 - 51
spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/BlendModeMaterialsAsset.cs

@@ -52,67 +52,77 @@ namespace Spine.Unity {
 		public static void ApplyMaterials (SkeletonData skeletonData, Material multiplyTemplate, Material screenTemplate, Material additiveTemplate, bool includeAdditiveSlots) {
 			if (skeletonData == null) throw new ArgumentNullException("skeletonData");
 
-			var atlasPageMaterialCache = new Dictionary<KeyValuePair<AtlasPage, Material>, AtlasPage>();
-			var attachmentBuffer = new List<Attachment>();
-			var slotsItems = skeletonData.Slots.Items;
-			for (int i = 0, slotCount = skeletonData.Slots.Count; i < slotCount; i++) {
-				var slot = slotsItems[i];
-				if (slot.blendMode == BlendMode.Normal) continue;
-				if (!includeAdditiveSlots && slot.blendMode == BlendMode.Additive) continue;
-
-				attachmentBuffer.Clear();
-				foreach (var skin in skeletonData.Skins)
-					skin.FindAttachmentsForSlot(i, attachmentBuffer);
-
-				Material templateMaterial = null;
-				switch (slot.blendMode) {
-					case BlendMode.Multiply:
-						templateMaterial = multiplyTemplate;
-						break;
-					case BlendMode.Screen:
-						templateMaterial = screenTemplate;
-						break;
-					case BlendMode.Additive:
-						templateMaterial = additiveTemplate;
-						break;
-				}
-				if (templateMaterial == null) continue;
+			using (var materialCache = new AtlasMaterialCache()) {
+				var attachmentBuffer = new List<Attachment>();
+				var slotsItems = skeletonData.Slots.Items;
+				for (int i = 0, slotCount = skeletonData.Slots.Count; i < slotCount; i++) {
+					var slot = slotsItems[i];
+					if (slot.blendMode == BlendMode.Normal) continue;
+					if (!includeAdditiveSlots && slot.blendMode == BlendMode.Additive) continue;
+
+					attachmentBuffer.Clear();
+					foreach (var skin in skeletonData.Skins)
+						skin.FindAttachmentsForSlot(i, attachmentBuffer);
+
+					Material templateMaterial = null;
+					switch (slot.blendMode) {
+						case BlendMode.Multiply:
+							templateMaterial = multiplyTemplate;
+							break;
+						case BlendMode.Screen:
+							templateMaterial = screenTemplate;
+							break;
+						case BlendMode.Additive:
+							templateMaterial = additiveTemplate;
+							break;
+					}
+					if (templateMaterial == null) continue;
 
-				foreach (var attachment in attachmentBuffer) {
-					var renderableAttachment = attachment as IHasRendererObject;
-					if (renderableAttachment != null) {
-						renderableAttachment.RendererObject = AtlasRegionCloneWithMaterial((AtlasRegion)renderableAttachment.RendererObject, templateMaterial, atlasPageMaterialCache);
+					foreach (var attachment in attachmentBuffer) {
+						var renderableAttachment = attachment as IHasRendererObject;
+						if (renderableAttachment != null) {
+							renderableAttachment.RendererObject = materialCache.CloneAtlasRegionWithMaterial((AtlasRegion)renderableAttachment.RendererObject, templateMaterial);
+						}
 					}
 				}
+
 			}
-			//atlasPageMaterialCache.Clear();
 			//attachmentBuffer.Clear();
 		}
 
-		static AtlasRegion AtlasRegionCloneWithMaterial (AtlasRegion originalRegion, Material materialTemplate, Dictionary<KeyValuePair<AtlasPage, Material>, AtlasPage> cache) {
-			var newRegion = originalRegion.Clone();
-			newRegion.page = GetAtlasPageWithMaterial(originalRegion.page, materialTemplate, cache);
-			return newRegion;
-		}
+		class AtlasMaterialCache : IDisposable {
+			readonly Dictionary<KeyValuePair<AtlasPage, Material>, AtlasPage> cache = new Dictionary<KeyValuePair<AtlasPage, Material>, AtlasPage>();
 
-		static AtlasPage GetAtlasPageWithMaterial (AtlasPage originalPage, Material materialTemplate, Dictionary<KeyValuePair<AtlasPage, Material>, AtlasPage> cache) {
-			if (originalPage == null) throw new ArgumentNullException("originalPage");
-
-			AtlasPage newPage = null;
-			var key = new KeyValuePair<AtlasPage, Material>(originalPage, materialTemplate);
-			cache.TryGetValue(key, out newPage);
-
-			if (newPage == null) {
-				newPage = originalPage.Clone();
-				var originalMaterial = originalPage.rendererObject as Material;
-				newPage.rendererObject = new Material(materialTemplate) {
-					name = originalMaterial.name + " " + materialTemplate.name,
-					mainTexture = originalMaterial.mainTexture
-				};
-				cache.Add(key, newPage);
+			/// <summary>Creates a clone of an AtlasRegion that uses different Material settings, while retaining the original texture.</summary>
+			public AtlasRegion CloneAtlasRegionWithMaterial (AtlasRegion originalRegion, Material materialTemplate) {
+				var newRegion = originalRegion.Clone();
+				newRegion.page = GetAtlasPageWithMaterial(originalRegion.page, materialTemplate);
+				return newRegion;
 			}
 
-			return newPage;
+			AtlasPage GetAtlasPageWithMaterial (AtlasPage originalPage, Material materialTemplate) {
+				if (originalPage == null) throw new ArgumentNullException("originalPage");
+
+				AtlasPage newPage = null;
+				var key = new KeyValuePair<AtlasPage, Material>(originalPage, materialTemplate);
+				cache.TryGetValue(key, out newPage);
+
+				if (newPage == null) {
+					newPage = originalPage.Clone();
+					var originalMaterial = originalPage.rendererObject as Material;
+					newPage.rendererObject = new Material(materialTemplate) {
+						name = originalMaterial.name + " " + materialTemplate.name,
+						mainTexture = originalMaterial.mainTexture
+					};
+					cache.Add(key, newPage);
+				}
+
+				return newPage;
+			}
+
+			public void Dispose () {
+				cache.Clear();
+			}
 		}
 	
 	}

+ 19 - 19
spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SkeletonDataAsset.cs

@@ -100,6 +100,13 @@ namespace Spine.Unity {
 			stateData = null;
 		}
 
+		public AnimationStateData GetAnimationStateData () {
+			if (stateData != null)
+				return stateData;
+			GetSkeletonData(false);
+			return stateData;
+		}
+
 		/// <summary>Loads, caches and returns the SkeletonData from the skeleton data file. Returns the cached SkeletonData after the first time it is called. Pass false to prevent direct errors from being logged.</summary>
 		public SkeletonData GetSkeletonData (bool quiet) {
 			if (skeletonJSON == null) {
@@ -187,6 +194,18 @@ namespace Spine.Unity {
 			FillStateData();
 		}
 
+		public void FillStateData () {
+			if (stateData != null) {
+				stateData.defaultMix = defaultMix;
+
+				for (int i = 0, n = fromAnimation.Length; i < n; i++) {
+					if (fromAnimation[i].Length == 0 || toAnimation[i].Length == 0)
+						continue;
+					stateData.SetMix(fromAnimation[i], toAnimation[i], duration[i]);
+				}
+			}
+		}
+
 		internal Atlas[] GetAtlasArray () {
 			var returnList = new System.Collections.Generic.List<Atlas>(atlasAssets.Length);
 			for (int i = 0; i < atlasAssets.Length; i++) {
@@ -215,25 +234,6 @@ namespace Spine.Unity {
 			return json.ReadSkeletonData(input);
 		}
 
-		public void FillStateData () {
-			if (stateData != null) {
-				stateData.defaultMix = defaultMix;
-
-				for (int i = 0, n = fromAnimation.Length; i < n; i++) {
-					if (fromAnimation[i].Length == 0 || toAnimation[i].Length == 0)
-						continue;
-					stateData.SetMix(fromAnimation[i], toAnimation[i], duration[i]);
-				}
-			}
-		}
-
-		public AnimationStateData GetAnimationStateData () {
-			if (stateData != null)
-				return stateData;
-			GetSkeletonData(false);
-			return stateData;
-		}
-
 	}
 
 }

+ 11 - 1
spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SkeletonDataAsset.cs.meta

@@ -1,8 +1,18 @@
 fileFormatVersion: 2
 guid: f1b3b4b945939a54ea0b23d3396115fb
+timeCreated: 1536403985
+licenseType: Pro
 MonoImporter:
   serializedVersion: 2
-  defaultReferences: []
+  defaultReferences:
+  - multiplyMaterialTemplate: {fileID: 2100000, guid: 53bf0ab317d032d418cf1252d68f51df,
+      type: 2}
+  - screenMaterialTemplate: {fileID: 2100000, guid: 73f0f46d3177c614baf0fa48d646a9be,
+      type: 2}
+  - additiveMaterialTemplate: {fileID: 2100000, guid: 4deba332d47209e4780b3c5fcf0e3745,
+      type: 2}
+  - skeletonJSON: {instanceID: 0}
+  - controller: {instanceID: 0}
   executionOrder: 0
   icon: {fileID: 2800000, guid: 68defdbc95b30a74a9ad396bfc9a2277, type: 3}
   userData: 

+ 0 - 2
spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SpineAtlasAsset.cs

@@ -115,8 +115,6 @@ namespace Spine.Unity {
 		}
 		#endregion
 
-
-
 		void Reset () {
 			Clear();
 		}

+ 1 - 3
spine-unity/Assets/Spine/Runtime/spine-unity/Components/BoneFollower.cs

@@ -65,10 +65,8 @@ namespace Spine.Unity {
 		#endregion
 
 		[NonSerialized] public bool valid;
-		/// <summary>
-		/// The bone.
-		/// </summary>
 		[NonSerialized] public Bone bone;
+
 		Transform skeletonTransform;
 		bool skeletonTransformIsParent;
 

+ 3 - 1
spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonRenderer.cs

@@ -48,6 +48,7 @@ namespace Spine.Unity {
 
 		public SkeletonDataAsset skeletonDataAsset;
 		public SkeletonDataAsset SkeletonDataAsset { get { return skeletonDataAsset; } } // ISkeletonComponent
+		[SpineSkin(defaultAsEmptyString:true)]
 		public string initialSkinName;
 		public bool initialFlipX, initialFlipY;
 
@@ -57,7 +58,6 @@ namespace Spine.Unity {
 		[System.NonSerialized] public readonly List<Slot> separatorSlots = new List<Slot>();
 
 		[Range(-0.1f, 0f)] public float zSpacing;
-		//public bool renderMeshes = true;
 		public bool useClipping = true;
 		public bool immutableTriangles = false;
 		public bool pmaVertexColors = true;
@@ -310,7 +310,9 @@ namespace Spine.Unity {
 			// STEP 3. Move the mesh data into a UnityEngine.Mesh ===========================================================================
 			var currentMesh = currentSmartMesh.mesh;
 			meshGenerator.FillVertexData(currentMesh);
+
 			rendererBuffers.UpdateSharedMaterials(workingSubmeshInstructions);
+
 			if (updateTriangles) { // Check if the triangles should also be updated.
 				meshGenerator.FillTriangles(currentMesh);
 				meshRenderer.sharedMaterials = rendererBuffers.GetUpdatedSharedMaterialsArray();

+ 0 - 2
spine-unity/Assets/Spine/Runtime/spine-unity/ISkeletonAnimation.cs

@@ -36,8 +36,6 @@ namespace Spine.Unity {
 		event UpdateBonesDelegate UpdateLocal;
 		event UpdateBonesDelegate UpdateWorld;
 		event UpdateBonesDelegate UpdateComplete;
-
-		//void LateUpdate ();
 		Skeleton Skeleton { get; }
 	}
 

+ 7 - 0
spine-unity/Assets/Spine/Runtime/spine-unity/Modules/AttachmentTools/AttachmentTools.cs

@@ -805,6 +805,12 @@ namespace Spine.Unity.Modules.AttachmentTools {
 			skin.AddAttachment(slotIndex, keyName, attachment);
 		}
 
+		/// <summary>Adds skin items from another skin. For items that already exist, the previous values are replaced.</summary>
+		public static void AddAttachments (this Skin skin, Skin otherSkin) {
+			if (otherSkin == null) return;
+			otherSkin.CopyTo(skin, true, false);
+		}
+
 		/// <summary>Gets an attachment from the skin for the specified slot index and name.</summary>
 		public static Attachment GetAttachment (this Skin skin, string slotName, string keyName, Skeleton skeleton) {
 			int slotIndex = skeleton.FindSlotIndex(slotName);
@@ -835,6 +841,7 @@ namespace Spine.Unity.Modules.AttachmentTools {
 			skin.Attachments.Clear();
 		}
 
+		//[System.Obsolete]
 		public static void Append (this Skin destination, Skin source) {
 			source.CopyTo(destination, true, false);
 		}

+ 3 - 10
spine-unity/Assets/Spine/Runtime/spine-unity/Modules/SkeletonUtility Modules/SkeletonUtilityEyeConstraint.cs

@@ -42,9 +42,7 @@ namespace Spine.Unity.Modules {
 		Vector3 centerPoint;
 
 		protected override void OnEnable () {
-			if (!Application.isPlaying)
-				return;
-
+			if (!Application.isPlaying) return;
 			base.OnEnable();
 
 			Bounds centerBounds = new Bounds(eyes[0].localPosition, Vector3.zero);
@@ -58,19 +56,14 @@ namespace Spine.Unity.Modules {
 		}
 
 		protected override void OnDisable () {
-			if (!Application.isPlaying)
-				return;
-
+			if (!Application.isPlaying) return;
 			base.OnDisable();
 		}
 
 		public override void DoUpdate () {
-
-			if (target != null)
-				targetPosition = target.position;
+			if (target != null) targetPosition = target.position;
 
 			Vector3 goal = targetPosition;
-
 			Vector3 center = transform.TransformPoint(centerPoint);
 			Vector3 dir = goal - center;
 

+ 2 - 2
spine-unity/Assets/Spine/Runtime/spine-unity/Modules/SkeletonUtility Modules/SkeletonUtilityGroundConstraint.cs

@@ -105,8 +105,8 @@ namespace Spine.Unity.Modules {
 			v.y = Mathf.Clamp(v.y, Mathf.Min(lastHitY, hitY), float.MaxValue);
 			transform.position = v;
 
-			utilBone.bone.X = transform.localPosition.x;
-			utilBone.bone.Y = transform.localPosition.y;
+			bone.bone.X = transform.localPosition.x;
+			bone.bone.Y = transform.localPosition.y;
 
 			lastHitY = hitY;
 		}

+ 24 - 4
spine-unity/Assets/Spine/Runtime/spine-unity/Modules/Timeline/SpineAnimationState/SpineAnimationStateMixerBehaviour.cs

@@ -105,6 +105,9 @@ namespace Spine.Unity.Playables {
 		}
 
 		#if SPINE_EDITMODEPOSE
+
+		AnimationState dummyAnimationState;
+
 		public void PreviewEditModePose (Playable playable, SkeletonAnimation spineComponent) {
 			if (Application.isPlaying) return;
 			if (spineComponent == null) return;
@@ -150,11 +153,28 @@ namespace Spine.Unity.Playables {
 
 				// Approximate what AnimationState might do at runtime.
 				if (fromAnimation != null && mixDuration > 0 && toClipTime < mixDuration) {
+					dummyAnimationState = dummyAnimationState ?? new AnimationState(spineComponent.skeletonDataAsset.GetAnimationStateData());
+
+					var toTrack = dummyAnimationState.GetCurrent(0);
+					var fromTrack = toTrack != null ? toTrack.mixingFrom : null;
+					bool isAnimationTransitionMatch = (toTrack != null && toTrack.animation == toAnimation && fromTrack != null && fromTrack.animation == fromAnimation);
+					
+					if (!isAnimationTransitionMatch) {
+						dummyAnimationState.ClearTracks();
+						fromTrack = dummyAnimationState.SetAnimation(0, fromAnimation, fromClipLoop);
+						fromTrack.AllowImmediateQueue();
+						toTrack = dummyAnimationState.SetAnimation(0, toAnimation, clipData.loop);
+					}
+
+					// Update track times.
+					fromTrack.trackTime = fromClipTime;
+					toTrack.trackTime = toClipTime;
+					toTrack.mixTime = toClipTime;
+					
+					// Apply Pose
 					skeleton.SetToSetupPose();
-					float fauxFromAlpha = (1f - toClipTime/mixDuration);
-					fauxFromAlpha = fauxFromAlpha > 0.5f ? 1f : fauxFromAlpha * 2f;  // fake value, but reduce dip.
-					fromAnimation.Apply(skeleton, 0, fromClipTime, fromClipLoop, null, fauxFromAlpha, MixBlend.Setup, MixDirection.Out); //fromAnimation.PoseSkeleton(skeleton, fromClipTime, fromClipLoop);
-					toAnimation.Apply(skeleton, 0, toClipTime, clipData.loop, null, toClipTime/mixDuration, MixBlend.Replace, MixDirection.In);
+					dummyAnimationState.Update(0);
+					dummyAnimationState.Apply(skeleton);
 				} else {
 					skeleton.SetToSetupPose();
 					toAnimation.PoseSkeleton(skeleton, toClipTime, clipData.loop);

+ 16 - 9
spine-unity/Assets/Spine/Runtime/spine-unity/Modules/Timeline/SpineSkeletonFlip/SpineSkeletonFlipMixerBehaviour.cs

@@ -38,7 +38,8 @@ using Spine.Unity;
 
 namespace Spine.Unity.Playables {
 	public class SpineSkeletonFlipMixerBehaviour : PlayableBehaviour {
-		bool defaultFlipX, defaultFlipY;
+		float originalScaleX, originalScaleY;
+		float baseScaleX, baseScaleY;
 
 		SpinePlayableHandleBase playableHandle;
 		bool m_FirstFrameHappened;
@@ -52,8 +53,10 @@ namespace Spine.Unity.Playables {
 			var skeleton = playableHandle.Skeleton;
 
 			if (!m_FirstFrameHappened) {
-				defaultFlipX = skeleton.FlipX;
-				defaultFlipY = skeleton.FlipY;
+				originalScaleX = skeleton.ScaleX;
+				originalScaleY = skeleton.ScaleY;
+				baseScaleX = Mathf.Abs(originalScaleX);
+				baseScaleY = Mathf.Abs(originalScaleY);
 				m_FirstFrameHappened = true;
 			}
 
@@ -71,8 +74,7 @@ namespace Spine.Unity.Playables {
 				totalWeight += inputWeight;
 
 				if (inputWeight > greatestWeight) {
-					skeleton.FlipX = input.flipX;
-					skeleton.FlipY = input.flipY;
+					SetSkeletonScaleFromFlip(skeleton, input.flipX, input.flipY);
 					greatestWeight = inputWeight;
 				}
 
@@ -81,11 +83,16 @@ namespace Spine.Unity.Playables {
 			}
 
 			if (currentInputs != 1 && 1f - totalWeight > greatestWeight) {
-				skeleton.FlipX = defaultFlipX;
-				skeleton.FlipY = defaultFlipY;
+				skeleton.scaleX = originalScaleX;
+				skeleton.scaleY = originalScaleY;
 			}
 		}
 
+		public void SetSkeletonScaleFromFlip (Skeleton skeleton, bool flipX, bool flipY) {
+			skeleton.scaleX = flipX ? -baseScaleX : baseScaleX;
+			skeleton.scaleY = flipY ? -baseScaleY : baseScaleY;
+		}
+
 		public override void OnGraphStop (Playable playable) {
 			m_FirstFrameHappened = false;
 
@@ -93,8 +100,8 @@ namespace Spine.Unity.Playables {
 				return;
 
 			var skeleton = playableHandle.Skeleton;
-			skeleton.FlipX = defaultFlipX;
-			skeleton.FlipY = defaultFlipY;
+			skeleton.scaleX = originalScaleX;
+			skeleton.scaleY = originalScaleY;
 		}
 	}
 

+ 44 - 53
spine-unity/Assets/Spine/Runtime/spine-unity/SkeletonUtility/SkeletonUtility.cs

@@ -37,7 +37,7 @@ using Spine;
 namespace Spine.Unity {
 	[RequireComponent(typeof(ISkeletonAnimation))]
 	[ExecuteInEditMode]
-	public class SkeletonUtility : MonoBehaviour {
+	public sealed class SkeletonUtility : MonoBehaviour {
 
 		#region BoundingBoxAttachment
 		public static PolygonCollider2D AddBoundingBoxGameObject (Skeleton skeleton, string skinName, string slotName, string attachmentName, Transform parent, bool isTrigger = true) {
@@ -122,23 +122,19 @@ namespace Spine.Unity {
 
 		void Update () {
 			var skeleton = skeletonRenderer.skeleton;
-			if (boneRoot != null && skeleton != null) {
+			if (skeleton != null && boneRoot != null) {
 				boneRoot.localScale = new Vector3(skeleton.scaleX, skeleton.scaleY, 1f);
 			}
 		}
 
-		[HideInInspector]
-		public SkeletonRenderer skeletonRenderer;
-		[HideInInspector]
-		public ISkeletonAnimation skeletonAnimation;
-		[System.NonSerialized]
-		public List<SkeletonUtilityBone> utilityBones = new List<SkeletonUtilityBone>();
-		[System.NonSerialized]
-		public List<SkeletonUtilityConstraint> utilityConstraints = new List<SkeletonUtilityConstraint>();
+		[HideInInspector] public SkeletonRenderer skeletonRenderer;
+		[HideInInspector] public ISkeletonAnimation skeletonAnimation;
+		[System.NonSerialized] public List<SkeletonUtilityBone> boneComponents = new List<SkeletonUtilityBone>();
+		[System.NonSerialized] public List<SkeletonUtilityConstraint> constraintComponents = new List<SkeletonUtilityConstraint>();
 
-		protected bool hasTransformBones;
-		protected bool hasUtilityConstraints;
-		protected bool needToReprocessBones;
+		bool hasOverrideBones;
+		bool hasConstraints;
+		bool needToReprocessBones;
 
 		void OnEnable () {
 			if (skeletonRenderer == null) {
@@ -176,36 +172,34 @@ namespace Spine.Unity {
 		}
 
 		void HandleRendererReset (SkeletonRenderer r) {
-			if (OnReset != null)
-				OnReset();
-
+			if (OnReset != null) OnReset();
 			CollectBones();
 		}
 
 		public void RegisterBone (SkeletonUtilityBone bone) {
-			if (utilityBones.Contains(bone))
+			if (boneComponents.Contains(bone)) {
 				return;
-			else {
-				utilityBones.Add(bone);
+			} else {
+				boneComponents.Add(bone);
 				needToReprocessBones = true;
 			}
 		}
 
 		public void UnregisterBone (SkeletonUtilityBone bone) {
-			utilityBones.Remove(bone);
+			boneComponents.Remove(bone);
 		}
 
 		public void RegisterConstraint (SkeletonUtilityConstraint constraint) {
-			if (utilityConstraints.Contains(constraint))
+			if (constraintComponents.Contains(constraint))
 				return;
 			else {
-				utilityConstraints.Add(constraint);
+				constraintComponents.Add(constraint);
 				needToReprocessBones = true;
 			}
 		}
 
 		public void UnregisterConstraint (SkeletonUtilityConstraint constraint) {
-			utilityConstraints.Remove(constraint);
+			constraintComponents.Remove(constraint);
 		}
 
 		public void CollectBones () {
@@ -222,31 +216,31 @@ namespace Spine.Unity {
 				for (int i = 0, n = transformConstraints.Count; i < n; i++)
 					constraintTargets.Add(transformConstraints.Items[i].target);
 
-				var utilityBones = this.utilityBones;
-				for (int i = 0, n = utilityBones.Count; i < n; i++) {
-					var b = utilityBones[i];
+				var boneComponents = this.boneComponents;
+				for (int i = 0, n = boneComponents.Count; i < n; i++) {
+					var b = boneComponents[i];
 					if (b.bone == null) continue;
-					hasTransformBones |= (b.mode == SkeletonUtilityBone.Mode.Override);
-					hasUtilityConstraints |= constraintTargets.Contains(b.bone);
+					hasOverrideBones |= (b.mode == SkeletonUtilityBone.Mode.Override);
+					hasConstraints |= constraintTargets.Contains(b.bone);
 				}
 
-				hasUtilityConstraints |= utilityConstraints.Count > 0;
+				hasConstraints |= constraintComponents.Count > 0;
 
 				if (skeletonAnimation != null) {
 					skeletonAnimation.UpdateWorld -= UpdateWorld;
 					skeletonAnimation.UpdateComplete -= UpdateComplete;
 
-					if (hasTransformBones || hasUtilityConstraints)
+					if (hasOverrideBones || hasConstraints)
 						skeletonAnimation.UpdateWorld += UpdateWorld;
 
-					if (hasUtilityConstraints)
+					if (hasConstraints)
 						skeletonAnimation.UpdateComplete += UpdateComplete;
 				}
 
 				needToReprocessBones = false;
 			} else {
-				utilityBones.Clear();
-				utilityConstraints.Clear();
+				boneComponents.Clear();
+				constraintComponents.Clear();
 			}
 		}
 
@@ -254,18 +248,18 @@ namespace Spine.Unity {
 			if (needToReprocessBones)
 				CollectBones();
 
-			var utilityBones = this.utilityBones;
-			if (utilityBones == null) return;
-			for (int i = 0, n = utilityBones.Count; i < n; i++)
-				utilityBones[i].transformLerpComplete = false;
+			var boneComponents = this.boneComponents;
+			if (boneComponents == null) return;
+			for (int i = 0, n = boneComponents.Count; i < n; i++)
+				boneComponents[i].transformLerpComplete = false;
 
 			UpdateAllBones(SkeletonUtilityBone.UpdatePhase.Local);
 		}
 
 		void UpdateWorld (ISkeletonAnimation anim) {
 			UpdateAllBones(SkeletonUtilityBone.UpdatePhase.World);
-			for (int i = 0, n = utilityConstraints.Count; i < n; i++)
-				utilityConstraints[i].DoUpdate();
+			for (int i = 0, n = constraintComponents.Count; i < n; i++)
+				constraintComponents[i].DoUpdate();
 		}
 
 		void UpdateComplete (ISkeletonAnimation anim) {
@@ -276,17 +270,17 @@ namespace Spine.Unity {
 			if (boneRoot == null)
 				CollectBones();
 
-			var utilityBones = this.utilityBones;
-			if (utilityBones == null) return;
-			for (int i = 0, n = utilityBones.Count; i < n; i++)
-				utilityBones[i].DoUpdate(phase);
+			var boneComponents = this.boneComponents;
+			if (boneComponents == null) return;
+			for (int i = 0, n = boneComponents.Count; i < n; i++)
+				boneComponents[i].DoUpdate(phase);
 		}
 
 		public Transform GetBoneRoot () {
 			if (boneRoot != null)
 				return boneRoot;
 
-			boneRoot = new GameObject("SkeletonUtility-Root").transform;
+			boneRoot = new GameObject("SkeletonUtility-SkeletonRoot").transform;
 			boneRoot.parent = transform;
 			boneRoot.localPosition = Vector3.zero;
 			boneRoot.localRotation = Quaternion.identity;
@@ -326,10 +320,11 @@ namespace Spine.Unity {
 
 		public GameObject SpawnBone (Bone bone, Transform parent, SkeletonUtilityBone.Mode mode, bool pos, bool rot, bool sca) {
 			GameObject go = new GameObject(bone.Data.Name);
-			go.transform.parent = parent;
+			var goTransform = go.transform;
+			goTransform.parent = parent;
 
 			SkeletonUtilityBone b = go.AddComponent<SkeletonUtilityBone>();
-			b.skeletonUtility = this;
+			b.hierarchy = this;
 			b.position = pos;
 			b.rotation = rot;
 			b.scale = sca;
@@ -341,13 +336,9 @@ namespace Spine.Unity {
 			b.valid = true;
 
 			if (mode == SkeletonUtilityBone.Mode.Override) {
-				if (rot)
-					go.transform.localRotation = Quaternion.Euler(0, 0, b.bone.AppliedRotation);
-
-				if (pos)
-					go.transform.localPosition = new Vector3(b.bone.X, b.bone.Y, 0);
-
-				go.transform.localScale = new Vector3(b.bone.scaleX, b.bone.scaleY, 0);
+				if (rot) goTransform.localRotation = Quaternion.Euler(0, 0, b.bone.AppliedRotation);
+				if (pos) goTransform.localPosition = new Vector3(b.bone.X, b.bone.Y, 0);
+				goTransform.localScale = new Vector3(b.bone.scaleX, b.bone.scaleY, 0);
 			}
 
 			return go;

+ 14 - 18
spine-unity/Assets/Spine/Runtime/spine-unity/SkeletonUtility/SkeletonUtilityBone.cs

@@ -28,15 +28,13 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-// Contributed by: Mitch Thompson
-
 using UnityEngine;
 using Spine;
 
 namespace Spine.Unity {
 	/// <summary>Sets a GameObject's transform to match a bone on a Spine skeleton.</summary>
 	[ExecuteInEditMode]
-	[AddComponentMenu("Spine/SkeletonUtilityBone")]
+	[AddComponentMenu("Spine/SkeletonGameObjectsBone")]
 	public class SkeletonUtilityBone : MonoBehaviour {
 		public enum Mode {
 			Follow,
@@ -59,7 +57,7 @@ namespace Spine.Unity {
 		public float overrideAlpha = 1;
 		#endregion
 
-		[System.NonSerialized] public SkeletonUtility skeletonUtility;
+		[System.NonSerialized] public SkeletonUtility hierarchy;
 		[System.NonSerialized] public Bone bone;
 		[System.NonSerialized] public bool transformLerpComplete;
 		[System.NonSerialized] public bool valid;
@@ -71,23 +69,21 @@ namespace Spine.Unity {
 		public void Reset () {
 			bone = null;
 			cachedTransform = transform;
-			valid = skeletonUtility != null && skeletonUtility.skeletonRenderer != null && skeletonUtility.skeletonRenderer.valid;
+			valid = hierarchy != null && hierarchy.skeletonRenderer != null && hierarchy.skeletonRenderer.valid;
 			if (!valid)
 				return;
-			skeletonTransform = skeletonUtility.transform;
-			skeletonUtility.OnReset -= HandleOnReset;
-			skeletonUtility.OnReset += HandleOnReset;
+			skeletonTransform = hierarchy.transform;
+			hierarchy.OnReset -= HandleOnReset;
+			hierarchy.OnReset += HandleOnReset;
 			DoUpdate(UpdatePhase.Local);
 		}
 
 		void OnEnable () {
-			skeletonUtility = transform.GetComponentInParent<SkeletonUtility>();
-
-			if (skeletonUtility == null)
-				return;
+			hierarchy = transform.GetComponentInParent<SkeletonUtility>();
+			if (hierarchy == null) return;
 
-			skeletonUtility.RegisterBone(this);
-			skeletonUtility.OnReset += HandleOnReset;
+			hierarchy.RegisterBone(this);
+			hierarchy.OnReset += HandleOnReset;
 		}
 
 		void HandleOnReset () {
@@ -95,9 +91,9 @@ namespace Spine.Unity {
 		}
 
 		void OnDisable () {
-			if (skeletonUtility != null) {
-				skeletonUtility.OnReset -= HandleOnReset;
-				skeletonUtility.UnregisterBone(this);
+			if (hierarchy != null) {
+				hierarchy.OnReset -= HandleOnReset;
+				hierarchy.UnregisterBone(this);
 			}
 		}
 
@@ -107,7 +103,7 @@ namespace Spine.Unity {
 				return;
 			}
 
-			var skeleton = skeletonUtility.skeletonRenderer.skeleton;
+			var skeleton = hierarchy.skeletonRenderer.skeleton;
 
 			if (bone == null) {
 				if (string.IsNullOrEmpty(boneName)) return;

+ 6 - 8
spine-unity/Assets/Spine/Runtime/spine-unity/SkeletonUtility/SkeletonUtilityConstraint.cs

@@ -28,25 +28,23 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-// Contributed by: Mitch Thompson
-
 using UnityEngine;
 
 namespace Spine.Unity {
 	[RequireComponent(typeof(SkeletonUtilityBone)), ExecuteInEditMode]
 	public abstract class SkeletonUtilityConstraint : MonoBehaviour {
 
-		protected SkeletonUtilityBone utilBone;
-		protected SkeletonUtility skeletonUtility;
+		protected SkeletonUtilityBone bone;
+		protected SkeletonUtility hierarchy;
 
 		protected virtual void OnEnable () {
-			utilBone = GetComponent<SkeletonUtilityBone>();
-			skeletonUtility = transform.GetComponentInParent<SkeletonUtility>();
-			skeletonUtility.RegisterConstraint(this);
+			bone = GetComponent<SkeletonUtilityBone>();
+			hierarchy = transform.GetComponentInParent<SkeletonUtility>();
+			hierarchy.RegisterConstraint(this);
 		}
 
 		protected virtual void OnDisable () {
-			skeletonUtility.UnregisterConstraint(this);
+			hierarchy.UnregisterConstraint(this);
 		}
 
 		public abstract void DoUpdate ();

+ 7 - 1
spine-unity/Assets/Spine/Runtime/spine-unity/SpineAttributes.cs

@@ -149,17 +149,23 @@ namespace Spine.Unity {
 		/// <param name="startsWith">Filters popup results to elements that begin with supplied string.</param>
 		/// <param name = "includeNone">If true, the dropdown list will include a "none" option which stored as an empty string.</param>
 		/// <param name = "fallbackToTextField">If true, and an animation list source can't be found, the field will fall back to a normal text field. If false, it will show an error.</param>
+		/// <param name = "defaultAsEmptyString">If true, the default choice will be serialized as an empty string.</param>
 		/// <param name="dataField">If specified, a locally scoped field with the name supplied by in dataField will be used to fill the popup results.
 		/// Valid types are SkeletonDataAsset and SkeletonRenderer (and derivatives)
 		/// If left empty and the script the attribute is applied to is derived from Component, GetComponent<SkeletonRenderer>() will be called as a fallback.
 		/// </param>
-		public SpineSkin (string startsWith = "", string dataField = "", bool includeNone = true, bool fallbackToTextField = false) {
+
+		public bool defaultAsEmptyString = false;
+
+		public SpineSkin (string startsWith = "", string dataField = "", bool includeNone = true, bool fallbackToTextField = false, bool defaultAsEmptyString = false) {
 			this.startsWith = startsWith;
 			this.dataField = dataField;
 			this.includeNone = includeNone;
 			this.fallbackToTextField = fallbackToTextField;
+			this.defaultAsEmptyString = defaultAsEmptyString;
 		}
 	}
+
 	public class SpineAnimation : SpineAttributeBase {
 		/// <summary>
 		/// Smart popup menu for Spine Animations

+ 2 - 0
spine-xna/example/src/ExampleGame.cs

@@ -119,6 +119,8 @@ namespace Spine {
 			state = new AnimationState(stateData);
 
 			if (name == "spineboy-ess") {
+				skeleton.SetAttachment("head-bb", "head");
+
 				stateData.SetMix("run", "jump", 0.2f);
 				stateData.SetMix("jump", "run", 0.4f);