diff --git a/spine-c/src/spine/Skeleton.c b/spine-c/src/spine/Skeleton.c index a193d2bf7..4af6db685 100644 --- a/spine-c/src/spine/Skeleton.c +++ b/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]]); } } diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java index 465cfb21b..8bc95c640 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java +++ b/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 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. diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java index f3806240c..12cec0d18 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java @@ -52,7 +52,7 @@ public class AnimationState { static private final Animation emptyAnimation = new Animation("", new Array(0), 0); private AnimationStateData data; - private final Array tracks = new Array(); + final Array tracks = new Array(); private final Array events = new Array(); final Array 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 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 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 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 events = mix < from.eventThreshold ? this.events : null; boolean attachments = mix < from.attachmentThreshold, drawOrder = mix < from.drawOrderThreshold; - float animationLast = from.animationLast, animationTime = from.getAnimationTime(); - Array 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 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 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(); } diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Bone.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Bone.java index 3b3d451a1..98e398107 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Bone.java +++ b/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; diff --git a/spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewer.java b/spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewer.java index 59c722885..34a8c6245 100644 --- a/spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewer.java +++ b/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(); diff --git a/spine-unity/Assets/spine-unity/Asset Types/AtlasAsset.cs b/spine-unity/Assets/spine-unity/Asset Types/AtlasAsset.cs index 07daab329..e66986163 100644 --- a/spine-unity/Assets/spine-unity/Asset Types/AtlasAsset.cs +++ b/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; } /// The atlas or null if it could not be loaded. - public Atlas GetAtlas () { + public virtual Atlas GetAtlas () { if (atlasFile == null) { Debug.LogError("Atlas file not set for atlas asset: " + name, this); Reset(); diff --git a/spine-unity/Assets/spine-unity/Editor/SkeletonAnimationInspector.cs b/spine-unity/Assets/spine-unity/Editor/SkeletonAnimationInspector.cs index 6c5faac14..f79c67ed6 100644 --- a/spine-unity/Assets/spine-unity/Editor/SkeletonAnimationInspector.cs +++ b/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() == null) { - if (GUILayout.Button(new GUIContent("Add Skeleton Utility", SpineEditorUtilities.Icons.skeletonUtility), GUILayout.Height(30))) - component.gameObject.AddComponent(); - } - } } } } diff --git a/spine-unity/Assets/spine-unity/Editor/SkeletonAnimatorInspector.cs b/spine-unity/Assets/spine-unity/Editor/SkeletonAnimatorInspector.cs index 369249e10..8204cd800 100644 --- a/spine-unity/Assets/spine-unity/Editor/SkeletonAnimatorInspector.cs +++ b/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() == null) { - if (GUILayout.Button(new GUIContent("Add Skeleton Utility", SpineEditorUtilities.Icons.skeletonUtility), GUILayout.Height(30))) { - component.gameObject.AddComponent(); - } - } - } + if (!isInspectingPrefab) + DrawSkeletonUtilityButton(multi); } } } diff --git a/spine-unity/Assets/spine-unity/Editor/SkeletonRendererInspector.cs b/spine-unity/Assets/spine-unity/Editor/SkeletonRendererInspector.cs index 37d81745f..f106870d3 100644 --- a/spine-unity/Assets/spine-unity/Editor/SkeletonRendererInspector.cs +++ b/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(); - 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); } - component.skeletonDataAsset.Reset(); } + } + + 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(); + 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); + } + } + } + + 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 (meshFilter == null) - meshFilter = component.GetComponent(); - - 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; + #if NO_PREFAB_MESH + if (isInspectingPrefab) { + MeshFilter meshFilter = component.GetComponent(); + if (meshFilter != null) + meshFilter.sharedMesh = null; + } + #endif + + // 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() == null) + // component.gameObject.AddComponent(); + // } + } else { + EditorGUILayout.Space(); + var component = (SkeletonAnimation)target; + if (component.GetComponent() == null) { + if (GUILayout.Button(buttonContent, GUILayout.Height(30))) + component.gameObject.AddComponent(); + } + } + } + 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); + } + + } + + } } diff --git a/spine-unity/Assets/spine-unity/Editor/SpineAttributeDrawers.cs b/spine-unity/Assets/spine-unity/Editor/SpineAttributeDrawers.cs index d773176e4..219488a91 100644 --- a/spine-unity/Assets/spine-unity/Editor/SpineAttributeDrawers.cs +++ b/spine-unity/Assets/spine-unity/Editor/SpineAttributeDrawers.cs @@ -53,8 +53,11 @@ namespace Spine.Unity.Editor { internal const string NoneLabel = ""; 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 { - protected override void PopulateMenu (GenericMenu menu, SerializedProperty property, SpineBone targetAttribute, SkeletonData data) { menu.AddDisabledItem(new GUIContent(skeletonDataAsset.name)); menu.AddSeparator(""); diff --git a/spine-unity/Assets/spine-unity/Editor/SpineEditorUtilities.cs b/spine-unity/Assets/spine-unity/Editor/SpineEditorUtilities.cs index ee46f4ab9..db41f7d4b 100644 --- a/spine-unity/Assets/spine-unity/Editor/SpineEditorUtilities.cs +++ b/spine-unity/Assets/spine-unity/Editor/SpineEditorUtilities.cs @@ -1162,7 +1162,7 @@ namespace Spine.Unity.Editor { var skeletonInfo = (Dictionary)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; diff --git a/spine-unity/Assets/spine-unity/Editor/SpineInspectorUtility.cs b/spine-unity/Assets/spine-unity/Editor/SpineInspectorUtility.cs index 6839d7a37..d96e5bd1c 100644 --- a/spine-unity/Assets/spine-unity/Editor/SpineInspectorUtility.cs +++ b/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(); + foreach (var o in serializedObject.targetObjects) { + var component = o as Component; + if (component != null) { + var renderer = component.GetComponent(); + 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(); + 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); } diff --git a/spine-unity/Assets/spine-unity/Modules/SkeletonGraphic/Editor/SkeletonGraphicInspector.cs b/spine-unity/Assets/spine-unity/Modules/SkeletonGraphic/Editor/SkeletonGraphicInspector.cs index c75a3e09e..2c096fa00 100644 --- a/spine-unity/Assets/spine-unity/Modules/SkeletonGraphic/Editor/SkeletonGraphicInspector.cs +++ b/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_; diff --git a/spine-unity/Assets/spine-unity/SkeletonUtility/Editor/SkeletonUtilityInspector.cs b/spine-unity/Assets/spine-unity/SkeletonUtility/Editor/SkeletonUtilityInspector.cs index daacc7fed..3b32920a8 100644 --- a/spine-unity/Assets/spine-unity/SkeletonUtility/Editor/SkeletonUtilityInspector.cs +++ b/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>(); - Skin skin = skeleton.Skin ?? skeletonRenderer.skeletonDataAsset.GetSkeletonData(true).DefaultSkin; for (int i = skeleton.Slots.Count-1; i >= 0; i--) { List attachments = new List(); @@ -176,20 +177,22 @@ namespace Spine.Unity.Editor { return; } + if (!skeletonRenderer.valid) { + GUILayout.Label(new GUIContent("Spine Component invalid. Check Skeleton Data Asset.", SpineEditorUtilities.Icons.warning)); + 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(); + 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(); + } + + // if (GUILayout.Button(new GUIContent("Spawn Submeshes", SpineEditorUtilities.Icons.subMeshRenderer), GUILayout.Width(150), GUILayout.Height(24))) + // skeletonUtility.SpawnSubRenderers(true); } - EditorGUI.EndDisabledGroup(); - - // if (GUILayout.Button(new GUIContent("Spawn Submeshes", SpineEditorUtilities.Icons.subMeshRenderer), GUILayout.Width(150), GUILayout.Height(24))) - // skeletonUtility.SpawnSubRenderers(true); - - GUILayout.EndHorizontal(); EditorGUI.BeginChangeCheck(); skeleton.FlipX = EditorGUILayout.ToggleLeft("Flip X", skeleton.FlipX);