Initial commit: Client Doc docs Server Tools

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ud18010
2026-07-10 10:24:29 +08:00
co-authored by Cursor
commit 7e35d8da31
3374 changed files with 680813 additions and 0 deletions
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 4c4e5bff8901a8a4fa1d2ab7f5d2bda6
folderAsset: yes
timeCreated: 1686835686
licenseType: Free
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,535 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated September 24, 2021. Replaces all prior versions.
*
* Copyright (c) 2013-2021, 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
#if UNITY_2018_1_OR_NEWER
#define PER_MATERIAL_PROPERTY_BLOCKS
#endif
#if UNITY_2019_3_OR_NEWER
#define CONFIGURABLE_ENTER_PLAY_MODE
#endif
#if !SPINE_DISABLE_THREADING
#define USE_THREADED_ANIMATION_UPDATE
#endif
#if !SPINE_AUTO_UPGRADE_COMPONENTS_OFF
#define AUTO_UPGRADE_TO_43_COMPONENTS
#endif
using System.Collections.Generic;
using UnityEngine;
namespace Spine.Unity {
#if NEW_PREFAB_SYSTEM
[ExecuteAlways]
#else
[ExecuteInEditMode]
#endif
[System.Serializable]
public abstract class SkeletonAnimationBase : MonoBehaviour,
ISkeletonAnimation, ISkeletonComponent, IHasSkeletonRenderer,
ISkeletonRendererEvents, IHasModifyableSkeletonDataAsset, IUpgradable {
[SerializeField] protected UpdateTiming updateTiming = UpdateTiming.InUpdate;
protected int frameOfLastUpdate = -1;
protected ISkeletonRenderer skeletonRenderer;
protected bool skipUpdate = false;
public UpdateTiming UpdateTiming {
get { return updateTiming; }
set {
if (updateTiming == value) return;
#if USE_THREADED_ANIMATION_UPDATE
if (Application.isPlaying && UsesThreadedAnimation && isUpdatedExternally) {
SkeletonUpdateSystem system = SkeletonUpdateSystem.Instance;
if (system) {
// unregister from old update timing mode, register for new one.
system.UnregisterFromUpdate(updateTiming, this);
system.RegisterForUpdate(value, this);
}
}
#endif
updateTiming = value;
}
}
#if UNITY_EDITOR
protected bool requiresEditorUpdate = false;
#endif
#if USE_THREADED_ANIMATION_UPDATE
#region Threaded update system
protected static int mainThreadID = -1;
protected static float externalDeltaTime = 0f;
protected static float unscaledDeltaTime = 0f;
[SerializeField] protected SettingsTriState threadedAnimation = SettingsTriState.UseGlobalSetting;
protected bool isUpdatedExternally = false;
public static float ExternalDeltaTime {
get { return externalDeltaTime; }
set { externalDeltaTime = value; }
}
public static float ExternalUnscaledDeltaTime {
get { return unscaledDeltaTime; }
set { unscaledDeltaTime = value; }
}
public virtual float UsedExternalDeltaTime { get { return ExternalDeltaTime; } }
public bool IsUpdatedExternally {
get { return isUpdatedExternally; }
set { isUpdatedExternally = value; }
}
protected bool UsesThreadedAnimation {
get { return threadedAnimation == SettingsTriState.Enable || RuntimeSettings.UseThreadedAnimation; }
}
public SettingsTriState ThreadedAnimation {
get { return threadedAnimation; }
set {
if (threadedAnimation == value) return;
threadedAnimation = value;
SkeletonUpdateSystem system = SkeletonUpdateSystem.Instance;
if (system) {
if (threadedAnimation == SettingsTriState.Enable || RuntimeSettings.UseThreadedAnimation)
system.RegisterForUpdate(updateTiming, this);
else
system.UnregisterFromUpdate(updateTiming, this);
}
}
}
#endregion
#if UNITY_EDITOR
static bool applicationIsPlaying = false;
// ApplicationIsPlaying for threaded access. Unfortunately Application.isPlaying throws
// when called from worker thread.
public static bool ApplicationIsPlaying {
get { return applicationIsPlaying; }
set { applicationIsPlaying = value; }
}
#endif
#else
protected bool UseThreading {
get { return false; }
set { }
}
#if UNITY_EDITOR
public static bool ApplicationIsPlaying {
get { return Application.isPlaying; }
set { }
}
#endif
#endif
#region Threading Asserts
[System.Diagnostics.Conditional("UNITY_EDITOR")]
protected void InitializeMainThreadID () {
#if USE_THREADED_ANIMATION_UPDATE
if (mainThreadID == -1)
mainThreadID = System.Threading.Thread.CurrentThread.ManagedThreadId;
#endif
}
[System.Diagnostics.Conditional("UNITY_EDITOR")]
private void AssertIsMainThread () {
#if USE_THREADED_ANIMATION_UPDATE
if (System.Threading.Thread.CurrentThread.ManagedThreadId != mainThreadID)
Debug.LogError("AssertIsMainThread failed: worker thread calling main thread code. Thread ID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
#endif
}
[System.Diagnostics.Conditional("UNITY_EDITOR")]
private void AssertIsWorkerThread () {
#if USE_THREADED_ANIMATION_UPDATE
if (System.Threading.Thread.CurrentThread.ManagedThreadId == mainThreadID)
Debug.LogError("AssertIsWorkerThread failed: main thread calling worker thread code! Thread ID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
#endif
}
#endregion
#region Interface Implementation
public UnityEngine.MonoBehaviour Component { get { return this; } }
public ISkeletonRenderer Renderer {
get {
if (skeletonRenderer == null) {
skeletonRenderer = this.GetComponent<ISkeletonRenderer>();
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
if (skeletonRenderer == null) {
UpgradeTo43();
skeletonRenderer = this.GetComponent<ISkeletonRenderer>();
}
#endif
}
return skeletonRenderer;
}
}
public Skeleton Skeleton {
get { return Renderer.Skeleton; }
set { Renderer.Skeleton = value; }
}
public Skeleton skeleton {
get { return Renderer.Skeleton; }
set { Renderer.Skeleton = value; }
}
public SkeletonDataAsset SkeletonDataAsset {
get { return Renderer.SkeletonDataAsset; }
set { Renderer.SkeletonDataAsset = value; }
}
public SkeletonDataAsset skeletonDataAsset {
get { return Renderer.SkeletonDataAsset; }
set { Renderer.SkeletonDataAsset = value; }
}
protected event SkeletonAnimationDelegate _BeforeUpdate, _BeforeApply, _OnAnimationRebuild;
/// <summary>OnAnimationRebuild is raised after the SkeletonAnimation component is successfully initialized.</summary>
public event SkeletonAnimationDelegate OnAnimationRebuild { add { _OnAnimationRebuild += value; } remove { _OnAnimationRebuild -= value; } }
/// <summary>
/// Occurs before the animation state is updated.
/// Use this callback when you want to change the skeleton state before animation state is updated.
/// </summary>
public event SkeletonAnimationDelegate BeforeUpdate { add { _BeforeUpdate += value; } remove { _BeforeUpdate -= value; } }
/// <summary>
/// Occurs before the animations are applied.
/// Use this callback when you want to change the skeleton state before animations are applied on top.
/// </summary>
public event SkeletonAnimationDelegate BeforeApply { add { _BeforeApply += value; } remove { _BeforeApply -= value; } }
/// <summary>A compatibility wrapper for <see cref="SkeletonRenderer.UpdateLocal"/></summary>
public event SkeletonRendererDelegate UpdateLocal { add { Renderer.UpdateLocal += value; } remove { Renderer.UpdateLocal -= value; } }
/// <summary>A compatibility wrapper for <see cref="Renderer.UpdateWorld"/></summary>
public event SkeletonRendererDelegate UpdateWorld { add { Renderer.UpdateWorld += value; } remove { Renderer.UpdateWorld -= value; } }
/// <summary>A compatibility wrapper for <see cref="Renderer.UpdateComplete"/></summary>
public event SkeletonRendererDelegate UpdateComplete { add { Renderer.UpdateComplete += value; } remove { Renderer.UpdateComplete -= value; } }
/// <summary>A compatibility wrapper for <see cref="Renderer.OnRebuild"/></summary>
public event SkeletonRendererDelegate OnRebuild { add { Renderer.OnRebuild += value; } remove { Renderer.OnRebuild -= value; } }
/// <summary>A compatibility wrapper for <see cref="Renderer.OnMeshAndMaterialsUpdated"/></summary>
public event SkeletonRendererDelegate OnMeshAndMaterialsUpdated { add { Renderer.OnMeshAndMaterialsUpdated += value; } remove { Renderer.OnMeshAndMaterialsUpdated -= value; } }
/// <summary>Update mode to optionally limit updates to e.g. only apply animations but not update the mesh.</summary>
public UpdateMode UpdateMode { get { return Renderer.UpdateMode; } set { Renderer.UpdateMode = value; } }
public abstract bool IsValid { get; }
#endregion
public virtual void Awake () {
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
UpgradeTo43();
#endif
InitializeMainThreadID();
#if UNITY_EDITOR
SkeletonAnimationBase.ApplicationIsPlaying = Application.isPlaying;
#endif
EnsureRendererEventsSubscribed();
}
public void EnsureRendererEventsSubscribed () {
Renderer.OnRebuild -= OnRendererRebuild;
Renderer.OnRebuild += OnRendererRebuild;
}
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
// compatibility layer between 4.1 and 4.2, automatically transfer serialized attributes.
public abstract void UpgradeTo43 ();
#endif
/// <summary>
/// Initialize the associated renderer component and subsequently this animation component.
/// Creates the internal Spine objects and buffers.</summary>
/// <param name="overwrite">If set to <c>true</c>, force overwrite an already initialized object.</param>
public virtual void Initialize (bool overwrite, bool quiet = false, bool calledFromRendererCallback = false) {
#if UNITY_EDITOR
if (BuildUtilities.IsInSkeletonAssetBuildPreProcessing)
return;
#endif
if (!calledFromRendererCallback) {
if (skeletonRenderer == null)
skeletonRenderer = this.GetComponent<ISkeletonRenderer>();
if (!skeletonRenderer.IsValid || !this.IsValid || overwrite)
skeletonRenderer.Initialize(overwrite, quiet);
}
if (this.IsValid && !overwrite)
return;
InitializeAnimationComponent();
if (_OnAnimationRebuild != null)
_OnAnimationRebuild(this);
}
/// <summary>
/// Manually initializes just this animation component without initializing the associated renderer component.
/// The renderer component has to be initialized before calling this method, which happens automatically via
/// renderer component Awake, or when needed earlier by calling <see cref="Initialize(bool, bool, bool)"/> on
/// this animation component which initializes both components in the correct order.
/// </summary>
public virtual void InitializeAnimationComponent () {
if (skeletonRenderer == null)
skeletonRenderer = this.GetComponent<ISkeletonRenderer>();
#if UNITY_EDITOR
if (requiresEditorUpdate && skeletonRenderer != null && skeletonRenderer.Skeleton != null)
skeletonRenderer.Skeleton.SetupPose();
requiresEditorUpdate = false;
#endif
}
/// <summary>
/// Clears the previously generated mesh, resets the skeleton's pose, and clears all previously active animations.</summary>
public void ClearState () {
skeletonRenderer.ClearSkeletonState();
ClearAnimationState();
}
public virtual void ClearAnimationState () { }
public void OnEnable () {
if (skeletonRenderer == null)
skeletonRenderer = this.GetComponent<SkeletonRenderer>();
#if USE_THREADED_ANIMATION_UPDATE
if (Application.isPlaying && UsesThreadedAnimation && !isUpdatedExternally) {
SkeletonUpdateSystem system = SkeletonUpdateSystem.Instance;
if (system)
system.RegisterForUpdate(updateTiming, this);
}
#endif
}
#if USE_THREADED_ANIMATION_UPDATE
public void OnDisable () {
if (Application.isPlaying && UsesThreadedAnimation) {
SkeletonUpdateSystem system = SkeletonUpdateSystem.Instance;
if (system)
system.UnregisterFromUpdate(updateTiming, this);
}
}
#endif
protected void OnRendererRebuild (ISkeletonRenderer skeletonRenderer) {
Initialize(overwrite: true, quiet: false, calledFromRendererCallback: true); //InitializeAnimationComponent();
}
#if UNITY_EDITOR
protected void OnValidate () {
if (!Application.isPlaying) {
Renderer.OnRebuild -= OnRendererRebuild;
Renderer.OnRebuild += OnRendererRebuild;
requiresEditorUpdate = true;
} else if (Time.frameCount != 0) {
// OnValidate is called once when starting play mode in the Editor, don't trigger re-init then.
requiresEditorUpdate = true;
}
}
#endif
protected virtual void Update () {
#if UNITY_EDITOR
if (requiresEditorUpdate)
InitializeAnimationComponent();
if (!ApplicationIsPlaying) {
if (!IsValid)
Initialize(false);
Update(0f);
return;
}
#endif
#if USE_THREADED_ANIMATION_UPDATE
if (isUpdatedExternally) return;
#endif
if (updateTiming != UpdateTiming.InUpdate) return;
UpdateOncePerFrame(DeltaTime);
}
protected virtual void FixedUpdate () {
#if USE_THREADED_ANIMATION_UPDATE
if (isUpdatedExternally) return;
#endif
if (updateTiming != UpdateTiming.InFixedUpdate) return;
UpdateOncePerFrame(DeltaTime);
}
protected virtual void LateUpdate () {
#if USE_THREADED_ANIMATION_UPDATE
if (isUpdatedExternally) return;
#endif
if (updateTiming != UpdateTiming.InLateUpdate) return;
UpdateOncePerFrame(DeltaTime);
}
/// <summary>Calls <see cref="Update()"/> if it has not yet been called this frame.</summary>
public virtual void UpdateOncePerFrame (float deltaTime) {
if (frameOfLastUpdate != Time.frameCount) {
MainThreadBeforeUpdateInternal();
UpdateInternal(deltaTime, Time.frameCount, calledFromOnlyMainThread: true);
}
}
/// <summary>
/// Main thread update part preparing properties only accessible from main thread.
/// To be followed by a potentially threaded call to <see cref="UpdateInternal"/>.
/// </summary>
public virtual void MainThreadBeforeUpdateInternal () {
if (skeletonRenderer == null || !skeletonRenderer.IsValid || skeletonRenderer.Freeze || !this.IsValid
|| skeletonRenderer.UpdateMode < UpdateMode.OnlyAnimationStatus) {
skipUpdate = true;
return;
}
skipUpdate = false;
skeletonRenderer.GatherTransformMovementForPhysics();
if (_BeforeUpdate != null)
_BeforeUpdate(this);
}
public virtual void MainThreadAfterUpdateInternal () { }
#if USE_THREADED_ANIMATION_UPDATE
public virtual void UpdateExternal (int currentFrameCount, bool calledFromOnlyMainThread = true) {
UpdateInternal(UsedExternalDeltaTime, currentFrameCount, calledFromOnlyMainThread);
}
#endif
public virtual void UpdateInternal (float deltaTime, int currentFrameCount, bool calledFromOnlyMainThread = true) {
if (skipUpdate)
return;
frameOfLastUpdate = currentFrameCount;
UpdateAnimationStatus(deltaTime);
skeletonRenderer.ApplyTransformMovementToPhysics();
if (skeletonRenderer.UpdateMode == UpdateMode.OnlyAnimationStatus)
return;
ApplyAnimation(calledFromOnlyMainThread);
}
#if USE_THREADED_ANIMATION_UPDATE
/// <summary>Progresses the AnimationState according to the given deltaTime, and applies it to the Skeleton.
/// Use Time.deltaTime to update manually. Use deltaTime 0 to update without progressing the time.</summary>
public virtual CoroutineIterator UpdateInternalSplit (CoroutineIterator coroutineIterator,
int currentFrameCount) {
if (coroutineIterator.IsDone)
return CoroutineIterator.Done;
const int StateBits = 1;
const uint StateMask = (1 << StateBits) - 1;
switch (coroutineIterator.State(StateMask)) {
case 0:
if (skipUpdate)
return CoroutineIterator.Done;
frameOfLastUpdate = currentFrameCount;
UpdateAnimationStatus(UsedExternalDeltaTime);
skeletonRenderer.ApplyTransformMovementToPhysics();
if (skeletonRenderer.UpdateMode == UpdateMode.OnlyAnimationStatus)
return CoroutineIterator.Done;
goto case 1;
case 1:
return ApplyAnimationSplit(coroutineIterator.ToNestedCall(StateBits))
.FromNestedCall(1, StateBits);
default:
Debug.LogError(string.Format(
"Internal coroutine logic error: SkeletonAnimationBase.UpdateInternalSplit state was {0}.",
coroutineIterator.State(StateMask)), this);
return CoroutineIterator.Done;
}
}
#endif
/// <summary>Progresses the AnimationState according to the given deltaTime, and applies it to the Skeleton.
/// Use Time.deltaTime to update manually. Use deltaTime 0 to update without progressing the time.</summary>
public virtual void Update (float deltaTime) {
MainThreadBeforeUpdateInternal();
UpdateInternal(deltaTime, Time.frameCount, calledFromOnlyMainThread: true);
}
public virtual void ApplyAnimation (bool calledFromMainThread = true) {
if (_BeforeApply != null)
_BeforeApply(this);
ApplyStateToSkeleton(calledFromMainThread);
skeletonRenderer.AfterAnimationApplied(calledFromMainThread);
}
public virtual CoroutineIterator ApplyAnimationSplit (CoroutineIterator coroutineIterator) {
if (coroutineIterator.IsDone)
return CoroutineIterator.Done;
const int StateBits = 2;
const uint StateMask = (1 << StateBits) - 1;
switch (coroutineIterator.State(StateMask)) {
case 0:
if (_BeforeApply != null) {
AssertIsWorkerThread();
return coroutineIterator.YieldReturnAtState(1, StateMask);
} else {
goto case 2;
}
case 1:
AssertIsMainThread();
_BeforeApply(this);
return coroutineIterator.YieldReturnAtState(2, StateMask);
case 2:
AssertIsWorkerThread();
ApplyStateToSkeleton(calledFromMainThread: false);
goto case 3;
case 3:
return skeletonRenderer.AfterAnimationAppliedSplit(coroutineIterator.ToNestedCall(StateBits))
.FromNestedCall(3, StateBits);
default:
Debug.LogError(string.Format(
"Internal coroutine logic error: SkeletonAnimationBase.ApplyAnimationSplit state was {0}.",
coroutineIterator.State(StateMask)), this);
return CoroutineIterator.Done;
}
}
public void OnBecameVisibleFromMode (UpdateMode previousUpdateMode) {
// OnBecameVisible is called after Update and LateUpdate(),
// so update if previousUpdateMode didn't already update this frame.
if (previousUpdateMode != UpdateMode.FullUpdate &&
previousUpdateMode != UpdateMode.EverythingExceptMesh)
Update(0);
}
protected virtual float DeltaTime { get { return Time.deltaTime; } }
protected abstract void UpdateAnimationStatus (float deltaTime);
protected abstract void ApplyStateToSkeleton (bool calledFromMainThread = true);
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 70671e8c557db624b9ba695f42aa543e
timeCreated: 1622830533
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,149 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2026, 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_2022_2_OR_NEWER
#define USE_FIND_OBJECTS_BY_TYPE
#endif
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEngine;
namespace Spine.Unity {
/// <summary>
/// Experimental Editor Skeleton Player component enabling Editor playback of the
/// selected animation outside of Play mode for SkeletonAnimation and SkeletonGraphic.
/// </summary>
[ExecuteInEditMode]
[AddComponentMenu("Spine/EditorSkeletonPlayer")]
[RequireComponent(typeof(SkeletonAnimation))]
public class EditorSkeletonPlayer : MonoBehaviour {
public bool playWhenSelected = true;
public bool playWhenDeselected = true;
public float fixedTrackTime = 0.0f;
private SkeletonAnimation skeletonAnimation;
private TrackEntry trackEntry;
private string oldAnimationName;
private bool oldLoop;
private double oldTime;
[DidReloadScripts]
private static void OnReloaded () {
// Force start when scripts are reloaded
#if USE_FIND_OBJECTS_BY_TYPE
EditorSkeletonPlayer[] editorSpineAnimations = FindObjectsByType<EditorSkeletonPlayer>(FindObjectsSortMode.None);
#else
EditorSkeletonPlayer[] editorSpineAnimations = FindObjectsOfType<EditorSkeletonPlayer>();
#endif
foreach (EditorSkeletonPlayer editorSpineAnimation in editorSpineAnimations)
editorSpineAnimation.Start();
}
private void Reset () {
// Note: when a skeleton has a varying number of active materials,
// we're moving this component first in the hierarchy to still be
// able to disable this component.
for (int i = 0; i < 10; ++i)
UnityEditorInternal.ComponentUtility.MoveComponentUp(this);
}
private void Start () {
if (Application.isPlaying) return;
if (skeletonAnimation == null) {
skeletonAnimation = this.GetComponent<SkeletonAnimation>();
}
oldTime = EditorApplication.timeSinceStartup;
EditorApplication.update += EditorUpdate;
}
private void OnDestroy () {
EditorApplication.update -= EditorUpdate;
}
private void Update () {
if (enabled == false || Application.isPlaying) return;
if (skeletonAnimation == null) return;
AnimationState animationState = skeletonAnimation.AnimationState;
if (animationState == null || animationState.Tracks.Count == 0) return;
TrackEntry currentEntry = animationState.Tracks.Items[0];
if (currentEntry != null && fixedTrackTime != 0) {
currentEntry.TrackTime = fixedTrackTime;
}
}
private void EditorUpdate () {
if (enabled == false || Application.isPlaying) return;
if (skeletonAnimation == null) return;
AnimationState animationState = skeletonAnimation.AnimationState;
if (animationState == null) return;
bool isSelected = Selection.Contains(this.gameObject);
if (!this.playWhenSelected && isSelected) return;
if (!this.playWhenDeselected && !isSelected) return;
if (fixedTrackTime != 0) return;
// Update animation
string animationName = skeletonAnimation.AnimationName;
bool loop = skeletonAnimation.loop;
if (oldAnimationName != animationName || oldLoop != loop) {
SkeletonData skeletonData = skeletonAnimation.Skeleton.Data;
Spine.Animation animation = (skeletonData == null || animationName == null) ?
null : skeletonData.FindAnimation(animationName);
if (animation != null)
trackEntry = animationState.SetAnimation(0, animationName, loop);
else
trackEntry = animationState.SetEmptyAnimation(0, 0);
oldAnimationName = animationName;
oldLoop = loop;
}
// Update speed
if (trackEntry != null)
trackEntry.TimeScale = skeletonAnimation.timeScale;
float deltaTime = (float)(EditorApplication.timeSinceStartup - oldTime);
skeletonAnimation.Update(deltaTime);
skeletonAnimation.Renderer.UpdateMesh();
oldTime = EditorApplication.timeSinceStartup;
// Force repaint to update animation smoothly
#if UNITY_2017_2_OR_NEWER
EditorApplication.QueuePlayerLoopUpdate();
#else
SceneView.RepaintAll();
#endif
}
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8b473abb659158442b906826a22f18bc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: b0fd98019ca00c74f929c6d1f7ee3544
folderAsset: yes
timeCreated: 1563290418
licenseType: Free
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,279 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2026, 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
#if !SPINE_AUTO_UPGRADE_COMPONENTS_OFF
#define AUTO_UPGRADE_TO_43_COMPONENTS
#endif
using System;
using UnityEngine;
using UnityEngine.Serialization;
namespace Spine.Unity {
/// <summary>Sets a GameObject's transform to match a bone on a Spine skeleton.</summary>
#if NEW_PREFAB_SYSTEM
[ExecuteAlways]
#else
[ExecuteInEditMode]
#endif
[AddComponentMenu("Spine/BoneFollower")]
[HelpURL("https://esotericsoftware.com/spine-unity-utility-components#BoneFollower")]
public class BoneFollower : MonoBehaviour, IUpgradable {
#region Inspector
public SkeletonRenderer skeletonRenderer;
public SkeletonRenderer SkeletonRenderer {
get { return skeletonRenderer; }
set {
skeletonRenderer = value;
Initialize();
}
}
/// <summary>If a bone isn't set in code, boneName is used to find the bone at the beginning. For runtime switching by name, use SetBoneByName. You can also set the BoneFollower.bone field directly.</summary>
[SpineBone(dataField: "skeletonRenderer")]
public string boneName;
public bool followXYPosition = true;
public bool followZPosition = true;
public bool followAttachmentZSpacing = false;
public bool followBoneRotation = true;
[Tooltip("Follows the skeleton's flip state by controlling this Transform's local scale.")]
public bool followSkeletonFlip = true;
[Tooltip("Follows the target bone's local scale.")]
[UnityEngine.Serialization.FormerlySerializedAs("followScale")]
public bool followLocalScale = false;
[Tooltip("Includes the parent bone's lossy world scale. BoneFollower cannot inherit rotated/skewed scale because of UnityEngine.Transform property limitations.")]
public bool followParentWorldScale = false;
public enum AxisOrientation {
XAxis = 1,
YAxis
}
[Tooltip("Applies when 'Follow Skeleton Flip' is disabled but 'Follow Bone Rotation' is enabled."
+ " When flipping the skeleton by scaling its Transform, this follower's rotation is adjusted"
+ " instead of its scale to follow the bone orientation. When one of the axes is flipped, "
+ " only one axis can be followed, either the X or the Y axis, which is selected here.")]
public AxisOrientation maintainedAxisOrientation = AxisOrientation.XAxis;
[UnityEngine.Serialization.FormerlySerializedAs("resetOnAwake")]
public bool initializeOnAwake = true;
#endregion
[NonSerialized] public bool valid;
[NonSerialized] public Bone bone;
Transform skeletonTransform;
bool skeletonTransformIsParent;
/// <summary>
/// Sets the target bone by its bone name. Returns false if no bone was found. To set the bone by reference, use BoneFollower.bone directly.</summary>
public bool SetBone (string name) {
bone = skeletonRenderer.skeleton.FindBone(name);
if (bone == null) {
Debug.LogError("Bone not found: " + name, this);
return false;
}
boneName = name;
return true;
}
public void Awake () {
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
if (!Application.isPlaying && !wasUpgradedTo43) {
UpgradeTo43();
}
#endif
if (initializeOnAwake) Initialize();
}
public void HandleRebuildRenderer (ISkeletonRenderer skeletonRenderer) {
Initialize();
}
public virtual void Initialize () {
bone = null;
valid = skeletonRenderer != null && skeletonRenderer.valid;
if (!valid) return;
skeletonTransform = skeletonRenderer.transform;
skeletonRenderer.OnRebuild -= HandleRebuildRenderer;
skeletonRenderer.OnRebuild += HandleRebuildRenderer;
skeletonTransformIsParent = Transform.ReferenceEquals(skeletonTransform, transform.parent);
if (!string.IsNullOrEmpty(boneName))
bone = skeletonRenderer.skeleton.FindBone(boneName);
#if UNITY_EDITOR
if (Application.isEditor)
LateUpdate();
#endif
}
void OnDestroy () {
if (skeletonRenderer != null)
skeletonRenderer.OnRebuild -= HandleRebuildRenderer;
}
public virtual void LateUpdate () {
if (!valid) {
Initialize();
return;
}
#if UNITY_EDITOR
if (!Application.isPlaying)
skeletonTransformIsParent = Transform.ReferenceEquals(skeletonTransform, transform.parent);
#endif
Skeleton skeleton = skeletonRenderer.skeleton;
if (bone == null) {
if (string.IsNullOrEmpty(boneName)) return;
bone = skeleton.FindBone(boneName);
if (!SetBone(boneName)) return;
}
Transform thisTransform = this.transform;
float additionalFlipScale = 1;
float scaleSignX = 1;
float scaleSignY = 1;
Bone parentBone = bone.Parent;
if (followParentWorldScale || followLocalScale || followSkeletonFlip) {
Vector3 localScale = new Vector3(1f, 1f, 1f);
if (followParentWorldScale && parentBone != null) {
float cumulativeScaleX = 1.0f;
float cumulativeScaleY = 1.0f;
Bone p = parentBone;
while (p != null) {
cumulativeScaleX *= p.AppliedPose.ScaleX;
cumulativeScaleY *= p.AppliedPose.ScaleY;
p = p.Parent;
};
scaleSignX = Mathf.Sign(cumulativeScaleX);
scaleSignY = Mathf.Sign(cumulativeScaleY);
localScale = new Vector3(parentBone.AppliedPose.WorldScaleX * scaleSignX, parentBone.AppliedPose.WorldScaleY * scaleSignY, 1f);
}
if (followLocalScale)
localScale.Scale(new Vector3(bone.AppliedPose.ScaleX, bone.AppliedPose.ScaleY, 1f));
if (followSkeletonFlip)
localScale.y *= Mathf.Sign(skeleton.ScaleX * skeleton.ScaleY) * additionalFlipScale;
thisTransform.localScale = localScale;
}
if (skeletonTransformIsParent) {
// Recommended setup: Use local transform properties if Spine GameObject is the immediate parent
thisTransform.localPosition = new Vector3(
followXYPosition ? bone.AppliedPose.WorldX : thisTransform.localPosition.x,
followXYPosition ? bone.AppliedPose.WorldY : thisTransform.localPosition.y,
followZPosition ? (followAttachmentZSpacing ? GetAttachmentZPosition() : 0f) : thisTransform.localPosition.z);
if (followBoneRotation) {
float halfRotation = Mathf.Atan2(bone.AppliedPose.C, bone.AppliedPose.A) * 0.5f;
if (followLocalScale && bone.AppliedPose.ScaleX < 0) // Negate rotation from negative scaleX. Don't use negative determinant. local scaleY doesn't factor into used rotation.
halfRotation += Mathf.PI * 0.5f;
if (followParentWorldScale && scaleSignX < 0)
halfRotation += Mathf.PI * 0.5f;
Quaternion q = default(Quaternion);
q.z = Mathf.Sin(halfRotation);
q.w = Mathf.Cos(halfRotation);
thisTransform.localRotation = q;
}
} else { // For special cases: Use transform world properties if transform relationship is complicated
if (!skeletonTransform) return;
float z0Position = (followZPosition && followAttachmentZSpacing) ? GetAttachmentZPosition() : 0f;
Vector3 targetWorldPosition = skeletonTransform.TransformPoint(new Vector3(bone.AppliedPose.WorldX, bone.AppliedPose.WorldY, z0Position));
if (!followZPosition) targetWorldPosition.z = thisTransform.position.z;
if (!followXYPosition) {
targetWorldPosition.x = thisTransform.position.x;
targetWorldPosition.y = thisTransform.position.y;
}
Vector3 skeletonLossyScale = skeletonTransform.lossyScale;
Transform transformParent = thisTransform.parent;
Vector3 parentLossyScale = transformParent != null ? transformParent.lossyScale : Vector3.one;
if (followBoneRotation) {
float boneWorldRotation = bone.AppliedPose.WorldRotationX;
if ((skeletonLossyScale.x * skeletonLossyScale.y) < 0)
boneWorldRotation = -boneWorldRotation;
if (followSkeletonFlip || maintainedAxisOrientation == AxisOrientation.XAxis) {
if ((skeletonLossyScale.x * parentLossyScale.x < 0))
boneWorldRotation += 180f;
} else {
if ((skeletonLossyScale.y * parentLossyScale.y < 0))
boneWorldRotation += 180f;
}
if (followParentWorldScale && scaleSignX < 0)
boneWorldRotation += 180f;
Vector3 worldRotation = skeletonTransform.rotation.eulerAngles;
if (followLocalScale && bone.AppliedPose.ScaleX < 0) boneWorldRotation += 180f;
thisTransform.SetPositionAndRotation(targetWorldPosition, Quaternion.Euler(worldRotation.x, worldRotation.y, worldRotation.z + boneWorldRotation));
} else {
thisTransform.position = targetWorldPosition;
}
additionalFlipScale = Mathf.Sign(skeletonLossyScale.x * parentLossyScale.x
* skeletonLossyScale.y * parentLossyScale.y);
}
}
float GetAttachmentZPosition () {
var drawOrderPose = skeletonRenderer.Skeleton.DrawOrder.Pose;
int boneIndex = drawOrderPose.FindIndex(slot => slot.Bone == bone);
if (boneIndex < 0) return 0f;
return skeletonRenderer.zSpacing * boneIndex;
}
#region Transfer of Deprecated Fields
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
public virtual void UpgradeTo43 () {
wasUpgradedTo43 = true;
if (skeletonRenderer == null) {
Component previousReference = previousSkeletonRenderer != null ? previousSkeletonRenderer : this;
skeletonRenderer = previousReference.GetComponent<SkeletonRenderer>();
if (skeletonRenderer == null)
Debug.LogError("Please manually re-assign SkeletonRenderer at BoneFollower, " +
"automatic upgrade failed.", this);
}
}
[SerializeField, HideInInspector, FormerlySerializedAs("skeletonRenderer")] Component previousSkeletonRenderer;
[SerializeField] protected bool wasUpgradedTo43 = false;
#endif
#endregion
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: a1fd8daaed7b64148a34acb96ba14ce1
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,229 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2026, 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 UnityEngine;
namespace Spine.Unity {
using AxisOrientation = BoneFollower.AxisOrientation;
#if NEW_PREFAB_SYSTEM
[ExecuteAlways]
#else
[ExecuteInEditMode]
#endif
[RequireComponent(typeof(RectTransform)), DisallowMultipleComponent]
[AddComponentMenu("Spine/UI/BoneFollowerGraphic")]
[HelpURL("https://esotericsoftware.com/spine-unity-utility-components#BoneFollowerGraphic")]
public class BoneFollowerGraphic : MonoBehaviour {
public SkeletonGraphic skeletonGraphic;
public SkeletonGraphic SkeletonGraphic {
get { return skeletonGraphic; }
set {
skeletonGraphic = value;
Initialize();
}
}
public bool initializeOnAwake = true;
/// <summary>If a bone isn't set in code, boneName is used to find the bone at the beginning. For runtime switching by name, use SetBoneByName. You can also set the BoneFollower.bone field directly.</summary>
[SpineBone(dataField: "skeletonGraphic")]
public string boneName;
public bool followBoneRotation = true;
[Tooltip("Follows the skeleton's flip state by controlling this Transform's local scale.")]
public bool followSkeletonFlip = true;
[Tooltip("Follows the target bone's local scale.")]
public bool followLocalScale = false;
[Tooltip("Includes the parent bone's lossy world scale. BoneFollower cannot inherit rotated/skewed scale because of UnityEngine.Transform property limitations.")]
public bool followParentWorldScale = false;
public bool followXYPosition = true;
public bool followZPosition = true;
public bool followAttachmentZSpacing = false;
[Tooltip("Applies when 'Follow Skeleton Flip' is disabled but 'Follow Bone Rotation' is enabled."
+ " When flipping the skeleton by scaling its Transform, this follower's rotation is adjusted"
+ " instead of its scale to follow the bone orientation. When one of the axes is flipped, "
+ " only one axis can be followed, either the X or the Y axis, which is selected here.")]
public AxisOrientation maintainedAxisOrientation = AxisOrientation.XAxis;
[System.NonSerialized] public Bone bone;
Transform skeletonTransform;
bool skeletonTransformIsParent;
[System.NonSerialized] public bool valid;
/// <summary>
/// Sets the target bone by its bone name. Returns false if no bone was found.</summary>
public bool SetBone (string name) {
bone = skeletonGraphic.Skeleton.FindBone(name);
if (bone == null) {
Debug.LogError("Bone not found: " + name, this);
return false;
}
boneName = name;
return true;
}
public void Awake () {
if (initializeOnAwake) Initialize();
}
public virtual void Initialize () {
bone = null;
valid = skeletonGraphic != null && skeletonGraphic.IsValid;
if (!valid) return;
skeletonTransform = skeletonGraphic.transform;
// skeletonGraphic.OnRebuild -= HandleRebuildRenderer;
// skeletonGraphic.OnRebuild += HandleRebuildRenderer;
skeletonTransformIsParent = Transform.ReferenceEquals(skeletonTransform, transform.parent);
if (!string.IsNullOrEmpty(boneName))
bone = skeletonGraphic.Skeleton.FindBone(boneName);
#if UNITY_EDITOR
if (Application.isEditor) {
LateUpdate();
}
#endif
}
public virtual void LateUpdate () {
if (!valid) {
Initialize();
return;
}
#if UNITY_EDITOR
if (!Application.isPlaying)
skeletonTransformIsParent = Transform.ReferenceEquals(skeletonTransform, transform.parent);
#endif
Skeleton skeleton = skeletonGraphic.Skeleton;
if (bone == null) {
if (string.IsNullOrEmpty(boneName)) return;
bone = skeleton.FindBone(boneName);
if (!SetBone(boneName)) return;
}
RectTransform thisTransform = this.transform as RectTransform;
if (thisTransform == null) return;
float scale = skeletonGraphic.MeshScale;
Vector2 offset = skeletonGraphic.MeshOffset;
float additionalFlipScale = 1;
float scaleSignX = 1;
float scaleSignY = 1;
Bone parentBone = bone.Parent;
if (followParentWorldScale || followLocalScale || followSkeletonFlip) {
Vector3 localScale = new Vector3(1f, 1f, 1f);
if (followParentWorldScale && parentBone != null) {
float cumulativeScaleX = 1.0f;
float cumulativeScaleY = 1.0f;
Bone p = parentBone;
while (p != null) {
cumulativeScaleX *= p.AppliedPose.ScaleX;
cumulativeScaleY *= p.AppliedPose.ScaleY;
p = p.Parent;
};
scaleSignX = Mathf.Sign(cumulativeScaleX);
scaleSignY = Mathf.Sign(cumulativeScaleY);
localScale = new Vector3(parentBone.AppliedPose.WorldScaleX * scaleSignX, parentBone.AppliedPose.WorldScaleY * scaleSignY, 1f);
}
if (followLocalScale)
localScale.Scale(new Vector3(bone.AppliedPose.ScaleX, bone.AppliedPose.ScaleY, 1f));
if (followSkeletonFlip)
localScale.y *= Mathf.Sign(skeleton.ScaleX * skeleton.ScaleY) * additionalFlipScale;
thisTransform.localScale = localScale;
}
if (skeletonTransformIsParent) {
// Recommended setup: Use local transform properties if Spine GameObject is the immediate parent
thisTransform.localPosition = new Vector3(
followXYPosition ? bone.AppliedPose.WorldX * scale + offset.x : thisTransform.localPosition.x,
followXYPosition ? bone.AppliedPose.WorldY * scale + offset.y : thisTransform.localPosition.y,
followZPosition ? (followAttachmentZSpacing ? GetAttachmentZPosition() : 0f) : thisTransform.localPosition.z);
if (followBoneRotation) thisTransform.localRotation = bone.GetQuaternion();
} else { // For special cases: Use transform world properties if transform relationship is complicated
if (!skeletonTransform) return;
float z0Position = (followZPosition && followAttachmentZSpacing) ? GetAttachmentZPosition() : 0f;
Vector3 targetWorldPosition = skeletonTransform.TransformPoint(
new Vector3(bone.AppliedPose.WorldX * scale + offset.x, bone.AppliedPose.WorldY * scale + offset.y, z0Position));
if (!followZPosition) targetWorldPosition.z = thisTransform.position.z;
if (!followXYPosition) {
targetWorldPosition.x = thisTransform.position.x;
targetWorldPosition.y = thisTransform.position.y;
}
Vector3 skeletonLossyScale = skeletonTransform.lossyScale;
Transform transformParent = thisTransform.parent;
Vector3 parentLossyScale = transformParent != null ? transformParent.lossyScale : Vector3.one;
if (followBoneRotation) {
float boneWorldRotation = bone.AppliedPose.WorldRotationX;
if ((skeletonLossyScale.x * skeletonLossyScale.y) < 0)
boneWorldRotation = -boneWorldRotation;
if (followSkeletonFlip || maintainedAxisOrientation == AxisOrientation.XAxis) {
if ((skeletonLossyScale.x * parentLossyScale.x < 0))
boneWorldRotation += 180f;
} else {
if ((skeletonLossyScale.y * parentLossyScale.y < 0))
boneWorldRotation += 180f;
}
if (followParentWorldScale && scaleSignX < 0)
boneWorldRotation += 180f;
Vector3 worldRotation = skeletonTransform.rotation.eulerAngles;
if (followLocalScale && bone.AppliedPose.ScaleX < 0) boneWorldRotation += 180f;
thisTransform.SetPositionAndRotation(targetWorldPosition, Quaternion.Euler(worldRotation.x, worldRotation.y, worldRotation.z + boneWorldRotation));
} else {
thisTransform.position = targetWorldPosition;
}
additionalFlipScale = Mathf.Sign(skeletonLossyScale.x * parentLossyScale.x
* skeletonLossyScale.y * parentLossyScale.y);
}
}
float GetAttachmentZPosition () {
var drawOrderPose = skeletonGraphic.Skeleton.DrawOrder.Pose;
int boneIndex = drawOrderPose.FindIndex(slot => slot.Bone == bone);
if (boneIndex < 0) return 0f;
return skeletonGraphic.MeshSettings.zSpacing * skeletonGraphic.MeshScale * boneIndex;
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: b42a195b47491d34b9bcbc40898bcb29
timeCreated: 1499211965
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,291 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2026, 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
#if UNITY_2023_1_OR_NEWER
#define USE_COLLIDER_COMPOSITE_OPERATION
#endif
#if !SPINE_AUTO_UPGRADE_COMPONENTS_OFF
#define AUTO_UPGRADE_TO_43_COMPONENTS
#endif
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
namespace Spine.Unity {
#if NEW_PREFAB_SYSTEM
[ExecuteAlways]
#else
[ExecuteInEditMode]
#endif
[HelpURL("http://esotericsoftware.com/spine-unity-utility-components#BoundingBoxFollower")]
public class BoundingBoxFollower : MonoBehaviour, IUpgradable {
internal static bool DebugMessages = true;
#region Inspector
public SkeletonRenderer skeletonRenderer;
[SpineSlot(dataField: "skeletonRenderer", containsBoundingBoxes: true)]
public string slotName;
public bool isTrigger, usedByEffector, usedByComposite;
public bool clearStateOnDisable = true;
#endregion
Slot slot;
BoundingBoxAttachment currentAttachment;
string currentAttachmentName;
PolygonCollider2D currentCollider;
bool skinBoneEnabled = true;
public readonly Dictionary<BoundingBoxAttachment, PolygonCollider2D> colliderTable = new Dictionary<BoundingBoxAttachment, PolygonCollider2D>();
public readonly Dictionary<BoundingBoxAttachment, string> nameTable = new Dictionary<BoundingBoxAttachment, string>();
public Slot Slot { get { return slot; } }
public BoundingBoxAttachment CurrentAttachment { get { return currentAttachment; } }
public string CurrentAttachmentName { get { return currentAttachmentName; } }
public PolygonCollider2D CurrentCollider { get { return currentCollider; } }
public bool IsTrigger { get { return isTrigger; } }
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
protected void Awake () {
if (!Application.isPlaying && !wasUpgradedTo43) {
UpgradeTo43();
}
}
#endif
void Start () {
Initialize();
}
void OnEnable () {
if (skeletonRenderer != null) {
skeletonRenderer.OnRebuild -= HandleRebuild;
skeletonRenderer.OnRebuild += HandleRebuild;
}
Initialize();
}
void HandleRebuild (ISkeletonRenderer sr) {
//if (BoundingBoxFollower.DebugMessages) Debug.Log("Skeleton was rebuilt. Repopulating BoundingBoxFollower.");
Initialize();
}
/// <summary>
/// Initialize and instantiate the BoundingBoxFollower colliders. This is method checks if the BoundingBoxFollower has already been initialized for the skeleton instance and slotName and prevents overwriting unless it detects a new setup.</summary>
public void Initialize (bool overwrite = false) {
if (skeletonRenderer == null)
return;
skeletonRenderer.Initialize(false);
if (string.IsNullOrEmpty(slotName))
return;
// Don't reinitialize if the setup did not change.
if (!overwrite &&
colliderTable.Count > 0 && slot != null && // Slot is set and colliders already populated.
slotName == slot.Data.Name // Slot object did not change.
)
return;
slot = null;
currentAttachment = null;
currentAttachmentName = null;
currentCollider = null;
colliderTable.Clear();
nameTable.Clear();
Skeleton skeleton = skeletonRenderer.skeleton;
if (skeleton == null)
return;
slot = skeleton.FindSlot(slotName);
if (slot == null) {
if (BoundingBoxFollower.DebugMessages)
Debug.LogWarning(string.Format("Slot '{0}' not found for BoundingBoxFollower on '{1}'. (Previous colliders were disposed.)", slotName, this.gameObject.name));
return;
}
int slotIndex = slot.Data.Index;
int requiredCollidersCount = 0;
PolygonCollider2D[] colliders = GetComponents<PolygonCollider2D>();
if (this.gameObject.activeInHierarchy) {
foreach (Skin skin in skeleton.Data.Skins)
AddCollidersForSkin(skin, skeleton, slotIndex, colliders, ref requiredCollidersCount);
if (skeleton.Skin != null)
AddCollidersForSkin(skeleton.Skin, skeleton, slotIndex, colliders, ref requiredCollidersCount);
}
DisposeExcessCollidersAfter(requiredCollidersCount);
skinBoneEnabled = slot.Bone.Active;
if (BoundingBoxFollower.DebugMessages) {
bool valid = colliderTable.Count != 0;
if (!valid) {
if (this.gameObject.activeInHierarchy)
Debug.LogWarning("Bounding Box Follower not valid! Slot [" + slotName + "] does not contain any Bounding Box Attachments!");
else
Debug.LogWarning("Bounding Box Follower tried to rebuild as a prefab.");
}
}
}
void AddCollidersForSkin (Skin skin, Skeleton skeleton, int slotIndex, PolygonCollider2D[] previousColliders, ref int collidersCount) {
if (skin == null) return;
List<Skin.SkinEntry> skinEntries = new List<Skin.SkinEntry>();
skin.GetAttachments(slotIndex, skinEntries);
foreach (Skin.SkinEntry entry in skinEntries) {
Attachment attachment = skin.GetAttachment(slotIndex, entry.Placeholder);
BoundingBoxAttachment boundingBoxAttachment = attachment as BoundingBoxAttachment;
if (BoundingBoxFollower.DebugMessages && attachment != null && boundingBoxAttachment == null)
Debug.Log("BoundingBoxFollower tried to follow a slot that contains non-boundingbox attachments: " + slotName);
if (boundingBoxAttachment != null) {
if (!colliderTable.ContainsKey(boundingBoxAttachment)) {
PolygonCollider2D bbCollider = collidersCount < previousColliders.Length ?
previousColliders[collidersCount] : gameObject.AddComponent<PolygonCollider2D>();
++collidersCount;
SkeletonUtility.SetColliderPointsLocal(bbCollider, skeleton, slot, boundingBoxAttachment);
bbCollider.isTrigger = isTrigger;
bbCollider.usedByEffector = usedByEffector;
#if USE_COLLIDER_COMPOSITE_OPERATION
bbCollider.compositeOperation = usedByComposite ?
Collider2D.CompositeOperation.Merge : Collider2D.CompositeOperation.None;
#else
bbCollider.usedByComposite = usedByComposite;
#endif
bbCollider.enabled = false;
bbCollider.hideFlags = HideFlags.NotEditable;
colliderTable.Add(boundingBoxAttachment, bbCollider);
nameTable.Add(boundingBoxAttachment, entry.Placeholder);
}
}
}
}
void OnDisable () {
if (clearStateOnDisable)
ClearState();
if (skeletonRenderer != null)
skeletonRenderer.OnRebuild -= HandleRebuild;
}
public void ClearState () {
if (colliderTable != null)
foreach (PolygonCollider2D col in colliderTable.Values)
col.enabled = false;
currentAttachment = null;
currentAttachmentName = null;
currentCollider = null;
}
void DisposeExcessCollidersAfter (int requiredCount) {
PolygonCollider2D[] colliders = GetComponents<PolygonCollider2D>();
if (colliders.Length == 0) return;
for (int i = requiredCount; i < colliders.Length; ++i) {
PolygonCollider2D collider = colliders[i];
if (collider != null) {
#if UNITY_EDITOR
if (Application.isEditor && !Application.isPlaying)
DestroyImmediate(collider);
else
#endif
Destroy(collider);
}
}
}
void LateUpdate () {
var slotPose = slot.AppliedPose;
if (slot != null && (slotPose.Attachment != currentAttachment || skinBoneEnabled != slot.Bone.Active)) {
skinBoneEnabled = slot.Bone.Active;
MatchAttachment(slotPose.Attachment);
}
}
/// <summary>Sets the current collider to match attachment.</summary>
/// <param name="attachment">If the attachment is not a bounding box, it will be treated as null.</param>
void MatchAttachment (Attachment attachment) {
BoundingBoxAttachment bbAttachment = attachment as BoundingBoxAttachment;
if (BoundingBoxFollower.DebugMessages && attachment != null && bbAttachment == null)
Debug.LogWarning("BoundingBoxFollower tried to match a non-boundingbox attachment. It will treat it as null.");
if (currentCollider != null)
currentCollider.enabled = false;
if (bbAttachment == null || !skinBoneEnabled) {
currentCollider = null;
currentAttachment = null;
currentAttachmentName = null;
} else {
PolygonCollider2D foundCollider;
colliderTable.TryGetValue(bbAttachment, out foundCollider);
if (foundCollider != null) {
currentCollider = foundCollider;
currentCollider.enabled = true;
currentAttachment = bbAttachment;
currentAttachmentName = nameTable[bbAttachment];
} else {
currentCollider = null;
currentAttachment = bbAttachment;
currentAttachmentName = null;
if (BoundingBoxFollower.DebugMessages) Debug.LogFormat("Collider for BoundingBoxAttachment named '{0}' was not initialized. It is possibly from a new skin. currentAttachmentName will be null. You may need to call BoundingBoxFollower.Initialize(overwrite: true);", bbAttachment.Name);
}
}
}
#region Transfer of Deprecated Fields
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
public virtual void UpgradeTo43 () {
wasUpgradedTo43 = true;
if (skeletonRenderer == null) {
Component previousReference = previousSkeletonRenderer != null ? previousSkeletonRenderer : this;
skeletonRenderer = previousReference.GetComponent<SkeletonRenderer>();
if (skeletonRenderer == null)
Debug.LogError("Please manually re-assign SkeletonRenderer at BoundingBoxFollower, " +
"automatic upgrade failed.", this);
}
}
[SerializeField, HideInInspector, FormerlySerializedAs("skeletonRenderer")] Component previousSkeletonRenderer;
[SerializeField] protected bool wasUpgradedTo43 = false;
#endif
#endregion
}
}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0317ee9ba6e1b1e49a030268e026d372
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
@@ -0,0 +1,263 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2026, 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
#if UNITY_2023_1_OR_NEWER
#define USE_COLLIDER_COMPOSITE_OPERATION
#endif
using System.Collections.Generic;
using UnityEngine;
namespace Spine.Unity {
#if NEW_PREFAB_SYSTEM
[ExecuteAlways]
#else
[ExecuteInEditMode]
#endif
[HelpURL("http://esotericsoftware.com/spine-unity-utility-components#BoundingBoxFollowerGraphic")]
public class BoundingBoxFollowerGraphic : MonoBehaviour {
internal static bool DebugMessages = true;
#region Inspector
public SkeletonGraphic skeletonGraphic;
[SpineSlot(dataField: "skeletonGraphic", containsBoundingBoxes: true)]
public string slotName;
public bool isTrigger, usedByEffector, usedByComposite;
public bool clearStateOnDisable = true;
#endregion
Slot slot;
BoundingBoxAttachment currentAttachment;
string currentAttachmentName;
PolygonCollider2D currentCollider;
bool skinBoneEnabled = true;
public readonly Dictionary<BoundingBoxAttachment, PolygonCollider2D> colliderTable = new Dictionary<BoundingBoxAttachment, PolygonCollider2D>();
public readonly Dictionary<BoundingBoxAttachment, string> nameTable = new Dictionary<BoundingBoxAttachment, string>();
public Slot Slot { get { return slot; } }
public BoundingBoxAttachment CurrentAttachment { get { return currentAttachment; } }
public string CurrentAttachmentName { get { return currentAttachmentName; } }
public PolygonCollider2D CurrentCollider { get { return currentCollider; } }
public bool IsTrigger { get { return isTrigger; } }
void Start () {
Initialize();
}
void OnEnable () {
if (skeletonGraphic != null) {
skeletonGraphic.OnRebuild -= HandleRebuild;
skeletonGraphic.OnRebuild += HandleRebuild;
}
Initialize();
}
void HandleRebuild (ISkeletonRenderer sr) {
//if (BoundingBoxFollowerGraphic.DebugMessages) Debug.Log("Skeleton was rebuilt. Repopulating BoundingBoxFollowerGraphic.");
Initialize();
}
/// <summary>
/// Initialize and instantiate the BoundingBoxFollowerGraphic colliders. This is method checks if the BoundingBoxFollowerGraphic has already been initialized for the skeleton instance and slotName and prevents overwriting unless it detects a new setup.</summary>
public void Initialize (bool overwrite = false) {
if (skeletonGraphic == null)
return;
skeletonGraphic.Initialize(false);
if (string.IsNullOrEmpty(slotName))
return;
// Don't reinitialize if the setup did not change.
if (!overwrite &&
colliderTable.Count > 0 && slot != null && // Slot is set and colliders already populated.
slotName == slot.Data.Name // Slot object did not change.
)
return;
slot = null;
currentAttachment = null;
currentAttachmentName = null;
currentCollider = null;
colliderTable.Clear();
nameTable.Clear();
Skeleton skeleton = skeletonGraphic.Skeleton;
if (skeleton == null)
return;
slot = skeleton.FindSlot(slotName);
if (slot == null) {
if (BoundingBoxFollowerGraphic.DebugMessages)
Debug.LogWarning(string.Format("Slot '{0}' not found for BoundingBoxFollowerGraphic on '{1}'. (Previous colliders were disposed.)", slotName, this.gameObject.name));
return;
}
int slotIndex = slot.Data.Index;
int requiredCollidersCount = 0;
PolygonCollider2D[] colliders = GetComponents<PolygonCollider2D>();
if (this.gameObject.activeInHierarchy) {
float scale = skeletonGraphic.MeshScale;
foreach (Skin skin in skeleton.Data.Skins)
AddCollidersForSkin(skin, skeleton, slotIndex, colliders, scale, ref requiredCollidersCount);
if (skeleton.Skin != null)
AddCollidersForSkin(skeleton.Skin, skeleton, slotIndex, colliders, scale, ref requiredCollidersCount);
}
DisposeExcessCollidersAfter(requiredCollidersCount);
skinBoneEnabled = slot.Bone.Active;
if (BoundingBoxFollowerGraphic.DebugMessages) {
bool valid = colliderTable.Count != 0;
if (!valid) {
if (this.gameObject.activeInHierarchy)
Debug.LogWarning("Bounding Box Follower not valid! Slot [" + slotName + "] does not contain any Bounding Box Attachments!");
else
Debug.LogWarning("Bounding Box Follower tried to rebuild as a prefab.");
}
}
}
void AddCollidersForSkin (Skin skin, Skeleton skeleton, int slotIndex, PolygonCollider2D[] previousColliders, float scale, ref int collidersCount) {
if (skin == null) return;
List<Skin.SkinEntry> skinEntries = new List<Skin.SkinEntry>();
skin.GetAttachments(slotIndex, skinEntries);
foreach (Skin.SkinEntry entry in skinEntries) {
Attachment attachment = skin.GetAttachment(slotIndex, entry.Placeholder);
BoundingBoxAttachment boundingBoxAttachment = attachment as BoundingBoxAttachment;
if (BoundingBoxFollowerGraphic.DebugMessages && attachment != null && boundingBoxAttachment == null)
Debug.Log("BoundingBoxFollowerGraphic tried to follow a slot that contains non-boundingbox attachments: " + slotName);
if (boundingBoxAttachment != null) {
if (!colliderTable.ContainsKey(boundingBoxAttachment)) {
PolygonCollider2D bbCollider = collidersCount < previousColliders.Length ?
previousColliders[collidersCount] : gameObject.AddComponent<PolygonCollider2D>();
++collidersCount;
SkeletonUtility.SetColliderPointsLocal(bbCollider, skeleton, slot, boundingBoxAttachment, scale);
bbCollider.isTrigger = isTrigger;
bbCollider.usedByEffector = usedByEffector;
#if USE_COLLIDER_COMPOSITE_OPERATION
bbCollider.compositeOperation = usedByComposite ?
Collider2D.CompositeOperation.Merge : Collider2D.CompositeOperation.None;
#else
bbCollider.usedByComposite = usedByComposite;
#endif
bbCollider.enabled = false;
bbCollider.hideFlags = HideFlags.NotEditable;
colliderTable.Add(boundingBoxAttachment, bbCollider);
nameTable.Add(boundingBoxAttachment, entry.Placeholder);
}
}
}
}
void OnDisable () {
if (clearStateOnDisable)
ClearState();
if (skeletonGraphic != null)
skeletonGraphic.OnRebuild -= HandleRebuild;
}
public void ClearState () {
if (colliderTable != null)
foreach (PolygonCollider2D col in colliderTable.Values)
col.enabled = false;
currentAttachment = null;
currentAttachmentName = null;
currentCollider = null;
}
void DisposeExcessCollidersAfter (int requiredCount) {
PolygonCollider2D[] colliders = GetComponents<PolygonCollider2D>();
if (colliders.Length == 0) return;
for (int i = requiredCount; i < colliders.Length; ++i) {
PolygonCollider2D collider = colliders[i];
if (collider != null) {
#if UNITY_EDITOR
if (Application.isEditor && !Application.isPlaying)
DestroyImmediate(collider);
else
#endif
Destroy(collider);
}
}
}
void LateUpdate () {
var slotPose = slot.AppliedPose;
if (slot != null && (slotPose.Attachment != currentAttachment || skinBoneEnabled != slot.Bone.Active)) {
skinBoneEnabled = slot.Bone.Active;
MatchAttachment(slotPose.Attachment);
}
}
/// <summary>Sets the current collider to match attachment.</summary>
/// <param name="attachment">If the attachment is not a bounding box, it will be treated as null.</param>
void MatchAttachment (Attachment attachment) {
BoundingBoxAttachment bbAttachment = attachment as BoundingBoxAttachment;
if (BoundingBoxFollowerGraphic.DebugMessages && attachment != null && bbAttachment == null)
Debug.LogWarning("BoundingBoxFollowerGraphic tried to match a non-boundingbox attachment. It will treat it as null.");
if (currentCollider != null)
currentCollider.enabled = false;
if (bbAttachment == null) {
currentCollider = null;
currentAttachment = null;
currentAttachmentName = null;
} else {
PolygonCollider2D foundCollider;
colliderTable.TryGetValue(bbAttachment, out foundCollider);
if (foundCollider != null) {
currentCollider = foundCollider;
currentCollider.enabled = true;
currentAttachment = bbAttachment;
currentAttachmentName = nameTable[bbAttachment];
} else {
currentCollider = null;
currentAttachment = bbAttachment;
currentAttachmentName = null;
if (BoundingBoxFollowerGraphic.DebugMessages) Debug.LogFormat("Collider for BoundingBoxAttachment named '{0}' was not initialized. It is possibly from a new skin. currentAttachmentName will be null. You may need to call BoundingBoxFollowerGraphic.Initialize(overwrite: true);", bbAttachment.Name);
}
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1c0bf7b497af9f74280040d96cdf88da
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,197 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2026, 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
#if !SPINE_AUTO_UPGRADE_COMPONENTS_OFF
#define AUTO_UPGRADE_TO_43_COMPONENTS
#endif
using UnityEngine;
using UnityEngine.Serialization;
namespace Spine.Unity {
#if NEW_PREFAB_SYSTEM
[ExecuteAlways]
#else
[ExecuteInEditMode]
#endif
[AddComponentMenu("Spine/Point Follower")]
[HelpURL("https://esotericsoftware.com/spine-unity-utility-components#PointFollower")]
public class PointFollower : MonoBehaviour, IHasSkeletonRenderer, IHasSkeletonComponent, IUpgradable {
public SkeletonRenderer skeletonRenderer;
public ISkeletonRenderer SkeletonRenderer { get { return this.skeletonRenderer; } }
public ISkeletonRenderer Renderer { get { return this.skeletonRenderer; } }
public ISkeletonComponent SkeletonComponent { get { return skeletonRenderer as ISkeletonComponent; } }
[SpineSlot(dataField: "skeletonRenderer", includeNone: true)]
public string slotName;
[SpineAttachment(slotField: "slotName", dataField: "skeletonRenderer", fallbackToTextField: true, includeNone: true)]
public string pointAttachmentName;
public bool followRotation = true;
public bool followSkeletonFlip = true;
public bool followSkeletonZPosition = false;
Transform skeletonTransform;
bool skeletonTransformIsParent;
PointAttachment point;
Bone bone;
bool valid;
public bool IsValid { get { return valid; } }
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
protected void Awake () {
if (!Application.isPlaying && !wasUpgradedTo43) {
UpgradeTo43();
}
}
#endif
public void Initialize () {
valid = skeletonRenderer != null && skeletonRenderer.valid;
if (!valid)
return;
UpdateReferences();
#if UNITY_EDITOR
if (Application.isEditor) LateUpdate();
#endif
}
private void HandleRebuildRenderer (ISkeletonRenderer skeletonRenderer) {
Initialize();
}
void UpdateReferences () {
skeletonTransform = skeletonRenderer.transform;
skeletonRenderer.OnRebuild -= HandleRebuildRenderer;
skeletonRenderer.OnRebuild += HandleRebuildRenderer;
skeletonTransformIsParent = Transform.ReferenceEquals(skeletonTransform, transform.parent);
bone = null;
point = null;
if (!string.IsNullOrEmpty(pointAttachmentName)) {
Skeleton skeleton = skeletonRenderer.Skeleton;
Slot slot = skeleton.FindSlot(slotName);
if (slot != null) {
int slotIndex = slot.Data.Index;
bone = slot.Bone;
point = skeleton.GetAttachment(slotIndex, pointAttachmentName) as PointAttachment;
}
}
}
void OnDestroy () {
if (skeletonRenderer != null)
skeletonRenderer.OnRebuild -= HandleRebuildRenderer;
}
public void LateUpdate () {
#if UNITY_EDITOR
if (!Application.isPlaying) skeletonTransformIsParent = Transform.ReferenceEquals(skeletonTransform, transform.parent);
#endif
if (point == null) {
if (string.IsNullOrEmpty(pointAttachmentName)) return;
UpdateReferences();
if (point == null) return;
}
Vector2 worldPos;
var bonePose = bone.AppliedPose;
point.ComputeWorldPosition(bonePose, out worldPos.x, out worldPos.y);
float rotation = point.ComputeWorldRotation(bonePose);
Transform thisTransform = this.transform;
if (skeletonTransformIsParent) {
// Recommended setup: Use local transform properties if Spine GameObject is the immediate parent
thisTransform.localPosition = new Vector3(worldPos.x, worldPos.y, followSkeletonZPosition ? 0f : thisTransform.localPosition.z);
if (followRotation) {
float halfRotation = rotation * 0.5f * Mathf.Deg2Rad;
Quaternion q = default(Quaternion);
q.z = Mathf.Sin(halfRotation);
q.w = Mathf.Cos(halfRotation);
thisTransform.localRotation = q;
}
} else {
// For special cases: Use transform world properties if transform relationship is complicated
Vector3 targetWorldPosition = skeletonTransform.TransformPoint(new Vector3(worldPos.x, worldPos.y, 0f));
if (!followSkeletonZPosition)
targetWorldPosition.z = thisTransform.position.z;
Transform transformParent = thisTransform.parent;
if (transformParent != null) {
Matrix4x4 m = transformParent.localToWorldMatrix;
if (m.m00 * m.m11 - m.m01 * m.m10 < 0) // Determinant2D is negative
rotation = -rotation;
}
if (followRotation) {
Vector3 transformWorldRotation = skeletonTransform.rotation.eulerAngles;
thisTransform.SetPositionAndRotation(targetWorldPosition, Quaternion.Euler(transformWorldRotation.x, transformWorldRotation.y, transformWorldRotation.z + rotation));
} else {
thisTransform.position = targetWorldPosition;
}
}
if (followSkeletonFlip) {
Vector3 localScale = thisTransform.localScale;
Skeleton skeleton = skeletonRenderer.Skeleton;
localScale.y = Mathf.Abs(localScale.y) * Mathf.Sign(skeleton.ScaleX * skeleton.ScaleY);
thisTransform.localScale = localScale;
}
}
#region Transfer of Deprecated Fields
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
public virtual void UpgradeTo43 () {
wasUpgradedTo43 = true;
if (skeletonRenderer == null) {
Component previousReference = previousSkeletonRenderer != null ? previousSkeletonRenderer : this;
skeletonRenderer = previousReference.GetComponent<SkeletonRenderer>();
if (skeletonRenderer == null)
Debug.LogError("Please manually re-assign SkeletonRenderer at PointFollower, " +
"automatic upgrade failed.", this);
}
}
[SerializeField, HideInInspector, FormerlySerializedAs("skeletonRenderer")] Component previousSkeletonRenderer;
[SerializeField] protected bool wasUpgradedTo43 = false;
#endif
#endregion
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: daf461e4341180341a648c07e1899528
timeCreated: 1518094986
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8459fe8c08b88a84484f2b7ba5f36517
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,82 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2026, 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_2021_1_OR_NEWER
#define HAS_LIST_POOL
#endif
using System.Collections.Generic;
using UnityEngine;
#if HAS_LIST_POOL
using UnityEngine.Pool;
#endif
using UnityEngine.UI;
namespace Spine.Unity {
/// <summary>
/// A minimal MaskableGraphic subclass for rendering multiple submeshes
/// at a <see cref="SkeletonGraphic"/>.
/// </summary>
[RequireComponent(typeof(CanvasRenderer))]
public class SkeletonSubmeshGraphic : MaskableGraphic {
public override void SetMaterialDirty () { }
public override void SetVerticesDirty () { }
protected override void OnPopulateMesh (VertexHelper vh) {
vh.Clear();
}
protected override void OnDisable () {
base.OnDisable();
this.canvasRenderer.cull = true;
}
protected override void OnEnable () {
base.OnEnable();
this.canvasRenderer.cull = false;
}
#if HAS_LIST_POOL
public Material UpdateModifiedMaterial (Material baseMaterial) {
List<IMaterialModifier> modifierComponents = ListPool<IMaterialModifier>.Get();
GetComponents<IMaterialModifier>(modifierComponents);
Material currentMaterial = baseMaterial;
for (int i = 0; i < modifierComponents.Count; i++)
currentMaterial = modifierComponents[i].GetModifiedMaterial(currentMaterial);
ListPool<IMaterialModifier>.Release(modifierComponents);
return currentMaterial;
}
#else
public Material UpdateModifiedMaterial (Material baseMaterial) {
return GetModifiedMaterial(baseMaterial);
}
#endif
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: deeb12332c062954093c24a3fab10b83
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 2b957aa69dae9f948bacdeec549d28ea
folderAsset: yes
timeCreated: 1593173800
licenseType: Pro
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,136 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2026, 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.
*****************************************************************************/
using Spine.Unity.AnimationTools;
using System.Collections.Generic;
using UnityEngine;
namespace Spine.Unity {
/// <summary>
/// Add this component to a SkeletonMecanim GameObject
/// to turn motion of a selected root bone into Transform or RigidBody motion.
/// Local bone translation movement is used as motion.
/// All top-level bones of the skeleton are moved to compensate the root
/// motion bone location, keeping the distance relationship between bones intact.
/// </summary>
/// <remarks>
/// Only compatible with <c>SkeletonMecanim</c>.
/// For <c>SkeletonAnimation</c> or <c>SkeletonGraphic</c> please use
/// <see cref="SkeletonRootMotion">SkeletonRootMotion</see> instead.
/// </remarks>
[HelpURL("https://esotericsoftware.com/spine-unity-utility-components#SkeletonMecanimRootMotion")]
public class SkeletonMecanimRootMotion : SkeletonRootMotionBase {
#region Inspector
const int DefaultMecanimLayerFlags = -1;
public int mecanimLayerFlags = DefaultMecanimLayerFlags;
#endregion
protected Vector2 movementDelta;
protected float rotationDelta;
SkeletonMecanim skeletonMecanim;
public SkeletonMecanim SkeletonMecanim {
get {
return skeletonMecanim ? skeletonMecanim : skeletonMecanim = GetComponent<SkeletonMecanim>();
}
}
public override Vector2 GetRemainingRootMotion (int layerIndex) {
KeyValuePair<Animation, float> pair = skeletonMecanim.Translator.GetActiveAnimationAndTime(layerIndex);
Animation animation = pair.Key;
float time = pair.Value;
if (animation == null)
return Vector2.zero;
float start = time;
float end = animation.Duration;
return GetAnimationRootMotion(start, end, animation);
}
public override RootMotionInfo GetRootMotionInfo (int layerIndex) {
KeyValuePair<Animation, float> pair = skeletonMecanim.Translator.GetActiveAnimationAndTime(layerIndex);
Animation animation = pair.Key;
float time = pair.Value;
if (animation == null)
return new RootMotionInfo();
return GetAnimationRootMotionInfo(animation, time);
}
protected override void Reset () {
base.Reset();
mecanimLayerFlags = DefaultMecanimLayerFlags;
}
public override void Initialize () {
base.Initialize();
skeletonMecanim = GetComponent<SkeletonMecanim>();
if (skeletonMecanim) {
skeletonMecanim.Translator.OnClipApplied -= OnClipApplied;
skeletonMecanim.Translator.OnClipApplied += OnClipApplied;
}
}
void OnClipApplied (Spine.Animation animation, int layerIndex, float weight,
float time, float lastTime, bool playsBackward) {
if (((mecanimLayerFlags & 1 << layerIndex) == 0) || weight == 0)
return;
if (!playsBackward) {
movementDelta += weight * GetAnimationRootMotion(lastTime, time, animation);
} else {
movementDelta -= weight * GetAnimationRootMotion(time, lastTime, animation);
}
if (transformRotation) {
if (!playsBackward) {
rotationDelta += weight * GetAnimationRootMotionRotation(lastTime, time, animation);
} else {
rotationDelta -= weight * GetAnimationRootMotionRotation(time, lastTime, animation);
}
}
}
protected override Vector2 CalculateAnimationsMovementDelta () {
// Note: movement delta is not gathered after animation but
// in OnClipApplied after every applied animation.
Vector2 result = movementDelta;
movementDelta = Vector2.zero;
return result;
}
protected override float CalculateAnimationsRotationDelta () {
// Note: movement delta is not gathered after animation but
// in OnClipApplied after every applied animation.
float result = rotationDelta;
rotationDelta = 0;
return result;
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 95813afe390494344a6ce2cbc8bfb7d1
timeCreated: 1592849332
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,185 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2026, 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.
*****************************************************************************/
using Spine.Unity.AnimationTools;
using System.Collections.Generic;
using UnityEngine;
namespace Spine.Unity {
/// <summary>
/// Add this component to a SkeletonAnimation or SkeletonGraphic GameObject
/// to turn motion of a selected root bone into Transform or RigidBody motion.
/// Local bone translation movement is used as motion.
/// All top-level bones of the skeleton are moved to compensate the root
/// motion bone location, keeping the distance relationship between bones intact.
/// </summary>
/// <remarks>
/// Only compatible with SkeletonAnimation (or other components that implement
/// ISkeletonComponent, ISkeletonAnimation and IAnimationStateComponent).
/// For <c>SkeletonMecanim</c> please use
/// <see cref="SkeletonMecanimRootMotion">SkeletonMecanimRootMotion</see> instead.
/// </remarks>
[HelpURL("https://esotericsoftware.com/spine-unity-utility-components#SkeletonRootMotion")]
public class SkeletonRootMotion : SkeletonRootMotionBase {
#region Inspector
const int DefaultAnimationTrackFlags = -1;
public int animationTrackFlags = DefaultAnimationTrackFlags;
#endregion
AnimationState animationState;
SkeletonGraphic skeletonGraphic;
public override Vector2 GetRemainingRootMotion (int trackIndex) {
TrackEntry entry = animationState.GetTrack(trackIndex);
if (entry == null)
return Vector2.zero;
Animation animation = entry.Animation;
float start = entry.AnimationTime;
float end = animation.Duration;
return GetAnimationRootMotion(start, end, animation);
}
public override RootMotionInfo GetRootMotionInfo (int trackIndex) {
TrackEntry entry = animationState.GetTrack(trackIndex);
if (entry == null)
return new RootMotionInfo();
Animation animation = entry.Animation;
float time = entry.AnimationTime;
return GetAnimationRootMotionInfo(entry.Animation, time);
}
protected override float AdditionalScale {
get {
return skeletonGraphic ? skeletonGraphic.MeshScale : 1.0f;
}
}
protected override void Reset () {
base.Reset();
animationTrackFlags = DefaultAnimationTrackFlags;
}
public override void Initialize () {
base.Initialize();
IAnimationStateComponent animstateComponent = animationComponent as IAnimationStateComponent;
this.animationState = (animstateComponent != null) ? animstateComponent.AnimationState : null;
skeletonGraphic = this.GetComponent<SkeletonGraphic>();
}
protected override Vector2 CalculateAnimationsMovementDelta () {
Vector2 localDelta = Vector2.zero;
int trackCount = animationState.Tracks.Count;
for (int trackIndex = 0; trackIndex < trackCount; ++trackIndex) {
// note: animationTrackFlags != -1 below covers trackIndex >= 32,
// with -1 corresponding to entry "everything" of the dropdown list.
if (animationTrackFlags != -1 && (animationTrackFlags & 1 << trackIndex) == 0)
continue;
TrackEntry entry = animationState.GetTrack(trackIndex);
TrackEntry next = null;
while (entry != null) {
Animation animation = entry.Animation;
float start = entry.AnimationLast;
float end = entry.AnimationTime;
Vector2 currentDelta = GetAnimationRootMotion(start, end, animation);
if (currentDelta != Vector2.zero) {
ApplyMixAlphaToDelta(ref currentDelta, next, entry);
localDelta += currentDelta;
}
// Traverse mixingFrom chain.
next = entry;
entry = entry.MixingFrom;
}
}
return localDelta;
}
protected override float CalculateAnimationsRotationDelta () {
float localDelta = 0;
int trackCount = animationState.Tracks.Count;
for (int trackIndex = 0; trackIndex < trackCount; ++trackIndex) {
// note: animationTrackFlags != -1 below covers trackIndex >= 32,
// with -1 corresponding to entry "everything" of the dropdown list.
if (animationTrackFlags != -1 && (animationTrackFlags & 1 << trackIndex) == 0)
continue;
TrackEntry entry = animationState.GetTrack(trackIndex);
TrackEntry next = null;
while (entry != null) {
Animation animation = entry.Animation;
float start = entry.AnimationLast;
float end = entry.AnimationTime;
float currentDelta = GetAnimationRootMotionRotation(start, end, animation);
if (currentDelta != 0) {
ApplyMixAlphaToDelta(ref currentDelta, next, entry);
localDelta += currentDelta;
}
// Traverse mixingFrom chain.
next = entry;
entry = entry.MixingFrom;
}
}
return localDelta;
}
void ApplyMixAlphaToDelta (ref Vector2 currentDelta, TrackEntry next, TrackEntry entry) {
float mixAlpha = 1;
GetMixAlpha(ref mixAlpha, next, entry);
currentDelta *= mixAlpha;
}
void ApplyMixAlphaToDelta (ref float currentDelta, TrackEntry next, TrackEntry entry) {
float mixAlpha = 1;
GetMixAlpha(ref mixAlpha, next, entry);
currentDelta *= mixAlpha;
}
void GetMixAlpha (ref float cumulatedMixAlpha, TrackEntry next, TrackEntry entry) {
// code below based on AnimationState.cs
if (next != null) {
float mix = next.MixDuration == 0 ? 1 : Mathf.Min(1, next.MixTime / next.MixDuration);
float fromMix = (entry.MixingFrom == null || entry.MixDuration == 0) ?
1 : Mathf.Min(1, entry.MixTime / entry.MixDuration);
float mixAndAlpha = entry.Alpha * fromMix * (1 - mix);
cumulatedMixAlpha *= mixAndAlpha;
} else {
float mix = entry.MixDuration == 0 ? 1 : Mathf.Min(1, entry.Alpha * (entry.MixTime / entry.MixDuration));
cumulatedMixAlpha *= mix;
}
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: f21c9538588898a45a3da22bf4779ab3
timeCreated: 1591121072
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,766 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2026, 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_6000_0_OR_NEWER
#define RIGIDBODY2D_USES_LINEAR_VELOCITY
#endif
// In order to respect TransformConstraints modifying the scale of parent bones,
// GetScaleAffectingRootMotion() now uses parentBone.AScaleX and AScaleY instead
// of previously used ScaleX and ScaleY. If you require the previous behaviour,
// comment out the define below.
#define USE_APPLIED_PARENT_SCALE
using Spine.Unity.AnimationTools;
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Spine.Unity {
/// <summary>
/// Base class for skeleton root motion components.
/// </summary>
[DefaultExecutionOrder(1)]
abstract public class SkeletonRootMotionBase : MonoBehaviour {
#region Inspector
[SpineBone]
public string rootMotionBoneName = "root";
public bool transformPositionX = true;
public bool transformPositionY = true;
public bool transformRotation = false;
public float rootMotionScaleX = 1;
public float rootMotionScaleY = 1;
public float rootMotionScaleRotation = 1;
/// <summary>Skeleton space X translation per skeleton space Y translation root motion.</summary>
public float rootMotionTranslateXPerY = 0;
/// <summary>Skeleton space Y translation per skeleton space X translation root motion.</summary>
public float rootMotionTranslateYPerX = 0;
[Header("Optional")]
public Rigidbody2D rigidBody2D;
public bool applyRigidbody2DGravity = false;
public Rigidbody rigidBody;
/// <summary>Delegate type for customizing application of rootmotion.
public delegate void RootMotionDelegate (SkeletonRootMotionBase component, Vector2 translation, float rotation);
/// <summary>This callback can be used to apply root-motion in a custom way. It is raised after evaluating
/// this animation frame's root-motion, before it is potentially applied (see <see cref="disableOnOverride"/>)
/// to either Transform or Rigidbody.
/// When <see cref="SkeletonAnimation.UpdateTiming"/> is set to <see cref="UpdateTiming.InUpdate"/>, multiple
/// animation frames might take place before <c>FixedUpdate</c> is called once.
/// The callback parameters <c>translation</c> and <c>rotation</c> are filled out with
/// this animation frame's skeleton-space root-motion (not cumulated). You can use
/// e.g. <c>transform.TransformVector()</c> to transform skeleton-space root-motion to world space.
/// </summary>
/// <seealso cref="PhysicsUpdateRootMotionOverride"/>
public event RootMotionDelegate ProcessRootMotionOverride;
/// <summary>This callback can be used to apply root-motion in a custom way. It is raised in FixedUpdate
/// after (when <see cref="disableOnOverride"/> is set to false) or instead of when root-motion
/// would be applied at the Rigidbody.
/// When <see cref="SkeletonAnimation.UpdateTiming"/> is set to <see cref="UpdateTiming.InUpdate"/>, multiple
/// animation frames might take place before before <c>FixedUpdate</c> is called once.
/// The callback parameters <c>translation</c> and <c>rotation</c> are filled out with the
/// (cumulated) skeleton-space root-motion since the the last <c>FixedUpdate</c> call. You can use
/// e.g. <c>transform.TransformVector()</c> to transform skeleton-space root-motion to world space.
/// </summary>
/// <seealso cref="ProcessRootMotionOverride"/>
public event RootMotionDelegate PhysicsUpdateRootMotionOverride;
/// <summary>When true, root-motion is not applied to the Transform or Rigidbody.
/// Otherwise the delegate callbacks are issued additionally.</summary>
public bool disableOnOverride = true;
public Bone RootMotionBone { get { return rootMotionBone; } }
public bool UsesRigidbody {
get { return rigidBody != null || rigidBody2D != null; }
}
/// <summary>Root motion translation that has been applied in the preceding <c>FixedUpdate</c> call
/// if a rigidbody is assigned at either <c>rigidbody</c> or <c>rigidbody2D</c>.
/// Returns <c>Vector2.zero</c> when <c>rigidbody</c> and <c>rigidbody2D</c> are null.
/// This can be necessary when multiple scripts call <c>Rigidbody2D.MovePosition</c>,
/// where the last call overwrites the effect of preceding ones.</summary>
public Vector2 PreviousRigidbodyRootMotion2D {
get { return new Vector2(previousRigidbodyRootMotion.x, previousRigidbodyRootMotion.y); }
}
/// <summary>Root motion translation that has been applied in the preceding <c>FixedUpdate</c> call
/// if a rigidbody is assigned at either <c>rigidbody</c> or <c>rigidbody2D</c>.
/// Returns <c>Vector3.zero</c> when <c>rigidbody</c> and <c>rigidbody2D</c> are null.</summary>
public Vector3 PreviousRigidbodyRootMotion3D {
get { return previousRigidbodyRootMotion; }
}
/// <summary>Additional translation to add to <c>Rigidbody2D.MovePosition</c>
/// called in FixedUpdate. This can be necessary when multiple scripts call
/// <c>MovePosition</c>, where the last call overwrites the effect of preceding ones.
/// Has no effect if <c>rigidBody2D</c> is null.</summary>
public Vector2 AdditionalRigidbody2DMovement {
get { return additionalRigidbody2DMovement; }
set { additionalRigidbody2DMovement = value; }
}
#endregion
protected bool SkeletonAnimationUsesFixedUpdate {
get {
if (animationComponent != null) {
return animationComponent.UpdateTiming == UpdateTiming.InFixedUpdate;
}
return false;
}
}
protected SkeletonAnimationBase animationComponent;
protected Bone rootMotionBone;
protected int rootMotionBoneIndex;
protected List<int> transformConstraintIndices = new List<int>();
protected List<Vector2> transformConstraintLastPos = new List<Vector2>();
protected List<float> transformConstraintLastRotation = new List<float>();
protected List<Bone> topLevelBones = new List<Bone>();
protected Vector2 initialOffset = Vector2.zero;
protected bool accumulatedUntilFixedUpdate = false;
protected Vector2 tempSkeletonDisplacement;
protected Vector3 rigidbodyDisplacement;
protected Vector3 previousRigidbodyRootMotion = Vector2.zero;
protected Vector2 additionalRigidbody2DMovement = Vector2.zero;
protected Quaternion rigidbodyLocalRotation = Quaternion.identity;
protected float rigidbody2DRotation;
protected float initialOffsetRotation;
protected float tempSkeletonRotation;
protected virtual void Reset () {
FindRigidbodyComponent();
}
protected virtual void Start () {
Initialize();
}
protected void InitializeOnRebuild (ISkeletonAnimation animatedSkeletonComponent) {
Initialize();
}
public virtual void Initialize () {
animationComponent = GetComponent<SkeletonAnimationBase>();
GatherTopLevelBones();
SetRootMotionBone(rootMotionBoneName);
if (rootMotionBone != null) {
initialOffset = new Vector2(rootMotionBone.Pose.X, rootMotionBone.Pose.Y);
initialOffsetRotation = rootMotionBone.Pose.Rotation;
}
if (animationComponent != null) {
animationComponent.UpdateLocal -= HandleUpdateLocal;
animationComponent.UpdateLocal += HandleUpdateLocal;
animationComponent.OnAnimationRebuild -= InitializeOnRebuild;
animationComponent.OnAnimationRebuild += InitializeOnRebuild;
SkeletonUtility skeletonUtility = GetComponent<SkeletonUtility>();
if (skeletonUtility != null) {
// SkeletonUtilityBone shall receive UpdateLocal callbacks for bone-following after root motion
// clears the root-bone position.
skeletonUtility.ResubscribeEvents();
}
}
}
protected virtual void FixedUpdate () {
// Root motion is only applied when component is enabled.
if (!this.isActiveAndEnabled)
return;
// When SkeletonAnimation component uses UpdateTiming.InFixedUpdate,
// we directly call PhysicsUpdate in HandleUpdateLocal instead of here.
if (!SkeletonAnimationUsesFixedUpdate)
PhysicsUpdate(false);
}
protected virtual void PhysicsUpdate (bool skeletonAnimationUsesFixedUpdate) {
Vector2 callbackDisplacement = tempSkeletonDisplacement;
float callbackRotation = tempSkeletonRotation;
bool isApplyAtRigidbodyAllowed = PhysicsUpdateRootMotionOverride == null || !disableOnOverride;
if (isApplyAtRigidbodyAllowed) {
if (rigidBody2D != null) {
Vector2 gravityAndVelocityMovement = Vector2.zero;
if (applyRigidbody2DGravity) {
float deltaTime = Time.fixedDeltaTime;
float deltaTimeSquared = (deltaTime * deltaTime);
#if RIGIDBODY2D_USES_LINEAR_VELOCITY
rigidBody2D.linearVelocity += rigidBody2D.gravityScale * Physics2D.gravity * deltaTime;
gravityAndVelocityMovement = 0.5f * rigidBody2D.gravityScale * Physics2D.gravity * deltaTimeSquared +
rigidBody2D.linearVelocity * deltaTime;
#else
rigidBody2D.velocity += rigidBody2D.gravityScale * Physics2D.gravity * deltaTime;
gravityAndVelocityMovement = 0.5f * rigidBody2D.gravityScale * Physics2D.gravity * deltaTimeSquared +
rigidBody2D.velocity * deltaTime;
#endif
}
Vector2 rigidbodyDisplacement2D = new Vector2(rigidbodyDisplacement.x, rigidbodyDisplacement.y);
// Note: MovePosition seems to be the only precise and reliable way to set movement delta,
// for both 2D and 3D rigidbodies.
// Setting velocity like "rigidBody2D.velocity = movement/deltaTime" works perfectly in mid-air
// without gravity and ground collision, unfortunately when on the ground, friction causes severe
// slowdown. Using a zero-friction PhysicsMaterial leads to sliding endlessly along the ground as
// soon as forces are applied. Additionally, there is no rigidBody2D.isGrounded, requiring our own
// checks.
rigidBody2D.MovePosition(gravityAndVelocityMovement + new Vector2(rigidBody2D.position.x, rigidBody2D.position.y)
+ rigidbodyDisplacement2D + additionalRigidbody2DMovement);
rigidBody2D.MoveRotation(rigidbody2DRotation + rigidBody2D.rotation);
} else if (rigidBody != null) {
rigidBody.MovePosition(rigidBody.position
+ new Vector3(rigidbodyDisplacement.x, rigidbodyDisplacement.y, rigidbodyDisplacement.z));
rigidBody.MoveRotation(rigidBody.rotation * rigidbodyLocalRotation);
}
}
previousRigidbodyRootMotion = rigidbodyDisplacement;
if (accumulatedUntilFixedUpdate) {
Vector2 parentBoneScale;
GetScaleAffectingRootMotion(out parentBoneScale);
ClearEffectiveBoneOffsets(parentBoneScale);
animationComponent.Skeleton.UpdateWorldTransform(Physics.Pose);
}
ClearRigidbodyTempMovement();
if (PhysicsUpdateRootMotionOverride != null)
PhysicsUpdateRootMotionOverride(this, callbackDisplacement, callbackRotation);
}
protected virtual void OnDisable () {
ClearRigidbodyTempMovement();
}
protected void FindRigidbodyComponent () {
rigidBody2D = this.GetComponent<Rigidbody2D>();
if (!rigidBody2D)
rigidBody = this.GetComponent<Rigidbody>();
if (!rigidBody2D && !rigidBody) {
rigidBody2D = this.GetComponentInParent<Rigidbody2D>();
if (!rigidBody2D)
rigidBody = this.GetComponentInParent<Rigidbody>();
}
}
protected virtual float AdditionalScale { get { return 1.0f; } }
abstract protected Vector2 CalculateAnimationsMovementDelta ();
protected virtual float CalculateAnimationsRotationDelta () { return 0; }
abstract public Vector2 GetRemainingRootMotion (int trackIndex = 0);
public struct RootMotionInfo {
public Vector2 start;
public Vector2 current;
public Vector2 mid;
public Vector2 end;
public bool timeIsPastMid;
};
abstract public RootMotionInfo GetRootMotionInfo (int trackIndex = 0);
public ISkeletonComponent TargetSkeletonComponent {
get {
return TargetSkeletonAnimation;
}
}
public ISkeletonAnimation TargetSkeletonAnimationComponent {
get {
return TargetSkeletonAnimation;
}
}
public SkeletonAnimationBase TargetSkeletonAnimation {
get {
if (animationComponent == null)
animationComponent = GetComponent<SkeletonAnimationBase>();
return animationComponent;
}
}
public void SetRootMotionBone (string name) {
Skeleton skeleton = animationComponent.Skeleton;
Bone bone = skeleton.FindBone(name);
if (bone != null) {
this.rootMotionBoneIndex = bone.Data.Index;
this.rootMotionBone = bone;
FindTransformConstraintsAffectingBone();
} else {
Debug.Log("Bone named \"" + name + "\" could not be found. " +
"Set 'skeletonRootMotion.rootMotionBoneName' before calling 'skeletonAnimation.Initialize(true)'.");
this.rootMotionBoneIndex = 0;
this.rootMotionBone = skeleton.RootBone;
}
}
public void AdjustRootMotionToDistance (Vector2 distanceToTarget, int trackIndex = 0, bool adjustX = true, bool adjustY = true,
float minX = 0, float maxX = float.MaxValue, float minY = 0, float maxY = float.MaxValue,
bool allowXTranslation = false, bool allowYTranslation = false) {
Vector2 distanceToTargetSkeletonSpace = (Vector2)transform.InverseTransformVector(distanceToTarget);
Vector2 scaleAffectingRootMotion = GetScaleAffectingRootMotion();
if (UsesRigidbody)
distanceToTargetSkeletonSpace -= tempSkeletonDisplacement;
Vector2 remainingRootMotionSkeletonSpace = GetRemainingRootMotion(trackIndex);
remainingRootMotionSkeletonSpace.Scale(scaleAffectingRootMotion);
if (remainingRootMotionSkeletonSpace.x == 0)
remainingRootMotionSkeletonSpace.x = 0.0001f;
if (remainingRootMotionSkeletonSpace.y == 0)
remainingRootMotionSkeletonSpace.y = 0.0001f;
if (adjustX)
rootMotionScaleX = Math.Min(maxX, Math.Max(minX, distanceToTargetSkeletonSpace.x / remainingRootMotionSkeletonSpace.x));
if (adjustY)
rootMotionScaleY = Math.Min(maxY, Math.Max(minY, distanceToTargetSkeletonSpace.y / remainingRootMotionSkeletonSpace.y));
if (allowXTranslation)
rootMotionTranslateXPerY = (distanceToTargetSkeletonSpace.x - remainingRootMotionSkeletonSpace.x * rootMotionScaleX) / remainingRootMotionSkeletonSpace.y;
if (allowYTranslation)
rootMotionTranslateYPerX = (distanceToTargetSkeletonSpace.y - remainingRootMotionSkeletonSpace.y * rootMotionScaleY) / remainingRootMotionSkeletonSpace.x;
}
public Vector2 GetAnimationRootMotion (Animation animation) {
return GetAnimationRootMotion(0, animation.Duration, animation);
}
public Vector2 GetAnimationRootMotion (float startTime, float endTime,
Animation animation) {
if (startTime == endTime)
return Vector2.zero;
TranslateTimeline translateTimeline = animation.FindTranslateTimelineForBone(rootMotionBoneIndex);
TranslateXTimeline xTimeline = animation.FindTimelineForBone<TranslateXTimeline>(rootMotionBoneIndex);
TranslateYTimeline yTimeline = animation.FindTimelineForBone<TranslateYTimeline>(rootMotionBoneIndex);
// Non-looped base
Vector2 endPos = Vector2.zero;
Vector2 startPos = Vector2.zero;
if (translateTimeline != null) {
endPos = translateTimeline.Evaluate(endTime);
startPos = translateTimeline.Evaluate(startTime);
} else if (xTimeline != null || yTimeline != null) {
endPos = TimelineExtensions.Evaluate(xTimeline, yTimeline, endTime);
startPos = TimelineExtensions.Evaluate(xTimeline, yTimeline, startTime);
}
ExposedList<IConstraint> constraints = animationComponent.Skeleton.Constraints;
IConstraint[] constraintsItems = constraints.Items;
foreach (int constraintIndex in this.transformConstraintIndices) {
TransformConstraint constraint = constraintsItems[constraintIndex] as TransformConstraint;
if (constraint == null) continue;
ApplyConstraintToPos(animation, constraint, constraintIndex, endTime, false, ref endPos);
ApplyConstraintToPos(animation, constraint, constraintIndex, startTime, true, ref startPos);
}
Vector2 currentDelta = endPos - startPos;
// Looped additions
if (startTime > endTime) {
Vector2 loopPos = Vector2.zero;
Vector2 zeroPos = Vector2.zero;
if (translateTimeline != null) {
loopPos = translateTimeline.Evaluate(animation.Duration);
zeroPos = translateTimeline.Evaluate(0);
} else if (xTimeline != null || yTimeline != null) {
loopPos = TimelineExtensions.Evaluate(xTimeline, yTimeline, animation.Duration);
zeroPos = TimelineExtensions.Evaluate(xTimeline, yTimeline, 0);
}
foreach (int constraintIndex in this.transformConstraintIndices) {
TransformConstraint constraint = (TransformConstraint)constraintsItems[constraintIndex];
ApplyConstraintToPos(animation, constraint, constraintIndex, animation.Duration, false, ref loopPos);
ApplyConstraintToPos(animation, constraint, constraintIndex, 0, false, ref zeroPos);
}
currentDelta += loopPos - zeroPos;
}
UpdateLastConstraintPos(constraintsItems);
return currentDelta;
}
public float GetAnimationRootMotionRotation (Animation animation) {
return GetAnimationRootMotionRotation(0, animation.Duration, animation);
}
public float GetAnimationRootMotionRotation (float startTime, float endTime,
Animation animation) {
if (startTime == endTime)
return 0;
RotateTimeline rotateTimeline = animation.FindTimelineForBone<RotateTimeline>(rootMotionBoneIndex);
// Non-looped base
float endRotation = 0;
float startRotation = 0;
if (rotateTimeline != null) {
endRotation = rotateTimeline.Evaluate(endTime);
startRotation = rotateTimeline.Evaluate(startTime);
}
ExposedList<IConstraint> constraints = animationComponent.Skeleton.Constraints;
IConstraint[] constraintsItems = constraints.Items;
foreach (int constraintIndex in this.transformConstraintIndices) {
TransformConstraint constraint = constraintsItems[constraintIndex] as TransformConstraint;
if (constraint == null) continue;
ApplyConstraintToRotation(animation, constraint, constraintIndex, endTime, false, ref endRotation);
ApplyConstraintToRotation(animation, constraint, constraintIndex, startTime, true, ref startRotation);
}
float currentDelta = endRotation - startRotation;
// Looped additions
if (startTime > endTime) {
float loopRotation = 0;
float zeroPos = 0;
if (rotateTimeline != null) {
loopRotation = rotateTimeline.Evaluate(animation.Duration);
zeroPos = rotateTimeline.Evaluate(0);
}
foreach (int constraintIndex in this.transformConstraintIndices) {
TransformConstraint constraint = (TransformConstraint)constraintsItems[constraintIndex];
ApplyConstraintToRotation(animation, constraint, constraintIndex, animation.Duration, false, ref loopRotation);
ApplyConstraintToRotation(animation, constraint, constraintIndex, 0, false, ref zeroPos);
}
currentDelta += loopRotation - zeroPos;
}
UpdateLastConstraintRotation(constraintsItems);
return currentDelta;
}
void ApplyConstraintToPos (Animation animation, TransformConstraint constraint,
int constraintIndex, float time, bool useLastConstraintPos, ref Vector2 pos) {
TransformConstraintTimeline timeline = animation.FindTransformConstraintTimeline(constraintIndex);
if (timeline == null)
return;
Vector2 mixXY = timeline.EvaluateTranslateXYMix(time);
Vector2 invMixXY = timeline.EvaluateTranslateXYMix(time);
Vector2 constraintPos;
if (useLastConstraintPos)
constraintPos = transformConstraintLastPos[GetConstraintLastPosIndex(constraintIndex)];
else {
Bone sourceBone = constraint.Source;
constraintPos = new Vector2(sourceBone.Pose.X, sourceBone.Pose.Y);
}
pos = new Vector2(
pos.x * invMixXY.x + constraintPos.x * mixXY.x,
pos.y * invMixXY.y + constraintPos.y * mixXY.y);
}
void ApplyConstraintToRotation (Animation animation, TransformConstraint constraint,
int constraintIndex, float time, bool useLastConstraintRotation, ref float rotation) {
TransformConstraintTimeline timeline = animation.FindTransformConstraintTimeline(constraintIndex);
if (timeline == null)
return;
float mixRotate = timeline.EvaluateRotateMix(time);
float invMixRotate = timeline.EvaluateRotateMix(time);
float constraintRotation;
if (useLastConstraintRotation)
constraintRotation = transformConstraintLastRotation[GetConstraintLastPosIndex(constraintIndex)];
else {
Bone sourceBone = constraint.Source;
constraintRotation = sourceBone.Pose.Rotation;
}
rotation = rotation * invMixRotate + constraintRotation * mixRotate;
}
void UpdateLastConstraintPos (IConstraint[] constraintsItems) {
foreach (int constraintIndex in this.transformConstraintIndices) {
TransformConstraint constraint = (TransformConstraint)constraintsItems[constraintIndex];
Bone sourceBone = constraint.Source;
transformConstraintLastPos[GetConstraintLastPosIndex(constraintIndex)] = new Vector2(sourceBone.Pose.X, sourceBone.Pose.Y);
}
}
void UpdateLastConstraintRotation (IConstraint[] constraintsItems) {
foreach (int constraintIndex in this.transformConstraintIndices) {
TransformConstraint constraint = (TransformConstraint)constraintsItems[constraintIndex];
Bone sourceBone = constraint.Source;
transformConstraintLastRotation[GetConstraintLastPosIndex(constraintIndex)] = sourceBone.Pose.Rotation;
}
}
public RootMotionInfo GetAnimationRootMotionInfo (Animation animation, float currentTime) {
RootMotionInfo rootMotion = new RootMotionInfo();
float duration = animation.Duration;
float mid = duration * 0.5f;
rootMotion.timeIsPastMid = currentTime > mid;
TranslateTimeline timeline = animation.FindTranslateTimelineForBone(rootMotionBoneIndex);
if (timeline != null) {
rootMotion.start = timeline.Evaluate(0);
rootMotion.current = timeline.Evaluate(currentTime);
rootMotion.mid = timeline.Evaluate(mid);
rootMotion.end = timeline.Evaluate(duration);
return rootMotion;
}
TranslateXTimeline xTimeline = animation.FindTimelineForBone<TranslateXTimeline>(rootMotionBoneIndex);
TranslateYTimeline yTimeline = animation.FindTimelineForBone<TranslateYTimeline>(rootMotionBoneIndex);
if (xTimeline != null || yTimeline != null) {
rootMotion.start = TimelineExtensions.Evaluate(xTimeline, yTimeline, 0);
rootMotion.current = TimelineExtensions.Evaluate(xTimeline, yTimeline, currentTime);
rootMotion.mid = TimelineExtensions.Evaluate(xTimeline, yTimeline, mid);
rootMotion.end = TimelineExtensions.Evaluate(xTimeline, yTimeline, duration);
return rootMotion;
}
return rootMotion;
}
int GetConstraintLastPosIndex (int constraintIndex) {
ExposedList<IConstraint> constraints = animationComponent.Skeleton.Constraints;
return transformConstraintIndices.FindIndex(addedIndex => addedIndex == constraintIndex);
}
void FindTransformConstraintsAffectingBone () {
ExposedList<IConstraint> constraints = animationComponent.Skeleton.Constraints;
IConstraint[] constraintsItems = constraints.Items;
for (int constraintIndex = 0, n = constraints.Count; constraintIndex < n; ++constraintIndex) {
TransformConstraint constraint = constraintsItems[constraintIndex] as TransformConstraint;
if (constraint == null) continue;
if (constraint.Bones.Contains(rootMotionBone.AppliedPose)) {
transformConstraintIndices.Add(constraintIndex);
Bone sourceBone = constraint.Source;
Vector2 constraintPos = new Vector2(sourceBone.Pose.X, sourceBone.Pose.Y);
transformConstraintLastPos.Add(constraintPos);
transformConstraintLastRotation.Add(sourceBone.Pose.Rotation);
}
}
}
Vector2 GetTimelineMovementDelta (float startTime, float endTime,
TranslateXTimeline xTimeline, TranslateYTimeline yTimeline, Animation animation) {
Vector2 currentDelta;
if (startTime > endTime) // Looped
currentDelta =
(TimelineExtensions.Evaluate(xTimeline, yTimeline, animation.Duration)
- TimelineExtensions.Evaluate(xTimeline, yTimeline, startTime))
+ (TimelineExtensions.Evaluate(xTimeline, yTimeline, endTime)
- TimelineExtensions.Evaluate(xTimeline, yTimeline, 0));
else if (startTime != endTime) // Non-looped
currentDelta = TimelineExtensions.Evaluate(xTimeline, yTimeline, endTime)
- TimelineExtensions.Evaluate(xTimeline, yTimeline, startTime);
else
currentDelta = Vector2.zero;
return currentDelta;
}
void GatherTopLevelBones () {
topLevelBones.Clear();
Skeleton skeleton = animationComponent.Skeleton;
foreach (Bone bone in skeleton.Bones) {
if (bone.Parent == null)
topLevelBones.Add(bone);
}
}
void HandleUpdateLocal (ISkeletonRenderer skeletonRenderer) {
if (!this.isActiveAndEnabled)
return; // Root motion is only applied when component is enabled.
Vector2 boneLocalDelta = CalculateAnimationsMovementDelta();
Vector2 parentBoneScale;
Vector2 totalScale;
Vector2 skeletonTranslationDelta = GetSkeletonSpaceMovementDelta(boneLocalDelta, out parentBoneScale, out totalScale);
float skeletonRotationDelta = 0;
if (transformRotation) {
float boneLocalDeltaRotation = CalculateAnimationsRotationDelta();
boneLocalDeltaRotation *= rootMotionScaleRotation;
skeletonRotationDelta = GetSkeletonSpaceRotationDelta(boneLocalDeltaRotation, totalScale);
}
bool usesFixedUpdate = SkeletonAnimationUsesFixedUpdate;
ApplyRootMotion(skeletonTranslationDelta, skeletonRotationDelta, parentBoneScale, usesFixedUpdate);
if (usesFixedUpdate)
PhysicsUpdate(usesFixedUpdate);
}
void ApplyRootMotion (Vector2 skeletonTranslationDelta, float skeletonRotationDelta, Vector2 parentBoneScale,
bool skeletonAnimationUsesFixedUpdate) {
// Accumulated displacement is applied on the next Physics update in FixedUpdate.
// Until the next Physics update, tempSkeletonDisplacement and tempSkeletonRotation
// are offsetting bone locations to prevent stutter which would otherwise occur if
// we don't move every Update.
bool usesRigidbody = this.UsesRigidbody;
bool applyToTransform = !usesRigidbody && (ProcessRootMotionOverride == null || !disableOnOverride);
accumulatedUntilFixedUpdate = !applyToTransform && !skeletonAnimationUsesFixedUpdate;
if (ProcessRootMotionOverride != null)
ProcessRootMotionOverride(this, skeletonTranslationDelta, skeletonRotationDelta);
// Apply root motion to Transform or update values applied to RigidBody later (must happen in FixedUpdate).
if (usesRigidbody) {
rigidbodyDisplacement += transform.TransformVector(skeletonTranslationDelta);
if (skeletonRotationDelta != 0.0f) {
if (rigidBody != null) {
Quaternion addedWorldRotation = Quaternion.Euler(0, 0, skeletonRotationDelta);
rigidbodyLocalRotation = rigidbodyLocalRotation * addedWorldRotation;
} else if (rigidBody2D != null) {
Vector3 lossyScale = transform.lossyScale;
float rotationSign = lossyScale.x * lossyScale.y > 0 ? 1 : -1;
rigidbody2DRotation += rotationSign * skeletonRotationDelta;
}
}
} else if (applyToTransform) {
transform.position += transform.TransformVector(skeletonTranslationDelta);
if (skeletonRotationDelta != 0.0f) {
Vector3 lossyScale = transform.lossyScale;
float rotationSign = lossyScale.x * lossyScale.y > 0 ? 1 : -1;
transform.Rotate(0, 0, rotationSign * skeletonRotationDelta);
}
}
tempSkeletonDisplacement += skeletonTranslationDelta;
tempSkeletonRotation += skeletonRotationDelta;
if (accumulatedUntilFixedUpdate) {
SetEffectiveBoneOffsetsTo(tempSkeletonDisplacement, tempSkeletonRotation, parentBoneScale);
} else {
ClearEffectiveBoneOffsets(parentBoneScale);
}
}
void ApplyTransformConstraints () {
rootMotionBone.AppliedPose.X = rootMotionBone.Pose.X;
rootMotionBone.AppliedPose.Y = rootMotionBone.Pose.Y;
rootMotionBone.AppliedPose.Rotation = rootMotionBone.Pose.Rotation;
IConstraint[] transformConstraintsItems = animationComponent.Skeleton.Constraints.Items;
foreach (int constraintIndex in this.transformConstraintIndices) {
TransformConstraint constraint = (TransformConstraint)transformConstraintsItems[constraintIndex];
// apply the constraint and sets Bone.ax, Bone.ay and Bone.arotation values.
/// Update is based on Bone.x, Bone.y and Bone.rotation, so skeleton.UpdateWorldTransform()
/// can be called afterwards without having a different starting point.
constraint.Update(animationComponent.Skeleton, Physics.None);
}
}
Vector2 GetScaleAffectingRootMotion () {
Vector2 parentBoneScale;
return GetScaleAffectingRootMotion(out parentBoneScale);
}
Vector2 GetScaleAffectingRootMotion (out Vector2 parentBoneScale) {
Skeleton skeleton = animationComponent.Skeleton;
Vector2 totalScale = Vector2.one;
totalScale.x *= skeleton.ScaleX;
totalScale.y *= skeleton.ScaleY;
parentBoneScale = Vector2.one;
Bone scaleBone = rootMotionBone;
while ((scaleBone = scaleBone.Parent) != null) {
#if USE_APPLIED_PARENT_SCALE
parentBoneScale.x *= scaleBone.AppliedPose.ScaleX;
parentBoneScale.y *= scaleBone.AppliedPose.ScaleY;
#else
parentBoneScale.x *= scaleBone.Pose.ScaleX;
parentBoneScale.y *= scaleBone.Pose.ScaleY;
#endif
}
totalScale = Vector2.Scale(totalScale, parentBoneScale);
totalScale *= AdditionalScale;
return totalScale;
}
Vector2 GetSkeletonSpaceMovementDelta (Vector2 boneLocalDelta, out Vector2 parentBoneScale, out Vector2 totalScale) {
Vector2 skeletonDelta = boneLocalDelta;
totalScale = GetScaleAffectingRootMotion(out parentBoneScale);
skeletonDelta.Scale(totalScale);
Vector2 rootMotionTranslation = new Vector2(
rootMotionTranslateXPerY * skeletonDelta.y,
rootMotionTranslateYPerX * skeletonDelta.x);
skeletonDelta.x *= rootMotionScaleX;
skeletonDelta.y *= rootMotionScaleY;
skeletonDelta.x += rootMotionTranslation.x;
skeletonDelta.y += rootMotionTranslation.y;
if (!transformPositionX) skeletonDelta.x = 0f;
if (!transformPositionY) skeletonDelta.y = 0f;
return skeletonDelta;
}
float GetSkeletonSpaceRotationDelta (float boneLocalDelta, Vector2 totalScaleAffectingRootMotion) {
float rotationSign = totalScaleAffectingRootMotion.x * totalScaleAffectingRootMotion.y > 0 ? 1 : -1;
return rotationSign * boneLocalDelta;
}
void SetEffectiveBoneOffsetsTo (Vector2 displacementSkeletonSpace, float rotationSkeletonSpace, Vector2 parentBoneScale) {
ApplyTransformConstraints();
// Move top level bones in opposite direction of the root motion bone
Skeleton skeleton = animationComponent.Skeleton;
foreach (Bone topLevelBone in topLevelBones) {
if (topLevelBone == rootMotionBone) {
if (transformPositionX) topLevelBone.Pose.X = displacementSkeletonSpace.x / skeleton.ScaleX;
if (transformPositionY) topLevelBone.Pose.Y = displacementSkeletonSpace.y / skeleton.ScaleY;
if (transformRotation) {
float rotationSign = skeleton.ScaleX * skeleton.ScaleY > 0 ? 1 : -1;
topLevelBone.Pose.Rotation = rotationSign * rotationSkeletonSpace;
}
} else {
bool useAppliedTransform = transformConstraintIndices.Count > 0;
float rootMotionBoneX = useAppliedTransform ? rootMotionBone.AppliedPose.X : rootMotionBone.Pose.X;
float rootMotionBoneY = useAppliedTransform ? rootMotionBone.AppliedPose.Y : rootMotionBone.Pose.Y;
float offsetX = (initialOffset.x - rootMotionBoneX) * parentBoneScale.x;
float offsetY = (initialOffset.y - rootMotionBoneY) * parentBoneScale.y;
if (transformPositionX) topLevelBone.Pose.X = (displacementSkeletonSpace.x / skeleton.ScaleX) + offsetX;
if (transformPositionY) topLevelBone.Pose.Y = (displacementSkeletonSpace.y / skeleton.ScaleY) + offsetY;
if (transformRotation) {
float rootMotionBoneRotation = useAppliedTransform ? rootMotionBone.AppliedPose.Rotation : rootMotionBone.Pose.Rotation;
float parentBoneRotationSign = (parentBoneScale.x * parentBoneScale.y > 0 ? 1 : -1);
float offsetRotation = (initialOffsetRotation - rootMotionBoneRotation) * parentBoneRotationSign;
float skeletonRotationSign = skeleton.ScaleX * skeleton.ScaleY > 0 ? 1 : -1;
topLevelBone.Pose.Rotation = (rotationSkeletonSpace * skeletonRotationSign) + offsetRotation;
}
}
}
}
void ClearEffectiveBoneOffsets (Vector2 parentBoneScale) {
SetEffectiveBoneOffsetsTo(Vector2.zero, 0, parentBoneScale);
}
void ClearRigidbodyTempMovement () {
rigidbodyDisplacement = Vector2.zero;
tempSkeletonDisplacement = Vector2.zero;
rigidbodyLocalRotation = Quaternion.identity;
rigidbody2DRotation = 0;
tempSkeletonRotation = 0;
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: fc23a4f220b20024ab0592719f92587d
timeCreated: 1592849332
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,359 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2026, 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
#if UNITY_2017_1_OR_NEWER
#define BUILT_IN_SPRITE_MASK_COMPONENT
#endif
#if !SPINE_DISABLE_THREADING
#define USE_THREADED_ANIMATION_UPDATE
#endif
#if !SPINE_AUTO_UPGRADE_COMPONENTS_OFF
#define AUTO_UPGRADE_TO_43_COMPONENTS
#endif
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
namespace Spine.Unity {
#if NEW_PREFAB_SYSTEM
[ExecuteAlways]
#else
[ExecuteInEditMode]
#endif
[AddComponentMenu("Spine/SkeletonAnimation")]
[HelpURL("http://esotericsoftware.com/spine-unity#SkeletonAnimation-Component")]
public class SkeletonAnimation : SkeletonAnimationBase, IAnimationStateComponent, IUpgradable {
#region Serialized state and Beginner API
[FormerlySerializedAs("_animationName")] [SerializeField] [SpineAnimation] protected string animationName = "";
/// <summary>Whether or not <see cref="AnimationName"/> should loop. This only applies to the initial animation specified in the inspector, or any subsequent Animations played through .AnimationName. Animations set through state.SetAnimation are unaffected.</summary>
public bool loop;
/// <summary>
/// The rate at which animations progress over time. 1 means 100%. 0.5 means 50%.</summary>
/// <remarks>AnimationState and TrackEntry also have their own timeScale. These are combined multiplicatively.</remarks>
public float timeScale = 1;
/// <summary>If enabled, AnimationState time is advanced by Unscaled Game Time
/// (<c>Time.unscaledDeltaTime</c> instead of the default Game Time(<c>Time.deltaTime</c>).
/// to animate independent of game <c>Time.timeScale</c>.
/// Instance timeScale will still be applied.</summary>
public bool unscaledTime;
#endregion
public override void MainThreadBeforeUpdateInternal () {
base.MainThreadBeforeUpdateInternal();
#if USE_THREADED_ANIMATION_UPDATE
if (isUpdatedExternally) {
if (state != null) state.DelayListenerNotifications();
}
#endif
}
public override void MainThreadAfterUpdateInternal () {
base.MainThreadAfterUpdateInternal();
#if USE_THREADED_ANIMATION_UPDATE
if (isUpdatedExternally) {
if (state != null) state.IssueDelayedListenerNotifications();
}
#endif
}
protected Spine.AnimationState state;
/// <summary>
/// This is the Spine.AnimationState object of this SkeletonAnimation. You can control animations through it.
/// Note that this object, like .skeleton, is not guaranteed to exist in Awake. Do all accesses and caching to it in Start</summary>
public Spine.AnimationState AnimationState {
get {
Initialize(false);
return state;
}
set { state = value; }
}
public override bool IsValid {
get { return skeletonRenderer != null && skeletonRenderer.IsValid && state != null; }
}
public bool UnscaledTime { get { return unscaledTime; } set { unscaledTime = value; } }
#region Serialized state and Beginner API
/// <summary>
/// Setting this property sets the animation of the skeleton. If invalid, it will store the animation name for the next time the skeleton is properly initialized.
/// Getting this property gets the name of the currently playing animation. If invalid, it will return the last stored animation name set through this property.</summary>
public string AnimationName {
get {
if (!this.IsValid) {
return animationName;
} else {
TrackEntry entry = state.GetTrack(0);
return entry == null ? null : entry.Animation.Name;
}
}
set {
Initialize(false);
if (!IsValid) {
animationName = value;
return;
}
if (animationName == value) {
TrackEntry entry = state.GetTrack(0);
if (entry != null && entry.Loop == loop)
return;
}
animationName = value;
if (string.IsNullOrEmpty(value)) {
state.ClearTrack(0);
} else {
SkeletonData skeletonData = skeletonRenderer.SkeletonDataAsset.GetSkeletonData(false);
if (skeletonData == null)
return;
Spine.Animation animationObject = skeletonData.FindAnimation(value);
if (animationObject != null)
state.SetAnimation(0, animationObject, loop);
}
}
}
#endregion
/// <summary>
/// Clears the previously generated mesh, resets the skeleton's pose, and clears all previously active animations.</summary>
public override void ClearAnimationState () {
if (state != null) state.ClearTracks();
}
public override void InitializeAnimationComponent () {
base.InitializeAnimationComponent();
if (!skeletonRenderer.IsValid)
return;
AnimationStateData data = skeletonRenderer.SkeletonDataAsset.GetAnimationStateData();
#if UNITY_EDITOR
AnimationState oldAnimationState = state;
#endif
state = new Spine.AnimationState(data);
state.Dispose += OnAnimationDisposed;
if (state == null)
return;
#if UNITY_EDITOR
if (oldAnimationState != null)
state.AssignEventSubscribersFrom(oldAnimationState);
#endif
UpdateInitialAnimation();
}
protected virtual void OnAnimationDisposed (TrackEntry entry) {
// when updateMode disables applying animations, still ensure animations are mixed out
UpdateMode updateMode = skeletonRenderer.UpdateMode;
if (updateMode != UpdateMode.FullUpdate &&
updateMode != UpdateMode.EverythingExceptMesh) {
entry.Animation.Apply(skeleton, 0, 0, false, null, 0f, true, false, true, false);
}
}
public virtual void UpdateInitialAnimation () {
state.ClearTrack(0);
if (!string.IsNullOrEmpty(animationName)) {
SkeletonData skeletonData = skeletonRenderer.SkeletonDataAsset.GetSkeletonData(false);
if (skeletonData == null)
return;
Spine.Animation animation = skeletonData.FindAnimation(animationName);
if (animation != null) {
state.SetAnimation(0, animation, loop);
#if UNITY_EDITOR
if (!ApplicationIsPlaying)
Update(0f);
#endif
}
}
}
#if USE_THREADED_ANIMATION_UPDATE
public override float UsedExternalDeltaTime {
get {
return unscaledTime ? ExternalUnscaledDeltaTime : ExternalDeltaTime;
}
}
#endif
protected override float DeltaTime {
get {
return unscaledTime ? Time.unscaledDeltaTime : Time.deltaTime;
}
}
protected override void UpdateAnimationStatus (float deltaTime) {
deltaTime *= timeScale;
if (state != null) {
state.Update(deltaTime);
skeleton.Update(deltaTime);
#if UNITY_EDITOR
if (ApplicationIsPlaying)
UpdatePropertyToCurrentAnimationEditor();
#endif
if (skeletonRenderer.UpdateMode == UpdateMode.OnlyAnimationStatus) {
state.ApplyEventTimelinesOnly(skeleton, issueEvents: false);
}
}
}
protected override void ApplyStateToSkeleton (bool calledFromMainThread) {
if (skeletonRenderer.UpdateMode != UpdateMode.OnlyEventTimelines)
state.Apply(skeletonRenderer.Skeleton);
else
state.ApplyEventTimelinesOnly(skeletonRenderer.Skeleton, issueEvents: true);
}
#if UNITY_EDITOR
protected void UpdatePropertyToCurrentAnimationEditor () {
if (state.Tracks.Count == 0 || state.Tracks.Items[0] == null)
return;
Animation currentAnimation = state.Tracks.Items[0].Animation;
animationName = currentAnimation == null ? "<None>" : currentAnimation.Name;
}
#endif
#region Runtime Instantiation
/// <summary>Adds and prepares SkeletonAnimation and SkeletonRenderer components to a GameObject at runtime.</summary>
/// <returns>A struct referencing the newly instantiated SkeletonAnimation and SkeletonRenderer components.</returns>
public static SkeletonComponents<SkeletonRenderer, SkeletonAnimation> AddToGameObject (
GameObject gameObject, SkeletonDataAsset skeletonDataAsset, bool quiet = false) {
return Spine.Unity.SkeletonRenderer.AddSpineComponents<SkeletonRenderer, SkeletonAnimation>(
gameObject, skeletonDataAsset, quiet);
}
/// <summary>Instantiates a new UnityEngine.GameObject and adds SkeletonAnimation and SkeletonRenderer components to it.</summary>
/// <returns>A struct referencing the newly instantiated SkeletonAnimation and SkeletonRenderer components.</returns>
public static SkeletonComponents<SkeletonRenderer, SkeletonAnimation> NewSkeletonAnimationGameObject (SkeletonDataAsset skeletonDataAsset,
bool quiet = false) {
return Spine.Unity.SkeletonRenderer.NewSpineGameObject<SkeletonRenderer, SkeletonAnimation>(
skeletonDataAsset, quiet);
}
#endregion
#region Transfer of Deprecated Fields
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
// compatibility layer between 4.1 and 4.2, automatically transfer serialized attributes.
public override void UpgradeTo43 () {
if (!Application.isPlaying && !wasDeprecatedTransferred) {
UpgradeTo43Components();
TransferDeprecatedFields();
InitializeAnimationComponent();
}
}
protected void UpgradeTo43Components () {
if (gameObject.GetComponent<SkeletonRenderer>() == null &&
gameObject.GetComponent<SkeletonGraphic>() == null) {
gameObject.AddComponent<SkeletonRenderer>();
EditorBridge.RequestMarkDirty(gameObject);
Debug.Log(string.Format("{0}: Auto-migrated old SkeletonAnimation component to split SkeletonAnimation + SkeletonRenderer components.",
gameObject.name), gameObject);
}
}
/// <summary>Transfer of former base class SkeletonRenderer parameters.</summary>
protected void TransferDeprecatedFields () {
wasDeprecatedTransferred = true;
SkeletonRenderer skeletonRenderer = gameObject.GetComponent<SkeletonRenderer>();
if (skeletonRenderer == null)
return;
skeletonRenderer.skeletonDataAsset = this.skeletonDataAssetDeprecated;
skeletonRenderer.initialSkinName = this.initialSkinNameDeprecated;
skeletonRenderer.EditorSkipSkinSync = this.editorSkipSkinSyncDeprecated;
skeletonRenderer.initialFlipX = this.initialFlipXDeprecated;
skeletonRenderer.initialFlipY = this.initialFlipYDeprecated;
skeletonRenderer.UpdateMode = this.updateModeDeprecated;
skeletonRenderer.updateWhenInvisible = this.updateWhenInvisibleDeprecated;
skeletonRenderer.separatorSlotNames = this.separatorSlotNamesDeprecated;
skeletonRenderer.MeshSettings.zSpacing = this.zSpacingDeprecated;
skeletonRenderer.MeshSettings.useClipping = this.useClippingDeprecated;
skeletonRenderer.MeshSettings.immutableTriangles = this.immutableTrianglesDeprecated;
skeletonRenderer.MeshSettings.pmaVertexColors = this.pmaVertexColorsDeprecated;
skeletonRenderer.MeshSettings.tintBlack = this.tintBlackDeprecated;
skeletonRenderer.MeshSettings.addNormals = this.addNormalsDeprecated;
skeletonRenderer.MeshSettings.calculateTangents = this.calculateTangentsDeprecated;
skeletonRenderer.clearStateOnDisable = this.clearStateOnDisableDeprecated;
skeletonRenderer.singleSubmesh = this.singleSubmeshDeprecated;
skeletonRenderer.MaskInteraction = this.maskInteractionDeprecated;
}
[SerializeField] protected bool wasDeprecatedTransferred = false;
// SkeletonRenderer former base class parameters
[FormerlySerializedAs("skeletonDataAsset")] [SerializeField] private SkeletonDataAsset skeletonDataAssetDeprecated;
[FormerlySerializedAs("initialSkinName")] [SpineSkin(defaultAsEmptyString: true)] [SerializeField] private string initialSkinNameDeprecated;
[FormerlySerializedAs("editorSkipSkinSync")] [SerializeField] private bool editorSkipSkinSyncDeprecated = false;
[FormerlySerializedAs("initialFlipX")] [SerializeField] private bool initialFlipXDeprecated = false;
[FormerlySerializedAs("initialFlipY")] [SerializeField] private bool initialFlipYDeprecated = false;
[FormerlySerializedAs("updateMode")] [SerializeField] private UpdateMode updateModeDeprecated = UpdateMode.FullUpdate;
[FormerlySerializedAs("updateWhenInvisible")] [SerializeField] private UpdateMode updateWhenInvisibleDeprecated = UpdateMode.FullUpdate;
[UnityEngine.Serialization.FormerlySerializedAs("submeshSeparators"),
UnityEngine.Serialization.FormerlySerializedAs("separatorSlotNames")]
[SerializeField] private string[] separatorSlotNamesDeprecated = new string[0];
[FormerlySerializedAs("zSpacing")] [SerializeField] private float zSpacingDeprecated = 0f;
[FormerlySerializedAs("useClipping")] [SerializeField] private bool useClippingDeprecated = true;
[FormerlySerializedAs("immutableTriangles")] [SerializeField] private bool immutableTrianglesDeprecated = false;
[FormerlySerializedAs("pmaVertexColors")] [SerializeField] private bool pmaVertexColorsDeprecated = true;
[FormerlySerializedAs("clearStateOnDisable")] [SerializeField] private bool clearStateOnDisableDeprecated = false;
[FormerlySerializedAs("tintBlack")] [SerializeField] private bool tintBlackDeprecated = false;
[FormerlySerializedAs("singleSubmesh")] [SerializeField] private bool singleSubmeshDeprecated = false;
[FormerlySerializedAs("calculateNormals"),
FormerlySerializedAs("addNormals")]
[SerializeField] private bool addNormalsDeprecated = false;
[FormerlySerializedAs("calculateTangents")] [SerializeField] private bool calculateTangentsDeprecated = false;
#if BUILT_IN_SPRITE_MASK_COMPONENT
[FormerlySerializedAs("maskInteraction")] [SerializeField] private SpriteMaskInteraction maskInteractionDeprecated = SpriteMaskInteraction.None;
#endif // BUILT_IN_SPRITE_MASK_COMPONENT
#endif // UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
#endregion
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: d247ba06193faa74d9335f5481b2b56c
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,755 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2026, 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
#if UNITY_2018_1_OR_NEWER
#define PER_MATERIAL_PROPERTY_BLOCKS
#endif
#if UNITY_2019_3_OR_NEWER
#define CONFIGURABLE_ENTER_PLAY_MODE
#endif
#if !SPINE_DISABLE_THREADING
#define USE_THREADED_SKELETON_UPDATE
#endif
#if !SPINE_AUTO_UPGRADE_COMPONENTS_OFF
#define AUTO_UPGRADE_TO_43_COMPONENTS
#endif
#define SPINE_OPTIONAL_ON_DEMAND_LOADING
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
using UnityEngine.UI;
#if UNITY_EDITOR
using UnityEditor.SceneManagement;
#endif
namespace Spine.Unity {
// Partial class: covers common identical attributes, properties and methods shared across all
// ISkeletonRenderer subclasses. This is a workaround for single inheritance limitations and covers code which
// would otherwise be located in a base-class of SkeletonGraphic.
public partial class SkeletonGraphic : MaskableGraphic, ISkeletonRenderer {
// Identical code shared by ISkeletonRenderer subclasses as a workaround for single inheritance limitations.
#region Identical common ISkeletonRenderer code
#region ISkeletonRenderer Attributes
// Core Attributes
public ISkeletonAnimation skeletonAnimation;
public SkeletonDataAsset skeletonDataAsset;
[System.NonSerialized] public Skeleton skeleton;
[System.NonSerialized] public bool valid;
protected bool wasMeshUpdatedAfterInit = false;
protected bool updateTriangles = true;
// Initialization Settings
/// <summary>Skin name to use when the Skeleton is initialized.</summary>
[SpineSkin(dataField: "skeletonDataAsset", defaultAsEmptyString: true)] public string initialSkinName;
/// <summary>Flip X and Y to use when the Skeleton is initialized.</summary>
public bool initialFlipX, initialFlipY;
// Render Settings
[SerializeField] protected MeshGenerator.Settings meshSettings = MeshGenerator.Settings.Default;
[System.NonSerialized] protected readonly SkeletonRendererInstruction currentInstructions = new SkeletonRendererInstruction();
/// <summary>Update mode to optionally limit updates to e.g. only apply animations but not update the mesh.</summary>
protected UpdateMode updateMode = UpdateMode.FullUpdate;
/// <summary>Update mode used when the MeshRenderer becomes invisible
/// (when <c>OnBecameInvisible()</c> is called). Update mode is automatically
/// reset to <c>UpdateMode.FullUpdate</c> when the mesh becomes visible again.</summary>
public UpdateMode updateWhenInvisible = UpdateMode.FullUpdate;
/// <summary>Clears the state of the render and skeleton when this component or its GameObject is disabled. This prevents previous state from being retained when it is enabled again. When pooling your skeleton, setting this to true can be helpful.</summary>
public bool clearStateOnDisable = false;
// Submesh Separation
/// <summary>Slot names used to populate separatorSlots list when the Skeleton is initialized. Changing this after initialization does nothing.</summary>
[SpineSlot] public string[] separatorSlotNames = new string[0];
/// <summary>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.</summary>
[System.NonSerialized] public readonly List<Slot> separatorSlots = new List<Slot>();
public bool enableSeparatorSlots = false;
// Overrides Attributes
// These are API for anything that wants to take over rendering for a SkeletonRenderer.
public bool disableRenderingOnOverride = true;
event InstructionDelegate generateMeshOverride;
[System.NonSerialized] readonly Dictionary<Slot, Material> customSlotMaterials = new Dictionary<Slot, Material>();
[System.NonSerialized] protected bool materialsNeedUpdate = false;
// Physics Attributes
/// <seealso cref="PhysicsPositionInheritanceFactor"/>
[SerializeField] protected Vector2 physicsPositionInheritanceFactor = Vector2.one;
/// <seealso cref="PhysicsRotationInheritanceFactor"/>
[SerializeField] protected float physicsRotationInheritanceFactor = 1.0f;
/// <seealso cref="PhysicsPositionInheritanceLimit"/>
[SerializeField] protected Vector2 physicsPositionInheritanceLimit = Vector2.positiveInfinity;
/// <seealso cref="PhysicsRotationInheritanceLimit"/>
[SerializeField] protected float physicsRotationInheritanceLimit = float.MaxValue;
/// <summary>Reference transform relative to which physics movement will be calculated, or null to use world location.</summary>
[SerializeField] protected Transform physicsMovementRelativeTo = null;
/// <summary>Used for applying Transform translation to skeleton PhysicsConstraints.</summary>
protected Vector3 lastPosition;
/// <summary>Used for applying Transform rotation to skeleton PhysicsConstraints.</summary>
protected float lastRotation;
/// <summary>Position delta for threaded processing as Transform access from worker thread is not allowed.</summary>
protected Vector3 positionDelta;
/// <summary>Rotation delta for threaded processing as Transform access from worker thread is not allowed.</summary>
protected float rotationDelta;
// Threaded Update System Attributes
#if USE_THREADED_SKELETON_UPDATE
protected static int mainThreadID = -1;
[SerializeField] protected SettingsTriState threadedMeshGeneration = SettingsTriState.UseGlobalSetting;
protected bool isUpdatedExternally = false;
protected bool requiresMeshBufferAssignmentMainThread = false;
#if UNITY_EDITOR
static bool applicationIsPlaying = false;
#endif
#endif // USE_THREADED_SKELETON_UPDATE
#if UNITY_EDITOR
/// <summary>Enable this parameter when overwriting the Skeleton's skin from an editor script.
/// Otherwise any changes will be overwritten by the next inspector update.</summary>
protected bool editorSkipSkinSync = false;
protected bool requiresEditorUpdate = false;
#endif
#endregion ISkeletonRenderer Attributes
#region ISkeletonRenderer Properties
public ISkeletonRenderer Renderer { get { return this; } }
public UnityEngine.MonoBehaviour Component { get { return this; } }
public ISkeletonAnimation Animation { get { return skeletonAnimation; } set { skeletonAnimation = value; } }
public SkeletonDataAsset SkeletonDataAsset {
get { return skeletonDataAsset; }
set { skeletonDataAsset = value; }
}
public Skeleton Skeleton {
get {
Initialize(false);
return skeleton;
}
set {
skeleton = value;
}
}
public SkeletonData SkeletonData {
get {
Initialize(false);
return skeleton == null ? null : skeleton.Data;
}
}
public bool IsValid { get { return valid; } }
public string InitialSkinName { get { return initialSkinName; } set { initialSkinName = value; } }
/// <summary>Flip X to use when the Skeleton is initialized.</summary>
public bool InitialFlipX { get { return initialFlipX; } set { initialFlipX = value; } }
/// <summary>Flip Y to use when the Skeleton is initialized.</summary>
public bool InitialFlipY { get { return initialFlipY; } set { initialFlipY = value; } }
/// <summary>Update mode to optionally limit updates to e.g. only apply animations but not update the mesh.</summary>
public UpdateMode UpdateMode { get { return updateMode; } set { updateMode = value; } }
/// <summary>Update mode used when the MeshRenderer becomes invisible
/// (when <c>OnBecameInvisible()</c> is called). Update mode is automatically
/// reset to <c>UpdateMode.FullUpdate</c> when the mesh becomes visible again.</summary>
public UpdateMode UpdateWhenInvisible { get { return updateWhenInvisible; } set { updateWhenInvisible = value; } }
public MeshGenerator.Settings MeshSettings { get { return meshSettings; } set { meshSettings = value; } }
// Submesh Separation
/// <summary>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.</summary>
public List<Slot> SeparatorSlots { get { return separatorSlots; } }
public bool EnableSeparatorSlots { get { return enableSeparatorSlots; } set { enableSeparatorSlots = value; } }
// Overrides Properties
public bool HasGenerateMeshOverride { get { return generateMeshOverride != null; } }
/// <summary>Allows separate code to take over rendering for this SkeletonRenderer component. The subscriber is passed a SkeletonRendererInstruction argument to determine how to render a skeleton.</summary>
public event InstructionDelegate GenerateMeshOverride {
add {
generateMeshOverride += value;
if (disableRenderingOnOverride && generateMeshOverride != null) {
Initialize(false);
DisableRenderers();
updateMode = UpdateMode.FullUpdate;
}
}
remove {
generateMeshOverride -= value;
if (disableRenderingOnOverride && generateMeshOverride == null) {
Initialize(false);
EnableRenderers();
}
}
}
/// <summary>Use this Dictionary to use a different Material to render specific Slots.</summary>
public Dictionary<Slot, Material> CustomSlotMaterials {
get { materialsNeedUpdate = true; return customSlotMaterials; }
}
// Physics Properties
/// <summary>When set to non-zero, Transform position movement in X and Y direction
/// is applied to skeleton PhysicsConstraints, multiplied by this scale factor.
/// Typical values are <c>Vector2.one</c> to apply XY movement 1:1,
/// <c>Vector2(2f, 2f)</c> to apply movement with double intensity,
/// <c>Vector2(1f, 0f)</c> to apply only horizontal movement, or
/// <c>Vector2.zero</c> to not apply any Transform position movement at all.</summary>
public Vector2 PhysicsPositionInheritanceFactor {
get {
return physicsPositionInheritanceFactor;
}
set {
if (physicsPositionInheritanceFactor == Vector2.zero && value != Vector2.zero) ResetLastPosition();
physicsPositionInheritanceFactor = value;
}
}
/// <summary>When set to non-zero, Transform rotation movement is applied to skeleton PhysicsConstraints,
/// multiplied by this scale factor. Typical values are <c>1</c> to apply movement 1:1,
/// <c>2</c> to apply movement with double intensity, or
/// <c>0</c> to not apply any Transform rotation movement at all.</summary>
public float PhysicsRotationInheritanceFactor {
get {
return physicsRotationInheritanceFactor;
}
set {
if (physicsRotationInheritanceFactor == 0f && value != 0f) ResetLastRotation();
physicsRotationInheritanceFactor = value;
}
}
/// <summary>
/// Limits Transform position movement in X and Y direction that is applied to skeleton PhysicsConstraints,
/// after it has been multiplied by <see cref="PhysicsPositionInheritanceFactor"/>.
/// </summary>
public Vector2 PhysicsPositionInheritanceLimit {
get { return physicsPositionInheritanceLimit; }
set { physicsPositionInheritanceLimit = value; }
}
/// <summary>
/// Limits Transform rotation movement that is applied to skeleton PhysicsConstraints,
/// after it has been multiplied by <see cref="PhysicsRotationInheritanceFactor"/>.
/// </summary>
public float PhysicsRotationInheritanceLimit {
get { return physicsRotationInheritanceLimit; }
set { physicsRotationInheritanceLimit = value; }
}
/// <summary>Reference transform relative to which physics movement will be calculated, or null to use world location.</summary>
public Transform PhysicsMovementRelativeTo {
get {
return physicsMovementRelativeTo;
}
set {
physicsMovementRelativeTo = value;
if (physicsPositionInheritanceFactor != Vector2.zero) ResetLastPosition();
if (physicsRotationInheritanceFactor != 0f) ResetLastRotation();
}
}
// Threaded Update System Properties
#if USE_THREADED_SKELETON_UPDATE
public bool IsUpdatedExternally {
get { return isUpdatedExternally; }
set { isUpdatedExternally = value; }
}
protected bool UsesThreadedMeshGeneration {
get { return threadedMeshGeneration == SettingsTriState.Enable || RuntimeSettings.UseThreadedMeshGeneration; }
}
public bool RequiresMeshBufferAssignmentMainThread {
get { return requiresMeshBufferAssignmentMainThread; }
}
public SettingsTriState ThreadedMeshGeneration {
get { return threadedMeshGeneration; }
set {
if (threadedMeshGeneration == value) return;
threadedMeshGeneration = value;
SkeletonUpdateSystem system = SkeletonUpdateSystem.Instance;
if (system) {
if (threadedMeshGeneration == SettingsTriState.Enable || RuntimeSettings.UseThreadedMeshGeneration)
system.RegisterForUpdate(this);
else
system.UnregisterFromUpdate(this);
}
}
}
#if UNITY_EDITOR
// ApplicationIsPlaying for threaded access. Unfortunately Application.isPlaying throws
// when called from worker thread.
public static bool ApplicationIsPlaying {
get { return applicationIsPlaying; }
set { applicationIsPlaying = value; }
}
#endif
#else // USE_THREADED_SKELETON_UPDATE
public bool IsUpdatedExternally {
get { return false; }
set { }
}
#if UNITY_EDITOR
public static bool ApplicationIsPlaying {
get { return Application.isPlaying; }
set { }
}
#endif
public bool RequiresMeshBufferAssignmentMainThread { get { return true; } }
#endif // USE_THREADED_SKELETON_UPDATE
#if UNITY_EDITOR
/// <summary>Enable this parameter when overwriting the Skeleton's skin from an editor script.
/// Otherwise any changes will be overwritten by the next inspector update.</summary>
public bool EditorSkipSkinSync {
get { return editorSkipSkinSync; }
set { editorSkipSkinSync = value; }
}
#endif
#endregion ISkeletonRenderer Properties
#region ISkeletonRenderer Methods
protected virtual void ClearCommon () {
// Note: do not reset meshFilter.sharedMesh or meshRenderer.sharedMaterial to null,
// otherwise constant reloading will be triggered at prefabs.
currentInstructions.Clear();
rendererBuffers.Clear();
ClearMeshGenerator();
skeleton = null;
valid = false;
if (skeletonAnimation != null)
skeletonAnimation.ClearAnimationState();
}
protected virtual void InitializeCommon (bool overwrite, bool quiet = false) {
if (valid && !overwrite)
return;
if (skeletonDataAsset == null || skeletonDataAsset.skeletonJSON == null) {
Clear();
return;
} else if (skeleton != null && skeletonDataAsset.GetSkeletonData(true) != skeleton.Data) {
Clear();
} else if (this.Freeze) {
return;
} else {
ClearCommon();
}
SkeletonData skeletonData = skeletonDataAsset.GetSkeletonData(quiet);
if (skeletonData == null) return;
rendererBuffers.Initialize();
skeleton = new Skeleton(skeletonData) {
ScaleX = initialFlipX ? -1 : 1,
ScaleY = initialFlipY ? -1 : 1
};
valid = true;
ResetLastPositionAndRotation();
AssignInitialSkin();
separatorSlots.Clear();
for (int i = 0; i < separatorSlotNames.Length; i++)
separatorSlots.Add(skeleton.FindSlot(separatorSlotNames[i]));
AfterAnimationApplied();
wasMeshUpdatedAfterInit = false;
if (OnRebuild != null)
OnRebuild(this);
#if UNITY_EDITOR
if (!Application.isPlaying) {
string errorMessage = null;
if (!quiet && MaterialChecks.IsMaterialSetupProblematic(this, ref errorMessage))
Debug.LogWarningFormat(this, "Problematic material setup at {0}: {1}", this.name, errorMessage);
}
#endif
}
protected virtual void AssignInitialSkin () {
if (string.IsNullOrEmpty(initialSkinName) || string.Equals(initialSkinName, "default", System.StringComparison.Ordinal))
skeleton.SetSkin((Skin)null);
else
skeleton.SetSkin(initialSkinName);
}
public void ResetLastPosition () {
lastPosition = GetPhysicsTransformPosition();
}
public void ResetLastRotation () {
lastRotation = GetPhysicsTransformRotation();
}
public void ResetLastPositionAndRotation () {
lastPosition = GetPhysicsTransformPosition();
lastRotation = GetPhysicsTransformRotation();
}
/// <summary>
/// Gathers Transform movement for later application in <see cref="ApplyTransformMovementToPhysics"/>.
/// Must be called in main thread.
/// </summary>
public virtual void GatherTransformMovementForPhysics () {
#if UNITY_EDITOR
bool isPlaying = ApplicationIsPlaying;
#else
bool isPlaying = true;
#endif
if (isPlaying) {
if (physicsPositionInheritanceFactor != Vector2.zero) {
Vector3 position = GetPhysicsTransformPosition();
positionDelta = (position - lastPosition) / this.MeshScale;
positionDelta = transform.InverseTransformVector(positionDelta);
if (physicsMovementRelativeTo != null) {
positionDelta = physicsMovementRelativeTo.TransformVector(positionDelta);
}
positionDelta.x *= physicsPositionInheritanceFactor.x;
positionDelta.y *= physicsPositionInheritanceFactor.y;
positionDelta.x = Mathf.Clamp(positionDelta.x, -physicsPositionInheritanceLimit.x, physicsPositionInheritanceLimit.x);
positionDelta.y = Mathf.Clamp(positionDelta.y, -physicsPositionInheritanceLimit.y, physicsPositionInheritanceLimit.y);
lastPosition = position;
}
if (physicsRotationInheritanceFactor != 0f) {
float rotation = GetPhysicsTransformRotation();
rotationDelta = rotation - lastRotation;
if (rotationDelta > 180f) rotationDelta -= 360f;
else if (rotationDelta < -180f) rotationDelta += 360f;
rotationDelta = Mathf.Clamp(rotationDelta, -physicsRotationInheritanceLimit, physicsRotationInheritanceLimit);
lastRotation = rotation;
}
}
}
/// <summary>
/// Applies position and rotation Transform movement previously gathered via
/// <see cref="GatherTransformMovementForPhysics"/>. May be called in worker thread.
/// </summary>
public virtual void ApplyTransformMovementToPhysics () {
if (physicsPositionInheritanceFactor != Vector2.zero) {
skeleton.PhysicsTranslate(positionDelta.x, positionDelta.y);
}
if (physicsRotationInheritanceFactor != 0f) {
skeleton.PhysicsRotate(0, 0, physicsRotationInheritanceFactor * rotationDelta);
}
}
protected Vector3 GetPhysicsTransformPosition () {
if (physicsMovementRelativeTo == null) {
return transform.position;
} else {
if (physicsMovementRelativeTo == transform.parent)
return transform.localPosition;
else
return physicsMovementRelativeTo.InverseTransformPoint(transform.position);
}
}
protected float GetPhysicsTransformRotation () {
if (physicsMovementRelativeTo == null) {
return this.transform.rotation.eulerAngles.z;
} else {
if (physicsMovementRelativeTo == this.transform.parent)
return this.transform.localRotation.eulerAngles.z;
else {
Quaternion relative = Quaternion.Inverse(physicsMovementRelativeTo.rotation) * this.transform.rotation;
return relative.eulerAngles.z;
}
}
}
public virtual void AfterAnimationApplied (bool calledFromMainThread = true) {
if (_UpdateLocal != null)
_UpdateLocal(this);
if (_UpdateWorld == null) {
UpdateWorldTransform(Physics.Update);
} else {
UpdateWorldTransform(Physics.Pose);
_UpdateWorld(this);
UpdateWorldTransform(Physics.Update);
}
if (calledFromMainThread && _UpdateComplete != null) {
_UpdateComplete(this);
}
}
public CoroutineIterator AfterAnimationAppliedSplit (CoroutineIterator coroutineIterator) {
if (coroutineIterator.IsDone)
return CoroutineIterator.Done;
/*
0:
if (_UpdateLocal != null) {
yield return true; // continue in main thread
1:
_UpdateLocal(this);
yield return false; // continue in worker thread
}
2:
if (_UpdateWorld == null) {
UpdateWorldTransform(Physics.Update);
// goto 5
} else {
UpdateWorldTransform(Physics.Pose);
yield return true; // continue in main thread
3:
_UpdateWorld(this);
yield return false; // continue in worker thread
4:
UpdateWorldTransform(Physics.Update);
// goto 5
}
5:
if (_UpdateComplete != null) {
yield return true; // continue in main thread
6:
_UpdateComplete(this);
// last call, no need to switch back to worker thread.
}
*/
const int StateBits = 3;
const uint StateMask = (1 << StateBits) - 1;
switch (coroutineIterator.State(StateMask)) {
case 0:
if (_UpdateLocal != null) {
AssertIsWorkerThread();
return coroutineIterator.YieldReturnAtState(1, StateMask);
} else {
goto case 2;
}
case 1:
AssertIsMainThread();
_UpdateLocal(this);
return coroutineIterator.YieldReturnAtState(2, StateMask);
case 2:
if (_UpdateWorld == null) {
AssertIsWorkerThread();
UpdateWorldTransform(Physics.Update);
goto case 5;
} else {
AssertIsWorkerThread();
UpdateWorldTransform(Physics.Pose);
return coroutineIterator.YieldReturnAtState(3, StateMask);
}
case 3:
AssertIsMainThread();
_UpdateWorld(this);
return coroutineIterator.YieldReturnAtState(4, StateMask);
case 4:
AssertIsWorkerThread();
UpdateWorldTransform(Physics.Update);
goto case 5;
case 5:
if (_UpdateComplete != null) {
AssertIsWorkerThread();
return coroutineIterator.YieldReturnAtState(6, StateMask);
} else {
return CoroutineIterator.Done;
}
case 6:
AssertIsMainThread();
_UpdateComplete(this);
return CoroutineIterator.Done;
default:
Debug.LogError(string.Format(
"Internal coroutine logic error: SkeletonRenderer.AfterAnimationAppliedSplit state was {0}.",
coroutineIterator.State(StateMask)), this);
return CoroutineIterator.Done;
}
}
protected void IssueOnPostProcessVertices (MeshGeneratorBuffers buffers) {
if (OnPostProcessVertices != null)
OnPostProcessVertices.Invoke(buffers);
}
protected virtual void UpdateWorldTransform (Physics physics) {
skeleton.UpdateWorldTransform(physics);
}
public virtual void UpdateMesh (bool calledFromMainThread = true) {
#if USE_THREADED_SKELETON_UPDATE
bool canPrepareInstructions = calledFromMainThread || !NeedsMainThreadRendererPreparation;
#else
bool canPrepareInstructions = true;
#endif
if (canPrepareInstructions)
PrepareInstructionsAndRenderers();
updateTriangles = UpdateBuffersToInstructions(calledFromMainThread);
#if USE_THREADED_SKELETON_UPDATE
if (calledFromMainThread) {
requiresMeshBufferAssignmentMainThread = false;
UpdateMeshAndMaterialsToBuffers();
} else {
requiresMeshBufferAssignmentMainThread = true;
}
#else
UpdateMeshAndMaterialsToBuffers();
#endif
}
/// <returns>True if triangles (indices array) need to be updated.</returns>
public virtual bool UpdateBuffersToInstructions (bool calledFromMainThread = true) {
if (!valid || currentInstructions.rawVertexCount < 0) return false;
wasMeshUpdatedAfterInit = true;
if (this.generateMeshOverride != null) {
if (calledFromMainThread)
this.generateMeshOverride(currentInstructions);
if (disableRenderingOnOverride) return false;
}
ExposedList<SubmeshInstruction> workingSubmeshInstructions = currentInstructions.submeshInstructions;
MeshRendererBuffers.SmartMesh currentSmartMesh = rendererBuffers.GetNextMesh(); // Double-buffer for performance.
// Update vertex buffers based on vertices from the attachments and assign buffers to a target UnityEngine.Mesh.
bool updateTriangles = SkeletonRendererInstruction.GeometryNotEqual(currentInstructions, currentSmartMesh.instructionUsed, calledFromMainThread);
FillBuffersFromSubmeshInstructions(workingSubmeshInstructions, currentSmartMesh, updateTriangles);
return updateTriangles;
}
public virtual void UpdateMeshAndMaterialsToBuffers () {
ExposedList<SubmeshInstruction> workingSubmeshInstructions = currentInstructions.submeshInstructions;
MeshRendererBuffers.SmartMesh currentSmartMesh = rendererBuffers.GetCurrentMesh();
UpdateMeshAndMaterialsToBuffers(workingSubmeshInstructions, currentSmartMesh, updateTriangles);
}
protected virtual void UpdateMeshAndMaterialsToBuffers (
ExposedList<SubmeshInstruction> workingSubmeshInstructions, MeshRendererBuffers.SmartMesh currentSmartMesh,
bool updateTriangles) {
FillMeshFromBuffers(currentSmartMesh, updateTriangles);
bool materialsChanged;
rendererBuffers.GatherMaterialsFromInstructions(workingSubmeshInstructions, out materialsChanged);
if (materialsChanged || materialsNeedUpdate) {
UpdateUsedMaterialsForRenderers(workingSubmeshInstructions);
}
#if SPINE_OPTIONAL_ON_DEMAND_LOADING
if (Application.isPlaying)
HandleOnDemandLoading();
#endif
// The UnityEngine.Mesh is ready. Set it as the MeshFilter's mesh. Store the instructions used for that mesh.
AssignMeshAtRenderer(workingSubmeshInstructions, currentSmartMesh);
if (OnMeshAndMaterialsUpdated != null)
OnMeshAndMaterialsUpdated(this);
}
public virtual void UpdateMaterials () {
UpdateUsedMaterialsForRenderers(currentInstructions.submeshInstructions);
}
// Threading Asserts
[System.Diagnostics.Conditional("UNITY_EDITOR")]
protected void InitializeMainThreadID () {
#if USE_THREADED_SKELETON_UPDATE
if (mainThreadID == -1)
mainThreadID = System.Threading.Thread.CurrentThread.ManagedThreadId;
#endif
}
[System.Diagnostics.Conditional("UNITY_EDITOR")]
private void AssertIsMainThread () {
#if USE_THREADED_SKELETON_UPDATE
if (System.Threading.Thread.CurrentThread.ManagedThreadId != mainThreadID)
Debug.LogError("AssertIsMainThread failed: worker thread calling main thread code. Thread ID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
#endif
}
[System.Diagnostics.Conditional("UNITY_EDITOR")]
private void AssertIsWorkerThread () {
#if USE_THREADED_SKELETON_UPDATE
if (System.Threading.Thread.CurrentThread.ManagedThreadId == mainThreadID)
Debug.LogError("AssertIsWorkerThread failed: main thread calling worker thread code! Thread ID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
#endif
}
#endregion ISkeletonRenderer Methods
#region ISkeletonRenderer Events
protected event SkeletonRendererDelegate _UpdateLocal;
protected event SkeletonRendererDelegate _UpdateWorld;
protected event SkeletonRendererDelegate _UpdateComplete;
/// <summary>
/// Occurs after the animations are applied and before world space values are resolved.
/// Use this callback when you want to set bone local values.
/// </summary>
public event SkeletonRendererDelegate UpdateLocal { add { _UpdateLocal += value; } remove { _UpdateLocal -= value; } }
/// <summary>
/// Occurs after the Skeleton's bone world space values are resolved (including all constraints).
/// Using this callback will cause the world space values to be solved an extra time.
/// Use this callback if want to use bone world space values, and also set bone local values.</summary>
public event SkeletonRendererDelegate UpdateWorld { add { _UpdateWorld += value; } remove { _UpdateWorld -= value; } }
/// <summary>
/// Occurs after the Skeleton's bone world space values are resolved (including all constraints).
/// Use this callback if you want to use bone world space values, but don't intend to modify bone local values.
/// This callback can also be used when setting world position and the bone matrix.</summary>
public event SkeletonRendererDelegate UpdateComplete { add { _UpdateComplete += value; } remove { _UpdateComplete -= value; } }
/// <summary> Occurs after the vertex data is populated every frame, before the vertices are pushed into the mesh.</summary>
public event Spine.Unity.MeshGeneratorDelegate OnPostProcessVertices;
/// <summary>OnRebuild is raised after the Skeleton is successfully initialized.</summary>
public event SkeletonRendererDelegate OnRebuild;
/// <summary>OnInstructionsPrepared is raised at the end of <c>LateUpdate</c> after render instructions
/// are done, target renderers are prepared, and the mesh is ready to be generated.</summary>
public event InstructionDelegate OnInstructionsPrepared;
#endregion ISkeletonRenderer Events
#endregion Identical common ISkeletonRenderer code
// End of identical code shared by ISkeletonRenderer subclasses as a workaround for single inheritance limitations.
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 903c50a86a432684aa719e571dfd38ae
timeCreated: 1754679077
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: d85b887af7e6c3f45a2e2d2920d641bc
timeCreated: 1455576193
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences:
- m_Material: {fileID: 2100000, guid: b66cf7a186d13054989b33a5c90044e4, type: 2}
- skeletonDataAsset: {instanceID: 0}
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,869 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2026, 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 !SPINE_AUTO_UPGRADE_COMPONENTS_OFF
#define AUTO_UPGRADE_TO_43_COMPONENTS
#endif
#if UNITY_2017_1_OR_NEWER
#define BUILT_IN_SPRITE_MASK_COMPONENT
#endif
#if UNITY_6000_3_OR_NEWER
#define USES_ENTITY_ID
#endif
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
#if UNITY_EDITOR
using UnityEditor.Animations;
#endif
namespace Spine.Unity {
#if NEW_PREFAB_SYSTEM
[ExecuteAlways]
#else
[ExecuteInEditMode]
#endif
[RequireComponent(typeof(Animator))]
[HelpURL("https://esotericsoftware.com/spine-unity-main-components#SkeletonMecanim-Component")]
public class SkeletonMecanim : SkeletonAnimationBase, IUpgradable {
public SkeletonMecanim.MecanimTranslator translator;
public MecanimTranslator Translator { get { return translator; } }
public UnityEngine.Animator AnimatorComponent {
get { return this.GetComponent<Animator>(); }
}
public override bool IsValid {
get {
return skeletonRenderer != null && skeletonRenderer.IsValid && translator != null &&
translator.Animator && translator.Animator.isInitialized;
}
}
public override void InitializeAnimationComponent () {
base.InitializeAnimationComponent();
if (!skeletonRenderer.IsValid)
return;
if (translator == null) translator = new MecanimTranslator();
translator.Initialize(this.AnimatorComponent, skeletonRenderer.SkeletonDataAsset);
}
protected override void UpdateAnimationStatus (float deltaTime) {
// Note: main animation status is updated by Mecanim Animator component
skeleton.Update(deltaTime);
}
public override void MainThreadBeforeUpdateInternal () {
base.MainThreadBeforeUpdateInternal();
translator.GatherAnimatorState();
}
protected override void ApplyStateToSkeleton (bool calledFromMainThread) {
#if UNITY_EDITOR
if (calledFromMainThread)
EditorRebindAnimator();
if (ApplicationIsPlaying || !calledFromMainThread) {
translator.Apply(skeletonRenderer.Skeleton);
} else {
Animator translatorAnimator = translator.Animator;
if (translatorAnimator != null && translatorAnimator.isInitialized &&
translatorAnimator.isActiveAndEnabled && translatorAnimator.runtimeAnimatorController != null) {
// Note: Rebind is required to prevent warning "Animator is not playing an AnimatorController" with prefabs
translatorAnimator.Rebind();
translator.Apply(skeletonRenderer.Skeleton);
}
}
#else
translator.Apply(skeletonRenderer.Skeleton);
#endif
}
#if UNITY_EDITOR
private void EditorRebindAnimator () {
Animator translatorAnimator = translator.Animator;
if (translatorAnimator != null && !translatorAnimator.isInitialized)
translatorAnimator.Rebind();
}
#endif
#region Transfer of Deprecated Fields
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
// compatibility layer between 4.1 and 4.2, automatically transfer serialized attributes.
public override void UpgradeTo43 () {
if (!Application.isPlaying && !wasDeprecatedTransferred) {
UpgradeTo43Components();
TransferDeprecatedFields();
InitializeAnimationComponent();
}
}
protected void UpgradeTo43Components () {
if (gameObject.GetComponent<SkeletonRenderer>() == null &&
gameObject.GetComponent<SkeletonGraphic>() == null) {
gameObject.AddComponent<SkeletonRenderer>();
Debug.Log(string.Format("{0}: Auto-migrated old SkeletonMecanim component to split SkeletonMecanim + SkeletonRenderer components.",
gameObject.name), gameObject);
EditorBridge.RequestMarkDirty(gameObject);
}
}
/// <summary>Transfer of former base class SkeletonRenderer parameters.</summary>
protected void TransferDeprecatedFields () {
wasDeprecatedTransferred = true;
SkeletonRenderer skeletonRenderer = gameObject.GetComponent<SkeletonRenderer>();
if (skeletonRenderer == null)
return;
skeletonRenderer.skeletonDataAsset = this.skeletonDataAssetDeprecated;
skeletonRenderer.initialSkinName = this.initialSkinNameDeprecated;
skeletonRenderer.EditorSkipSkinSync = this.editorSkipSkinSyncDeprecated;
skeletonRenderer.initialFlipX = this.initialFlipXDeprecated;
skeletonRenderer.initialFlipY = this.initialFlipYDeprecated;
skeletonRenderer.UpdateMode = this.updateModeDeprecated;
skeletonRenderer.updateWhenInvisible = this.updateWhenInvisibleDeprecated;
skeletonRenderer.separatorSlotNames = this.separatorSlotNamesDeprecated;
skeletonRenderer.MeshSettings.zSpacing = this.zSpacingDeprecated;
skeletonRenderer.MeshSettings.useClipping = this.useClippingDeprecated;
skeletonRenderer.MeshSettings.immutableTriangles = this.immutableTrianglesDeprecated;
skeletonRenderer.MeshSettings.pmaVertexColors = this.pmaVertexColorsDeprecated;
skeletonRenderer.MeshSettings.tintBlack = this.tintBlackDeprecated;
skeletonRenderer.MeshSettings.addNormals = this.addNormalsDeprecated;
skeletonRenderer.MeshSettings.calculateTangents = this.calculateTangentsDeprecated;
skeletonRenderer.clearStateOnDisable = this.clearStateOnDisableDeprecated;
skeletonRenderer.singleSubmesh = this.singleSubmeshDeprecated;
skeletonRenderer.MaskInteraction = this.maskInteractionDeprecated;
translator.TransferDeprecatedFields();
}
[SerializeField] protected bool wasDeprecatedTransferred = false;
// SkeletonRenderer former base class parameters
[FormerlySerializedAs("skeletonDataAsset")] [SerializeField] private SkeletonDataAsset skeletonDataAssetDeprecated;
[FormerlySerializedAs("initialSkinName")] [SpineSkin(defaultAsEmptyString: true)] [SerializeField] private string initialSkinNameDeprecated;
[FormerlySerializedAs("editorSkipSkinSync")] [SerializeField] private bool editorSkipSkinSyncDeprecated = false;
[FormerlySerializedAs("initialFlipX")] [SerializeField] private bool initialFlipXDeprecated = false;
[FormerlySerializedAs("initialFlipY")] [SerializeField] private bool initialFlipYDeprecated = false;
[FormerlySerializedAs("updateMode")] [SerializeField] private UpdateMode updateModeDeprecated = UpdateMode.FullUpdate;
[FormerlySerializedAs("updateWhenInvisible")] [SerializeField] private UpdateMode updateWhenInvisibleDeprecated = UpdateMode.FullUpdate;
[UnityEngine.Serialization.FormerlySerializedAs("submeshSeparators"),
UnityEngine.Serialization.FormerlySerializedAs("separatorSlotNames")]
[SerializeField] private string[] separatorSlotNamesDeprecated = new string[0];
[FormerlySerializedAs("zSpacing")] [SerializeField] private float zSpacingDeprecated = 0f;
[FormerlySerializedAs("useClipping")] [SerializeField] private bool useClippingDeprecated = true;
[FormerlySerializedAs("immutableTriangles")] [SerializeField] private bool immutableTrianglesDeprecated = false;
[FormerlySerializedAs("pmaVertexColors")] [SerializeField] private bool pmaVertexColorsDeprecated = true;
[FormerlySerializedAs("clearStateOnDisable")] [SerializeField] private bool clearStateOnDisableDeprecated = false;
[FormerlySerializedAs("tintBlack")] [SerializeField] private bool tintBlackDeprecated = false;
[FormerlySerializedAs("singleSubmesh")] [SerializeField] private bool singleSubmeshDeprecated = false;
[FormerlySerializedAs("calculateNormals"),
FormerlySerializedAs("addNormals")]
[SerializeField] private bool addNormalsDeprecated = false;
[FormerlySerializedAs("calculateTangents")] [SerializeField] private bool calculateTangentsDeprecated = false;
#if BUILT_IN_SPRITE_MASK_COMPONENT
[FormerlySerializedAs("maskInteraction")] [SerializeField] private SpriteMaskInteraction maskInteractionDeprecated = SpriteMaskInteraction.None;
#endif // BUILT_IN_SPRITE_MASK_COMPONENT
#endif // UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
#endregion
[System.Serializable]
public class MecanimTranslator {
const float WeightEpsilon = 0.0001f;
#region Inspector
public bool autoReset = true;
public bool useCustomMixMode = true;
public MixMode[] layerMixModes = new MixMode[0];
public bool[] layerIsAdditive = new bool[0];
// Migration from removed MixBlend
[FormerlySerializedAs("layerBlendModes")] [SerializeField] private int[] layerBlendModesDeprecated = new int[0];
#endregion
public delegate void OnClipAppliedDelegate (Spine.Animation clip, int layerIndex, float weight,
float time, float lastTime, bool playsBackward);
protected event OnClipAppliedDelegate _OnClipApplied;
public event OnClipAppliedDelegate OnClipApplied { add { _OnClipApplied += value; } remove { _OnClipApplied -= value; } }
public enum MixMode { AlwaysMix, MixNext, Hard, Match }
readonly Dictionary<int, Spine.Animation> animationTable = new Dictionary<int, Spine.Animation>(IntEqualityComparer.Instance);
readonly Dictionary<AnimationClip, int> clipNameHashCodeTable = new Dictionary<AnimationClip, int>(AnimationClipEqualityComparer.Instance);
readonly List<Animation> previousAnimations = new List<Animation>();
protected struct ClipInfo {
public Spine.Animation animation;
public float weight;
public float length;
public bool isLooping;
}
protected class ClipInfos {
public bool isInterruptionActive = false;
public bool isLastFrameOfInterruption = false;
public int clipInfoCount = 0;
public int nextClipInfoCount = 0;
public int interruptingClipInfoCount = 0;
public readonly List<ClipInfo> clipInfos = new List<ClipInfo>();
public readonly List<ClipInfo> nextClipInfos = new List<ClipInfo>();
public readonly List<ClipInfo> interruptingClipInfos = new List<ClipInfo>();
public float[] clipResolvedWeights = new float[0];
public float[] nextClipResolvedWeights = new float[0];
public float[] interruptingClipResolvedWeights = new float[0];
public AnimatorStateInfo stateInfo;
public AnimatorStateInfo nextStateInfo;
public AnimatorStateInfo interruptingStateInfo;
public float interruptingClipTimeAddition = 0;
public float layerWeight = 0;
}
protected ClipInfos[] layerClipInfos = new ClipInfos[0];
protected int layerCount = 0;
Animator animator;
public Animator Animator { get { return this.animator; } }
public int MecanimLayerCount {
get {
if (!animator)
return 0;
if (!animator.isInitialized || !animator.isActiveAndEnabled || animator.runtimeAnimatorController == null)
return 0;
return animator.layerCount;
}
}
public string[] MecanimLayerNames {
get {
if (!animator)
return new string[0];
string[] layerNames = new string[animator.layerCount];
for (int i = 0; i < animator.layerCount; ++i) {
layerNames[i] = animator.GetLayerName(i);
}
return layerNames;
}
}
public void Initialize (Animator animator, SkeletonDataAsset skeletonDataAsset) {
this.animator = animator;
previousAnimations.Clear();
animationTable.Clear();
SkeletonData data = skeletonDataAsset.GetSkeletonData(true);
foreach (Animation a in data.Animations)
animationTable.Add(a.Name.GetHashCode(), a);
clipNameHashCodeTable.Clear();
ClearClipInfosForLayers();
}
private bool ApplyAnimation (Skeleton skeleton, ClipInfo info, AnimatorStateInfo stateInfo,
int layerIndex, float layerWeight, bool layerIsAdditive, bool fromSetup,
bool useCustomClipWeight = false, float customClipWeight = 1.0f) {
float weight = info.weight * layerWeight;
if (weight < WeightEpsilon)
return false;
if (info.animation == null)
return false;
float time = AnimationTime(stateInfo.normalizedTime, info.length,
info.isLooping, stateInfo.speed < 0);
weight = useCustomClipWeight ? layerWeight * customClipWeight : weight;
info.animation.Apply(skeleton, 0, time, info.isLooping, null,
weight, fromSetup, layerIsAdditive, false, false);
if (_OnClipApplied != null)
OnClipAppliedCallback(info.animation, stateInfo, layerIndex, time, info.isLooping, weight);
return true;
}
private bool ApplyInterruptionAnimation (Skeleton skeleton,
bool interpolateWeightTo1, ClipInfo info, AnimatorStateInfo stateInfo,
int layerIndex, float layerWeight, bool layerIsAdditive, float interruptingClipTimeAddition,
bool useCustomClipWeight = false, float customClipWeight = 1.0f) {
float clipWeight = interpolateWeightTo1 ? (info.weight + 1.0f) * 0.5f : info.weight;
float weight = clipWeight * layerWeight;
if (weight < WeightEpsilon)
return false;
if (info.animation == null)
return false;
float time = AnimationTime(stateInfo.normalizedTime + interruptingClipTimeAddition,
info.length, stateInfo.speed < 0);
weight = useCustomClipWeight ? layerWeight * customClipWeight : weight;
bool fromSetup = false;
info.animation.Apply(skeleton, 0, time, info.isLooping, null,
weight, fromSetup, layerIsAdditive, false, false);
if (_OnClipApplied != null) {
OnClipAppliedCallback(info.animation, stateInfo, layerIndex, time, info.isLooping, weight);
}
return true;
}
private void OnClipAppliedCallback (Spine.Animation animation, AnimatorStateInfo stateInfo,
int layerIndex, float time, bool isLooping, float weight) {
float speedFactor = stateInfo.speedMultiplier * stateInfo.speed;
float lastTime = time - (Time.deltaTime * speedFactor);
if (isLooping && animation.Duration != 0) {
time %= animation.Duration;
lastTime %= animation.Duration;
}
_OnClipApplied(animation, layerIndex, weight, time, lastTime, speedFactor < 0);
}
public void GatherAnimatorState () {
layerCount = animator.layerCount;
MigrateLayerBlendModes();
#if UNITY_EDITOR
GetLayerBlendModes();
#endif
if (layerIsAdditive.Length < layerCount)
System.Array.Resize<bool>(ref layerIsAdditive, layerCount);
if (layerMixModes.Length < layerCount) {
int oldSize = layerMixModes.Length;
System.Array.Resize<MixMode>(ref layerMixModes, layerCount);
for (int layer = oldSize; layer < layerCount; ++layer) {
layerMixModes[layer] = layerIsAdditive[layer] ? MixMode.AlwaysMix : MixMode.MixNext;
}
}
InitClipInfosForLayers();
for (int layer = 0, n = layerCount; layer < n; layer++) {
GetStateUpdatesFromAnimator(layer);
}
}
public void Apply (Skeleton skeleton) {
// Clear Previous
if (autoReset) {
List<Animation> previousAnimations = this.previousAnimations;
for (int i = 0, n = previousAnimations.Count; i < n; i++)
previousAnimations[i].Apply(skeleton, 0, 0, false, null, 0, true, false, true, false); // SetKeyedItemsToSetupPose
previousAnimations.Clear();
for (int layer = 0, n = layerCount; layer < n; layer++) {
ClipInfos layerInfos = layerClipInfos[layer];
float layerWeight = layerInfos.layerWeight;
if (layerWeight <= 0) continue;
AnimatorStateInfo nextStateInfo = layerInfos.nextStateInfo;
bool hasNext = nextStateInfo.fullPathHash != 0;
int clipInfoCount, nextClipInfoCount, interruptingClipInfoCount;
IList<ClipInfo> clipInfo, nextClipInfo, interruptingClipInfo;
bool isInterruptionActive, shallInterpolateWeightTo1;
GetAnimatorClipInfos(layer, out isInterruptionActive, out clipInfoCount, out nextClipInfoCount, out interruptingClipInfoCount,
out clipInfo, out nextClipInfo, out interruptingClipInfo, out shallInterpolateWeightTo1);
for (int c = 0; c < clipInfoCount; c++) {
ClipInfo info = clipInfo[c];
float weight = info.weight * layerWeight; if (weight < WeightEpsilon) continue;
if (info.animation != null)
previousAnimations.Add(info.animation);
}
if (hasNext) {
for (int c = 0; c < nextClipInfoCount; c++) {
ClipInfo info = nextClipInfo[c];
float weight = info.weight * layerWeight; if (weight < WeightEpsilon) continue;
if (info.animation != null)
previousAnimations.Add(info.animation);
}
}
if (isInterruptionActive) {
for (int c = 0; c < interruptingClipInfoCount; c++) {
ClipInfo info = interruptingClipInfo[c];
float clipWeight = shallInterpolateWeightTo1 ? (info.weight + 1.0f) * 0.5f : info.weight;
float weight = clipWeight * layerWeight; if (weight < WeightEpsilon) continue;
if (info.animation != null)
previousAnimations.Add(info.animation);
}
}
}
}
// Apply
int appliedCount = 0;
for (int layer = 0, n = layerCount; layer < n; layer++) {
ClipInfos layerInfos = layerClipInfos[layer];
float layerWeight = layerInfos.layerWeight;
bool isInterruptionActive;
AnimatorStateInfo stateInfo;
AnimatorStateInfo nextStateInfo;
AnimatorStateInfo interruptingStateInfo;
float interruptingClipTimeAddition;
GetAnimatorStateInfos(layer, out isInterruptionActive, out stateInfo, out nextStateInfo, out interruptingStateInfo, out interruptingClipTimeAddition);
bool hasNext = nextStateInfo.fullPathHash != 0;
int clipInfoCount, nextClipInfoCount, interruptingClipInfoCount;
IList<ClipInfo> clipInfo, nextClipInfo, interruptingClipInfo;
bool interpolateWeightTo1;
GetAnimatorClipInfos(layer, out isInterruptionActive, out clipInfoCount, out nextClipInfoCount, out interruptingClipInfoCount,
out clipInfo, out nextClipInfo, out interruptingClipInfo, out interpolateWeightTo1);
bool add = layerIsAdditive[layer];
MixMode mode = GetMixMode(layer, add);
if (mode == MixMode.AlwaysMix) {
// Always use Mix instead of Applying the first non-zero weighted clip.
for (int c = 0; c < clipInfoCount; c++) {
ApplyAnimation(skeleton, clipInfo[c], stateInfo, layer, layerWeight, add, appliedCount++ == 0);
}
if (hasNext) {
for (int c = 0; c < nextClipInfoCount; c++) {
ApplyAnimation(skeleton, nextClipInfo[c], nextStateInfo, layer, layerWeight, add, appliedCount++ == 0);
}
}
if (isInterruptionActive) {
for (int c = 0; c < interruptingClipInfoCount; c++) {
appliedCount++;
ApplyInterruptionAnimation(skeleton, interpolateWeightTo1,
interruptingClipInfo[c], interruptingStateInfo,
layer, layerWeight, add, interruptingClipTimeAddition);
}
}
} else if (mode == MixMode.Match) {
// Calculate matching Spine lerp(lerp(A, B, w2), C, w3) weights
// from Unity's absolute weights A*W1 + B*W2 + C*W3.
MatchWeights(layerClipInfos[layer], hasNext, isInterruptionActive, clipInfoCount,
nextClipInfoCount, interruptingClipInfoCount, clipInfo, nextClipInfo, interruptingClipInfo);
float[] customWeights = layerClipInfos[layer].clipResolvedWeights;
for (int c = 0; c < clipInfoCount; c++) {
ApplyAnimation(skeleton, clipInfo[c], stateInfo, layer, layerWeight, add,
appliedCount++ == 0, true, customWeights[c]);
}
if (hasNext) {
customWeights = layerClipInfos[layer].nextClipResolvedWeights;
for (int c = 0; c < nextClipInfoCount; c++) {
ApplyAnimation(skeleton, nextClipInfo[c], nextStateInfo, layer, layerWeight, add,
appliedCount++ == 0, true, customWeights[c]);
}
}
if (isInterruptionActive) {
customWeights = layerClipInfos[layer].interruptingClipResolvedWeights;
for (int c = 0; c < interruptingClipInfoCount; c++) {
appliedCount++;
ApplyInterruptionAnimation(skeleton, interpolateWeightTo1,
interruptingClipInfo[c], interruptingStateInfo,
layer, layerWeight, add, interruptingClipTimeAddition,
true, customWeights[c]);
}
}
} else { // case MixNext || Hard
// Apply first non-zero weighted clip
int c = 0;
for (; c < clipInfoCount; c++) {
if (!ApplyAnimation(skeleton, clipInfo[c], stateInfo, layer, layerWeight, add,
appliedCount++ == 0, true, 1.0f))
continue;
++c; break;
}
// Mix the rest
for (; c < clipInfoCount; c++) {
ApplyAnimation(skeleton, clipInfo[c], stateInfo, layer, layerWeight, add, appliedCount++ == 0);
}
c = 0;
if (hasNext) {
// Apply next clip directly instead of mixing (ie: no crossfade, ignores mecanim transition weights)
if (mode == MixMode.Hard) {
for (; c < nextClipInfoCount; c++) {
if (!ApplyAnimation(skeleton, nextClipInfo[c], nextStateInfo, layer, layerWeight, add,
appliedCount++ == 0, true, 1.0f))
continue;
++c; break;
}
}
// Mix the rest
for (; c < nextClipInfoCount; c++) {
if (!ApplyAnimation(skeleton, nextClipInfo[c], nextStateInfo, layer, layerWeight, add, appliedCount++ == 0))
continue;
}
}
c = 0;
if (isInterruptionActive) {
// Apply next clip directly instead of mixing (ie: no crossfade, ignores mecanim transition weights)
if (mode == MixMode.Hard) {
for (; c < interruptingClipInfoCount; c++) {
appliedCount++;
if (ApplyInterruptionAnimation(skeleton, interpolateWeightTo1,
interruptingClipInfo[c], interruptingStateInfo,
layer, layerWeight, add, interruptingClipTimeAddition, true, 1.0f)) {
++c; break;
}
}
}
// Mix the rest
for (; c < interruptingClipInfoCount; c++) {
appliedCount++;
ApplyInterruptionAnimation(skeleton, interpolateWeightTo1,
interruptingClipInfo[c], interruptingStateInfo,
layer, layerWeight, add, interruptingClipTimeAddition);
}
}
}
}
}
/// <summary>
/// Resolve matching weights from Unity's absolute weights A*w1 + B*w2 + C*w3 to
/// Spine's lerp(lerp(A, B, x), C, y) weights, in reverse order of clips.
/// </summary>
protected void MatchWeights (ClipInfos clipInfos, bool hasNext, bool isInterruptionActive,
int clipInfoCount, int nextClipInfoCount, int interruptingClipInfoCount,
IList<ClipInfo> clipInfo, IList<ClipInfo> nextClipInfo, IList<ClipInfo> interruptingClipInfo) {
if (clipInfos.clipResolvedWeights.Length < clipInfoCount) {
System.Array.Resize<float>(ref clipInfos.clipResolvedWeights, clipInfoCount);
}
if (hasNext && clipInfos.nextClipResolvedWeights.Length < nextClipInfoCount) {
System.Array.Resize<float>(ref clipInfos.nextClipResolvedWeights, nextClipInfoCount);
}
if (isInterruptionActive && clipInfos.interruptingClipResolvedWeights.Length < interruptingClipInfoCount) {
System.Array.Resize<float>(ref clipInfos.interruptingClipResolvedWeights, interruptingClipInfoCount);
}
float inverseWeight = 1.0f;
if (isInterruptionActive) {
for (int c = interruptingClipInfoCount - 1; c >= 0; c--) {
float unityWeight = interruptingClipInfo[c].weight;
clipInfos.interruptingClipResolvedWeights[c] = interruptingClipInfo[c].weight * inverseWeight;
inverseWeight /= (1.0f - unityWeight);
}
}
if (hasNext) {
for (int c = nextClipInfoCount - 1; c >= 0; c--) {
float unityWeight = nextClipInfo[c].weight;
clipInfos.nextClipResolvedWeights[c] = nextClipInfo[c].weight * inverseWeight;
inverseWeight /= (1.0f - unityWeight);
}
}
for (int c = clipInfoCount - 1; c >= 0; c--) {
float unityWeight = clipInfo[c].weight;
clipInfos.clipResolvedWeights[c] = (c == 0) ? 1f : clipInfo[c].weight * inverseWeight;
inverseWeight /= (1.0f - unityWeight);
}
}
public KeyValuePair<Spine.Animation, float> GetActiveAnimationAndTime (int layer) {
if (layer >= layerClipInfos.Length)
return new KeyValuePair<Spine.Animation, float>(null, 0);
ClipInfos layerInfos = layerClipInfos[layer];
bool isInterruptionActive = layerInfos.isInterruptionActive;
ClipInfo clipInfo;
Spine.Animation animation = null;
AnimatorStateInfo stateInfo;
if (isInterruptionActive && layerInfos.interruptingClipInfoCount > 0) {
clipInfo = layerInfos.interruptingClipInfos[0];
stateInfo = layerInfos.interruptingStateInfo;
} else if (layerInfos.clipInfoCount > 0) {
clipInfo = layerInfos.clipInfos[0];
stateInfo = layerInfos.stateInfo;
} else {
return new KeyValuePair<Spine.Animation, float>(null, 0);
}
animation = clipInfo.animation;
float time = AnimationTime(stateInfo.normalizedTime, clipInfo.length,
clipInfo.isLooping, stateInfo.speed < 0);
return new KeyValuePair<Animation, float>(animation, time);
}
static float AnimationTime (float normalizedTime, float clipLength, bool loop, bool reversed) {
float time = AnimationTime(normalizedTime, clipLength, reversed);
if (loop) return time;
const float EndSnapEpsilon = 1f / 30f; // Workaround for end-duration keys not being applied.
return (clipLength - time < EndSnapEpsilon) ? clipLength : time; // return a time snapped to clipLength;
}
static float AnimationTime (float normalizedTime, float clipLength, bool reversed) {
if (reversed)
normalizedTime = (1 - normalizedTime);
if (normalizedTime < 0.0f)
normalizedTime = (normalizedTime % 1.0f) + 1.0f;
return normalizedTime * clipLength;
}
void InitClipInfosForLayers () {
if (layerClipInfos.Length < layerCount) {
System.Array.Resize<ClipInfos>(ref layerClipInfos, layerCount);
for (int layer = 0, n = layerCount; layer < n; ++layer) {
if (layerClipInfos[layer] == null)
layerClipInfos[layer] = new ClipInfos();
}
}
}
void ClearClipInfosForLayers () {
for (int layer = 0, n = layerClipInfos.Length; layer < n; ++layer) {
if (layerClipInfos[layer] == null)
layerClipInfos[layer] = new ClipInfos();
else {
layerClipInfos[layer].isInterruptionActive = false;
layerClipInfos[layer].isLastFrameOfInterruption = false;
layerClipInfos[layer].clipInfos.Clear();
layerClipInfos[layer].nextClipInfos.Clear();
layerClipInfos[layer].interruptingClipInfos.Clear();
}
}
}
private MixMode GetMixMode (int layer, bool layerIsAdditive) {
if (useCustomMixMode) {
MixMode mode = layerMixModes[layer];
// Note: at additive blending it makes no sense to use constant weight 1 at a fadeout anim add1 as
// with override layers, so we use AlwaysMix instead to use the proper weights.
// AlwaysMix leads to the expected result = lower_layer + lerp(add1, add2, transition_weight).
if (layerIsAdditive && mode == MixMode.MixNext) {
mode = MixMode.AlwaysMix;
layerMixModes[layer] = mode;
}
return mode;
} else {
return layerIsAdditive ? MixMode.AlwaysMix : MixMode.MixNext;
}
}
#if UNITY_EDITOR
public void TransferDeprecatedFields () {
MigrateLayerBlendModes();
}
#endif
void MigrateLayerBlendModes () {
if (layerIsAdditive.Length == 0 && layerBlendModesDeprecated.Length > 0) {
layerIsAdditive = new bool[layerBlendModesDeprecated.Length];
for (int i = 0; i < layerBlendModesDeprecated.Length; i++)
layerIsAdditive[i] = layerBlendModesDeprecated[i] == 3; // MixBlend.Add was 3.
layerBlendModesDeprecated = new int[0];
}
}
#if UNITY_EDITOR
void GetLayerBlendModes () {
if (layerIsAdditive.Length < layerCount) {
System.Array.Resize<bool>(ref layerIsAdditive, layerCount);
}
AnimatorController controller = animator.runtimeAnimatorController as AnimatorController;
for (int layer = 0, n = layerCount; layer < n; ++layer) {
if (controller != null)
layerIsAdditive[layer] = controller.layers[layer].blendingMode == AnimatorLayerBlendingMode.Additive;
}
}
#endif
void GetStateUpdatesFromAnimator (int layer) {
ClipInfos layerInfos = layerClipInfos[layer];
// Note: Animator.GetLayerWeight always returns 0 on the first layer. Should be interpreted as 1.
layerInfos.layerWeight = (layer == 0) ? 1 : animator.GetLayerWeight(layer);
int clipInfoCount = animator.GetCurrentAnimatorClipInfoCount(layer);
int nextClipInfoCount = animator.GetNextAnimatorClipInfoCount(layer);
List<ClipInfo> clipInfos = layerInfos.clipInfos;
List<ClipInfo> nextClipInfos = layerInfos.nextClipInfos;
List<ClipInfo> interruptingClipInfos = layerInfos.interruptingClipInfos;
layerInfos.isInterruptionActive = (clipInfoCount == 0 && clipInfos.Count != 0 &&
nextClipInfoCount == 0 && nextClipInfos.Count != 0);
// Note: during interruption, GetCurrentAnimatorClipInfoCount and GetNextAnimatorClipInfoCount
// are returning 0 in calls above. Therefore we keep previous clipInfos and nextClipInfos
// until the interruption is over.
if (layerInfos.isInterruptionActive) {
// Note: The last frame of a transition interruption
// will have fullPathHash set to 0, therefore we have to use previous
// frame's infos about interruption clips and correct some values
// accordingly (normalizedTime and weight).
AnimatorStateInfo interruptingStateInfo = animator.GetNextAnimatorStateInfo(layer);
layerInfos.isLastFrameOfInterruption = interruptingStateInfo.fullPathHash == 0;
if (!layerInfos.isLastFrameOfInterruption) {
List<AnimatorClipInfo> tempInterruptingClipInfos = new List<AnimatorClipInfo>();
animator.GetNextAnimatorClipInfo(layer, tempInterruptingClipInfos);
interruptingClipInfos.Clear();
for (int i = 0; i < tempInterruptingClipInfos.Count; i++) {
AnimatorClipInfo animatorInfo = tempInterruptingClipInfos[i];
ClipInfo info = new ClipInfo();
info.animation = GetAnimation(animatorInfo.clip);
info.weight = animatorInfo.weight;
info.length = animatorInfo.clip.length;
info.isLooping = animatorInfo.clip.isLooping;
interruptingClipInfos.Add(info);
}
layerInfos.interruptingClipInfoCount = interruptingClipInfos.Count;
float oldTime = layerInfos.interruptingStateInfo.normalizedTime;
float newTime = interruptingStateInfo.normalizedTime;
layerInfos.interruptingClipTimeAddition = newTime - oldTime;
layerInfos.interruptingStateInfo = interruptingStateInfo;
}
} else {
layerInfos.clipInfoCount = clipInfoCount;
layerInfos.nextClipInfoCount = nextClipInfoCount;
layerInfos.interruptingClipInfoCount = 0;
layerInfos.isLastFrameOfInterruption = false;
if (clipInfos.Capacity < clipInfoCount) clipInfos.Capacity = clipInfoCount;
if (nextClipInfos.Capacity < nextClipInfoCount) nextClipInfos.Capacity = nextClipInfoCount;
// Get current clip infos and extract data
List<AnimatorClipInfo> tempClipInfos = new List<AnimatorClipInfo>();
animator.GetCurrentAnimatorClipInfo(layer, tempClipInfos);
clipInfos.Clear();
for (int i = 0; i < tempClipInfos.Count; i++) {
AnimatorClipInfo animatorInfo = tempClipInfos[i];
ClipInfo info = new ClipInfo();
info.animation = GetAnimation(animatorInfo.clip);
info.weight = animatorInfo.weight;
info.length = animatorInfo.clip.length;
info.isLooping = animatorInfo.clip.isLooping;
clipInfos.Add(info);
}
// Get next clip infos and extract data
List<AnimatorClipInfo> tempNextClipInfos = new List<AnimatorClipInfo>();
animator.GetNextAnimatorClipInfo(layer, tempNextClipInfos);
nextClipInfos.Clear();
for (int i = 0; i < tempNextClipInfos.Count; i++) {
AnimatorClipInfo animatorInfo = tempNextClipInfos[i];
ClipInfo info = new ClipInfo();
info.animation = GetAnimation(animatorInfo.clip);
info.weight = animatorInfo.weight;
info.length = animatorInfo.clip.length;
info.isLooping = animatorInfo.clip.isLooping;
nextClipInfos.Add(info);
}
layerInfos.stateInfo = animator.GetCurrentAnimatorStateInfo(layer);
layerInfos.nextStateInfo = animator.GetNextAnimatorStateInfo(layer);
}
}
void GetAnimatorClipInfos (
int layer,
out bool isInterruptionActive,
out int clipInfoCount,
out int nextClipInfoCount,
out int interruptingClipInfoCount,
out IList<ClipInfo> clipInfo,
out IList<ClipInfo> nextClipInfo,
out IList<ClipInfo> interruptingClipInfo,
out bool shallInterpolateWeightTo1) {
ClipInfos layerInfos = layerClipInfos[layer];
isInterruptionActive = layerInfos.isInterruptionActive;
clipInfoCount = layerInfos.clipInfoCount;
nextClipInfoCount = layerInfos.nextClipInfoCount;
interruptingClipInfoCount = layerInfos.interruptingClipInfoCount;
clipInfo = layerInfos.clipInfos;
nextClipInfo = layerInfos.nextClipInfos;
interruptingClipInfo = isInterruptionActive ? layerInfos.interruptingClipInfos : null;
shallInterpolateWeightTo1 = layerInfos.isLastFrameOfInterruption;
}
void GetAnimatorStateInfos (
int layer,
out bool isInterruptionActive,
out AnimatorStateInfo stateInfo,
out AnimatorStateInfo nextStateInfo,
out AnimatorStateInfo interruptingStateInfo,
out float interruptingClipTimeAddition) {
ClipInfos layerInfos = layerClipInfos[layer];
isInterruptionActive = layerInfos.isInterruptionActive;
stateInfo = layerInfos.stateInfo;
nextStateInfo = layerInfos.nextStateInfo;
interruptingStateInfo = layerInfos.interruptingStateInfo;
interruptingClipTimeAddition = layerInfos.isLastFrameOfInterruption ? layerInfos.interruptingClipTimeAddition : 0;
}
Spine.Animation GetAnimation (AnimationClip clip) {
int clipNameHashCode;
if (!clipNameHashCodeTable.TryGetValue(clip, out clipNameHashCode)) {
clipNameHashCode = clip.name.GetHashCode();
clipNameHashCodeTable.Add(clip, clipNameHashCode);
}
Spine.Animation animation;
animationTable.TryGetValue(clipNameHashCode, out animation);
return animation;
}
class AnimationClipEqualityComparer : IEqualityComparer<AnimationClip> {
internal static readonly IEqualityComparer<AnimationClip> Instance = new AnimationClipEqualityComparer();
#if USES_ENTITY_ID
public bool Equals (AnimationClip x, AnimationClip y) { return x.GetEntityId() == y.GetEntityId(); }
public int GetHashCode (AnimationClip o) { return o.GetHashCode(); }
#else
public bool Equals (AnimationClip x, AnimationClip y) { return x.GetInstanceID() == y.GetInstanceID(); }
public int GetHashCode (AnimationClip o) { return o.GetInstanceID(); }
#endif
}
class IntEqualityComparer : IEqualityComparer<int> {
internal static readonly IEqualityComparer<int> Instance = new IntEqualityComparer();
public bool Equals (int x, int y) { return x == y; }
public int GetHashCode (int o) { return o; }
}
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: f9db98c60740638449864eb028fbe7ad
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 3a361f5ac799a5149b340f9e20da27d1
folderAsset: yes
timeCreated: 1457405502
licenseType: Free
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,161 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2026, 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.
*****************************************************************************/
using UnityEngine;
namespace Spine.Unity {
[RequireComponent(typeof(MeshRenderer), typeof(MeshFilter))]
[HelpURL("https://esotericsoftware.com/spine-unity-utility-components#SkeletonRenderSeparator")]
public class SkeletonPartsRenderer : MonoBehaviour {
#region Properties
MeshGenerator meshGenerator;
public MeshGenerator MeshGenerator {
get {
LazyIntialize();
return meshGenerator;
}
}
MeshRenderer meshRenderer;
public MeshRenderer MeshRenderer {
get {
LazyIntialize();
return meshRenderer;
}
}
MeshFilter meshFilter;
public MeshFilter MeshFilter {
get {
LazyIntialize();
return meshFilter;
}
}
#endregion
#region Callback Delegates
public delegate void SkeletonPartsRendererDelegate (SkeletonPartsRenderer skeletonPartsRenderer);
/// <summary>OnMeshAndMaterialsUpdated is called at the end of LateUpdate after the Mesh and
/// all materials have been updated.</summary>
public event SkeletonPartsRendererDelegate OnMeshAndMaterialsUpdated;
#endregion
MeshRendererBuffers buffers;
SkeletonRendererInstruction currentInstructions = new SkeletonRendererInstruction();
void LazyIntialize () {
if (buffers == null) {
buffers = new MeshRendererBuffers();
buffers.Initialize();
if (meshGenerator != null) return;
meshGenerator = new MeshGenerator();
meshFilter = GetComponent<MeshFilter>();
meshRenderer = GetComponent<MeshRenderer>();
currentInstructions.Clear();
}
}
void OnDestroy () {
if (buffers != null) buffers.Dispose();
}
public void ClearMesh () {
LazyIntialize();
meshFilter.sharedMesh = null;
}
public void RenderParts (SkeletonRenderer skeletonRenderer, ExposedList<SubmeshInstruction> instructions,
int startSubmesh, int endSubmesh) {
LazyIntialize();
// STEP 1: Create instruction
MeshRendererBuffers.SmartMesh smartMesh = buffers.GetNextMesh();
currentInstructions.SetWithSubset(instructions, startSubmesh, endSubmesh);
bool updateTriangles = SkeletonRendererInstruction.GeometryNotEqual(currentInstructions, smartMesh.instructionUsed);
// STEP 2: Generate mesh buffers.
SubmeshInstruction[] currentInstructionsSubmeshesItems = currentInstructions.submeshInstructions.Items;
meshGenerator.Begin();
if (currentInstructions.hasActiveClipping) {
for (int i = 0; i < currentInstructions.submeshInstructions.Count; i++)
meshGenerator.AddSubmesh(currentInstructionsSubmeshesItems[i], updateTriangles);
} else {
meshGenerator.BuildMeshWithArrays(currentInstructions, updateTriangles);
}
bool materialsChanged;
buffers.GatherMaterialsFromInstructions(currentInstructions.submeshInstructions, out materialsChanged);
// STEP 3: modify mesh.
Mesh mesh = smartMesh.mesh;
if (meshGenerator.VertexCount <= 0) { // Clear an empty mesh
updateTriangles = false;
mesh.Clear();
} else {
meshGenerator.FillVertexData(mesh);
if (updateTriangles) {
meshGenerator.FillTriangles(mesh);
}
if (updateTriangles || materialsChanged) {
Material[] materials = buffers.UpdateSharedMaterialsArray();
if (skeletonRenderer)
skeletonRenderer.ConfigureMaterials(materials, currentInstructions.submeshInstructions);
meshRenderer.sharedMaterials = materials;
}
meshGenerator.FillLateVertexData(mesh);
}
meshFilter.sharedMesh = mesh;
smartMesh.instructionUsed.Set(currentInstructions);
if (OnMeshAndMaterialsUpdated != null)
OnMeshAndMaterialsUpdated(this);
}
public void SetPropertyBlock (MaterialPropertyBlock block) {
LazyIntialize();
meshRenderer.SetPropertyBlock(block);
}
public static SkeletonPartsRenderer NewPartsRendererGameObject (Transform parent, string name, int sortingOrder = 0) {
GameObject go = new GameObject(name, typeof(MeshFilter), typeof(MeshRenderer));
go.transform.SetParent(parent, false);
SkeletonPartsRenderer returnComponent = go.AddComponent<SkeletonPartsRenderer>();
returnComponent.MeshRenderer.sortingOrder = sortingOrder;
return returnComponent;
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 1c0b968d1e7333b499e347acb644f1c1
timeCreated: 1458045480
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,331 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2026, 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
#if UNITY_2018_1_OR_NEWER
#define HAS_PROPERTY_BLOCK_QUERY
#endif
#if !SPINE_AUTO_UPGRADE_COMPONENTS_OFF
#define AUTO_UPGRADE_TO_43_COMPONENTS
#endif
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
namespace Spine.Unity {
#if NEW_PREFAB_SYSTEM
[ExecuteAlways]
#else
[ExecuteInEditMode]
#endif
[HelpURL("https://esotericsoftware.com/spine-unity-utility-components#SkeletonRenderSeparator")]
public class SkeletonRenderSeparator : MonoBehaviour, IUpgradable {
public const int DefaultSortingOrderIncrement = 5;
#region Inspector
[SerializeField]
protected SkeletonRenderer skeletonRenderer;
public SkeletonRenderer SkeletonRenderer {
get { return skeletonRenderer; }
set {
if (skeletonRenderer != null)
skeletonRenderer.GenerateMeshOverride -= HandleRender;
skeletonRenderer = value;
if (value == null)
this.enabled = false;
}
}
MeshRenderer mainMeshRenderer;
public bool copyPropertyBlock = true;
[Tooltip("Copies MeshRenderer flags into each parts renderer")]
public bool copyMeshRendererFlags = true;
public List<Spine.Unity.SkeletonPartsRenderer> partsRenderers = new List<SkeletonPartsRenderer>();
[System.NonSerialized] public bool isVisible = true;
#if UNITY_EDITOR
void Reset () {
if (skeletonRenderer == null)
skeletonRenderer = GetComponent<SkeletonRenderer>();
}
#endif
#endregion
#region Callback Delegates
/// <summary>OnMeshAndMaterialsUpdated is called at the end of LateUpdate after the Mesh and
/// all materials have been updated.</summary>
public event SkeletonRendererDelegate OnMeshAndMaterialsUpdated;
#endregion
#region Runtime Instantiation
/// <summary>Adds a SkeletonRenderSeparator and child SkeletonPartsRenderer GameObjects to a given SkeletonRenderer.</summary>
/// <returns>The to skeleton renderer.</returns>
/// <param name="skeletonRenderer">The target SkeletonRenderer or SkeletonAnimation.</param>
/// <param name="sortingLayerID">Sorting layer to be used for the parts renderers.</param>
/// <param name="extraPartsRenderers">Number of additional SkeletonPartsRenderers on top of the ones determined by counting the number of separator slots.</param>
/// <param name="sortingOrderIncrement">The integer to increment the sorting order per SkeletonPartsRenderer to separate them.</param>
/// <param name="baseSortingOrder">The sorting order value of the first SkeletonPartsRenderer.</param>
/// <param name="addMinimumPartsRenderers">If set to <c>true</c>, a minimum number of SkeletonPartsRenderer GameObjects (determined by separatorSlots.Count + 1) will be added.</param>
public static SkeletonRenderSeparator AddToSkeletonRenderer (SkeletonRenderer skeletonRenderer, int sortingLayerID = 0, int extraPartsRenderers = 0, int sortingOrderIncrement = DefaultSortingOrderIncrement, int baseSortingOrder = 0, bool addMinimumPartsRenderers = true) {
if (skeletonRenderer == null) {
Debug.Log("Tried to add SkeletonRenderSeparator to a null SkeletonRenderer reference.");
return null;
}
SkeletonRenderSeparator srs = skeletonRenderer.gameObject.AddComponent<SkeletonRenderSeparator>();
srs.skeletonRenderer = skeletonRenderer;
skeletonRenderer.Initialize(false);
int count = extraPartsRenderers;
if (addMinimumPartsRenderers)
count = extraPartsRenderers + skeletonRenderer.separatorSlots.Count + 1;
Transform skeletonRendererTransform = skeletonRenderer.transform;
List<SkeletonPartsRenderer> componentRenderers = srs.partsRenderers;
for (int i = 0; i < count; i++) {
SkeletonPartsRenderer spr = SkeletonPartsRenderer.NewPartsRendererGameObject(skeletonRendererTransform, i.ToString());
MeshRenderer mr = spr.MeshRenderer;
mr.sortingLayerID = sortingLayerID;
mr.sortingOrder = baseSortingOrder + (i * sortingOrderIncrement);
componentRenderers.Add(spr);
}
srs.OnEnable();
#if UNITY_EDITOR
// Make sure editor updates properly in edit mode.
if (!Application.isPlaying) {
skeletonRenderer.enabled = false;
skeletonRenderer.enabled = true;
skeletonRenderer.UpdateMesh();
}
#endif
return srs;
}
/// <summary>Add a child SkeletonPartsRenderer GameObject to this SkeletonRenderSeparator.</summary>
public SkeletonPartsRenderer AddPartsRenderer (int sortingOrderIncrement = DefaultSortingOrderIncrement, string name = null) {
int sortingLayerID = 0;
int sortingOrder = 0;
if (partsRenderers.Count > 0) {
SkeletonPartsRenderer previous = partsRenderers[partsRenderers.Count - 1];
MeshRenderer previousMeshRenderer = previous.MeshRenderer;
sortingLayerID = previousMeshRenderer.sortingLayerID;
sortingOrder = previousMeshRenderer.sortingOrder + sortingOrderIncrement;
}
if (string.IsNullOrEmpty(name))
name = partsRenderers.Count.ToString();
SkeletonPartsRenderer spr = SkeletonPartsRenderer.NewPartsRendererGameObject(skeletonRenderer.transform, name);
partsRenderers.Add(spr);
MeshRenderer mr = spr.MeshRenderer;
mr.sortingLayerID = sortingLayerID;
mr.sortingOrder = sortingOrder;
return spr;
}
#endregion
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
public virtual void Awake () {
if (!Application.isPlaying && !wasUpgradedTo43) {
UpgradeTo43();
}
}
#endif
public void OnEnable () {
if (skeletonRenderer == null) return;
if (copiedBlock == null) copiedBlock = new MaterialPropertyBlock();
mainMeshRenderer = skeletonRenderer.GetComponent<MeshRenderer>();
skeletonRenderer.enableSeparatorSlots = true;
skeletonRenderer.GenerateMeshOverride -= HandleRender;
skeletonRenderer.GenerateMeshOverride += HandleRender;
if (copyMeshRendererFlags) {
var lightProbeUsage = mainMeshRenderer.lightProbeUsage;
bool receiveShadows = mainMeshRenderer.receiveShadows;
var reflectionProbeUsage = mainMeshRenderer.reflectionProbeUsage;
var shadowCastingMode = mainMeshRenderer.shadowCastingMode;
var motionVectorGenerationMode = mainMeshRenderer.motionVectorGenerationMode;
var probeAnchor = mainMeshRenderer.probeAnchor;
for (int i = 0; i < partsRenderers.Count; i++) {
SkeletonPartsRenderer currentRenderer = partsRenderers[i];
if (currentRenderer == null) continue; // skip null items.
MeshRenderer mr = currentRenderer.MeshRenderer;
mr.lightProbeUsage = lightProbeUsage;
mr.receiveShadows = receiveShadows;
mr.reflectionProbeUsage = reflectionProbeUsage;
mr.shadowCastingMode = shadowCastingMode;
mr.motionVectorGenerationMode = motionVectorGenerationMode;
mr.probeAnchor = probeAnchor;
}
}
if (skeletonRenderer.updateWhenInvisible != UpdateMode.FullUpdate)
skeletonRenderer.UpdateMesh();
}
public void Update () {
UpdateVisibility();
}
public void OnDisable () {
if (skeletonRenderer == null) return;
skeletonRenderer.enableSeparatorSlots = false;
skeletonRenderer.GenerateMeshOverride -= HandleRender;
skeletonRenderer.UpdateMesh();
ClearPartsRendererMeshes();
}
public void UpdateVisibility () {
if (skeletonRenderer == null) return;
foreach (SkeletonPartsRenderer partsRenderer in partsRenderers) {
if (partsRenderer == null) continue;
if (partsRenderer.MeshRenderer.isVisible) {
if (!isVisible) {
skeletonRenderer.OnBecameVisible();
isVisible = true;
}
return;
}
}
if (isVisible) {
isVisible = false;
skeletonRenderer.OnBecameInvisible();
}
}
MaterialPropertyBlock copiedBlock;
void HandleRender (SkeletonRendererInstruction instruction) {
int rendererCount = partsRenderers.Count;
if (rendererCount <= 0) return;
#if HAS_PROPERTY_BLOCK_QUERY
bool assignPropertyBlock = this.copyPropertyBlock && mainMeshRenderer.HasPropertyBlock();
#else
bool assignPropertyBlock = this.copyPropertyBlock;
#endif
if (assignPropertyBlock)
mainMeshRenderer.GetPropertyBlock(copiedBlock);
MeshGenerator.Settings originalSettings = skeletonRenderer.MeshSettings;
MeshGenerator.Settings settings = new MeshGenerator.Settings {
addNormals = originalSettings.addNormals,
calculateTangents = originalSettings.calculateTangents,
immutableTriangles = false, // parts cannot do immutable triangles.
pmaVertexColors = originalSettings.pmaVertexColors,
tintBlack = originalSettings.tintBlack,
useClipping = true,
zSpacing = originalSettings.zSpacing
};
ExposedList<SubmeshInstruction> submeshInstructions = instruction.submeshInstructions;
SubmeshInstruction[] submeshInstructionsItems = submeshInstructions.Items;
int lastSubmeshInstruction = submeshInstructions.Count - 1;
int rendererIndex = 0;
SkeletonPartsRenderer currentRenderer = partsRenderers[rendererIndex];
for (int si = 0, start = 0; si <= lastSubmeshInstruction; si++) {
if (currentRenderer == null)
continue;
if (submeshInstructionsItems[si].forceSeparate || si == lastSubmeshInstruction) {
// Apply properties
MeshGenerator meshGenerator = currentRenderer.MeshGenerator;
meshGenerator.settings = settings;
if (assignPropertyBlock)
currentRenderer.SetPropertyBlock(copiedBlock);
// Render
currentRenderer.RenderParts(skeletonRenderer, instruction.submeshInstructions, start, si + 1);
start = si + 1;
rendererIndex++;
if (rendererIndex < rendererCount) {
currentRenderer = partsRenderers[rendererIndex];
} else {
// Not enough renderers. Skip the rest of the instructions.
break;
}
}
}
if (OnMeshAndMaterialsUpdated != null)
OnMeshAndMaterialsUpdated(this.skeletonRenderer);
// Clear extra renderers if they exist.
for (; rendererIndex < rendererCount; rendererIndex++) {
currentRenderer = partsRenderers[rendererIndex];
if (currentRenderer != null)
partsRenderers[rendererIndex].ClearMesh();
}
}
protected void ClearPartsRendererMeshes () {
foreach (SkeletonPartsRenderer partsRenderer in partsRenderers) {
if (partsRenderer != null)
partsRenderer.ClearMesh();
}
}
#region Transfer of Deprecated Fields
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
public virtual void UpgradeTo43 () {
wasUpgradedTo43 = true;
if (skeletonRenderer == null) {
Component previousReference = previousSkeletonRenderer != null ? previousSkeletonRenderer : this;
skeletonRenderer = previousReference.GetComponent<SkeletonRenderer>();
if (skeletonRenderer == null)
Debug.LogError("Please manually re-assign SkeletonRenderer at SkeletonRenderSeparator, " +
"automatic upgrade failed.", this);
}
}
[SerializeField, HideInInspector, FormerlySerializedAs("skeletonRenderer")] Component previousSkeletonRenderer;
[SerializeField] protected bool wasUpgradedTo43 = false;
#endif
#endregion
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 5c70a5b35f6ff2541aed8e8346b7e4d5
timeCreated: 1457405791
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,754 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2026, 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
#if UNITY_2018_1_OR_NEWER
#define PER_MATERIAL_PROPERTY_BLOCKS
#endif
#if UNITY_2019_3_OR_NEWER
#define CONFIGURABLE_ENTER_PLAY_MODE
#endif
#if !SPINE_DISABLE_THREADING
#define USE_THREADED_SKELETON_UPDATE
#endif
#if !SPINE_AUTO_UPGRADE_COMPONENTS_OFF
#define AUTO_UPGRADE_TO_43_COMPONENTS
#endif
#define SPINE_OPTIONAL_ON_DEMAND_LOADING
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
#if UNITY_EDITOR
using UnityEditor.SceneManagement;
#endif
namespace Spine.Unity {
// Partial class: covers common identical attributes, properties and methods shared across all
// ISkeletonRenderer subclasses. This is a workaround for single inheritance limitations and covers code which
// would otherwise be located in a base-class of SkeletonRenderer.
public partial class SkeletonRenderer : MonoBehaviour, ISkeletonRenderer, IHasSkeletonRenderer {
// Identical code shared by ISkeletonRenderer subclasses as a workaround for single inheritance limitations.
#region Identical common ISkeletonRenderer code
#region ISkeletonRenderer Attributes
// Core Attributes
public ISkeletonAnimation skeletonAnimation;
public SkeletonDataAsset skeletonDataAsset;
[System.NonSerialized] public Skeleton skeleton;
[System.NonSerialized] public bool valid;
protected bool wasMeshUpdatedAfterInit = false;
protected bool updateTriangles = true;
// Initialization Settings
/// <summary>Skin name to use when the Skeleton is initialized.</summary>
[SpineSkin(dataField: "skeletonDataAsset", defaultAsEmptyString: true)] public string initialSkinName;
/// <summary>Flip X and Y to use when the Skeleton is initialized.</summary>
public bool initialFlipX, initialFlipY;
// Render Settings
[SerializeField] protected MeshGenerator.Settings meshSettings = MeshGenerator.Settings.Default;
[System.NonSerialized] protected readonly SkeletonRendererInstruction currentInstructions = new SkeletonRendererInstruction();
/// <summary>Update mode to optionally limit updates to e.g. only apply animations but not update the mesh.</summary>
protected UpdateMode updateMode = UpdateMode.FullUpdate;
/// <summary>Update mode used when the MeshRenderer becomes invisible
/// (when <c>OnBecameInvisible()</c> is called). Update mode is automatically
/// reset to <c>UpdateMode.FullUpdate</c> when the mesh becomes visible again.</summary>
public UpdateMode updateWhenInvisible = UpdateMode.FullUpdate;
/// <summary>Clears the state of the render and skeleton when this component or its GameObject is disabled. This prevents previous state from being retained when it is enabled again. When pooling your skeleton, setting this to true can be helpful.</summary>
public bool clearStateOnDisable = false;
// Submesh Separation
/// <summary>Slot names used to populate separatorSlots list when the Skeleton is initialized. Changing this after initialization does nothing.</summary>
[SpineSlot] public string[] separatorSlotNames = new string[0];
/// <summary>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.</summary>
[System.NonSerialized] public readonly List<Slot> separatorSlots = new List<Slot>();
public bool enableSeparatorSlots = false;
// Overrides Attributes
// These are API for anything that wants to take over rendering for a SkeletonRenderer.
public bool disableRenderingOnOverride = true;
event InstructionDelegate generateMeshOverride;
[System.NonSerialized] readonly Dictionary<Slot, Material> customSlotMaterials = new Dictionary<Slot, Material>();
[System.NonSerialized] protected bool materialsNeedUpdate = false;
// Physics Attributes
/// <seealso cref="PhysicsPositionInheritanceFactor"/>
[SerializeField] protected Vector2 physicsPositionInheritanceFactor = Vector2.one;
/// <seealso cref="PhysicsRotationInheritanceFactor"/>
[SerializeField] protected float physicsRotationInheritanceFactor = 1.0f;
/// <seealso cref="PhysicsPositionInheritanceLimit"/>
[SerializeField] protected Vector2 physicsPositionInheritanceLimit = Vector2.positiveInfinity;
/// <seealso cref="PhysicsRotationInheritanceLimit"/>
[SerializeField] protected float physicsRotationInheritanceLimit = float.MaxValue;
/// <summary>Reference transform relative to which physics movement will be calculated, or null to use world location.</summary>
[SerializeField] protected Transform physicsMovementRelativeTo = null;
/// <summary>Used for applying Transform translation to skeleton PhysicsConstraints.</summary>
protected Vector3 lastPosition;
/// <summary>Used for applying Transform rotation to skeleton PhysicsConstraints.</summary>
protected float lastRotation;
/// <summary>Position delta for threaded processing as Transform access from worker thread is not allowed.</summary>
protected Vector3 positionDelta;
/// <summary>Rotation delta for threaded processing as Transform access from worker thread is not allowed.</summary>
protected float rotationDelta;
// Threaded Update System Attributes
#if USE_THREADED_SKELETON_UPDATE
protected static int mainThreadID = -1;
[SerializeField] protected SettingsTriState threadedMeshGeneration = SettingsTriState.UseGlobalSetting;
protected bool isUpdatedExternally = false;
protected bool requiresMeshBufferAssignmentMainThread = false;
#if UNITY_EDITOR
static bool applicationIsPlaying = false;
#endif
#endif // USE_THREADED_SKELETON_UPDATE
#if UNITY_EDITOR
/// <summary>Enable this parameter when overwriting the Skeleton's skin from an editor script.
/// Otherwise any changes will be overwritten by the next inspector update.</summary>
protected bool editorSkipSkinSync = false;
protected bool requiresEditorUpdate = false;
#endif
#endregion ISkeletonRenderer Attributes
#region ISkeletonRenderer Properties
public ISkeletonRenderer Renderer { get { return this; } }
public UnityEngine.MonoBehaviour Component { get { return this; } }
public ISkeletonAnimation Animation { get { return skeletonAnimation; } set { skeletonAnimation = value; } }
public SkeletonDataAsset SkeletonDataAsset {
get { return skeletonDataAsset; }
set { skeletonDataAsset = value; }
}
public Skeleton Skeleton {
get {
Initialize(false);
return skeleton;
}
set {
skeleton = value;
}
}
public SkeletonData SkeletonData {
get {
Initialize(false);
return skeleton == null ? null : skeleton.Data;
}
}
public bool IsValid { get { return valid; } }
public string InitialSkinName { get { return initialSkinName; } set { initialSkinName = value; } }
/// <summary>Flip X to use when the Skeleton is initialized.</summary>
public bool InitialFlipX { get { return initialFlipX; } set { initialFlipX = value; } }
/// <summary>Flip Y to use when the Skeleton is initialized.</summary>
public bool InitialFlipY { get { return initialFlipY; } set { initialFlipY = value; } }
/// <summary>Update mode to optionally limit updates to e.g. only apply animations but not update the mesh.</summary>
public UpdateMode UpdateMode { get { return updateMode; } set { updateMode = value; } }
/// <summary>Update mode used when the MeshRenderer becomes invisible
/// (when <c>OnBecameInvisible()</c> is called). Update mode is automatically
/// reset to <c>UpdateMode.FullUpdate</c> when the mesh becomes visible again.</summary>
public UpdateMode UpdateWhenInvisible { get { return updateWhenInvisible; } set { updateWhenInvisible = value; } }
public MeshGenerator.Settings MeshSettings { get { return meshSettings; } set { meshSettings = value; } }
// Submesh Separation
/// <summary>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.</summary>
public List<Slot> SeparatorSlots { get { return separatorSlots; } }
public bool EnableSeparatorSlots { get { return enableSeparatorSlots; } set { enableSeparatorSlots = value; } }
// Overrides Properties
public bool HasGenerateMeshOverride { get { return generateMeshOverride != null; } }
/// <summary>Allows separate code to take over rendering for this SkeletonRenderer component. The subscriber is passed a SkeletonRendererInstruction argument to determine how to render a skeleton.</summary>
public event InstructionDelegate GenerateMeshOverride {
add {
generateMeshOverride += value;
if (disableRenderingOnOverride && generateMeshOverride != null) {
Initialize(false);
DisableRenderers();
updateMode = UpdateMode.FullUpdate;
}
}
remove {
generateMeshOverride -= value;
if (disableRenderingOnOverride && generateMeshOverride == null) {
Initialize(false);
EnableRenderers();
}
}
}
/// <summary>Use this Dictionary to use a different Material to render specific Slots.</summary>
public Dictionary<Slot, Material> CustomSlotMaterials {
get { materialsNeedUpdate = true; return customSlotMaterials; }
}
// Physics Properties
/// <summary>When set to non-zero, Transform position movement in X and Y direction
/// is applied to skeleton PhysicsConstraints, multiplied by this scale factor.
/// Typical values are <c>Vector2.one</c> to apply XY movement 1:1,
/// <c>Vector2(2f, 2f)</c> to apply movement with double intensity,
/// <c>Vector2(1f, 0f)</c> to apply only horizontal movement, or
/// <c>Vector2.zero</c> to not apply any Transform position movement at all.</summary>
public Vector2 PhysicsPositionInheritanceFactor {
get {
return physicsPositionInheritanceFactor;
}
set {
if (physicsPositionInheritanceFactor == Vector2.zero && value != Vector2.zero) ResetLastPosition();
physicsPositionInheritanceFactor = value;
}
}
/// <summary>When set to non-zero, Transform rotation movement is applied to skeleton PhysicsConstraints,
/// multiplied by this scale factor. Typical values are <c>1</c> to apply movement 1:1,
/// <c>2</c> to apply movement with double intensity, or
/// <c>0</c> to not apply any Transform rotation movement at all.</summary>
public float PhysicsRotationInheritanceFactor {
get {
return physicsRotationInheritanceFactor;
}
set {
if (physicsRotationInheritanceFactor == 0f && value != 0f) ResetLastRotation();
physicsRotationInheritanceFactor = value;
}
}
/// <summary>
/// Limits Transform position movement in X and Y direction that is applied to skeleton PhysicsConstraints,
/// after it has been multiplied by <see cref="PhysicsPositionInheritanceFactor"/>.
/// </summary>
public Vector2 PhysicsPositionInheritanceLimit {
get { return physicsPositionInheritanceLimit; }
set { physicsPositionInheritanceLimit = value; }
}
/// <summary>
/// Limits Transform rotation movement that is applied to skeleton PhysicsConstraints,
/// after it has been multiplied by <see cref="PhysicsRotationInheritanceFactor"/>.
/// </summary>
public float PhysicsRotationInheritanceLimit {
get { return physicsRotationInheritanceLimit; }
set { physicsRotationInheritanceLimit = value; }
}
/// <summary>Reference transform relative to which physics movement will be calculated, or null to use world location.</summary>
public Transform PhysicsMovementRelativeTo {
get {
return physicsMovementRelativeTo;
}
set {
physicsMovementRelativeTo = value;
if (physicsPositionInheritanceFactor != Vector2.zero) ResetLastPosition();
if (physicsRotationInheritanceFactor != 0f) ResetLastRotation();
}
}
// Threaded Update System Properties
#if USE_THREADED_SKELETON_UPDATE
public bool IsUpdatedExternally {
get { return isUpdatedExternally; }
set { isUpdatedExternally = value; }
}
protected bool UsesThreadedMeshGeneration {
get { return threadedMeshGeneration == SettingsTriState.Enable || RuntimeSettings.UseThreadedMeshGeneration; }
}
public bool RequiresMeshBufferAssignmentMainThread {
get { return requiresMeshBufferAssignmentMainThread; }
}
public SettingsTriState ThreadedMeshGeneration {
get { return threadedMeshGeneration; }
set {
if (threadedMeshGeneration == value) return;
threadedMeshGeneration = value;
SkeletonUpdateSystem system = SkeletonUpdateSystem.Instance;
if (system) {
if (threadedMeshGeneration == SettingsTriState.Enable || RuntimeSettings.UseThreadedMeshGeneration)
system.RegisterForUpdate(this);
else
system.UnregisterFromUpdate(this);
}
}
}
#if UNITY_EDITOR
// ApplicationIsPlaying for threaded access. Unfortunately Application.isPlaying throws
// when called from worker thread.
public static bool ApplicationIsPlaying {
get { return applicationIsPlaying; }
set { applicationIsPlaying = value; }
}
#endif
#else // USE_THREADED_SKELETON_UPDATE
public bool IsUpdatedExternally {
get { return false; }
set { }
}
#if UNITY_EDITOR
public static bool ApplicationIsPlaying {
get { return Application.isPlaying; }
set { }
}
#endif
public bool RequiresMeshBufferAssignmentMainThread { get { return true; } }
#endif // USE_THREADED_SKELETON_UPDATE
#if UNITY_EDITOR
/// <summary>Enable this parameter when overwriting the Skeleton's skin from an editor script.
/// Otherwise any changes will be overwritten by the next inspector update.</summary>
public bool EditorSkipSkinSync {
get { return editorSkipSkinSync; }
set { editorSkipSkinSync = value; }
}
#endif
#endregion ISkeletonRenderer Properties
#region ISkeletonRenderer Methods
protected virtual void ClearCommon () {
// Note: do not reset meshFilter.sharedMesh or meshRenderer.sharedMaterial to null,
// otherwise constant reloading will be triggered at prefabs.
currentInstructions.Clear();
rendererBuffers.Clear();
ClearMeshGenerator();
skeleton = null;
valid = false;
if (skeletonAnimation != null)
skeletonAnimation.ClearAnimationState();
}
protected virtual void InitializeCommon (bool overwrite, bool quiet = false) {
if (valid && !overwrite)
return;
if (skeletonDataAsset == null || skeletonDataAsset.skeletonJSON == null) {
Clear();
return;
} else if (skeleton != null && skeletonDataAsset.GetSkeletonData(true) != skeleton.Data) {
Clear();
} else if (this.Freeze) {
return;
} else {
ClearCommon();
}
SkeletonData skeletonData = skeletonDataAsset.GetSkeletonData(quiet);
if (skeletonData == null) return;
rendererBuffers.Initialize();
skeleton = new Skeleton(skeletonData) {
ScaleX = initialFlipX ? -1 : 1,
ScaleY = initialFlipY ? -1 : 1
};
valid = true;
ResetLastPositionAndRotation();
AssignInitialSkin();
separatorSlots.Clear();
for (int i = 0; i < separatorSlotNames.Length; i++)
separatorSlots.Add(skeleton.FindSlot(separatorSlotNames[i]));
AfterAnimationApplied();
wasMeshUpdatedAfterInit = false;
if (OnRebuild != null)
OnRebuild(this);
#if UNITY_EDITOR
if (!Application.isPlaying) {
string errorMessage = null;
if (!quiet && MaterialChecks.IsMaterialSetupProblematic(this, ref errorMessage))
Debug.LogWarningFormat(this, "Problematic material setup at {0}: {1}", this.name, errorMessage);
}
#endif
}
protected virtual void AssignInitialSkin () {
if (string.IsNullOrEmpty(initialSkinName) || string.Equals(initialSkinName, "default", System.StringComparison.Ordinal))
skeleton.SetSkin((Skin)null);
else
skeleton.SetSkin(initialSkinName);
}
public void ResetLastPosition () {
lastPosition = GetPhysicsTransformPosition();
}
public void ResetLastRotation () {
lastRotation = GetPhysicsTransformRotation();
}
public void ResetLastPositionAndRotation () {
lastPosition = GetPhysicsTransformPosition();
lastRotation = GetPhysicsTransformRotation();
}
/// <summary>
/// Gathers Transform movement for later application in <see cref="ApplyTransformMovementToPhysics"/>.
/// Must be called in main thread.
/// </summary>
public virtual void GatherTransformMovementForPhysics () {
#if UNITY_EDITOR
bool isPlaying = ApplicationIsPlaying;
#else
bool isPlaying = true;
#endif
if (isPlaying) {
if (physicsPositionInheritanceFactor != Vector2.zero) {
Vector3 position = GetPhysicsTransformPosition();
positionDelta = (position - lastPosition) / this.MeshScale;
positionDelta = transform.InverseTransformVector(positionDelta);
if (physicsMovementRelativeTo != null) {
positionDelta = physicsMovementRelativeTo.TransformVector(positionDelta);
}
positionDelta.x *= physicsPositionInheritanceFactor.x;
positionDelta.y *= physicsPositionInheritanceFactor.y;
positionDelta.x = Mathf.Clamp(positionDelta.x, -physicsPositionInheritanceLimit.x, physicsPositionInheritanceLimit.x);
positionDelta.y = Mathf.Clamp(positionDelta.y, -physicsPositionInheritanceLimit.y, physicsPositionInheritanceLimit.y);
lastPosition = position;
}
if (physicsRotationInheritanceFactor != 0f) {
float rotation = GetPhysicsTransformRotation();
rotationDelta = rotation - lastRotation;
if (rotationDelta > 180f) rotationDelta -= 360f;
else if (rotationDelta < -180f) rotationDelta += 360f;
rotationDelta = Mathf.Clamp(rotationDelta, -physicsRotationInheritanceLimit, physicsRotationInheritanceLimit);
lastRotation = rotation;
}
}
}
/// <summary>
/// Applies position and rotation Transform movement previously gathered via
/// <see cref="GatherTransformMovementForPhysics"/>. May be called in worker thread.
/// </summary>
public virtual void ApplyTransformMovementToPhysics () {
if (physicsPositionInheritanceFactor != Vector2.zero) {
skeleton.PhysicsTranslate(positionDelta.x, positionDelta.y);
}
if (physicsRotationInheritanceFactor != 0f) {
skeleton.PhysicsRotate(0, 0, physicsRotationInheritanceFactor * rotationDelta);
}
}
protected Vector3 GetPhysicsTransformPosition () {
if (physicsMovementRelativeTo == null) {
return transform.position;
} else {
if (physicsMovementRelativeTo == transform.parent)
return transform.localPosition;
else
return physicsMovementRelativeTo.InverseTransformPoint(transform.position);
}
}
protected float GetPhysicsTransformRotation () {
if (physicsMovementRelativeTo == null) {
return this.transform.rotation.eulerAngles.z;
} else {
if (physicsMovementRelativeTo == this.transform.parent)
return this.transform.localRotation.eulerAngles.z;
else {
Quaternion relative = Quaternion.Inverse(physicsMovementRelativeTo.rotation) * this.transform.rotation;
return relative.eulerAngles.z;
}
}
}
public virtual void AfterAnimationApplied (bool calledFromMainThread = true) {
if (_UpdateLocal != null)
_UpdateLocal(this);
if (_UpdateWorld == null) {
UpdateWorldTransform(Physics.Update);
} else {
UpdateWorldTransform(Physics.Pose);
_UpdateWorld(this);
UpdateWorldTransform(Physics.Update);
}
if (calledFromMainThread && _UpdateComplete != null) {
_UpdateComplete(this);
}
}
public CoroutineIterator AfterAnimationAppliedSplit (CoroutineIterator coroutineIterator) {
if (coroutineIterator.IsDone)
return CoroutineIterator.Done;
/*
0:
if (_UpdateLocal != null) {
yield return true; // continue in main thread
1:
_UpdateLocal(this);
yield return false; // continue in worker thread
}
2:
if (_UpdateWorld == null) {
UpdateWorldTransform(Physics.Update);
// goto 5
} else {
UpdateWorldTransform(Physics.Pose);
yield return true; // continue in main thread
3:
_UpdateWorld(this);
yield return false; // continue in worker thread
4:
UpdateWorldTransform(Physics.Update);
// goto 5
}
5:
if (_UpdateComplete != null) {
yield return true; // continue in main thread
6:
_UpdateComplete(this);
// last call, no need to switch back to worker thread.
}
*/
const int StateBits = 3;
const uint StateMask = (1 << StateBits) - 1;
switch (coroutineIterator.State(StateMask)) {
case 0:
if (_UpdateLocal != null) {
AssertIsWorkerThread();
return coroutineIterator.YieldReturnAtState(1, StateMask);
} else {
goto case 2;
}
case 1:
AssertIsMainThread();
_UpdateLocal(this);
return coroutineIterator.YieldReturnAtState(2, StateMask);
case 2:
if (_UpdateWorld == null) {
AssertIsWorkerThread();
UpdateWorldTransform(Physics.Update);
goto case 5;
} else {
AssertIsWorkerThread();
UpdateWorldTransform(Physics.Pose);
return coroutineIterator.YieldReturnAtState(3, StateMask);
}
case 3:
AssertIsMainThread();
_UpdateWorld(this);
return coroutineIterator.YieldReturnAtState(4, StateMask);
case 4:
AssertIsWorkerThread();
UpdateWorldTransform(Physics.Update);
goto case 5;
case 5:
if (_UpdateComplete != null) {
AssertIsWorkerThread();
return coroutineIterator.YieldReturnAtState(6, StateMask);
} else {
return CoroutineIterator.Done;
}
case 6:
AssertIsMainThread();
_UpdateComplete(this);
return CoroutineIterator.Done;
default:
Debug.LogError(string.Format(
"Internal coroutine logic error: SkeletonRenderer.AfterAnimationAppliedSplit state was {0}.",
coroutineIterator.State(StateMask)), this);
return CoroutineIterator.Done;
}
}
protected void IssueOnPostProcessVertices (MeshGeneratorBuffers buffers) {
if (OnPostProcessVertices != null)
OnPostProcessVertices.Invoke(buffers);
}
protected virtual void UpdateWorldTransform (Physics physics) {
skeleton.UpdateWorldTransform(physics);
}
public virtual void UpdateMesh (bool calledFromMainThread = true) {
#if USE_THREADED_SKELETON_UPDATE
bool canPrepareInstructions = calledFromMainThread || !NeedsMainThreadRendererPreparation;
#else
bool canPrepareInstructions = true;
#endif
if (canPrepareInstructions)
PrepareInstructionsAndRenderers();
updateTriangles = UpdateBuffersToInstructions(calledFromMainThread);
#if USE_THREADED_SKELETON_UPDATE
if (calledFromMainThread) {
requiresMeshBufferAssignmentMainThread = false;
UpdateMeshAndMaterialsToBuffers();
} else {
requiresMeshBufferAssignmentMainThread = true;
}
#else
UpdateMeshAndMaterialsToBuffers();
#endif
}
/// <returns>True if triangles (indices array) need to be updated.</returns>
public virtual bool UpdateBuffersToInstructions (bool calledFromMainThread = true) {
if (!valid || currentInstructions.rawVertexCount < 0) return false;
wasMeshUpdatedAfterInit = true;
if (this.generateMeshOverride != null) {
if (calledFromMainThread)
this.generateMeshOverride(currentInstructions);
if (disableRenderingOnOverride) return false;
}
ExposedList<SubmeshInstruction> workingSubmeshInstructions = currentInstructions.submeshInstructions;
MeshRendererBuffers.SmartMesh currentSmartMesh = rendererBuffers.GetNextMesh(); // Double-buffer for performance.
// Update vertex buffers based on vertices from the attachments and assign buffers to a target UnityEngine.Mesh.
bool updateTriangles = SkeletonRendererInstruction.GeometryNotEqual(currentInstructions, currentSmartMesh.instructionUsed, calledFromMainThread);
FillBuffersFromSubmeshInstructions(workingSubmeshInstructions, currentSmartMesh, updateTriangles);
return updateTriangles;
}
public virtual void UpdateMeshAndMaterialsToBuffers () {
ExposedList<SubmeshInstruction> workingSubmeshInstructions = currentInstructions.submeshInstructions;
MeshRendererBuffers.SmartMesh currentSmartMesh = rendererBuffers.GetCurrentMesh();
UpdateMeshAndMaterialsToBuffers(workingSubmeshInstructions, currentSmartMesh, updateTriangles);
}
protected virtual void UpdateMeshAndMaterialsToBuffers (
ExposedList<SubmeshInstruction> workingSubmeshInstructions, MeshRendererBuffers.SmartMesh currentSmartMesh,
bool updateTriangles) {
FillMeshFromBuffers(currentSmartMesh, updateTriangles);
bool materialsChanged;
rendererBuffers.GatherMaterialsFromInstructions(workingSubmeshInstructions, out materialsChanged);
if (materialsChanged || materialsNeedUpdate) {
UpdateUsedMaterialsForRenderers(workingSubmeshInstructions);
}
#if SPINE_OPTIONAL_ON_DEMAND_LOADING
if (Application.isPlaying)
HandleOnDemandLoading();
#endif
// The UnityEngine.Mesh is ready. Set it as the MeshFilter's mesh. Store the instructions used for that mesh.
AssignMeshAtRenderer(workingSubmeshInstructions, currentSmartMesh);
if (OnMeshAndMaterialsUpdated != null)
OnMeshAndMaterialsUpdated(this);
}
public virtual void UpdateMaterials () {
UpdateUsedMaterialsForRenderers(currentInstructions.submeshInstructions);
}
// Threading Asserts
[System.Diagnostics.Conditional("UNITY_EDITOR")]
protected void InitializeMainThreadID () {
#if USE_THREADED_SKELETON_UPDATE
if (mainThreadID == -1)
mainThreadID = System.Threading.Thread.CurrentThread.ManagedThreadId;
#endif
}
[System.Diagnostics.Conditional("UNITY_EDITOR")]
private void AssertIsMainThread () {
#if USE_THREADED_SKELETON_UPDATE
if (System.Threading.Thread.CurrentThread.ManagedThreadId != mainThreadID)
Debug.LogError("AssertIsMainThread failed: worker thread calling main thread code. Thread ID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
#endif
}
[System.Diagnostics.Conditional("UNITY_EDITOR")]
private void AssertIsWorkerThread () {
#if USE_THREADED_SKELETON_UPDATE
if (System.Threading.Thread.CurrentThread.ManagedThreadId == mainThreadID)
Debug.LogError("AssertIsWorkerThread failed: main thread calling worker thread code! Thread ID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
#endif
}
#endregion ISkeletonRenderer Methods
#region ISkeletonRenderer Events
protected event SkeletonRendererDelegate _UpdateLocal;
protected event SkeletonRendererDelegate _UpdateWorld;
protected event SkeletonRendererDelegate _UpdateComplete;
/// <summary>
/// Occurs after the animations are applied and before world space values are resolved.
/// Use this callback when you want to set bone local values.
/// </summary>
public event SkeletonRendererDelegate UpdateLocal { add { _UpdateLocal += value; } remove { _UpdateLocal -= value; } }
/// <summary>
/// Occurs after the Skeleton's bone world space values are resolved (including all constraints).
/// Using this callback will cause the world space values to be solved an extra time.
/// Use this callback if want to use bone world space values, and also set bone local values.</summary>
public event SkeletonRendererDelegate UpdateWorld { add { _UpdateWorld += value; } remove { _UpdateWorld -= value; } }
/// <summary>
/// Occurs after the Skeleton's bone world space values are resolved (including all constraints).
/// Use this callback if you want to use bone world space values, but don't intend to modify bone local values.
/// This callback can also be used when setting world position and the bone matrix.</summary>
public event SkeletonRendererDelegate UpdateComplete { add { _UpdateComplete += value; } remove { _UpdateComplete -= value; } }
/// <summary> Occurs after the vertex data is populated every frame, before the vertices are pushed into the mesh.</summary>
public event Spine.Unity.MeshGeneratorDelegate OnPostProcessVertices;
/// <summary>OnRebuild is raised after the Skeleton is successfully initialized.</summary>
public event SkeletonRendererDelegate OnRebuild;
/// <summary>OnInstructionsPrepared is raised at the end of <c>LateUpdate</c> after render instructions
/// are done, target renderers are prepared, and the mesh is ready to be generated.</summary>
public event InstructionDelegate OnInstructionsPrepared;
#endregion ISkeletonRenderer Events
#endregion Identical common ISkeletonRenderer code
// End of identical code shared by ISkeletonRenderer subclasses as a workaround for single inheritance limitations.
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 9178134ad83033c48a2e3dec43d8d200
timeCreated: 1754676176
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,908 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2026, 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
#if UNITY_2018_1_OR_NEWER
#define PER_MATERIAL_PROPERTY_BLOCKS
#endif
#if UNITY_2017_1_OR_NEWER
#define BUILT_IN_SPRITE_MASK_COMPONENT
#endif
#if UNITY_2019_3_OR_NEWER
#define CONFIGURABLE_ENTER_PLAY_MODE
#endif
#if UNITY_2020_1_OR_NEWER
#define REVERT_HAS_OVERLOADS
#endif
#if !SPINE_DISABLE_THREADING
#define USE_THREADED_SKELETON_UPDATE
#endif
#if !SPINE_AUTO_UPGRADE_COMPONENTS_OFF
#define AUTO_UPGRADE_TO_43_COMPONENTS
#endif
#define SPINE_OPTIONAL_ON_DEMAND_LOADING
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
#if UNITY_EDITOR
using UnityEditor.SceneManagement;
#endif
namespace Spine.Unity {
[DefaultExecutionOrder(1)]
/// <summary>
/// Component for managing and rendering a Spine skeleton using a standard MeshRenderer.
/// </summary>
#if NEW_PREFAB_SYSTEM
[ExecuteAlways]
#else
[ExecuteInEditMode]
#endif
[RequireComponent(typeof(MeshRenderer)), DisallowMultipleComponent]
[HelpURL("https://esotericsoftware.com/spine-unity-main-components#SkeletonRenderer-Component")]
public partial class SkeletonRenderer : MonoBehaviour, ISkeletonRenderer, IHasSkeletonRenderer, IUpgradable {
// Note: Partial class. As a workaround for single inheritance limitations, this class
// is split into a common code part (what would be the base class) and a specific code part.
// This file covers specific attributes, properties and methods.
// Common ISkeletonRenderer members can be found in SkeletonRenderer.Common.cs.
#region SkeletonRenderer specific code
#region Events
/// <summary>OnMeshAndMaterialsUpdated is called at the end of LateUpdate after the Mesh and
/// all materials have been updated.</summary>
public event SkeletonRendererDelegate OnMeshAndMaterialsUpdated;
#endregion Events
#region Attributes
// Component References
MeshRenderer meshRenderer;
MeshFilter meshFilter;
// Mesh Generation
/// <summary>If true, the renderer assumes the skeleton only requires one Material and one submesh to render. This allows the MeshGenerator to skip checking for changes in Materials. Enable this as an optimization if the skeleton only uses one Material.</summary>
/// <remarks>This disables SkeletonRenderSeparator functionality.</remarks>
public bool singleSubmesh = false;
#if USE_THREADED_SKELETON_UPDATE
protected MeshGenerator meshGenerator = null;
#else
protected readonly MeshGenerator meshGenerator = new MeshGenerator();
#endif
[System.NonSerialized] readonly MeshRendererBuffers rendererBuffers = new MeshRendererBuffers();
[System.NonSerialized] readonly Dictionary<Material, Material> customMaterialOverride = new Dictionary<Material, Material>();
#if PER_MATERIAL_PROPERTY_BLOCKS
/// <summary> Applies only when 3+ submeshes are used (2+ materials with alternating order, e.g. "A B A").
/// If true, GPU instancing is disabled at all materials and MaterialPropertyBlocks are assigned at each
/// material to prevent aggressive batching of submeshes by e.g. the LWRP renderer, leading to incorrect
/// draw order (e.g. "A1 B A2" changed to "A1A2 B").
/// You can disable this parameter when everything is drawn correctly to save the additional performance cost.
/// </summary>
public bool fixDrawOrder = false;
#endif
#if BUILT_IN_SPRITE_MASK_COMPONENT
/// <seealso cref="MaskInteraction"/>
[SerializeField] protected SpriteMaskInteraction maskInteraction = SpriteMaskInteraction.None;
/// <summary>Cached reference to the already setup material override set at the respective
/// SkeletonDataAsset.atlasAssets array entry.</summary>
[System.NonSerialized] public MaterialOverrideSet[] insideMaskMaterials = null;
[System.NonSerialized] public MaterialOverrideSet[] outsideMaskMaterials = null;
/// <summary>Shader property ID used for the Stencil comparison function.</summary>
public static readonly int STENCIL_COMP_PARAM_ID = Shader.PropertyToID("_StencilComp");
/// <summary>Shader property value used as Stencil comparison function for <see cref="SpriteMaskInteraction.None"/>.</summary>
public const UnityEngine.Rendering.CompareFunction STENCIL_COMP_MASKINTERACTION_NONE = UnityEngine.Rendering.CompareFunction.Always;
/// <summary>Shader property value used as Stencil comparison function for <see cref="SpriteMaskInteraction.VisibleInsideMask"/>.</summary>
public const UnityEngine.Rendering.CompareFunction STENCIL_COMP_MASKINTERACTION_VISIBLE_INSIDE = UnityEngine.Rendering.CompareFunction.LessEqual;
/// <summary>Shader property value used as Stencil comparison function for <see cref="SpriteMaskInteraction.VisibleOutsideMask"/>.</summary>
public const UnityEngine.Rendering.CompareFunction STENCIL_COMP_MASKINTERACTION_VISIBLE_OUTSIDE = UnityEngine.Rendering.CompareFunction.Greater;
public const string MATERIAL_OVERRIDE_SET_INSIDE_MASK_NAME = "InsideMask";
public const string MATERIAL_OVERRIDE_SET_OUTSIDE_MASK_NAME = "OutsideMask";
#if UNITY_EDITOR
private static bool haveStencilParametersBeenFixed = false;
#endif
#endif // #if BUILT_IN_SPRITE_MASK_COMPONENT
#if PER_MATERIAL_PROPERTY_BLOCKS
private MaterialPropertyBlock reusedPropertyBlock;
public static readonly int SUBMESH_DUMMY_PARAM_ID = Shader.PropertyToID("_Submesh");
#endif
#if UNITY_EDITOR
/// <summary>Sets the MeshFilter's hide flags to DontSaveInEditor which fixes the prefab
/// always being marked as changed, but at the cost of references to the MeshFilter by other
/// components being lost.</summary>
public SettingsTriState fixPrefabOverrideViaMeshFilter = SettingsTriState.UseGlobalSetting;
public static bool fixPrefabOverrideViaMeshFilterGlobal = false;
#endif
#endregion Attributes
#region Properties
#region General Properties
#if BUILT_IN_SPRITE_MASK_COMPONENT
/// <summary>This enum controls the mode under which the sprite will interact with the masking system.</summary>
/// <remarks>Interaction modes with <see cref="UnityEngine.SpriteMask"/> components are identical to Unity's <see cref="UnityEngine.SpriteRenderer"/>,
/// see https://docs.unity3d.com/ScriptReference/SpriteMaskInteraction.html. </remarks>
public SpriteMaskInteraction MaskInteraction {
set {
if (maskInteraction == value) return;
maskInteraction = value;
materialsNeedUpdate = true;
}
get { return maskInteraction; }
}
#endif
/// <summary>Use this Dictionary to override a Material with a different Material.</summary>
public Dictionary<Material, Material> CustomMaterialOverride {
get { materialsNeedUpdate = true; return customMaterialOverride; }
}
/// <summary>Returns the <see cref="SkeletonClipping"/> used by this renderer for use with e.g.
/// <see cref="Skeleton.GetBounds(out float, out float, out float, out float, ref float[], SkeletonClipping)"/>
/// </summary>
public SkeletonClipping SkeletonClipping { get { return meshGenerator.SkeletonClipping; } }
public bool Freeze { get { return false; } }
public float MeshScale { get { return 1; } }
public Vector2 MeshOffset { get { return Vector2.zero; } }
protected bool UsesSingleSubmesh { get { return singleSubmesh; } }
protected bool NeedsToGenerateMesh { get { return (meshRenderer && meshRenderer.enabled) || HasGenerateMeshOverride; } }
#if USE_THREADED_SKELETON_UPDATE
public virtual bool NeedsMainThreadRendererPreparation {
get { return generateMeshOverride != null || OnInstructionsPrepared != null; }
}
#endif
#endregion General Properties
#region Render Settings Compatibility Properties
public float zSpacing { get { return meshSettings.zSpacing; } set { meshSettings.zSpacing = value; } }
/// <summary>Use Spine's clipping feature. If false, ClippingAttachments will be ignored.</summary>
public bool useClipping { get { return meshSettings.useClipping; } set { meshSettings.useClipping = value; } }
/// <summary>If true, triangles will not be updated. Enable this as an optimization if the skeleton does not make use of attachment swapping or hiding, or draw order keys. Otherwise, setting this to false may cause errors in rendering.</summary>
public bool immutableTriangles { get { return meshSettings.immutableTriangles; } set { meshSettings.immutableTriangles = value; } }
/// <summary>Multiply vertex color RGB with vertex color alpha. Set this to true if the shader used for rendering is a premultiplied alpha shader. Setting this to false disables single-batch additive slots.</summary>
public bool pmaVertexColors { get { return meshSettings.pmaVertexColors; } set { meshSettings.pmaVertexColors = value; } }
/// <summary>If true, second colors on slots will be added to the output Mesh as UV2 and UV3. A special "tint black" shader that interprets UV2 and UV3 as black point colors is required to render this properly.</summary>
public bool tintBlack { get { return meshSettings.tintBlack; } set { meshSettings.tintBlack = value; } }
/// <summary>If true, the mesh generator adds normals to the output mesh. For better performance and reduced memory requirements, use a shader that assumes the desired normal.</summary>
public bool addNormals { get { return meshSettings.addNormals; } set { meshSettings.addNormals = value; } }
/// <summary>If true, tangents are calculated every frame and added to the Mesh. Enable this when using a shader that uses lighting that requires tangents.</summary>
public bool calculateTangents { get { return meshSettings.calculateTangents; } set { meshSettings.calculateTangents = value; } }
#endregion Render Settings Compatibility Properties
#endregion Properties
#region Methods
#region Lifecycle
public virtual void Awake () {
InitializeMainThreadID();
#if UNITY_EDITOR
SkeletonRenderer.ApplicationIsPlaying = Application.isPlaying;
#endif
MeshGenerator.InitializeGlobalSettings();
#if USE_THREADED_SKELETON_UPDATE
if (meshGenerator == null)
meshGenerator = new MeshGenerator();
#endif
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
if (!Application.isPlaying && !wasDeprecatedTransferred) {
UpgradeTo43();
}
#endif
#if UNITY_EDITOR && BUILT_IN_SPRITE_MASK_COMPONENT
EditorFixStencilCompParameters();
#endif
Initialize(false);
if (generateMeshOverride == null || !disableRenderingOnOverride)
updateMode = updateWhenInvisible;
}
#if UNITY_EDITOR && CONFIGURABLE_ENTER_PLAY_MODE
public virtual void Start () {
Initialize(false);
}
#endif
/// <summary>
/// Initialize this component. Attempts to load the SkeletonData and creates the internal Skeleton object and buffers.</summary>
/// <param name="overwrite">If set to <c>true</c>, it will overwrite internal objects if they were already generated. Otherwise, the initialized component will ignore subsequent calls to initialize.</param>
public virtual void Initialize (bool overwrite, bool quiet = false) {
if (valid && !overwrite)
return;
skeletonAnimation = this.GetComponent<ISkeletonAnimation>();
if (skeletonAnimation != null)
skeletonAnimation.EnsureRendererEventsSubscribed();
#if UNITY_EDITOR
if (BuildUtilities.IsInSkeletonAssetBuildPreProcessing)
return;
#endif
meshFilter = GetComponent<MeshFilter>();
if (meshFilter == null)
meshFilter = gameObject.AddComponent<MeshFilter>();
meshRenderer = GetComponent<MeshRenderer>();
InitializeCommon(overwrite, quiet);
#if UNITY_EDITOR
requiresEditorUpdate = false;
#endif
}
public void Clear () {
ClearMeshAtRenderer();
ClearCommon();
}
/// <summary>
/// Clears the previously generated mesh and resets the skeleton's pose.
/// Also clears the animation state when a SkeletonAnimation component is
/// associated with this SkeletonRenderer</summary>
public virtual void ClearState () {
ClearSkeletonState();
skeletonAnimation.ClearAnimationState();
}
/// <summary>
/// Clears the previously generated mesh and resets the skeleton's pose.</summary>
public virtual void ClearSkeletonState () {
ClearMeshAtRenderer();
currentInstructions.Clear();
if (skeleton != null) skeleton.SetupPose();
}
#if UNITY_EDITOR || USE_THREADED_SKELETON_UPDATE
void OnEnable () {
#if UNITY_EDITOR
if (!Application.isPlaying) {
LateUpdate();
}
#endif
#if USE_THREADED_SKELETON_UPDATE
if (Application.isPlaying && UsesThreadedMeshGeneration && !isUpdatedExternally) {
SkeletonUpdateSystem system = SkeletonUpdateSystem.Instance;
if (system)
system.RegisterForUpdate(this);
}
#endif
}
#endif // UNITY_EDITOR || USE_THREADED_SKELETON_UPDATE
void OnDisable () {
#if USE_THREADED_SKELETON_UPDATE
if (Application.isPlaying && UsesThreadedMeshGeneration) {
SkeletonUpdateSystem system = SkeletonUpdateSystem.Instance;
if (system)
system.UnregisterFromUpdate(this);
}
#endif
if (clearStateOnDisable && valid)
ClearState();
}
void OnDestroy () {
rendererBuffers.Dispose();
valid = false;
}
public void OnBecameVisible () {
UpdateMode previousUpdateMode = updateMode;
updateMode = UpdateMode.FullUpdate;
// OnBecameVisible is called after Update and LateUpdate(),
// so update if previousUpdateMode didn't already update this frame.
if (previousUpdateMode != UpdateMode.FullUpdate) {
if (skeletonAnimation != null)
skeletonAnimation.OnBecameVisibleFromMode(previousUpdateMode);
LateUpdate();
}
}
public void OnBecameInvisible () {
updateMode = updateWhenInvisible;
}
public void LateUpdate () {
#if USE_THREADED_SKELETON_UPDATE
if (isUpdatedExternally) return;
#endif
LateUpdateImplementation();
}
#endregion Lifecycle
#region Mesh Generation
/// <summary>Applies MeshGenerator settings to the SkeletonRenderer and its internal MeshGenerator.</summary>
public void SetMeshSettings (MeshGenerator.Settings settings) {
meshSettings = settings;
}
protected void ClearMeshGenerator () {
#if USE_THREADED_SKELETON_UPDATE
if (meshGenerator == null)
meshGenerator = new MeshGenerator();
#endif
meshGenerator.Begin();
}
protected Mesh GetCurrentMesh () {
return rendererBuffers.GetCurrentMesh().mesh;
}
protected bool RequiresMultipleSubmeshesByDrawOrder () {
if (!valid)
return false;
return MeshGenerator.RequiresMultipleSubmeshesByDrawOrder(skeleton);
}
public void PrepareInstructionsAndRenderers () {
if (UsesSingleSubmesh) {
MeshGenerator.GenerateSingleSubmeshInstruction(currentInstructions, skeleton, skeletonDataAsset.atlasAssets[0].PrimaryMaterial);
} else {
GenerateSkeletonRendererInstructions();
}
if (OnInstructionsPrepared != null)
OnInstructionsPrepared(this.currentInstructions);
}
protected virtual void GenerateSkeletonRendererInstructions () {
MeshGenerator.GenerateSkeletonRendererInstruction(currentInstructions, skeleton, customSlotMaterials,
enableSeparatorSlots ? separatorSlots : null,
enableSeparatorSlots ? separatorSlots.Count > 0 : false,
meshSettings.immutableTriangles);
}
protected virtual bool FillBuffersFromSubmeshInstructions (ExposedList<SubmeshInstruction> workingSubmeshInstructions,
MeshRendererBuffers.SmartMesh currentSmartMesh, bool updateTriangles) {
return FillSingleBufferFromInstructions(workingSubmeshInstructions, currentSmartMesh, updateTriangles);
}
/// <returns>True if any mesh has been filled with data, false otherwise.</returns>
protected virtual bool FillSingleBufferFromInstructions (ExposedList<SubmeshInstruction> workingSubmeshInstructions,
MeshRendererBuffers.SmartMesh currentSmartMesh, bool updateTriangles) {
meshGenerator.settings = meshSettings;
meshGenerator.Begin();
if (!currentInstructions.hasActiveClipping)
meshGenerator.BuildMeshWithArrays(currentInstructions, updateTriangles);
else if (UsesSingleSubmesh)
meshGenerator.AddSubmesh(workingSubmeshInstructions.Items[0], updateTriangles);
else
meshGenerator.BuildMesh(currentInstructions, updateTriangles);
currentSmartMesh.instructionUsed.Set(currentInstructions);
IssueOnPostProcessVertices(meshGenerator.Buffers);
return true;
}
protected virtual bool FillMeshFromBuffers (MeshRendererBuffers.SmartMesh currentSmartMesh, bool updateTriangles) {
Mesh currentMesh = currentSmartMesh.mesh;
meshGenerator.FillVertexData(currentMesh);
if (updateTriangles) { // Check if the triangles should also be updated.
meshGenerator.FillTriangles(currentMesh);
}
meshGenerator.FillLateVertexData(currentMesh);
return true;
}
#endregion Mesh Generation
#region Renderer Assignment
protected void EnableRenderers () {
if (meshRenderer)
meshRenderer.enabled = true;
}
protected void DisableRenderers () {
if (meshRenderer)
meshRenderer.enabled = false;
}
protected virtual void AssignMeshAtRenderer (ExposedList<SubmeshInstruction> workingSubmeshInstructions,
MeshRendererBuffers.SmartMesh currentSmartMesh) {
Mesh currentMesh = currentSmartMesh.mesh;
AssignMeshAtRenderer(currentMesh);
}
protected virtual void AssignMeshAtRenderer (UnityEngine.Mesh mesh) {
if (meshFilter != null)
meshFilter.sharedMesh = mesh;
if (meshRenderer != null) {
meshRenderer.sharedMaterials = rendererBuffers.sharedMaterials;
}
#if PER_MATERIAL_PROPERTY_BLOCKS
if (fixDrawOrder && meshRenderer.sharedMaterials.Length > 2) {
SetMaterialSettingsToFixDrawOrder();
}
#endif
}
protected void ClearMeshAtRenderer () {
if (meshFilter == null) meshFilter = GetComponent<MeshFilter>();
if (meshFilter != null) meshFilter.sharedMesh = null;
}
#endregion Renderer Assignment
#region Separator Slots
public void FindAndApplySeparatorSlots (string startsWith, bool clearExistingSeparators = true, bool updateStringArray = false) {
if (string.IsNullOrEmpty(startsWith)) return;
FindAndApplySeparatorSlots(
(slotName) => slotName.StartsWith(startsWith),
clearExistingSeparators,
updateStringArray
);
}
public void FindAndApplySeparatorSlots (System.Func<string, bool> slotNamePredicate, bool clearExistingSeparators = true, bool updateStringArray = false) {
if (slotNamePredicate == null) return;
if (!valid) return;
if (clearExistingSeparators)
separatorSlots.Clear();
ExposedList<Slot> slots = skeleton.Slots;
foreach (Slot slot in slots) {
if (slotNamePredicate.Invoke(slot.Data.Name))
separatorSlots.Add(slot);
}
if (updateStringArray) {
List<string> detectedSeparatorNames = new List<string>();
foreach (Slot slot in skeleton.Slots) {
string slotName = slot.Data.Name;
if (slotNamePredicate.Invoke(slotName))
detectedSeparatorNames.Add(slotName);
}
if (!clearExistingSeparators) {
string[] originalNames = separatorSlotNames;
foreach (string originalName in originalNames)
detectedSeparatorNames.Add(originalName);
}
separatorSlotNames = detectedSeparatorNames.ToArray();
}
}
public void ReapplySeparatorSlotNames () {
if (!valid)
return;
separatorSlots.Clear();
for (int i = 0, n = separatorSlotNames.Length; i < n; i++) {
Slot slot = skeleton.FindSlot(separatorSlotNames[i]);
if (slot != null) {
separatorSlots.Add(slot);
}
#if UNITY_EDITOR
else if (!string.IsNullOrEmpty(separatorSlotNames[i])) {
Debug.LogWarning(separatorSlotNames[i] + " is not a slot in " + skeletonDataAsset.skeletonJSON.name);
}
#endif
}
}
#endregion Separator Slots
#region Material Configuration
protected virtual void UpdateUsedMaterialsForRenderers (ExposedList<SubmeshInstruction> instructions) {
rendererBuffers.UpdateSharedMaterialsArray();
ConfigureMaterials(instructions);
materialsNeedUpdate = false;
}
public virtual void ConfigureMaterials (Material[] sharedMaterials, ExposedList<SubmeshInstruction> instructions) {
if (customMaterialOverride.Count > 0) {
for (int i = 0, count = sharedMaterials.Length; i < count; ++i) {
Material material = sharedMaterials[i];
if (material == null) continue;
Material overrideMaterial;
if (customMaterialOverride.TryGetValue(material, out overrideMaterial))
sharedMaterials[i] = overrideMaterial;
}
}
#if BUILT_IN_SPRITE_MASK_COMPONENT
if (maskInteraction == SpriteMaskInteraction.VisibleInsideMask) {
if (insideMaskMaterials == null || insideMaskMaterials.Length == 0) {
InitSpriteMaskMaterialsMaskMode(ref insideMaskMaterials, this,
MATERIAL_OVERRIDE_SET_INSIDE_MASK_NAME, STENCIL_COMP_MASKINTERACTION_VISIBLE_INSIDE);
if (insideMaskMaterials == null) return;
}
foreach (MaterialOverrideSet overrideSet in insideMaskMaterials) {
overrideSet.ApplyOverrideTo(sharedMaterials);
}
} else if (maskInteraction == SpriteMaskInteraction.VisibleOutsideMask) {
if (outsideMaskMaterials == null || outsideMaskMaterials.Length == 0) {
InitSpriteMaskMaterialsMaskMode(ref outsideMaskMaterials, this,
MATERIAL_OVERRIDE_SET_OUTSIDE_MASK_NAME, STENCIL_COMP_MASKINTERACTION_VISIBLE_OUTSIDE);
if (outsideMaskMaterials == null) return;
}
foreach (MaterialOverrideSet overrideSet in outsideMaskMaterials) {
overrideSet.ApplyOverrideTo(sharedMaterials);
}
}
#endif
}
protected virtual void ConfigureMaterials (ExposedList<SubmeshInstruction> instructions) {
ConfigureMaterials(rendererBuffers.sharedMaterials, instructions);
}
#if BUILT_IN_SPRITE_MASK_COMPONENT
private void InitSpriteMaskMaterialsMaskMode (ref MaterialOverrideSet[] maskMaterials,
SkeletonRenderer skeletonRenderer, string overrideSetName,
UnityEngine.Rendering.CompareFunction maskFunction) {
AtlasAssetBase[] atlasAssets = skeletonRenderer.skeletonDataAsset.atlasAssets;
int atlasAssetCount = atlasAssets.Length;
if (maskMaterials == null || maskMaterials.Length != atlasAssetCount)
maskMaterials = new MaterialOverrideSet[atlasAssetCount];
for (int i = 0, n = atlasAssetCount; i < n; ++i) {
AtlasAssetBase atlasAsset = atlasAssets[i];
maskMaterials[i] = atlasAsset.GetMaterialOverrideSet(overrideSetName);
if (maskMaterials[i] == null) {
#if UNITY_EDITOR
// Editor script shall create assets.
if (!Application.isPlaying) {
maskMaterials = null;
return;
}
#endif
maskMaterials[i] = InitSpriteMaskOverrideSet(
atlasAsset, overrideSetName, maskFunction);
}
}
}
private static MaterialOverrideSet InitSpriteMaskOverrideSet (
AtlasAssetBase atlasAsset, string overrideSetName, UnityEngine.Rendering.CompareFunction maskFunction) {
MaterialOverrideSet overrideSet = atlasAsset.AddMaterialOverrideSet(overrideSetName);
foreach (Material originalMaterial in atlasAsset.Materials) {
if (originalMaterial == null)
continue;
Material maskMaterial = new Material(originalMaterial);
maskMaterial.name += overrideSetName;
maskMaterial.SetFloat(SkeletonRenderer.STENCIL_COMP_PARAM_ID, (int)maskFunction);
overrideSet.AddOverride(originalMaterial, maskMaterial);
}
return overrideSet;
}
#endif //#if BUILT_IN_SPRITE_MASK_COMPONENT
#if PER_MATERIAL_PROPERTY_BLOCKS
/// <summary>
/// This method was introduced as a workaround for too aggressive submesh draw call batching,
/// leading to incorrect draw order when 3+ materials are used at submeshes in alternating order.
/// Otherwise, e.g. when using Lightweight Render Pipeline, deliberately separated draw calls
/// "A1 B A2" are reordered to "A1A2 B", regardless of batching-related project settings.
/// </summary>
private void SetMaterialSettingsToFixDrawOrder () {
if (reusedPropertyBlock == null) reusedPropertyBlock = new MaterialPropertyBlock();
bool hasPerRendererBlock = meshRenderer.HasPropertyBlock();
if (hasPerRendererBlock) {
meshRenderer.GetPropertyBlock(reusedPropertyBlock);
}
for (int i = 0; i < meshRenderer.sharedMaterials.Length; ++i) {
if (!meshRenderer.sharedMaterials[i])
continue;
if (!hasPerRendererBlock) meshRenderer.GetPropertyBlock(reusedPropertyBlock, i);
// Note: this parameter shall not exist at any shader, then Unity will create separate
// material instances (not in terms of memory cost or leakage).
reusedPropertyBlock.SetFloat(SUBMESH_DUMMY_PARAM_ID, i);
meshRenderer.SetPropertyBlock(reusedPropertyBlock, i);
meshRenderer.sharedMaterials[i].enableInstancing = false;
}
}
#endif
#if SPINE_OPTIONAL_ON_DEMAND_LOADING
void HandleOnDemandLoading () {
foreach (AtlasAssetBase atlasAsset in skeletonDataAsset.atlasAssets) {
if (atlasAsset.TextureLoadingMode != AtlasAssetBase.LoadingMode.Normal) {
atlasAsset.BeginCustomTextureLoading();
Material[] materials = rendererBuffers.sharedMaterials;
for (int i = 0, count = materials.Length; i < count; ++i) {
Material overrideMaterial = null;
atlasAsset.RequireTexturesLoaded(materials[i], ref overrideMaterial);
if (overrideMaterial != null)
materials[i] = overrideMaterial;
}
atlasAsset.EndCustomTextureLoading();
}
}
}
#endif
#endregion Material Configuration
#region Runtime Instantiation
public static SkeletonComponents<Renderer, Animation> NewSpineGameObject<Renderer, Animation> (
SkeletonDataAsset skeletonDataAsset, bool quiet = false)
where Renderer : MonoBehaviour, ISkeletonRenderer
where Animation : SkeletonAnimationBase {
return SkeletonRenderer.AddSpineComponents<Renderer, Animation>(new GameObject("New Spine GameObject"), skeletonDataAsset, quiet);
}
/// <summary>Add and prepare a Spine animation component and the default
/// SkeletonRenderer components to a GameObject at runtime.</summary>
/// <typeparam name="Animation">Animation should be SkeletonAnimation, SkeletonMecanim or any custom derived class.</typeparam>
public static SkeletonComponents<Renderer, Animation> AddSpineComponents<Renderer, Animation> (
GameObject gameObject, SkeletonDataAsset skeletonDataAsset, bool quiet = false)
where Renderer : MonoBehaviour, ISkeletonRenderer
where Animation : SkeletonAnimationBase {
Renderer rendererComponent = gameObject.AddComponent<Renderer>();
if (skeletonDataAsset != null) {
rendererComponent.SkeletonDataAsset = skeletonDataAsset;
rendererComponent.Initialize(false, quiet);
}
Animation animationComponent = gameObject.AddComponent<Animation>();
if (skeletonDataAsset != null) {
animationComponent.Initialize(false, quiet);
}
rendererComponent.Animation = animationComponent;
return new SkeletonComponents<Renderer, Animation>(rendererComponent, animationComponent);
}
#endregion Runtime Instantiation
#region Internal Methods
#if USE_THREADED_SKELETON_UPDATE
public virtual void MainThreadPrepareLateUpdateInternal () {
if (!valid) return;
if (updateMode != UpdateMode.FullUpdate && wasMeshUpdatedAfterInit) return;
if (NeedsMainThreadRendererPreparation)
PrepareInstructionsAndRenderers();
if (generateMeshOverride != null)
generateMeshOverride(currentInstructions);
}
#else
public virtual void MainThreadPrepareLateUpdateInternal () {
}
#endif
/// <summary>
/// Generates a new UnityEngine.Mesh from the internal Skeleton.</summary>
public virtual void LateUpdateImplementation (bool calledFromMainThread = true) {
if (calledFromMainThread) {
#if UNITY_EDITOR && NEW_PREFAB_SYSTEM
// Don't store mesh or material at the prefab, otherwise it will permanently reload
UnityEditor.PrefabAssetType prefabType = UnityEditor.PrefabUtility.GetPrefabAssetType(this);
if (UnityEditor.PrefabUtility.IsPartOfPrefabAsset(this) &&
(prefabType == UnityEditor.PrefabAssetType.Regular || prefabType == UnityEditor.PrefabAssetType.Variant)) {
return;
}
EditorUpdateMeshFilterHideFlags();
#endif
#if UNITY_EDITOR
if (!Application.isPlaying && requiresEditorUpdate) {
Initialize(true);
}
#endif
}
if (!valid) return;
// instantiation can happen from Update() after this component, leading to a missing Update() call.
if (calledFromMainThread && skeletonAnimation != null)
skeletonAnimation.UpdateOncePerFrame(0);
// Generate mesh once, required to update mesh bounds for visibility
if (updateMode != UpdateMode.FullUpdate && wasMeshUpdatedAfterInit) return;
if (calledFromMainThread && !NeedsToGenerateMesh) return;
UpdateMesh(calledFromMainThread);
}
#endregion Internal Methods
#region Editor Methods
#if UNITY_EDITOR
protected void OnValidate () {
requiresEditorUpdate = true;
}
// revert each prefab override only once each editor-frame.
private static int lastPrefabRevertFrame = -1;
private static HashSet<MeshFilter> revertedPrefabMeshes = new HashSet<MeshFilter>();
private static bool preventReentrance = false;
public void EditorUpdateMeshFilterHideFlags () {
if (!meshFilter) {
meshFilter = GetComponent<MeshFilter>();
if (meshFilter == null)
meshFilter = gameObject.AddComponent<MeshFilter>();
}
bool dontSaveInEditor = false;
if (fixPrefabOverrideViaMeshFilter == SettingsTriState.Enable ||
(fixPrefabOverrideViaMeshFilter == SettingsTriState.UseGlobalSetting &&
fixPrefabOverrideViaMeshFilterGlobal))
dontSaveInEditor = true;
if (dontSaveInEditor) {
#if NEW_PREFAB_SYSTEM
int currentFrame = Time.frameCount;
if (lastPrefabRevertFrame != currentFrame) {
lastPrefabRevertFrame = currentFrame;
revertedPrefabMeshes.Clear();
}
if (!preventReentrance && UnityEditor.PrefabUtility.IsPartOfAnyPrefab(meshFilter)) {
if (!revertedPrefabMeshes.Contains(meshFilter)) {
GameObject instanceRoot = UnityEditor.PrefabUtility.GetOutermostPrefabInstanceRoot(meshFilter);
if (instanceRoot != null) {
UnityEditor.PropertyModification[] mods = UnityEditor.PrefabUtility.GetPropertyModifications(instanceRoot);
bool hasMeshOverride = false;
if (mods != null) {
foreach (var mod in mods) {
if (mod.target == meshFilter && mod.propertyPath == "m_Mesh") {
hasMeshOverride = true;
break;
}
}
}
if (hasMeshOverride) {
preventReentrance = true;
try {
List<ObjectOverride> objectOverrides = UnityEditor.PrefabUtility.GetObjectOverrides(instanceRoot);
foreach (ObjectOverride objectOverride in objectOverrides) {
if (objectOverride.instanceObject == meshFilter) {
#if REVERT_HAS_OVERLOADS
objectOverride.Revert(UnityEditor.InteractionMode.AutomatedAction);
#else
objectOverride.Revert();
#endif
revertedPrefabMeshes.Add(meshFilter);
break;
}
}
} finally {
preventReentrance = false;
}
}
}
}
}
#endif
meshFilter.hideFlags = HideFlags.DontSaveInEditor;
} else {
meshFilter.hideFlags = HideFlags.None;
}
}
private void EditorFixStencilCompParameters () {
if (!haveStencilParametersBeenFixed && HasAnyStencilComp0Material()) {
haveStencilParametersBeenFixed = true;
FixAllProjectMaterialsStencilCompParameters();
}
}
private void FixAllProjectMaterialsStencilCompParameters () {
string[] materialGUIDS = UnityEditor.AssetDatabase.FindAssets("t:material");
foreach (string guid in materialGUIDS) {
string path = UnityEditor.AssetDatabase.GUIDToAssetPath(guid);
if (!string.IsNullOrEmpty(path)) {
Material mat = UnityEditor.AssetDatabase.LoadAssetAtPath<Material>(path);
if (mat.HasProperty(STENCIL_COMP_PARAM_ID) && mat.GetFloat(STENCIL_COMP_PARAM_ID) == 0) {
mat.SetFloat(STENCIL_COMP_PARAM_ID, (int)STENCIL_COMP_MASKINTERACTION_NONE);
}
}
}
UnityEditor.AssetDatabase.Refresh();
UnityEditor.AssetDatabase.SaveAssets();
}
private bool HasAnyStencilComp0Material () {
if (meshRenderer == null)
return false;
foreach (Material mat in meshRenderer.sharedMaterials) {
if (mat != null && mat.HasProperty(STENCIL_COMP_PARAM_ID)) {
float currentCompValue = mat.GetFloat(STENCIL_COMP_PARAM_ID);
if (currentCompValue == 0)
return true;
}
}
return false;
}
#endif // UNITY_EDITOR
#endregion Editor Methods
#region Transfer of Deprecated Fields
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
public void UpgradeTo43 () {
TransferDeprecatedFields();
}
protected virtual void TransferDeprecatedFields () {
wasDeprecatedTransferred = true;
// only transfer once, let SkeletonAnimation transfer properties if present
if (this.Animation != null) return;
meshSettings.zSpacing = this.zSpacingDeprecated;
meshSettings.useClipping = this.useClippingDeprecated;
meshSettings.immutableTriangles = this.immutableTrianglesDeprecated;
meshSettings.pmaVertexColors = this.pmaVertexColorsDeprecated;
meshSettings.tintBlack = this.tintBlackDeprecated;
meshSettings.addNormals = this.addNormalsDeprecated;
meshSettings.calculateTangents = this.calculateTangentsDeprecated;
}
[SerializeField] protected bool wasDeprecatedTransferred = false;
[FormerlySerializedAs("zSpacing")] [SerializeField] private float zSpacingDeprecated = 0f;
[FormerlySerializedAs("useClipping")] [SerializeField] private bool useClippingDeprecated = true;
[FormerlySerializedAs("immutableTriangles")] [SerializeField] private bool immutableTrianglesDeprecated = false;
[FormerlySerializedAs("pmaVertexColors")] [SerializeField] private bool pmaVertexColorsDeprecated = true;
[FormerlySerializedAs("tintBlack")] [SerializeField] private bool tintBlackDeprecated = false;
[FormerlySerializedAs("calculateNormals"),
FormerlySerializedAs("addNormals")]
[SerializeField] private bool addNormalsDeprecated = false;
[FormerlySerializedAs("calculateTangents")] [SerializeField] private bool calculateTangentsDeprecated = false;
#endif // UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
#endregion Transfer of Deprecated Fields
#endregion Methods
#endregion SkeletonRenderer specific code
}
public struct SkeletonComponents<Renderer, Animation> {
public Renderer skeletonRenderer;
public Animation skeletonAnimation;
public SkeletonComponents (Renderer skeletonRenderer, Animation skeletonAnimation) {
this.skeletonRenderer = skeletonRenderer;
this.skeletonAnimation = skeletonAnimation;
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: e075b9a3e08e2f74fbd651c858ab16ed
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: a7236dbdc6a4e5a4989483dac97aee0b
folderAsset: yes
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,269 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2026, 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;
using System.Collections.Generic;
using UnityEngine;
namespace Spine.Unity {
#if NEW_PREFAB_SYSTEM
[ExecuteAlways]
#else
[ExecuteInEditMode]
#endif
[HelpURL("https://esotericsoftware.com/spine-unity-utility-components#SkeletonGraphicCustomMaterials")]
public class SkeletonGraphicCustomMaterials : MonoBehaviour {
#region Inspector
public SkeletonGraphic skeletonGraphic;
[SerializeField] protected List<SlotMaterialOverride> customSlotMaterials = new List<SlotMaterialOverride>();
[SerializeField] protected List<AtlasMaterialOverride> customMaterialOverrides = new List<AtlasMaterialOverride>();
[SerializeField] protected List<AtlasTextureOverride> customTextureOverrides = new List<AtlasTextureOverride>();
#if UNITY_EDITOR
void Reset () {
skeletonGraphic = GetComponent<SkeletonGraphic>();
// Populate material list
if (skeletonGraphic != null && skeletonGraphic.skeletonDataAsset != null) {
AtlasAssetBase[] atlasAssets = skeletonGraphic.skeletonDataAsset.atlasAssets;
List<AtlasMaterialOverride> initialAtlasMaterialOverrides = new List<AtlasMaterialOverride>();
foreach (AtlasAssetBase atlasAsset in atlasAssets) {
foreach (Material atlasMaterial in atlasAsset.Materials) {
AtlasMaterialOverride atlasMaterialOverride = new AtlasMaterialOverride {
overrideEnabled = false,
originalTexture = atlasMaterial.mainTexture
};
initialAtlasMaterialOverrides.Add(atlasMaterialOverride);
}
}
customMaterialOverrides = initialAtlasMaterialOverrides;
}
// Populate texture list
if (skeletonGraphic != null && skeletonGraphic.skeletonDataAsset != null) {
AtlasAssetBase[] atlasAssets = skeletonGraphic.skeletonDataAsset.atlasAssets;
List<AtlasTextureOverride> initialAtlasTextureOverrides = new List<AtlasTextureOverride>();
foreach (AtlasAssetBase atlasAsset in atlasAssets) {
foreach (Material atlasMaterial in atlasAsset.Materials) {
AtlasTextureOverride atlasTextureOverride = new AtlasTextureOverride {
overrideEnabled = false,
originalTexture = atlasMaterial.mainTexture
};
initialAtlasTextureOverrides.Add(atlasTextureOverride);
}
}
customTextureOverrides = initialAtlasTextureOverrides;
}
}
#endif
#endregion
void SetCustomSlotMaterials () {
if (skeletonGraphic == null) {
Debug.LogError("skeletonRenderer == null");
return;
}
for (int i = 0; i < customSlotMaterials.Count; i++) {
SlotMaterialOverride slotMaterialOverride = customSlotMaterials[i];
if (!slotMaterialOverride.overrideEnabled || string.IsNullOrEmpty(slotMaterialOverride.slotName))
continue;
Slot slotObject = skeletonGraphic.skeleton.FindSlot(slotMaterialOverride.slotName);
if (slotObject != null)
skeletonGraphic.CustomSlotMaterials[slotObject] = slotMaterialOverride.material;
}
}
void RemoveCustomSlotMaterials () {
if (skeletonGraphic == null) {
Debug.LogError("skeletonRenderer == null");
return;
}
for (int i = 0; i < customSlotMaterials.Count; i++) {
SlotMaterialOverride slotMaterialOverride = customSlotMaterials[i];
if (string.IsNullOrEmpty(slotMaterialOverride.slotName))
continue;
Slot slotObject = skeletonGraphic.skeleton.FindSlot(slotMaterialOverride.slotName);
if (slotObject == null)
continue;
Material currentMaterial;
if (!skeletonGraphic.CustomSlotMaterials.TryGetValue(slotObject, out currentMaterial))
continue;
// Do not revert the material if it was changed by something else
if (currentMaterial != slotMaterialOverride.material)
continue;
skeletonGraphic.CustomSlotMaterials.Remove(slotObject);
}
}
void SetCustomMaterialOverrides () {
if (skeletonGraphic == null) {
Debug.LogError("skeletonGraphic == null");
return;
}
for (int i = 0; i < customMaterialOverrides.Count; i++) {
AtlasMaterialOverride atlasMaterialOverride = customMaterialOverrides[i];
if (atlasMaterialOverride.overrideEnabled)
skeletonGraphic.CustomMaterialOverride[atlasMaterialOverride.originalTexture] = atlasMaterialOverride.replacementMaterial;
}
}
void RemoveCustomMaterialOverrides () {
if (skeletonGraphic == null) {
Debug.LogError("skeletonGraphic == null");
return;
}
for (int i = 0; i < customMaterialOverrides.Count; i++) {
AtlasMaterialOverride atlasMaterialOverride = customMaterialOverrides[i];
Material currentMaterial;
if (!skeletonGraphic.CustomMaterialOverride.TryGetValue(atlasMaterialOverride.originalTexture, out currentMaterial))
continue;
// Do not revert the material if it was changed by something else
if (currentMaterial != atlasMaterialOverride.replacementMaterial)
continue;
skeletonGraphic.CustomMaterialOverride.Remove(atlasMaterialOverride.originalTexture);
}
}
void SetCustomTextureOverrides () {
if (skeletonGraphic == null) {
Debug.LogError("skeletonGraphic == null");
return;
}
for (int i = 0; i < customTextureOverrides.Count; i++) {
AtlasTextureOverride atlasTextureOverride = customTextureOverrides[i];
if (atlasTextureOverride.overrideEnabled)
skeletonGraphic.CustomTextureOverride[atlasTextureOverride.originalTexture] = atlasTextureOverride.replacementTexture;
}
}
void RemoveCustomTextureOverrides () {
if (skeletonGraphic == null) {
Debug.LogError("skeletonGraphic == null");
return;
}
for (int i = 0; i < customTextureOverrides.Count; i++) {
AtlasTextureOverride atlasTextureOverride = customTextureOverrides[i];
Texture currentTexture;
if (!skeletonGraphic.CustomTextureOverride.TryGetValue(atlasTextureOverride.originalTexture, out currentTexture))
continue;
// Do not revert the material if it was changed by something else
if (currentTexture != atlasTextureOverride.replacementTexture)
continue;
skeletonGraphic.CustomTextureOverride.Remove(atlasTextureOverride.originalTexture);
}
}
// OnEnable applies the overrides at runtime, and when the editor loads.
void OnEnable () {
if (skeletonGraphic == null)
skeletonGraphic = GetComponent<SkeletonGraphic>();
if (skeletonGraphic == null) {
Debug.LogError("skeletonGraphic == null");
return;
}
skeletonGraphic.Initialize(false);
SetCustomSlotMaterials();
SetCustomMaterialOverrides();
SetCustomTextureOverrides();
}
// OnDisable removes the overrides at runtime, and in the editor when the component is disabled or destroyed.
void OnDisable () {
if (skeletonGraphic == null) {
Debug.LogError("skeletonGraphic == null");
return;
}
RemoveCustomMaterialOverrides();
RemoveCustomTextureOverrides();
RemoveCustomSlotMaterials();
}
[Serializable]
public struct AtlasMaterialOverride : IEquatable<AtlasMaterialOverride> {
public bool overrideEnabled;
public Texture originalTexture;
public Material replacementMaterial;
public bool Equals (AtlasMaterialOverride other) {
return overrideEnabled == other.overrideEnabled && originalTexture == other.originalTexture && replacementMaterial == other.replacementMaterial;
}
}
[Serializable]
public struct AtlasTextureOverride : IEquatable<AtlasTextureOverride> {
public bool overrideEnabled;
public Texture originalTexture;
public Texture replacementTexture;
public bool Equals (AtlasTextureOverride other) {
return overrideEnabled == other.overrideEnabled && originalTexture == other.originalTexture && replacementTexture == other.replacementTexture;
}
}
[Serializable]
public struct SlotMaterialOverride : IEquatable<SlotMaterialOverride> {
public bool overrideEnabled;
[SpineSlot]
public string slotName;
public Material material;
public bool Equals (SlotMaterialOverride other) {
return overrideEnabled == other.overrideEnabled && slotName == other.slotName && material == other.material;
}
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 6c8717e10b272bf42b05d363ac2679a6
timeCreated: 1588789074
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,239 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2026, 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
#if !SPINE_AUTO_UPGRADE_COMPONENTS_OFF
#define AUTO_UPGRADE_TO_43_COMPONENTS
#endif
// Contributed by: Lost Polygon
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
namespace Spine.Unity {
#if NEW_PREFAB_SYSTEM
[ExecuteAlways]
#else
[ExecuteInEditMode]
#endif
[HelpURL("https://esotericsoftware.com/spine-unity-utility-components#SkeletonRendererCustomMaterials")]
public class SkeletonRendererCustomMaterials : MonoBehaviour, IUpgradable {
#region Inspector
public SkeletonRenderer skeletonRenderer;
[SerializeField] protected List<SlotMaterialOverride> customSlotMaterials = new List<SlotMaterialOverride>();
[SerializeField] protected List<AtlasMaterialOverride> customMaterialOverrides = new List<AtlasMaterialOverride>();
#if UNITY_EDITOR
#if AUTO_UPGRADE_TO_43_COMPONENTS
public void Awake () {
if (!Application.isPlaying && !wasUpgradedTo43) {
UpgradeTo43();
}
}
#endif
void Reset () {
skeletonRenderer = GetComponent<SkeletonRenderer>();
// Populate atlas list
if (skeletonRenderer != null && skeletonRenderer.skeletonDataAsset != null) {
AtlasAssetBase[] atlasAssets = skeletonRenderer.skeletonDataAsset.atlasAssets;
List<AtlasMaterialOverride> initialAtlasMaterialOverrides = new List<AtlasMaterialOverride>();
foreach (AtlasAssetBase atlasAsset in atlasAssets) {
foreach (Material atlasMaterial in atlasAsset.Materials) {
AtlasMaterialOverride atlasMaterialOverride = new AtlasMaterialOverride {
overrideDisabled = true,
originalMaterial = atlasMaterial
};
initialAtlasMaterialOverrides.Add(atlasMaterialOverride);
}
}
customMaterialOverrides = initialAtlasMaterialOverrides;
}
}
#endif
#endregion
void SetCustomSlotMaterials () {
if (skeletonRenderer == null) {
Debug.LogError("skeletonRenderer == null");
return;
}
for (int i = 0; i < customSlotMaterials.Count; i++) {
SlotMaterialOverride slotMaterialOverride = customSlotMaterials[i];
if (slotMaterialOverride.overrideDisabled || string.IsNullOrEmpty(slotMaterialOverride.slotName))
continue;
Slot slotObject = skeletonRenderer.skeleton.FindSlot(slotMaterialOverride.slotName);
if (slotObject != null)
skeletonRenderer.CustomSlotMaterials[slotObject] = slotMaterialOverride.material;
}
}
void RemoveCustomSlotMaterials () {
if (skeletonRenderer == null) {
Debug.LogError("skeletonRenderer == null");
return;
}
for (int i = 0; i < customSlotMaterials.Count; i++) {
SlotMaterialOverride slotMaterialOverride = customSlotMaterials[i];
if (string.IsNullOrEmpty(slotMaterialOverride.slotName))
continue;
Slot slotObject = skeletonRenderer.skeleton.FindSlot(slotMaterialOverride.slotName);
if (slotObject == null)
continue;
Material currentMaterial;
if (!skeletonRenderer.CustomSlotMaterials.TryGetValue(slotObject, out currentMaterial))
continue;
// Do not revert the material if it was changed by something else
if (currentMaterial != slotMaterialOverride.material)
continue;
skeletonRenderer.CustomSlotMaterials.Remove(slotObject);
}
}
void SetCustomMaterialOverrides () {
if (skeletonRenderer == null) {
Debug.LogError("skeletonRenderer == null");
return;
}
for (int i = 0; i < customMaterialOverrides.Count; i++) {
AtlasMaterialOverride atlasMaterialOverride = customMaterialOverrides[i];
if (atlasMaterialOverride.overrideDisabled)
continue;
skeletonRenderer.CustomMaterialOverride[atlasMaterialOverride.originalMaterial] = atlasMaterialOverride.replacementMaterial;
}
}
void RemoveCustomMaterialOverrides () {
if (skeletonRenderer == null) {
Debug.LogError("skeletonRenderer == null");
return;
}
for (int i = 0; i < customMaterialOverrides.Count; i++) {
AtlasMaterialOverride atlasMaterialOverride = customMaterialOverrides[i];
Material currentMaterial;
if (!skeletonRenderer.CustomMaterialOverride.TryGetValue(atlasMaterialOverride.originalMaterial, out currentMaterial))
continue;
// Do not revert the material if it was changed by something else
if (currentMaterial != atlasMaterialOverride.replacementMaterial)
continue;
skeletonRenderer.CustomMaterialOverride.Remove(atlasMaterialOverride.originalMaterial);
}
}
// OnEnable applies the overrides at runtime, and when the editor loads.
void OnEnable () {
if (skeletonRenderer == null)
skeletonRenderer = GetComponent<SkeletonRenderer>();
if (skeletonRenderer == null) {
Debug.LogError("skeletonRenderer == null");
return;
}
skeletonRenderer.Initialize(false);
SetCustomMaterialOverrides();
SetCustomSlotMaterials();
}
// OnDisable removes the overrides at runtime, and in the editor when the component is disabled or destroyed.
void OnDisable () {
if (skeletonRenderer == null) {
Debug.LogError("skeletonRenderer == null");
return;
}
RemoveCustomMaterialOverrides();
RemoveCustomSlotMaterials();
}
[Serializable]
public struct SlotMaterialOverride : IEquatable<SlotMaterialOverride> {
public bool overrideDisabled;
[SpineSlot]
public string slotName;
public Material material;
public bool Equals (SlotMaterialOverride other) {
return overrideDisabled == other.overrideDisabled && slotName == other.slotName && material == other.material;
}
}
[Serializable]
public struct AtlasMaterialOverride : IEquatable<AtlasMaterialOverride> {
public bool overrideDisabled;
public Material originalMaterial;
public Material replacementMaterial;
public bool Equals (AtlasMaterialOverride other) {
return overrideDisabled == other.overrideDisabled && originalMaterial == other.originalMaterial && replacementMaterial == other.replacementMaterial;
}
}
#region Transfer of Deprecated Fields
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
public virtual void UpgradeTo43 () {
wasUpgradedTo43 = true;
if (skeletonRenderer == null) {
Component previousReference = previousSkeletonRenderer != null ? previousSkeletonRenderer : this;
skeletonRenderer = previousReference.GetComponent<SkeletonRenderer>();
if (skeletonRenderer == null)
Debug.LogError("Please manually re-assign SkeletonRenderer at SkeletonRendererCustomMaterials, " +
"automatic upgrade failed.", this);
}
}
[SerializeField, HideInInspector, FormerlySerializedAs("skeletonRenderer")] Component previousSkeletonRenderer;
[SerializeField] protected bool wasUpgradedTo43 = false;
#endif
#endregion
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 26947ae098a8447408d80c0c86e35b48
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,5 @@
fileFormatVersion: 2
guid: f6e0caaafe294de48af468a6a9321473
folderAsset: yes
DefaultImporter:
userData:
@@ -0,0 +1,122 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2026, 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 !SPINE_AUTO_UPGRADE_COMPONENTS_OFF
#define AUTO_UPGRADE_TO_43_COMPONENTS
#endif
using UnityEngine;
using UnityEngine.Serialization;
namespace Spine.Unity {
/// <summary>
/// Utility component to support flipping of 2D hinge chains (chains of HingeJoint2D objects) along
/// with the parent skeleton by activating the respective mirrored versions of the hinge chain.
/// Note: This component is automatically attached when calling "Create Hinge Chain 2D" at <see cref="SkeletonUtilityBone"/>,
/// do not attempt to use this component for other purposes.
/// </summary>
public class ActivateBasedOnFlipDirection : MonoBehaviour, IUpgradable {
public SkeletonRenderer skeletonRenderer;
public SkeletonGraphic skeletonGraphic;
public GameObject activeOnNormalX;
public GameObject activeOnFlippedX;
HingeJoint2D[] jointsNormalX;
HingeJoint2D[] jointsFlippedX;
ISkeletonComponent skeletonComponent;
bool wasFlippedXBefore = false;
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
protected void Awake () {
if (!Application.isPlaying && !wasUpgradedTo43) {
UpgradeTo43();
}
}
#endif
private void Start () {
jointsNormalX = activeOnNormalX.GetComponentsInChildren<HingeJoint2D>();
jointsFlippedX = activeOnFlippedX.GetComponentsInChildren<HingeJoint2D>();
skeletonComponent = skeletonRenderer != null ? (ISkeletonComponent)skeletonRenderer : (ISkeletonComponent)skeletonGraphic;
}
private void FixedUpdate () {
bool isFlippedX = (skeletonComponent.Skeleton.ScaleX < 0);
if (isFlippedX != wasFlippedXBefore) {
HandleFlip(isFlippedX);
}
wasFlippedXBefore = isFlippedX;
}
void HandleFlip (bool isFlippedX) {
GameObject gameObjectToActivate = isFlippedX ? activeOnFlippedX : activeOnNormalX;
GameObject gameObjectToDeactivate = isFlippedX ? activeOnNormalX : activeOnFlippedX;
gameObjectToActivate.SetActive(true);
gameObjectToDeactivate.SetActive(false);
ResetJointPositions(isFlippedX ? jointsFlippedX : jointsNormalX);
ResetJointPositions(isFlippedX ? jointsNormalX : jointsFlippedX);
CompensateMovementAfterFlipX(gameObjectToActivate.transform, gameObjectToDeactivate.transform);
}
void ResetJointPositions (HingeJoint2D[] joints) {
for (int i = 0; i < joints.Length; ++i) {
HingeJoint2D joint = joints[i];
Transform parent = joint.connectedBody.transform;
joint.transform.position = parent.TransformPoint(joint.connectedAnchor);
}
}
void CompensateMovementAfterFlipX (Transform toActivate, Transform toDeactivate) {
Transform targetLocation = toDeactivate.GetChild(0);
Transform currentLocation = toActivate.GetChild(0);
toActivate.position += targetLocation.position - currentLocation.position;
}
#region Transfer of Deprecated Fields
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
public virtual void UpgradeTo43 () {
wasUpgradedTo43 = true;
if (skeletonRenderer == null && skeletonGraphic == null) {
Component previousReference = previousSkeletonRenderer != null ? previousSkeletonRenderer : this;
skeletonRenderer = previousReference.GetComponent<SkeletonRenderer>();
if (skeletonRenderer == null)
Debug.LogError("Please manually re-assign SkeletonRenderer at ActivateBasedOnFlipDirection, " +
"automatic upgrade failed.", this);
}
}
[SerializeField, HideInInspector, FormerlySerializedAs("skeletonRenderer")] Component previousSkeletonRenderer;
[SerializeField] protected bool wasUpgradedTo43 = false;
#endif
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 70ae96e4f2feb654681a2f16e4effeec
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,54 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2026, 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.
*****************************************************************************/
using UnityEngine;
namespace Spine.Unity {
/// <summary>
/// Utility component to support flipping of hinge chains (chains of HingeJoint objects) along with the parent skeleton.
///
/// Note: This component is automatically attached when calling "Create Hinge Chain" at <see cref="SkeletonUtilityBone"/>.
/// </summary>
[RequireComponent(typeof(Rigidbody))]
public class FollowLocationRigidbody : MonoBehaviour {
public Transform reference;
Rigidbody ownRigidbody;
private void Awake () {
ownRigidbody = this.GetComponent<Rigidbody>();
}
void FixedUpdate () {
ownRigidbody.rotation = reference.rotation;
ownRigidbody.position = reference.position;
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 9fc20d5e917562341a5007777a9d0db2
timeCreated: 1571763023
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,58 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2026, 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.
*****************************************************************************/
using UnityEngine;
namespace Spine.Unity {
/// <summary>
/// Utility component to support flipping of hinge chains (chains of HingeJoint objects) along with the parent skeleton.
///
/// Note: This component is automatically attached when calling "Create Hinge Chain" at <see cref="SkeletonUtilityBone"/>.
/// </summary>
[RequireComponent(typeof(Rigidbody2D))]
public class FollowLocationRigidbody2D : MonoBehaviour {
public Transform reference;
public bool followFlippedX;
Rigidbody2D ownRigidbody;
private void Awake () {
ownRigidbody = this.GetComponent<Rigidbody2D>();
}
void FixedUpdate () {
if (followFlippedX) {
ownRigidbody.rotation = ((-reference.rotation.eulerAngles.z + 270f) % 360f) - 90f;
} else
ownRigidbody.rotation = reference.rotation.eulerAngles.z;
ownRigidbody.position = reference.position;
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 02aae87c39b869548a9051fbdb1975e6
timeCreated: 1572012493
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,86 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2026, 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.
*****************************************************************************/
using UnityEngine;
namespace Spine.Unity {
/// <summary>
/// Utility component to support flipping of hinge chains (chains of HingeJoint objects) along with the parent skeleton.
/// Note that flipping needs to be performed by 180 degree rotation at <see cref="SkeletonUtility"/>,
/// by setting <see cref="SkeletonUtility.flipBy180DegreeRotation"/> to true, not via negative scale.
///
/// Note: This component is automatically attached when calling "Create Hinge Chain" at <see cref="SkeletonUtilityBone"/>,
/// do not attempt to use this component for other purposes.
/// </summary>
public class FollowSkeletonUtilityRootRotation : MonoBehaviour {
const float FLIP_ANGLE_THRESHOLD = 100.0f;
public Transform reference;
Vector3 prevLocalEulerAngles;
private void Start () {
prevLocalEulerAngles = this.transform.localEulerAngles;
}
void FixedUpdate () {
this.transform.rotation = reference.rotation;
bool wasFlippedAroundY = Mathf.Abs(this.transform.localEulerAngles.y - prevLocalEulerAngles.y) > FLIP_ANGLE_THRESHOLD;
bool wasFlippedAroundX = Mathf.Abs(this.transform.localEulerAngles.x - prevLocalEulerAngles.x) > FLIP_ANGLE_THRESHOLD;
if (wasFlippedAroundY)
CompensatePositionToYRotation();
if (wasFlippedAroundX)
CompensatePositionToXRotation();
prevLocalEulerAngles = this.transform.localEulerAngles;
}
/// <summary>
/// Compensates the position so that a child at the reference position remains in the same place,
/// to counter any movement that occurred by rotation.
/// </summary>
void CompensatePositionToYRotation () {
Vector3 newPosition = reference.position + (reference.position - this.transform.position);
newPosition.y = this.transform.position.y;
this.transform.position = newPosition;
}
/// <summary>
/// Compensates the position so that a child at the reference position remains in the same place,
/// to counter any movement that occurred by rotation.
/// </summary>
void CompensatePositionToXRotation () {
Vector3 newPosition = reference.position + (reference.position - this.transform.position);
newPosition.x = this.transform.position.x;
this.transform.position = newPosition;
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 456a736ebb92ebf4b959fa9c4b704427
timeCreated: 1571763206
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,534 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2026, 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
#if UNITY_6000_0_OR_NEWER
#define USE_RIGIDBODY_BODY_TYPE
#endif
#if !SPINE_AUTO_UPGRADE_COMPONENTS_OFF
#define AUTO_UPGRADE_TO_43_COMPONENTS
#endif
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
namespace Spine.Unity {
#if NEW_PREFAB_SYSTEM
[ExecuteAlways]
#else
[ExecuteInEditMode]
#endif
[RequireComponent(typeof(ISkeletonRenderer))]
[HelpURL("https://esotericsoftware.com/spine-unity-utility-components#SkeletonUtility")]
public sealed class SkeletonUtility : MonoBehaviour, IUpgradable {
#region BoundingBoxAttachment
public static PolygonCollider2D AddBoundingBoxGameObject (Skeleton skeleton, string skinName, string slotName, string attachmentName, Transform parent, bool isTrigger = true) {
Skin skin = string.IsNullOrEmpty(skinName) ? skeleton.Data.DefaultSkin : skeleton.Data.FindSkin(skinName);
if (skin == null) {
Debug.LogError("Skin " + skinName + " not found!");
return null;
}
Slot slot = skeleton.FindSlot(slotName);
Attachment attachment = slot != null ? skin.GetAttachment(slot.Data.Index, attachmentName) : null;
if (attachment == null) {
Debug.LogFormat("Attachment in slot '{0}' named '{1}' not found in skin '{2}'.", slotName, attachmentName, skin.Name);
return null;
}
BoundingBoxAttachment box = attachment as BoundingBoxAttachment;
if (box != null) {
return AddBoundingBoxGameObject(box.Name, box, skeleton, slot, parent, isTrigger);
} else {
Debug.LogFormat("Attachment '{0}' was not a Bounding Box.", attachmentName);
return null;
}
}
public static PolygonCollider2D AddBoundingBoxGameObject (string name, BoundingBoxAttachment box, Skeleton skeleton, Slot slot, Transform parent, bool isTrigger = true) {
GameObject go = new GameObject("[BoundingBox]" + (string.IsNullOrEmpty(name) ? box.Name : name));
#if UNITY_EDITOR
if (!Application.isPlaying)
UnityEditor.Undo.RegisterCreatedObjectUndo(go, "Spawn BoundingBox");
#endif
Transform got = go.transform;
got.parent = parent;
got.localPosition = Vector3.zero;
got.localRotation = Quaternion.identity;
got.localScale = Vector3.one;
return AddBoundingBoxAsComponent(box, skeleton, slot, go, isTrigger);
}
public static PolygonCollider2D AddBoundingBoxAsComponent (BoundingBoxAttachment box, Skeleton skeleton, Slot slot, GameObject gameObject, bool isTrigger = true) {
if (box == null) return null;
PolygonCollider2D collider = gameObject.AddComponent<PolygonCollider2D>();
collider.isTrigger = isTrigger;
SetColliderPointsLocal(collider, skeleton, slot, box);
return collider;
}
public static void SetColliderPointsLocal (PolygonCollider2D collider, Skeleton skeleton, Slot slot, BoundingBoxAttachment box, float scale = 1.0f) {
if (box == null) return;
if (box.IsWeighted()) Debug.LogWarning("UnityEngine.PolygonCollider2D does not support weighted or animated points. Collider points will not be animated and may have incorrect orientation. If you want to use it as a collider, please remove weights and animations from the bounding box in Spine editor.");
Vector2[] verts = box.GetLocalVertices(skeleton, slot, null);
if (scale != 1.0f) {
for (int i = 0, n = verts.Length; i < n; ++i)
verts[i] *= scale;
}
collider.SetPath(0, verts);
}
public static Bounds GetBoundingBoxBounds (BoundingBoxAttachment boundingBox, float depth = 0) {
float[] floats = boundingBox.Vertices;
int floatCount = floats.Length;
Bounds bounds = new Bounds();
bounds.center = new Vector3(floats[0], floats[1], 0);
for (int i = 2; i < floatCount; i += 2)
bounds.Encapsulate(new Vector3(floats[i], floats[i + 1], 0));
Vector3 size = bounds.size;
size.z = depth;
bounds.size = size;
return bounds;
}
public static Rigidbody2D AddBoneRigidbody2D (GameObject gameObject, bool isKinematic = true, float gravityScale = 0f) {
Rigidbody2D rb = gameObject.GetComponent<Rigidbody2D>();
if (rb == null) {
rb = gameObject.AddComponent<Rigidbody2D>();
#if USE_RIGIDBODY_BODY_TYPE
rb.bodyType = isKinematic ? RigidbodyType2D.Kinematic : RigidbodyType2D.Dynamic;
#else
rb.isKinematic = isKinematic;
#endif
rb.gravityScale = gravityScale;
}
return rb;
}
#endregion
public delegate void SkeletonUtilityDelegate ();
public event SkeletonUtilityDelegate OnReset;
public Transform boneRoot;
/// <summary>
/// If true, <see cref="Skeleton.ScaleX"/> and <see cref="Skeleton.ScaleY"/> are followed
/// by 180 degree rotation. If false, negative Transform scale is used.
/// Note that using negative scale is consistent with previous behaviour (hence the default),
/// however causes serious problems with rigidbodies and physics. Therefore, it is recommended to
/// enable this parameter where possible. When creating hinge chains for a chain of skeleton bones
/// via <see cref="SkeletonUtilityBone"/>, it is mandatory to have <c>flipBy180DegreeRotation</c> enabled.
/// </summary>
public bool flipBy180DegreeRotation = false;
void Update () {
Skeleton skeleton = skeletonComponent.Skeleton;
if (skeleton != null && boneRoot != null) {
if (flipBy180DegreeRotation) {
boneRoot.localScale = new Vector3(Mathf.Abs(skeleton.ScaleX), Mathf.Abs(skeleton.ScaleY), 1f);
boneRoot.eulerAngles = new Vector3(skeleton.ScaleY > 0 ? 0 : 180,
skeleton.ScaleX > 0 ? 0 : 180,
0);
} else {
boneRoot.localScale = new Vector3(skeleton.ScaleX, skeleton.ScaleY, 1f);
}
}
if (skeletonGraphic != null) {
positionScale = skeletonGraphic.MeshScale;
lastPositionScale = positionScale;
if (boneRoot) {
positionOffset = skeletonGraphic.MeshOffset;
if (positionOffset != Vector2.zero) {
boneRoot.localPosition = positionOffset;
}
}
}
}
void UpdateToMeshScaleAndOffset (MeshGeneratorBuffers ignoredParameter) {
if (skeletonGraphic == null) return;
positionScale = skeletonGraphic.MeshScale;
if (boneRoot) {
positionOffset = skeletonGraphic.MeshOffset;
if (positionOffset != Vector2.zero) {
boneRoot.localPosition = positionOffset;
}
}
// Note: skeletonGraphic.MeshScale and MeshOffset can be one frame behind in Update() above.
// Unfortunately update order is:
// 1. SkeletonGraphic.Update updating skeleton bones and calling UpdateWorld callback,
// calling SkeletonUtilityBone.DoUpdate() reading hierarchy.PositionScale.
// 2. Layout change triggers SkeletonGraphic.Rebuild, updating MeshScale and MeshOffset.
// Thus to prevent a one-frame-behind offset after a layout change affecting mesh scale,
// we have to re-evaluate the callbacks via the lines below.
if (lastPositionScale != positionScale) {
UpdateLocal(skeletonGraphic);
UpdateWorld(skeletonGraphic);
UpdateComplete(skeletonGraphic);
}
}
[HideInInspector] public SkeletonRenderer skeletonRenderer;
[HideInInspector] public SkeletonGraphic skeletonGraphic;
private ISkeletonRenderer skeletonComponent;
[System.NonSerialized] public List<SkeletonUtilityBone> boneComponents = new List<SkeletonUtilityBone>();
[System.NonSerialized] public List<SkeletonUtilityConstraint> constraintComponents = new List<SkeletonUtilityConstraint>();
public ISkeletonComponent SkeletonComponent { get { return this.SkeletonRenderer; } }
public ISkeletonRenderer SkeletonRenderer {
get {
if (skeletonComponent == null) {
skeletonComponent = skeletonRenderer != null ? skeletonRenderer :
skeletonGraphic != null ? skeletonGraphic :
GetComponent<ISkeletonRenderer>();
}
return skeletonComponent;
}
}
public Skeleton Skeleton {
get {
if (SkeletonComponent == null)
return null;
return skeletonComponent.Skeleton;
}
}
public bool IsValid {
get {
ISkeletonRenderer skeletonComponent = this.SkeletonRenderer;
return (skeletonComponent != null && skeletonComponent.IsValid);
}
}
public float PositionScale { get { return positionScale; } }
public Vector2 PositionOffset { get { return positionOffset; } }
float positionScale = 1.0f;
float lastPositionScale = 1.0f;
Vector2 positionOffset = Vector2.zero;
bool hasOverrideBones;
bool hasConstraintTargetBones;
bool needToReprocessBones;
public void OnUtilityBoneChanged () {
needToReprocessBones = true;
}
public void ResubscribeEvents () {
ResubscribeIndependentEvents();
ResubscribeDependentEvents();
}
void ResubscribeIndependentEvents () {
ISkeletonRenderer skeletonComponent = this.SkeletonRenderer;
if (skeletonComponent != null) {
skeletonComponent.OnRebuild -= HandleRendererReset;
skeletonComponent.OnRebuild += HandleRendererReset;
}
if (skeletonGraphic != null) {
skeletonGraphic.OnPostProcessVertices -= UpdateToMeshScaleAndOffset;
skeletonGraphic.OnPostProcessVertices += UpdateToMeshScaleAndOffset;
}
}
void ResubscribeDependentEvents () {
ISkeletonRenderer skeletonComponent = this.SkeletonRenderer;
if (skeletonComponent != null) {
skeletonComponent.UpdateLocal -= UpdateLocal;
skeletonComponent.UpdateWorld -= UpdateWorld;
skeletonComponent.UpdateComplete -= UpdateComplete;
bool hasConstraintComponents = constraintComponents.Count > 0;
if (hasOverrideBones || !hasConstraintTargetBones)
skeletonComponent.UpdateLocal += UpdateLocal;
if (hasOverrideBones || hasConstraintComponents)
skeletonComponent.UpdateWorld += UpdateWorld;
if (hasConstraintTargetBones)
skeletonComponent.UpdateComplete += UpdateComplete;
}
}
void OnEnable () {
if (skeletonRenderer == null) {
skeletonRenderer = GetComponent<SkeletonRenderer>();
}
if (skeletonGraphic == null) {
skeletonGraphic = GetComponent<SkeletonGraphic>();
}
CollectBones();
ResubscribeEvents();
}
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
void Awake () {
if (!Application.isPlaying && !wasUpgradedTo43) {
UpgradeTo43();
}
}
#endif
void Start () {
//recollect because order of operations failure when switching between game mode and edit mode...
CollectBones();
}
void OnDisable () {
ISkeletonRenderer skeletonComponent = this.SkeletonRenderer;
if (skeletonComponent != null) {
skeletonComponent.OnRebuild -= HandleRendererReset;
skeletonComponent.UpdateLocal -= UpdateLocal;
skeletonComponent.UpdateWorld -= UpdateWorld;
skeletonComponent.UpdateComplete -= UpdateComplete;
}
if (skeletonGraphic) {
skeletonGraphic.OnPostProcessVertices -= UpdateToMeshScaleAndOffset;
}
}
void HandleRendererReset (ISkeletonRenderer r) {
if (OnReset != null) OnReset();
CollectBones();
}
public void RegisterBone (SkeletonUtilityBone bone) {
if (boneComponents.Contains(bone)) {
return;
} else {
boneComponents.Add(bone);
needToReprocessBones = true;
}
}
public void UnregisterBone (SkeletonUtilityBone bone) {
boneComponents.Remove(bone);
}
public void RegisterConstraint (SkeletonUtilityConstraint constraint) {
if (constraintComponents.Contains(constraint))
return;
else {
constraintComponents.Add(constraint);
needToReprocessBones = true;
}
}
public void UnregisterConstraint (SkeletonUtilityConstraint constraint) {
constraintComponents.Remove(constraint);
}
public void CollectBones () {
ISkeletonRenderer skeletonComponent = this.SkeletonRenderer;
if (skeletonComponent == null) return;
Skeleton skeleton = skeletonComponent.Skeleton;
if (skeleton == null) return;
if (boneRoot != null) {
hasOverrideBones = false;
hasConstraintTargetBones = false;
List<Bone> constrainedBones = new List<Bone>();
ExposedList<IConstraint> constraints = skeleton.Constraints;
for (int i = 0, n = constraints.Count; i < n; i++) {
IConstraint constraint = constraints.Items[i];
ExposedList<BonePose> bones = null;
if (constraint is IkConstraint)
bones = ((IkConstraint)constraint).Bones;
else if (constraint is TransformConstraint)
bones = ((TransformConstraint)constraint).Bones;
else if (constraint is PathConstraint)
bones = ((PathConstraint)constraint).Bones;
if (bones != null) {
for (int j = 0, m = bones.Count; j < m; j++)
constrainedBones.Add(bones.Items[j].bone);
}
}
List<SkeletonUtilityBone> boneComponents = this.boneComponents;
for (int i = 0, n = boneComponents.Count; i < n; i++) {
SkeletonUtilityBone b = boneComponents[i];
if (b.bone == null) {
b.DoUpdate(SkeletonUtilityBone.UpdatePhase.Local);
if (b.bone == null) continue;
}
hasOverrideBones |= (b.mode == SkeletonUtilityBone.Mode.Override);
hasConstraintTargetBones |= constrainedBones.Contains(b.bone);
}
needToReprocessBones = false;
} else {
boneComponents.Clear();
constraintComponents.Clear();
}
ResubscribeDependentEvents();
}
void UpdateLocal (ISkeletonRenderer skeletonRenderer) {
UpdateAllBones(SkeletonUtilityBone.UpdatePhase.Local);
}
void UpdateWorld (ISkeletonRenderer skeletonRenderer) {
UpdateAllBones(SkeletonUtilityBone.UpdatePhase.World);
for (int i = 0, n = constraintComponents.Count; i < n; i++)
constraintComponents[i].DoUpdate();
}
void UpdateComplete (ISkeletonRenderer skeletonRenderer) {
UpdateAllBones(SkeletonUtilityBone.UpdatePhase.Complete);
}
void UpdateAllBones (SkeletonUtilityBone.UpdatePhase phase) {
if (boneRoot == null || needToReprocessBones)
CollectBones();
List<SkeletonUtilityBone> boneComponents = this.boneComponents;
if (boneComponents == null) return;
for (int i = 0, n = boneComponents.Count; i < n; i++)
boneComponents[i].DoUpdate(phase);
}
public Transform GetBoneRoot () {
if (boneRoot != null)
return boneRoot;
GameObject boneRootObject = new GameObject("SkeletonUtility-SkeletonRoot");
#if UNITY_EDITOR
if (!Application.isPlaying)
UnityEditor.Undo.RegisterCreatedObjectUndo(boneRootObject, "Spawn Bone");
#endif
if (skeletonGraphic != null)
boneRootObject.AddComponent<RectTransform>();
boneRoot = boneRootObject.transform;
boneRoot.SetParent(transform);
boneRoot.localPosition = Vector3.zero;
boneRoot.localRotation = Quaternion.identity;
boneRoot.localScale = Vector3.one;
return boneRoot;
}
public GameObject SpawnRoot (SkeletonUtilityBone.Mode mode, bool pos, bool rot, bool sca) {
GetBoneRoot();
Skeleton skeleton = this.skeletonComponent.Skeleton;
GameObject go = SpawnBone(skeleton.RootBone, boneRoot, mode, pos, rot, sca);
CollectBones();
return go;
}
public GameObject SpawnHierarchy (SkeletonUtilityBone.Mode mode, bool pos, bool rot, bool sca) {
GetBoneRoot();
Skeleton skeleton = this.skeletonComponent.Skeleton;
GameObject go = SpawnBoneRecursively(skeleton.RootBone, boneRoot, mode, pos, rot, sca);
CollectBones();
return go;
}
public GameObject SpawnBoneRecursively (Bone bone, Transform parent, SkeletonUtilityBone.Mode mode, bool pos, bool rot, bool sca) {
GameObject go = SpawnBone(bone, parent, mode, pos, rot, sca);
ExposedList<Bone> childrenBones = bone.Children;
for (int i = 0, n = childrenBones.Count; i < n; i++) {
Bone child = childrenBones.Items[i];
SpawnBoneRecursively(child, go.transform, mode, pos, rot, sca);
}
return go;
}
public GameObject SpawnBone (Bone bone, Transform parent, SkeletonUtilityBone.Mode mode, bool pos, bool rot, bool sca) {
GameObject go = new GameObject(bone.Data.Name);
#if UNITY_EDITOR
if (!Application.isPlaying)
UnityEditor.Undo.RegisterCreatedObjectUndo(go, "Spawn Bone");
#endif
if (skeletonGraphic != null)
go.AddComponent<RectTransform>();
Transform goTransform = go.transform;
goTransform.SetParent(parent);
SkeletonUtilityBone b = go.AddComponent<SkeletonUtilityBone>();
b.hierarchy = this;
b.position = pos;
b.rotation = rot;
b.scale = sca;
b.mode = mode;
b.zPosition = true;
b.Reset();
b.bone = bone;
b.boneName = bone.Data.Name;
b.valid = true;
if (mode == SkeletonUtilityBone.Mode.Override) {
var bonePose = b.bone.AppliedPose;
if (rot) goTransform.localRotation = Quaternion.Euler(0, 0, bonePose.Rotation);
if (pos) goTransform.localPosition = new Vector3(
bonePose.X * positionScale + positionOffset.x, bonePose.Y * positionScale + positionOffset.y, 0);
goTransform.localScale = new Vector3(bonePose.ScaleX, bonePose.ScaleY, 0);
}
return go;
}
#region Transfer of Deprecated Fields
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
public void UpgradeTo43 () {
wasUpgradedTo43 = true;
if (skeletonRenderer == null && skeletonGraphic == null) {
Component previousReference = previousSkeletonRenderer != null ? previousSkeletonRenderer : this;
skeletonRenderer = previousReference.GetComponent<SkeletonRenderer>();
if (skeletonRenderer == null)
Debug.LogError("Please manually re-assign SkeletonRenderer at SkeletonUtility, " +
"automatic upgrade failed.", this);
}
}
[SerializeField, HideInInspector, FormerlySerializedAs("skeletonRenderer")] Component previousSkeletonRenderer;
[SerializeField] bool wasUpgradedTo43 = false;
#endif
#endregion
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 7f726fb798ad621458c431cb9966d91d
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,259 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2026, 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 UnityEngine;
using UnityEngine.Serialization;
namespace Spine.Unity {
/// <summary>Sets a GameObject's transform to match a bone on a Spine skeleton.</summary>
#if NEW_PREFAB_SYSTEM
[ExecuteAlways]
#else
[ExecuteInEditMode]
#endif
[AddComponentMenu("Spine/SkeletonUtilityBone")]
[HelpURL("https://esotericsoftware.com/spine-unity-utility-components#SkeletonUtilityBone")]
public class SkeletonUtilityBone : MonoBehaviour {
public enum Mode {
Follow,
Override
}
public enum UpdatePhase {
Local,
World,
Complete
}
#region Inspector
/// <summary>If a bone isn't set, boneName is used to find the bone.</summary>
public string boneName;
public Transform parentReference;
[SerializeField, FormerlySerializedAs("mode")] Mode boneMode;
public Mode mode {
get { return boneMode; }
set {
if (boneMode != value) {
boneMode = value;
if (hierarchy != null)
hierarchy.OnUtilityBoneChanged();
}
}
}
public bool position, rotation, scale, zPosition = true;
[Range(0f, 1f)]
public float overrideAlpha = 1;
#endregion
public SkeletonUtility hierarchy;
[System.NonSerialized] public Bone bone;
[System.NonSerialized] public bool valid;
Transform cachedTransform;
Transform skeletonTransform;
Vector3 TransformLocalPosition { get { return cachedTransform.localPosition; } }
Quaternion TransformLocalRotation { get { return cachedTransform.localRotation; } }
Vector3 TransformLocalScale { get { return cachedTransform.localScale; } }
#if UNITY_EDITOR
bool incompatibleTransformMode;
public bool IncompatibleTransformMode { get { return incompatibleTransformMode; } }
#endif
public void Reset () {
bone = null;
cachedTransform = transform;
valid = hierarchy != null && hierarchy.IsValid;
if (!valid)
return;
skeletonTransform = hierarchy.transform;
hierarchy.OnReset -= HandleOnReset;
hierarchy.OnReset += HandleOnReset;
DoUpdate(UpdatePhase.Local);
}
void OnEnable () {
if (hierarchy == null) hierarchy = transform.GetComponentInParent<SkeletonUtility>();
if (hierarchy == null) return;
hierarchy.RegisterBone(this);
hierarchy.OnReset += HandleOnReset;
}
void HandleOnReset () {
Reset();
}
void OnDisable () {
if (hierarchy != null) {
hierarchy.OnReset -= HandleOnReset;
hierarchy.UnregisterBone(this);
}
}
public void DoUpdate (UpdatePhase phase) {
if (!valid) {
Reset();
return;
}
Skeleton skeleton = hierarchy.Skeleton;
if (bone == null) {
if (string.IsNullOrEmpty(boneName)) return;
bone = skeleton.FindBone(boneName);
if (bone == null) {
Debug.LogError("Bone not found: " + boneName, this);
return;
}
}
if (!bone.Active) return;
float positionScale = hierarchy.PositionScale;
Transform thisTransform = cachedTransform;
float skeletonFlipRotation = Mathf.Sign(skeleton.ScaleX * skeleton.ScaleY);
if (mode == Mode.Follow) {
var bonePose = bone.Pose;
switch (phase) {
case UpdatePhase.Local:
if (position)
thisTransform.localPosition = new Vector3(bonePose.X * positionScale, bonePose.Y * positionScale,
zPosition ? 0 : thisTransform.localPosition.z);
if (rotation) {
if (bone.Data.GetSetupPose().Inherit.InheritsRotation()) {
thisTransform.localRotation = Quaternion.Euler(0, 0, bonePose.Rotation);
} else {
Vector3 euler = skeletonTransform.rotation.eulerAngles;
thisTransform.rotation = Quaternion.Euler(euler.x, euler.y, euler.z + (bone.AppliedPose.WorldRotationX * skeletonFlipRotation));
}
}
if (scale) {
thisTransform.localScale = new Vector3(bonePose.ScaleX, bonePose.ScaleY, 1f);
#if UNITY_EDITOR
incompatibleTransformMode = BoneTransformModeIncompatible(bone);
#endif
}
break;
case UpdatePhase.World:
case UpdatePhase.Complete:
var appliedPose = bone.AppliedPose;
appliedPose.ValidateLocalTransform(skeleton);
if (position)
thisTransform.localPosition = new Vector3(appliedPose.X * positionScale, appliedPose.Y * positionScale,
zPosition ? 0 : thisTransform.localPosition.z);
if (rotation) {
if (bone.Data.GetSetupPose().Inherit.InheritsRotation()) {
thisTransform.localRotation = Quaternion.Euler(0, 0, appliedPose.Rotation);
} else {
Vector3 euler = skeletonTransform.rotation.eulerAngles;
thisTransform.rotation = Quaternion.Euler(euler.x, euler.y, euler.z + (appliedPose.WorldRotationX * skeletonFlipRotation));
}
}
if (scale) {
thisTransform.localScale = new Vector3(appliedPose.ScaleX, appliedPose.ScaleY, 1f);
#if UNITY_EDITOR
incompatibleTransformMode = BoneTransformModeIncompatible(bone);
#endif
}
break;
}
} else if (mode == Mode.Override) {
if (phase != UpdatePhase.Local)
return;
var bonePose = bone.Pose;
if (parentReference == null) {
if (position) {
Vector3 clp = TransformLocalPosition / positionScale;
bonePose.X = Mathf.Lerp(bonePose.X, clp.x, overrideAlpha);
bonePose.Y = Mathf.Lerp(bonePose.Y, clp.y, overrideAlpha);
}
if (rotation) {
float angle = Mathf.LerpAngle(bonePose.Rotation, TransformLocalRotation.eulerAngles.z, overrideAlpha);
bonePose.Rotation = angle;
bone.AppliedPose.Rotation = angle;
}
if (scale) {
Vector3 cls = TransformLocalScale;
bonePose.ScaleX = Mathf.Lerp(bonePose.ScaleX, cls.x, overrideAlpha);
bonePose.ScaleY = Mathf.Lerp(bonePose.ScaleY, cls.y, overrideAlpha);
}
} else {
if (position) {
Vector3 pos = parentReference.InverseTransformPoint(thisTransform.position) / positionScale;
bonePose.X = Mathf.Lerp(bonePose.X, pos.x, overrideAlpha);
bonePose.Y = Mathf.Lerp(bonePose.Y, pos.y, overrideAlpha);
}
if (rotation) {
float angle = Mathf.LerpAngle(bonePose.Rotation, Quaternion.LookRotation(Vector3.forward, parentReference.InverseTransformDirection(thisTransform.up)).eulerAngles.z, overrideAlpha);
bonePose.Rotation = angle;
bone.AppliedPose.Rotation = angle;
}
if (scale) {
Vector3 cls = TransformLocalScale;
bonePose.ScaleX = Mathf.Lerp(bonePose.ScaleX, cls.x, overrideAlpha);
bonePose.ScaleY = Mathf.Lerp(bonePose.ScaleY, cls.y, overrideAlpha);
}
#if UNITY_EDITOR
incompatibleTransformMode = BoneTransformModeIncompatible(bone);
#endif
}
}
}
public static bool BoneTransformModeIncompatible (Bone bone) {
return !bone.Data.GetSetupPose().Inherit.InheritsScale();
}
public void AddBoundingBox (Skeleton skeleton, string skinName, string slotName, string attachmentName) {
SkeletonUtility.AddBoneRigidbody2D(transform.gameObject);
SkeletonUtility.AddBoundingBoxGameObject(skeleton, skinName, slotName, attachmentName, transform);
}
#if UNITY_EDITOR
void OnDrawGizmos () {
if (IncompatibleTransformMode)
Gizmos.DrawIcon(transform.position + new Vector3(0, 0.128f, 0), "icon-warning");
}
#endif
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: b238dfcde8209044b97d23f62bcaadf6
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,63 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2026, 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 UnityEngine;
namespace Spine.Unity {
#if NEW_PREFAB_SYSTEM
[ExecuteAlways]
#else
[ExecuteInEditMode]
#endif
[RequireComponent(typeof(SkeletonUtilityBone))]
[HelpURL("https://esotericsoftware.com/spine-unity-utility-components#SkeletonUtilityConstraint")]
public abstract class SkeletonUtilityConstraint : MonoBehaviour {
protected SkeletonUtilityBone bone;
protected SkeletonUtility hierarchy;
protected virtual void OnEnable () {
bone = GetComponent<SkeletonUtilityBone>();
hierarchy = transform.GetComponentInParent<SkeletonUtility>();
hierarchy.RegisterBone(bone); // prevent update order issues
hierarchy.RegisterConstraint(this);
}
protected virtual void OnDisable () {
hierarchy.UnregisterConstraint(this);
}
public abstract void DoUpdate ();
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 522dbfcc6c916df4396f14f35048d185
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: