提交Spine插件

This commit is contained in:
YiHan0621
2025-08-28 18:32:49 +08:00
parent 254a40d87d
commit 316427ebef
970 changed files with 139525 additions and 0 deletions

View File

@@ -0,0 +1,185 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated May 1, 2019. Replaces all prior versions.
*
* Copyright (c) 2013-2019, 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.
*
* THIS SOFTWARE IS 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 THIS SOFTWARE,
* 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 UnityEngine;
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")]
public class BoneFollower : MonoBehaviour {
#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")]
[SerializeField] public string boneName;
public bool followZPosition = true;
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. BoneFollower cannot inherit world/skewed scale because of UnityEngine.Transform property limitations.")]
public bool followLocalScale = false;
[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 (initializeOnAwake) Initialize();
}
public void HandleRebuildRenderer (SkeletonRenderer skeletonRenderer) {
Initialize();
}
public 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 void LateUpdate () {
if (!valid) {
Initialize();
return;
}
#if UNITY_EDITOR
if (!Application.isPlaying)
skeletonTransformIsParent = Transform.ReferenceEquals(skeletonTransform, transform.parent);
#endif
if (bone == null) {
if (string.IsNullOrEmpty(boneName)) return;
bone = skeletonRenderer.skeleton.FindBone(boneName);
if (!SetBone(boneName)) return;
}
Transform thisTransform = this.transform;
if (skeletonTransformIsParent) {
// Recommended setup: Use local transform properties if Spine GameObject is the immediate parent
thisTransform.localPosition = new Vector3(bone.worldX, bone.worldY, followZPosition ? 0f : thisTransform.localPosition.z);
if (followBoneRotation) {
float halfRotation = Mathf.Atan2(bone.c, bone.a) * 0.5f;
if (followLocalScale && bone.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;
var 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(bone.worldX, bone.worldY, 0f));
if (!followZPosition) targetWorldPosition.z = thisTransform.position.z;
float boneWorldRotation = bone.WorldRotationX;
Transform transformParent = thisTransform.parent;
if (transformParent != null) {
Matrix4x4 m = transformParent.localToWorldMatrix;
if (m.m00 * m.m11 - m.m01 * m.m10 < 0) // Determinant2D is negative
boneWorldRotation = -boneWorldRotation;
}
if (followBoneRotation) {
Vector3 worldRotation = skeletonTransform.rotation.eulerAngles;
if (followLocalScale && bone.scaleX < 0) boneWorldRotation += 180f;
thisTransform.SetPositionAndRotation(targetWorldPosition, Quaternion.Euler(worldRotation.x, worldRotation.y, worldRotation.z + boneWorldRotation));
} else {
thisTransform.position = targetWorldPosition;
}
}
Vector3 localScale = followLocalScale ? new Vector3(bone.scaleX, bone.scaleY, 1f) : new Vector3(1f, 1f, 1f);
if (followSkeletonFlip) localScale.y *= Mathf.Sign(bone.skeleton.scaleX * bone.skeleton.scaleY);
thisTransform.localScale = localScale;
}
}
}

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: a1fd8daaed7b64148a34acb96ba14ce1
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,158 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated May 1, 2019. Replaces all prior versions.
*
* Copyright (c) 2013-2019, 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.
*
* THIS SOFTWARE IS 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 THIS SOFTWARE,
* 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
[AddComponentMenu("Spine/Point Follower")]
public class PointFollower : MonoBehaviour, IHasSkeletonRenderer, IHasSkeletonComponent {
[SerializeField] public SkeletonRenderer skeletonRenderer;
public SkeletonRenderer SkeletonRenderer { 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; } }
public void Initialize () {
valid = skeletonRenderer != null && skeletonRenderer.valid;
if (!valid)
return;
UpdateReferences();
#if UNITY_EDITOR
if (Application.isEditor) LateUpdate();
#endif
}
private void HandleRebuildRenderer (SkeletonRenderer 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)) {
var skeleton = skeletonRenderer.Skeleton;
int slotIndex = skeleton.FindSlotIndex(slotName);
if (slotIndex >= 0) {
var slot = skeleton.slots.Items[slotIndex];
bone = slot.bone;
point = skeleton.GetAttachment(slotIndex, pointAttachmentName) as PointAttachment;
}
}
}
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;
point.ComputeWorldPosition(bone, out worldPos.x, out worldPos.y);
float rotation = point.ComputeWorldRotation(bone);
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;
var 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;
localScale.y = Mathf.Abs(localScale.y) * Mathf.Sign(bone.skeleton.scaleX * bone.skeleton.scaleY);
thisTransform.localScale = localScale;
}
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: daf461e4341180341a648c07e1899528
timeCreated: 1518094986
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,208 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated May 1, 2019. Replaces all prior versions.
*
* Copyright (c) 2013-2019, 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.
*
* THIS SOFTWARE IS 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 THIS SOFTWARE,
* 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
[AddComponentMenu("Spine/SkeletonAnimation")]
public class SkeletonAnimation : SkeletonRenderer, ISkeletonAnimation, IAnimationStateComponent {
#region IAnimationStateComponent
/// <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 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 { return this.state; } }
#endregion
#region Bone Callbacks ISkeletonAnimation
protected event UpdateBonesDelegate _UpdateLocal;
protected event UpdateBonesDelegate _UpdateWorld;
protected event UpdateBonesDelegate _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 UpdateBonesDelegate 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 UpdateBonesDelegate 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 UpdateBonesDelegate UpdateComplete { add { _UpdateComplete += value; } remove { _UpdateComplete -= value; } }
#endregion
#region Serialized state and Beginner API
[SerializeField]
[SpineAnimation]
private string _animationName;
/// <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 (!valid) {
return _animationName;
} else {
TrackEntry entry = state.GetCurrent(0);
return entry == null ? null : entry.Animation.Name;
}
}
set {
if (_animationName == value)
return;
_animationName = value;
if (string.IsNullOrEmpty(value)) {
state.ClearTrack(0);
} else {
var animationObject = skeletonDataAsset.GetSkeletonData(false).FindAnimation(value);
if (animationObject != null)
state.SetAnimation(0, animationObject, loop);
}
}
}
/// <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;
#endregion
#region Runtime Instantiation
/// <summary>Adds and prepares a SkeletonAnimation component to a GameObject at runtime.</summary>
/// <returns>The newly instantiated SkeletonAnimation</returns>
public static SkeletonAnimation AddToGameObject (GameObject gameObject, SkeletonDataAsset skeletonDataAsset) {
return SkeletonRenderer.AddSpineComponent<SkeletonAnimation>(gameObject, skeletonDataAsset);
}
/// <summary>Instantiates a new UnityEngine.GameObject and adds a prepared SkeletonAnimation component to it.</summary>
/// <returns>The newly instantiated SkeletonAnimation component.</returns>
public static SkeletonAnimation NewSkeletonAnimationGameObject (SkeletonDataAsset skeletonDataAsset) {
return SkeletonRenderer.NewSpineGameObject<SkeletonAnimation>(skeletonDataAsset);
}
#endregion
/// <summary>
/// Clears the previously generated mesh, resets the skeleton's pose, and clears all previously active animations.</summary>
public override void ClearState () {
base.ClearState();
if (state != null) state.ClearTracks();
}
/// <summary>
/// Initialize this component. Attempts to load the SkeletonData and 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 override void Initialize (bool overwrite) {
if (valid && !overwrite)
return;
base.Initialize(overwrite);
if (!valid)
return;
state = new Spine.AnimationState(skeletonDataAsset.GetAnimationStateData());
if (!string.IsNullOrEmpty(_animationName)) {
var animationObject = skeletonDataAsset.GetSkeletonData(false).FindAnimation(_animationName);
if (animationObject != null) {
animationObject.PoseSkeleton(skeleton, 0f);
skeleton.UpdateWorldTransform();
#if UNITY_EDITOR
if (Application.isPlaying) {
#endif
// In Unity Editor edit mode, make this block not run.
state.SetAnimation(0, animationObject, loop);
#if UNITY_EDITOR
}
#endif
}
}
}
void Update () {
Update(Time.deltaTime);
}
/// <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 void Update (float deltaTime) {
if (!valid)
return;
deltaTime *= timeScale;
skeleton.Update(deltaTime);
state.Update(deltaTime);
state.Apply(skeleton);
if (_UpdateLocal != null)
_UpdateLocal(this);
skeleton.UpdateWorldTransform();
if (_UpdateWorld != null) {
_UpdateWorld(this);
skeleton.UpdateWorldTransform();
}
if (_UpdateComplete != null) {
_UpdateComplete(this);
}
}
}
}

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: d247ba06193faa74d9335f5481b2b56c
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,496 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated May 1, 2019. Replaces all prior versions.
*
* Copyright (c) 2013-2019, 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.
*
* THIS SOFTWARE IS 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 THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
using UnityEngine;
using System.Collections.Generic;
namespace Spine.Unity {
[RequireComponent(typeof(Animator))]
public class SkeletonMecanim : SkeletonRenderer, ISkeletonAnimation {
[SerializeField] protected MecanimTranslator translator;
public MecanimTranslator Translator { get { return translator; } }
#region Bone Callbacks (ISkeletonAnimation)
protected event UpdateBonesDelegate _UpdateLocal;
protected event UpdateBonesDelegate _UpdateWorld;
protected event UpdateBonesDelegate _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 UpdateBonesDelegate 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 UpdateBonesDelegate 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 UpdateBonesDelegate UpdateComplete { add { _UpdateComplete += value; } remove { _UpdateComplete -= value; } }
#endregion
public override void Initialize (bool overwrite) {
if (valid && !overwrite) return;
base.Initialize(overwrite);
if (!valid) return;
if (translator == null) translator = new MecanimTranslator();
translator.Initialize(GetComponent<Animator>(), this.skeletonDataAsset);
}
public void Update () {
if (!valid) return;
#if UNITY_EDITOR
var translatorAnimator = translator.Animator;
if (translatorAnimator != null && !translatorAnimator.isInitialized)
translatorAnimator.Rebind();
if (Application.isPlaying) {
translator.Apply(skeleton);
} else {
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(skeleton);
}
}
#else
translator.Apply(skeleton);
#endif
// UpdateWorldTransform and Bone Callbacks
{
if (_UpdateLocal != null)
_UpdateLocal(this);
skeleton.UpdateWorldTransform();
if (_UpdateWorld != null) {
_UpdateWorld(this);
skeleton.UpdateWorldTransform();
}
if (_UpdateComplete != null)
_UpdateComplete(this);
}
}
[System.Serializable]
public class MecanimTranslator {
#region Inspector
public bool autoReset = true;
public MixMode[] layerMixModes = new MixMode[0];
public MixBlend[] layerBlendModes = new MixBlend[0];
#endregion
public enum MixMode { AlwaysMix, MixNext, SpineStyle }
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 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<AnimatorClipInfo> clipInfos = new List<AnimatorClipInfo>();
public readonly List<AnimatorClipInfo> nextClipInfos = new List<AnimatorClipInfo>();
public readonly List<AnimatorClipInfo> interruptingClipInfos = new List<AnimatorClipInfo>();
public AnimatorStateInfo stateInfo;
public AnimatorStateInfo nextStateInfo;
public AnimatorStateInfo interruptingStateInfo;
public float interruptingClipTimeAddition = 0;
}
protected ClipInfos[] layerClipInfos = new ClipInfos[0];
Animator animator;
public Animator Animator { get { return this.animator; } }
public void Initialize(Animator animator, SkeletonDataAsset skeletonDataAsset) {
this.animator = animator;
previousAnimations.Clear();
animationTable.Clear();
var data = skeletonDataAsset.GetSkeletonData(true);
foreach (var a in data.Animations)
animationTable.Add(a.Name.GetHashCode(), a);
clipNameHashCodeTable.Clear();
ClearClipInfosForLayers();
}
public void Apply (Skeleton skeleton) {
if (layerMixModes.Length < animator.layerCount)
System.Array.Resize<MixMode>(ref layerMixModes, animator.layerCount);
#if UNITY_EDITOR
if (!Application.isPlaying) {
GetLayerBlendModes();
}
#endif
InitClipInfosForLayers();
for (int layer = 0, n = animator.layerCount; layer < n; layer++) {
GetStateUpdatesFromAnimator(layer);
}
//skeleton.Update(Time.deltaTime); // Doesn't actually do anything, currently. (Spine 3.6).
// Clear Previous
if (autoReset) {
var previousAnimations = this.previousAnimations;
for (int i = 0, n = previousAnimations.Count; i < n; i++)
previousAnimations[i].SetKeyedItemsToSetupPose(skeleton);
previousAnimations.Clear();
for (int layer = 0, n = animator.layerCount; layer < n; layer++) {
float layerWeight = (layer == 0) ? 1 : animator.GetLayerWeight(layer); // Animator.GetLayerWeight always returns 0 on the first layer. Should be interpreted as 1.
if (layerWeight <= 0) continue;
AnimatorStateInfo nextStateInfo = animator.GetNextAnimatorStateInfo(layer);
bool hasNext = nextStateInfo.fullPathHash != 0;
int clipInfoCount, nextClipInfoCount, interruptingClipInfoCount;
IList<AnimatorClipInfo> 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++) {
var info = clipInfo[c];
float weight = info.weight * layerWeight; if (weight == 0) continue;
previousAnimations.Add(GetAnimation(info.clip));
}
if (hasNext) {
for (int c = 0; c < nextClipInfoCount; c++) {
var info = nextClipInfo[c];
float weight = info.weight * layerWeight; if (weight == 0) continue;
previousAnimations.Add(GetAnimation(info.clip));
}
}
if (isInterruptionActive) {
for (int c = 0; c < interruptingClipInfoCount; c++)
{
var info = interruptingClipInfo[c];
float clipWeight = shallInterpolateWeightTo1 ? (info.weight + 1.0f) * 0.5f : info.weight;
float weight = clipWeight * layerWeight; if (weight == 0) continue;
previousAnimations.Add(GetAnimation(info.clip));
}
}
}
}
// Apply
for (int layer = 0, n = animator.layerCount; layer < n; layer++) {
float layerWeight = (layer == 0) ? 1 : animator.GetLayerWeight(layer); // Animator.GetLayerWeight always returns 0 on the first layer. Should be interpreted as 1.
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<AnimatorClipInfo> clipInfo, nextClipInfo, interruptingClipInfo;
bool shallInterpolateWeightTo1;
GetAnimatorClipInfos(layer, out isInterruptionActive, out clipInfoCount, out nextClipInfoCount, out interruptingClipInfoCount,
out clipInfo, out nextClipInfo, out interruptingClipInfo, out shallInterpolateWeightTo1);
MixMode mode = layerMixModes[layer];
MixBlend layerBlendMode = (layer < layerBlendModes.Length) ? layerBlendModes[layer] : MixBlend.Replace;
if (mode == MixMode.AlwaysMix) {
// Always use Mix instead of Applying the first non-zero weighted clip.
for (int c = 0; c < clipInfoCount; c++) {
var info = clipInfo[c]; float weight = info.weight * layerWeight; if (weight == 0) continue;
GetAnimation(info.clip).Apply(skeleton, 0, AnimationTime(stateInfo.normalizedTime, info.clip.length, stateInfo.loop, stateInfo.speed < 0), stateInfo.loop, null, weight, layerBlendMode, MixDirection.In);
}
if (hasNext) {
for (int c = 0; c < nextClipInfoCount; c++) {
var info = nextClipInfo[c]; float weight = info.weight * layerWeight; if (weight == 0) continue;
GetAnimation(info.clip).Apply(skeleton, 0, AnimationTime(nextStateInfo.normalizedTime, info.clip.length, nextStateInfo.speed < 0), nextStateInfo.loop, null, weight, layerBlendMode, MixDirection.In);
}
}
if (isInterruptionActive) {
for (int c = 0; c < interruptingClipInfoCount; c++)
{
var info = interruptingClipInfo[c];
float clipWeight = shallInterpolateWeightTo1 ? (info.weight + 1.0f) * 0.5f : info.weight;
float weight = clipWeight * layerWeight; if (weight == 0) continue;
GetAnimation(info.clip).Apply(skeleton, 0, AnimationTime(interruptingStateInfo.normalizedTime + interruptingClipTimeAddition, info.clip.length, interruptingStateInfo.speed < 0),
interruptingStateInfo.loop, null, weight, layerBlendMode, MixDirection.In);
}
}
} else { // case MixNext || SpineStyle
// Apply first non-zero weighted clip
int c = 0;
for (; c < clipInfoCount; c++) {
var info = clipInfo[c]; float weight = info.weight * layerWeight; if (weight == 0) continue;
GetAnimation(info.clip).Apply(skeleton, 0, AnimationTime(stateInfo.normalizedTime, info.clip.length, stateInfo.loop, stateInfo.speed < 0), stateInfo.loop, null, 1f, layerBlendMode, MixDirection.In);
break;
}
// Mix the rest
for (; c < clipInfoCount; c++) {
var info = clipInfo[c]; float weight = info.weight * layerWeight; if (weight == 0) continue;
GetAnimation(info.clip).Apply(skeleton, 0, AnimationTime(stateInfo.normalizedTime, info.clip.length, stateInfo.loop, stateInfo.speed < 0), stateInfo.loop, null, weight, layerBlendMode, MixDirection.In);
}
c = 0;
if (hasNext) {
// Apply next clip directly instead of mixing (ie: no crossfade, ignores mecanim transition weights)
if (mode == MixMode.SpineStyle) {
for (; c < nextClipInfoCount; c++) {
var info = nextClipInfo[c]; float weight = info.weight * layerWeight; if (weight == 0) continue;
GetAnimation(info.clip).Apply(skeleton, 0, AnimationTime(nextStateInfo.normalizedTime, info.clip.length, nextStateInfo.speed < 0), nextStateInfo.loop, null, 1f, layerBlendMode, MixDirection.In);
break;
}
}
// Mix the rest
for (; c < nextClipInfoCount; c++) {
var info = nextClipInfo[c]; float weight = info.weight * layerWeight; if (weight == 0) continue;
GetAnimation(info.clip).Apply(skeleton, 0, AnimationTime(nextStateInfo.normalizedTime, info.clip.length, nextStateInfo.speed < 0), nextStateInfo.loop, null, weight, layerBlendMode, MixDirection.In);
}
}
c = 0;
if (isInterruptionActive) {
// Apply next clip directly instead of mixing (ie: no crossfade, ignores mecanim transition weights)
if (mode == MixMode.SpineStyle) {
for (; c < interruptingClipInfoCount; c++) {
var info = interruptingClipInfo[c];
float clipWeight = shallInterpolateWeightTo1 ? (info.weight + 1.0f) * 0.5f : info.weight;
float weight = clipWeight * layerWeight; if (weight == 0) continue;
GetAnimation(info.clip).Apply(skeleton, 0, AnimationTime(interruptingStateInfo.normalizedTime + interruptingClipTimeAddition, info.clip.length, interruptingStateInfo.speed < 0), interruptingStateInfo.loop, null, 1f, layerBlendMode, MixDirection.In);
break;
}
}
// Mix the rest
for (; c < interruptingClipInfoCount; c++) {
var info = interruptingClipInfo[c];
float clipWeight = shallInterpolateWeightTo1 ? (info.weight + 1.0f) * 0.5f : info.weight;
float weight = clipWeight * layerWeight; if (weight == 0) continue;
GetAnimation(info.clip).Apply(skeleton, 0, AnimationTime(interruptingStateInfo.normalizedTime + interruptingClipTimeAddition, info.clip.length, interruptingStateInfo.speed < 0), interruptingStateInfo.loop, null, weight, layerBlendMode, MixDirection.In);
}
}
}
}
}
static float AnimationTime (float normalizedTime, float clipLength, bool loop, bool reversed) {
if (reversed)
normalizedTime = (1 - normalizedTime + (int)normalizedTime) + (int)normalizedTime;
float time = normalizedTime * clipLength;
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 + (int)normalizedTime) + (int)normalizedTime;
return normalizedTime * clipLength;
}
void InitClipInfosForLayers () {
if (layerClipInfos.Length < animator.layerCount) {
System.Array.Resize<ClipInfos>(ref layerClipInfos, animator.layerCount);
for (int layer = 0, n = animator.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();
}
}
}
#if UNITY_EDITOR
void GetLayerBlendModes() {
if (layerBlendModes.Length < animator.layerCount) {
System.Array.Resize<MixBlend>(ref layerBlendModes, animator.layerCount);
}
for (int layer = 0, n = animator.layerCount; layer < n; ++layer) {
var controller = animator.runtimeAnimatorController as UnityEditor.Animations.AnimatorController;
if (controller != null) {
layerBlendModes[layer] = MixBlend.Replace;
if (layer > 0) {
layerBlendModes[layer] = controller.layers[layer].blendingMode == UnityEditor.Animations.AnimatorLayerBlendingMode.Additive ?
MixBlend.Add : MixBlend.Replace;
}
}
}
}
#endif
void GetStateUpdatesFromAnimator (int layer) {
var layerInfos = layerClipInfos[layer];
int clipInfoCount = animator.GetCurrentAnimatorClipInfoCount(layer);
int nextClipInfoCount = animator.GetNextAnimatorClipInfoCount(layer);
var clipInfos = layerInfos.clipInfos;
var nextClipInfos = layerInfos.nextClipInfos;
var 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).
var interruptingStateInfo = animator.GetNextAnimatorStateInfo(layer);
layerInfos.isLastFrameOfInterruption = interruptingStateInfo.fullPathHash == 0;
if (!layerInfos.isLastFrameOfInterruption) {
layerInfos.interruptingClipInfoCount = interruptingClipInfos.Count;
animator.GetNextAnimatorClipInfo(layer, interruptingClipInfos);
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;
animator.GetCurrentAnimatorClipInfo(layer, clipInfos);
animator.GetNextAnimatorClipInfo(layer, nextClipInfos);
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<AnimatorClipInfo> clipInfo,
out IList<AnimatorClipInfo> nextClipInfo,
out IList<AnimatorClipInfo> interruptingClipInfo,
out bool shallInterpolateWeightTo1) {
var 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) {
var 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();
public bool Equals (AnimationClip x, AnimationClip y) { return x.GetInstanceID() == y.GetInstanceID(); }
public int GetHashCode (AnimationClip o) { return o.GetInstanceID(); }
}
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; }
}
}
}
}

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: f9db98c60740638449864eb028fbe7ad
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,576 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated May 1, 2019. Replaces all prior versions.
*
* Copyright (c) 2013-2019, 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.
*
* THIS SOFTWARE IS 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 THIS SOFTWARE,
* 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
#define SPINE_OPTIONAL_RENDEROVERRIDE
#define SPINE_OPTIONAL_MATERIALOVERRIDE
using System.Collections.Generic;
using UnityEngine;
namespace Spine.Unity {
/// <summary>Base class of animated Spine skeleton components. This component manages and renders a skeleton.</summary>
#if NEW_PREFAB_SYSTEM
[ExecuteAlways]
#else
[ExecuteInEditMode]
#endif
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer)), DisallowMultipleComponent]
[HelpURL("http://esotericsoftware.com/spine-unity-rendering")]
public class SkeletonRenderer : MonoBehaviour, ISkeletonComponent, IHasSkeletonDataAsset {
[SerializeField] public SkeletonDataAsset skeletonDataAsset;
#region Initialization settings
/// <summary>Skin name to use when the Skeleton is initialized.</summary>
[SerializeField] [SpineSkin(defaultAsEmptyString:true)] public string initialSkinName;
/// <summary>Flip X and Y to use when the Skeleton is initialized.</summary>
[SerializeField] public bool initialFlipX, initialFlipY;
#endregion
#region Advanced Render Settings
// Submesh Separation
/// <summary>Slot names used to populate separatorSlots list when the Skeleton is initialized. Changing this after initialization does nothing.</summary>
[UnityEngine.Serialization.FormerlySerializedAs("submeshSeparators")][SerializeField][SpineSlot] protected 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>();
// Render Settings
[Range(-0.1f, 0f)] public float zSpacing;
/// <summary>Use Spine's clipping feature. If false, ClippingAttachments will be ignored.</summary>
public bool useClipping = true;
/// <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 = false;
/// <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 = true;
/// <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;
/// <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 = false;
/// <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;
/// <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>
[UnityEngine.Serialization.FormerlySerializedAs("calculateNormals")] public bool addNormals = false;
/// <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 = false;
#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 = SpriteMaskInteraction.None;
[System.Serializable]
public class SpriteMaskInteractionMaterials {
/// <summary>Material references for switching material sets at runtime when <see cref="SkeletonRenderer.maskInteraction"/> changes to <see cref="SpriteMaskInteraction.None"/>.</summary>
public Material[] materialsMaskDisabled = new Material[0];
/// <summary>Material references for switching material sets at runtime when <see cref="SkeletonRenderer.maskInteraction"/> changes to <see cref="SpriteMaskInteraction.VisibleInsideMask"/>.</summary>
public Material[] materialsInsideMask = new Material[0];
/// <summary>Material references for switching material sets at runtime when <see cref="SkeletonRenderer.maskInteraction"/> changes to <see cref="SpriteMaskInteraction.VisibleOutsideMask"/>.</summary>
public Material[] materialsOutsideMask = new Material[0];
}
/// <summary>Material references for switching material sets at runtime when <see cref="SkeletonRenderer.maskInteraction"/> changes.</summary>
public SpriteMaskInteractionMaterials maskMaterials = new SpriteMaskInteractionMaterials();
/// <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;
#if UNITY_EDITOR
private static bool haveStencilParametersBeenFixed = false;
#endif
#endif // #if BUILT_IN_SPRITE_MASK_COMPONENT
#endregion
#region Overrides
#if SPINE_OPTIONAL_RENDEROVERRIDE
// These are API for anything that wants to take over rendering for a SkeletonRenderer.
public bool disableRenderingOnOverride = true;
public delegate void InstructionDelegate (SkeletonRendererInstruction instruction);
event InstructionDelegate generateMeshOverride;
/// <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);
meshRenderer.enabled = false;
}
}
remove {
generateMeshOverride -= value;
if (disableRenderingOnOverride && generateMeshOverride == null) {
Initialize(false);
meshRenderer.enabled = true;
}
}
}
/// <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;
#endif
#if SPINE_OPTIONAL_MATERIALOVERRIDE
[System.NonSerialized] readonly Dictionary<Material, Material> customMaterialOverride = new Dictionary<Material, Material>();
/// <summary>Use this Dictionary to override a Material with a different Material.</summary>
public Dictionary<Material, Material> CustomMaterialOverride { get { return customMaterialOverride; } }
#endif
[System.NonSerialized] readonly Dictionary<Slot, Material> customSlotMaterials = new Dictionary<Slot, Material>();
/// <summary>Use this Dictionary to use a different Material to render specific Slots.</summary>
public Dictionary<Slot, Material> CustomSlotMaterials { get { return customSlotMaterials; } }
#endregion
#region Mesh Generator
[System.NonSerialized] readonly SkeletonRendererInstruction currentInstructions = new SkeletonRendererInstruction();
readonly MeshGenerator meshGenerator = new MeshGenerator();
[System.NonSerialized] readonly MeshRendererBuffers rendererBuffers = new MeshRendererBuffers();
#endregion
#region Cached component references
MeshRenderer meshRenderer;
MeshFilter meshFilter;
#endregion
#region Skeleton
[System.NonSerialized] public bool valid;
[System.NonSerialized] public Skeleton skeleton;
public Skeleton Skeleton {
get {
Initialize(false);
return skeleton;
}
}
#endregion
public delegate void SkeletonRendererDelegate (SkeletonRenderer skeletonRenderer);
/// <summary>OnRebuild is raised after the Skeleton is successfully initialized.</summary>
public event SkeletonRendererDelegate OnRebuild;
public SkeletonDataAsset SkeletonDataAsset { get { return skeletonDataAsset; } } // ISkeletonComponent
#region Runtime Instantiation
public static T NewSpineGameObject<T> (SkeletonDataAsset skeletonDataAsset) where T : SkeletonRenderer {
return SkeletonRenderer.AddSpineComponent<T>(new GameObject("New Spine GameObject"), skeletonDataAsset);
}
/// <summary>Add and prepare a Spine component that derives from SkeletonRenderer to a GameObject at runtime.</summary>
/// <typeparam name="T">T should be SkeletonRenderer or any of its derived classes.</typeparam>
public static T AddSpineComponent<T> (GameObject gameObject, SkeletonDataAsset skeletonDataAsset) where T : SkeletonRenderer {
var c = gameObject.AddComponent<T>();
if (skeletonDataAsset != null) {
c.skeletonDataAsset = skeletonDataAsset;
c.Initialize(false);
}
return c;
}
/// <summary>Applies MeshGenerator settings to the SkeletonRenderer and its internal MeshGenerator.</summary>
public void SetMeshSettings (MeshGenerator.Settings settings) {
this.calculateTangents = settings.calculateTangents;
this.immutableTriangles = settings.immutableTriangles;
this.pmaVertexColors = settings.pmaVertexColors;
this.tintBlack = settings.tintBlack;
this.useClipping = settings.useClipping;
this.zSpacing = settings.zSpacing;
this.meshGenerator.settings = settings;
}
#endregion
public virtual void Awake () {
Initialize(false);
}
void OnDisable () {
if (clearStateOnDisable && valid)
ClearState();
}
void OnDestroy () {
rendererBuffers.Dispose();
valid = false;
}
/// <summary>
/// Clears the previously generated mesh and resets the skeleton's pose.</summary>
public virtual void ClearState () {
var meshFilter = GetComponent<MeshFilter>();
if (meshFilter != null) meshFilter.sharedMesh = null;
currentInstructions.Clear();
if (skeleton != null) skeleton.SetToSetupPose();
}
/// <summary>
/// Sets a minimum buffer size for the internal MeshGenerator to prevent excess allocations during animation.
/// </summary>
public void EnsureMeshGeneratorCapacity (int minimumVertexCount) {
meshGenerator.EnsureVertexCapacity(minimumVertexCount);
}
/// <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) {
if (valid && !overwrite)
return;
// Clear
{
if (meshFilter != null)
meshFilter.sharedMesh = null;
meshRenderer = GetComponent<MeshRenderer>();
if (meshRenderer != null && meshRenderer.enabled) meshRenderer.sharedMaterial = null;
currentInstructions.Clear();
rendererBuffers.Clear();
meshGenerator.Begin();
skeleton = null;
valid = false;
}
if (skeletonDataAsset == null)
return;
SkeletonData skeletonData = skeletonDataAsset.GetSkeletonData(false);
if (skeletonData == null) return;
valid = true;
meshFilter = GetComponent<MeshFilter>();
meshRenderer = GetComponent<MeshRenderer>();
rendererBuffers.Initialize();
skeleton = new Skeleton(skeletonData) {
scaleX = initialFlipX ? -1 : 1,
scaleY = initialFlipY ? -1 : 1
};
if (!string.IsNullOrEmpty(initialSkinName) && !string.Equals(initialSkinName, "default", System.StringComparison.Ordinal))
skeleton.SetSkin(initialSkinName);
separatorSlots.Clear();
for (int i = 0; i < separatorSlotNames.Length; i++)
separatorSlots.Add(skeleton.FindSlot(separatorSlotNames[i]));
LateUpdate(); // Generate mesh for the first frame it exists.
if (OnRebuild != null)
OnRebuild(this);
}
/// <summary>
/// Generates a new UnityEngine.Mesh from the internal Skeleton.</summary>
public virtual void LateUpdate () {
if (!valid) return;
#if SPINE_OPTIONAL_RENDEROVERRIDE
bool doMeshOverride = generateMeshOverride != null;
if ((!meshRenderer.enabled) && !doMeshOverride) return;
#else
const bool doMeshOverride = false;
if (!meshRenderer.enabled) return;
#endif
var currentInstructions = this.currentInstructions;
var workingSubmeshInstructions = currentInstructions.submeshInstructions;
var currentSmartMesh = rendererBuffers.GetNextMesh(); // Double-buffer for performance.
bool updateTriangles;
if (this.singleSubmesh) {
// STEP 1. Determine a SmartMesh.Instruction. Split up instructions into submeshes. =============================================
MeshGenerator.GenerateSingleSubmeshInstruction(currentInstructions, skeleton, skeletonDataAsset.atlasAssets[0].PrimaryMaterial);
// STEP 1.9. Post-process workingInstructions. ==================================================================================
#if SPINE_OPTIONAL_MATERIALOVERRIDE
if (customMaterialOverride.Count > 0) // isCustomMaterialOverridePopulated
MeshGenerator.TryReplaceMaterials(workingSubmeshInstructions, customMaterialOverride);
#endif
// STEP 2. Update vertex buffer based on verts from the attachments. ===========================================================
meshGenerator.settings = new MeshGenerator.Settings {
pmaVertexColors = this.pmaVertexColors,
zSpacing = this.zSpacing,
useClipping = this.useClipping,
tintBlack = this.tintBlack,
calculateTangents = this.calculateTangents,
addNormals = this.addNormals
};
meshGenerator.Begin();
updateTriangles = SkeletonRendererInstruction.GeometryNotEqual(currentInstructions, currentSmartMesh.instructionUsed);
if (currentInstructions.hasActiveClipping) {
meshGenerator.AddSubmesh(workingSubmeshInstructions.Items[0], updateTriangles);
} else {
meshGenerator.BuildMeshWithArrays(currentInstructions, updateTriangles);
}
} else {
// STEP 1. Determine a SmartMesh.Instruction. Split up instructions into submeshes. =============================================
MeshGenerator.GenerateSkeletonRendererInstruction(currentInstructions, skeleton, customSlotMaterials, separatorSlots, doMeshOverride, this.immutableTriangles);
// STEP 1.9. Post-process workingInstructions. ==================================================================================
#if SPINE_OPTIONAL_MATERIALOVERRIDE
if (customMaterialOverride.Count > 0) // isCustomMaterialOverridePopulated
MeshGenerator.TryReplaceMaterials(workingSubmeshInstructions, customMaterialOverride);
#endif
#if SPINE_OPTIONAL_RENDEROVERRIDE
if (doMeshOverride) {
this.generateMeshOverride(currentInstructions);
if (disableRenderingOnOverride) return;
}
#endif
updateTriangles = SkeletonRendererInstruction.GeometryNotEqual(currentInstructions, currentSmartMesh.instructionUsed);
// STEP 2. Update vertex buffer based on verts from the attachments. ===========================================================
meshGenerator.settings = new MeshGenerator.Settings {
pmaVertexColors = this.pmaVertexColors,
zSpacing = this.zSpacing,
useClipping = this.useClipping,
tintBlack = this.tintBlack,
calculateTangents = this.calculateTangents,
addNormals = this.addNormals
};
meshGenerator.Begin();
if (currentInstructions.hasActiveClipping)
meshGenerator.BuildMesh(currentInstructions, updateTriangles);
else
meshGenerator.BuildMeshWithArrays(currentInstructions, updateTriangles);
}
if (OnPostProcessVertices != null) OnPostProcessVertices.Invoke(this.meshGenerator.Buffers);
// STEP 3. Move the mesh data into a UnityEngine.Mesh ===========================================================================
var currentMesh = currentSmartMesh.mesh;
meshGenerator.FillVertexData(currentMesh);
rendererBuffers.UpdateSharedMaterials(workingSubmeshInstructions);
if (updateTriangles) { // Check if the triangles should also be updated.
meshGenerator.FillTriangles(currentMesh);
meshRenderer.sharedMaterials = rendererBuffers.GetUpdatedSharedMaterialsArray();
} else if (rendererBuffers.MaterialsChangedInLastUpdate()) {
meshRenderer.sharedMaterials = rendererBuffers.GetUpdatedSharedMaterialsArray();
}
meshGenerator.FillLateVertexData(currentMesh);
// STEP 4. The UnityEngine.Mesh is ready. Set it as the MeshFilter's mesh. Store the instructions used for that mesh. ===========
meshFilter.sharedMesh = currentMesh;
currentSmartMesh.instructionUsed.Set(currentInstructions);
#if BUILT_IN_SPRITE_MASK_COMPONENT
if (meshRenderer != null) {
AssignSpriteMaskMaterials();
}
#endif
}
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();
var slots = skeleton.slots;
foreach (var slot in slots) {
if (slotNamePredicate.Invoke(slot.data.name))
separatorSlots.Add(slot);
}
if (updateStringArray) {
var detectedSeparatorNames = new List<string>();
foreach (var slot in skeleton.slots) {
string slotName = slot.data.name;
if (slotNamePredicate.Invoke(slotName))
detectedSeparatorNames.Add(slotName);
}
if (!clearExistingSeparators) {
string[] originalNames = this.separatorSlotNames;
foreach (string originalName in originalNames)
detectedSeparatorNames.Add(originalName);
}
this.separatorSlotNames = detectedSeparatorNames.ToArray();
}
}
public void ReapplySeparatorSlotNames () {
if (!valid)
return;
separatorSlots.Clear();
for (int i = 0, n = separatorSlotNames.Length; i < n; i++) {
var slot = skeleton.FindSlot(separatorSlotNames[i]);
if (slot != null) {
separatorSlots.Add(slot);
}
#if UNITY_EDITOR
else
{
Debug.LogWarning(separatorSlotNames[i] + " is not a slot in " + skeletonDataAsset.skeletonJSON.name);
}
#endif
}
}
#if BUILT_IN_SPRITE_MASK_COMPONENT
private void AssignSpriteMaskMaterials()
{
#if UNITY_EDITOR
if (!Application.isPlaying && !UnityEditor.EditorApplication.isUpdating) {
EditorFixStencilCompParameters();
}
#endif
if (maskMaterials.materialsMaskDisabled.Length > 0 && maskMaterials.materialsMaskDisabled[0] != null &&
maskInteraction == SpriteMaskInteraction.None) {
this.meshRenderer.materials = maskMaterials.materialsMaskDisabled;
}
else if (maskInteraction == SpriteMaskInteraction.VisibleInsideMask) {
if (maskMaterials.materialsInsideMask.Length == 0 || maskMaterials.materialsInsideMask[0] == null) {
if (!InitSpriteMaskMaterialsInsideMask())
return;
}
this.meshRenderer.materials = maskMaterials.materialsInsideMask;
}
else if (maskInteraction == SpriteMaskInteraction.VisibleOutsideMask) {
if (maskMaterials.materialsOutsideMask.Length == 0 || maskMaterials.materialsOutsideMask[0] == null) {
if (!InitSpriteMaskMaterialsOutsideMask())
return;
}
this.meshRenderer.materials = maskMaterials.materialsOutsideMask;
}
}
private bool InitSpriteMaskMaterialsInsideMask()
{
return InitSpriteMaskMaterialsForMaskType(STENCIL_COMP_MASKINTERACTION_VISIBLE_INSIDE, ref maskMaterials.materialsInsideMask);
}
private bool InitSpriteMaskMaterialsOutsideMask()
{
return InitSpriteMaskMaterialsForMaskType(STENCIL_COMP_MASKINTERACTION_VISIBLE_OUTSIDE, ref maskMaterials.materialsOutsideMask);
}
private bool InitSpriteMaskMaterialsForMaskType(UnityEngine.Rendering.CompareFunction maskFunction, ref Material[] materialsToFill)
{
#if UNITY_EDITOR
if (!Application.isPlaying) {
return false;
}
#endif
var originalMaterials = maskMaterials.materialsMaskDisabled;
materialsToFill = new Material[originalMaterials.Length];
for (int i = 0; i < originalMaterials.Length; i++) {
Material newMaterial = new Material(originalMaterials[i]);
newMaterial.SetFloat(STENCIL_COMP_PARAM_ID, (int)maskFunction);
materialsToFill[i] = newMaterial;
}
return true;
}
#if UNITY_EDITOR
private void EditorFixStencilCompParameters() {
if (!haveStencilParametersBeenFixed && HasAnyStencilComp0Material()) {
haveStencilParametersBeenFixed = true;
FixAllProjectMaterialsStencilCompParameters();
}
}
private void FixAllProjectMaterialsStencilCompParameters() {
string[] materialGUIDS = UnityEditor.AssetDatabase.FindAssets("t:material");
foreach (var guid in materialGUIDS) {
string path = UnityEditor.AssetDatabase.GUIDToAssetPath(guid);
if (!string.IsNullOrEmpty(path)) {
var 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 (var 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
#endif //#if BUILT_IN_SPRITE_MASK_COMPONENT
}
}

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: e075b9a3e08e2f74fbd651c858ab16ed
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: