Skip to content

Instantly share code, notes, and snippets.

@Andicraft
Last active February 4, 2024 19:18
Show Gist options
  • Save Andicraft/004cdff7d79947d946887859bac50382 to your computer and use it in GitHub Desktop.
Save Andicraft/004cdff7d79947d946887859bac50382 to your computer and use it in GitHub Desktop.
Stair-Stepping Character Example for Godot in C#
using Godot;
// Use this class as a base for a character controller in Godot to enable stair-stepping
//
// In your character code, simply call StairStepUp() just before MoveAndSlide(),
// and then StairStepDown() afterward.
//
// Make sure your character collider margins are set as low as possible.
// Inspired by and partially based on https://github.com/JheKWall/Godot-Stair-Step-Demo
public partial class StairsCharacter : CharacterBody3D
{
[ExportCategory("Stair Stepping")]
[Export] private float _stepHeight = 0.33f;
private float _colliderMargin;
private RayCast3D _collisionRay;
public bool Grounded;
private readonly Vector3 _horizontal = new Vector3(1, 0, 1);
protected Vector3 DesiredVelocity = Vector3.Zero;
public override void _Ready()
{
base._Ready();
// Only requirement for your Player scene is that your collider is named Collider
// (or replace this with an [Export]'ed Node3D and just grab it from there)
// Your margin should be set real low or it starts snagging on everything - 0.001 works for me.
_colliderMargin = GetNode<CollisionShape3D>("Collider").Shape.Margin;
}
public override void _PhysicsProcess(double delta)
{
// Use Grounded instead of IsOnFloor() in actual character controller because we reset this if we step down
Grounded = IsOnFloor();
// !!IMPORTANT!!
// DesiredVelocity should be set in your character controller just so we know where we _want_ to go
// In my character code it's just the direction of the controller input rotated with the camera
// The magnitude doesn't matter much, it just needs a direction. Y component should be zero.
DesiredVelocity = Vector3.Zero;
}
protected void StairStepDown()
{
// Not on the ground last stair step, or currently jumping? Don't snap to the ground
// Prevents from suddenly snapping when you're falling
if (Grounded == false || Velocity.Y >= 0) return;
// MoveAndSlide() kept us on the floor so no need to do anything
if (IsOnFloor()) return;
var result = new PhysicsTestMotionResult3D();
var parameters = new PhysicsTestMotionParameters3D();
parameters.From = GlobalTransform;
parameters.Motion = Vector3.Down * _stepHeight;
parameters.Margin = _colliderMargin;
if (!PhysicsServer3D.BodyTestMotion(GetRid(), parameters, result)) return;
GlobalTransform = GlobalTransform.Translated(result.GetTravel());
ApplyFloorSnap();
}
protected void StairStepUp()
{
if (!Grounded) return; //Let's not bother if we're in the air
var horizontalVelocity = Velocity * _horizontal;
var testingVelocity = horizontalVelocity;
if (horizontalVelocity == Vector3.Zero)
testingVelocity = DesiredVelocity;
// Not moving or attempting to move, let's not bother
if (testingVelocity == Vector3.Zero) return;
var result = new PhysicsTestMotionResult3D();
var parameters = new PhysicsTestMotionParameters3D();
var transform = GlobalTransform;
// Game is my autoload because I don't like passing 'delta' around everywhere
// Replace with 'delta' parameter if in your own game
var distance = testingVelocity * Game.PhysicsDeltaTime;
parameters.From = transform;
parameters.Motion = distance;
parameters.Margin = _colliderMargin;
if (PhysicsServer3D.BodyTestMotion(GetRid(), parameters, result) == false)
return; // No stair step to bother with because we're not hitting anything
//Move to collision
var remainder = result.GetRemainder();
transform = transform.Translated(result.GetTravel());
var horizontalCollision = transform.Origin * _horizontal;
// Raise up to ceiling - can't walk on steps if the corridor is too low for example
var stepUp = _stepHeight * Vector3.Up;
parameters.From = transform;
parameters.Motion = stepUp;
PhysicsServer3D.BodyTestMotion(GetRid(), parameters, result);
transform = transform.Translated(result.GetTravel()); // GetTravel will be full length if we didn't hit anything
var stepUpDistance = result.GetTravel().Length();
// Move forward remaining distance
parameters.From = transform;
parameters.Motion = remainder;
PhysicsServer3D.BodyTestMotion(GetRid(), parameters, result);
transform = transform.Translated(result.GetTravel());
// And set the collider back down again
parameters.From = transform;
// But no further than how far we stepped up
parameters.Motion = Vector3.Down * stepUpDistance;
// Don't bother with the rest if we're not actually gonna land back down on something
if (PhysicsServer3D.BodyTestMotion(GetRid(), parameters, result) == false)
return;
transform = transform.Translated(result.GetTravel());
var surfaceNormal = result.GetCollisionNormal();
if (surfaceNormal.AngleTo(Vector3.Up) > FloorMaxAngle) return; //Can't stand on the thing we're trying to step on anyway
var gp = GlobalPosition;
gp.Y = transform.Origin.Y;
GlobalPosition = gp;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment