godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.11k stars 69 forks source link

Add automatic smooth stairs step-up and step-down for KinematicBody using `move_and_slide()` #2751

Open elvisish opened 3 years ago

elvisish commented 3 years ago

Describe the project you are working on

Character controller for Godot

Describe the problem or limitation you are having in your project

For any type of movement that requires a kinematic rigidbody, a stair test should be built-in to the move_and_slide function, allowing the user to:

Describe the feature / enhancement and how it helps to overcome the problem or limitation

Step up distance: Any stable surface below this height will be walkable by the character.

image

Step down distance: When losing contact with the ground, if the distance between the character and the ground is less or equal to this value, then the character will be automatically grounded (sticking to the ground). If there is no ground at all, then the character will be not grounded.

image

image

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

The user would specify the if smooth stepping up and down stairs is enabled, and the step height: Vector3 move_and_slide ( Vector3 linear_velocity, Vector3 up_direction=Vector3( 0, 0, 0 ), bool stop_on_slope=false, int max_slides=4, float floor_max_angle=0.785398, bool infinite_inertia=true, smooth_step=true, step_height=4 )

It would also work with move_and_slide_with_snap: Vector3 move_and_slide_with_snap ( Vector3 linear_velocity, Vector3 snap, Vector3 up_direction=Vector3( 0, 0, 0 ), bool stop_on_slope=false, int max_slides=4, float floor_max_angle=0.785398, bool infinite_inertia=true, smooth_step=true, step_height=4 )

If this enhancement will not be used often, can it be worked around with a few lines of script?

No, it would need to be integrated to move_and_slide.

Is there a reason why this should be core and not an add-on in the asset library?

It would need to be implemented in the kinematic body engine code.

Calinou commented 3 years ago

See also https://github.com/godotengine/godot-proposals/issues/2184 and Shifty's Godot Character Movement Manifesto, which recommends using RigidBodies for characters instead of KinematicBodies.

wareya commented 3 years ago

+1

Logic for stepping up stairs is basically mandatory to a lot of styles of first person shooter, and no, using capsules doesn't cut it as a universal solution (they only work for relatively short steps and cause other gameplay changes).

I once tried writing my own FPS-style movement controller on top of move_and_collide before, but the way that move_and_collide handles marginal collisions (in ways that move_and_slide's internal traces don't) caused a lot of problems. Stuff like this should be built in.

Getting move_and_slide_with_snap to handle stepping down stairs for you is already possible to a certain limited degree.

SaracenOne commented 3 years ago

Very much in favour of exploring this further. Trying to retrofit this behaviour with existing physics engine issues has consistenly been a massive pain point, breaking in so many instances. An official high quality representation of a character controller with a reliable smooth climb would be a huge benefit to anyone doing any kind of action game.

atlrvrse commented 2 years ago

Very very much in favor as well, would make things so much easier. I tried implementing it via script and I got pretty close but results didn't satisfy

Zireael07 commented 1 year ago

Bump. I hacked around with some local translations, but it's jittery. I need something smooth.

timshannon commented 1 year ago

FWIW @Zireael07 I get around the jittery issues in my stair-stepping code by lerping the camera's offset from the character controller.

immccc commented 1 year ago

This would be a really nice feature for next version.

In the meantime I'm trying to implement this on my own. The idea I have in mind, in short, is:

  1. Get the next collision according to the direction the kinematic body has.
  2. If collision position is NOT below certain threshold of the kinematic body, do not climb the step.
  3. If collision normal is NOT horizontal and with opposite direction than the kinematic body movement, do not climb the step.
  4. Otherwise, climb the step.

stairs_example

But I have some problems with this approach:

climbing wall

I could solve this to have two separate tilemaps, one for the floor and another for the wall, but trust me when I say I have not enough collision layers to waste, as I implemented a "multilayered" system of tilemaps (as you can see from the first gif on this message).

Calinou commented 1 year ago

I need to have a particular collision layer for the stairs, in order to avoid the body to climb walls. Unfortunately, a particular tilemap can only have one collision layer.

