Skip to content

Physics: Add step handling to CharacterBody3D#114447

Open
laspencer91 wants to merge 5 commits into
godotengine:masterfrom
laspencer91:feature/characterbody3d-step-handling
Open

Physics: Add step handling to CharacterBody3D#114447
laspencer91 wants to merge 5 commits into
godotengine:masterfrom
laspencer91:feature/characterbody3d-step-handling

Conversation

@laspencer91
Copy link
Copy Markdown
Contributor

@laspencer91 laspencer91 commented Dec 30, 2025

Summary

Adds native step handling to CharacterBody3D using an up-forward-down trace algorithm, similar to classic FPS engines like Unreal Engine 1. Also includes visual position smoothing for seamless camera integration. Optimally used with Cylinder collider. Any "jittering" in the video is a result of my video capture, not in the game itself.

This feature makes Godot's Character Controller the simplest to setup, and most versatile Character Controller of the most popular game engines. Lack of stair step is the biggest weakness the current controller has. This remedies it.

smooth_stairstep.mp4

New Properties On CharacterBody3D

Use in conjunction with floor snap for climbing down stairs (with smoothing too)!

image

Closes

godotengine/godot-proposals#2751

The Journey

Initially, I attempted to leverage Jolt Physics' built-in CharacterVirtual step handling via body_move_and_step(). While Jolt's implementation works well internally, exposing it through Godot's physics abstraction proved problematic:

  • The CharacterVirtual approach requires a separate physics body type with different collision semantics
  • Integrating it with CharacterBody3D's existing move_and_slide() workflow created state synchronization issues
  • The abstraction layer made it difficult to get consistent behavior across physics backends

The solution was to implement step handling natively in CharacterBody3D using Godot's existing body_test_motion() API, which works consistently across all physics backends (Godot Physics, Jolt, etc.).

Implementation

The algorithm integrates into _move_and_slide_grounded() during wall collision response:

  1. UP - Test moving up by step_height
  2. FORWARD - Test moving in the intended direction
  3. DOWN - Test moving down by step_height
  4. If a walkable floor is found, accept the stepped position

Step-down is handled by the existing floor_snap_length feature.

Visual Smoothing

Stepping causes instant Y position changes that create jarring camera movement. The new get_visual_position() method returns a smoothed position using proportional interpolation - only the Y component is smoothed, and only while grounded (jumping/falling is unaffected).

New API

Properties:

  • step_enabled (bool, default: false) - Enable automatic step handling
  • step_height (float, default: 0.3m) - Maximum obstacle height to step onto
  • step_smooth_enabled (bool, default: true) - Enable visual position smoothing
  • step_smooth_speed (float, default: 10.0) - Smoothing rate factor

Methods:

  • get_visual_position() - Returns smoothed position for camera attachment

Usage

# CharacterBody3D setup
step_enabled = true
step_height = 0.4

# Camera code
func _process(delta):
    # Use smoothed position instead of global_position
    var visual_offset = player.get_visual_position().y - player.global_position.y
    global_position.y += visual_offset

Notes

  • Only works in MOTION_MODE_GROUNDED
  • For best results, use CylinderShape3D colliders. CapsuleShape3D has a rounded bottom that can cause step detection issues.
  • Works with any physics backend (Godot Physics, Jolt, etc.)

@Calinou
Copy link
Copy Markdown
Member

Calinou commented Dec 30, 2025

This looks great! Can you upload a demo project for easier testing of this PR?

@wareya
Copy link
Copy Markdown
Contributor

wareya commented Dec 30, 2025

Preemptive caveat and counter: there might be issues with how this handles very small motions. If you only move by like 0.002 units per frame, the stair-stepping might get confused and fail to ever find the top of the step, treating it as a wall that you can't climb up, because of other safety margins pushing the character away from the step every frame.

