SkeletonRagdoll2D.cs 15 KB

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