You can probably use a second raycast to check whether the step can actually be climbed (i.e. it's not obstructed by a wall).

immccc commented 1 year ago

I need to have a particular collision layer for the stairs, in order to avoid the body to climb walls. Unfortunately, a particular tilemap can only have one collision layer.

That's a good idea. I was thinking on two areas, a top and a bottom. If only bottom one is colliding, then a step is detected and the body climbs it.

Problem is that my kinematic body is getting more areas than population in eastern Asia 😂

wareya commented 1 year ago

Stairstepping algorithms usually work by performing several "real" collision traces in a row rather than by comparing the collision point. For example: trace up, trace sideways, trace down, check if downwards trace is a "floor". If it's not a floor, there's no stairs to climb here, reset and use normal slide-based movement; and if it instead is a floor, you're done.

(Sometimes "floor" for stair stepping is defined in a more complicated way than just using the collision normal; for example, if you have small steep edges that you don't want the player to snag on, you might want to flag their geometry as "always a floor", or you might want to do a raycast (as in, actual raycast) to find the floor instead of a collision trace, depending on the game.)

elvisish commented 1 year ago

This should be at engine core level for best integration with the physics instead of glued on top after with gdscript. And this should probably be changed to include move() and CharacterBody since we’re nearly into GD4 now 🙄

elvisish commented 1 year ago

Godot 4.0 nearing stable, it would put Godot on pairing with UE and Unity if it had stair-stepping built-in, but given the extraordinarily long list of physics bugs in Godot Physics and lack of contributors in this area right now, it seems unlikely anything will happen any time soon. I still think it should be a built-in feature in CharacterBody and people shouldn't have to use their own (usually flawed) implementations to do something so basic and standard in almost every physics engine.

KeyboardDanni commented 1 year ago

So the way early games approached this was to sample tiles at points around the character. For example, to check for walls, a single point is tested on the left and right sides. This point is typically vertically centered around the character, so if the player walks into a low enough step, the side detectors will miss it, and then the floor detectors will eventually come into contact with the step and move the character upward.

Here is how M.C. Kids did it: https://games.greggman.com/game/programming_m_c__kids/

And here's a demo of the movement and sensors in action in Sonic 1:

SPGCollisionDemoLite (taken from https://info.sonicretro.org/SPG:Slope_Collision )

Notice how right after the loop here, there's a sudden "bump" in the terrain, but it's not an issue as the wall sensor is high enough to pass over it.

I played around with this and it looks like yes, you can in fact do this today in Godot, even with the standard CharacterBody2D! You just need to use several SeparationRayShape2D colliders in place of a box or capsule collider. So far it seems to work pretty well with a few caveats:

SPGSensorsOnTiles

KeyboardDanni commented 1 year ago

Here's the setup in Godot:

2023-02-20 16_36_08-Godot

Unfortunately, it looks like I ran into my first "character getting stuck" bug:

2023-02-20 16_47_25-2D Test (DEBUG)

Full video: https://user-images.githubusercontent.com/34800072/220202852-00e441ca-1149-462f-870d-77db7e09650b.mp4

baznzab commented 1 year ago

There is an implementation of stair movement in godot 3.5. It also refers to an article which describes the idea behind it. I tried to implement such algorithm myself and it worked pretty well, I probably could polish it to some extent, but the thing is, in order to do that I would need to reimplement the whole logic behind existing CharacterBody3D. I think it would make much more sense if it was built-in into engine's default node, since it already has pretty much everything in order to achieve this.

Here is my code:

class_name ThirdPersonController
extends CharacterBody3D

@export var camera: ThirdPersonCamera
@export var walk_speed: float = 4
@export var sprint_speed: float = 6
@export var jump_height: float = 1
@export var gravity: float = -9.8
@export var step_height: float = 0.25

func _physics_process(delta: float) -> void:
    # Add the gravity.
    if not is_on_floor():
        velocity.y += gravity * delta

    # Handle Jump.
    if Input.is_action_just_pressed("jump") and is_on_floor():
        velocity.y = sqrt(-2 * gravity * jump_height)

    # Get the input direction and handle the movement/deceleration.
    # As good practice, you should replace UI actions with custom gameplay actions.
    var input_dir := Input.get_vector("move_left", "move_right", "move_up", "move_down")
    var direction := (camera.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
    var speed = sprint_speed if Input.is_action_pressed("sprint") else walk_speed
    if direction:
        velocity.x = direction.x * speed
        velocity.z = direction.z * speed

        var yaw = atan2(-direction.x, -direction.z)
        quaternion = Quaternion(Vector3.UP, yaw).normalized()
    else:
        velocity.x = move_toward(velocity.x, 0, speed)
        velocity.z = move_toward(velocity.z, 0, speed)

#   _step_up(delta)
#   move_and_slide()
    if not _step_up(delta):
        move_and_slide()

func _slide(
    body: RID,
    from: Transform3D,
    motion: Vector3,
    margin: float = 0.001,
    max_slides: int = 6,
    max_collisions: int = 16
    ) -> Vector3:

    for i in range(max_slides):
        var params := PhysicsTestMotionParameters3D.new()
        params.from = from
        params.motion = motion
        params.margin = margin
        params.max_collisions = max_collisions

        var result := PhysicsTestMotionResult3D.new()
        if not PhysicsServer3D.body_test_motion(body, params, result):
            break

        var normal: Vector3 = (
            range(result.get_collision_count())
            .map(func(collision_index): return result.get_collision_normal(collision_index))
            .reduce(func(sum, normal): return sum + normal, Vector3.ZERO)
            .normalized()
        )
        motion = result.get_remainder().slide(normal)
        from = from.translated(result.get_travel())

    return motion

func _step_up(delta: float) -> bool:
    # do step only if grounded
    if not is_on_floor():
        return false

    # cast body upword by step_height
    var up_test_params := PhysicsTestMotionParameters3D.new()
    up_test_params.from = global_transform
    up_test_params.motion = step_height * up_direction
    up_test_params.margin = safe_margin
    if PhysicsServer3D.body_test_motion(get_rid(), up_test_params):
        print("up block")
        return false

    var up_transform = global_transform.translated(step_height * up_direction)
    var slide_motion = (
        _slide(get_rid(), up_transform, velocity * delta, safe_margin, max_slides)
        .slide(up_direction)
    )

    # cast body by slide motion
    var forward_test_params := PhysicsTestMotionParameters3D.new()
    forward_test_params.from = up_transform
    forward_test_params.motion = slide_motion
    forward_test_params.margin = safe_margin
    if PhysicsServer3D.body_test_motion(get_rid(), forward_test_params):
        print("fwd block")
        return false

    # cast body downward by step_height
    var down_test_from := up_transform.translated(slide_motion)
    var down_test_params := PhysicsTestMotionParameters3D.new()
    down_test_params.from = down_test_from
    down_test_params.motion = -step_height * up_direction
    down_test_params.margin = safe_margin
    var down_test_result := PhysicsTestMotionResult3D.new()
    if PhysicsServer3D.body_test_motion(get_rid(), down_test_params, down_test_result):
        #global_transform.translated(-down_test_result.get_remainder())
        global_transform = down_test_from.translated(down_test_result.get_travel())
        return true

    return false

I needed to reimplement sliding logic here to virtually move character body to test if it's possible to step up, which might have some collisions with existing sliding logic inside the CharacterBody3D. It also is very likely to conflict with floor snapping inside move_and_slide(). Of course my code lacking slope detection, moving platforms and so on... and this is the reason why I think it should be built-in (cause it's already done). Otherwise if u just need proper stair movement (which u usually do), u simply need to reimplement the whole CharacterBody3D and add stair movement on top.

Calinou commented 1 year ago

https://github.com/godotengine/godot-demo-projects/pull/849 also features stairstepping using a ShapeCast3D, although the camera height isn't smoothed yet.

baznzab commented 1 year ago

@Calinou thank you for your reply. I checked your solutions out. I think it might work as fast and easy implementation. But it isn't that reliable. It doesn't take into account how the controller moves under the hood, which makes it pretty easy to break. For example it took me just a few minutes to get my character jitter up and down when trying to move forward while facing more or less complex corner. The reason why ShapeCast3D is not reliable in this case is because it just checks how character would move in one direction while it is necessary to trace the whole path (first upwords to check if character can elevate, second along velocity direction to check if body can advance and lastly downward to check where to step exactly). Furthermore event doing those checks isn't sufficient because down the whole path I just described the body should follow same rules CharacterBody3D does. Simple example why it is needed is when character is trying to step up while facing wall at some angle. If u just stop him from moving as soon as "forward" ShapeCast hits something body would just stuck instead of both stepping up and sliding down the wall. I'm not even mentioning more advanced cases like detecting moving platforms and so on... That is why I'm telling that it would make a lot of sense if it was implemented inside CharacterBody3D itself since this functionality already exists in there. I did some investigation on how the node works and sure enough it isn't quite ready to easily do it atm, and it would require some refactoring, but it is totally doable. If it was implemented it would really make CharacterBody3D much more usefull. It already has great advantages over for example Unity's built-in PhysX-based CharacterController (like the ability to set up_direction or moving platform awareness out of the box), the only thing it really lacks is built-in reliable step handling unfortunately.

wareya commented 1 year ago

Having implemented movement solvers from scratch many times before, both in 2d and 3d, I want to add my two cents and say that juju8e's solution, referencing the The Low Rooms article, is The Way. It is very common and resilient. It's the approach Quake uses, for example, and consequently also Source Engine games.

elvisish commented 1 year ago

Best stairs implementation so far is mrezai's: https://github.com/mrezai/GodotStairs I ported it to 4.0: https://github.com/elvisish/GodotStairs

This built-in to KinemeticBody3D/CharacterBody3D would be more than adequate, honestly.

elvisish commented 1 year ago

Best stairs implementation so far is mrezai's: https://github.com/mrezai/GodotStairs I ported it to 4.0: https://github.com/elvisish/GodotStairs

This built-in to KinemeticBody3D/CharacterBody3D would be more than adequate, honestly.

This is the best there is for stairs, but a good example of why this should be built-in: image

A stress test of 250 enemies shows just how slow it is to run the stair-step function on every frame. Comparatively on fps:

With stairs: image

Without stairs: image

This really, really should be built-in by now. At the very least if Godot is planning on switching over to Jolt physics eventually, consider building stair-stepping into that.

reduz commented 1 year ago

There clearly is a pretty bad documentation issue if nobody here realized you can use SeparationRayShape for stair stepping.

Zireael07 commented 1 year ago

I think I'm inclined to agree - I use raycasts for "stepping" up sloped roads (which are effectively stairs as far as the physics are concerned)

reduz commented 1 year ago

You can use SeparationRayShapes in both 2D and 3D with kinematic controllers and the snap functions. This makes it work fine with stairs and has been well tested. It was designed for this very use case.

elvisish commented 1 year ago

There clearly is a pretty bad documentation issue if nobody here realized you can use SeparationRayShape for stair stepping.

To be fair, this was opened long before SeparationRayShape was added to the engine.

You can use SeparationRayShapes in both 2D and 3D with kinematic controllers and the snap functions. This makes it work fine with stairs and has been well tested. It was designed for this very use case.

It would be interesting to use this in conjunction with @mrezai 's stair-stepping method, since proper stair-stepping requires the motion to be checked in a multiple of ways to ensure the stepping works correctly. Is there a working example of SeparationRayShape being used for stairstepping? I still believe it would be more performant to have it built-in to the engine at low-level though.

elvisish commented 1 year ago

I think I'm inclined to agree - I use raycasts for "stepping" up sloped roads (which are effectively stairs as far as the physics are concerned)

That's really an error of the physics, slopes should never be considered stairs unless the slope angle is too high.

reduz commented 1 year ago

If it helps, I remember discussing this in old forums or mailing lists with developers from several AAA studios and the general consensus is to just throw a short raycast downards with a capsule the size of the maximum step you expect to have (genreally low). This will never result your character getting stuck, anywhere and since this is generally combined with using IK to adjust the feet to the floor, it pretty much works. It's one of those cases where simplicity is the best approach even if not perfect.

reduz commented 1 year ago

The CharacterBody was also adapted to snap to the raycasts too, so you should be able to go up and downstairs fine.

elvisish commented 1 year ago

The CharacterBody was also adapted to snap to the raycasts too, so you should be able to go up and downstairs fine.

Is there an example of this working? Stair examples are pretty rare for Godot (by comparison Unity had five major character-controller assets a few years ago that all had custom stair-stepping).

timshannon commented 1 year ago

There clearly is a pretty bad documentation issue if nobody here realized you can use SeparationRayShape for stair stepping.

image

Yep, I must not be smart enough to translate spears falling from the sky to stair stepping. After some preliminary testing, this solves several different issues I'm having including stair stepping.

elvisish commented 1 year ago

There clearly is a pretty bad documentation issue if nobody here realized you can use SeparationRayShape for stair stepping.

image

Yep, I must not be smart enough to translate spears falling from the sky to stair stepping. After some preliminary testing, this solves several different issues I'm having including stair stepping.

I still don't know what "spears falling from the sky" means 🤷‍♀️

KeyboardDanni commented 1 year ago

Right now the biggest performance problem with using either separation rays or object repositioning is time spent updating the BVH. This can get particularly expensive if you have many close/overlapping objects, even if they don't collide with each other according to their collision masks. We need more convenient ways to query the physics world for collisions without modifying BVH state for each shape used to test, and only update BVH for the resulting object's position for other objects to collide against.

It could also help to have separate BVHs for different collision layers. This would dramatically speed up lookups for objects that can't collide with each other, but it might hurt performance for using multiple simultaneous layers.

wareya commented 1 year ago

SeparationRayShapes and Raycasts do not solve stairs. Proper stair-climbing with "do a bunch of slides, never overlap solid geometry" movement solvers, like Godot's, must involve three or so distinct collision traces/sweeps in different directions and from different positions, as detailed in other people's earlier posts here. If you try to use a different approach to solving stairs, then you will run into situations where you teleport through the corners of walls or get stuck in ceilings/overhangs or glitch out in cramped spaces or clip around strangely on small objects.

The strategy of moving the character into a new position on top of a stair step based on a single collision test (after movement has already happened) only works for movement solvers that deliberately overlap solid geometry (and then fix the overlap later), which isn't the approach to movement solving that Godot uses for character bodies, only rigid body simulation.

reduz commented 1 year ago

@wareya

If you try to use a different approach to solving stairs, then you will run into situations where you teleport through the corners of walls or get stuck in ceilings/overhangs or glitch out in cramped spaces or clip around strangely on small objects.

No, it really does not happen, there is no chance you will teleport through geometry, separator ray solves the collision using CCD for CharacterBody so there is no way this will happen.

The strategy of moving the character into a new position on top of a stair step based on a single collision test (after movement has already happened) only works for movement solvers that deliberately overlap solid geometry (and then fix the overlap later), which isn't the approach to movement solving that Godot uses for character bodies, only rigid body simulation.

It is the same approach, the ray is solved via CCD.

wareya commented 1 year ago

The reason for using several collision traces isn't to overcome a lack of CCD (CCD is assumed for all techniques); it's to avoid probing from inaccessible locations, without at the same time missing any gaps that the character Can actually travel through. For example, in this image:

image

Even if you know that there is a stair at the target location, with a safe place to stand on top of it... You must perform an extra trace, from the original character position, upwards, to know that there is a ceiling above the player and that the player cannot fit through the gap because it is too small.

In general, to avoid strange clipping behavior at high movement speeds in this kind of geometry, you have to outline the shape of the stair step with collision traces/sweeps, which boils down to at least three collision sweeps: one upwards, one forwards, and one down. Importantly, each of these traces starts from the actual end location of the previous trace (with the upwards one starting from the original position), to accommodate any differences in the actual locations of nearby ceilings and walls.

wareya commented 1 year ago

I just made a small test project for testing stair-stepping. It has CSG geometry set up to make it easy to test common stair-stepping bugs. (Edit: It's licensed as CC0, feel free to look without worry.)

It also has an FPS character controller, one that tries to use a basic three-trace stair-stepping algorithm, but it falls back to plain old move_and_slide if it doesn't find a stair. The implementation is not ideal, since it doesn't check every possible slide like move_and_slide does, so it's not perfect. But any recommended approach should work at least as well as this, especially since it's not perfect.

https://github.com/wareya/GodotStairTester/tree/main

Godot_v4 1-stable_win64_2023-07-24_19-54-26


    # check for simple stairs; three steps
    var start_position = global_position
    var found_stairs = false
    var wall_test_travel = null
    var wall_collision = null
    if do_stairs:
        # step 1: upwards trace
        var ceiling_collision = move_and_collide(step_height * Vector3.UP)
        var ceiling_travel_distance = step_height if not ceiling_collision else abs(ceiling_collision.get_travel().y)
        # step 2: "check if there's a wall" trace
        wall_test_travel = velocity * delta
        wall_collision = move_and_collide(wall_test_travel)
        # step 3: downwards trace
        var floor_collision = move_and_collide(Vector3.DOWN * (ceiling_travel_distance + (step_height if is_on_floor() else 0.0)))
        if floor_collision and floor_collision.get_collision_count() > 0 and acos(floor_collision.get_normal(0).y) < floor_max_angle:
            found_stairs = true

    # (this section is more complex than it needs to be, because of move_and_slide taking velocity and delta for granted)
    if found_stairs:
        # if we found stairs, climb up them
        var old_velocity = velocity
        if wall_collision and wall_test_travel.length_squared() > 0.0:
            # try to apply the remaining travel distance if we hit a wall
            var remaining_factor = wall_collision.get_remainder().length() / wall_test_travel.length()
            velocity *= remaining_factor
            move_and_slide()
            velocity /= remaining_factor
        else:
            # even if we didn't hit a wall, we still need to use move_and_slide to make is_on_floor() work properly
            var old_vel = velocity
            velocity = Vector3()
            move_and_slide()
            velocity = old_vel
    else:
        # no stairs, do "normal" non-stairs movement
        global_position = start_position
        move_and_slide()
Zireael07 commented 1 year ago

@elvisish In case I wasn't clear: visually it's a slope but physically it's stairs

operatios commented 1 year ago

I think this thread https://github.com/godotengine/godot-proposals/issues/333 is relevant to the discussion of SeparationRayShapes and why they're not robust or all that useful for stairstepping.

Even if you don't care about the fact that the bottom part of the character is being approximated by a single raycast, here's an illustration of the issue @wareya is talking about: scene

https://github.com/godotengine/godot-proposals/assets/23556388/b06754e3-f6c6-4768-b1be-7a6138a54f61

The obvious thing is that you end up getting stuck inside the second cube, but even if you didn't, the path you take is not reversible.

Below are some code references (I'm not sure if Godot maintainers are allowed to look at them based on licensing or whatever), but basically, you have to do multiple traces to achieve proper stairstepping in your character controller, meaning SeparationRayShapes are not the solution for this problem.

https://github.com/ValveSoftware/source-sdk-2013/blob/0d8dceea4310fde5706b3ce1c70609d72a38efdf/mp/src/game/shared/gamemovement.cpp#L1517

https://github.com/NVIDIA-Omniverse/PhysX/blob/8f6cf9cfe89f2bedafbf7788a72cb04f11c31e1f/physx/source/physxcharacterkinematic/src/CctCharacterController.cpp#L1762

https://github.com/jrouwe/JoltPhysics/blob/b1cd84951022a565a23f33ff77ffee9e71487bbc/Jolt/Physics/Character/CharacterVirtual.cpp#L1149

https://github.com/dimforge/rapier/blob/958fba8ed43328e7e33bb9585a735577fa1061e1/src/control/character_controller.rs#L526

wareya commented 1 year ago

Colliders with ramp-like bottoms have their own problems and aren't a solution either:

1) The maximum step height is limited by your maximum ramp angle and the width of your collider. Most stair stepping heights in games are about half a meter. To replicate that your collision hull would have to be a full meter wide in diameter. 2) You can't control whether stair-stepping only works when you're on the ground or not, so you can jump up larger ledges than you would otherwise be able to. 3) You always start to decline on the edge of any object even if that's not part of how your game is supposed to work (e.g. with arena FPSs and voxel games, you're not supposed to decline on edges, and people will make fun of your game or complain about it feeling "off" if you do). 4) The best collider with a ramp-like-bottom is a cylinder with cones on the top and bottom, which is a rare and unsupported shape. Capsules are inappropriate because they waste potential stair height, making point 1 even worse. So you have to use a polyhedral approximation of a cylinder-with-cone-ends, which is either slow (at large vertex counts) or feels bad to use because the corners are so chunky (at small vertex counts). 5) They "vibrate" when climbing stair cases that aren't exactly the same rise-over-run as the bottom of the collider.

timshannon commented 1 year ago

I've been using the separation ray 3d shape for a while, and it definitely simplifies things, but there are several issues as well that I thought would be worth laying out here.

1) It doesn't seem to respect floor / wall min / max angles. It'll always just push you away from the collision point. So in my case where a separation ray is my "feet", it'll just push the player vertically up any ramp or angled wall, regardless of the max floor angle. You can enable "slide on slope" which prevents this, but that also doesn't seem to respect min / max slope angles, and of course if you don't want to slide on slopes it's not an option.

2) I started with a single ray, and it bounced like crazy. I think it's fighting back and forth with separation vs de-penetration. It stabilizes considerably with more rays, but if you have a collision that connects between the rays, and collides with a traditional collision shape like a cylinder or capsule, then stop abruptly. I'm up to 8 separation rays as my "feet" currently for smooth stepping, and it still stops abruptly at certain angles.

joeymanderley commented 11 months ago

Hello, I spent a month this summer implementing a 3D rigidbody-based character controller for Unity, which I have recently switched away from for reasons you can probably guess. I agree with wareya et al that the only way to do this robustly is with (at least) three shapecasts. Here's the pseudocode for the core movement of my CC:

Assuming the character is on the ground:

Try to do a step up.
    Simulate (with a shapecast) moving up by step height
    Then simulate a slide move forward in the amount the character would've moved on the ground this physics tick
    Then simulate a step down by the step height
    Check that the final position is valid (on the ground, not in a solid, vertical delta > 0, etc.)