However, this is basically impossible to cleanly fix on the engine side, and the game logic programmer can detect this situation and add a wider fallback step motion when running into walls. So this PR shouldn't try to handle it. Doing this workaround correctly can be very tricky and depends on the exact behavior desired by the game. In some games you don't care about the edge cases much and trying to handle them is a performance hog or results in glitchy-feeling behavior (like faster movement when walking into walls, or jerkiness on rough ground surfaces). If you expose a state flag that says whether or not the previous call to move_and_slide() successfully performed stair stepping, then that would make it slightly easier to do it correctly. But either way it's better for the workaround for this issue to be in the game logic programmer's hands instead of trying to handle it on the engine side.

Also this should absolutely not be taken as a push against this PR and absolutely not taken as a request for any changes, we really need this PR and the general strategy outlined in the original post is the right strategy.

@sinewavey
Copy link
Copy Markdown

How does this handle with other shapes? Cylinders are my preferred choice, too, but they're by far the least stable. I'd imagine it probably has some difficulties with capsules, which is understandable.

@wareya
Copy link
Copy Markdown
Contributor

wareya commented Dec 30, 2025

How does this handle with other shapes? Cylinders are my preferred choice, too, but they're by far the least stable. I'd imagine it probably has some difficulties with capsules, which is understandable.

Roughly problematic section of capsules marked in red in below image. If your collision shape doesn't have any steep bottom parts, it's probably fine. If it does, it'll probably behave weird. The workarounds are similar to the ones you get for extremely low velocities, and are game-specific. This up-sideways-down stairstepping method still works better in more cases than all the alternatives.

image

@laspencer91
Copy link
Copy Markdown
Contributor Author

laspencer91 commented Dec 30, 2025

How does this handle with other shapes? Cylinders are my preferred choice, too, but they're by far the least stable. I'd imagine it probably has some difficulties with capsules, which is understandable.

It didn't work with Capsules well in my testing. I thought about implementing some type of warning when Capsule is selected and stair step enabled.m, but considered that maybe I wasn't using optimal parameter settings.

I think a different method might be needed for Capsules, or a lot of tweaking to get the right parameters. Or someone with a good idea.

I felt it would be best to let the community get a stab at it. It worked so well for cylinders that I thought the value is worth the current PR.

I do have minor concern that someone using Capsules gets frustrated if stair stepping isn't working for them. I hope it's something we can resolve.

@laspencer91 laspencer91 force-pushed the feature/characterbody3d-step-handling branch from 665811d to cc70b3f Compare December 31, 2025 02:47
@laspencer91
Copy link
Copy Markdown
Contributor Author

I did more testing with Capsules. I confirmed that it is not simple to add support for. It will require an entirely different algorithm, and I am not sure of a reliable one. Cylinder / Boxes will always be the most reliable shapes to use for step handling. Capsules are best used with terrain and ramps and other odd complex geometry.

@wareya
Copy link
Copy Markdown
Contributor

wareya commented Jan 1, 2026

The strategy I used for stairstepping with capsules in my most recent capsule-using game was to have a fallback forward trace with a long fixed distance (about 0.2 meters), when normal stairstepping failed and hit a collision plane with a very steep slope. This would have been horribly bad for any game other than the specific game I was making, but it was the best choice for my particular game. Trying to move this kind of hack, or even every possible hack, into the engine's own stairstepper would be a huge mistake, so you're making the right decision, IMO.

@Xiexe
Copy link
Copy Markdown

Xiexe commented Jan 14, 2026

I'd recommend taking a look at what PhysX does for it's built in Character Controller. It's very similar but handles capsules easily enough. Unity's Character Controller is built on this, and though it does have one annoying bug, steps work extremely well and are extremely robust, even with a capsule shape.

The aforementioned bug with with the PhysX controller, at least the version that Unity uses, is that you cannot enter doorways that are less than the height of your capsule + step up distance. Since they do the sweep up, sweep forward, sweep down method, but they stop movement if the sweep forward hits anything. Your capsule effectively ends up being Height + StepHeight combined. Not sure if this was fixed in newer versions, either way it's irrelevant here.

You may also opt to take a look at OpenKCC, which also handles capsules just fine.

In the custom controller I was making for the last company I worked at, I also fell back to a fixed trace distance like @wareya mentioned to solve the problem.. however, I think now the way that I'd probably do it is just treat the capsule as a cylinder when doing step checks. I'm not sure how possible that is here, but it might be something to look into.

