Przeglądaj źródła

Merge branch 'dev' into spine-lua-3.5

badlogic 9 lat temu
rodzic
commit
03070b9604

+ 2 - 1
spine-c/src/spine/Skeleton.c

@@ -188,7 +188,8 @@ static void _sortPathConstraintAttachmentBones(_spSkeleton* const internal, spAt
 		int i = 0;
 		while (i < pathBonesCount) {
 			int boneCount = pathBones[i++];
-			for (int n = i + boneCount; i < n; i++)
+			int n;
+			for (n = i + boneCount; i < n; i++)
 				_sortBone(internal, bones[pathBones[i]]);
 		}
 	}

+ 3 - 3
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java

@@ -136,8 +136,8 @@ public class Animation {
 	static public interface Timeline {
 		/** Sets the value(s) for the specified time.
 		 * @param events May be null to not collect fired events.
-		 * @param setupPose If true, the timeline is mixed with the setup pose, else it is mixed with the current pose. Passing true
-		 *           when alpha is 1 is slightly more efficient.
+		 * @param setupPose True when the timeline is mixed with the setup pose, false when it is mixed with the current pose.
+		 *           Passing true when alpha is 1 is slightly more efficient.
 		 * @param mixingOut True when mixing over time toward the setup or current pose, false when mixing toward the keyed pose.
 		 *           Irrelevant when alpha is 1. */
 		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha, boolean setupPose,
@@ -916,7 +916,7 @@ public class Animation {
 			if (time < frames[0]) return; // Time is before first frame.
 
 			// BOZO - Finish timelines handling setupPose and mixingOut from here down.
-			
+
 			IkConstraint constraint = skeleton.ikConstraints.get(ikConstraintIndex);
 
 			if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame.

+ 45 - 100
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java

@@ -52,7 +52,7 @@ public class AnimationState {
 	static private final Animation emptyAnimation = new Animation("<empty>", new Array(0), 0);
 
 	private AnimationStateData data;
-	private final Array<TrackEntry> tracks = new Array();
+	final Array<TrackEntry> tracks = new Array();
 	private final Array<Event> events = new Array();
 	final Array<AnimationStateListener> listeners = new Array();
 	private final EventQueue queue = new EventQueue();
@@ -60,9 +60,6 @@ public class AnimationState {
 	boolean animationsChanged;
 	private float timeScale = 1;
 
-	StringBuilder last = new StringBuilder();
-	StringBuilder log = new StringBuilder();
-
 	final Pool<TrackEntry> trackEntryPool = new Pool() {
 		protected Object newObject () {
 			return new TrackEntry();
@@ -112,9 +109,9 @@ public class AnimationState {
 					}
 					continue;
 				}
-				updateMixingFrom(current, delta);
+				updateMixingFrom(current, delta, true);
 			} else {
-				updateMixingFrom(current, delta);
+				updateMixingFrom(current, delta, true);
 				// Clear the track when there is no next entry, the track end time is reached, and there is no mixingFrom.
 				if (current.trackLast >= current.trackEnd && current.mixingFrom == null) {
 					tracks.set(i, null);
@@ -130,11 +127,11 @@ public class AnimationState {
 		queue.drain();
 	}
 
-	private void updateMixingFrom (TrackEntry entry, float delta) {
+	private void updateMixingFrom (TrackEntry entry, float delta, boolean canEnd) {
 		TrackEntry from = entry.mixingFrom;
 		if (from == null) return;
 
-		if (entry.mixTime >= entry.mixDuration && entry.mixTime > 0) {
+		if (canEnd && entry.mixTime >= entry.mixDuration && entry.mixTime > 0) {
 			queue.end(from);
 			TrackEntry newFrom = from.mixingFrom;
 			entry.mixingFrom = newFrom;
@@ -150,7 +147,7 @@ public class AnimationState {
 		from.trackTime += mixingFromDelta;
 		entry.mixTime += mixingFromDelta;
 
-		updateMixingFrom(from, delta);
+		updateMixingFrom(from, delta, canEnd && from.alpha == 1);
 	}
 
 	/** Poses the skeleton using the track entry animations. There are no side effects other than invoking listeners, so the
@@ -161,7 +158,7 @@ public class AnimationState {
 
 		Array<Event> events = this.events;
 
-		for (int i = 0; i < tracks.size; i++) {
+		for (int i = 0, n = tracks.size; i < n; i++) {
 			TrackEntry current = tracks.get(i);
 			if (current == null || current.delay > 0) continue;
 
@@ -171,24 +168,24 @@ public class AnimationState {
 
 			// Apply current entry.
 			float animationLast = current.animationLast, animationTime = current.getAnimationTime();
-			Array<Timeline> timelines = current.animation.timelines;
-			log("apply current: " + current + ", mix: " + mix + " * " + current.alpha);
+			int timelineCount = current.animation.timelines.size;
+			Object[] timelines = current.animation.timelines.items;
 			if (mix == 1) {
-				for (int ii = 0, n = timelines.size; ii < n; ii++)
-					timelines.get(ii).apply(skeleton, animationLast, animationTime, events, 1, false, false);
+				for (int ii = 0; ii < timelineCount; ii++)
+					((Timeline)timelines[ii]).apply(skeleton, animationLast, animationTime, events, 1, true, false);
 			} else {
 				boolean firstFrame = current.timelinesRotation.size == 0;
-				if (firstFrame) current.timelinesRotation.setSize(timelines.size << 1);
+				if (firstFrame) current.timelinesRotation.setSize(timelineCount << 1);
 				float[] timelinesRotation = current.timelinesRotation.items;
+
 				boolean[] timelinesFirst = current.timelinesFirst.items;
-				for (int ii = 0, n = timelines.size; ii < n; ii++) {
-					Timeline timeline = timelines.get(ii);
+				for (int ii = 0; ii < timelineCount; ii++) {
+					Timeline timeline = (Timeline)timelines[ii];
 					if (timeline instanceof RotateTimeline) {
-						applyRotateTimeline((RotateTimeline)timeline, skeleton, animationLast, animationTime, events, mix,
-							timelinesFirst[ii], false, timelinesRotation, ii << 1, firstFrame);
-					} else {
+						applyRotateTimeline(timeline, skeleton, animationTime, mix, timelinesFirst[ii], timelinesRotation, ii << 1,
+							firstFrame);
+					} else
 						timeline.apply(skeleton, animationLast, animationTime, events, mix, timelinesFirst[ii], false);
-					}
 				}
 			}
 			queueEvents(current, animationTime);
@@ -197,62 +194,44 @@ public class AnimationState {
 		}
 
 		queue.drain();
-
-		if (!log.toString().equals(last.toString())) {
-			System.out.println(log);
-			last.setLength(0);
-			last.append(log);
-		}
-		log.setLength(0);
-	}
-
-	void log (String m) {
-		log.append(m);
-		log.append('\n');
 	}
 
 	private float applyMixingFrom (TrackEntry entry, Skeleton skeleton, float alpha) {
+		TrackEntry from = entry.mixingFrom;
+		if (from.mixingFrom != null) applyMixingFrom(from, skeleton, alpha);
+
 		float mix;
 		if (entry.mixDuration == 0) // Single frame mix to undo mixingFrom changes.
 			mix = 1;
 		else {
-			mix = alpha * entry.mixTime / entry.mixDuration;
+			mix = entry.mixTime / entry.mixDuration;
 			if (mix > 1) mix = 1;
+			mix *= alpha;
 		}
 
-		TrackEntry from = entry.mixingFrom;
-		if (from.mixingFrom != null) applyMixingFrom(from, skeleton, alpha);
-
 		Array<Event> events = mix < from.eventThreshold ? this.events : null;
 		boolean attachments = mix < from.attachmentThreshold, drawOrder = mix < from.drawOrderThreshold;
-
 		float animationLast = from.animationLast, animationTime = from.getAnimationTime();
-		Array<Timeline> timelines = from.animation.timelines;
-		int timelineCount = timelines.size;
-		boolean[] timelinesFirst = from.timelinesFirst.items, timelinesLast = from.timelinesLast.items;
-		float alphaFull = from.alpha, alphaMix = alphaFull * (1 - mix);
+		alpha = from.alpha * (1 - mix);
+		int timelineCount = from.animation.timelines.size;
+		Object[] timelines = from.animation.timelines.items;
+		boolean[] timelinesFirst = from.timelinesFirst.items;
 
 		boolean firstFrame = from.timelinesRotation.size == 0;
 		if (firstFrame) from.timelinesRotation.setSize(timelineCount << 1);
 		float[] timelinesRotation = from.timelinesRotation.items;
 
-		log("applyMixingFrom: " + entry.mixingFrom + " -> " + entry + ", mix: " + entry.mixTime / entry.mixDuration);
-		if (timelineCount == 0) log("apply from: " + from + " " + alphaFull + " * " + entry.alpha);
-
 		for (int i = 0; i < timelineCount; i++) {
-			Timeline timeline = timelines.get(i);
+			Timeline timeline = (Timeline)timelines[i];
 			boolean setupPose = timelinesFirst[i];
-			float a = timelinesLast[i] ? alphaMix : alphaFull;
-			log("apply from: " + from + " " + a + " * " + entry.alpha);
-			if (timeline instanceof RotateTimeline) {
-				applyRotateTimeline((RotateTimeline)timeline, skeleton, animationLast, animationTime, events, a, setupPose, setupPose,
-					timelinesRotation, i << 1, firstFrame);
-			} else {
+			if (timeline instanceof RotateTimeline)
+				applyRotateTimeline(timeline, skeleton, animationTime, alpha, setupPose, timelinesRotation, i << 1, firstFrame);
+			else {
 				if (!setupPose) {
 					if (!attachments && timeline instanceof AttachmentTimeline) continue;
 					if (!drawOrder && timeline instanceof DrawOrderTimeline) continue;
 				}
-				timeline.apply(skeleton, animationLast, animationTime, events, a, setupPose, setupPose);
+				timeline.apply(skeleton, animationLast, animationTime, events, alpha, setupPose, true);
 			}
 		}
 
@@ -263,18 +242,18 @@ public class AnimationState {
 		return mix;
 	}
 
-	/** @param events May be null. */
-	private void applyRotateTimeline (RotateTimeline timeline, Skeleton skeleton, float lastTime, float time, Array<Event> events,
-		float alpha, boolean setupPose, boolean mixingOut, float[] timelinesRotation, int i, boolean firstFrame) {
+	private void applyRotateTimeline (Timeline timeline, Skeleton skeleton, float time, float alpha, boolean setupPose,
+		float[] timelinesRotation, int i, boolean firstFrame) {
 		if (alpha == 1) {
-			timeline.apply(skeleton, lastTime, time, events, 1, setupPose, setupPose);
+			timeline.apply(skeleton, 0, time, null, 1, setupPose, false);
 			return;
 		}
 
-		float[] frames = timeline.frames;
+		RotateTimeline rotateTimeline = (RotateTimeline)timeline;
+		float[] frames = rotateTimeline.frames;
 		if (time < frames[0]) return; // Time is before first frame.
 
-		Bone bone = skeleton.bones.get(timeline.boneIndex);
+		Bone bone = skeleton.bones.get(rotateTimeline.boneIndex);
 
 		float r2;
 		if (time >= frames[frames.length - ENTRIES]) // Time is after last frame.
@@ -284,7 +263,7 @@ public class AnimationState {
 			int frame = Animation.binarySearch(frames, time, ENTRIES);
 			float prevRotation = frames[frame + PREV_ROTATION];
 			float frameTime = frames[frame];
-			float percent = timeline.getCurvePercent((frame >> 1) - 1,
+			float percent = rotateTimeline.getCurvePercent((frame >> 1) - 1,
 				1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
 
 			r2 = frames[frame + ROTATION] - prevRotation;
@@ -293,7 +272,7 @@ public class AnimationState {
 			r2 -= (16384 - (int)(16384.499999999996 - r2 / 360)) * 360;
 		}
 
-		// Mix between two rotations using the direction of the shortest route on the first frame while detecting crosses.
+		// Mix between rotations using the direction of the shortest route on the first frame while detecting crosses.
 		float r1 = setupPose ? bone.data.rotation : bone.rotation;
 		float total, diff = r2 - r1;
 		if (diff == 0) {
@@ -319,7 +298,7 @@ public class AnimationState {
 				if (Math.abs(lastTotal) > 180) lastTotal += 360 * Math.signum(lastTotal);
 				dir = current;
 			}
-			total = diff + lastTotal - lastTotal % 360; // Keep loops part of lastTotal.
+			total = diff + lastTotal - lastTotal % 360; // Store loops as part of lastTotal.
 			if (dir != current) total += 360 * Math.signum(lastTotal);
 			timelinesRotation[i] = total;
 		}
@@ -405,19 +384,12 @@ public class AnimationState {
 		if (from != null) {
 			queue.interrupt(from);
 			current.mixingFrom = from;
-			// entry.mixTime = Math.max(0, entry.mixDuration - current.trackTime);
-			// log("setCurrent mixTime: " + entry.mixDuration + " - " + current.trackTime + " = " + entry.mixTime);
 			current.mixTime = 0;
 
-			from.timelinesRotation.clear(); // BOZO - Needed? Recursive?
+			from.timelinesRotation.clear();
 
-//			float alpha = 1;
-			float duration = from.animationEnd - from.animationStart;
-			if (duration > 0) from.alpha *= (from.getAnimationTime() - from.animationStart) / duration;
-//			do {
-//				from.alpha *= alpha;
-//				from = from.mixingFrom;
-//			} while (from != null);
+			// If not completely mixed in, set alpha so mixing out happens from current mix to zero.
+			if (from.mixingFrom != null) from.alpha *= Math.min(from.mixTime / from.mixDuration, 1);
 		}
 
 		queue.start(current);
@@ -592,32 +564,6 @@ public class AnimationState {
 			TrackEntry entry = tracks.get(i);
 			if (entry != null) checkTimelinesFirst(entry);
 		}
-
-		// Compute timelinesLast from highest to lowest track entries that have mixingFrom.
-		propertyIDs.clear();
-		int lowestMixingFrom = n;
-		for (i = 0; i < n; i++) { // Find lowest with a mixingFrom entry.
-			TrackEntry entry = tracks.get(i);
-			if (entry == null) continue;
-			if (entry.mixingFrom != null) {
-				lowestMixingFrom = i;
-				break;
-			}
-		}
-		for (i = n - 1; i >= lowestMixingFrom; i--) {
-			TrackEntry entry = tracks.get(i);
-			if (entry == null) continue;
-
-			Array<Timeline> timelines = entry.animation.timelines;
-			for (int ii = 0, nn = timelines.size; ii < nn; ii++)
-				propertyIDs.add(timelines.get(ii).getPropertyId());
-
-			entry = entry.mixingFrom;
-			while (entry != null) {
-				checkTimelinesUsage(entry, entry.timelinesLast);
-				entry = entry.mixingFrom;
-			}
-		}
 	}
 
 	/** From last to first mixingFrom entries, sets timelinesFirst to true on last, calls checkTimelineUsage on rest. */
@@ -727,7 +673,7 @@ public class AnimationState {
 		float animationStart, animationEnd, animationLast, nextAnimationLast;
 		float delay, trackTime, trackLast, nextTrackLast, trackEnd, timeScale;
 		float alpha, mixTime, mixDuration, mixAlpha;
-		final BooleanArray timelinesFirst = new BooleanArray(), timelinesLast = new BooleanArray();
+		final BooleanArray timelinesFirst = new BooleanArray();
 		final FloatArray timelinesRotation = new FloatArray();
 
 		public void reset () {
@@ -736,7 +682,6 @@ public class AnimationState {
 			animation = null;
 			listener = null;
 			timelinesFirst.clear();
-			timelinesLast.clear();
 			timelinesRotation.clear();
 		}
 

+ 9 - 12
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Bone.java

@@ -153,28 +153,25 @@ public class Bone implements Updatable {
 			break;
 		}
 		case noRotationOrReflection: {
-			float psx = (float)Math.sqrt(pa * pa + pc * pc), psy, prx;
-			if (psx > 0.0001f) {
-				psy = Math.abs((pa * pd - pb * pc) / psx);
+			float s = pa * pa + pc * pc, prx;
+			if (s > 0.0001f) {
+				s = Math.abs(pa * pd - pb * pc) / s;
+				pb = pc * s;
+				pd = pa * s;
 				prx = atan2(pc, pa) * radDeg;
 			} else {
-				psx = 0;
-				psy = (float)Math.sqrt(pb * pb + pd * pd);
+				pa = 0;
+				pc = 0;
 				prx = 90 - atan2(pd, pb) * radDeg;
 			}
-			float cos = cosDeg(prx), sin = sinDeg(prx);
-			pa = cos * psx;
-			pb = -sin * psy;
-			pc = sin * psx;
-			pd = cos * psy;
 			float rx = rotation + shearX - prx;
 			float ry = rotation + shearY - prx + 90;
 			float la = cosDeg(rx) * scaleX;
 			float lb = cosDeg(ry) * scaleY;
 			float lc = sinDeg(rx) * scaleX;
 			float ld = sinDeg(ry) * scaleY;
-			a = pa * la + pb * lc;
-			b = pa * lb + pb * ld;
+			a = pa * la - pb * lc;
+			b = pa * lb - pb * ld;
 			c = pc * la + pd * lc;
 			d = pc * lb + pd * ld;
 			break;

+ 1 - 0
spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewer.java

@@ -447,6 +447,7 @@ public class SkeletonViewer extends ApplicationAdapter {
 				table.add(new Label("", skin, "default", Color.LIGHT_GRAY)); // Version.
 			}
 
+			// Events.
 			window.addListener(new InputListener() {
 				public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) {
 					event.cancel();

+ 3 - 3
spine-unity/Assets/spine-unity/Asset Types/AtlasAsset.cs

@@ -38,14 +38,14 @@ namespace Spine.Unity {
 	public class AtlasAsset : ScriptableObject {
 		public TextAsset atlasFile;
 		public Material[] materials;
-		private Atlas atlas;
+		protected Atlas atlas;
 
-		public void Reset () {
+		public virtual void Reset () {
 			atlas = null;
 		}
 
 		/// <returns>The atlas or null if it could not be loaded.</returns>
-		public Atlas GetAtlas () {
+		public virtual Atlas GetAtlas () {
 			if (atlasFile == null) {
 				Debug.LogError("Atlas file not set for atlas asset: " + name, this);
 				Reset();

+ 65 - 34
spine-unity/Assets/spine-unity/Editor/SkeletonAnimationInspector.cs

@@ -35,9 +35,11 @@ using Spine;
 namespace Spine.Unity.Editor {
 	
 	[CustomEditor(typeof(SkeletonAnimation))]
+	[CanEditMultipleObjects]
 	public class SkeletonAnimationInspector : SkeletonRendererInspector {
 		protected SerializedProperty animationName, loop, timeScale, autoReset;
 		protected bool wasAnimationNameChanged;
+		protected bool requireRepaint;
 
 		protected override void OnEnable () {
 			base.OnEnable();
@@ -46,64 +48,93 @@ namespace Spine.Unity.Editor {
 			timeScale = serializedObject.FindProperty("timeScale");
 		}
 
-		protected override void DrawInspectorGUI () {
-			base.DrawInspectorGUI();
+		protected override void DrawInspectorGUI (bool multi) {
+			base.DrawInspectorGUI(multi);
+			if (!TargetIsValid) return;
+			bool sameData = SpineInspectorUtility.TargetsUseSameData(serializedObject);
 
-			SkeletonAnimation component = (SkeletonAnimation)target;
-			if (!component.valid)
+			// Try to reflect the animation name on the scene object.
+			{
+				if (multi)
+					foreach (var o in targets)		
+						TrySetAnimation(o);
+				else
+					TrySetAnimation(target);
+			}
+			
+			EditorGUILayout.Space();
+
+			if (multi && !sameData)
+				EditorGUILayout.DelayedTextField(animationName);
+			else {
+				EditorGUI.BeginChangeCheck();
+				EditorGUILayout.PropertyField(animationName);
+				wasAnimationNameChanged |= EditorGUI.EndChangeCheck(); // Value used in the next update.
+			}
+				
+			EditorGUILayout.PropertyField(loop);
+
+			EditorGUILayout.PropertyField(timeScale);
+			if (multi) {
+				foreach (var o in targets) {
+					var component = o as SkeletonAnimation;
+					component.timeScale = Mathf.Max(component.timeScale, 0);
+				}
+			} else {
+				var component = (SkeletonAnimation)target;
+				component.timeScale = Mathf.Max(component.timeScale, 0);
+			}
+
+			if (!isInspectingPrefab) {
+				if (requireRepaint) {
+					SceneView.RepaintAll();
+					requireRepaint = false;
+				}
+
+				DrawSkeletonUtilityButton(multi);
+			}
+		}
+
+		protected void TrySetAnimation (Object o) {
+			var skeletonAnimation = o as SkeletonAnimation;
+			if (skeletonAnimation == null) return;
+			if (!skeletonAnimation.valid)
 				return;
 
 			if (!isInspectingPrefab) {
 				if (wasAnimationNameChanged) {
 					if (!Application.isPlaying) {
-						if (component.state != null) component.state.ClearTrack(0);
-						component.skeleton.SetToSetupPose();
+						if (skeletonAnimation.state != null) skeletonAnimation.state.ClearTrack(0);
+						skeletonAnimation.skeleton.SetToSetupPose();
 					}
 
-					Spine.Animation animationToUse = component.skeleton.Data.FindAnimation(animationName.stringValue);
+					Spine.Animation animationToUse = skeletonAnimation.skeleton.Data.FindAnimation(animationName.stringValue);
 
 					if (!Application.isPlaying) {
-						if (animationToUse != null) animationToUse.Apply(component.skeleton, 0f, 0f, false, null);
-						component.Update();
-						component.LateUpdate();
-						SceneView.RepaintAll();
+						if (animationToUse != null) animationToUse.Apply(skeletonAnimation.skeleton, 0f, 0f, false, null);
+						skeletonAnimation.Update();
+						skeletonAnimation.LateUpdate();
+						requireRepaint = true;
 					} else {
 						if (animationToUse != null)
-							component.state.SetAnimation(0, animationToUse, loop.boolValue);
+							skeletonAnimation.state.SetAnimation(0, animationToUse, loop.boolValue);
 						else
-							component.state.ClearTrack(0);
+							skeletonAnimation.state.ClearTrack(0);
 					}
 
 					wasAnimationNameChanged = false;
 				}
 
 				// Reflect animationName serialized property in the inspector even if SetAnimation API was used.
-				if (Application.isPlaying) {
-					TrackEntry current = component.state.GetCurrent(0);
+				bool multi = animationName.serializedObject.isEditingMultipleObjects;
+				if (!multi && Application.isPlaying) {
+					TrackEntry current = skeletonAnimation.state.GetCurrent(0);
 					if (current != null) {
-						if (component.AnimationName != animationName.stringValue)
+						if (skeletonAnimation.AnimationName != animationName.stringValue)
 							animationName.stringValue = current.Animation.Name;
 					}
 				}
 			}
-				
-			EditorGUILayout.Space();
-			EditorGUI.BeginChangeCheck();
-			EditorGUILayout.PropertyField(animationName);
-			wasAnimationNameChanged |= EditorGUI.EndChangeCheck(); // Value used in the next update.
-
-			EditorGUILayout.PropertyField(loop);
-			EditorGUILayout.PropertyField(timeScale);
-			component.timeScale = Mathf.Max(component.timeScale, 0);
-
-			EditorGUILayout.Space();
-
-			if (!isInspectingPrefab) {
-				if (component.GetComponent<SkeletonUtility>() == null) {
-					if (GUILayout.Button(new GUIContent("Add Skeleton Utility", SpineEditorUtilities.Icons.skeletonUtility), GUILayout.Height(30)))
-						component.gameObject.AddComponent<SkeletonUtility>();
-				}
-			}
 		}
 	}
 }

+ 6 - 14
spine-unity/Assets/spine-unity/Editor/SkeletonAnimatorInspector.cs

@@ -31,10 +31,10 @@
 // Contributed by: Mitch Thompson
 
 using UnityEditor;
-using UnityEngine;
 
 namespace Spine.Unity.Editor {
 	[CustomEditor(typeof(SkeletonAnimator))]
+	[CanEditMultipleObjects]
 	public class SkeletonAnimatorInspector : SkeletonRendererInspector {
 		protected SerializedProperty layerMixModes;
 		protected override void OnEnable () {
@@ -42,22 +42,14 @@ namespace Spine.Unity.Editor {
 			layerMixModes = serializedObject.FindProperty("layerMixModes");
 		}
 
-		protected override void DrawInspectorGUI () {
-			base.DrawInspectorGUI();
+		protected override void DrawInspectorGUI (bool multi) {
+			base.DrawInspectorGUI(multi);
 			EditorGUILayout.PropertyField(layerMixModes, true);
-			var component = (SkeletonAnimator)target;
-			if (!component.valid)
-				return;
 
-			EditorGUILayout.Space();
+			if (!TargetIsValid) return;
 
-			if (!isInspectingPrefab) {
-				if (component.GetComponent<SkeletonUtility>() == null) {
-					if (GUILayout.Button(new GUIContent("Add Skeleton Utility", SpineEditorUtilities.Icons.skeletonUtility), GUILayout.Height(30))) {
-						component.gameObject.AddComponent<SkeletonUtility>();
-					}
-				}
-			}
+			if (!isInspectingPrefab)
+				DrawSkeletonUtilityButton(multi);
 		}
 	}
 }

+ 144 - 49
spine-unity/Assets/spine-unity/Editor/SkeletonRendererInspector.cs

@@ -31,17 +31,34 @@
 #define NO_PREFAB_MESH
 
 using UnityEditor;
+using System.Collections.Generic;
 using UnityEngine;
 
 namespace Spine.Unity.Editor {
 	
 	[CustomEditor(typeof(SkeletonRenderer))]
+	[CanEditMultipleObjects]
 	public class SkeletonRendererInspector : UnityEditor.Editor {
 		protected static bool advancedFoldout;
 		protected SerializedProperty skeletonDataAsset, initialSkinName, normals, tangents, meshes, immutableTriangles, separatorSlotNames, frontFacing, zSpacing, pmaVertexColors;
 		protected SpineInspectorUtility.SerializedSortingProperties sortingProperties;
 		protected bool isInspectingPrefab;
-		protected MeshFilter meshFilter;
+
+		protected bool TargetIsValid {
+			get {
+				if (serializedObject.isEditingMultipleObjects) {
+					foreach (var o in targets) {
+						var component = (SkeletonRenderer)o;
+						if (!component.valid)
+							return false;
+					}
+					return true;
+				} else {
+					var component = (SkeletonRenderer)target;
+					return component.valid;
+				}
+			}
+		}
 
 		protected virtual void OnEnable () {
 			isInspectingPrefab = (PrefabUtility.GetPrefabType(target) == PrefabType.Prefab);
@@ -60,8 +77,8 @@ namespace Spine.Unity.Editor {
 			frontFacing = serializedObject.FindProperty("frontFacing");
 			zSpacing = serializedObject.FindProperty("zSpacing");
 
-			var renderer = ((SkeletonRenderer)target).GetComponent<Renderer>();
-			sortingProperties = new SpineInspectorUtility.SerializedSortingProperties(renderer);
+			SerializedObject rso = SpineInspectorUtility.GetRenderersSerializedObject(serializedObject);
+			sortingProperties = new SpineInspectorUtility.SerializedSortingProperties(rso);
 		}
 
 		public static void ReapplySeparatorSlotNames (SkeletonRenderer skeletonRenderer) {
@@ -76,62 +93,105 @@ namespace Spine.Unity.Editor {
 				var slot = skeleton.FindSlot(separatorSlotNames[i]);
 				if (slot != null) {
 					separatorSlots.Add(slot);
-					//Debug.Log(slot + " added as separator.");
 				} else {
 					Debug.LogWarning(separatorSlotNames[i] + " is not a slot in " + skeletonRenderer.skeletonDataAsset.skeletonJSON.name);				
 				}
 			}
-
-			//Debug.Log("Reapplied Separator Slot Names. Count is now: " + separatorSlots.Count);
 		}
 
-		protected virtual void DrawInspectorGUI () {
-			// JOHN: todo: support multiediting.
-			SkeletonRenderer component = (SkeletonRenderer)target;
-
-			using (new EditorGUILayout.HorizontalScope()) {
-				EditorGUILayout.PropertyField(skeletonDataAsset);
-				const string ReloadButtonLabel = "Reload";
-				float reloadWidth = GUI.skin.label.CalcSize(new GUIContent(ReloadButtonLabel)).x + 20;
-				if (GUILayout.Button(ReloadButtonLabel, GUILayout.Width(reloadWidth))) {
-					if (component.skeletonDataAsset != null) {
-						foreach (AtlasAsset aa in component.skeletonDataAsset.atlasAssets) {
-							if (aa != null)
-								aa.Reset();
+		protected virtual void DrawInspectorGUI (bool multi) {
+			bool valid = TargetIsValid;
+			if (multi) {
+				using (new EditorGUILayout.HorizontalScope()) {
+					EditorGUILayout.PropertyField(skeletonDataAsset);
+					const string ReloadButtonLabel = "Reload";
+					float reloadWidth = GUI.skin.label.CalcSize(new GUIContent(ReloadButtonLabel)).x + 20;
+					if (GUILayout.Button(ReloadButtonLabel, GUILayout.Width(reloadWidth))) {
+						foreach (var c in targets) {
+							var component = c as SkeletonRenderer;
+							if (component.skeletonDataAsset != null) {
+								foreach (AtlasAsset aa in component.skeletonDataAsset.atlasAssets) {
+									if (aa != null)
+										aa.Reset();
+								}
+								component.skeletonDataAsset.Reset();
+							}
+							component.Initialize(true);
+						}
+					}
+				}
+
+				foreach (var c in targets) {
+					var component = c as SkeletonRenderer;
+					if (!component.valid) {
+						component.Initialize(true);
+						component.LateUpdate();
+						if (!component.valid)
+							continue;
+					}
+
+					#if NO_PREFAB_MESH
+					if (isInspectingPrefab) {
+						MeshFilter meshFilter = component.GetComponent<MeshFilter>();
+						if (meshFilter != null)
+							meshFilter.sharedMesh = null;
+					}
+					#endif
+				}
+					
+				if (valid)
+					EditorGUILayout.PropertyField(initialSkinName);
+			} else {
+				var component = (SkeletonRenderer)target;
+
+				using (new EditorGUILayout.HorizontalScope()) {
+					EditorGUILayout.PropertyField(skeletonDataAsset);
+					if (valid) {
+						const string ReloadButtonLabel = "Reload";
+						float reloadWidth = GUI.skin.label.CalcSize(new GUIContent(ReloadButtonLabel)).x + 20;
+						if (GUILayout.Button(ReloadButtonLabel, GUILayout.Width(reloadWidth))) {
+							if (component.skeletonDataAsset != null) {
+								foreach (AtlasAsset aa in component.skeletonDataAsset.atlasAssets) {
+									if (aa != null)
+										aa.Reset();
+								}
+								component.skeletonDataAsset.Reset();
+							}
+							component.Initialize(true);
 						}
-						component.skeletonDataAsset.Reset();
 					}
+				}
+
+				if (!component.valid) {
 					component.Initialize(true);
+					component.LateUpdate();
+					if (!component.valid) {
+						EditorGUILayout.HelpBox("Skeleton Data Asset required", MessageType.Warning);
+						return;
+					}
 				}
-			}
 
-			if (!component.valid) {
-				component.Initialize(true);
-				component.LateUpdate();
-				if (!component.valid)
-					return;
-			}
+				#if NO_PREFAB_MESH
+				if (isInspectingPrefab) {
+					MeshFilter meshFilter = component.GetComponent<MeshFilter>();
+					if (meshFilter != null)
+						meshFilter.sharedMesh = null;
+				}
+				#endif
 
-			#if NO_PREFAB_MESH
-			if (meshFilter == null)
-				meshFilter = component.GetComponent<MeshFilter>();
-
-			if (isInspectingPrefab)
-				meshFilter.sharedMesh = null;
-			#endif
-
-			// Initial skin name.
-			{
-				string[] skins = new string[component.skeleton.Data.Skins.Count];
-				int skinIndex = 0;
-				for (int i = 0; i < skins.Length; i++) {
-					string skinNameString = component.skeleton.Data.Skins.Items[i].Name;
-					skins[i] = skinNameString;
-					if (skinNameString == initialSkinName.stringValue)
-						skinIndex = i;
+				// Initial skin name.
+				if (valid) {
+					string[] skins = new string[component.skeleton.Data.Skins.Count];
+					int skinIndex = 0;
+					for (int i = 0; i < skins.Length; i++) {
+						string skinNameString = component.skeleton.Data.Skins.Items[i].Name;
+						skins[i] = skinNameString;
+						if (skinNameString == initialSkinName.stringValue)
+							skinIndex = i;
+					}
+					skinIndex = EditorGUILayout.Popup("Initial Skin", skinIndex, skins);			
+					initialSkinName.stringValue = skins[skinIndex];
 				}
-				skinIndex = EditorGUILayout.Popup("Initial Skin", skinIndex, skins);			
-				initialSkinName.stringValue = skins[skinIndex];
 			}
 
 			EditorGUILayout.Space();
@@ -139,6 +199,8 @@ namespace Spine.Unity.Editor {
 			// Sorting Layers
 			SpineInspectorUtility.SortingPropertyFields(sortingProperties, applyModifiedProperties: true);
 
+			if (!valid) return;
+			
 			// More Render Options...
 			using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) {
 				EditorGUI.indentLevel++;
@@ -183,14 +245,47 @@ namespace Spine.Unity.Editor {
 			}
 		}
 
+		public void DrawSkeletonUtilityButton (bool multi) {
+			var buttonContent = new GUIContent("Add Skeleton Utility", SpineEditorUtilities.Icons.skeletonUtility);
+			if (multi) {
+				// Support multi-edit SkeletonUtility button.
+				//	EditorGUILayout.Space();
+				//	bool addSkeletonUtility = GUILayout.Button(buttonContent, GUILayout.Height(30));
+				//	foreach (var t in targets) {
+				//		var component = t as SkeletonAnimation;
+				//		if (addSkeletonUtility && component.GetComponent<SkeletonUtility>() == null)
+				//			component.gameObject.AddComponent<SkeletonUtility>();
+				//	}
+			} else {
+				EditorGUILayout.Space();
+				var component = (SkeletonAnimation)target;
+				if (component.GetComponent<SkeletonUtility>() == null) {						
+					if (GUILayout.Button(buttonContent, GUILayout.Height(30)))
+						component.gameObject.AddComponent<SkeletonUtility>();
+				}
+			}
+		}
+
 		override public void OnInspectorGUI () {
 			//serializedObject.Update();
-			DrawInspectorGUI();
+			bool multi = serializedObject.isEditingMultipleObjects;
+			DrawInspectorGUI(multi);
 			if (serializedObject.ApplyModifiedProperties() ||
 				(UnityEngine.Event.current.type == EventType.ValidateCommand && UnityEngine.Event.current.commandName == "UndoRedoPerformed")
 			) {
-				if (!Application.isPlaying)
-					((SkeletonRenderer)target).Initialize(true);
+				if (!Application.isPlaying) {
+					if (multi) {
+						foreach (var o in targets) {
+							var sr = o as SkeletonRenderer;
+							sr.Initialize(true);
+						}
+					} else {
+						((SkeletonRenderer)target).Initialize(true);
+					}
+
+				}
+					
+					
 			}
 		}
 

+ 4 - 2
spine-unity/Assets/spine-unity/Editor/SpineAttributeDrawers.cs

@@ -53,8 +53,11 @@ namespace Spine.Unity.Editor {
 		internal const string NoneLabel = "<None>";
 
 		protected T TargetAttribute { get { return (T)attribute; } }
+		protected SerializedProperty SerializedProperty { get; private set; }
 
 		public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) {
+			SerializedProperty = property;
+
 			if (property.propertyType != SerializedPropertyType.String) {
 				EditorGUI.LabelField(position, "ERROR:", "May only apply to type string");
 				return;
@@ -87,7 +90,7 @@ namespace Spine.Unity.Editor {
 
 			position = EditorGUI.PrefixLabel(position, label);
 
-			var propertyStringValue = property.stringValue;
+			var propertyStringValue = (property.hasMultipleDifferentValues) ? SpineInspectorUtility.EmDash : property.stringValue;
 			if (GUI.Button(position, string.IsNullOrEmpty(propertyStringValue) ? NoneLabel : propertyStringValue, EditorStyles.popup))
 				Selector(property);
 
@@ -302,7 +305,6 @@ namespace Spine.Unity.Editor {
 
 	[CustomPropertyDrawer(typeof(SpineBone))]
 	public class SpineBoneDrawer : SpineTreeItemDrawerBase<SpineBone> {
-
 		protected override void PopulateMenu (GenericMenu menu, SerializedProperty property, SpineBone targetAttribute, SkeletonData data) {
 			menu.AddDisabledItem(new GUIContent(skeletonDataAsset.name));
 			menu.AddSeparator("");

+ 1 - 1
spine-unity/Assets/spine-unity/Editor/SpineEditorUtilities.cs

@@ -1162,7 +1162,7 @@ namespace Spine.Unity.Editor {
 				var skeletonInfo = (Dictionary<string, object>)root["skeleton"];
 				object jv;
 				skeletonInfo.TryGetValue("spine", out jv);
-				string jsonVersion = (jv == null) ? (string)jv : null;
+				string jsonVersion = jv as string;
 				if (!string.IsNullOrEmpty(jsonVersion)) {
 					string[] jsonVersionSplit = jsonVersion.Split('.');
 					bool match = false;

+ 57 - 5
spine-unity/Assets/spine-unity/Editor/SpineInspectorUtility.cs

@@ -30,6 +30,7 @@
 
 using UnityEngine;
 using UnityEditor;
+using System.Collections.Generic;
 using System.Reflection;
 
 namespace Spine.Unity.Editor {
@@ -43,6 +44,10 @@ namespace Spine.Unity.Editor {
 			return n == 1 ? "" : "s";
 		}
 
+		public static string EmDash {
+			get { return "\u2014"; }
+		}
+
 		public static void PropertyFieldWideLabel (SerializedProperty property, GUIContent label = null, float minimumLabelWidth = 150) {
 			using (new EditorGUILayout.HorizontalScope()) {
 				GUILayout.Label(label ?? new GUIContent(property.displayName, property.tooltip), GUILayout.MinWidth(minimumLabelWidth));
@@ -70,25 +75,72 @@ namespace Spine.Unity.Editor {
 			public SerializedProperty sortingLayerID;
 			public SerializedProperty sortingOrder;
 
-			public SerializedSortingProperties (Renderer r) {
-				renderer = new SerializedObject(r);
+			public SerializedSortingProperties (Renderer r) : this(new SerializedObject(r)) {}
+			public SerializedSortingProperties (Object[] renderers) : this(new SerializedObject(renderers)) {}
+			public SerializedSortingProperties (SerializedObject rendererSerializedObject) {
+				renderer = rendererSerializedObject;
 				sortingLayerID = renderer.FindProperty("m_SortingLayerID");
 				sortingOrder = renderer.FindProperty("m_SortingOrder");
 			}
 
 			public void ApplyModifiedProperties () {
 				renderer.ApplyModifiedProperties();
+				this.SetDirty();
+			}
+
+			internal void SetDirty () {
+				if (renderer.isEditingMultipleObjects)
+					foreach (var o in renderer.targetObjects)
+						EditorUtility.SetDirty(o);
+				else
+					EditorUtility.SetDirty(renderer.targetObject);
+			}
+		}
+
+		public static SerializedObject GetRenderersSerializedObject (SerializedObject serializedObject) {
+			if (serializedObject.isEditingMultipleObjects) {
+				var renderers = new List<Object>();
+				foreach (var o in serializedObject.targetObjects) {
+					var component = o as Component;
+					if (component != null) {
+						var renderer = component.GetComponent<Renderer>();
+						if (renderer != null) 
+							renderers.Add(renderer);
+					}
+				}
+				return new SerializedObject(renderers.ToArray());
+			} else {
+				var component = serializedObject.targetObject as Component;
+				if (component != null) {
+					var renderer = component.GetComponent<Renderer>();
+					if (renderer != null)
+						return new SerializedObject(renderer);
+				}
+			}
+
+			return null;
+		}
+
+		public static bool TargetsUseSameData (SerializedObject so) {
+			bool multi = so.isEditingMultipleObjects;
+			if (multi) {
+				int n = so.targetObjects.Length;
+				var first = so.targetObjects[0] as SkeletonRenderer;
+				for (int i = 1; i < n; i++) {
+					var sr = so.targetObjects[i] as SkeletonRenderer;
+					if (sr != null && sr.skeletonDataAsset != first.skeletonDataAsset)
+						return false;
+				}
 			}
+			return true;
 		}
 
 		public static void SortingPropertyFields (SerializedSortingProperties prop, bool applyModifiedProperties) {
 			if (applyModifiedProperties) {
 				EditorGUI.BeginChangeCheck();
 				SortingPropertyFields(prop.sortingLayerID, prop.sortingOrder);
-				if(EditorGUI.EndChangeCheck()) {
+				if(EditorGUI.EndChangeCheck())
 					prop.ApplyModifiedProperties();
-					EditorUtility.SetDirty(prop.renderer.targetObject);
-				}
 			} else {
 				SortingPropertyFields(prop.sortingLayerID, prop.sortingOrder);
 			}

+ 1 - 0
spine-unity/Assets/spine-unity/Modules/SkeletonGraphic/Editor/SkeletonGraphicInspector.cs

@@ -40,6 +40,7 @@ namespace Spine.Unity.Editor {
 
 	[InitializeOnLoad]
 	[CustomEditor(typeof(SkeletonGraphic))]
+	[CanEditMultipleObjects]
 	public class SkeletonGraphicInspector : UnityEditor.Editor {
 		SerializedProperty material_, color_;
 		SerializedProperty skeletonDataAsset_, initialSkinName_;

+ 15 - 12
spine-unity/Assets/spine-unity/SkeletonUtility/Editor/SkeletonUtilityInspector.cs

@@ -114,6 +114,8 @@ namespace Spine.Unity.Editor {
 				skeleton = skeletonRenderer.skeleton;
 			}
 
+			if (!skeletonRenderer.valid) return;
+
 			UpdateAttachments();
 			isPrefab |= PrefabUtility.GetPrefabType(this.target) == PrefabType.Prefab;
 		}
@@ -142,7 +144,6 @@ namespace Spine.Unity.Editor {
 
 		void UpdateAttachments () {
 			attachmentTable = new Dictionary<Slot, List<Attachment>>();
-
 			Skin skin = skeleton.Skin ?? skeletonRenderer.skeletonDataAsset.GetSkeletonData(true).DefaultSkin;
 			for (int i = skeleton.Slots.Count-1; i >= 0; i--) {
 				List<Attachment> attachments = new List<Attachment>();
@@ -176,20 +177,22 @@ namespace Spine.Unity.Editor {
 				return;
 			}
 
-			skeletonUtility.boneRoot = (Transform)EditorGUILayout.ObjectField("Bone Root", skeletonUtility.boneRoot, typeof(Transform), true);
-
-			GUILayout.BeginHorizontal();
-			EditorGUI.BeginDisabledGroup(skeletonUtility.boneRoot != null);
-			{
-				if (GUILayout.Button(new GUIContent("Spawn Hierarchy", SpineEditorUtilities.Icons.skeleton), GUILayout.Width(150), GUILayout.Height(24)))
-					SpawnHierarchyContextMenu();
+			if (!skeletonRenderer.valid) {
+				GUILayout.Label(new GUIContent("Spine Component invalid. Check Skeleton Data Asset.", SpineEditorUtilities.Icons.warning));
+				return;	
 			}
-			EditorGUI.EndDisabledGroup();
 
-			//		if (GUILayout.Button(new GUIContent("Spawn Submeshes", SpineEditorUtilities.Icons.subMeshRenderer), GUILayout.Width(150), GUILayout.Height(24)))
-			//			skeletonUtility.SpawnSubRenderers(true);
+			skeletonUtility.boneRoot = (Transform)EditorGUILayout.ObjectField("Bone Root", skeletonUtility.boneRoot, typeof(Transform), true);
+
+			using (new GUILayout.HorizontalScope()) {
+				using (new EditorGUI.DisabledGroupScope(skeletonUtility.boneRoot != null)) {
+					if (GUILayout.Button(new GUIContent("Spawn Hierarchy", SpineEditorUtilities.Icons.skeleton), GUILayout.Width(150), GUILayout.Height(24)))
+						SpawnHierarchyContextMenu();
+				}
 
-			GUILayout.EndHorizontal();
+				// if (GUILayout.Button(new GUIContent("Spawn Submeshes", SpineEditorUtilities.Icons.subMeshRenderer), GUILayout.Width(150), GUILayout.Height(24)))
+				// skeletonUtility.SpawnSubRenderers(true);
+			}
 
 			EditorGUI.BeginChangeCheck();
 			skeleton.FlipX = EditorGUILayout.ToggleLeft("Flip X", skeleton.FlipX);