godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.16k stars 97 forks source link

The addition of more separation shapes besides RayShape (and their relation to KinematicBody) #333

Open DenisBelmondo opened 4 years ago

DenisBelmondo commented 4 years ago

September 1, 2024 Edit:

Holy smokes, nearly 5 years have passed since I submitted this proposal! Many of the things outlined below were written before the release of even 3.3 and 4.0 on the dot. Since then, numerous improvements to the physics engine have been made, including less jitter and better slope handling. As for the stair problem, I have since (pretty much less than a year after submitting this) discovered the way Quake handles stair movement:

  1. Move the player upward
  2. Move the player forward
  3. Move the player downward
  4. If the euclidean distance between the position before moving and the position after moving is larger than that of just moving forward normally, then take that move. If not, then "undo" the movement, then move the player forwards normally.

The technique above has been the most robust for moving characters up stairs and various other bumps and lips in geometry. Doing it script-level rather than the engine-level would of course prevent increasing the complexity of the engine for this one use case.

Offhand, there may still be a use for more separation shapes, for instance: user-configurable static depenetration or third person action game cameras, though I'm not sure what the word on the street is-- so to speak-- among developers of these genres of games with regards to how they'd typically solve these problems. I'll leave this proposal up, but I'm personally not quite as compelled these days to advocate for it.


Describe the project you are working on:

I am currently working on a first person 3D action game. The level geometry of which is full of slopes and stairs and all kinds of non-orthogonal bits. The player is required to move relatively fast and often comes across lips and cracks in the floor.

Describe the problem or limitation you are having in your project:

I may have a complete misunderstanding how RayShape works. Correct me if I'm wrong, but to my knowledge, RayShapes' entire purpose is to "separate" a body from another one using a ray.

  • Me, further down this proposal

tl;dr: it's possible to get a CollisionShape + RayShape rig caught on the lip of a ledge so many RayShapes are needed across the bottom of the player scene. However, that gets messy really fast so I propose that there be a separator like RayShape but instead of using a simple ray, we should be able to use primitives (or meshes) that push the player out of the floor just like RayShape does.

The long winded version:

KinematicBody suffices for most sloped walls, however, sloped floors have always been a problem for the node. In 3.1 (including 3.1.2), the node did not seem to implement the RayShape approach as shown here; it only worked this way in 2D, which means that the player always slowly slid down slopes even when stop_on_slope was true,1 as well as walking much slower when traversing them.2 As of recent, in the 3.2 branch, KinematicBody(3D) was given the ability to use the RayShape method to solve both of these problems. In other words, picture a lollipop. The lollipop's stick is a RayShape, and the lollipop's head is a BoxShape/SphereShape/CapsuleShape, etc. This exact scene is how I implemented the RayShape technique in the link above in 3D. In 3.2, it correctly stops the player from sliding down slopes, as well as making it so that they do not have a harder time walking up them. Also, as some kind of very welcomed side effect, they traverse most staircases seamlessly, which is a huge plus in my opinion as literally 0 code needed to be written for this behavior.

However, there are a couple of issues when using this exact approach in 3D.

The ledge problem:

The first one being that this player is shaped like a lollipop. In 3D (and possibly 2D), it's entirely possible to get the head of the lollipop to hang off the edge of a platform, while its stick is dangling. Here is an illustration:

image (I can verify that this exact thing can occur in 3D.)

While it may be less likely for this to occur during normal play, it could still happen, possibly causing confusion to the player and visual oddities. Not only that, but when the StaticBody happens to have a slope like in the following illustration, the RayShape does not stop it from sliding since its not colliding along with the head in order to inform KinematicBody of the information it needs in order to stop slope sliding. In other words, RayShape cannot separate KinematicBody from the ground since there is no ground beneath RayShape, but there is ground beneath the head. This is definitely desired behavior, but that will be touched on a little further down this proposal.

image

And if we were to move them a little bit to the right...

