Last active
February 4, 2024 19:18
-
-
Save Andicraft/004cdff7d79947d946887859bac50382 to your computer and use it in GitHub Desktop.
Stair-Stepping Character Example for Godot in C#
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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