If the final position is valid, teleport the character to the simulated end point and exit.

If the final position isn't valid, do a forward slide move, then simulate a step down if the character isn't already moving up 
(just do a shapecast down, then move the character to the shapecast unless it doesn't hit anything)

I found this to be fairly smooth (1) and solid for most kinds of arbitrary geometry (2), but there are still some quirks, such as slope movement being treated the same way as stair movement (3), inconsistent behavior depending on the angle of approach for certain step heights (4), and the usual capsule problems (5). Many of these issues are tied to choice of collision shape + algorithm and are therefore likely to be present in some form regardless of the game engine used (see footnotes).

That said, 90% of my time was spent fixing edge cases, and if there's anything I learned, it's that physics programming is a PITA and full of gotchas, even when you're using multiple full shape casts instead of raycasts. Which is a good argument for why if Godot were to have stair stepping and/or rigidbody support, it should be built-in (or a C++ module) rather than left to scripters. No shade toward GDScript, but it doesn't seem like it's meant for lowish-level programming work (correct me if I'm wrong) but rather there should be a try_move_with_step_up API function that returns a bool or detailed information if the attempted move fails. Or it could work like the now-defunct move_and_slide_with_snap and just be part of move_and_slide, although having a separate function would allow certain obscure use cases (6)

(1) Smooth for capsules! This is because capsules have a round bottom and can therefore "slide up" the lip of a stair step whereas with AABBs, you'd probably have to do some interpolation (2) Including on top of other rigidbodies. While you can design around and add invisible ramps to static geometry, I've found stair stepping to be most useful for allowing the CC to interact well with dynamic geometry (3) Possibly desirable for shallow slopes but bad on steep slopes (4) Unlike with AABBs used by classic shooters (which can also do an arbitrary number of box casts against BSP for almost no cost btw), capsules are round on the bottom, which means they can't "catch" straight stair steps as easily when moving toward them non-orthogonally (5) The step down code and/or gravity will cause the round bottom of the capsule to "grip" edges when partially over them, which actually helps make walking down steps feel smooth but is undesired when over the edge of a tall cliff or something (6) One of the Quake games runs stair stepping code to allow the player to get up over ledges when using a grappling hook

