public override bool ComputeGroundHit(Vector3 position, Quaternion rotation, ControllerHitInfo controllerHitInfo, ref GroundHit groundHitInfo, float distance = Mathf.Infinity) { // Downward cast from campsule's bottom sphere (filter any 'wall') RaycastHit hitInfo; if (BottomSphereCast(position, rotation, out hitInfo, distance) && Vector3.Angle(hitInfo.normal, rotation * Vector3.up) < 89.0f) { // Update ground hit info groundHitInfo.SetFrom(hitInfo); // Check if standing on a ledge or step DetectLedgeAndSteps(position, rotation, ref groundHitInfo, distance, hitInfo.point, hitInfo.normal); // Inform we found 'ground' and if is valid groundHitInfo.isOnGround = true; groundHitInfo.isValidGround = !groundHitInfo.isOnLedgeEmptySide && Vector3.Angle(groundHitInfo.surfaceNormal, Vector3.up) < groundLimit; return(true); } // If initial sphere cast fails, or found a 'wall', // fallback to a raycast from character's bottom position if (!BottomRaycast(position, rotation, out hitInfo, distance)) { if (!controllerHitInfo.Valid) { return(false); } groundHitInfo.SetFrom(controllerHitInfo); groundHitInfo.surfaceNormal = controllerHitInfo.HitNormal; groundHitInfo.isOnGround = true; groundHitInfo.isValidGround = Vector3.Angle(groundHitInfo.surfaceNormal, Vector3.up) < groundLimit; //DebugDraw.DebugArrow(controllerHitInfo.HitPoint, controllerHitInfo.HitNormal * 2, Color.magenta, 0, false); } else { // If found 'ground', update ground info and return groundHitInfo.SetFrom(hitInfo); groundHitInfo.surfaceNormal = hitInfo.normal; groundHitInfo.isOnGround = true; groundHitInfo.isValidGround = Vector3.Angle(groundHitInfo.surfaceNormal, Vector3.up) < groundLimit; } return(true); }
/// <summary> /// Check if we are standing on a ledge or a step and update 'grounding' info. /// </summary> /// <param name="position">A probing position. This can be different from character's position.</param> /// <param name="rotation">A probing rotation. This can be different from character's rotation.</param> /// <param name="distance">The cast distance.</param> /// <param name="point">The current contact point.</param> /// <param name="normal">The current contact normal.</param> /// <param name="groundHitInfo">If found any 'ground', hitInfo will contain more information about it.</param> private void DetectLedgeAndSteps(Vector3 position, Quaternion rotation, ref GroundHit groundHitInfo, float distance, Vector3 point, Vector3 normal) { Vector3 up = rotation * Vector3.up, down = -up; var projectedNormal = Vector3.ProjectOnPlane(normal, up).normalized; var nearPoint = point + projectedNormal * kHorizontalOffset; var farPoint = point - projectedNormal * kHorizontalOffset; var ledgeStepDistance = Mathf.Max(kMinLedgeDistance, Mathf.Max(stepOffset, distance)); RaycastHit nearHitInfo; var nearHit = Raycast(nearPoint, down, out nearHitInfo, ledgeStepDistance); var isNearGroundValid = nearHit && Vector3.Angle(nearHitInfo.normal, up) < groundLimit; RaycastHit farHitInfo; var farHit = Raycast(farPoint, down, out farHitInfo, ledgeStepDistance); var isFarGroundValid = farHit && Vector3.Angle(farHitInfo.normal, up) < groundLimit; // Flush if (farHit && !isFarGroundValid) { groundHitInfo.surfaceNormal = farHitInfo.normal; // Attemp to retrieve the 'ground' below us RaycastHit secondaryHitInfo; if (BottomRaycast(position, rotation, out secondaryHitInfo, distance)) { // Update ground info and return groundHitInfo.SetFrom(secondaryHitInfo); groundHitInfo.surfaceNormal = secondaryHitInfo.normal; } return; } // Steps if (isNearGroundValid && isFarGroundValid) { // Choose nearest normal groundHitInfo.surfaceNormal = (point - nearHitInfo.point).sqrMagnitude < (point - farHitInfo.point).sqrMagnitude ? nearHitInfo.normal : farHitInfo.normal; // Check if max vertical distance between points is steep enough to be considered as a step var nearHeight = Vector3.Dot(point - nearHitInfo.point, up); var farHeight = Vector3.Dot(point - farHitInfo.point, up); var height = Mathf.Max(nearHeight, farHeight); if (height > kMinLedgeDistance && height < stepOffset) { groundHitInfo.isOnStep = true; groundHitInfo.stepHeight = height; } return; } // Ledges // If only one of the near / far rays hit we are on a ledge var isOnLedge = isNearGroundValid != isFarGroundValid; if (!isOnLedge) { return; } // On ledge, compute ledge info and check which side of the ledge we are groundHitInfo.surfaceNormal = isFarGroundValid ? farHitInfo.normal : nearHitInfo.normal; groundHitInfo.ledgeDistance = Vector3.ProjectOnPlane(point - position, up).magnitude; if (isFarGroundValid && groundHitInfo.ledgeDistance > ledgeOffset) { // On possible ledge 'empty' side, // cast downwards using de ledgeOffset as radius groundHitInfo.isOnLedgeEmptySide = true; var radius = ledgeOffset; var offset = Mathf.Max(0.0f, capsuleCollider.height * 0.5f - radius); var bottomSphereCenter = capsuleCollider.center - Vector3.up * offset; var bottomSphereOrigin = position + rotation * bottomSphereCenter; RaycastHit hitInfo; if (SphereCast(bottomSphereOrigin, radius, down, out hitInfo, Mathf.Max(stepOffset, distance))) { var verticalSquareDistance = Vector3.Project(point - hitInfo.point, up).sqrMagnitude; if (verticalSquareDistance <= stepOffset * stepOffset) { groundHitInfo.isOnLedgeEmptySide = false; } } } groundHitInfo.isOnLedgeSolidSide = !groundHitInfo.isOnLedgeEmptySide; }