Hello Everyone,
This is going to to be my first post on this forum. For weeks I’ve been trying to create a character controller for a platformer using raycasts. I just started learning Unity and C# a few months ago and I wanted to try a project out on my own.
Everything was going fine, until I realized that I could only get the raycast to shoot from the center of my character, which obviously isn’t ideal in a platformer where your player should be able to stay on the ground even if only one of his feet is touching it and etc.
So I did some hunting around for tutorials on using raycasts for platforming, character controllers for platformers, the whole nine yards. I ran into two problems:
-
Either they were too simplistic (As in they were using a single raycast as well just to show people the very basics of getting a platformer up and running and never went past using that single raycast)
-
They weren’t made with the latest version of Unity so they were hopelessly out of date through no fault of their own.
So I headed over to the Asset store to look at some free character controllers to see how they worked so that I could try and understand them and make my own in the future.
Here’s my problem: I really can’t wrap my head around these scripts used for character controllers. They don’t seem to use rigid bodies, they have raycasts coming out from every side of the character and it’s just very confusing for me.
I was hoping someone could give me a piece by piece break down of each part of these scripts and explain how they fit together so that I can emulate those functionalities in the future. So, without further ado, here are the scripts I was looking at.
using UnityEngine;
[RequireComponent(typeof(BoxCollider2D))]
public class RaycastController : MonoBehaviour
{
public LayerMask collisionMask;
public const float skinWidth = .015f;
private const float dstBetweenRays = .25f;
[HideInInspector]
public int horizontalRayCount;
[HideInInspector]
public int verticalRayCount;
[HideInInspector]
public float horizontalRaySpacing;
[HideInInspector]
public float verticalRaySpacing;
[HideInInspector]
public BoxCollider2D coll;
[HideInInspector]
public RaycastOrigins raycastOrigins;
public virtual void Awake()
{
coll = GetComponent();
}
public virtual void Start()
{
CalculateRaySpacing();
}
public void UpdateRaycastOrigins()
{
Bounds bounds = coll.bounds;
bounds.Expand(skinWidth * -2);
raycastOrigins.bottomLeft = new Vector2(bounds.min.x, bounds.min.y);
raycastOrigins.bottomRight = new Vector2(bounds.max.x, bounds.min.y);
raycastOrigins.topLeft = new Vector2(bounds.min.x, bounds.max.y);
raycastOrigins.topRight = new Vector2(bounds.max.x, bounds.max.y);
}
public void CalculateRaySpacing()
{
Bounds bounds = coll.bounds;
bounds.Expand(skinWidth * -2);
float boundsWidth = bounds.size.x;
float boundsHeight = bounds.size.y;
horizontalRayCount = Mathf.RoundToInt(boundsHeight / dstBetweenRays);
verticalRayCount = Mathf.RoundToInt(boundsWidth / dstBetweenRays);
horizontalRaySpacing = bounds.size.y / (horizontalRayCount - 1);
verticalRaySpacing = bounds.size.x / (verticalRayCount - 1);
}
public struct RaycastOrigins
{
public Vector2 topLeft, topRight;
public Vector2 bottomLeft, bottomRight;
}
}
using UnityEngine;
[RequireComponent(typeof(Player))]
public class PlayerInput : MonoBehaviour
{
private Player player;
private void Start()
{
player = GetComponent();
}
private void Update()
{
Vector2 directionalInput = new Vector2(Input.GetAxisRaw(“Horizontal”), Input.GetAxisRaw(“Vertical”));
player.SetDirectionalInput(directionalInput);
if (Input.GetButtonDown(“Jump”))
{
player.OnJumpInputDown();
}
if (Input.GetButtonUp(“Jump”))
{
player.OnJumpInputUp();
}
}
}
using UnityEngine;
public class Controller2D : RaycastController
{
public float fallingThroughPlatformResetTimer = 0.1f;
private float maxClimbAngle = 80f;
private float maxDescendAngle = 80f;
public CollisionInfo collisions;
[HideInInspector]
public Vector2 playerInput;
public override void Start()
{
base.Start();
collisions.faceDir = 1;
}
public void Move(Vector2 moveAmount, bool standingOnPlatform = false)
{
Move(moveAmount, Vector2.zero, standingOnPlatform);
}
public void Move(Vector2 moveAmount, Vector2 input, bool standingOnPlatform = false)
{
UpdateRaycastOrigins();
collisions.Reset();
collisions.moveAmountOld = moveAmount;
playerInput = input;
if (moveAmount.x != 0)
{
collisions.faceDir = (int)Mathf.Sign(moveAmount.x);
}
if (moveAmount.y < 0)
{
DescendSlope(ref moveAmount);
}
HorizontalCollisions(ref moveAmount);
if (moveAmount.y != 0)
{
VerticalCollisions(ref moveAmount);
}
transform.Translate(moveAmount);
if (standingOnPlatform)
{
collisions.below = true;
}
}
private void HorizontalCollisions(ref Vector2 moveAmount)
{
float directionX = collisions.faceDir;
float rayLength = Mathf.Abs(moveAmount.x) + skinWidth;
if (Mathf.Abs(moveAmount.x) < skinWidth)
{
rayLength = 2 * skinWidth;
}
for (int i = 0; i < horizontalRayCount; i++)
{
Vector2 rayOrigin = (directionX == -1) ? raycastOrigins.bottomLeft : raycastOrigins.bottomRight;
rayOrigin += Vector2.up * (horizontalRaySpacing * i);
RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.right * directionX, rayLength, collisionMask);
Debug.DrawRay(rayOrigin, Vector2.right * directionX, Color.red);
if (hit)
{
if (hit.distance == 0)
{
continue;
}
float slopeAngle = Vector2.Angle(hit.normal, Vector2.up);
if (i == 0 && slopeAngle <= maxClimbAngle)
{
if (collisions.descendingSlope)
{
collisions.descendingSlope = false;
moveAmount = collisions.moveAmountOld;
}
float distanceToSlopeStart = 0f;
if (slopeAngle != collisions.slopeAngleOld)
{
distanceToSlopeStart = hit.distance - skinWidth;
moveAmount.x -= distanceToSlopeStart * directionX;
}
ClimbSlope(ref moveAmount, slopeAngle);
moveAmount.x += distanceToSlopeStart * directionX;
}
if (!collisions.climbingSlope || slopeAngle > maxClimbAngle)
{
moveAmount.x = (hit.distance - skinWidth) * directionX;
rayLength = hit.distance;
if (collisions.climbingSlope)
{
moveAmount.y = Mathf.Tan(collisions.slopeAngle * Mathf.Deg2Rad) * Mathf.Abs(moveAmount.x);
}
collisions.left = directionX == -1;
collisions.right = directionX == 1;
}
}
}
}
private void ClimbSlope(ref Vector2 moveAmount, float slopeAngle)
{
float moveDistance = Mathf.Abs(moveAmount.x);
float climbmoveAmountY = Mathf.Sin(slopeAngle * Mathf.Deg2Rad) * moveDistance;
if (moveAmount.y <= climbmoveAmountY)
{
moveAmount.y = climbmoveAmountY;
moveAmount.x = Mathf.Cos(slopeAngle * Mathf.Deg2Rad) * moveDistance * Mathf.Sign(moveAmount.x);
collisions.below = true;
collisions.climbingSlope = true;
collisions.slopeAngle = slopeAngle;
}
}
private void DescendSlope(ref Vector2 moveAmount)
{
float directionX = Mathf.Sign(moveAmount.x);
Vector2 rayOrigin = (directionX == -1) ? raycastOrigins.bottomRight : raycastOrigins.bottomLeft;
RaycastHit2D hit = Physics2D.Raycast(rayOrigin, -Vector2.up, Mathf.Infinity, collisionMask);
if (hit)
{
float slopeAngle = Vector2.Angle(hit.normal, Vector2.up);
if (slopeAngle != 0 && slopeAngle <= maxDescendAngle)
{
if (Mathf.Sign(hit.normal.x) == directionX)
{
if (hit.distance - skinWidth <= Mathf.Tan(slopeAngle * Mathf.Deg2Rad) * Mathf.Abs(moveAmount.x))
{
float moveDistance = Mathf.Abs(moveAmount.x);
float descendmoveAmountY = Mathf.Sin(slopeAngle * Mathf.Deg2Rad) * moveDistance;
moveAmount.x = Mathf.Cos(slopeAngle * Mathf.Deg2Rad) * moveDistance * Mathf.Sign(moveAmount.x);
moveAmount.y -= descendmoveAmountY;
collisions.slopeAngle = slopeAngle;
collisions.descendingSlope = true;
collisions.below = true;
}
}
}
}
}
private void VerticalCollisions(ref Vector2 moveAmount)
{
float directionY = Mathf.Sign(moveAmount.y);
float rayLength = Mathf.Abs(moveAmount.y) + skinWidth;
for (int i = 0; i < verticalRayCount; i++)
{
Vector2 rayOrigin = (directionY == -1) ? raycastOrigins.bottomLeft : raycastOrigins.topLeft;
rayOrigin += Vector2.right * (verticalRaySpacing * i + moveAmount.x);
RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.up * directionY, rayLength, collisionMask);
Debug.DrawRay(rayOrigin, Vector2.up * directionY, Color.red);
if (hit)
{
if (hit.collider.tag == “Through”)
{
if (directionY == 1 || hit.distance == 0)
{
continue;
}
if (collisions.fallingThroughPlatform)
{
continue;
}
if (playerInput.y == -1)
{
collisions.fallingThroughPlatform = true;
Invoke(“ResetFallingThroughPlatform”, fallingThroughPlatformResetTimer);
continue;
}
}
moveAmount.y = (hit.distance - skinWidth) * directionY;
rayLength = hit.distance;
if (collisions.climbingSlope)
{
moveAmount.x = moveAmount.y / Mathf.Tan(collisions.slopeAngle * Mathf.Deg2Rad) * Mathf.Sign(moveAmount.x);
}
collisions.below = directionY == -1;
collisions.above = directionY == 1;
}
}
if (collisions.climbingSlope)
{
float directionX = Mathf.Sign(moveAmount.x);
rayLength = Mathf.Abs(moveAmount.x) + skinWidth;
Vector2 rayOrigin = ((directionX == -1) ? raycastOrigins.bottomLeft : raycastOrigins.bottomRight) + Vector2.up * moveAmount.y;
RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.right * directionX, rayLength, collisionMask);
if (hit)
{
float slopeAngle = Vector2.Angle(hit.normal, Vector2.up);
if (slopeAngle != collisions.slopeAngle)
{
moveAmount.x = (hit.distance * skinWidth) * directionX;
collisions.slopeAngle = slopeAngle;
}
}
}
}
private void ResetFallingThroughPlatform()
{
collisions.fallingThroughPlatform = false;
}
public struct CollisionInfo
{
public bool above, below;
public bool left, right;
public bool climbingSlope;
public bool descendingSlope;
public float slopeAngle, slopeAngleOld;
public Vector2 moveAmountOld;
public int faceDir;
public bool fallingThroughPlatform;
public void Reset()
{
above = below = false;
left = right = false;
climbingSlope = false;
descendingSlope = false;
slopeAngleOld = slopeAngle;
slopeAngle = 0f;
}
}
}
using UnityEngine;
[RequireComponent(typeof(Controller2D))]
public class Player : MonoBehaviour
{
public float maxJumpHeight = 4f;
public float minJumpHeight = 1f;
public float timeToJumpApex = .4f;
private float accelerationTimeAirborne = .2f;
private float accelerationTimeGrounded = .1f;
private float moveSpeed = 6f;
public Vector2 wallJumpClimb;
public Vector2 wallJumpOff;
public Vector2 wallLeap;
public bool canDoubleJump;
private bool isDoubleJumping = false;
public float wallSlideSpeedMax = 3f;
public float wallStickTime = .25f;
private float timeToWallUnstick;
private float gravity;
private float maxJumpVelocity;
private float minJumpVelocity;
private Vector3 velocity;
private float velocityXSmoothing;
private Controller2D controller;
private Vector2 directionalInput;
private bool wallSliding;
private int wallDirX;
private void Start()
{
controller = GetComponent();
gravity = -(2 * maxJumpHeight) / Mathf.Pow(timeToJumpApex, 2);
maxJumpVelocity = Mathf.Abs(gravity) * timeToJumpApex;
minJumpVelocity = Mathf.Sqrt(2 * Mathf.Abs(gravity) * minJumpHeight);
}
private void Update()
{
CalculateVelocity();
HandleWallSliding();
controller.Move(velocity * Time.deltaTime, directionalInput);
if (controller.collisions.above || controller.collisions.below)
{
velocity.y = 0f;
}
}
public void SetDirectionalInput(Vector2 input)
{
directionalInput = input;
}
public void OnJumpInputDown()
{
if (wallSliding)
{
if (wallDirX == directionalInput.x)
{
velocity.x = -wallDirX * wallJumpClimb.x;
velocity.y = wallJumpClimb.y;
}
else if (directionalInput.x == 0)
{
velocity.x = -wallDirX * wallJumpOff.x;
velocity.y = wallJumpOff.y;
}
else
{
velocity.x = -wallDirX * wallLeap.x;
velocity.y = wallLeap.y;
}
isDoubleJumping = false;
}
if (controller.collisions.below)
{
velocity.y = maxJumpVelocity;
isDoubleJumping = false;
}
if (canDoubleJump && !controller.collisions.below && !isDoubleJumping && !wallSliding)
{
velocity.y = maxJumpVelocity;
isDoubleJumping = true;
}
}
public void OnJumpInputUp()
{
if (velocity.y > minJumpVelocity)
{
velocity.y = minJumpVelocity;
}
}
private void HandleWallSliding()
{
wallDirX = (controller.collisions.left) ? -1 : 1;
wallSliding = false;
if ((controller.collisions.left || controller.collisions.right) && !controller.collisions.below && velocity.y < 0)
{
wallSliding = true;
if (velocity.y < -wallSlideSpeedMax)
{
velocity.y = -wallSlideSpeedMax;
}
if (timeToWallUnstick > 0f)
{
velocityXSmoothing = 0f;
velocity.x = 0f;
if (directionalInput.x != wallDirX && directionalInput.x != 0f)
{
timeToWallUnstick -= Time.deltaTime;
}
else
{
timeToWallUnstick = wallStickTime;
}
}
else
{
timeToWallUnstick = wallStickTime;
}
}
}
private void CalculateVelocity()
{
float targetVelocityX = directionalInput.x * moveSpeed;
velocity.x = Mathf.SmoothDamp(velocity.x, targetVelocityX, ref velocityXSmoothing, (controller.collisions.below ? accelerationTimeGrounded : accelerationTimeAirborne));
velocity.y += gravity * Time.deltaTime;
}
}