vensauro commented 11 months ago

Edit: the affirmation bellow is false, i have misconfigured my setup here, is just more evidences that the separation ray have more problems than solutions for the "stairs problem"

the bug that @operatios have seen here https://github.com/godotengine/godot-proposals/issues/2751#issuecomment-1649733609 can be solved if the separation ray have the size of the character.

On the next image, the "wrong" way, having the capsule of 1m and the separation ray of 1m for a char of 2m
image

getting stuck on 2 cubes with same xz and different y
image

or in an stair with close ceiling
image

next a character where the separation ray have 2m of height, and the collision shape 1m

https://github.com/godotengine/godot-proposals/assets/41435789/8a130ec4-f096-4611-9567-685e512538a9

yet this solution is not good at all (but can be useful), on the video we can see some bugs, and we have others problems (on slopers angles) like cited by @timshannon.

wareya commented 11 months ago

This doesn't actually solve the bug, it just turns it into a different bug; "head stuck in object" turns into "teleporting around or through things when the other version would get stuck".

vensauro commented 11 months ago

Yeah, i have said this, that is not the ideal, but forget all that i have said, i have misconfigured my things here, and see that this behaviour is not the normal, i was using the Jolt physics and not the GodotPhysics3D. On the default physics system, we are stucked on the things again, even with the separation ray having the height to teleport over.