I don't agree that you shouldn't use capsules and that a cylinder is fine - capsules naturally prevent things like standing on another entities head and such due to their rounded nature. If this stepper is to be implemented in the engine it should handle every standard shape, and reliably, in my opinion.

@elvisish
Copy link
Copy Markdown

Preemptive caveat and counter: there might be issues with how this handles very small motions. If you only move by like 0.002 units per frame, the stair-stepping might get confused and fail to ever find the top of the step, treating it as a wall that you can't climb up, because of other safety margins pushing the character away from the step every frame.

Does the engine need surface normals exposing to make small units more precise? This fork might be of some use: elim2g@d0563ce

@wareya
Copy link
Copy Markdown
Contributor

wareya commented Jan 14, 2026

Does the engine need surface normals exposing to make small units more precise? This fork might be of some use: elim2g@d0563ce

Unfortunately that could only be used to solve part of the problem. Edge-to-edge collisions make a new collision normal that doesn't exist anywhere on the surfaces that are colliding with each other. It also doesn't help purely analytical shapes like spheres.

@Xiexe
Copy link
Copy Markdown

Xiexe commented Jan 14, 2026

I've just remembered what my final solution was when I was working on the character controller before!

It was something along the lines of this:

First, do the standard up, forward, down sweep tests. The problem with stopping here is that the surface normal of the geometry could be a slope, or the capsule could be causing some fun normal interactions.

So second, get the hit position, and move up and forward (from the direction of movement) by some small amount. This will be our actual sampling point. You really don't have to offset very far, only a small amount in most cases. Trace a ray downwards from this point to find the actual normal.

Use that normal as the normal for determining if the step is valid or not.

IIRC this was what OpenKCC does, and in my case, I had to design a character controller that would handle endless amounts of potentially scuffed user generated content -- this was the solution that worked best across most content. Obviously this does not work in situations where you have a stair with a sloped front face and the collider is beveled to match, but generally that isn't how colliders are laid out, so it works out fine.

The gist is, you essentailly should not trust the original probe for the normal direction, and should instead only trust it as a point of collision, where the normal is derived from a second, cheaper sample of the surface that is slightly ahead of the collision point.

Very bad illustration provided.

(blue line is the normal direction returned, green line is the offset position from original collision from sweep test, red line is the trace down.)
image

@Xiexe
Copy link
Copy Markdown

Xiexe commented Jan 20, 2026

I found an example of what I implemented in a custom Godot character controller awhile back.

Here's the code for the thing I described before, it's in C#, so itll need to be ported, but this should give an overall idea of the solution I'm describing and at the very least a starting point if someone wants to bother porting it.

