Unity - Ground Checker: Get angle of sloped ground underneath the player
using UnityEngine; | |
using System; | |
// Finds the slope/grade/incline angle of ground underneath a CharacterController | |
public class GroundChecker : MonoBehaviour { | |
[Header("Results")] | |
public float groundSlopeAngle = 0f; // Angle of the slope in degrees | |
public Vector3 groundSlopeDir = Vector3.zero; // The calculated slope as a vector | |
[Header("Settings")] | |
public bool showDebug = false; // Show debug gizmos and lines | |
public LayerMask castingMask; // Layer mask for casts. You'll want to ignore the player. | |
public float startDistanceFromBottom = 0.2f; // Should probably be higher than skin width | |
public float sphereCastRadius = 0.25f; | |
public float sphereCastDistance = 0.75f; // How far spherecast moves down from origin point | |
public float raycastLength = 0.75f; | |
public Vector3 rayOriginOffset1 = new Vector3(-0.2f, 0f, 0.16f); | |
public Vector3 rayOriginOffset2 = new Vector3(0.2f, 0f, -0.16f); | |
// Component reference | |
private CharacterController controller; | |
void Awake() | |
{ | |
// Get component on the same GameObject | |
controller = GetComponent<CharacterController>(); | |
if (controller == null) { Debug.LogError("GroundChecker did not find a CharacterController component."); } | |
} | |
void FixedUpdate() | |
{ | |
// Check ground, with an origin point defaulting to the bottom middle | |
// of the char controller's collider. Plus a little higher | |
if (controller && controller.isGrounded) { | |
CheckGround(new Vector3(transform.position.x, transform.position.y - (controller.height / 2) + startDistanceFromBottom, transform.position.z)); | |
} | |
} | |
/// <summary> | |
/// Checks for ground underneath, to determine some info about it, including the slope angle. | |
/// </summary> | |
/// <param name="origin">Point to start checking downwards from</param> | |
public void CheckGround(Vector3 origin) | |
{ | |
// Out hit point from our cast(s) | |
RaycastHit hit; | |
// SPHERECAST | |
// "Casts a sphere along a ray and returns detailed information on what was hit." | |
if (Physics.SphereCast(origin, sphereCastRadius, Vector3.down, out hit, sphereCastDistance, castingMask)) | |
{ | |
// Angle of our slope (between these two vectors). | |
// A hit normal is at a 90 degree angle from the surface that is collided with (at the point of collision). | |
// e.g. On a flat surface, both vectors are facing straight up, so the angle is 0. | |
groundSlopeAngle = Vector3.Angle(hit.normal, Vector3.up); | |
// Find the vector that represents our slope as well. | |
// temp: basically, finds vector moving across hit surface | |
Vector3 temp = Vector3.Cross(hit.normal, Vector3.down); | |
// Now use this vector and the hit normal, to find the other vector moving up and down the hit surface | |
groundSlopeDir = Vector3.Cross(temp, hit.normal); | |
} | |
// Now that's all fine and dandy, but on edges, corners, etc, we get angle values that we don't want. | |
// To correct for this, let's do some raycasts. You could do more raycasts, and check for more | |
// edge cases here. There are lots of situations that could pop up, so test and see what gives you trouble. | |
RaycastHit slopeHit1; | |
RaycastHit slopeHit2; | |
// FIRST RAYCAST | |
if (Physics.Raycast(origin + rayOriginOffset1, Vector3.down, out slopeHit1, raycastLength)) | |
{ | |
// Debug line to first hit point | |
if (showDebug) { Debug.DrawLine(origin + rayOriginOffset1, slopeHit1.point, Color.red); } | |
// Get angle of slope on hit normal | |
float angleOne = Vector3.Angle(slopeHit1.normal, Vector3.up); | |
// 2ND RAYCAST | |
if (Physics.Raycast(origin + rayOriginOffset2, Vector3.down, out slopeHit2, raycastLength)) | |
{ | |
// Debug line to second hit point | |
if (showDebug) { Debug.DrawLine(origin + rayOriginOffset2, slopeHit2.point, Color.red); } | |
// Get angle of slope of these two hit points. | |
float angleTwo = Vector3.Angle(slopeHit2.normal, Vector3.up); | |
// 3 collision points: Take the MEDIAN by sorting array and grabbing middle. | |
float[] tempArray = new float[] { groundSlopeAngle, angleOne, angleTwo }; | |
Array.Sort(tempArray); | |
groundSlopeAngle = tempArray[1]; | |
} | |
else | |
{ | |
// 2 collision points (sphere and first raycast): AVERAGE the two | |
float average = (groundSlopeAngle + angleOne) / 2; | |
groundSlopeAngle = average; | |
} | |
} | |
} | |
void OnDrawGizmosSelected() | |
{ | |
if (showDebug) | |
{ | |
// Visualize SphereCast with two spheres and a line | |
Vector3 startPoint = new Vector3(transform.position.x, transform.position.y - (controller.height / 2) + startDistanceFromBottom, transform.position.z); | |
Vector3 endPoint = new Vector3(transform.position.x, transform.position.y - (controller.height / 2) + startDistanceFromBottom - sphereCastDistance, transform.position.z); | |
Gizmos.color = Color.white; | |
Gizmos.DrawWireSphere(startPoint, sphereCastRadius); | |
Gizmos.color = Color.gray; | |
Gizmos.DrawWireSphere(endPoint, sphereCastRadius); | |
Gizmos.DrawLine(startPoint, endPoint); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment