SkeletonDataAssetInspector for Spine 3.5 that allows both Spine.Unity.AtlasAssets and TK2D sprite collections.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/****************************************************************************** | |
* 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