using UnityEngine; namespace CreatureControl { public class Creature : MonoBehaviour { /// /// The (hopefully rigged) 3D model of the creature /// public Transform model; /// The target bones rig /// The target bones rig contain the target pose of the creature /// The creature movements will try to move the creature such that the target pose is reached /// as closely as possible public TargetRig targetRig; /// /// The positional different between the target rig and model root /// public Vector3 targetToModelTranslation; /// /// The rotational difference between the target rig and the model root /// public Quaternion targetToModelRotation; public Animator animator; public string animationsPath = "Assets"; /// /// The maximum height of objects from the ground which do not stop the creature /// public float stepOffset = 0.3F; /// /// If there is not static object below the feet of the avatar the avatar will fall down until it reaches solid ground /// public bool useGravity = true; [Range(0f, 1f)] public float slopeAlignment = 0.3f; /// /// The velocity caused by gravity /// [HideInInspector] protected Vector3 fallSpeed; #region Init protected virtual void Awake() { if (this.targetRig != null) this.animator = this.targetRig.GetComponent(); CheckColliders(); } /// /// Ensure a target rig is available /// /// The name of the target rig resource /// True when the target rig has been updated /// The parameter is used to instantiate a new target rig when none has been found. public bool CheckTargetRig(string targetRigResourceName) { if (this.targetRig != null) return false; // See if there is a target rig, but we just haven't found it this.targetRig = this.GetComponentInChildren(); if (this.targetRig == null) { // Nope, there is no target rig, so instantiate it using the given resource name GameObject targetsRigPrefab = Resources.Load(targetRigResourceName); GameObject targetRig = Instantiate(targetsRigPrefab); targetRig.name = "Target Rig"; targetRig.transform.SetPositionAndRotation(this.transform.position, this.transform.rotation); targetRig.transform.SetParent(this.transform); this.targetRig = targetRig.GetComponent(); } return true; } /// /// Ensure a target rig is available /// /// True when the target rig has been updated /// This tries to instantiate the default target rig resource public virtual bool CheckTargetRig() { return CheckTargetRig("TargetRig"); } /// /// Ensure that the creature rig is available /// /// True when the creature rig has been updated public bool CheckModel() { if (this.model != null) return false; // We determine the model root as the parent of the renderers SkinnedMeshRenderer[] skinnedMeshRenderers = this.GetComponentsInChildren(); foreach (SkinnedMeshRenderer skinnedMeshRenderer in skinnedMeshRenderers) { Transform rendererParent = skinnedMeshRenderer.transform.parent; if (this.model == null || this.model == rendererParent) this.model = rendererParent; else // Oops! There are multiple renders with different parents.... Debug.LogWarning("Unclear model root"); // We still return a model root, but this may not be the correct one... } return this.model != null; } #endregion Init #region Start /// /// Start the creature /// protected virtual void Start() { this.CheckTargetRig(); this.CheckModel(); this.targetRig.MatchTo(this); } #endregion Start #region Update /// /// Update the creature /// public virtual void Update() { if (this.targetRig == null) // Without a target rig, the creature cannot move return; UpdatePose(); // copy animator root motion to the creature //this.transform.SetPositionAndRotation(targetRig.transform.position, targetRig.transform.rotation); // As target rig is probably a child of this.transform, // We need to restore the position/rotation of the targetsRig. //targetRig.transform.SetPositionAndRotation(this.transform.position, this.transform.rotation); } /// /// Update the pose of the creature using the target rig /// public void UpdatePose() { if (this.targetRig == null) return; this.targetRig.Pose(); UpdateModel(); } /// /// Update the bones of the creature's rig from the target rig pose /// public virtual void UpdateModel() { if (this.model == null) return; this.targetRig.transform.GetPositionAndRotation(out Vector3 targetRigPosition, out Quaternion targetRigOrientation); Vector3 newPosition = targetRigPosition + this.targetToModelTranslation; Quaternion newOrientation = targetRigOrientation * this.targetToModelRotation; this.model.SetPositionAndRotation(newPosition, newOrientation); // This leads to problems when the targetRig is a child of the model // (which is often the case) // to prevent this, we restore the targetRig pose this.targetRig.transform.SetPositionAndRotation(targetRigPosition, targetRigOrientation); } #region Collisions [HideInInspector] public Rigidbody creatureRigidbody; [HideInInspector] public CapsuleCollider bodyCollider; [HideInInspector] public float colliderToGround; private void CheckColliders() { creatureRigidbody = this.GetComponent(); if (bodyCollider != null) return; bodyCollider = this.GetComponent(); // Assuming transform.position.y is on the ground if (bodyCollider != null) { Vector3 centerWorld = transform.TransformPoint(bodyCollider.center); colliderToGround = centerWorld.y - transform.position.y; colliderToGround -= bodyCollider.direction switch { 0 => bodyCollider.radius, 1 => bodyCollider.height / 2, _ => bodyCollider.radius }; } } #endregion Collisions #region Ground /// /// The ground Transform on which the pawn is standing /// /// When the pawn is not standing on the ground, the value is null public Transform ground; protected readonly float rotationSmoothTime = 0.06f; protected void CheckGrounded() { Vector3 footBase = this.transform.position; float distance = GetDistanceToGround(stepOffset, out ground, out Vector3 groundNormal); if (distance <= 0) { // We are on or under the ground this.fallSpeed = Vector3.zero; if (distance <= -stepOffset) Debug.Log($"d: {-distance}"); // Ensure that we are on the ground again if (creatureRigidbody != null && creatureRigidbody.isKinematic == false) { float requiredV0 = (-distance - 0.5f * Physics.gravity.y * Time.fixedDeltaTime * Time.fixedDeltaTime) / Time.fixedDeltaTime; requiredV0 = Mathf.Clamp(requiredV0, -20, 20); Vector3 v = creatureRigidbody.velocity; v.y = requiredV0; creatureRigidbody.velocity = v; } else transform.Translate(0, -distance, 0, Space.World); } else { // We are above the ground ground = null; if (useGravity) Fall(distance); } if (slopeAlignment > 0f) { Vector3 forward = Vector3.ProjectOnPlane(this.transform.forward, groundNormal).normalized; if (forward.sqrMagnitude < 0.001f) forward = this.transform.forward; Quaternion targetRot = Quaternion.LookRotation(forward, groundNormal); // blend between no-tilt and full alignment targetRot = Quaternion.Slerp(this.transform.rotation, targetRot, slopeAlignment); this.transform.rotation = Quaternion.Slerp(this.transform.rotation, targetRot, 1f - Mathf.Exp(-Time.deltaTime / rotationSmoothTime)); } } protected void Fall(float distanceToGround) { Vector3 translation = fallSpeed * Time.deltaTime; if (translation.magnitude > distanceToGround) translation = Physics.gravity.normalized * distanceToGround; transform.Translate(translation); fallSpeed += Physics.gravity * Time.deltaTime; } public float GetDistanceToGround(float maxDistance, out Transform ground, out Vector3 normal) { normal = -Physics.gravity.normalized; Vector3 rayDirection = normal; int layerMask = Physics.DefaultRaycastLayers; float distance = maxDistance; RaycastHit[] hits; // - Why use Ray/CapsuleCast All? // Because I need to ignore my own colliders and // I don't want to force users to add some layer before this can be used if (bodyCollider != null) { Vector3 centerWorld = transform.TransformPoint(bodyCollider.center); float half = (bodyCollider.height * 0.5f) - bodyCollider.radius; Vector3 up = bodyCollider.direction switch { 0 => transform.TransformDirection(Vector3.right), 1 => transform.TransformDirection(Vector3.up), _ => transform.TransformDirection(Vector3.forward), }; Vector3 p1 = centerWorld + up * half + normal * maxDistance; // top point Vector3 p2 = centerWorld - up * half + normal * maxDistance; // bottom point // Debug.DrawRay(p1, 2 * maxDistance * -rayDirection, Color.magenta); // Debug.DrawRay(p2, 2 * maxDistance * -rayDirection, Color.magenta); hits = Physics.CapsuleCastAll(p1, p2, bodyCollider.radius, -rayDirection, maxDistance * 2, layerMask, QueryTriggerInteraction.Ignore); maxDistance += colliderToGround; } else { Vector3 rayStart = this.transform.position - normal * maxDistance; // Debug.DrawRay(rayStart, 2 * maxDistance * rayDirection, Color.magenta); hits = Physics.RaycastAll(rayStart, rayDirection, maxDistance * 2, layerMask, QueryTriggerInteraction.Ignore); } if (hits.Length == 0) { ground = null; return maxDistance; } int closestHitIx = 0; bool foundClosest = false; for (int i = 0; i < hits.Length; i++) { if (hits[i].rigidbody == null && hits[i].distance > 0 && hits[i].distance <= hits[closestHitIx].distance) { closestHitIx = i; foundClosest = true; } } if (!foundClosest) { ground = null; return maxDistance; } RaycastHit closestHit = hits[closestHitIx]; //Debug.Log($"hit.distance {closestHit.distance} - {maxDistance}"); ground = closestHit.transform; normal = closestHit.normal; distance = closestHit.distance - maxDistance; return distance; } #endregion Ground #endregion Update #region Scene view /// /// Update the pose of the creature when the application is not running /// private void OnDrawGizmos() { // This ensures that the model is always following the target rig if (Application.isPlaying == false) { this.UpdatePose(); } } #endregion Scene view } }