Physics: Add step handling to CharacterBody3D#114447
Conversation
|
This looks great! Can you upload a demo project for easier testing of this PR? |
|
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 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. |
|
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. |
665811d to
cc70b3f
Compare
|
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. |
|
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. |
|
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. |
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. |
|
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;
}
}
}
} |
|
Most problems that you have using |
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. |
|
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. |
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. |
|
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. |
|
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. |
|
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. |
|
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. |
|
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 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
|
|
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. |
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. |
It doesn't work reliably for movement speeds of 1.5 - 2.0 or less. For context:
|
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? |
|
Here’s another stair-stepper, this time for C#: https://github.com/PolarBears-studio/player-controller/blob/main/addons/player_controller/Scripts/StairsSystem.cs |
There was a problem hiding this comment.
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
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.
-
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.
| 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. |
There was a problem hiding this comment.
| 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. |
There was a problem hiding this comment.
| 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. |
There was a problem hiding this comment.
| 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()
cc70b3f to
7f81cc8
Compare
|
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 |
|
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. |
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? |
|
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. |
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>
|
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>
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>
|
@laspencer91 please stop pushing unrelated changes to this PR running CI unnecessarily |




Summary
Adds native step handling to
CharacterBody3Dusing 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)!
Closes
godotengine/godot-proposals#2751
The Journey
Initially, I attempted to leverage Jolt Physics' built-in
CharacterVirtualstep handling viabody_move_and_step(). While Jolt's implementation works well internally, exposing it through Godot's physics abstraction proved problematic:CharacterVirtualapproach requires a separate physics body type with different collision semanticsCharacterBody3D's existingmove_and_slide()workflow created state synchronization issuesThe solution was to implement step handling natively in
CharacterBody3Dusing Godot's existingbody_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:step_heightstep_heightStep-down is handled by the existing
floor_snap_lengthfeature.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 handlingstep_height(float, default: 0.3m) - Maximum obstacle height to step ontostep_smooth_enabled(bool, default: true) - Enable visual position smoothingstep_smooth_speed(float, default: 10.0) - Smoothing rate factorMethods:
get_visual_position()- Returns smoothed position for camera attachmentUsage
Notes