MyKneecaps

Who is MyKneecaps?

Andrew. Game Designer @ Fowl Machinations. Job hunting & working on a stupid boat game.


torcado
@torcado

by far the longest amount of time spent to far has been on the movement physics and collision physics.
i'm very happy with the movement physics, and actually this part didn't take long at all. all of the nice systems regarding moving platforms, momentum, etc. was stuff i already figured out for the previous project and stole.
the vast majority of the work came from switching engines. i'm trying out godot for this project, and i spent a very long time tweaking its physics system to get it to work how i'd expect (going so far as to modify the inner physics system in the engine code). it was a pain, but i think it's at a nice spot now :)

jumping high off a moving platform

i really see movement playing a big role in the game, so i want it to be as clean and versatile as possible. you can see this especially in the first gif, as i use a platform and a box to gain a bunch of speed >:)

more details about my work here below:


hoh boy. where do i begin.

i'm going to get into godot-specific behavior, in the hopes that it helps anyone else out there also using godot and running into the same problems.

physics system (in godot)

here's all im looking for in a physics system: i want rigid boxes, unmovable moving platforms, and a controllable character to all interact without any squishing or jitter or collision rebounding. i want a stack of dozens of boxes that i can push around without any overlaps. i want to be able to stand on an arbitrary number of objects stacked on a moving platform and have none of them sway, perfectly in-line. this is the idealistic 2d physics system, in my eyes. godot fails at most of these out-of-the-box.

in godot there are several physics based nodes for different situations.
one of these physics based nodes is called RigidBody2D. this one's used for more natural physics responses, like free-rotating boxes and friction and bounciness and blah blah. it uses an internal physics system for processing and handling collisions, and doesn't act kindly to setting position/velocity manually or else you risk conflicting with the inner system.

the physics node built specifically for character movement, e.g. in a 2d platformer, is called CharacterBody2D, and it has a really lovely move_and_slide method which has a ton of internal code handling all sorts of edge cases so that you can move around and stick to slopes and not bounce on a flat ground when running into seams and blah blah blah.
well, this node would be really lovely if it weren't for the fact that it doesnt interact well with RigidBody2Ds! here's just one of the many many possible scenarios:

CharacterBody2D pushing RigidBody2D boxes into walls

there are all sorts of things you can try to improve this, but they all fall short in one way or another, trust me i've tried hundreds. you could change the collision layers and masks in all sorts of permutations so one object can push but can't be pushed by another. you could manually detect collisions and impart forces through code. you could add several different collision shapes for each object in every permutation. you could turn one object into a group of multiple physics nodes with different collision layers connected with a pin joint so that one side can detect collisions and the other can react.

really, the only sensible solution is to just make every physics object a RigidBody2D (or alternative), so that everything runs off the same internal system.
RigidBody2D has an _integrate_forces callback in which you can manipulate the body's physics yourself, including its velocity. this is how i'm handling the player's movement control. in here i have a bunch of custom code for riding platforms and handling slopes and conserving momentum, basically reimplementing a lot of the features of CharacterBody2D.move_and_slide and adding my own features on top.

annoying godot physics issue

making everything RigidBody2Ds with custom physics handling solves most of the collision response issues, but has one major problem that makes character movement especially difficult: collision contacts are reported at least 2 frames late. this is essentially unresolvable. even the simplest possible player action, jumping off the ground, becomes a difficult annoying pile of workarounds.

if i have it so you stop moving vertically when on the ground, and you press jump, the game will still think you're colliding with the ground on the next frame and cancel the jump velocity. getting around this specific jumping issue can be done but the core problem still remains.

the best workaround i could come up with is to manually query the physics server with PhysicsDirectSpaceState2D.intersect_shape (which runs immediately) to verify that the detected collision is actually overlapping or not. however, this only helps with exiting collisions. i can use this to detect entering collisions early, but it won't give me any info like the normals or collision points or velocity.

