Making Space Gal Move - Part 2: The Struggle
So yeah, I said that I was going to make a post about the player controls. Well, I was going to, because I had foolishly assumed that my basic physics implementation was complete.
I was wrong.
Let's start with an explanation of how I handled collision detection, since I glossed over it in the last dev log. You might naively assume that it's as simple as checking: "am I colliding anything? Yes or no", but that's only sufficient for objects that aren't in motion. If you only check if you are, right now, colliding with something, then you bump into a major issue, one that, to my knowledge, does not have a formal name, but let's call it the Frame Barrier. The frame barrier is the basic principle that a frame is the smallest amount of time possible to measure in a game.
The implication of the frame barrier for collision detection is that, with a simple check on every frame for collision, interpenetration cannot be prevented. And this isn't just a simple rookie error, many professionally released video games don't account for this universally, and neither does Unity's physics engine. You can see this in action by aiming a physics object with a very high velocity at another object with the same dimensions. Depending on the ratio of Object 1's velocity to its distance to Object 2, Object 1 may appear to phase directly through Object 2.
What's happening is that on one frame, Object 1 is on the near side of Object 2, and on the next frame, it's on the far side, with no frame during which the objects actually intersect. This is because objects in game engines are not truly moving in any physical sense, but simply changing their coordinates within a cartesian matrix. In layman's terms, the object's movement isn't actually like moving along a line, but teleporting between a series of points, but those points are so close and the steps happen so fast that your brain perceives it as smooth motion. And the problem is that collision detection isn't fooled by this illusion, it only cares about the state of the program at every individual moment, or in our lingo, frame.
So what the hell do we do?
The way I've been talking, it might seem like this is impossible to overcome. I called it the frame "barrier" after all. But we're clever programmers, and clever programmers always find a solution. The trick lies in the fact that movement and collision code are intertwined, meaning that when we want to solve one, we have the information for the other, and vice versa.
Here's what we do: we can't just ask the collision detection "am I colliding with anything?", we must ask "given my velocity, is there anything that I would collide with during the movement that will happen during this frame?"
Now, that makes it sound really simple, but of course any statement with that many qualifiers is going to have a myriad ways to implement it in code, and this is where we arrive at
THE STRUGGLE PART 1: THINGS GO WELL
My first notion of how to implement... let's call it "preemptive collision checking", went unexpectedly smoothly. I mentioned in the last dev log that I had decided early on to have a one pixel = one unit scale, and that made it possible to deal with collision detection in terms of full integers. And because I wanted sprites to always line up with the "grid", and I wanted collision detection to be based on the sprite's position for the sake of the player not being misled about the state of collisions, that meant there was no issue at all using a for() loop to check outwards from the collider, up to the value of the object's velocity in that dimension.
(An aside: yes, I'm aware that a for() loop is fully able to increment in non-integer values, but the important thing is having a predictable "grid" that collision boxes should always be a complete multiple of. It just makes things more human-readable if the base unit of that grid is 1.)
In plain English: here's how the object's movement works, as a to-do list:
- Wait for other objects to make modifications to our velocity. (This doesn't exist in code, it just represents the fact that this code always executes last on every frame)
- Am I colliding with anything right now? If so, we can't move in that direction, so let's modify the velocity right now so we don't even try.
- Knowing what my velocity is, let's check in that direction for things I might collide into. If we find something, save that value for later.
- Update my current position based on my velocity, or based on any value we saved from Step 3.
And the problem was in how I implemented Step 3.
THE STRUGGLE PART 2: SHIT GOES SOUTH
The problem wasn't even that I did the collision checking with a for() loop instead of something more normal, like a raycast. The problem, at least seemed to be, that I was doing preemptive checking along the x-axis, and along the y-axis, but not both together. The following diagram will explain. The orange box is the collision box, the green areas are being checked for collisions, and the red area is not.
This resulted in Space Gal potentially becoming stuck inside objects if she approaches at a hard diagonal. Unfortunately, in a 2D platformer, just barely trying to land on the edge of a platform is something that needs to work perfectly 100% of the time.
At first, the fix seemed simple. I would just nest one of the axis checks inside the other, thereby basically checking the intersecting area. The problem with how I did this should be pretty apparent from the following line of code:
For those not familiar with C#, this code declares a loop that will execute code for as long as the value of x is less than the horizontal velocity of the object. And a similar one exists for checking the y axis. Meaning that, if either horizontal or vertical velocity is zero, then the loop says "oh hey, looks like we're already done here!" and no checking happens at all. Meaning that the checking now looks like this:
At this point my brain was beginning to implode. There probably was some way to make an implementation like this work, but I was done fiddling with numbers. It was time to try a raytrace like a big-boy developer.
THE STRUGGLE PART 3: A FALSE HOPE
Raytracing didn't solve the problem. So I tried doing both the original solution, and raytracing.
THE STRUGGLE PART 4: I'M GONNA SHIT MY PANTS
So at this point, this devlog has been sitting as a draft for two days, because I started writing it thinking that I was on the cusp of having it all figured out. I've ripped out almost all of my original collision detection code, and now I'm trying to build a holistic solution from scratch. I am about ready to pull my hair out. Seriously considering just using the fucking built-in Unity physics.
Leave a comment
Log in with itch.io to leave a comment.