Request For In-depth Break-Down On Character Controller Scripts Posted Here

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:

  1. 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)

  2. 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;
}
}

You have an option in this forum called insert, then click Code, and past your code inside.Right now it is a pain to read.

You can check the platformercharacter standard assets, they have some different ways of dealing with it.
They use empty gameObjects at the top, down, ( you have to create left and right yourself )of the character, and create a circle around these to check if it overlaps a wall instead of using raycasts.

You can also use colliders and use Collider2D.isTouchingLayers(_TheLayerOfYourWalls) / OnCollisionEnter2D / OnCollisionStay2D, etc etc for this. I went for a mix of colliders for top/left/right and a raycast for the ground checking.

You can add a game object attached to your player prefab, make it a little further or behind your player and cast the ray from it. Create two of these, attach these to your feets and check if one or both are touching the ground.

On this thread, it is explained how to create a raycast that checks what is the angle of the slope the player is on. You can modify it to check for other things at the same time.

I believe you should start from the standard asset platformer script, look at what it lacks of ( well, lot of the things you want won’t be there at all) . Then add one by one the features you want, it will be easier to ask for precise problems.

Or maybe, play with the script you got in the asset store, and then ask more precisely what feature of it you don’t understand.