image

The RayShape is now able to properly do its job by separating the body from the floor3.

One could simply mitigate this issue by changing the shape of the head to be a sphere or a capsule like so:

image

This causes the head to still catch onto the ledge, but the sliding off is much more gradual. However, there is still a catch.

Addressing the ledge problem:

I went with the most obvious solution. I gave the head of this ex-lollipop two sticks on either side of the head. The allegory no longer applies, so I'll call them legs. Again, the same technique was done in 3D (one RayShape for each corner of a BoxShape) This resulted in a setup like so:

image

This worked for a while, though sometimes, a player may encounter a situation like this:

In 2D:

image

In 3D:

image

Needless to say, this particular collision rig seems to "get confused" at this point. When the player tries to move forwards and backwards when on the peak of this pyramid or the furthest corner a rectangular prism rotated at 30-45 degrees, jutting out of the ground, they seem to have trouble moving out of it for some reason. It's possible to unstick themselves, but there is still a very apparent catch and struggle to get out of a position like this.

Thankfully, the problem in this very particular instance can be solved by adding one more RayShape:

image

While extremely unlikely at this point, it would still be hypothetically possible to encounter a situation like so:

image

At this point, it would be a much more sane choice to not use a BoxShape. I suppose capsules are the most popular player controller shapes for a reason.

image

Much better. In the extremely unlikely event that the point of collision is exactly between any of these rays at any time, the smooth bottom of the capsule collider will simply let the player glide off of it, almost seamlessly un-sticking itself.

However

I can't help but to feel like there must be a more elegant way of handling "separation" the way this particular RayShape approach does. When does the density start and stop? How many is "the right amount" to put on? It must differ for every use case, but I've found the most paranoid, airtight amount is about 9.

image

(That's one for every corner, one in between every corner, and one dead center)

If only there could be a RayShape on the player's bottom everywhere. All the time.

I may have a complete misunderstanding how RayShape works. Correct me if I'm wrong, but to my knowledge, RayShapes' (an apparently more accurate name for which would be "SeparationRayShape" 4) entire purpose is to "separate" a body from another one using a ray. In most cases, that's probably the player or some other KinematicBody entity like an enemy.

I am not 100% clear on how the implementation for RayShape works (will discuss under the proper header), but if all the separation ray does is check to see if the ray is colliding with something and then snapping the body out of the ground based on the information given by the ray, then hopefully it wouldn't be too hard to just do the very same but with a shape query.

Describe how this feature / enhancement will help you overcome this problem or limitation :

Using a primitive instead of multiple rays, I believe, would provide a number of benefits. I'll knock the small fry off the list first AKA the more philosophical, convenience-oriented things.

More concrete things:

Show a mock up screenshots/video or a flow diagram explaining how your proposal will work:

If only there could be a RayShape on the player's bottom everywhere. All the time.

Enter: SeparationBoxShape

Forgive the crude drawings again, but here is what I have in mind:

image

Hopefully, as you can see, the underlying SeparationBoxShape is properly separating the BoxShape from the StaticBody. Because the entire surface area of the BoxShape's bottom is covered, there should (hypothetically) be no fear of the player catching on the ledge. For instance: the following should never happen the same way it would happen if you were using an ordinary RayShape lollipop style:

image

Since SeparationBoxShape detects a collision, it will properly snap the player out of the platform.

image

Additionally, as mentioned above with regards to stairs, the player should be able to traverse the following:

image

With a plain RayShape setup, the player would have to advance forward a little more before they notice actually ascend the step:

image

This is, of course, the very thing that causes "ledge catching" detailed in the diagrams way above.

Additionally, SeparationBoxShape is simply an example. I assume that if boxes can be used for separation shapes, then so can capsules, spheres, cones, etc. Perhaps even meshes?

Editor-side:

image

Perhaps the icons should be red to differentiate them? :) In any case, selecting a separation shape should be fairly straightforward. You would then place it in your scene normally as if it were any other CollisionShape.

