Player Unable to Climb Stairs in C#

Godot Version

Godot V4.2.2 Stable Mono

Question

I’m working on a script for a character to climb up and down stairs and other smaller obstacles such as platforms, without having to jump to get up them. I’m following this tutorial, however, my coding is in Csharp, as I had already coded other things, so I came across “the ladder problem”. When I write the code and put it in the character everything works “without errors” compatibility or something, the problem I’m facing:

        var downCheckResult = new PhysicsTestMotionResult3D();

        if (RunBodyTestMotion(step_pos_with_clearance, new Vector3(0, -maxStepHeight * 2, 0), downCheckResult) &&
            (downCheckResult.GetCollider().IsClass("StaticBody3D") || downCheckResult.GetCollider().IsClass("CsgShape3D"))) {

Is that the collision returns nothing, it always appears as empty.
Which means that all the code doesn’t work. I have no idea what it could be, I’ve tried just using raycast, but it doesn’t work very well

I managed to solve the problem. I’ll post the code so that anyone working on the same thing in C# in the future can have a reference.

There have been some changes from the initial question, but now it’s working as expected. It’s important to note that the player (or character) must inherit from this Stair class, and it should have 2 raycasts. The first one, AHeadRay, should be positioned at (X: 0 | Y: 0.55 | Z: -0.55), and the second (belowRay) one at (X: 0 | Y: 0 | Z: 0) relative to its parent.

The code is not fully finalized yet—I haven’t added camera smoothing, for example. However, I had some issues with it, so this might help someone in the future.

using Godot;
using System;

/// <summary>
/// This modifier adds the ability to chimb stairs to CharacterBody3D. But is necessary to inherit the Stair Climbing Control.
/// </summary>
public partial class StairClimbingControl: CharacterBody3D { //Stair Climbing Control

    protected float lastFrameWasFloor = -Mathf.Inf;
    private bool snappedToStairLastFrame = false;
    private float maxStepHeight = 0.5f;

    [Export] protected RayCast3D belowRay;
    [Export] protected RayCast3D aHeadRay;


    public void SnapDownToStairCheck() {
        bool didSnap = false;

        belowRay.ForceRaycastUpdate();
        bool floorBelow = belowRay.IsColliding() && !IsSurfaceTooSteep(belowRay.GetCollisionNormal());
        var wasOnFloorLastFrame = Engine.GetPhysicsFrames() == lastFrameWasFloor;

        if (this.IsOnFloor() && Velocity.Y <= 0 && (wasOnFloorLastFrame || snappedToStairLastFrame) && floorBelow) {
            KinematicCollision3D bodyTestResult = new KinematicCollision3D();

            if (TestMove(this.GlobalTransform, new Vector3(0, -maxStepHeight, 0), bodyTestResult)) {
                // Necessary smoothing camera

                float translateY = bodyTestResult.GetTravel().Y;
                this.Position += new Vector3(0, translateY, 0);
                this.ApplyFloorSnap();
                didSnap = true;
            }
            snappedToStairLastFrame = didSnap;
        }
    }

    public bool SnapUpStairCheck(float delta) {

        if (this.IsOnFloor() && !snappedToStairLastFrame)
            return false;

        Vector3 vectorOneXZ = new Vector3(1, 0, 1);

        if (this.Velocity.Y > 0 || (this.Velocity * vectorOneXZ).Length() == 0)
            return false;

        Vector3 expectedMoveMotion = this.Velocity * vectorOneXZ * delta;
        Transform3D stepPosWithClearence = this.GlobalTransform.Translated(expectedMoveMotion + new Vector3(0, maxStepHeight * 2, 0));

        KinematicCollision3D downCheckResult = new KinematicCollision3D();

        if (this.TestMove(stepPosWithClearence, new Vector3(0, -maxStepHeight * 2, 0), downCheckResult)
            && (downCheckResult.GetCollider().IsClass("StaticBody3D") || downCheckResult.GetCollider().IsClass("CsgShape3D"))) {

            float stepHeight = ((stepPosWithClearence.Origin + downCheckResult.GetTravel()) - this.GlobalPosition).Y;

            if (stepHeight > maxStepHeight || stepHeight <= .001f || (downCheckResult.GetPosition() - this.GlobalPosition).Y > maxStepHeight)
                return false;

            aHeadRay.GlobalPosition = (downCheckResult.GetPosition() + new Vector3(0, maxStepHeight, 0) + expectedMoveMotion.Normalized() * .1f);
            aHeadRay.ForceRaycastUpdate();

            if (aHeadRay.IsColliding() && !IsSurfaceTooSteep(aHeadRay.GetCollisionNormal())) {
                // Necessary smoothing camera


                this.GlobalPosition = stepPosWithClearence.Origin + downCheckResult.GetTravel();

                this.ApplyFloorSnap();
                snappedToStairLastFrame = true;
                GD.Print("Snap UP ");

                return true;

            }
        }

        return false;
    }

    private bool IsSurfaceTooSteep(Vector3 _normal) {
        return _normal.AngleTo(Vector3.Up) > this.FloorMaxAngle;
    }

}


In the implementation within _PhysicsProcess, the movement should look like this:

    public override void _PhysicsProcess(double delta) {

        if (IsOnFloor())
            lastFrameWasFloor = Engine.GetPhysicsFrames();

        if (!SnapUpStairCheck((float)delta)) {
            MovementAndAnimation((float)delta);
            SnapDownToStairCheck();
        }

In this function, ‘MovementAndAnimation((float)delta);’, it applies my player’s movements based on input and other necessary actions. At the end of this function, there’s a standard ‘MoveAndSlide()’.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.