/****************************************************************************** * Spine Runtimes License Agreement * Last updated April 5, 2025. Replaces all prior versions. * * Copyright (c) 2013-2025, 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_5_3_OR_NEWER #define IS_UNITY #endif using System; namespace Spine { #if IS_UNITY using Color32F = UnityEngine.Color; #endif public class Skeleton { static private readonly int[] quadTriangles = { 0, 1, 2, 2, 3, 0 }; internal SkeletonData data; internal ExposedList bones; internal ExposedList slots; internal ExposedList drawOrder; internal ExposedList constraints; internal ExposedList physics; internal ExposedList updateCache = new ExposedList(); internal ExposedList resetCache = new ExposedList(16); internal Skin skin; // Color is a struct, set to protected to prevent // Color32F color = slot.color; color.a = 0.5; // modifying just a copy of the struct instead of the original // object as in reference implementation. protected Color32F color; internal float x, y, scaleX = 1, time, windX = 1, windY = 0, gravityX = 0, gravityY = 1; /// Private to enforce usage of ScaleY getter taking Bone.yDown into account. private float scaleY = 1; internal int update; public Skeleton (SkeletonData data) { if (data == null) throw new ArgumentNullException("data", "data cannot be null."); this.data = data; bones = new ExposedList(data.bones.Count); Bone[] bonesItems = this.bones.Items; foreach (BoneData boneData in data.bones) { Bone bone; if (boneData.parent == null) { bone = new Bone(boneData, null); } else { Bone parent = bonesItems[boneData.parent.index]; bone = new Bone(boneData, parent); parent.children.Add(bone); } this.bones.Add(bone); } slots = new ExposedList(data.slots.Count); drawOrder = new ExposedList(data.slots.Count); foreach (SlotData slotData in data.slots) { Bone bone = bonesItems[slotData.boneData.index]; Slot slot = new Slot(slotData, this); slots.Add(slot); drawOrder.Add(slot); } physics = new ExposedList(8); constraints = new ExposedList(data.constraints.Count); foreach (IConstraintData constraintData in data.constraints) { IConstraint constraint = constraintData.Create(this); PhysicsConstraint physicsConstraint = constraint as PhysicsConstraint; if (physicsConstraint != null) physics.Add(physicsConstraint); constraints.Add(constraint); } physics.TrimExcess(); color = new Color32F(1, 1, 1, 1); UpdateCache(); } /// Copy constructor. public Skeleton (Skeleton skeleton) { if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null."); data = skeleton.data; bones = new ExposedList(skeleton.bones.Count); foreach (Bone bone in skeleton.bones) { Bone newBone; if (bone.parent == null) newBone = new Bone(bone, null); else { Bone parent = bones.Items[bone.parent.data.index]; newBone = new Bone(bone, parent); parent.children.Add(newBone); } bones.Add(newBone); } slots = new ExposedList(skeleton.slots.Count); Bone[] bonesItems = bones.Items; foreach (Slot slot in skeleton.slots) slots.Add(new Slot(slot, bonesItems[slot.bone.data.index], this)); drawOrder = new ExposedList(slots.Count); Slot[] slotsItems = slots.Items; foreach (Slot slot in skeleton.drawOrder) drawOrder.Add(slotsItems[slot.data.index]); physics = new ExposedList(skeleton.physics.Count); constraints = new ExposedList(skeleton.constraints.Count); foreach (IConstraint other in skeleton.constraints) { IConstraint constraint = other.Copy(this); PhysicsConstraint physicsConstraint = constraint as PhysicsConstraint; if (physicsConstraint != null) physics.Add(physicsConstraint); constraints.Add(constraint); } skin = skeleton.skin; color = skeleton.color; x = skeleton.x; y = skeleton.y; scaleX = skeleton.scaleX; scaleY = skeleton.scaleY; time = skeleton.time; UpdateCache(); } /// Caches information about bones and constraints. Must be called if the is modified or if bones, constraints, or /// constraints, or weighted path attachments are added or removed. public void UpdateCache () { updateCache.Clear(); resetCache.Clear(); Slot[] slots = this.slots.Items; for (int i = 0, n = this.slots.Count; i < n; i++) { ((IPosedInternal)slots[i]).UsePose(); } int boneCount = this.bones.Count; Bone[] bones = this.bones.Items; for (int i = 0; i < boneCount; i++) { Bone bone = bones[i]; bone.sorted = bone.data.skinRequired; bone.active = !bone.sorted; ((IPosedInternal)bone).UsePose(); } if (skin != null) { BoneData[] skinBones = skin.bones.Items; for (int i = 0, n = skin.bones.Count; i < n; i++) { Bone bone = bones[skinBones[i].index]; do { bone.sorted = false; bone.active = true; bone = bone.parent; } while (bone != null); } } IConstraint[] constraints = this.constraints.Items; { // scope added to prevent compile error of n already being declared in enclosing scope int n = this.constraints.Count; for (int i = 0; i < n; i++) { ((IPosedInternal)constraints[i]).UsePose(); } for (int i = 0; i < n; i++) { IConstraint constraint = constraints[i]; constraint.Active = constraint.IsSourceActive && (!constraint.IData.SkinRequired || (skin != null && skin.constraints.Contains(constraint.IData))); if (constraint.Active) constraint.Sort(this); } for (int i = 0; i < boneCount; i++) SortBone(bones[i]); object[] updateCache = this.updateCache.Items; n = this.updateCache.Count; for (int i = 0; i < n; i++) { Bone bone = updateCache[i] as Bone; if (bone != null) updateCache[i] = bone.applied; } } } internal void Constrained (IPosedInternal obj) { if (obj.PoseEqualsApplied) { // if (obj.pose == obj.applied) { obj.UseConstrained(); resetCache.Add(obj); } } internal void SortBone (Bone bone) { if (bone.sorted || !bone.active) return; Bone parent = bone.parent; if (parent != null) SortBone(parent); bone.sorted = true; updateCache.Add(bone); } internal void SortReset (ExposedList bones) { Bone[] items = bones.Items; for (int i = 0, n = bones.Count; i < n; i++) { Bone bone = items[i]; if (bone.active) { if (bone.sorted) SortReset(bone.children); bone.sorted = false; } } } /// /// Updates the world transform for each bone and applies all constraints. /// /// See World transforms in the Spine /// Runtimes Guide. /// public void UpdateWorldTransform (Physics physics) { update++; IPosedInternal[] resetCache = this.resetCache.Items; for (int i = 0, n = this.resetCache.Count; i < n; i++) { resetCache[i].ResetConstrained(); } object[] updateCache = this.updateCache.Items; for (int i = 0, n = this.updateCache.Count; i < n; i++) ((IUpdate)updateCache[i]).Update(this, physics); } /// Sets the bones, constraints, slots, and draw order to their setup pose values. public void SetupPose () { SetupPoseBones(); SetupPoseSlots(); } /// Sets the bones and constraints to their setup pose values. public void SetupPoseBones () { Bone[] bones = this.bones.Items; for (int i = 0, n = this.bones.Count; i < n; i++) bones[i].SetupPose(); IConstraint[] constraints = this.constraints.Items; for (int i = 0, n = this.constraints.Count; i < n; i++) constraints[i].SetupPose(); } /// Sets the slots and draw order to their setup pose values. public void SetupPoseSlots () { Slot[] slots = this.slots.Items; int n = this.slots.Count; Array.Copy(slots, 0, drawOrder.Items, 0, n); for (int i = 0; i < n; i++) slots[i].SetupPose(); } /// The skeleton's setup pose data. public SkeletonData Data { get { return data; } } /// The skeleton's bones, sorted parent first. The root bone is always the first bone. public ExposedList Bones { get { return bones; } } /// /// The list of bones and constraints, sorted in the order they should be updated, as computed by . /// public ExposedList UpdateCacheList { get { return updateCache; } } /// Returns the root bone, or null if the skeleton has no bones. public Bone RootBone { get { return bones.Count == 0 ? null : bones.Items[0]; } } /// Finds a bone by comparing each bone's name. It is more efficient to cache the results of this method than to call it /// repeatedly. /// May be null. public Bone FindBone (string boneName) { if (boneName == null) throw new ArgumentNullException("boneName", "boneName cannot be null."); Bone[] bones = this.bones.Items; for (int i = 0, n = this.bones.Count; i < n; i++) { Bone bone = bones[i]; if (bone.data.name == boneName) return bone; } return null; } /// The skeleton's slots. public ExposedList Slots { get { return slots; } } /// Finds a slot by comparing each slot's name. It is more efficient to cache the results of this method than to call it /// repeatedly. /// May be null. public Slot FindSlot (string slotName) { if (slotName == null) throw new ArgumentNullException("slotName", "slotName cannot be null."); Slot[] slots = this.slots.Items; for (int i = 0, n = this.slots.Count; i < n; i++) { Slot slot = slots[i]; if (slot.data.name == slotName) return slot; } return null; } /// /// The skeleton's slots in the order they should be drawn. The returned array may be modified to change the draw order. /// public ExposedList DrawOrder { get { return drawOrder; } set { if (value == null) throw new ArgumentNullException("drawOrder ", "drawOrder cannot be null."); this.drawOrder = value; } } /// The skeleton's current skin. May be null. See public Skin Skin { /// The skeleton's current skin. May be null. get { return skin; } /// Sets a skin, . set { SetSkin(value); } } /// Sets a skin by name (see ). public void SetSkin (string skinName) { Skin foundSkin = data.FindSkin(skinName); if (foundSkin == null) throw new ArgumentException("Skin not found: " + skinName, "skinName"); SetSkin(foundSkin); } /// /// Sets the skin used to look up attachments before looking in the . If the /// skin is changed, is called. /// /// Attachments from the new skin are attached if the corresponding attachment from the old skin was attached. If there was no /// old skin, each slot's setup mode attachment is attached from the new skin. /// /// After changing the skin, the visible attachments can be reset to those attached in the setup pose by calling /// . Also, often is called before the next time the skeleton is /// rendered to allow any attachment keys in the current animation(s) to hide or show attachments from the new skin. /// /// May be null. public void SetSkin (Skin newSkin) { if (newSkin == skin) return; if (newSkin != null) { if (skin != null) newSkin.AttachAll(this, skin); else { Slot[] slots = this.slots.Items; for (int i = 0, n = this.slots.Count; i < n; i++) { Slot slot = slots[i]; string name = slot.data.attachmentName; if (name != null) { Attachment attachment = newSkin.GetAttachment(i, name); if (attachment != null) slot.pose.Attachment = attachment; } } } } skin = newSkin; UpdateCache(); } /// Finds an attachment by looking in the and using the slot name and attachment /// name. /// May be null. /// public Attachment GetAttachment (string slotName, string attachmentName) { SlotData slot = data.FindSlot(slotName); if (slot == null) throw new ArgumentException("Slot not found: " + slotName, "slotName"); return GetAttachment(slot.index, attachmentName); } /// Finds an attachment by looking in the skin and skeletonData.defaultSkin using the slot index and /// attachment name. First the skin is checked and if the attachment was not found, the default skin is checked. /// /// See Runtime skins in the Spine Runtimes Guide. /// May be null. public Attachment GetAttachment (int slotIndex, string attachmentName) { if (attachmentName == null) throw new ArgumentNullException("attachmentName", "attachmentName cannot be null."); if (skin != null) { Attachment attachment = skin.GetAttachment(slotIndex, attachmentName); if (attachment != null) return attachment; } if (data.defaultSkin != null) return data.defaultSkin.GetAttachment(slotIndex, attachmentName); return null; } /// A convenience method to set an attachment by finding the slot with , finding the attachment with /// , then setting the slot's . /// May be null to clear the slot's attachment. public void SetAttachment (string slotName, string attachmentName) { if (slotName == null) throw new ArgumentNullException("slotName", "slotName cannot be null."); Slot slot = FindSlot(slotName); if (slot == null) throw new ArgumentException("Slot not found: " + slotName, "slotName"); Attachment attachment = null; if (attachmentName != null) { attachment = GetAttachment(slot.data.index, attachmentName); if (attachment == null) throw new ArgumentException("Attachment not found: " + attachmentName + ", for slot: " + slotName, "attachmentName"); } slot.pose.Attachment = attachment; } /// The skeleton's constraints. public ExposedList Constraints { get { return constraints; } } /// The skeleton's physics constraints. public ExposedList PhysicsConstraints { get { return physics; } } /// May be null. public T FindConstraint (string constraintName) where T : class, IConstraint { if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null."); IConstraint[] constraints = this.constraints.Items; for (int i = 0, n = this.constraints.Count; i < n; i++) { IConstraint constraint = constraints[i]; if (constraint is T && constraint.IData.Name == constraintName) return (T)constraint; } return null; } /// Returns the axis aligned bounding box (AABB) of the region and mesh attachments for the current pose. /// The horizontal distance between the skeleton origin and the left side of the AABB. /// The vertical distance between the skeleton origin and the bottom side of the AABB. /// The width of the AABB /// The height of the AABB. /// Reference to hold a float[]. May be a null reference. This method will assign it a new float[] with the appropriate size as needed. public void GetBounds (out float x, out float y, out float width, out float height, ref float[] vertexBuffer, SkeletonClipping clipper = null) { float[] temp = vertexBuffer; temp = temp ?? new float[8]; Slot[] drawOrder = this.drawOrder.Items; float minX = int.MaxValue, minY = int.MaxValue, maxX = int.MinValue, maxY = int.MinValue; for (int i = 0, n = this.drawOrder.Count; i < n; i++) { Slot slot = drawOrder[i]; if (!slot.bone.active) continue; int verticesLength = 0; float[] vertices = null; int[] triangles = null; Attachment attachment = slot.pose.attachment; RegionAttachment region = attachment as RegionAttachment; if (region != null) { verticesLength = 8; vertices = temp; if (vertices.Length < 8) vertices = temp = new float[8]; region.ComputeWorldVertices(slot, vertices, 0, 2); triangles = quadTriangles; } else { MeshAttachment mesh = attachment as MeshAttachment; if (mesh != null) { verticesLength = mesh.WorldVerticesLength; vertices = temp; if (vertices.Length < verticesLength) vertices = temp = new float[verticesLength]; mesh.ComputeWorldVertices(this, slot, 0, verticesLength, temp, 0, 2); triangles = mesh.Triangles; } else if (clipper != null) { ClippingAttachment clip = attachment as ClippingAttachment; if (clip != null) { clipper.ClipEnd(slot); clipper.ClipStart(this, slot, clip); continue; } } } if (vertices != null) { if (clipper != null && clipper.IsClipping && clipper.ClipTriangles(vertices, triangles, triangles.Length)) { vertices = clipper.ClippedVertices.Items; verticesLength = clipper.ClippedVertices.Count; } for (int ii = 0; ii < verticesLength; ii += 2) { float vx = vertices[ii], vy = vertices[ii + 1]; minX = Math.Min(minX, vx); minY = Math.Min(minY, vy); maxX = Math.Max(maxX, vx); maxY = Math.Max(maxY, vy); } } if (clipper != null) clipper.ClipEnd(slot); } if (clipper != null) clipper.ClipEnd(); x = minX; y = minY; width = maxX - minX; height = maxY - minY; vertexBuffer = temp; } /// A copy of the color to tint all the skeleton's attachments. public Color32F GetColor () { return color; } /// Sets the color to tint all the skeleton's attachments. public void SetColor (Color32F color) { this.color = color; } /// /// A convenience method for setting the skeleton color. The color can also be set by /// /// public void SetColor (float r, float g, float b, float a) { color = new Color32F(r, g, b, a); } /// Scales the entire skeleton on the X axis. /// /// Bones that do not inherit scale are still affected by this property. public float ScaleX { get { return scaleX; } set { scaleX = value; } } /// Scales the entire skeleton on the Y axis. /// /// Bones that do not inherit scale are still affected by this property. public float ScaleY { get { return scaleY * (Bone.yDown ? -1 : 1); } set { scaleY = value; } } /// /// Scales the entire skeleton on the X and Y axes. /// /// Bones that do not inherit scale are still affected by this property. /// public void SetScale (float scaleX, float scaleY) { this.scaleX = scaleX; this.scaleY = scaleY; } /// The skeleton X position, which is added to the root bone worldX position. /// /// Bones that do not inherit translation are still affected by this property. public float X { get { return x; } set { x = value; } } /// The skeleton Y position, which is added to the root bone worldY position. /// /// Bones that do not inherit translation are still affected by this property. public float Y { get { return y; } set { y = value; } } /// /// Sets the skeleton X and Y position, which is added to the root bone worldX and worldY position. /// /// Bones that do not inherit translation are still affected by this property. public void SetPosition (float x, float y) { this.x = x; this.y = y; } public float WindX { get { return windX; } set { windX = value; } } public float WindY { get { return windY; } set { windY = value; } } public float GravityX { get { return gravityX; } set { gravityX = value; } } public float GravityY { get { return gravityY; } set { gravityY = value; } } /// /// Calls for each physics constraint. /// public void PhysicsTranslate (float x, float y) { PhysicsConstraint[] physicsConstraints = this.physics.Items; for (int i = 0, n = this.physics.Count; i < n; i++) physicsConstraints[i].Translate(x, y); } /// /// Calls for each physics constraint. /// public void PhysicsRotate (float x, float y, float degrees) { PhysicsConstraint[] physicsConstraints = this.physics.Items; for (int i = 0, n = this.physics.Count; i < n; i++) physicsConstraints[i].Rotate(x, y, degrees); } /// Returns the skeleton's time. This is used for time-based manipulations, such as . /// public float Time { get { return time; } set { time = value; } } /// Increments the skeleton's . public void Update (float delta) { time += delta; } override public string ToString () { return data.name; } } }