SkeletonRagdoll2D.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. /******************************************************************************
  2. * Spine Runtimes License Agreement
  3. * Last updated January 1, 2020. Replaces all prior versions.
  4. *
  5. * Copyright (c) 2013-2020, Esoteric Software LLC
  6. *
  7. * Integration of the Spine Runtimes into software or otherwise creating
  8. * derivative works of the Spine Runtimes is permitted under the terms and
  9. * conditions of Section 2 of the Spine Editor License Agreement:
  10. * http://esotericsoftware.com/spine-editor-license
  11. *
  12. * Otherwise, it is permitted to integrate the Spine Runtimes into software
  13. * or otherwise create derivative works of the Spine Runtimes (collectively,
  14. * "Products"), provided that each user of the Products must obtain their own
  15. * Spine Editor license and redistribution of the Products in any form must
  16. * include this license and copyright notice.
  17. *
  18. * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
  19. * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  20. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  21. * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
  22. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  23. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
  24. * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
  25. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  26. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
  27. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  28. *****************************************************************************/
  29. // Contributed by: Mitch Thompson
  30. #if UNITY_2019_2 || UNITY_2019_3 || UNITY_2019_4 || UNITY_2020_1 || UNITY_2020_2 // note: 2020.3+ uses old bahavior again
  31. #define HINGE_JOINT_2019_BEHAVIOUR
  32. #endif
  33. using System.Collections;
  34. using System.Collections.Generic;
  35. using UnityEngine;
  36. namespace Spine.Unity.Examples {
  37. [RequireComponent(typeof(SkeletonRenderer))]
  38. public class SkeletonRagdoll2D : MonoBehaviour {
  39. static Transform parentSpaceHelper;
  40. #region Inspector
  41. [Header("Hierarchy")]
  42. [SpineBone]
  43. public string startingBoneName = "";
  44. [SpineBone]
  45. public List<string> stopBoneNames = new List<string>();
  46. [Header("Parameters")]
  47. public bool applyOnStart;
  48. [Tooltip("Warning! You will have to re-enable and tune mix values manually if attempting to remove the ragdoll system.")]
  49. public bool disableIK = true;
  50. public bool disableOtherConstraints = false;
  51. [Space]
  52. [Tooltip("Set RootRigidbody IsKinematic to true when Apply is called.")]
  53. public bool pinStartBone;
  54. public float gravityScale = 1;
  55. [Tooltip("If no BoundingBox Attachment is attached to a bone, this becomes the default Width or Radius of a Bone's ragdoll Rigidbody")]
  56. public float thickness = 0.125f;
  57. [Tooltip("Default rotational limit value. Min is negative this value, Max is this value.")]
  58. public float rotationLimit = 20;
  59. public float rootMass = 20;
  60. [Tooltip("If your ragdoll seems unstable or uneffected by limits, try lowering this value.")]
  61. [Range(0.01f, 1f)]
  62. public float massFalloffFactor = 0.4f;
  63. [Tooltip("The layer assigned to all of the rigidbody parts.")]
  64. [SkeletonRagdoll.LayerField]
  65. public int colliderLayer = 0;
  66. [Range(0, 1)]
  67. public float mix = 1;
  68. public bool oldRagdollBehaviour = false;
  69. #endregion
  70. ISkeletonAnimation targetSkeletonComponent;
  71. Skeleton skeleton;
  72. struct BoneFlipEntry {
  73. public BoneFlipEntry (bool flipX, bool flipY) {
  74. this.flipX = flipX;
  75. this.flipY = flipY;
  76. }
  77. public bool flipX;
  78. public bool flipY;
  79. }
  80. Dictionary<Bone, Transform> boneTable = new Dictionary<Bone, Transform>();
  81. Dictionary<Bone, BoneFlipEntry> boneFlipTable = new Dictionary<Bone, BoneFlipEntry>();
  82. Transform ragdollRoot;
  83. public Rigidbody2D RootRigidbody { get; private set; }
  84. public Bone StartingBone { get; private set; }
  85. Vector2 rootOffset;
  86. public Vector3 RootOffset { get { return this.rootOffset; } }
  87. bool isActive;
  88. public bool IsActive { get { return this.isActive; } }
  89. IEnumerator Start () {
  90. if (parentSpaceHelper == null) {
  91. parentSpaceHelper = (new GameObject("Parent Space Helper")).transform;
  92. }
  93. targetSkeletonComponent = GetComponent<SkeletonRenderer>() as ISkeletonAnimation;
  94. if (targetSkeletonComponent == null) Debug.LogError("Attached Spine component does not implement ISkeletonAnimation. This script is not compatible.");
  95. skeleton = targetSkeletonComponent.Skeleton;
  96. if (applyOnStart) {
  97. yield return null;
  98. Apply();
  99. }
  100. }
  101. #region API
  102. public Rigidbody2D[] RigidbodyArray {
  103. get {
  104. if (!isActive)
  105. return new Rigidbody2D[0];
  106. var rigidBodies = new Rigidbody2D[boneTable.Count];
  107. int i = 0;
  108. foreach (Transform t in boneTable.Values) {
  109. rigidBodies[i] = t.GetComponent<Rigidbody2D>();
  110. i++;
  111. }
  112. return rigidBodies;
  113. }
  114. }
  115. public Vector3 EstimatedSkeletonPosition {
  116. get { return this.RootRigidbody.position - rootOffset; }
  117. }
  118. /// <summary>Instantiates the ragdoll simulation and applies its transforms to the skeleton.</summary>
  119. public void Apply () {
  120. isActive = true;
  121. mix = 1;
  122. Bone startingBone = this.StartingBone = skeleton.FindBone(startingBoneName);
  123. RecursivelyCreateBoneProxies(startingBone);
  124. RootRigidbody = boneTable[startingBone].GetComponent<Rigidbody2D>();
  125. RootRigidbody.isKinematic = pinStartBone;
  126. RootRigidbody.mass = rootMass;
  127. var boneColliders = new List<Collider2D>();
  128. foreach (var pair in boneTable) {
  129. var b = pair.Key;
  130. var t = pair.Value;
  131. Transform parentTransform;
  132. boneColliders.Add(t.GetComponent<Collider2D>());
  133. if (b == startingBone) {
  134. ragdollRoot = new GameObject("RagdollRoot").transform;
  135. ragdollRoot.SetParent(transform, false);
  136. if (b == skeleton.RootBone) { // RagdollRoot is skeleton root's parent, thus the skeleton's scale and position.
  137. ragdollRoot.localPosition = new Vector3(skeleton.X, skeleton.Y, 0);
  138. ragdollRoot.localRotation = (skeleton.ScaleX < 0) ? Quaternion.Euler(0, 0, 180.0f) : Quaternion.identity;
  139. } else {
  140. ragdollRoot.localPosition = new Vector3(b.Parent.WorldX, b.Parent.WorldY, 0);
  141. ragdollRoot.localRotation = Quaternion.Euler(0, 0, b.Parent.WorldRotationX - b.Parent.ShearX);
  142. }
  143. parentTransform = ragdollRoot;
  144. rootOffset = t.position - transform.position;
  145. } else {
  146. parentTransform = boneTable[b.Parent];
  147. }
  148. // Add joint and attach to parent.
  149. var rbParent = parentTransform.GetComponent<Rigidbody2D>();
  150. if (rbParent != null) {
  151. var joint = t.gameObject.AddComponent<HingeJoint2D>();
  152. joint.connectedBody = rbParent;
  153. Vector3 localPos = parentTransform.InverseTransformPoint(t.position);
  154. joint.connectedAnchor = localPos;
  155. joint.GetComponent<Rigidbody2D>().mass = joint.connectedBody.mass * massFalloffFactor;
  156. #if HINGE_JOINT_2019_BEHAVIOUR
  157. float referenceAngle = (rbParent.transform.eulerAngles.z - t.eulerAngles.z + 360f) % 360f;
  158. float minAngle = referenceAngle - rotationLimit;
  159. float maxAngle = referenceAngle + rotationLimit;
  160. if (maxAngle > 180f) {
  161. minAngle -= 360f;
  162. maxAngle -= 360f;
  163. }
  164. #else
  165. float minAngle = -rotationLimit;
  166. float maxAngle = rotationLimit;
  167. #endif
  168. joint.limits = new JointAngleLimits2D {
  169. min = minAngle,
  170. max = maxAngle
  171. };
  172. joint.useLimits = true;
  173. }
  174. }
  175. // Ignore collisions among bones.
  176. for (int x = 0; x < boneColliders.Count; x++) {
  177. for (int y = 0; y < boneColliders.Count; y++) {
  178. if (x == y) continue;
  179. Physics2D.IgnoreCollision(boneColliders[x], boneColliders[y]);
  180. }
  181. }
  182. // Destroy existing override-mode SkeletonUtility bones.
  183. var utilityBones = GetComponentsInChildren<SkeletonUtilityBone>();
  184. if (utilityBones.Length > 0) {
  185. var destroyedUtilityBoneNames = new List<string>();
  186. foreach (var ub in utilityBones) {
  187. if (ub.mode == SkeletonUtilityBone.Mode.Override) {
  188. destroyedUtilityBoneNames.Add(ub.gameObject.name);
  189. Destroy(ub.gameObject);
  190. }
  191. }
  192. if (destroyedUtilityBoneNames.Count > 0) {
  193. string msg = "Destroyed Utility Bones: ";
  194. for (int i = 0; i < destroyedUtilityBoneNames.Count; i++) {
  195. msg += destroyedUtilityBoneNames[i];
  196. if (i != destroyedUtilityBoneNames.Count - 1) {
  197. msg += ",";
  198. }
  199. }
  200. Debug.LogWarning(msg);
  201. }
  202. }
  203. // Disable skeleton constraints.
  204. if (disableIK) {
  205. var ikConstraints = skeleton.IkConstraints;
  206. for (int i = 0, n = ikConstraints.Count; i < n; i++)
  207. ikConstraints.Items[i].Mix = 0;
  208. }
  209. if (disableOtherConstraints) {
  210. var transformConstraints = skeleton.TransformConstraints;
  211. for (int i = 0, n = transformConstraints.Count; i < n; i++) {
  212. transformConstraints.Items[i].MixRotate = 0;
  213. transformConstraints.Items[i].MixScaleX = 0;
  214. transformConstraints.Items[i].MixScaleY = 0;
  215. transformConstraints.Items[i].MixShearY = 0;
  216. transformConstraints.Items[i].MixX = 0;
  217. transformConstraints.Items[i].MixY = 0;
  218. }
  219. var pathConstraints = skeleton.PathConstraints;
  220. for (int i = 0, n = pathConstraints.Count; i < n; i++) {
  221. pathConstraints.Items[i].MixRotate = 0;
  222. pathConstraints.Items[i].MixX = 0;
  223. pathConstraints.Items[i].MixY = 0;
  224. }
  225. }
  226. targetSkeletonComponent.UpdateWorld += UpdateSpineSkeleton;
  227. }
  228. /// <summary>Transitions the mix value from the current value to a target value.</summary>
  229. public Coroutine SmoothMix (float target, float duration) {
  230. return StartCoroutine(SmoothMixCoroutine(target, duration));
  231. }
  232. IEnumerator SmoothMixCoroutine (float target, float duration) {
  233. float startTime = Time.time;
  234. float startMix = mix;
  235. while (mix > 0) {
  236. skeleton.SetBonesToSetupPose();
  237. mix = Mathf.SmoothStep(startMix, target, (Time.time - startTime) / duration);
  238. yield return null;
  239. }
  240. }
  241. /// <summary>Set the transform world position while preserving the ragdoll parts world position.</summary>
  242. public void SetSkeletonPosition (Vector3 worldPosition) {
  243. if (!isActive) {
  244. Debug.LogWarning("Can't call SetSkeletonPosition while Ragdoll is not active!");
  245. return;
  246. }
  247. Vector3 offset = worldPosition - transform.position;
  248. transform.position = worldPosition;
  249. foreach (Transform t in boneTable.Values)
  250. t.position -= offset;
  251. UpdateSpineSkeleton(null);
  252. skeleton.UpdateWorldTransform();
  253. }
  254. /// <summary>Removes the ragdoll instance and effect from the animated skeleton.</summary>
  255. public void Remove () {
  256. isActive = false;
  257. foreach (var t in boneTable.Values)
  258. Destroy(t.gameObject);
  259. Destroy(ragdollRoot.gameObject);
  260. boneTable.Clear();
  261. targetSkeletonComponent.UpdateWorld -= UpdateSpineSkeleton;
  262. }
  263. public Rigidbody2D GetRigidbody (string boneName) {
  264. var bone = skeleton.FindBone(boneName);
  265. return (bone != null && boneTable.ContainsKey(bone)) ? boneTable[bone].GetComponent<Rigidbody2D>() : null;
  266. }
  267. #endregion
  268. /// <summary>Generates the ragdoll simulation's Transform and joint setup.</summary>
  269. void RecursivelyCreateBoneProxies (Bone b) {
  270. string boneName = b.Data.Name;
  271. if (stopBoneNames.Contains(boneName))
  272. return;
  273. var boneGameObject = new GameObject(boneName);
  274. boneGameObject.layer = this.colliderLayer;
  275. Transform t = boneGameObject.transform;
  276. boneTable.Add(b, t);
  277. t.parent = transform;
  278. t.localPosition = new Vector3(b.WorldX, b.WorldY, 0);
  279. t.localRotation = Quaternion.Euler(0, 0, b.WorldRotationX - b.ShearX);
  280. t.localScale = new Vector3(b.WorldScaleX, b.WorldScaleY, 1);
  281. var colliders = AttachBoundingBoxRagdollColliders(b, boneGameObject, skeleton, this.gravityScale);
  282. if (colliders.Count == 0) {
  283. float length = b.Data.Length;
  284. if (length == 0) {
  285. var circle = boneGameObject.AddComponent<CircleCollider2D>();
  286. circle.radius = thickness * 0.5f;
  287. } else {
  288. var box = boneGameObject.AddComponent<BoxCollider2D>();
  289. box.size = new Vector2(length, thickness);
  290. box.offset = new Vector2(length * 0.5f, 0); // box.center in UNITY_4
  291. }
  292. }
  293. var rb = boneGameObject.GetComponent<Rigidbody2D>();
  294. if (rb == null) rb = boneGameObject.AddComponent<Rigidbody2D>();
  295. rb.gravityScale = this.gravityScale;
  296. foreach (Bone child in b.Children)
  297. RecursivelyCreateBoneProxies(child);
  298. }
  299. /// <summary>Performed every skeleton animation update to translate Unity Transforms positions into Spine bone transforms.</summary>
  300. void UpdateSpineSkeleton (ISkeletonAnimation animatedSkeleton) {
  301. bool parentFlipX;
  302. bool parentFlipY;
  303. var startingBone = this.StartingBone;
  304. GetStartBoneParentFlipState(out parentFlipX, out parentFlipY);
  305. foreach (var pair in boneTable) {
  306. var b = pair.Key;
  307. var t = pair.Value;
  308. bool isStartingBone = (b == startingBone);
  309. var parentBone = b.Parent;
  310. Transform parentTransform = isStartingBone ? ragdollRoot : boneTable[parentBone];
  311. if (!isStartingBone) {
  312. var parentBoneFlip = boneFlipTable[parentBone];
  313. parentFlipX = parentBoneFlip.flipX;
  314. parentFlipY = parentBoneFlip.flipY;
  315. }
  316. bool flipX = parentFlipX ^ (b.ScaleX < 0);
  317. bool flipY = parentFlipY ^ (b.ScaleY < 0);
  318. BoneFlipEntry boneFlip;
  319. boneFlipTable.TryGetValue(b, out boneFlip);
  320. boneFlip.flipX = flipX;
  321. boneFlip.flipY = flipY;
  322. boneFlipTable[b] = boneFlip;
  323. bool flipXOR = flipX ^ flipY;
  324. bool parentFlipXOR = parentFlipX ^ parentFlipY;
  325. if (!oldRagdollBehaviour && isStartingBone) {
  326. if (b != skeleton.RootBone) { // RagdollRoot is not skeleton root.
  327. ragdollRoot.localPosition = new Vector3(parentBone.WorldX, parentBone.WorldY, 0);
  328. ragdollRoot.localRotation = Quaternion.Euler(0, 0, parentBone.WorldRotationX - parentBone.ShearX);
  329. ragdollRoot.localScale = new Vector3(parentBone.WorldScaleX, parentBone.WorldScaleY, 1);
  330. }
  331. }
  332. Vector3 parentTransformWorldPosition = parentTransform.position;
  333. Quaternion parentTransformWorldRotation = parentTransform.rotation;
  334. parentSpaceHelper.position = parentTransformWorldPosition;
  335. parentSpaceHelper.rotation = parentTransformWorldRotation;
  336. parentSpaceHelper.localScale = parentTransform.lossyScale;
  337. if (oldRagdollBehaviour) {
  338. if (isStartingBone && b != skeleton.RootBone) {
  339. Vector3 localPosition = new Vector3(b.Parent.WorldX, b.Parent.WorldY, 0);
  340. parentSpaceHelper.position = ragdollRoot.TransformPoint(localPosition);
  341. parentSpaceHelper.localRotation = Quaternion.Euler(0, 0, parentBone.WorldRotationX - parentBone.ShearX);
  342. parentSpaceHelper.localScale = new Vector3(parentBone.WorldScaleX, parentBone.WorldScaleY, 1);
  343. }
  344. }
  345. Vector3 boneWorldPosition = t.position;
  346. Vector3 right = parentSpaceHelper.InverseTransformDirection(t.right);
  347. Vector3 boneLocalPosition = parentSpaceHelper.InverseTransformPoint(boneWorldPosition);
  348. float boneLocalRotation = Mathf.Atan2(right.y, right.x) * Mathf.Rad2Deg;
  349. if (flipXOR) boneLocalPosition.y *= -1f;
  350. if (parentFlipXOR != flipXOR) boneLocalPosition.y *= -1f;
  351. if (parentFlipXOR) boneLocalRotation *= -1f;
  352. if (parentFlipX != flipX) boneLocalRotation += 180;
  353. b.X = Mathf.Lerp(b.X, boneLocalPosition.x, mix);
  354. b.Y = Mathf.Lerp(b.Y, boneLocalPosition.y, mix);
  355. b.Rotation = Mathf.Lerp(b.Rotation, boneLocalRotation, mix);
  356. //b.AppliedRotation = Mathf.Lerp(b.AppliedRotation, boneLocalRotation, mix);
  357. }
  358. }
  359. void GetStartBoneParentFlipState (out bool parentFlipX, out bool parentFlipY) {
  360. parentFlipX = skeleton.ScaleX < 0;
  361. parentFlipY = skeleton.ScaleY < 0;
  362. var parent = this.StartingBone == null ? null : this.StartingBone.Parent;
  363. while (parent != null) {
  364. parentFlipX ^= parent.ScaleX < 0;
  365. parentFlipY ^= parent.ScaleY < 0;
  366. parent = parent.Parent;
  367. }
  368. }
  369. static List<Collider2D> AttachBoundingBoxRagdollColliders (Bone b, GameObject go, Skeleton skeleton, float gravityScale) {
  370. const string AttachmentNameMarker = "ragdoll";
  371. var colliders = new List<Collider2D>();
  372. var skin = skeleton.Skin ?? skeleton.Data.DefaultSkin;
  373. var skinEntries = new List<Skin.SkinEntry>();
  374. foreach (Slot slot in skeleton.Slots) {
  375. if (slot.Bone == b) {
  376. skin.GetAttachments(skeleton.Slots.IndexOf(slot), skinEntries);
  377. bool bbAttachmentAdded = false;
  378. foreach (var entry in skinEntries) {
  379. var bbAttachment = entry.Attachment as BoundingBoxAttachment;
  380. if (bbAttachment != null) {
  381. if (!entry.Name.ToLower().Contains(AttachmentNameMarker))
  382. continue;
  383. bbAttachmentAdded = true;
  384. var bbCollider = SkeletonUtility.AddBoundingBoxAsComponent(bbAttachment, slot, go, isTrigger: false);
  385. colliders.Add(bbCollider);
  386. }
  387. }
  388. if (bbAttachmentAdded)
  389. SkeletonUtility.AddBoneRigidbody2D(go, isKinematic: false, gravityScale: gravityScale);
  390. }
  391. }
  392. return colliders;
  393. }
  394. static Vector3 FlipScale (bool flipX, bool flipY) {
  395. return new Vector3(flipX ? -1f : 1f, flipY ? -1f : 1f, 1f);
  396. }
  397. #if UNITY_EDITOR
  398. void OnDrawGizmosSelected () {
  399. if (isActive) {
  400. Gizmos.DrawWireSphere(transform.position, thickness * 1.2f);
  401. Vector3 newTransformPos = RootRigidbody.position - rootOffset;
  402. Gizmos.DrawLine(transform.position, newTransformPos);
  403. Gizmos.DrawWireSphere(newTransformPos, thickness * 1.2f);
  404. }
  405. }
  406. #endif
  407. }
  408. }