timshannon commented 11 months ago

More anecdata on the usefulness of adding stair stepping to the engine character body: https://www.reddit.com/r/godot/comments/175au28/coming_from_unity_i_was_very_surprised_what_godot/

image

The flood of developers coming over from Unity will likely expect stair stepping out of the box and separation rays are not a valid solution (at least in their current form).

JheKWall commented 8 months ago

I ended up running into issues implementing stair-stepping as well. Although SeparationRays are recommended, they are very inconsistent and seem to be actually broken when using Jolt Physics (it will randomly pick and choose which objects to separate from). So I hacked together a stair-stepping system that uses a 2-node collision hitbox, 5 raycasts, and body_test_moves with some camera smoothing to round it all out.

https://github.com/godotengine/godot-proposals/assets/97753156/01831a97-0e57-44fb-900d-9663955229ec

(Full vid here: https://youtu.be/FjD-Ndx8mBk)

While its kinda (really) messy, it seems to work pretty well. I tried as many edge-cases as possible including some of the ones discussed above and it seems to hold up. Here's a link to the demo project with all of the code if you want to pick it apart and test it some more: https://github.com/JheKWall/Godot-Stair-Step-Demo Edit: also on the asset library now if you just want to quickly grab it https://godotengine.org/asset-library/asset/2481

I hope we can get a standardized stair-stepping implementation for Godot soon, though. Especially since this topic is very poorly documented due to how most major engines already include it by default, turning it into a really big hurdle for beginner devs starting with Godot.

mrezai commented 7 months ago

I ported my stair stepping implementation to Godot 4: https://github.com/mrezai/GodotStairs This implementation only uses body_test_motion for stair stepping, no raycast and no separation ray used and because of that it is more reliable IMO. Some explanation about this project and its limitations: https://www.reddit.com/r/godot/comments/194vqe2/godotstairs_a_poc_implementation_of_stair/

iiMidknightii commented 7 months ago

I can add to this by saying that I have also experienced some inconsistencies with separation rays as well. I initially tried moving my capsule collision shape up by 0.5m, and putting a 0.5m ray pointing down to handle the floor for me. Like it was stated earlier:

1. It doesn't seem to respect floor / wall min / max angles.  It'll always just push you away from the collision point.  So in my case where a separation ray is my "feet", it'll just push the player vertically up any ramp or angled wall, regardless of the max floor angle.  You can enable "slide on slope" which prevents this, but that also doesn't seem to respect min / max slope angles, and of course if you don't want to slide on slopes it's not an option.

Using this setup, and a CharacterBody3D.floor_max_angle of 45 degrees, I was able to walk up and down 60 degree slopes at full horizontal velocity. Obviously I shouldn't have been able to walk up them at all, and it wasn't even respecting the fact that floor_constant_speed was false. If I selected slide on slope, it would at least prevent me from going up the 60 degree slopes, but then it would also make my character slide down a 30 degree slope.

I also tried keeping the capsule shape on the ground and moving the separation ray to about 0.2 meters along the character's +Z. It also worked in allowing me to climb up stairs, but exhibited the same problem of not respecting slopes. Also, if you move the separation ray too far forward, it has the problem of automatically teleporting you to the top of the stair, no matter how tall it was (it was teleporting me to the top of 20m walls). I suspect it's because, while the ray was only from 0.5m to 0.0m on the y axis, that technically fell inside the steps and it tried to push itself to the top no matter what distance that took to do so.

In response to this, I've had to implement a similar technique as @mrezai where I cast the motion up->toward velocity->down in order to make sure I can climb the steps/obstacles.

I'm not super experienced in physics engines, so take my word with a grain of salt, but I the best option would be to add a property to CharacterBody3D (and maybe 2D) that to signify move_and_slide should handle steps. I think floor_snap_length is a good property to use for the stair stepping height, or maybe a separate one could be added if you want it separated. When that property is true, move_and_slide can automatically do the full tests and move up steps lower than the threshold.

Lamoot commented 7 months ago

I hope we can get a standardized stair-stepping implementation for Godot soon, though. Especially since this topic is very poorly documented due to how most major engines already include it by default, turning it into a really big hurdle for beginner devs starting with Godot.

I'm one of those beginner devs and it's indeed a hurdle setting it up. I've been researching this in the past days and only yesterday thought to check Godot proposals if there's an existing issue. I didn't expect the issue to actually be the best source of information on the topic and I'll be using the system you provided at https://github.com/JheKWall/Godot-Stair-Step-Demo Many thanks for setting this up and sharing the code.

I'm definitely not the only one faced with this problem and having stair handling built into CharacterBody3D would be very welcome. I hope this gets some attention sooner rather than later.

elvisish commented 7 months ago

Version 4.3 and 3.6 of Godot should just add a C++ engine-core port of any one of the excellent stair-step examples in this proposal and be done with it. Unity/Unreal converts would be a lot less confused and long-time users would have lot better efficiency without having GDScript-based solutions pulling performance down.

Lamoot commented 7 months ago

Version 4.3 and 3.6 of Godot should just add a C++ engine-core port of any one of the excellent stair-step examples in this proposal and be done with it. Unity/Unreal converts would be a lot less confused and long-time users would have lot better efficiency without having GDScript-based solutions pulling performance down.

We'll also need a test level however, so the implementation can be evaluated to cover as many situations as possible. A combination of level geometry found in https://github.com/mrezai/GodotStairs and https://github.com/JheKWall/Godot-Stair-Step-Demo would be a good testing playground. Both share quite a few of the test obstacles, but they each have some of their own, valid ones, as well.

I'll try to find time in the next few days to combine them so a potential implementer has a reference to test against. While I'm limited in coding skills, I hope such a contribution will help with solving the issue. I'll share the combined testing level here and of course accept feedback if anyone sees there's something missing from it.