Note: This probably is not optimized, this was when i first started using godot and I was porting a similar thing I did from Unity. Also using doubles since I use a double precision build.

    private void TryStepUp(double dt)
    {
        IsSteppingUp = false;
        
        // Only attempt step-up if there's significant horizontal movement.
        Vector3 horizontalVelocity = new Vector3(Velocity.X, 0, Velocity.Z);
        if (horizontalVelocity.Length() < 0.001f)
            return;
        
        // We first test the capsule going up to the max step height. If it collides, we move up to the point of collision.
        // Then we move forward by the amount of horizontal velocity we have.
        // Finally, we test moving down by the max step height.
        
        Transform3D currentTestTransform = GlobalTransform;
        
        double allowedUp = MaxStepHeight;
        KinematicCollision3D result = new KinematicCollision3D();
        if (TestMove(currentTestTransform, UpDirection * allowedUp, result))
        {
            allowedUp = Mathf.Min(allowedUp, result.GetTravel().Dot(UpDirection));
        }
        currentTestTransform.Origin += UpDirection * allowedUp;
        
        // Check how far we can move forward, starting from the offset we have from allowedUp
        Vector3 forwardDirection = horizontalVelocity.Normalized();
        double allowedForward = Mathf.Max(0.0f, horizontalVelocity.Length() * dt);
        if (TestMove(currentTestTransform, forwardDirection * allowedForward, result))
        {
            allowedForward = Mathf.Min(allowedForward, result.GetTravel().Dot(forwardDirection));
        }
        currentTestTransform.Origin += forwardDirection * allowedForward;
        
        // Check if we can move down from the position we're at now
        double downTravelDistance = MaxStepHeight;
        if (TestMove(currentTestTransform, -UpDirection * downTravelDistance, result))
        {
            downTravelDistance = Mathf.Min(downTravelDistance, result.GetTravel().Dot(-UpDirection));
            currentTestTransform.Origin += -UpDirection * downTravelDistance;
            
            // draw a raw from above hit position towards the step to get the actual height
            // since we use a capsule this needs to be done, otherwise the actual reported height could/will generally
            // be wrong from the curved base of the capsule
            Vector3 stepHitPosition = result.GetPosition() + (UpDirection * 0.1f) + (forwardDirection * 0.1f);
            var spaceState = GetWorld3D().DirectSpaceState;
            var query = PhysicsRayQueryParameters3D.Create(stepHitPosition, stepHitPosition - UpDirection * 10.0f);
            query.Exclude = [GetRid()];
            
            Dictionary stepHit = spaceState.IntersectRay(query);
            if (stepHit.Count > 0)
            {
                Vector3 stepHitPoint = (Vector3)stepHit["position"];
                Vector3 stepHitNormal = (Vector3)stepHit["normal"];
                
                if (stepHitNormal.Dot(UpDirection) > Mathf.Cos(Mathf.DegToRad(MaxSlope)))
                {
                    IsSteppingUp = true;
                    double hitDistanceFromFeet = (stepHitPoint - GlobalPosition).Dot(UpDirection);
                    GlobalPosition += UpDirection * hitDistanceFromFeet;
                }
            }
        }
    }

@azur-wolve
Copy link
Copy Markdown

azur-wolve commented Jan 21, 2026

Most problems that you have using CapsuleShape will disappear if you use BoxShape or CylinderShape.
The edge case issues with the curved bottom make it not worth it in general IMO.

@Xiexe
Copy link
Copy Markdown

Xiexe commented Jan 21, 2026

Most problems that you have using CapsuleShape will disappear if you use BoxShape or CylinderShape.

The edge case issues with the curved bottom make it not worth it in general IMO.

It is standard practice across pretty much any modern 3D engine to use a capsule. Not solving the problem properly only leads to frustration and confusion to anyone coming from something like Unity or Unreal engine.

@wareya
Copy link
Copy Markdown
Contributor

wareya commented Jan 21, 2026

Capsules can be fully supported later. This feature doesn't need to solve every single possible edge case on the first attempt. For example, it doesn't fully support the thing maybe games do (like the Jak series, and Zelda 64) where, in addition to using collision normals to detect walkability in most cases, some surfaces are marked as forcibly walkable or non-walkable regardless of collision normal direction. You don't see me begging for that to be added even though I know it's a thing that a lot of games do because it's not as important as having stair stepping in the first place.

@elvisish
Copy link
Copy Markdown

Most problems that you have using CapsuleShape will disappear if you use BoxShape or CylinderShape.

The edge case issues with the curved bottom make it not worth it in general IMO.

It is standard practice across pretty much any modern 3D engine to use a capsule. Not solving the problem properly only leads to frustration and confusion to anyone coming from something like Unity or Unreal engine.

It's only standard practice in Unity because they use PhysX and that only has capsule as a collider. If you could choose which collider to use, I'd imagine a lot of people would opt for box or cylinder.

@sinewavey
Copy link
Copy Markdown

I don't think anyone's really asking for perfection here. Nor do I think a comparison to marking surfaces as walkable is fair -- that's asking for interaction with a lot of other parts of Godot and changes well beyond those in CharacterBody3D.

Even though I'm not a fan of capsules for a player controller, I don't personally think it's okay to have the feature shipped in a state where it doesn't actually work with some of the more commonly used primitive shapes, and is inviting a lot of trouble for users.

It's probably best to get a maintainer/team opinion on this to really push forward one way or the other.

@wareya
Copy link
Copy Markdown
Contributor

wareya commented Jan 21, 2026

No, the surface marking problem is the same type of solution as reading face normals from the collision object rather than using the collision normal. It's a direct comparison, not apples to oranges.

@Xiexe
Copy link
Copy Markdown

Xiexe commented Jan 21, 2026

I have just read in the proposal thread that the original opener of this PR doesn't have a lot of time currently.

If my suggestion on how to handle it is approved, I would be more than happy to spend the time porting it into the implementation that exists in this PR.

The solution isn't perfect by any stretch of the word, and I'm not looking for perfection, but it is a tried and tested method and works with a variety of complex scenarios.

All I want is a somewhat robust step handler built into the engine and done in a way that people migrating from other engines are at least roughly happy with it.

I'm hesitant to spend time on it without approval, though, and I don't want to step on any toes.

@wareya
Copy link
Copy Markdown
Contributor

wareya commented Jan 21, 2026

The main gotcha is making sure that the collision engine gives you the right surface normals. If you can't control whether it gives you the top surface or side surface of a step when you're touching the step's edge then it's going to be inconsistent. You also need to be careful in some cases where using surface normals makes halfwalls look like steps (floating point imprecision can make the straight down downcast hook on things that it shouldn't -- I ran into this a lot when godot physics was the main thing). Working through all of the edge cases is non trivial and involves doing a lot of testing, and I don't want this feature to be held back by that.

@Lamoot
Copy link
Copy Markdown

Lamoot commented Feb 8, 2026

Hi,

thank you for implementing this much needed feature. I've built Godot with the PR and tested it with my character controller and it works beautifully!

My only suggestion is to make it more discoverable in the editor that floor_snap_length is used for walking down stairs. I've only learned of this after checking your initial comment here.

I couldn't figure out why stepping down stairs was not working as expected from the information provided in the editor. Now that I know it's easy to set it right, but other users will likely get into the same issue initially. It would help if this was mentioned in internal docs, perhaps added to step_enabled description. Something similar to what you wrote in your initial comment.

Step-down is handled by the existing floor_snap_length feature.

@elvisish
Copy link
Copy Markdown

Is there even a physics maintainer anymore to review this?

@PhairZ
Copy link
Copy Markdown
Contributor

PhairZ commented Feb 14, 2026

Is there even a physics maintainer anymore to review this?

No, as of right now I don't think the Godot team has a physics maintainer (according to what I heard in Godot tomorrow from Emi), which is why these might take a while to get reviewed and merged.

@elvisish
Copy link
Copy Markdown

Hi,

thank you for implementing this much needed feature. I've built Godot with the PR and tested it with my character controller and it works beautifully!

My only suggestion is to make it more discoverable in the editor that floor_snap_length is used for walking down stairs. I've only learned of this after checking your initial comment here.

I couldn't figure out why stepping down stairs was not working as expected from the information provided in the editor. Now that I know it's easy to set it right, but other users will likely get into the same issue initially. It would help if this was mentioned in internal docs, perhaps added to step_enabled description. Something similar to what you wrote in your initial comment.

Step-down is handled by the existing floor_snap_length feature.

How is it for small velocity movements? There's not really any solution currently that works for small motion so crawling or nudging against steps just slides along them. It might be another 5 years before this gets reviewed/merged and I have no idea how to build myself to test it out.

@Lamoot
Copy link
Copy Markdown

Lamoot commented Feb 15, 2026

How is it for small velocity movements?

It doesn't work reliably for movement speeds of 1.5 - 2.0 or less. For context:

  • The testing CharacterBody3D controller is based on the built-in template. The template has default speed set at 5.0.
  • Collision shape is a cylinder, 1.8 m high, 0.4 m radius.
  • Step height is set at 0.6 m

@elvisish
Copy link
Copy Markdown

How is it for small velocity movements?

It doesn't work reliably for movement speeds of 1.5 - 2.0 or less. For context:

  • The testing CharacterBody3D controller is based on the built-in template. The template has default speed set at 5.0.

  • Collision shape is a cylinder, 1.8 m high, 0.4 m radius.

  • Step height is set at 0.6 m

That sounds about right, and there's no stair-step addon as far as I know that can currently that handle such low penetration amounts. I spent a considerable amount of time trying to get small motions work with GDPhysics by clamping the travel test lengths, but it just caused more issues with accidental wall climbing, I assumed Jolt would work better for this kind of thing but I guess not.

Do you have the same issues with a box collider?

@elvisish
Copy link
Copy Markdown

Copy link
Copy Markdown
Member

@Calinou Calinou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested locally, it works as expected. I think this is a great usability improvement in terms of designing character controllers. Combine this with a simple friction edit to the CharacterBody3D template, some mouselook code, then you have a fully functional first-person character controller.

Testing project: https://github.com/Calinou/godot-cmvalley/tree/gh-114447
⚠️ Also download cmvalley.zip, extract it, and move cmvalley.obj to the cmvalley/ folder before opening the project in the editor.

The above project contains a lot of complex geometry, with no manual clipping or simplified geometry for collision. It's a good example of a difficult scenario, and this PR handles it quite well (although not perfectly).

Some feedback:

  • When walking near to a wall that can't be stepped on (but eventually becomes steppable due to a lowering height), stair stepping will fail to activate if you don't get momentum first. This is more common at higher physics tick rates.
step_momentum.mp4

For example, if you try to step on the left, it won't work (as expected, since it's too high). If you try to step on the right, it will only work if you've already garnered enough speed. Trying to hug the slope's side then step up on the right won't work as one would expect.