the journey of trying to get RigidBody collision contact data without any delay was long and frustrating. i had no experience with the internal structure of the engine, so i was going in blind. i parsed and researched as much as i could, and spent like 2 weeks trying everything i could think of to resolve this. i eventually made a call for help on twitter/mastodon, but i got no help (i had some nice replies on mastodon but understandably my issue was far too out of scope for most normal users). you can read my insane ramblings about this here if you want.
anyway, i fumbled my way into a solution. i have no idea if it's a "correct" solution, it may be non-optimal (and it causes a crash if you turn on a specific setting) but it fucking works so whatever. i submitted an issue+PR for this, mostly with the hopes that it would reach anybody with the knowledge to help or answer any of my questions, but ive gotten no responses on any issue or PR ive made to the godot repository so whatever. (the tool is good, i don't blame anyone for this of course, but i can't help but feel frustrated. Godot, you're making it very hard for me to want to improve your software)
for anyone that wants to use my solution, the PR is here, you can fork godot and merge this pr in and you'll have it.

the basic idea is that it runs a second pass through the collision detection after moving the bodies so that the contact data is updated before the next call to the user code.
now i get updated collision contacts after moving my physics bodies :)

custom movement system

this sets me up to have full control over the movement physics of objects, for the character especially but also any other miscellaneous objects like boxes.
here's a stress test of way too many boxes:

bany boxes stress test

the movement system requires a lot of tweaking on its own, but that's largely unavoidable.

such as, maintaining momentum after running off a moving platform:
(incorrect behavior on the left, fixed on the right)

running off moving platform, incorrect
running off moving platform, correct

and also handling tons of edge cases regarding momentum buildup:

pushing against object to gain speed

a lot of these kinds of "movement exploits" will be explicitly kept in to some degree for people who want to do funny things, including myself. such as making it harder to build momentum but still possible: tweaked momentum buildup exploit

i could show a hundred more gifs of tiny edge cases i've tweaked. like running on an angled box that's flying through the air such that the player doesnt reject off the box when walking from one side of a corner to the other.
idk why i'm like this, but the results make me happy so i guess that's reason enough.

collision seams

another issue that plagues games in any engine is collision seams. e.g. in 2D games specifically, seams between tiles will cause "phantom" collisions with whatever is running on top of it, like completely stopping on flat ground as if you hit a wall. a lot of games get around this issue by making the collision shape a capsule or circle, but even with that change it can still noticeably bounce off the seams.
the simple solution in my case involves converting the tile collisions into one big collision shape. i wrote an editor plugin that does this for me (based off of existing code by afk-mario). this almost entirely solves the issue (especially if you use a rounded collider).

baked tilemap collisions

you can get the plugin here!

this adds a "bake tilemap" button to the toolbar in the scene editor. optionally, you can add tilemaps to the "bake" group to have them automatially baked when saving the scene

final thoughts

there are tons of other edge cases with all of this stuff, of course. but the current system i have sets a great baseline to let me customize and fix things, so i'm happy for now :)
i cant wait to add more stuff to play around with the movement!

thanks for reading :)


You must log in to comment.

in reply to @torcado's post:

I'd love to know more about how you essentially rebuilt the handy features of the CharacterBody2D node if you'd be willing to share! I'm prototyping a platformer where the player can pick up objects, and when I started reading this I thought "oh neat, but I probably don't need to do all that." Until I saw the gif of the boxes being pushed into each other and the floor, which is an exact issue I have run into... 😬

yeah it's a bit unavoidable. there are a bunch of solutions that are good enough for most cases, with regards to RigidBodies and CharacterBodies interacting. i'd recommend just playing with the collision layer/mask bits for both objects and seeing if any permutation is ok, to begin with. but for reimplementing the move_and_slide features, i'm just manually handling physics adjustments inside of _integrate_forces. for example, you can do something like this to make horizontal movement consistent when walking on slopes:

if is_on_floor:
	# rotate x component by floor angle (difference between floor normal and UP)
	#  then add resulting y component to current y velocity
	#  (this results in an increase in energy, but maintains x-speed on slopes)
	var xVel = Vector2(movementVel.x, 0.0)
	slopeAdjustVel = Vector2(0.0, xVel.rotated(Vector2.UP.angle_to(floorNormal)).y)
	movementVel.y += slopeAdjustVel.y