Describe implementation detail for your proposal (in code), if possible:

Here's the interesting part. To reiterate, I am not very familiar with the exact inner-workings of RayShape, but I'm going to proceed under the assumption that it works by just pushing the player out of a body when it's colliding with one.

To reference this blog post once more, the only benefit described in the article is to prevent the player from moving more slowly up slopes which RayShape does flawlessly. The way that it achieves this is through a very interesting technique. I'll copy the figure here for convenience's sake:

image Image (presumably) made by Juan Linietsky

It is apparent from the image alone that the internal RayShape approach can be summarized by saying that if the RayShape is touching the floor but the capsule is not, then the slope is not too steep and the player stops on the slope completely without sliding even a little bit. However, if the capsule is touching the slope and the RayShape isn't, then we know it's too steep.

If you recall from the last bullet point in the previous section:

Using the multiple RayShape approach completely breaks the way move_and_slide() detects what slopes are too steep or not since there is always a RayShape that is preventing the non-RayShape CollisionShape from touching the slope itself.

If you can excuse my crude drawings again, I hypothesize it's because of this:

image

This makes it entirely possible for a player to climb a slope that is hilariously steep. A solution for this, I've found, is to use as many RayCast nodes as there are RayShape nodes that cast from and to the exact same places as the RayShapes and grab which one of these RayCast nodes detects the largest angle, and flip all the RayShapes' "slide on slopes" property to true if the angle derived from the normal is above a certain threshold. This works pretty nicely if it weren't for two issues:

With all of that said, I think that the way RayShape collision is implemented right now is manageable for most 2D applications, especially in combination with my RayShape + RayCast approach, but 3D might need another method for determining what is too steep and what isn't.

My suggestion is the most obvious one: if Separation Shapes are not that much different from other CollisionShapes, then hopefully the talented crew that are infinitely more intimate with the physics engine than me will probably know whether or not the following is a valid method:

image

There should be an additional (exposed) property for SeparationBoxShape: floor_max_angle. If the normal at the point of collision is above floor_max_angle, then that's when the player should start sliding (slides_on_slopes is internally overriden to true?). In more technical speak, at this point, the SeparationBoxShape should return the correct normal instead of some kind of a floor_normal constant which what I hypothesize stops it from sliding. My hypothesis comes from information I gleaned from studying the internal move_and_slide() and playing around with it, and also this tooltip from RayShape in the editor:

image

To address what this will actually look like in move_and_slide(), take a look at (currently, at the time of writing) line 1185 in physics_body.cpp. In the else block, separate_raycast_shapes() is called, which, after a few glances, seems to tell the physics server to test a ray separation to find the deepest collision point and update's KinematicBody's global_transform.origin with a "recovery" vector, which I assume in the case of a floor, would be (0.0, <amount needed to snap the RayShape out of the ground, AKA, the "recovery amount">, 0.0).

If separate_raycast_shapes() could be further generalized to separate_separation_shapes() or something along those lines, hopefully that would suffice. separate_separation_shapes() could be a similar check except instead of using PhysicsServer::body_test_ray_separation(), it could use PhysicsServer::body_test_motion() or something that returns more sufficient information, like a shape intersection query.

If this enhancement will not be used often, can it be worked around with a few lines of script? Is there a reason why this should be core and not an add-on in the asset library?:

I am very certain these features belong in the main engine for the reasons I described above. I assume they will be used very often and that not enough of the physics engine is exposed to GDScript make SeparationBoxShape in any elegant fashion.

Conclusion:

To reiterate once more, I am not 100% on how possible or what's possible and how RayShape works. If there are no plans to add more separation shapes, would it be possible to have RayShape expose the normal of whatever it's hitting? Simultaneously, would it be possible to check for collisions on a player's non-RayShape CollisionShape first to prevent the occasional "climb up the wall for 0.1 seconds" bug? May not be that simple to fix, but I'm interested to hear what might be happening under the trunk.

