/****************************************************************************** * Spine Runtimes License Agreement * Last updated January 1, 2020. Replaces all prior versions. * * Copyright (c) 2013-2020, Esoteric Software LLC * * Integration of the Spine Runtimes into software or otherwise creating * derivative works of the Spine Runtimes is permitted under the terms and * conditions of Section 2 of the Spine Editor License Agreement: * http://esotericsoftware.com/spine-editor-license * * Otherwise, it is permitted to integrate the Spine Runtimes into software * or otherwise create derivative works of the Spine Runtimes (collectively, * "Products"), provided that each user of the Products must obtain their own * Spine Editor license and redistribution of the Products in any form must * include this license and copyright notice. * * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "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 LLC 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 * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ #if UNITY_2018_3 || UNITY_2019 || UNITY_2018_3_OR_NEWER #define NEW_PREFAB_SYSTEM #endif using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; namespace Spine.Unity { #if NEW_PREFAB_SYSTEM [ExecuteAlways] #else [ExecuteInEditMode] #endif [RequireComponent(typeof(CanvasRenderer), typeof(RectTransform)), DisallowMultipleComponent] [AddComponentMenu("Spine/SkeletonGraphic (Unity UI Canvas)")] [HelpURL("http://esotericsoftware.com/spine-unity#SkeletonGraphic-Component")] public class SkeletonGraphic : MaskableGraphic, ISkeletonComponent, IAnimationStateComponent, ISkeletonAnimation, IHasSkeletonDataAsset { #region Inspector public SkeletonDataAsset skeletonDataAsset; public SkeletonDataAsset SkeletonDataAsset { get { return skeletonDataAsset; } } [SpineSkin(dataField:"skeletonDataAsset", defaultAsEmptyString:true)] public string initialSkinName; public bool initialFlipX, initialFlipY; [SpineAnimation(dataField:"skeletonDataAsset")] public string startingAnimation; public bool startingLoop; public float timeScale = 1f; public bool freeze; /// Update mode to optionally limit updates to e.g. only apply animations but not update the mesh. public UpdateMode UpdateMode { get { return updateMode; } set { updateMode = value; } } protected UpdateMode updateMode = UpdateMode.FullUpdate; /// Update mode used when the MeshRenderer becomes invisible /// (when OnBecameInvisible() is called). Update mode is automatically /// reset to UpdateMode.FullUpdate when the mesh becomes visible again. public UpdateMode updateWhenInvisible = UpdateMode.FullUpdate; public bool unscaledTime; public bool allowMultipleCanvasRenderers = false; public List canvasRenderers = new List(); protected List rawImages = new List(); protected int usedRenderersCount = 0; // Submesh Separation public const string SeparatorPartGameObjectName = "Part"; /// Slot names used to populate separatorSlots list when the Skeleton is initialized. Changing this after initialization does nothing. [SerializeField] [SpineSlot] protected string[] separatorSlotNames = new string[0]; /// Slots that determine where the render is split. This is used by components such as SkeletonRenderSeparator so that the skeleton can be rendered by two separate renderers on different GameObjects. [System.NonSerialized] public readonly List separatorSlots = new List(); public bool enableSeparatorSlots = false; [SerializeField] protected List separatorParts = new List(); public List SeparatorParts { get { return separatorParts; } } public bool updateSeparatorPartLocation = true; private bool wasUpdatedAfterInit = true; private Texture baseTexture = null; #if UNITY_EDITOR protected override void OnValidate () { // This handles Scene View preview. base.OnValidate (); if (this.IsValid) { if (skeletonDataAsset == null) { Clear(); } else if (skeletonDataAsset.skeletonJSON == null) { Clear(); } else if (skeletonDataAsset.GetSkeletonData(true) != skeleton.data) { Clear(); Initialize(true); if (!allowMultipleCanvasRenderers && (skeletonDataAsset.atlasAssets.Length > 1 || skeletonDataAsset.atlasAssets[0].MaterialCount > 1)) Debug.LogError("Unity UI does not support multiple textures per Renderer. Please enable 'Advanced - Multiple CanvasRenderers' to generate the required CanvasRenderer GameObjects. Otherwise your skeleton will not be rendered correctly.", this); } else { if (freeze) return; if (!string.IsNullOrEmpty(initialSkinName)) { var skin = skeleton.data.FindSkin(initialSkinName); if (skin != null) { if (skin == skeleton.data.defaultSkin) skeleton.SetSkin((Skin)null); else skeleton.SetSkin(skin); } } // Only provide visual feedback to inspector changes in Unity Editor Edit mode. if (!Application.isPlaying) { skeleton.ScaleX = this.initialFlipX ? -1 : 1; skeleton.ScaleY = this.initialFlipY ? -1 : 1; state.ClearTrack(0); skeleton.SetToSetupPose(); if (!string.IsNullOrEmpty(startingAnimation)) { state.SetAnimation(0, startingAnimation, startingLoop); Update(0f); } } } } else { // Under some circumstances (e.g. sometimes on the first import) OnValidate is called // before SpineEditorUtilities.ImportSpineContent, causing an unnecessary exception. // The (skeletonDataAsset.skeletonJSON != null) condition serves to prevent this exception. if (skeletonDataAsset != null && skeletonDataAsset.skeletonJSON != null) Initialize(true); } } protected override void Reset () { base.Reset(); if (material == null || material.shader != Shader.Find("Spine/SkeletonGraphic")) Debug.LogWarning("SkeletonGraphic works best with the SkeletonGraphic material."); } #endif #endregion #region Runtime Instantiation /// Create a new GameObject with a SkeletonGraphic component. /// Material for the canvas renderer to use. Usually, the default SkeletonGraphic material will work. public static SkeletonGraphic NewSkeletonGraphicGameObject (SkeletonDataAsset skeletonDataAsset, Transform parent, Material material) { var sg = SkeletonGraphic.AddSkeletonGraphicComponent(new GameObject("New Spine GameObject"), skeletonDataAsset, material); if (parent != null) sg.transform.SetParent(parent, false); return sg; } /// Add a SkeletonGraphic component to a GameObject. /// Material for the canvas renderer to use. Usually, the default SkeletonGraphic material will work. public static SkeletonGraphic AddSkeletonGraphicComponent (GameObject gameObject, SkeletonDataAsset skeletonDataAsset, Material material) { var c = gameObject.AddComponent(); if (skeletonDataAsset != null) { c.material = material; c.skeletonDataAsset = skeletonDataAsset; c.Initialize(false); } return c; } #endregion #region Overrides [System.NonSerialized] readonly Dictionary customTextureOverride = new Dictionary(); /// Use this Dictionary to override a Texture with a different Texture. public Dictionary CustomTextureOverride { get { return customTextureOverride; } } [System.NonSerialized] readonly Dictionary customMaterialOverride = new Dictionary(); /// Use this Dictionary to override the Material where the Texture was used at the original atlas. public Dictionary CustomMaterialOverride { get { return customMaterialOverride; } } // This is used by the UI system to determine what to put in the MaterialPropertyBlock. Texture overrideTexture; public Texture OverrideTexture { get { return overrideTexture; } set { overrideTexture = value; canvasRenderer.SetTexture(this.mainTexture); // Refresh canvasRenderer's texture. Make sure it handles null. } } #endregion #region Internals public override Texture mainTexture { get { if (overrideTexture != null) return overrideTexture; return baseTexture; } } protected override void Awake () { base.Awake (); this.onCullStateChanged.AddListener(OnCullStateChanged); SyncRawImagesWithCanvasRenderers(); if (!this.IsValid) { #if UNITY_EDITOR // workaround for special import case of open scene where OnValidate and Awake are // called in wrong order, before setup of Spine assets. if (!Application.isPlaying) { if (this.skeletonDataAsset != null && this.skeletonDataAsset.skeletonJSON == null) return; } #endif Initialize(false); Rebuild(CanvasUpdate.PreRender); } } protected override void OnDestroy () { Clear(); base.OnDestroy(); } public override void Rebuild (CanvasUpdate update) { base.Rebuild(update); if (canvasRenderer.cull) return; if (update == CanvasUpdate.PreRender) UpdateMesh(keepRendererCount : true); if (allowMultipleCanvasRenderers) canvasRenderer.Clear(); } protected override void OnDisable () { base.OnDisable(); foreach (var canvasRenderer in canvasRenderers) { canvasRenderer.Clear(); } } public virtual void Update () { #if UNITY_EDITOR if (!Application.isPlaying) { Update(0f); return; } #endif if (freeze) return; Update(unscaledTime ? Time.unscaledDeltaTime : Time.deltaTime); } public virtual void Update (float deltaTime) { if (!this.IsValid) return; wasUpdatedAfterInit = true; if (updateMode < UpdateMode.OnlyAnimationStatus) return; UpdateAnimationStatus(deltaTime); if (updateMode == UpdateMode.OnlyAnimationStatus) return; ApplyAnimation(); } protected void SyncRawImagesWithCanvasRenderers () { rawImages.Clear(); foreach (var canvasRenderer in canvasRenderers) { var rawImage = canvasRenderer.GetComponent(); if (rawImage == null) { rawImage = canvasRenderer.gameObject.AddComponent(); rawImage.maskable = this.maskable; rawImage.raycastTarget = false; } rawImages.Add(rawImage); } } protected void UpdateAnimationStatus (float deltaTime) { deltaTime *= timeScale; skeleton.Update(deltaTime); state.Update(deltaTime); } protected void ApplyAnimation () { if (BeforeApply != null) BeforeApply(this); if (updateMode != UpdateMode.OnlyEventTimelines) state.Apply(skeleton); else state.ApplyEventTimelinesOnly(skeleton); if (UpdateLocal != null) UpdateLocal(this); skeleton.UpdateWorldTransform(); if (UpdateWorld != null) { UpdateWorld(this); skeleton.UpdateWorldTransform(); } if (UpdateComplete != null) UpdateComplete(this); } public void LateUpdate () { // instantiation can happen from Update() after this component, leading to a missing Update() call. if (!wasUpdatedAfterInit) Update(0); if (freeze) return; if (updateMode != UpdateMode.FullUpdate) return; UpdateMesh(); } protected void OnCullStateChanged (bool culled) { if (culled) OnBecameInvisible(); else OnBecameVisible(); } public void OnBecameVisible () { updateMode = UpdateMode.FullUpdate; } public void OnBecameInvisible () { updateMode = updateWhenInvisible; } public void ReapplySeparatorSlotNames () { if (!IsValid) return; separatorSlots.Clear(); for (int i = 0, n = separatorSlotNames.Length; i < n; i++) { string slotName = separatorSlotNames[i]; if (slotName == "") continue; var slot = skeleton.FindSlot(slotName); if (slot != null) { separatorSlots.Add(slot); } #if UNITY_EDITOR else { Debug.LogWarning(slotName + " is not a slot in " + skeletonDataAsset.skeletonJSON.name); } #endif } UpdateSeparatorPartParents(); } #endregion #region API protected Skeleton skeleton; public Skeleton Skeleton { get { Initialize(false); return skeleton; } set { skeleton = value; } } public SkeletonData SkeletonData { get { return skeleton == null ? null : skeleton.data; } } public bool IsValid { get { return skeleton != null; } } public delegate void SkeletonRendererDelegate (SkeletonGraphic skeletonGraphic); /// OnRebuild is raised after the Skeleton is successfully initialized. public event SkeletonRendererDelegate OnRebuild; /// OnMeshAndMaterialsUpdated is at the end of LateUpdate after the Mesh and /// all materials have been updated. public event SkeletonRendererDelegate OnMeshAndMaterialsUpdated; protected Spine.AnimationState state; public Spine.AnimationState AnimationState { get { Initialize(false); return state; } } [SerializeField] protected Spine.Unity.MeshGenerator meshGenerator = new MeshGenerator(); public Spine.Unity.MeshGenerator MeshGenerator { get { return this.meshGenerator; } } DoubleBuffered meshBuffers; SkeletonRendererInstruction currentInstructions = new SkeletonRendererInstruction(); readonly ExposedList meshes = new ExposedList(); public Mesh GetLastMesh () { return meshBuffers.GetCurrent().mesh; } public bool MatchRectTransformWithBounds () { UpdateMesh(); if (!this.allowMultipleCanvasRenderers) return MatchRectTransformSingleRenderer(); else return MatchRectTransformMultipleRenderers(); } protected bool MatchRectTransformSingleRenderer () { Mesh mesh = this.GetLastMesh(); if (mesh == null) { return false; } if (mesh.vertexCount == 0) { this.rectTransform.sizeDelta = new Vector2(50f, 50f); this.rectTransform.pivot = new Vector2(0.5f, 0.5f); return false; } mesh.RecalculateBounds(); SetRectTransformBounds(mesh.bounds); return true; } protected bool MatchRectTransformMultipleRenderers () { bool anyBoundsAdded = false; Bounds combinedBounds = new Bounds(); for (int i = 0; i < canvasRenderers.Count; ++i) { var canvasRenderer = canvasRenderers[i]; if (!canvasRenderer.gameObject.activeSelf) continue; Mesh mesh = meshes.Items[i]; if (mesh == null || mesh.vertexCount == 0) continue; mesh.RecalculateBounds(); var bounds = mesh.bounds; if (anyBoundsAdded) combinedBounds.Encapsulate(bounds); else { anyBoundsAdded = true; combinedBounds = bounds; } } if (!anyBoundsAdded) { this.rectTransform.sizeDelta = new Vector2(50f, 50f); this.rectTransform.pivot = new Vector2(0.5f, 0.5f); return false; } SetRectTransformBounds(combinedBounds); return true; } private void SetRectTransformBounds (Bounds combinedBounds) { var size = combinedBounds.size; var center = combinedBounds.center; var p = new Vector2( 0.5f - (center.x / size.x), 0.5f - (center.y / size.y) ); this.rectTransform.sizeDelta = size; this.rectTransform.pivot = p; } public event UpdateBonesDelegate BeforeApply; public event UpdateBonesDelegate UpdateLocal; public event UpdateBonesDelegate UpdateWorld; public event UpdateBonesDelegate UpdateComplete; /// Occurs after the vertex data populated every frame, before the vertices are pushed into the mesh. public event Spine.Unity.MeshGeneratorDelegate OnPostProcessVertices; public void Clear () { skeleton = null; canvasRenderer.Clear(); for (int i = 0; i < canvasRenderers.Count; ++i) canvasRenderers[i].Clear(); DestroyMeshes(); DisposeMeshBuffers(); } public void TrimRenderers () { var newList = new List(); foreach (var canvasRenderer in canvasRenderers) { if (canvasRenderer.gameObject.activeSelf) { newList.Add(canvasRenderer); } else { if (Application.isEditor && !Application.isPlaying) DestroyImmediate(canvasRenderer.gameObject); else Destroy(canvasRenderer.gameObject); } } canvasRenderers = newList; SyncRawImagesWithCanvasRenderers(); } public void Initialize (bool overwrite) { if (this.IsValid && !overwrite) return; if (this.skeletonDataAsset == null) return; var skeletonData = this.skeletonDataAsset.GetSkeletonData(false); if (skeletonData == null) return; if (skeletonDataAsset.atlasAssets.Length <= 0 || skeletonDataAsset.atlasAssets[0].MaterialCount <= 0) return; this.state = new Spine.AnimationState(skeletonDataAsset.GetAnimationStateData()); if (state == null) { Clear(); return; } this.skeleton = new Skeleton(skeletonData) { ScaleX = this.initialFlipX ? -1 : 1, ScaleY = this.initialFlipY ? -1 : 1 }; InitMeshBuffers(); baseTexture = skeletonDataAsset.atlasAssets[0].PrimaryMaterial.mainTexture; canvasRenderer.SetTexture(this.mainTexture); // Needed for overwriting initializations. // Set the initial Skin and Animation if (!string.IsNullOrEmpty(initialSkinName)) skeleton.SetSkin(initialSkinName); separatorSlots.Clear(); for (int i = 0; i < separatorSlotNames.Length; i++) separatorSlots.Add(skeleton.FindSlot(separatorSlotNames[i])); wasUpdatedAfterInit = false; if (!string.IsNullOrEmpty(startingAnimation)) { var animationObject = skeletonDataAsset.GetSkeletonData(false).FindAnimation(startingAnimation); if (animationObject != null) { state.SetAnimation(0, animationObject, startingLoop); #if UNITY_EDITOR if (!Application.isPlaying) Update(0f); #endif } } if (OnRebuild != null) OnRebuild(this); } public void UpdateMesh (bool keepRendererCount = false) { if (!this.IsValid) return; skeleton.SetColor(this.color); var currentInstructions = this.currentInstructions; if (!this.allowMultipleCanvasRenderers) { UpdateMeshSingleCanvasRenderer(); } else { UpdateMeshMultipleCanvasRenderers(currentInstructions, keepRendererCount); } if (OnMeshAndMaterialsUpdated != null) OnMeshAndMaterialsUpdated(this); } public bool HasMultipleSubmeshInstructions () { if (!IsValid) return false; return MeshGenerator.RequiresMultipleSubmeshesByDrawOrder(skeleton); } #endregion protected void InitMeshBuffers () { if (meshBuffers != null) { meshBuffers.GetNext().Clear(); meshBuffers.GetNext().Clear(); } else { meshBuffers = new DoubleBuffered(); } } protected void DisposeMeshBuffers () { if (meshBuffers != null) { meshBuffers.GetNext().Dispose(); meshBuffers.GetNext().Dispose(); meshBuffers = null; } } protected void UpdateMeshSingleCanvasRenderer () { if (canvasRenderers.Count > 0) DisableUnusedCanvasRenderers(usedCount : 0); var smartMesh = meshBuffers.GetNext(); MeshGenerator.GenerateSingleSubmeshInstruction(currentInstructions, skeleton, null); bool updateTriangles = SkeletonRendererInstruction.GeometryNotEqual(currentInstructions, smartMesh.instructionUsed); meshGenerator.Begin(); if (currentInstructions.hasActiveClipping && currentInstructions.submeshInstructions.Count > 0) { meshGenerator.AddSubmesh(currentInstructions.submeshInstructions.Items[0], updateTriangles); } else { meshGenerator.BuildMeshWithArrays(currentInstructions, updateTriangles); } if (canvas != null) meshGenerator.ScaleVertexData(canvas.referencePixelsPerUnit); if (OnPostProcessVertices != null) OnPostProcessVertices.Invoke(this.meshGenerator.Buffers); var mesh = smartMesh.mesh; meshGenerator.FillVertexData(mesh); if (updateTriangles) meshGenerator.FillTriangles(mesh); meshGenerator.FillLateVertexData(mesh); canvasRenderer.SetMesh(mesh); smartMesh.instructionUsed.Set(currentInstructions); if (currentInstructions.submeshInstructions.Count > 0) { var material = currentInstructions.submeshInstructions.Items[0].material; if (material != null && baseTexture != material.mainTexture) { baseTexture = material.mainTexture; if (overrideTexture == null) canvasRenderer.SetTexture(this.mainTexture); } } //this.UpdateMaterial(); // note: This would allocate memory. usedRenderersCount = 0; } protected void UpdateMeshMultipleCanvasRenderers (SkeletonRendererInstruction currentInstructions, bool keepRendererCount) { MeshGenerator.GenerateSkeletonRendererInstruction(currentInstructions, skeleton, null, enableSeparatorSlots ? separatorSlots : null, enableSeparatorSlots ? separatorSlots.Count > 0 : false, false); int submeshCount = currentInstructions.submeshInstructions.Count; if (keepRendererCount && submeshCount != usedRenderersCount) return; EnsureCanvasRendererCount(submeshCount); EnsureMeshesCount(submeshCount); EnsureSeparatorPartCount(); var c = canvas; float scale = (c == null) ? 100 : c.referencePixelsPerUnit; // Generate meshes. var meshesItems = meshes.Items; bool useOriginalTextureAndMaterial = (customMaterialOverride.Count == 0 && customTextureOverride.Count == 0); int separatorSlotGroupIndex = 0; Transform parent = this.separatorSlots.Count == 0 ? this.transform : this.separatorParts[0]; if (updateSeparatorPartLocation) { for (int p = 0; p < this.separatorParts.Count; ++p) { separatorParts[p].position = this.transform.position; separatorParts[p].rotation = this.transform.rotation; } } int targetSiblingIndex = 0; for (int i = 0; i < submeshCount; i++) { var submeshInstructionItem = currentInstructions.submeshInstructions.Items[i]; meshGenerator.Begin(); meshGenerator.AddSubmesh(submeshInstructionItem); var targetMesh = meshesItems[i]; meshGenerator.ScaleVertexData(scale); if (OnPostProcessVertices != null) OnPostProcessVertices.Invoke(this.meshGenerator.Buffers); meshGenerator.FillVertexData(targetMesh); meshGenerator.FillTriangles(targetMesh); meshGenerator.FillLateVertexData(targetMesh); var submeshMaterial = submeshInstructionItem.material; var canvasRenderer = canvasRenderers[i]; if (i >= usedRenderersCount) canvasRenderer.gameObject.SetActive(true); canvasRenderer.SetMesh(targetMesh); canvasRenderer.materialCount = 1; if (canvasRenderer.transform.parent != parent.transform) { canvasRenderer.transform.SetParent(parent.transform, false); canvasRenderer.transform.localPosition = Vector3.zero; } canvasRenderer.transform.SetSiblingIndex(targetSiblingIndex++); if (submeshInstructionItem.forceSeparate) { targetSiblingIndex = 0; parent = separatorParts[++separatorSlotGroupIndex]; } if (useOriginalTextureAndMaterial) canvasRenderer.SetMaterial(this.materialForRendering, submeshMaterial.mainTexture); else { var originalTexture = submeshMaterial.mainTexture; Material usedMaterial; Texture usedTexture; if (!customMaterialOverride.TryGetValue(originalTexture, out usedMaterial)) usedMaterial = material; if (!customTextureOverride.TryGetValue(originalTexture, out usedTexture)) usedTexture = originalTexture; canvasRenderer.SetMaterial(usedMaterial, usedTexture); } } DisableUnusedCanvasRenderers(usedCount : submeshCount); usedRenderersCount = submeshCount; } protected void EnsureCanvasRendererCount (int targetCount) { #if UNITY_EDITOR RemoveNullCanvasRenderers(); #endif int currentCount = canvasRenderers.Count; for (int i = currentCount; i < targetCount; ++i) { var go = new GameObject(string.Format("Renderer{0}", i), typeof(RectTransform)); go.transform.SetParent(this.transform, false); go.transform.localPosition = Vector3.zero; var canvasRenderer = go.AddComponent(); canvasRenderers.Add(canvasRenderer); var rawImage = go.AddComponent(); rawImage.maskable = this.maskable; rawImage.raycastTarget = false; rawImages.Add(rawImage); } } protected void DisableUnusedCanvasRenderers (int usedCount) { #if UNITY_EDITOR RemoveNullCanvasRenderers(); #endif for (int i = usedCount; i < canvasRenderers.Count; i++) { canvasRenderers[i].Clear(); canvasRenderers[i].gameObject.SetActive(false); } } #if UNITY_EDITOR private void RemoveNullCanvasRenderers () { if (Application.isEditor && !Application.isPlaying) { for (int i = canvasRenderers.Count - 1; i >= 0; --i) { if (canvasRenderers[i] == null) { canvasRenderers.RemoveAt(i); } } } } #endif protected void EnsureMeshesCount (int targetCount) { int oldCount = meshes.Count; meshes.EnsureCapacity(targetCount); for (int i = oldCount; i < targetCount; i++) meshes.Add(SpineMesh.NewSkeletonMesh()); } protected void DestroyMeshes () { foreach (var mesh in meshes) { #if UNITY_EDITOR if (Application.isEditor && !Application.isPlaying) UnityEngine.Object.DestroyImmediate(mesh); else UnityEngine.Object.Destroy(mesh); #else UnityEngine.Object.Destroy(mesh); #endif } meshes.Clear(); } protected void EnsureSeparatorPartCount () { #if UNITY_EDITOR RemoveNullSeparatorParts(); #endif int targetCount = separatorSlots.Count + 1; if (targetCount == 1) return; #if UNITY_EDITOR if (Application.isEditor && !Application.isPlaying) { for (int i = separatorParts.Count-1; i >= 0; --i) { if (separatorParts[i] == null) { separatorParts.RemoveAt(i); } } } #endif int currentCount = separatorParts.Count; for (int i = currentCount; i < targetCount; ++i) { var go = new GameObject(string.Format("{0}[{1}]", SeparatorPartGameObjectName, i), typeof(RectTransform)); go.transform.SetParent(this.transform, false); go.transform.localPosition = Vector3.zero; separatorParts.Add(go.transform); } } protected void UpdateSeparatorPartParents () { int usedCount = separatorSlots.Count + 1; if (usedCount == 1) { usedCount = 0; // placed directly at the SkeletonGraphic parent for (int i = 0; i < canvasRenderers.Count; ++i) { var canvasRenderer = canvasRenderers[i]; if (canvasRenderer.transform.parent.name.Contains(SeparatorPartGameObjectName)) { canvasRenderer.transform.SetParent(this.transform, false); canvasRenderer.transform.localPosition = Vector3.zero; } } } for (int i = 0; i < separatorParts.Count; ++i) { bool isUsed = i < usedCount; separatorParts[i].gameObject.SetActive(isUsed); } } #if UNITY_EDITOR private void RemoveNullSeparatorParts () { if (Application.isEditor && !Application.isPlaying) { for (int i = separatorParts.Count - 1; i >= 0; --i) { if (separatorParts[i] == null) { separatorParts.RemoveAt(i); } } } } #endif } }