using UnityEngine; using System.Collections; namespace RootMotion.FinalIK { /// /// Foot placement system. /// [System.Serializable] public partial class Grounding { #region Main Interface /// /// The raycasting quality. Fastest is a single raycast per foot, Simple is three raycasts, Best is one raycast and a capsule cast per foot. /// [System.Serializable] public enum Quality { Fastest, Simple, Best } /// /// Layers to ground the character to. Make sure to exclude the layer of the character controller. /// [Tooltip("Layers to ground the character to. Make sure to exclude the layer of the character controller.")] public LayerMask layers; /// /// Max step height. Maximum vertical distance of Grounding from the root of the character. /// [Tooltip("Max step height. Maximum vertical distance of Grounding from the root of the character.")] public float maxStep = 0.5f; /// /// The height offset of the root. /// [Tooltip("The height offset of the root.")] public float heightOffset; /// /// The speed of moving the feet up/down. /// [Tooltip("The speed of moving the feet up/down.")] public float footSpeed = 2.5f; /// /// CapsuleCast radius. Should match approximately with the size of the feet. /// [Tooltip("CapsuleCast radius. Should match approximately with the size of the feet.")] public float footRadius = 0.15f; /// /// Offset of the foot center along character forward axis. /// [Tooltip("Offset of the foot center along character forward axis.")] [HideInInspector] public float footCenterOffset; // TODO make visible in inspector if Grounder Visualization is finished. /// /// Amount of velocity based prediction of the foot positions. /// [Tooltip("Amount of velocity based prediction of the foot positions.")] public float prediction = 0.05f; /// /// Weight of rotating the feet to the ground normal offset. /// [Tooltip("Weight of rotating the feet to the ground normal offset.")] [Range(0f, 1f)] public float footRotationWeight = 1f; /// /// Speed of slerping the feet to their grounded rotations. /// [Tooltip("Speed of slerping the feet to their grounded rotations.")] public float footRotationSpeed = 7f; /// /// Max Foot Rotation Angle, Max angular offset from the foot's rotation (Reasonable range: 0-90 degrees). /// [Tooltip("Max Foot Rotation Angle. Max angular offset from the foot's rotation.")] [Range(0f, 90f)] public float maxFootRotationAngle = 45f; /// /// If true, solver will rotate with the character root so the character can be grounded for example to spherical planets. /// For performance reasons leave this off unless needed. /// [Tooltip("If true, solver will rotate with the character root so the character can be grounded for example to spherical planets. For performance reasons leave this off unless needed.")] public bool rotateSolver; /// /// The speed of moving the character up/down. /// [Tooltip("The speed of moving the character up/down.")] public float pelvisSpeed = 5f; /// /// Used for smoothing out vertical pelvis movement (range 0 - 1). /// [Tooltip("Used for smoothing out vertical pelvis movement (range 0 - 1).")] [Range(0f, 1f)] public float pelvisDamper; /// /// The weight of lowering the pelvis to the lowest foot. /// [Tooltip("The weight of lowering the pelvis to the lowest foot.")] public float lowerPelvisWeight = 1f; /// /// The weight of lifting the pelvis to the highest foot. This is useful when you don't want the feet to go too high relative to the body when crouching. /// [Tooltip("The weight of lifting the pelvis to the highest foot. This is useful when you don't want the feet to go too high relative to the body when crouching.")] public float liftPelvisWeight; /// /// The radius of the spherecast from the root that determines whether the character root is grounded. /// [Tooltip("The radius of the spherecast from the root that determines whether the character root is grounded.")] public float rootSphereCastRadius = 0.1f; /// /// If false, keeps the foot that is over a ledge at the root level. If true, lowers the overstepping foot and body by the 'Max Step' value. /// [Tooltip("If false, keeps the foot that is over a ledge at the root level. If true, lowers the overstepping foot and body by the 'Max Step' value.")] public bool overstepFallsDown = true; /// /// The raycasting quality. Fastest is a single raycast per foot, Simple is three raycasts, Best is one raycast and a capsule cast per foot. /// [Tooltip("The raycasting quality. Fastest is a single raycast per foot, Simple is three raycasts, Best is one raycast and a capsule cast per foot.")] public Quality quality = Quality.Best; /// /// The %Grounding legs. /// public Leg[] legs { get; private set; } /// /// The %Grounding pelvis. /// public Pelvis pelvis { get; private set; } /// /// Gets a value indicating whether any of the legs are grounded /// public bool isGrounded { get; private set; } /// /// The root Transform /// public Transform root { get; private set; } /// /// Ground height at the root position. /// public RaycastHit rootHit { get; private set; } /// /// Is the RaycastHit from the root grounded? /// public bool rootGrounded { get { return rootHit.distance < maxStep * 2f; } } /// /// Raycasts or sphereCasts to find the root ground point. Distance of the Ray/Sphere cast is maxDistanceMlp x maxStep. Use this instead of rootHit if the Grounder is weighed out/disabled and not updated. /// public RaycastHit GetRootHit(float maxDistanceMlp = 10f) { RaycastHit h = new RaycastHit(); Vector3 _up = up; Vector3 legsCenter = Vector3.zero; foreach (Leg leg in legs) legsCenter += leg.transform.position; legsCenter /= (float)legs.Length; h.point = legsCenter - _up * maxStep * 10f; float distMlp = maxDistanceMlp + 1; h.distance = maxStep * distMlp; if (maxStep <= 0f) return h; if (quality != Quality.Best) Physics.Raycast(legsCenter + _up * maxStep, -_up, out h, maxStep * distMlp, layers, QueryTriggerInteraction.Ignore); else Physics.SphereCast(legsCenter + _up * maxStep, rootSphereCastRadius, -up, out h, maxStep * distMlp, layers, QueryTriggerInteraction.Ignore); return h; } /// /// Gets a value indicating whether this is valid. /// public bool IsValid(ref string errorMessage) { if (root == null) { errorMessage = "Root transform is null. Can't initiate Grounding."; return false; } if (legs == null) { errorMessage = "Grounding legs is null. Can't initiate Grounding."; return false; } if (pelvis == null) { errorMessage = "Grounding pelvis is null. Can't initiate Grounding."; return false; } if (legs.Length == 0) { errorMessage = "Grounding has 0 legs. Can't initiate Grounding."; return false; } return true; } /// /// Initiate the %Grounding as an integrated solver by providing the root Transform, leg solvers, pelvis Transform and spine solver. /// public void Initiate(Transform root, Transform[] feet) { this.root = root; initiated = false; rootHit = new RaycastHit(); // Constructing Legs if (legs == null) legs = new Leg[feet.Length]; if (legs.Length != feet.Length) legs = new Leg[feet.Length]; for (int i = 0; i < feet.Length; i++) if (legs[i] == null) legs[i] = new Leg(); // Constructing pelvis if (pelvis == null) pelvis = new Pelvis(); string errorMessage = string.Empty; if (!IsValid(ref errorMessage)) { Warning.Log(errorMessage, root, false); return; } // Initiate solvers only if application is playing if (Application.isPlaying) { for (int i = 0; i < feet.Length; i++) legs[i].Initiate(this, feet[i]); pelvis.Initiate(this); initiated = true; } } /// /// Updates the Grounding. /// public void Update() { if (!initiated) return; if (layers == 0) LogWarning("Grounding layers are set to nothing. Please add a ground layer."); maxStep = Mathf.Clamp(maxStep, 0f, maxStep); footRadius = Mathf.Clamp(footRadius, 0.0001f, maxStep); pelvisDamper = Mathf.Clamp(pelvisDamper, 0f, 1f); rootSphereCastRadius = Mathf.Clamp(rootSphereCastRadius, 0.0001f, rootSphereCastRadius); maxFootRotationAngle = Mathf.Clamp(maxFootRotationAngle, 0f, 90f); prediction = Mathf.Clamp(prediction, 0f, prediction); footSpeed = Mathf.Clamp(footSpeed, 0f, footSpeed); // Root hit rootHit = GetRootHit(); float lowestOffset = Mathf.NegativeInfinity; float highestOffset = Mathf.Infinity; isGrounded = false; // Process legs foreach (Leg leg in legs) { leg.Process(); if (leg.IKOffset > lowestOffset) lowestOffset = leg.IKOffset; if (leg.IKOffset < highestOffset) highestOffset = leg.IKOffset; if (leg.isGrounded) isGrounded = true; } // Precess pelvis pelvis.Process(-lowestOffset * lowerPelvisWeight, -highestOffset * liftPelvisWeight, isGrounded); } // Calculate the normal of the plane defined by leg positions, so we know how to rotate the body public Vector3 GetLegsPlaneNormal() { if (!initiated) return Vector3.up; Vector3 _up = up; Vector3 normal = _up; // Go through all the legs, rotate the normal by it's offset for (int i = 0; i < legs.Length; i++) { // Direction from the root to the leg Vector3 legDirection = legs[i].IKPosition - root.position; // Find the tangent Vector3 legNormal = _up; Vector3 legTangent = legDirection; Vector3.OrthoNormalize(ref legNormal, ref legTangent); // Find the rotation offset from the tangent to the direction Quaternion fromTo = Quaternion.FromToRotation(legTangent, legDirection); // Rotate the normal normal = fromTo * normal; } return normal; } // Set everything to 0 public void Reset() { if (!Application.isPlaying) return; pelvis.Reset(); foreach (Leg leg in legs) leg.Reset(); } #endregion Main Interface private bool initiated; // Logs the warning if no other warning has beed logged in this session. public void LogWarning(string message) { Warning.Log(message, root); } // The up vector in solver rotation space. public Vector3 up { get { return (useRootRotation? root.up: Vector3.up); } } // Gets the vertical offset between two vectors in solver rotation space public float GetVerticalOffset(Vector3 p1, Vector3 p2) { if (useRootRotation) { Vector3 v = Quaternion.Inverse(root.rotation) * (p1 - p2); return v.y; } return p1.y - p2.y; } // Flattens a vector to ground plane in solver rotation space public Vector3 Flatten(Vector3 v) { if (useRootRotation) { Vector3 tangent = v; Vector3 normal = root.up; Vector3.OrthoNormalize(ref normal, ref tangent); return Vector3.Project(v, tangent); } v.y = 0; return v; } // Determines whether to use root rotation as solver rotation private bool useRootRotation { get { if (!rotateSolver) return false; if (root.up == Vector3.up) return false; return true; } } public Vector3 GetFootCenterOffset() { return root.forward * footRadius + root.forward * footCenterOffset; } } }