SkeletonDataAssetInspector for Spine 3.5 that allows both Spine.Unity.AtlasAssets and TK2D sprite collections.
/****************************************************************************** | |
* Spine Runtimes Software License v2.5 | |
* | |
* Copyright (c) 2013-2016, Esoteric Software | |
* All rights reserved. | |
* | |
* You are granted a perpetual, non-exclusive, non-sublicensable, and | |
* non-transferable license to use, install, execute, and perform the Spine | |
* Runtimes software and derivative works solely for personal or internal | |
* use. Without the written permission of Esoteric Software (see Section 2 of | |
* the Spine Software License Agreement), you may not (a) modify, translate, | |
* adapt, or develop new applications using the Spine Runtimes or otherwise | |
* create derivative works or improvements of the Spine Runtimes or (b) remove, | |
* delete, alter, or obscure any trademarks or any copyright, trademark, patent, | |
* or other intellectual property or proprietary rights notices on or in the | |
* Software, including any copy thereof. Redistributions in binary or source | |
* form must include this license and terms. | |
* | |
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR | |
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF | |
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO | |
* EVENT SHALL ESOTERIC SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, | |
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF | |
* USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER | |
* IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | |
* POSSIBILITY OF SUCH DAMAGE. | |
*****************************************************************************/ | |
// Contributed by: Mitch Thompson | |
#define SPINE_SKELETON_ANIMATOR | |
//#define SPINE_BAKING | |
using System; | |
using System.Collections.Generic; | |
using UnityEditor; | |
using UnityEngine; | |
using Spine; | |
namespace Spine.Unity.Editor { | |
using Event = UnityEngine.Event; | |
using Icons = SpineEditorUtilities.Icons; | |
[CustomEditor(typeof(SkeletonDataAsset)), CanEditMultipleObjects] | |
public class SkeletonDataAssetInspector : UnityEditor.Editor { | |
static bool showAnimationStateData = true; | |
static bool showAnimationList = true; | |
static bool showSlotList = false; | |
static bool showAttachments = false; | |
#if SPINE_BAKING | |
static bool isBakingExpanded = false; | |
static bool bakeAnimations = true; | |
static bool bakeIK = true; | |
static SendMessageOptions bakeEventOptions = SendMessageOptions.DontRequireReceiver; | |
const string ShowBakingPrefsKey = "SkeletonDataAssetInspector_showUnity"; | |
#endif | |
SerializedProperty atlasAssets, skeletonJSON, scale, fromAnimation, toAnimation, duration, defaultMix; | |
#if SPINE_TK2D | |
SerializedProperty spriteCollection; | |
#endif | |
#if SPINE_SKELETON_ANIMATOR | |
static bool isMecanimExpanded = false; | |
SerializedProperty controller; | |
#endif | |
bool m_initialized = false; | |
SkeletonDataAsset m_skeletonDataAsset; | |
SkeletonData m_skeletonData; | |
string m_skeletonDataAssetGUID; | |
bool needToSerialize; | |
readonly List<string> warnings = new List<string>(); | |
GUIStyle activePlayButtonStyle, idlePlayButtonStyle; | |
readonly GUIContent DefaultMixLabel = new GUIContent("Default Mix Duration", "Sets 'SkeletonDataAsset.defaultMix' in the asset and 'AnimationState.data.defaultMix' at runtime load time."); | |
void OnEnable () { | |
SpineEditorUtilities.ConfirmInitialization(); | |
m_skeletonDataAsset = (SkeletonDataAsset)target; | |
atlasAssets = serializedObject.FindProperty("atlasAssets"); | |
skeletonJSON = serializedObject.FindProperty("skeletonJSON"); | |
scale = serializedObject.FindProperty("scale"); | |
fromAnimation = serializedObject.FindProperty("fromAnimation"); | |
toAnimation = serializedObject.FindProperty("toAnimation"); | |
duration = serializedObject.FindProperty("duration"); | |
defaultMix = serializedObject.FindProperty("defaultMix"); | |
#if SPINE_SKELETON_ANIMATOR | |
controller = serializedObject.FindProperty("controller"); | |
#endif | |
#if SPINE_TK2D | |
atlasAssets.isExpanded = false; | |
spriteCollection = serializedObject.FindProperty("spriteCollection"); | |
#else | |
atlasAssets.isExpanded = true; | |
#endif | |
#if SPINE_BAKING | |
isBakingExpanded = EditorPrefs.GetBool(ShowBakingPrefsKey, false); | |
#endif | |
m_skeletonDataAssetGUID = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(m_skeletonDataAsset)); | |
EditorApplication.update += EditorUpdate; | |
RepopulateWarnings(); | |
m_skeletonData = warnings.Count == 0 ? m_skeletonDataAsset.GetSkeletonData(false) : null; | |
} | |
void OnDestroy () { | |
m_initialized = false; | |
EditorApplication.update -= EditorUpdate; | |
this.DestroyPreviewInstances(); | |
if (this.m_previewUtility != null) { | |
this.m_previewUtility.Cleanup(); | |
this.m_previewUtility = null; | |
} | |
} | |
override public void OnInspectorGUI () { | |
if (serializedObject.isEditingMultipleObjects) { | |
using (new SpineInspectorUtility.BoxScope()) { | |
EditorGUILayout.LabelField("SkeletonData", EditorStyles.boldLabel); | |
EditorGUILayout.PropertyField(skeletonJSON, new GUIContent(skeletonJSON.displayName, Icons.spine)); | |
EditorGUILayout.PropertyField(scale); | |
} | |
using (new SpineInspectorUtility.BoxScope()) { | |
EditorGUILayout.LabelField("Atlas", EditorStyles.boldLabel); | |
#if !SPINE_TK2D | |
EditorGUILayout.PropertyField(atlasAssets, true); | |
#else | |
using (new EditorGUI.DisabledGroupScope(spriteCollection.objectReferenceValue != null)) { | |
EditorGUILayout.PropertyField(atlasAssets, true); | |
} | |
EditorGUILayout.LabelField("spine-tk2d", EditorStyles.boldLabel); | |
EditorGUILayout.PropertyField(spriteCollection, true); | |
#endif | |
} | |
using (new SpineInspectorUtility.BoxScope()) { | |
EditorGUILayout.LabelField("Mix Settings", EditorStyles.boldLabel); | |
SpineInspectorUtility.PropertyFieldWideLabel(defaultMix, DefaultMixLabel, 160); | |
EditorGUILayout.Space(); | |
} | |
return; | |
} | |
{ | |
// Lazy initialization because accessing EditorStyles values in OnEnable during a recompile causes UnityEditor to throw null exceptions. (Unity 5.3.5) | |
idlePlayButtonStyle = idlePlayButtonStyle ?? new GUIStyle(EditorStyles.miniButton); | |
if (activePlayButtonStyle == null) { | |
activePlayButtonStyle = new GUIStyle(idlePlayButtonStyle); | |
activePlayButtonStyle.normal.textColor = Color.red; | |
} | |
} | |
serializedObject.Update(); | |
EditorGUILayout.LabelField(new GUIContent(target.name + " (SkeletonDataAsset)", Icons.spine), EditorStyles.whiteLargeLabel); | |
if (m_skeletonData != null) { | |
EditorGUILayout.LabelField("(Drag and Drop to instantiate.)", EditorStyles.miniLabel); | |
} | |
EditorGUI.BeginChangeCheck(); | |
// SkeletonData | |
using (new SpineInspectorUtility.BoxScope()) { | |
using (new EditorGUILayout.HorizontalScope()) { | |
EditorGUILayout.LabelField("SkeletonData", EditorStyles.boldLabel); | |
// if (m_skeletonData != null) { | |
// var sd = m_skeletonData; | |
// string m = string.Format("{8} - {0} {1}\nBones: {2}\tConstraints: {5} IK + {6} Path + {7} Transform\nSlots: {3}\t\tSkins: {4}\n", | |
// sd.Version, string.IsNullOrEmpty(sd.Version) ? "" : "export", sd.Bones.Count, sd.Slots.Count, sd.Skins.Count, sd.IkConstraints.Count, sd.PathConstraints.Count, sd.TransformConstraints.Count, skeletonJSON.objectReferenceValue.name); | |
// EditorGUILayout.LabelField(new GUIContent("SkeletonData"), new GUIContent("+", m), EditorStyles.boldLabel); | |
// } | |
} | |
EditorGUILayout.PropertyField(skeletonJSON, new GUIContent(skeletonJSON.displayName, Icons.spine)); | |
EditorGUILayout.PropertyField(scale); | |
} | |
// if (m_skeletonData != null) { | |
// if (SpineInspectorUtility.CenteredButton(new GUIContent("Instantiate", Icons.spine, "Creates a new Spine GameObject in the active scene using this Skeleton Data.\nYou can also instantiate by dragging the SkeletonData asset from Project view into Scene View."))) | |
// SpineEditorUtilities.ShowInstantiateContextMenu(this.m_skeletonDataAsset, Vector3.zero); | |
// } | |
// Atlas | |
using (new SpineInspectorUtility.BoxScope()) { | |
EditorGUILayout.LabelField("Atlas", EditorStyles.boldLabel); | |
#if !SPINE_TK2D | |
EditorGUILayout.PropertyField(atlasAssets, true); | |
#else | |
using (new EditorGUI.DisabledGroupScope(spriteCollection.objectReferenceValue != null)) { | |
EditorGUILayout.PropertyField(atlasAssets, true); | |
} | |
EditorGUILayout.LabelField("spine-tk2d", EditorStyles.boldLabel); | |
EditorGUILayout.PropertyField(spriteCollection, true); | |
#endif | |
} | |
if (EditorGUI.EndChangeCheck()) { | |
if (serializedObject.ApplyModifiedProperties()) { | |
if (m_previewUtility != null) { | |
m_previewUtility.Cleanup(); | |
m_previewUtility = null; | |
} | |
m_skeletonDataAsset.Clear(); | |
m_skeletonData = null; | |
OnEnable(); // Should call RepopulateWarnings. | |
return; | |
} | |
} | |
// Some code depends on the existence of m_skeletonAnimation instance. | |
// If m_skeletonAnimation is lazy-instantiated elsewhere, this can cause contents to change between Layout and Repaint events, causing GUILayout control count errors. | |
InitPreview(); | |
if (m_skeletonData != null) { | |
GUILayout.Space(20f); | |
using (new SpineInspectorUtility.BoxScope()) { | |
EditorGUILayout.LabelField("Mix Settings", EditorStyles.boldLabel); | |
DrawAnimationStateInfo(); | |
EditorGUILayout.Space(); | |
} | |
EditorGUILayout.LabelField("Preview", EditorStyles.boldLabel); | |
DrawAnimationList(); | |
EditorGUILayout.Space(); | |
DrawSlotList(); | |
EditorGUILayout.Space(); | |
DrawUnityTools(); | |
} else { | |
#if !SPINE_TK2D | |
// Reimport Button | |
using (new EditorGUI.DisabledGroupScope(skeletonJSON.objectReferenceValue == null)) { | |
if (GUILayout.Button(new GUIContent("Attempt Reimport", Icons.warning))) { | |
DoReimport(); | |
} | |
} | |
#else | |
EditorGUILayout.HelpBox("Couldn't load SkeletonData.", MessageType.Error); | |
#endif | |
// List warnings. | |
foreach (var line in warnings) | |
EditorGUILayout.LabelField(new GUIContent(line, Icons.warning)); | |
} | |
if (!Application.isPlaying) | |
serializedObject.ApplyModifiedProperties(); | |
} | |
void DrawUnityTools () { | |
#if SPINE_SKELETON_ANIMATOR | |
using (new SpineInspectorUtility.BoxScope()) { | |
isMecanimExpanded = EditorGUILayout.Foldout(isMecanimExpanded, new GUIContent("SkeletonAnimator", Icons.unityIcon)); | |
if (isMecanimExpanded) { | |
EditorGUI.indentLevel++; | |
EditorGUILayout.PropertyField(controller, new GUIContent("Controller", Icons.controllerIcon)); | |
if (controller.objectReferenceValue == null) { | |
// Generate Mecanim Controller Button | |
using (new GUILayout.HorizontalScope()) { | |
GUILayout.Space(EditorGUIUtility.labelWidth); | |
if (GUILayout.Button(new GUIContent("Generate Mecanim Controller"), GUILayout.Height(20))) | |
SkeletonBaker.GenerateMecanimAnimationClips(m_skeletonDataAsset); | |
} | |
EditorGUILayout.HelpBox("SkeletonAnimator is the Mecanim alternative to SkeletonAnimation.\nIt is not required.", MessageType.Info); | |
} else { | |
// Update AnimationClips button. | |
using (new GUILayout.HorizontalScope()) { | |
GUILayout.Space(EditorGUIUtility.labelWidth); | |
if (GUILayout.Button(new GUIContent("Force Update AnimationClips"), GUILayout.Height(20))) | |
SkeletonBaker.GenerateMecanimAnimationClips(m_skeletonDataAsset); | |
} | |
} | |
EditorGUI.indentLevel--; | |
} | |
} | |
#endif | |
#if SPINE_BAKING | |
bool pre = isBakingExpanded; | |
isBakingExpanded = EditorGUILayout.Foldout(isBakingExpanded, new GUIContent("Baking", Icons.unityIcon)); | |
if (pre != isBakingExpanded) | |
EditorPrefs.SetBool(ShowBakingPrefsKey, isBakingExpanded); | |
if (isBakingExpanded) { | |
EditorGUI.indentLevel++; | |
const string BakingWarningMessage = | |
// "WARNING!" + | |
// "\nBaking is NOT the same as SkeletonAnimator!" + | |
// "\n\n" + | |
"The main use of Baking is to export Spine projects to be used without the Spine Runtime (ie: for sale on the Asset Store, or background objects that are animated only with a wind noise generator)" + | |
"\n\nBaking does not support the following:" + | |
"\n\tDisabled transform inheritance" + | |
"\n\tShear" + | |
"\n\tColor Keys" + | |
"\n\tDraw Order Keys" + | |
"\n\tAll Constraint types" + | |
"\n\nCurves are sampled at 60fps and are not realtime." + | |
"\nPlease read SkeletonBaker.cs comments for full details."; | |
EditorGUILayout.HelpBox(BakingWarningMessage, MessageType.Info, true); | |
EditorGUI.indentLevel++; | |
bakeAnimations = EditorGUILayout.Toggle("Bake Animations", bakeAnimations); | |
using (new EditorGUI.DisabledGroupScope(!bakeAnimations)) { | |
EditorGUI.indentLevel++; | |
bakeIK = EditorGUILayout.Toggle("Bake IK", bakeIK); | |
bakeEventOptions = (SendMessageOptions)EditorGUILayout.EnumPopup("Event Options", bakeEventOptions); | |
EditorGUI.indentLevel--; | |
} | |
// Bake Skin buttons. | |
using (new GUILayout.HorizontalScope()) { | |
if (GUILayout.Button(new GUIContent("Bake All Skins", Icons.unityIcon), GUILayout.Height(32), GUILayout.Width(150))) | |
SkeletonBaker.BakeToPrefab(m_skeletonDataAsset, m_skeletonData.Skins, "", bakeAnimations, bakeIK, bakeEventOptions); | |
if (m_skeletonAnimation != null && m_skeletonAnimation.skeleton != null) { | |
Skin bakeSkin = m_skeletonAnimation.skeleton.Skin; | |
string skinName = "<No Skin>"; | |
if (bakeSkin == null) { | |
skinName = "Default"; | |
bakeSkin = m_skeletonData.Skins.Items[0]; | |
} else | |
skinName = m_skeletonAnimation.skeleton.Skin.Name; | |
using (new GUILayout.VerticalScope()) { | |
if (GUILayout.Button(new GUIContent("Bake \"" + skinName + "\"", Icons.unityIcon), GUILayout.Height(32), GUILayout.Width(250))) | |
SkeletonBaker.BakeToPrefab(m_skeletonDataAsset, new ExposedList<Skin>(new [] { bakeSkin }), "", bakeAnimations, bakeIK, bakeEventOptions); | |
using (new GUILayout.HorizontalScope()) { | |
GUILayout.Label(new GUIContent("Skins", Icons.skinsRoot), GUILayout.Width(50)); | |
if (GUILayout.Button(skinName, EditorStyles.popup, GUILayout.Width(196))) { | |
DrawSkinDropdown(); | |
} | |
} | |
} | |
} | |
} | |
EditorGUI.indentLevel--; | |
EditorGUI.indentLevel--; | |
} | |
#endif | |
} | |
void DoReimport () { | |
SpineEditorUtilities.ImportSpineContent(new string[] { AssetDatabase.GetAssetPath(skeletonJSON.objectReferenceValue) }, true); | |
if (m_previewUtility != null) { | |
m_previewUtility.Cleanup(); | |
m_previewUtility = null; | |
} | |
OnEnable(); // Should call RepopulateWarnings. | |
EditorUtility.SetDirty(m_skeletonDataAsset); | |
} | |
void DrawAnimationStateInfo () { | |
showAnimationStateData = EditorGUILayout.Foldout(showAnimationStateData, "Animation State Data"); | |
if (!showAnimationStateData) | |
return; | |
EditorGUI.BeginChangeCheck(); | |
SpineInspectorUtility.PropertyFieldWideLabel(defaultMix, DefaultMixLabel, 160); | |
var animations = new string[m_skeletonData.Animations.Count]; | |
for (int i = 0; i < animations.Length; i++) | |
animations[i] = m_skeletonData.Animations.Items[i].Name; | |
for (int i = 0; i < fromAnimation.arraySize; i++) { | |
SerializedProperty from = fromAnimation.GetArrayElementAtIndex(i); | |
SerializedProperty to = toAnimation.GetArrayElementAtIndex(i); | |
SerializedProperty durationProp = duration.GetArrayElementAtIndex(i); | |
using (new EditorGUILayout.HorizontalScope()) { | |
from.stringValue = animations[EditorGUILayout.Popup(Math.Max(Array.IndexOf(animations, from.stringValue), 0), animations)]; | |
to.stringValue = animations[EditorGUILayout.Popup(Math.Max(Array.IndexOf(animations, to.stringValue), 0), animations)]; | |
durationProp.floatValue = EditorGUILayout.FloatField(durationProp.floatValue); | |
if (GUILayout.Button("Delete")) { | |
duration.DeleteArrayElementAtIndex(i); | |
toAnimation.DeleteArrayElementAtIndex(i); | |
fromAnimation.DeleteArrayElementAtIndex(i); | |
} | |
} | |
} | |
using (new EditorGUILayout.HorizontalScope()) { | |
EditorGUILayout.Space(); | |
if (GUILayout.Button("Add Mix")) { | |
duration.arraySize++; | |
toAnimation.arraySize++; | |
fromAnimation.arraySize++; | |
} | |
EditorGUILayout.Space(); | |
} | |
if (EditorGUI.EndChangeCheck()) { | |
m_skeletonDataAsset.FillStateData(); | |
EditorUtility.SetDirty(m_skeletonDataAsset); | |
serializedObject.ApplyModifiedProperties(); | |
needToSerialize = true; | |
} | |
} | |
void DrawAnimationList () { | |
showAnimationList = EditorGUILayout.Foldout(showAnimationList, new GUIContent(string.Format("Animations [{0}]", m_skeletonData.Animations.Count), Icons.animationRoot)); | |
if (!showAnimationList) | |
return; | |
if (m_skeletonAnimation != null && m_skeletonAnimation.state != null) { | |
if (GUILayout.Button(new GUIContent("Setup Pose", Icons.skeleton), GUILayout.Width(105), GUILayout.Height(18))) { | |
StopAnimation(); | |
m_skeletonAnimation.skeleton.SetToSetupPose(); | |
m_requireRefresh = true; | |
} | |
} else { | |
EditorGUILayout.HelpBox("Animations can be previewed if you expand the Preview window below.", MessageType.Info); | |
} | |
EditorGUILayout.LabelField("Name", "Duration"); | |
foreach (Spine.Animation animation in m_skeletonData.Animations) { | |
using (new GUILayout.HorizontalScope()) { | |
if (m_skeletonAnimation != null && m_skeletonAnimation.state != null) { | |
var activeTrack = m_skeletonAnimation.state.GetCurrent(0); | |
if (activeTrack != null && activeTrack.Animation == animation) { | |
if (GUILayout.Button("\u25BA", activePlayButtonStyle, GUILayout.Width(24))) { | |
StopAnimation(); | |
} | |
} else { | |
if (GUILayout.Button("\u25BA", idlePlayButtonStyle, GUILayout.Width(24))) { | |
PlayAnimation(animation.Name, true); | |
} | |
} | |
} else { | |
GUILayout.Label("-", GUILayout.Width(24)); | |
} | |
EditorGUILayout.LabelField(new GUIContent(animation.Name, Icons.animation), new GUIContent(animation.Duration.ToString("f3") + "s" + ("(" + (Mathf.RoundToInt(animation.Duration * 30)) + ")").PadLeft(12, ' '))); | |
} | |
} | |
} | |
void DrawSlotList () { | |
showSlotList = EditorGUILayout.Foldout(showSlotList, new GUIContent("Slots", Icons.slotRoot)); | |
if (!showSlotList) return; | |
if (m_skeletonAnimation == null || m_skeletonAnimation.skeleton == null) return; | |
EditorGUI.indentLevel++; | |
showAttachments = EditorGUILayout.ToggleLeft("Show Attachments", showAttachments); | |
var slotAttachments = new List<Attachment>(); | |
var slotAttachmentNames = new List<string>(); | |
var defaultSkinAttachmentNames = new List<string>(); | |
var defaultSkin = m_skeletonData.Skins.Items[0]; | |
Skin skin = m_skeletonAnimation.skeleton.Skin ?? defaultSkin; | |
var slotsItems = m_skeletonAnimation.skeleton.Slots.Items; | |
for (int i = m_skeletonAnimation.skeleton.Slots.Count - 1; i >= 0; i--) { | |
Slot slot = slotsItems[i]; | |
EditorGUILayout.LabelField(new GUIContent(slot.Data.Name, Icons.slot)); | |
if (showAttachments) { | |
EditorGUI.indentLevel++; | |
slotAttachments.Clear(); | |
slotAttachmentNames.Clear(); | |
defaultSkinAttachmentNames.Clear(); | |
skin.FindNamesForSlot(i, slotAttachmentNames); | |
skin.FindAttachmentsForSlot(i, slotAttachments); | |
if (skin != defaultSkin) { | |
defaultSkin.FindNamesForSlot(i, defaultSkinAttachmentNames); | |
defaultSkin.FindNamesForSlot(i, slotAttachmentNames); | |
defaultSkin.FindAttachmentsForSlot(i, slotAttachments); | |
} else { | |
defaultSkin.FindNamesForSlot(i, defaultSkinAttachmentNames); | |
} | |
for (int a = 0; a < slotAttachments.Count; a++) { | |
Attachment attachment = slotAttachments[a]; | |
string attachmentName = slotAttachmentNames[a]; | |
Texture2D icon = Icons.GetAttachmentIcon(attachment); | |
bool initialState = slot.Attachment == attachment; | |
bool toggled = EditorGUILayout.ToggleLeft(new GUIContent(attachmentName, icon), slot.Attachment == attachment); | |
if (!defaultSkinAttachmentNames.Contains(attachmentName)) { | |
Rect skinPlaceHolderIconRect = GUILayoutUtility.GetLastRect(); | |
skinPlaceHolderIconRect.width = Icons.skinPlaceholder.width; | |
skinPlaceHolderIconRect.height = Icons.skinPlaceholder.height; | |
GUI.DrawTexture(skinPlaceHolderIconRect, Icons.skinPlaceholder); | |
} | |
if (toggled != initialState) { | |
slot.Attachment = toggled ? attachment : null; | |
m_requireRefresh = true; | |
} | |
} | |
EditorGUI.indentLevel--; | |
} | |
} | |
EditorGUI.indentLevel--; | |
} | |
void RepopulateWarnings () { | |
warnings.Clear(); | |
// Clear null entries. | |
{ | |
bool hasNulls = false; | |
foreach (var a in m_skeletonDataAsset.atlasAssets) { | |
if (a == null) { | |
hasNulls = true; | |
break; | |
} | |
} | |
if (hasNulls) { | |
var trimmedAtlasAssets = new List<AtlasAsset>(); | |
foreach (var a in m_skeletonDataAsset.atlasAssets) { | |
if (a != null) trimmedAtlasAssets.Add(a); | |
} | |
m_skeletonDataAsset.atlasAssets = trimmedAtlasAssets.ToArray(); | |
} | |
serializedObject.Update(); | |
} | |
if (skeletonJSON.objectReferenceValue == null) { | |
warnings.Add("Missing Skeleton JSON"); | |
} else { | |
if (SpineEditorUtilities.IsSpineData((TextAsset)skeletonJSON.objectReferenceValue) == false) { | |
warnings.Add("Skeleton data file is not a valid JSON or binary file."); | |
} else { | |
#if SPINE_TK2D | |
bool searchForSpineAtlasAssets = true; | |
bool isSpriteCollectionNull = spriteCollection.objectReferenceValue == null; | |
if (!isSpriteCollectionNull) searchForSpineAtlasAssets = false; | |
#else | |
const bool searchForSpineAtlasAssets = true; | |
#endif | |
if (searchForSpineAtlasAssets) { | |
bool detectedNullAtlasEntry = false; | |
var atlasList = new List<Atlas>(); | |
var actualAtlasAssets = m_skeletonDataAsset.atlasAssets; | |
for (int i = 0; i < actualAtlasAssets.Length; i++) { | |
if (m_skeletonDataAsset.atlasAssets[i] == null) { | |
detectedNullAtlasEntry = true; | |
break; | |
} else { | |
atlasList.Add(actualAtlasAssets[i].GetAtlas()); | |
} | |
} | |
if (detectedNullAtlasEntry) { | |
warnings.Add("AtlasAsset elements should not be null."); | |
} else { | |
// Get requirements. | |
var missingPaths = SpineEditorUtilities.GetRequiredAtlasRegions(AssetDatabase.GetAssetPath((TextAsset)skeletonJSON.objectReferenceValue)); | |
foreach (var atlas in atlasList) { | |
for (int i = 0; i < missingPaths.Count; i++) { | |
if (atlas.FindRegion(missingPaths[i]) != null) { | |
missingPaths.RemoveAt(i); | |
i--; | |
} | |
} | |
} | |
#if SPINE_TK2D | |
if (missingPaths.Count > 0) | |
warnings.Add("Missing regions. SkeletonDataAsset requires tk2DSpriteCollectionData or Spine AtlasAssets."); | |
#endif | |
foreach (var str in missingPaths) | |
warnings.Add("Missing Region: '" + str + "'"); | |
} | |
} | |
} | |
} | |
} | |
#region Preview Window | |
PreviewRenderUtility m_previewUtility; | |
GameObject m_previewInstance; | |
Vector2 previewDir; | |
SkeletonAnimation m_skeletonAnimation; | |
static readonly int SliderHash = "Slider".GetHashCode(); | |
float m_lastTime; | |
bool m_playing; | |
bool m_requireRefresh; | |
Color m_originColor = new Color(0.3f, 0.3f, 0.3f, 1); | |
void StopAnimation () { | |
if (m_skeletonAnimation == null) { | |
Debug.LogWarning("Animation was stopped but preview doesn't exist. It's possible that the Preview Panel is closed."); | |
} | |
m_skeletonAnimation.state.ClearTrack(0); | |
m_playing = false; | |
} | |
List<Spine.Event> m_animEvents = new List<Spine.Event>(); | |
List<float> m_animEventFrames = new List<float>(); | |
void PlayAnimation (string animName, bool loop) { | |
m_animEvents.Clear(); | |
m_animEventFrames.Clear(); | |
m_skeletonAnimation.state.SetAnimation(0, animName, loop); | |
Spine.Animation a = m_skeletonAnimation.state.GetCurrent(0).Animation; | |
foreach (Timeline t in a.Timelines) { | |
if (t.GetType() == typeof(EventTimeline)) { | |
var et = (EventTimeline)t; | |
for (int i = 0; i < et.Events.Length; i++) { | |
m_animEvents.Add(et.Events[i]); | |
m_animEventFrames.Add(et.Frames[i]); | |
} | |
} | |
} | |
m_playing = true; | |
} | |
void InitPreview () { | |
if (this.m_previewUtility == null) { | |
this.m_lastTime = Time.realtimeSinceStartup; | |
this.m_previewUtility = new PreviewRenderUtility(true); | |
var c = this.m_previewUtility.m_Camera; | |
c.orthographic = true; | |
c.orthographicSize = 1; | |
c.cullingMask = -2147483648; | |
c.nearClipPlane = 0.01f; | |
c.farClipPlane = 1000f; | |
this.CreatePreviewInstances(); | |
} | |
} | |
void CreatePreviewInstances () { | |
this.DestroyPreviewInstances(); | |
if (warnings.Count > 0) { | |
m_skeletonDataAsset.Clear(); | |
return; | |
} | |
var skeletonDataAsset = (SkeletonDataAsset)target; | |
if (skeletonDataAsset.GetSkeletonData(false) == null) | |
return; | |
if (this.m_previewInstance == null) { | |
string skinName = EditorPrefs.GetString(m_skeletonDataAssetGUID + "_lastSkin", ""); | |
try { | |
m_previewInstance = SpineEditorUtilities.InstantiateSkeletonAnimation(skeletonDataAsset, skinName).gameObject; | |
if (m_previewInstance != null) { | |
m_previewInstance.hideFlags = HideFlags.HideAndDontSave; | |
m_previewInstance.layer = 0x1f; | |
m_skeletonAnimation = m_previewInstance.GetComponent<SkeletonAnimation>(); | |
m_skeletonAnimation.initialSkinName = skinName; | |
m_skeletonAnimation.LateUpdate(); | |
m_skeletonData = m_skeletonAnimation.skeletonDataAsset.GetSkeletonData(true); | |
m_previewInstance.GetComponent<Renderer>().enabled = false; | |
m_initialized = true; | |
} | |
AdjustCameraGoals(true); | |
} catch { | |
DestroyPreviewInstances(); | |
} | |
} | |
} | |
void DestroyPreviewInstances () { | |
if (this.m_previewInstance != null) { | |
DestroyImmediate(this.m_previewInstance); | |
m_previewInstance = null; | |
} | |
m_initialized = false; | |
} | |
public override bool HasPreviewGUI () { | |
if (serializedObject.isEditingMultipleObjects) { | |
// JOHN: Implement multi-preview. | |
return false; | |
} | |
for (int i = 0; i < atlasAssets.arraySize; i++) { | |
var prop = atlasAssets.GetArrayElementAtIndex(i); | |
if (prop.objectReferenceValue == null) | |
return false; | |
} | |
return skeletonJSON.objectReferenceValue != null; | |
} | |
Texture m_previewTex = new Texture(); | |
public override void OnInteractivePreviewGUI (Rect r, GUIStyle background) { | |
this.InitPreview(); | |
if (Event.current.type == EventType.Repaint) { | |
if (m_requireRefresh) { | |
this.m_previewUtility.BeginPreview(r, background); | |
this.DoRenderPreview(true); | |
this.m_previewTex = this.m_previewUtility.EndPreview(); | |
m_requireRefresh = false; | |
} | |
if (this.m_previewTex != null) | |
GUI.DrawTexture(r, m_previewTex, ScaleMode.StretchToFill, false); | |
} | |
DrawSkinToolbar(r); | |
NormalizedTimeBar(r); | |
// MITCH: left a todo: Implement panning | |
// this.previewDir = Drag2D(this.previewDir, r); | |
MouseScroll(r); | |
} | |
float m_orthoGoal = 1; | |
Vector3 m_posGoal = new Vector3(0, 0, -10); | |
double m_adjustFrameEndTime = 0; | |
void AdjustCameraGoals (bool calculateMixTime) { | |
if (this.m_previewInstance == null) | |
return; | |
if (calculateMixTime) { | |
if (m_skeletonAnimation.state.GetCurrent(0) != null) | |
m_adjustFrameEndTime = EditorApplication.timeSinceStartup + m_skeletonAnimation.state.GetCurrent(0).Alpha; | |
} | |
GameObject go = this.m_previewInstance; | |
Bounds bounds = go.GetComponent<Renderer>().bounds; | |
m_orthoGoal = bounds.size.y; | |
m_posGoal = bounds.center + new Vector3(0, 0, -10f); | |
} | |
void AdjustCameraGoals () { | |
AdjustCameraGoals(false); | |
} | |
void AdjustCamera () { | |
if (m_previewUtility == null) | |
return; | |
if (EditorApplication.timeSinceStartup < m_adjustFrameEndTime) | |
AdjustCameraGoals(); | |
float orthoSet = Mathf.Lerp(this.m_previewUtility.m_Camera.orthographicSize, m_orthoGoal, 0.1f); | |
this.m_previewUtility.m_Camera.orthographicSize = orthoSet; | |
float dist = Vector3.Distance(m_previewUtility.m_Camera.transform.position, m_posGoal); | |
if(dist > 0f) { | |
Vector3 pos = Vector3.Lerp(this.m_previewUtility.m_Camera.transform.position, m_posGoal, 0.1f); | |
pos.x = 0; | |
this.m_previewUtility.m_Camera.transform.position = pos; | |
this.m_previewUtility.m_Camera.transform.rotation = Quaternion.identity; | |
m_requireRefresh = true; | |
} | |
} | |
void DoRenderPreview (bool drawHandles) { | |
GameObject go = this.m_previewInstance; | |
if (m_requireRefresh && go != null) { | |
go.GetComponent<Renderer>().enabled = true; | |
if (!EditorApplication.isPlaying) | |
m_skeletonAnimation.Update((Time.realtimeSinceStartup - m_lastTime)); | |
m_lastTime = Time.realtimeSinceStartup; | |
if (!EditorApplication.isPlaying) | |
m_skeletonAnimation.LateUpdate(); | |
if (drawHandles) { | |
Handles.SetCamera(m_previewUtility.m_Camera); | |
Handles.color = m_originColor; | |
Handles.DrawLine(new Vector3(-1000 * m_skeletonDataAsset.scale, 0, 0), new Vector3(1000 * m_skeletonDataAsset.scale, 0, 0)); | |
Handles.DrawLine(new Vector3(0, 1000 * m_skeletonDataAsset.scale, 0), new Vector3(0, -1000 * m_skeletonDataAsset.scale, 0)); | |
} | |
this.m_previewUtility.m_Camera.Render(); | |
if (drawHandles) { | |
Handles.SetCamera(m_previewUtility.m_Camera); | |
SpineHandles.DrawBoundingBoxes(m_skeletonAnimation.transform, m_skeletonAnimation.skeleton); | |
if (showAttachments) SpineHandles.DrawPaths(m_skeletonAnimation.transform, m_skeletonAnimation.skeleton); | |
} | |
go.GetComponent<Renderer>().enabled = false; | |
} | |
} | |
void EditorUpdate () { | |
AdjustCamera(); | |
if (m_playing) { | |
m_requireRefresh = true; | |
Repaint(); | |
} else if (m_requireRefresh) { | |
Repaint(); | |
} | |
//else { | |
//only needed if using smooth menus | |
//} | |
if (needToSerialize) { | |
needToSerialize = false; | |
serializedObject.ApplyModifiedProperties(); | |
} | |
} | |
void DrawSkinToolbar (Rect r) { | |
if (m_skeletonAnimation == null) | |
return; | |
if (m_skeletonAnimation.skeleton != null) { | |
string label = (m_skeletonAnimation.skeleton != null && m_skeletonAnimation.skeleton.Skin != null) ? m_skeletonAnimation.skeleton.Skin.Name : "default"; | |
Rect popRect = new Rect(r); | |
popRect.y += 32; | |
popRect.x += 4; | |
popRect.height = 24; | |
popRect.width = 40; | |
EditorGUI.DropShadowLabel(popRect, new GUIContent("Skin", Icons.skinsRoot)); | |
popRect.y += 11; | |
popRect.width = 150; | |
popRect.x += 44; | |
if (GUI.Button(popRect, label, EditorStyles.popup)) { | |
DrawSkinDropdown(); | |
} | |
} | |
} | |
void NormalizedTimeBar (Rect r) { | |
if (m_skeletonAnimation == null) | |
return; | |
Rect barRect = new Rect(r); | |
barRect.height = 32; | |
barRect.x += 4; | |
barRect.width -= 4; | |
GUI.Box(barRect, ""); | |
Rect lineRect = new Rect(barRect); | |
float width = lineRect.width; | |
TrackEntry t = m_skeletonAnimation.state.GetCurrent(0); | |
if (t != null) { | |
int loopCount = (int)(t.TrackTime / t.TrackEnd); | |
float currentTime = t.TrackTime - (t.TrackEnd * loopCount); | |
float normalizedTime = currentTime / t.Animation.Duration; | |
float wrappedTime = normalizedTime % 1; | |
lineRect.x = barRect.x + (width * wrappedTime) - 0.5f; | |
lineRect.width = 2; | |
GUI.color = Color.red; | |
GUI.DrawTexture(lineRect, EditorGUIUtility.whiteTexture); | |
GUI.color = Color.white; | |
for (int i = 0; i < m_animEvents.Count; i++) { | |
float fr = m_animEventFrames[i]; | |
var evRect = new Rect(barRect); | |
evRect.x = Mathf.Clamp(((fr / t.Animation.Duration) * width) - (Icons.userEvent.width / 2), barRect.x, float.MaxValue); | |
evRect.width = Icons.userEvent.width; | |
evRect.height = Icons.userEvent.height; | |
evRect.y += Icons.userEvent.height; | |
GUI.DrawTexture(evRect, Icons.userEvent); | |
Event ev = Event.current; | |
if (ev.type == EventType.Repaint) { | |
if (evRect.Contains(ev.mousePosition)) { | |
Rect tooltipRect = new Rect(evRect); | |
GUIStyle tooltipStyle = EditorStyles.helpBox; | |
tooltipRect.width = tooltipStyle.CalcSize(new GUIContent(m_animEvents[i].Data.Name)).x; | |
tooltipRect.y -= 4; | |
tooltipRect.x += 4; | |
GUI.Label(tooltipRect, m_animEvents[i].Data.Name, tooltipStyle); | |
GUI.tooltip = m_animEvents[i].Data.Name; | |
} | |
} | |
} | |
} | |
} | |
void MouseScroll (Rect position) { | |
Event current = Event.current; | |
int controlID = GUIUtility.GetControlID(SliderHash, FocusType.Passive); | |
switch (current.GetTypeForControl(controlID)) { | |
case EventType.ScrollWheel: | |
if (position.Contains(current.mousePosition)) { | |
m_orthoGoal += current.delta.y * 0.06f; | |
m_orthoGoal = Mathf.Max(0.01f, m_orthoGoal); | |
GUIUtility.hotControl = controlID; | |
current.Use(); | |
} | |
break; | |
} | |
} | |
// MITCH: left todo: Implement preview panning | |
/* | |
static Vector2 Drag2D(Vector2 scrollPosition, Rect position) | |
{ | |
int controlID = GUIUtility.GetControlID(sliderHash, FocusType.Passive); | |
UnityEngine.Event current = UnityEngine.Event.current; | |
switch (current.GetTypeForControl(controlID)) | |
{ | |
case EventType.MouseDown: | |
if (position.Contains(current.mousePosition) && (position.width > 50f)) | |
{ | |
GUIUtility.hotControl = controlID; | |
current.Use(); | |
EditorGUIUtility.SetWantsMouseJumping(1); | |
} | |
return scrollPosition; | |
case EventType.MouseUp: | |
if (GUIUtility.hotControl == controlID) | |
{ | |
GUIUtility.hotControl = 0; | |
} | |
EditorGUIUtility.SetWantsMouseJumping(0); | |
return scrollPosition; | |
case EventType.MouseMove: | |
return scrollPosition; | |
case EventType.MouseDrag: | |
if (GUIUtility.hotControl == controlID) | |
{ | |
scrollPosition -= (Vector2) (((current.delta * (!current.shift ? ((float) 1) : ((float) 3))) / Mathf.Min(position.width, position.height)) * 140f); | |
scrollPosition.y = Mathf.Clamp(scrollPosition.y, -90f, 90f); | |
current.Use(); | |
GUI.changed = true; | |
} | |
return scrollPosition; | |
} | |
return scrollPosition; | |
} | |
*/ | |
public override GUIContent GetPreviewTitle () { | |
return new GUIContent("Preview"); | |
} | |
public override void OnPreviewSettings () { | |
const float SliderWidth = 100; | |
if (!m_initialized) { | |
GUILayout.HorizontalSlider(0, 0, 2, GUILayout.MaxWidth(SliderWidth)); | |
} else { | |
float speed = GUILayout.HorizontalSlider(m_skeletonAnimation.timeScale, 0, 2, GUILayout.MaxWidth(SliderWidth)); | |
const float SliderSnap = 0.25f; | |
float y = speed / SliderSnap; | |
int q = Mathf.RoundToInt(y); | |
speed = q * SliderSnap; | |
m_skeletonAnimation.timeScale = speed; | |
} | |
} | |
public override Texture2D RenderStaticPreview (string assetPath, UnityEngine.Object[] subAssets, int width, int height) { | |
var tex = new Texture2D(width, height, TextureFormat.ARGB32, false); | |
this.InitPreview(); | |
if (this.m_previewUtility.m_Camera == null) | |
return null; | |
m_requireRefresh = true; | |
this.DoRenderPreview(false); | |
AdjustCameraGoals(false); | |
this.m_previewUtility.m_Camera.orthographicSize = m_orthoGoal / 2; | |
this.m_previewUtility.m_Camera.transform.position = m_posGoal; | |
this.m_previewUtility.BeginStaticPreview(new Rect(0, 0, width, height)); | |
this.DoRenderPreview(false); | |
tex = this.m_previewUtility.EndStaticPreview(); | |
return tex; | |
} | |
#endregion | |
#region Skin Dropdown Context Menu | |
void DrawSkinDropdown () { | |
var menu = new GenericMenu(); | |
foreach (Skin s in m_skeletonData.Skins) | |
menu.AddItem(new GUIContent(s.Name), this.m_skeletonAnimation.skeleton.Skin == s, SetSkin, s); | |
menu.ShowAsContext(); | |
} | |
void SetSkin (object o) { | |
Skin skin = (Skin)o; | |
m_skeletonAnimation.initialSkinName = skin.Name; | |
m_skeletonAnimation.Initialize(true); | |
m_requireRefresh = true; | |
EditorPrefs.SetString(m_skeletonDataAssetGUID + "_lastSkin", skin.Name); | |
} | |
#endregion | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment