I'm making a small platformer in js and I'm having trouble with the collisions. Unfortunately it seems that 90% of the info online is detecting the collisions, and not what comes after. I can easily detect collisions as everything in my game is an axis aligned 2d rectangle, but handlining those collisions is the hard part.
I've tried moving the player up to the nearest floor when a collision is detected, but it also happens when you collide with a wall. So I tried calculating the closest face and snapping the player there, but it leads to all kinds of weirdness. Here is the code I have so far (the current code is just for floor collisions now, but the same principal can be applied to the rest of the directions)
if (collided) {
let ld = {'a': 'l', 'b': Math.abs(player.left.x - col.left.x)}
let rd = {'a': 'r', 'b': Math.abs(player.right.x - col.right.x)}
let td = {'a': 't', 'b': Math.abs(player.top.y - col.top.y)}
let bd = {'a': 'b', 'b': Math.abs(player.bottom.y - col.bottom.y)}
let dirs = [ld, rd, td, bd]
let nearestFace = dirs.hasMin('b').a
if (nearestFace == 'b') {
player.grounded = true
player.yvel = 0
player.pos.y = col.top.y + player.size.y/2
} else {
player.grounded = false
}
}
Your code seems to not check where the nearest face collides just that it is the nearest face after collision. Which could cause this kind of problem:
If your game is in 2d, everything is a rectangle, and nothing has an angle, then you have a collision problem known as AABB (Axis-Aligned Bounding Boxes) collision, it's a great keyword to search for in your case and an amazing starting point.
First, I advise you to predict collisions instead of dealing with them after they happen because of potential problems like this:
Moreover, if you wish an advanced tutorial on this, the above image comes from this tutorial: Swept AABB Collision Detection and Response. It probably has everything you need.
In short, the input for your collision handling logic should be your objects old position, current size, its velocity, and all other collidable objects (their size and positions). The response should be the expected behavior of your object (i.e. the new velocity, corrected for 'avoiding' collision). That way you can easily test scenarios and your implementation with ease, consider this:
const player = {x: 0, y: 0, width: 10, height: 10, xvel: 12, yvel: 0};
// player.right = {x: player.x + player.width, y: player.y} // Your engine does this, right?
const collidableList = [{x: 15, y: 0, width: 5, height: 10}];
const newVelocity = handleCollision(player, collidableList);
I imagine that the player position is aligned at top left, so in this case you can predict that your new velocity ought to be xvel = 5, yvel = 0
. Which means you just have to create a handleCollision
that works for this test case, and then you could run multiple tests to make sure that the collision is behaving nicely on edge cases such as when nothing collides and when two things collides and one is closer than the other.
The main idea behind this collision is to find the velocity that is closest to zero as necessary to AVOID a collision in the next frame.
For example, imagine a scenario where the player is moving to the right. Let's disregard the Y axis because it should only be used to detect if the objects will collide in the future and will not influence the calculations of the horizontal velocity itself. If the player is moving to the right, it must check the distance between the left side of every object to the right side of the player, if this space is smaller than the velocity, it will obviously cause a collision, like this:
let targetVelocityX = player.xvel;
for (const collidable of ...) {
// ...
if (targetVelocityX > 0) {
// We are moving to the right so:
// Figure out how much space do we have between the objects
const leftOverSpace = collidable.left.x - player.right.x;
// Is our velocity larger than the space we have available?
if (targetVelocityX > leftOverSpace) {
// We must restrict the velocity because the player will collide otherwise
// We can only move as much as we have space available
targetVelocityX = leftOverSpace;
}
} else if (targetVelocityX < 0) {
// Moving to the left...
}
Using the test case I made before, we should get leftOverSpace = 5
, which is smaller than our targetVelocityX = 12
, so the new velocity will be 5, which will make it touch the collidable object, and in the next frame our player position will be x = 5
and xvel = 5
, if we run it again the collision logic will tell us that the left over space is zero so the horizontal velocity will be set to zero which means we cannot move to the right anymore because we are touching the object.
I should remind you that this is different than the tutorial I linked above, the tutorial tries to find the time where the collision happened as a floating point number which is useful if you want to conserve the velocity to deflect a pong ball, or slide the object along, which are some of his examples.