Image
  • Getting camera height smoothing and physics interpolation to work correctly is nontrivial. At the very least, it should be documented in the class reference (as per my review suggestions).

  • Stair stepping does not apply when you're airborne, which makes jumping up stairs cumbersome (you will hit the edge of stairs as you jump). It also prevents you from using the step height to successfully perform jumps that were just barely missing the edge of a wall, which can be frustrating for the player.

    • There should be a way to control the step height while airborne (airstepping), defaulting to the same value as the grounded step height. Some games use the same value as when grounded, while others will use lower values when airborne.

      • If stepping is changed to be applied when airborne, then this can be implemented in game logic by changing the step height according to the player's current state, without having to introduce a new property. The default behavior would then be to use the step height for airstepping as when grounded, which I think is reasonable for most games.

For future PRs:

  • I wonder how this algorithm would behave if we applied it to hitting walls (somewhat like skimming, if you've played CPMA). Combined with camera smoothing, this could reduce the need for level designers to aggressively clip levels by smoothing out collisions not just with floors, but also with walls.

Comment on lines +92 to +93
Returns a smoothed position for visual elements like cameras. When [member step_smooth_enabled] is [code]true[/code], this position interpolates the vertical component during step-up and step-down events, preventing jarring camera movement when traversing stairs. The horizontal components match the actual physics position.
Use this for camera positioning instead of [member Node3D.global_position] when step handling is enabled.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Returns a smoothed position for visual elements like cameras. When [member step_smooth_enabled] is [code]true[/code], this position interpolates the vertical component during step-up and step-down events, preventing jarring camera movement when traversing stairs. The horizontal components match the actual physics position.
Use this for camera positioning instead of [member Node3D.global_position] when step handling is enabled.
Returns a smoothed position for visual elements like cameras. When [member step_smooth_enabled] is [code]true[/code], this position interpolates the vertical component during step-up and step-down events, preventing jarring camera movement when traversing stairs. The horizontal components match the actual physics position.
Use this for camera positioning instead of [member Node3D.global_position] when step handling is enabled.
[codeblocks]
[gdscript]
var old_visual_position = get_visual_position()
var new_visual_position = get_visual_position()
func _process(_delta: float) -> void:
# Apply step smoothing with manual physics interpolation.
# The Camera3D node is marked as top-level and has physics
# interpolation set to Off in the inspector.
$Camera3D.global_position = old_visual_position.lerp(new_visual_position, Engine.get_physics_interpolation_fraction())
func _physics_process(delta: float) -> void:
old_visual_position = get_visual_position()
# [...]
move_and_slide()
new_visual_position = get_visual_position()
[/gdscript]
[/codeblocks]

Maximum height of obstacles the body can step onto when [member step_enabled] is [code]true[/code]. Obstacles taller than this will block movement and cause sliding.
</member>
<member name="step_smooth_enabled" type="bool" setter="set_step_smooth_enabled" getter="is_step_smooth_enabled" default="true">
If [code]true[/code], the [method get_visual_position] method returns a smoothed position that interpolates during step events. This prevents jarring camera movement when traversing stairs.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
If [code]true[/code], the [method get_visual_position] method returns a smoothed position that interpolates during step events. This prevents jarring camera movement when traversing stairs.
If [code]true[/code], the [method get_visual_position] method returns a smoothed position that interpolates during step events. This prevents jarring camera movement when traversing stairs.
[b]Note:[/b] Step smoothing applies on any height change while on the floor, which means [member floor_snap_height] will also result in step smoothing.

[b]Note:[/b] For best results, use a [CylinderShape3D] collider. [CapsuleShape3D] colliders have rounded bottoms that can cause step detection issues.
</member>
<member name="step_height" type="float" setter="set_step_height" getter="get_step_height" default="0.3">
Maximum height of obstacles the body can step onto when [member step_enabled] is [code]true[/code]. Obstacles taller than this will block movement and cause sliding.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Maximum height of obstacles the body can step onto when [member step_enabled] is [code]true[/code]. Obstacles taller than this will block movement and cause sliding.
Maximum height of obstacles the body can step onto when [member step_enabled] is [code]true[/code]. Obstacles taller than this will block movement and cause sliding.
[b]Note:[/b] Set [member floor_snap_height] to the same value as [member step_height] to also enable "step down" behavior, with the same camera smoothing.

Adds automatic stair stepping using up-forward-down trace algorithm.
Includes visual position smoothing for camera integration.

New properties: step_enabled, step_height, step_smooth_enabled, step_smooth_speed
New method: get_visual_position()
@laspencer91 laspencer91 force-pushed the feature/characterbody3d-step-handling branch from cc70b3f to 7f81cc8 Compare April 14, 2026 15:24
@2Sloppy4slime
Copy link
Copy Markdown

bump, this would be very useful, I personally use 2 raycasts (one to check if there is something to step on and the other to check if the thing isn't too high) but this would make it so much easier

@Andicraft
Copy link
Copy Markdown

I tried to fix the "no stepping unless you are at high enough speeds" problem in my own implementation by having the step be performed based on a desired movement direction rather than velocity, if the velocity was below a certain threshold.

Can't remember if it worked, but I would have to argue that the stair stepping needs to always work before it gets merged as a default feature - it would be confusing for users when they stop next to a step and then suddenly can't walk up it.

@TAGames
Copy link
Copy Markdown

TAGames commented Apr 30, 2026

Most problems that you have using CapsuleShape will disappear if you use BoxShape or CylinderShape.
The edge case issues with the curved bottom make it not worth it in general IMO.

It is standard practice across pretty much any modern 3D engine to use a capsule. Not solving the problem properly only leads to frustration and confusion to anyone coming from something like Unity or Unreal engine.

I feel like this topic was seen from first person characters only.
One problem I see when not using capsules:

image

@wareya
Copy link
Copy Markdown
Contributor

wareya commented Apr 30, 2026

One problem I see when not using capsules:

This is what inverse kinematics is for.

@TAGames
Copy link
Copy Markdown

TAGames commented Apr 30, 2026

One problem I see when not using capsules:

This is what inverse kinematics is for.

When using IK, are models usually not positioned at the bottom of the collision shape? But instead below to avoid strechting the models legs?

And what about projects that don't want to use IK, like games before IK was a thing?

@wareya
Copy link
Copy Markdown
Contributor

wareya commented Apr 30, 2026

When using IK, are models usually not positioned at the bottom of the collision shape? But instead below to avoid strechting the models legs?

Yes, the whole model is shifted down as needed to keep the legs from stretching.

And what about projects that don't want to use IK, like games before IK was a thing?

You can use a combined collision shape that has a wide middle but a narrow top/bottom, e.g. two cylinders at once. Or a convex hull shape with a narrow bottom. As long as you avoid having "upside-down steep slope" sides on the bottom of the final shape it won't have the same problems that capsules do for stairstepping. For example:

image

(You have to be careful with this and find a way to make the player slide off of ledges that the middle collision shape lands on, but it has its place in games that don't have jumping/platforming as a mechanic, like RPGs. Making the middle collision shape something like carefully-sized sphere would work too.)

You could also do a single shapecast and offset the model based on it, as a kind of "fake IK" that doesn't involve poking at the model's skeleton.

@TAGames
Copy link
Copy Markdown

TAGames commented Apr 30, 2026

Thanks for the infos. It was just something that bothered me. I don't know how much of an effort it is to implement this and would be happy with basic stair stepping for now.

I approached this topic from a users and not developers perspective. And for me personally, the simplest way to get decent results always was using capsule colliders in other engines. Nothing to worry about using multiple collision shapes or floating models on ramps.

@wareya
Copy link
Copy Markdown
Contributor

wareya commented Apr 30, 2026

I approached this topic from a users and not developers perspective. And for me personally, the simplest way to get decent results always was using capsule colliders in other engines. Nothing to worry about using multiple collision shapes or floating models on ramps.

The way that capsules interact badly with stair stepping is universal. All the edge cases being laid out for this pull request apply to everything else in the industry.

The intuition that capsules should handle everything universally well is a convenient tutorial lie, and it doesn't actually bear out in practice. Any game you play that has capsules and doesn't feel weird around small ledges or stairs has a bunch of invisible game-specific hacks happening to make it feel right.

Introduce single-inheritance-friendly interface contracts to GDScript via
the reserved `trait` keyword plus a new `implements` keyword:

    trait SimSystem:
        func get_value() -> int

    class Something extends RefCounted implements SimSystem:
        func get_value() -> int:
            return 42

A class may declare it satisfies one or more traits. The analyzer verifies
every trait method is implemented with a compatible signature, and a trait
is usable as a static type (a class is type-compatible with any trait it
implements). Traits are compile-time-only contracts with no runtime
identity -- they are lowered to untyped in the compiler -- so `implements`
is analyzer-only metadata. Runtime `is`/`as` reflection against traits is
intentionally deferred.

- Tokenizer: new `implements` keyword (reuses the reserved `trait`).
- Parser: file-level and inner trait declarations, `implements` lists,
  trait bodies restricted to bodyless (abstract) functions.
- Analyzer: trait implementation + signature checks; trait/implementer
  type compatibility.
- Compiler: lower trait-typed values to untyped at runtime.
- Tests: analyzer error tests + a runtime feature test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@laspencer91 laspencer91 requested review from a team as code owners June 3, 2026 11:37
@AThousandShips
Copy link
Copy Markdown
Member

AThousandShips commented Jun 3, 2026

Please disclose any AI use, see the PR guidelines.

You also seem to have included unrelated changes

A file-level `trait` (no colon, members at file scope) registers as a
global class and can be implemented and used as a static type from another
file, including being passed where the trait type is expected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
laspencer91 and others added 2 commits June 3, 2026 07:16
A file-level trait follows `class_name` syntax (no colon, members at file
scope). Writing `trait Foo:` at file scope now reports an actionable message
instead of the generic "Expected end of statement after trait declaration".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`obj is SomeTrait` and `obj as SomeTrait` now recognise trait implementation:
`is` is true (and `as` returns the object) when the object's class or any base
class declares the trait via `implements`; otherwise `is` is false and `as`
returns null.

The compiled GDScript records the traits each class implements, and the
`is`/`as` opcodes consult that list while walking the base-script chain. Trait
types remain lowered to untyped for variables/parameters, except in the
`is`/`as` paths, which need the real trait script type.

- GDScript: store `implemented_traits` per class.
- Compiler: populate it; emit the real trait type for `is`/`as` (not lowered).
- VM: check the trait list in OPCODE_TYPE_TEST_SCRIPT and OPCODE_CAST_TO_SCRIPT.
- Analyzer: allow `is`/`as` against a trait regardless of the operand's static type.
- Test: runtime is/as coverage including inheritance and a non-implementer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@AThousandShips
Copy link
Copy Markdown
Member

@laspencer91 please stop pushing unrelated changes to this PR running CI unnecessarily

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.