Background
The sweeping method for detecting and resolving collision has proven to be very versatile to implement collision for our game, and I have this GameDev.net article to thank for that: https://www.gamedev.net/tutorials/programming/general-and-gameplay-programming/swept-aabb-collision-detection-and-response-r3084/
The article was very helpful and I learned a lot from it. It did have some bugs in its example code (thankfully we don't have them ourselves), but the concept was the important part. I suggest you read it since it's a very important reference for this post.
The Conflict
The problem I am presented is an unwanted, yet expected behavior. Basically the player is unable to fall into tile-sized gaps if the player is also tile-sized (unless there is something at the end of the hole to block the player).
While the code works in accordance to the GameDev article (I have a demo further down this post), there is a subtle issue with the method of "sliding".
Sliding is a method of collision resolution so that the entity does not stick to the surface of the other physics bodies. It basically allows the player to walk on the ground and slide along walls when jumping on them without being stuck on the surface. This image from the article describes it better than I do:
This is an ideal solution, but restricts the tile-sized player from falling into tile-sized holes and instead allows the player walk over it.
As you can see, you would expect the player to fall into those holes, but Lo and behold he walks right past them! I have tested with various walking velocities, even lower than one pixel per frame and the player still walks past them.
In addition, I'd like to clarify that this is not a problem with just one axis, since the player can't jump into tile-sized holes either.
Note that the player is still capable of falling into those holes, there just needs to be a blockade to allow it.
...and the same goes for walking into one.
The Root of The Problem
I suggest you read some of the code in the demo I have provided, since the following code snipped is relevant for this section:
resolveCollisionOrMove(deltaTime, movement = this.vel, remainingTime = 1, collisionStack = this.collisionStack) {
// Sort collisions in the correct order: First by distance, then by
// time of impact to correct the order a bit.
collisionStack.sort((a, b) => a.distance - b.distance);
collisionStack.sort((a, b) => a.collisionTime - b.collisionTime);
let hit = collisionStack.shift();
// If a collision is detected at all, resolve for the closest AABB. Otherwise add velocity to position.
if (hit) {
this.center.x += movement.x * hit.collisionTime * deltaTime;
this.center.y += movement.y * hit.collisionTime * deltaTime;
// Calculate the correct time of impact for the remaining
// collisions or to apply movement
let time = remainingTime - hit.collisionTime;
// Calculate the collision normal (vector used to slide the object that collided)
let dotProduct = p5.Vector.dot(movement, hit.normal) * time;
hit.normal.mult(dotProduct);
// Handle events after each respective side that collided
this.onCollision(hit.side);
if (time > 0) {
// If the time of impact happened to be above zero, then recalculate
// collision for all other potential collisions with respect to the normal vector
let stack = [];
for (let sweep of collisionStack) {
// Pass the normal vector as the movement vector for the next possible collision
if (this.createMovementBoundary(deltaTime, hit.normal).overlaps(sweep.aabb)) {
let newSweep = this.calculateSweepData(sweep.aabb, hit.normal, deltaTime);
stack.push(newSweep);
}
}
// Keep resolving collisions for the other potential collisions
this.resolveCollisionOrMove(deltaTime, hit.normal, time, stack);
}
} else {
this.center.add(p5.Vector.mult(movement, deltaTime));
}
}
I presume this is a problem caused by the collision normal calculated for the sliding effect. Assume the player walks towards a tile-sized hole from the left (the hole is now to the right of the player). My collision resolution algorithm takes into account other potential collisions based on the sliding vector applied to the player upon the closest detected collision. This means the tile before the hole is what collides with the player first, which in turn applies a movement vector (the slide) with a y-component of zero. This makes the collision algorithm ignore the tile to the left of the hole as a potential collision since according to the logic, the player does not collide with its left side.
The green line is the normal vector used for sliding (which is used in the next collision calculation) while the red one is the actual velocity applied to the player before resolving collision...
The issue is I don't know how to solve this problem without breaking the code. I know this problem can be solved by simply reducing the player size. That is not something I'm willing to accept as a solution since it limits many gameplay mechanics which I won't get into here.
The Code
Here is the link to a JSFiddle I made where all the magic happens: https://jsfiddle.net/uwg0m9zp/
I've written it in JavaScript in a single HTML file where I use the p5.js library for simplifying input, graphics and vector calculation. Most of the p5.js function calls are not important for this problem, except for the vector mathematics, but if the structure of the code confuses you just look up the documentation (or ask in the comments): https://p5js.org/
Demo instructions
This demo features a blue rectangle who is our player. He can walk, fly and have collision with the purple tiles in the map.
The controls are:
- A: Walk Left
- D: Walk Right
- Space: Fly
Use these controls to walk/fly into the tile gaps
Final words
I hope I explained my problem well. Please do not hesitate to ask questions if I came across a bit confusing. Note that I have also tested to see if this is a float imprecision problem (using epsilon, etc.) and had no luck.
Thanks for reading this and for any help in advance!