Skip to content

Instantly share code, notes, and snippets.

@jawinn
Last active July 15, 2023 03:24
Show Gist options
  • Save jawinn/f466b237c0cdc5f92d96 to your computer and use it in GitHub Desktop.
Save jawinn/f466b237c0cdc5f92d96 to your computer and use it in GitHub Desktop.
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