There are still a couple known bugs regarding RayShape. Namely #34098 and some jittering.

Thanks for reading, this took me quite a while to assemble. I hope it didn't come off as too presumptuous. I am not an expert by any means, but I look forward to hearing from one.

Happy holidays!


  1. This, according to my observations, is caused by consistently applying downward force in move_and_slide, like gravity. In move_and_slide_with_snap, the slowly sliding down slopes is intrinsic to the function because it is technically always applying downward force in order to snap the player back down to the ground.

  2. You can circumvent this by not applying any downward force by checking if the player is on the floor like so: if !is_on_floor() && vel.y > -max_fall_accel: vel.y -= fall_accel. However, it is consistently recommended to apply some downward force to prevent the erratic flipping between true and false when checking for is_on_floor() every frame, making this technique not ideal if you need to check if the player is on the floor every frame. You could use move_and_collide() or test_move() to see if there is solid ground beneath the player, but that results in the use of an additional physics query every frame. move_and_slide_with_snap() already has two, internally.

  3. As many reading this would have assumed, this is exactly how stair collisions are automagically handled with KinematicBody + RayShape setups.

  4. "Yes, the name is confusing, maybe they should be renamed to SeparationRayShape in Godot 4.0 :)" - Juan Linietsky

KoBeWi commented 4 years ago

I haven't read the whole text, but from a quick glance it looks like it addresses the same problem I have in my game. I find move_and_slide() really hard to control, so I wrote my own movement logic with move_and_collide(), but it has lots of problems and still needs to be improved.

Anything that makes desired platform movement easier to achieve is welcome.

BanchouBoo commented 4 years ago

You should be able to do something like this pretty easily with a test_only move_and_collide call, you do a downward sweep and use the travel vector to offset the player upwards. It's not a perfect solution, it won't update if you're on the floor internally for example so you'll have to track that yourself.

Skaruts commented 4 years ago

I would rather vote for moving away from using any separation shapes at all. While a primitive separation shape would solve some of the problems introduced by the RayShape, there are problems that may still remain. One that I can see immediately is that in the very same way that using 9 RayShapes prevents the head shape from detecting slope steepness, so does any primitive shape:

slope steepness_problem

I feel like we're neglecting occam's razor here, and we're hacking around a more complex solution with more fail cases, which is not as intuitive to developers and is inconsistent with the methods used for other behaviors.

Personally, I'm quite happy about the way move_and_slide works out of the box, except I require constant speed and no sliding down (both at least until the max slope steepness is reached). The way I see it, there ought to be something like a move_and_slide_constant, which developers could simply use instead of move_and_slide, with the same collision setup. In that way there would be two functions: one for a more realistic approach to gameplay, and one for more arcadey approach gameplay.

I realize there can't be a magical function for every use case, but it seems to me that everyone's interested in achieving at least those two general gameplay concepts, so maybe it might be worth supporting both out of the box in the same clean and simple way.

BanchouBoo commented 4 years ago

@Skaruts I have a proposal open at #81 that would allow easy implementation of things such as maintaining a constant speed going up a slope. However, there are other use cases for having separation shapes beyond just maintaining constant speed (which I agree should not be something they're needed for), such as handling bumpy terrain better as well as handling stair steps.

Skaruts commented 4 years ago

@BanchouBoo yea I was just now going to add that using a shape can be useful for other things. In my use case, in a FPS controller, I use 5 RayCasts to detect ceilings and prevent uncrouching when under something, but it suffers a few similar problems as the OPs 9 rays, and it might be more reliably done with single box or capsule shape, if I could make it act more like a raycast would. (Maybe my use case can still be done with an area, but I haven't been able to so far, as it can't seem to collide with convex collision shapes imported from blender.)

Thanks for your efforts, btw.

YukoAioi commented 2 months ago

This has to be added in Godot 4.4!!!