diff --git a/src/collision.cpp b/src/collision.cpp index 7dae43a0c..5539593bd 100644 --- a/src/collision.cpp +++ b/src/collision.cpp @@ -72,6 +72,14 @@ inline v3f truncate(const v3f vec, const f32 factor) ); } +inline v3f rangelimv(const v3f vec, const f32 low, const f32 high) +{ + return v3f( + rangelim(vec.X, low, high), + rangelim(vec.Y, low, high), + rangelim(vec.Z, low, high) + ); +} } // Helper function: @@ -101,6 +109,8 @@ CollisionAxis axisAlignedCollision( if (speed.Y) { distance = relbox.MaxEdge.Y - relbox.MinEdge.Y; + // FIXME: The dtime calculation is inaccurate without acceleration information. + // Exact formula: `dtime = (-vel ± sqrt(vel² + 2 * acc * distance)) / acc` *dtime = distance / std::abs(speed.Y); time = std::max(*dtime, 0.0f); @@ -335,6 +345,10 @@ collisionMoveResult collisionMoveSimple(Environment *env, IGameDef *gamedef, collisionMoveResult result; + // Assume no collisions when no velocity and no acceleration + if (*speed_f == v3f() && accel_f == v3f()) + return result; + /* Calculate new velocity */ @@ -350,30 +364,19 @@ collisionMoveResult collisionMoveSimple(Environment *env, IGameDef *gamedef, time_notification_done = false; } - v3f dpos_f = (*speed_f + accel_f * 0.5f * dtime) * dtime; - v3f newpos_f = *pos_f + dpos_f; - *speed_f += accel_f * dtime; - - // If the object is static, there are no collisions - if (dpos_f == v3f()) - return result; - + // Average speed + v3f aspeed_f = *speed_f + accel_f * 0.5f * dtime; // Limit speed for avoiding hangs - speed_f->Y = rangelim(speed_f->Y, -5000, 5000); - speed_f->X = rangelim(speed_f->X, -5000, 5000); - speed_f->Z = rangelim(speed_f->Z, -5000, 5000); + aspeed_f = truncate(rangelimv(aspeed_f, -5000.0f, 5000.0f), 10000.0f); - *speed_f = truncate(*speed_f, 10000.0f); - - /* - Collect node boxes in movement range - */ + // Collect node boxes in movement range // cached allocation thread_local std::vector cinfo; cinfo.clear(); - { + // Movement if no collisions + v3f newpos_f = *pos_f + aspeed_f * dtime; v3f minpos_f( MYMIN(pos_f->X, newpos_f.X), MYMIN(pos_f->Y, newpos_f.Y) + 0.01f * BS, // bias rounding, player often at +/-n.5 @@ -399,24 +402,14 @@ collisionMoveResult collisionMoveSimple(Environment *env, IGameDef *gamedef, } } - /* - Collect object boxes in movement range - */ + // Collect object boxes in movement range if (collide_with_objects) { - add_object_boxes(env, box_0, dtime, *pos_f, *speed_f, self, cinfo); + add_object_boxes(env, box_0, dtime, *pos_f, aspeed_f, self, cinfo); } - /* - Collision detection - */ - + // Collision detection f32 d = 0.0f; - - int loopcount = 0; - - while(dtime > BS * 1e-10f) { - // Avoid infinite loop - loopcount++; + for (int loopcount = 0;; loopcount++) { if (loopcount >= 100) { warningstream << "collisionMoveSimple: Loop count exceeded, aborting to avoid infinite loop" << std::endl; g_collision_problems_encountered = true; @@ -431,9 +424,7 @@ collisionMoveResult collisionMoveSimple(Environment *env, IGameDef *gamedef, f32 nearest_dtime = dtime; int nearest_boxindex = -1; - /* - Go through every nodebox, find nearest collision - */ + // Go through every nodebox, find nearest collision for (u32 boxindex = 0; boxindex < cinfo.size(); boxindex++) { const NearbyCollisionInfo &box_info = cinfo[boxindex]; // Ignore if already stepped up this nodebox. @@ -443,8 +434,7 @@ collisionMoveResult collisionMoveSimple(Environment *env, IGameDef *gamedef, // Find nearest collision of the two boxes (raytracing-like) f32 dtime_tmp = nearest_dtime; CollisionAxis collided = axisAlignedCollision(box_info.box, - movingbox, *speed_f, &dtime_tmp); - + movingbox, aspeed_f, &dtime_tmp); if (collided == -1 || dtime_tmp >= nearest_dtime) continue; @@ -455,95 +445,119 @@ collisionMoveResult collisionMoveSimple(Environment *env, IGameDef *gamedef, if (nearest_collided == COLLISION_AXIS_NONE) { // No collision with any collision box. - *pos_f += truncate(*speed_f * dtime, 100.0f); - dtime = 0; // Set to 0 to avoid "infinite" loop due to small FP numbers - } else { - // Otherwise, a collision occurred. - NearbyCollisionInfo &nearest_info = cinfo[nearest_boxindex]; - const aabb3f& cbox = nearest_info.box; + *pos_f += aspeed_f * dtime; + // Final speed: + *speed_f += accel_f * dtime; + // Limit speed for avoiding hangs + *speed_f = truncate(rangelimv(*speed_f, -5000.0f, 5000.0f), 10000.0f); + break; + } + // Otherwise, a collision occurred. + NearbyCollisionInfo &nearest_info = cinfo[nearest_boxindex]; + const aabb3f& cbox = nearest_info.box; - //movingbox except moved to the horizontal position it would be after step up + //movingbox except moved to the horizontal position it would be after step up + bool step_up = false; + if (nearest_collided != COLLISION_AXIS_Y) { aabb3f stepbox = movingbox; - stepbox.MinEdge.X += speed_f->X * dtime; - stepbox.MinEdge.Z += speed_f->Z * dtime; - stepbox.MaxEdge.X += speed_f->X * dtime; - stepbox.MaxEdge.Z += speed_f->Z * dtime; + // Look slightly ahead for checking the height when stepping + // to ensure we also check above the node we collided with + // otherwise, might allow glitches such as a stack of stairs + float extra_dtime = nearest_dtime + 0.1f * fabsf(dtime - nearest_dtime); + stepbox.MinEdge.X += aspeed_f.X * extra_dtime; + stepbox.MinEdge.Z += aspeed_f.Z * extra_dtime; + stepbox.MaxEdge.X += aspeed_f.X * extra_dtime; + stepbox.MaxEdge.Z += aspeed_f.Z * extra_dtime; // Check for stairs. - bool step_up = (nearest_collided != COLLISION_AXIS_Y) && // must not be Y direction - (movingbox.MinEdge.Y < cbox.MaxEdge.Y) && - (movingbox.MinEdge.Y + stepheight > cbox.MaxEdge.Y) && - (!wouldCollideWithCeiling(cinfo, stepbox, - cbox.MaxEdge.Y - movingbox.MinEdge.Y, - d)); + step_up = (movingbox.MinEdge.Y < cbox.MaxEdge.Y) && + (movingbox.MinEdge.Y + stepheight > cbox.MaxEdge.Y) && + (!wouldCollideWithCeiling(cinfo, stepbox, + cbox.MaxEdge.Y - movingbox.MinEdge.Y, + d)); + } - // Get bounce multiplier - float bounce = -(float)nearest_info.bouncy / 100.0f; + // Get bounce multiplier + float bounce = -(float)nearest_info.bouncy / 100.0f; - // Move to the point of collision and reduce dtime by nearest_dtime - if (nearest_dtime < 0) { - // Handle negative nearest_dtime - if (!step_up) { - if (nearest_collided == COLLISION_AXIS_X) - pos_f->X += speed_f->X * nearest_dtime; - if (nearest_collided == COLLISION_AXIS_Y) - pos_f->Y += speed_f->Y * nearest_dtime; - if (nearest_collided == COLLISION_AXIS_Z) - pos_f->Z += speed_f->Z * nearest_dtime; - } - } else { - *pos_f += truncate(*speed_f * nearest_dtime, 100.0f); - dtime -= nearest_dtime; + // Move to the point of collision and reduce dtime by nearest_dtime + if (nearest_dtime < 0) { + // Handle negative nearest_dtime + // This largely means an "instant" collision, e.g., with the floor. + // We use aspeed and nearest_dtime to be consistent with above and resolve this collision + if (!step_up) { + if (nearest_collided == COLLISION_AXIS_X) + pos_f->X += aspeed_f.X * nearest_dtime; + if (nearest_collided == COLLISION_AXIS_Y) + pos_f->Y += aspeed_f.Y * nearest_dtime; + if (nearest_collided == COLLISION_AXIS_Z) + pos_f->Z += aspeed_f.Z * nearest_dtime; } + } else if (nearest_dtime > 0) { + // updated average speed for the sub-interval up to nearest_dtime + aspeed_f = *speed_f + accel_f * 0.5f * nearest_dtime; + *pos_f += aspeed_f * nearest_dtime; + // Speed at (approximated) collision: + *speed_f += accel_f * nearest_dtime; + // Limit speed for avoiding hangs + *speed_f = truncate(rangelimv(*speed_f, -5000.0f, 5000.0f), 10000.0f); + dtime -= nearest_dtime; + } - bool is_collision = true; - if (nearest_info.is_unloaded) - is_collision = false; + v3f old_speed_f = *speed_f; + // Set the speed component that caused the collision to zero + if (step_up) { + // Special case: Handle stairs + nearest_info.is_step_up = true; + } else if (nearest_collided == COLLISION_AXIS_X) { + if (bounce < -1e-4 && fabsf(speed_f->X) > BS * 3) { + speed_f->X *= bounce; + } else { + speed_f->X = 0; + accel_f.X = 0; // avoid colliding in the next interations + } + } else if (nearest_collided == COLLISION_AXIS_Y) { + if (bounce < -1e-4 && fabsf(speed_f->Y) > BS * 3) { + speed_f->Y *= bounce; + } else { + if (speed_f->Y < 0.0f) { + // FIXME: This code is necessary until `axisAlignedCollision` takes acceleration + // into consideration for the time calculation. Otherwise, the colliding faces + // never line up, especially at high step (dtime) intervals. + result.touching_ground = true; + result.standing_on_object = nearest_info.isObject(); + } + speed_f->Y = 0; + accel_f.Y = 0; // avoid colliding in the next interations + } + } else { /* nearest_collided == COLLISION_AXIS_Z */ + if (bounce < -1e-4 && fabsf(speed_f->Z) > BS * 3) { + speed_f->Z *= bounce; + } else { + speed_f->Z = 0; + accel_f.Z = 0; // avoid colliding in the next interations + } + } + + if (!nearest_info.is_unloaded && !step_up) { CollisionInfo info; - if (nearest_info.isObject()) - info.type = COLLISION_OBJECT; - else - info.type = COLLISION_NODE; - + info.axis = nearest_collided; + info.type = nearest_info.isObject() ? COLLISION_OBJECT : COLLISION_NODE; info.node_p = nearest_info.position; info.object = nearest_info.obj; info.new_pos = *pos_f; - info.old_speed = *speed_f; - - // Set the speed component that caused the collision to zero - if (step_up) { - // Special case: Handle stairs - nearest_info.is_step_up = true; - is_collision = false; - } else if (nearest_collided == COLLISION_AXIS_X) { - if (fabs(speed_f->X) > BS * 3) - speed_f->X *= bounce; - else - speed_f->X = 0; - result.collides = true; - } else if (nearest_collided == COLLISION_AXIS_Y) { - if(fabs(speed_f->Y) > BS * 3) - speed_f->Y *= bounce; - else - speed_f->Y = 0; - result.collides = true; - } else if (nearest_collided == COLLISION_AXIS_Z) { - if (fabs(speed_f->Z) > BS * 3) - speed_f->Z *= bounce; - else - speed_f->Z = 0; - result.collides = true; - } - + info.old_speed = old_speed_f; info.new_speed = *speed_f; - if (info.new_speed.getDistanceFrom(info.old_speed) < 0.1f * BS) - is_collision = false; - - if (is_collision) { - info.axis = nearest_collided; - result.collisions.push_back(std::move(info)); - } + result.collisions.push_back(info); } + + if (dtime < BS * 1e-10f) + break; + + // Speed for finding the next collision + aspeed_f = *speed_f + accel_f * 0.5f * dtime; + // Limit speed for avoiding hangs + aspeed_f = truncate(rangelimv(aspeed_f, -5000.0f, 5000.0f), 10000.0f); } /* @@ -573,14 +587,15 @@ collisionMoveResult collisionMoveSimple(Environment *env, IGameDef *gamedef, box.MaxEdge += *pos_f; } if (std::fabs(cbox.MaxEdge.Y - box.MinEdge.Y) < 0.05f) { + // This is code is technically only required if `box_info.is_step_up == true`. + // However, players rely on this check/condition to climb stairs faster. See PR #10587. result.touching_ground = true; - - if (box_info.isObject()) - result.standing_on_object = true; + result.standing_on_object = box_info.isObject(); } } } + result.collides = !result.collisions.empty(); return result; } diff --git a/src/unittest/test_collision.cpp b/src/unittest/test_collision.cpp index 40cd52798..87f71cd43 100644 --- a/src/unittest/test_collision.cpp +++ b/src/unittest/test_collision.cpp @@ -51,7 +51,7 @@ namespace { #define UASSERTEQ_F(actual, expected) do { \ f32 a = (actual); \ f32 e = (expected); \ - UTEST(fabsf(a - e) <= 0.0001f, "actual: %.f expected: %.f", a, e) \ + UTEST(fabsf(a - e) <= 0.0001f, "actual: %.5f expected: %.5f", a, e) \ } while (0) #define UASSERTEQ_V3F(actual, expected) do { \ @@ -86,7 +86,7 @@ void TestCollision::testAxisAlignedCollision() } { aabb3f s(bx, by, bz, bx+1, by+1, bz+1); - aabb3f m(bx-2, by+1.5, bz, bx-1, by+2.5, bz-1); + aabb3f m(bx-2, by+1.5, bz, bx-1, by+2.5, bz+1); v3f v(1, 0, 0); f32 dtime = 1.0f; UASSERT(axisAlignedCollision(s, m, v, &dtime) == -1); @@ -134,16 +134,16 @@ void TestCollision::testAxisAlignedCollision() { aabb3f s(bx, by, bz, bx+1, by+1, bz+1); aabb3f m(bx+2, by-1.5, bz, bx+2.5, by-0.5, bz+1); - v3f v(-0.5, 0.2, 0); - f32 dtime = 2.5f; + v3f v(-0.5, 0.2, 0); // 0.200000003 precisely + f32 dtime = 2.51f; UASSERT(axisAlignedCollision(s, m, v, &dtime) == 1); // Y, not X! UASSERT(fabs(dtime - 2.500) < 0.001); } { aabb3f s(bx, by, bz, bx+1, by+1, bz+1); aabb3f m(bx+2, by-1.5, bz, bx+2.5, by-0.5, bz+1); - v3f v(-0.5, 0.3, 0); - f32 dtime = 2.0f; + v3f v(-0.5, 0.3, 0); // 0.300000012 precisely + f32 dtime = 2.1f; UASSERT(axisAlignedCollision(s, m, v, &dtime) == 0); UASSERT(fabs(dtime - 2.000) < 0.001); } @@ -179,7 +179,7 @@ void TestCollision::testAxisAlignedCollision() aabb3f s(bx, by, bz, bx+2, by+2, bz+2); aabb3f m(bx-4.2, by-4.2, bz-4.2, bx-2.3, by-2.29, bz-2.29); v3f v(1./7, 1./7, 1./7); - f32 dtime = 17.0f; + f32 dtime = 17.1f; UASSERT(axisAlignedCollision(s, m, v, &dtime) == 0); UASSERT(fabs(dtime - 16.1) < 0.001); } @@ -224,18 +224,16 @@ void TestCollision::testCollisionMoveSimple(IGameDef *gamedef) res = collisionMoveSimple(env.get(), gamedef, box, 0.0f, 1.0f, &pos, &speed, accel); - UASSERT(!res.touching_ground || !res.collides || !res.standing_on_object); + UASSERT(!res.touching_ground && !res.collides && !res.standing_on_object); UASSERT(res.collisions.empty()); - // FIXME: it's easy to tell that this should be y=1.5f, but our code does it wrong. - // It's unclear if/how this will be fixed. - UASSERTEQ_V3F(pos, fpos(4, 2, 4)); + UASSERTEQ_V3F(pos, fpos(4, 1.5f, 4)); UASSERTEQ_V3F(speed, fpos(0, 1, 0)); /* standing on ground */ pos = fpos(0, 0.5f, 0); speed = fpos(0, 0, 0); accel = fpos(0, -9.81f, 0); - res = collisionMoveSimple(env.get(), gamedef, box, 0.0f, 0.04f, + res = collisionMoveSimple(env.get(), gamedef, box, 0.0f, 0.05f, &pos, &speed, accel); UASSERT(res.collides); @@ -251,6 +249,110 @@ void TestCollision::testCollisionMoveSimple(IGameDef *gamedef) UASSERTEQ(v3s16, ci.node_p, v3s16(0, 0, 0)); } + /* glitched into ground */ + pos = fpos(0, 0.499f, 0); + speed = fpos(0, 0, 0); + accel = fpos(0, -9.81f, 0); + res = collisionMoveSimple(env.get(), gamedef, box, 0.0f, 0.05f, + &pos, &speed, accel); + + UASSERTEQ_V3F(pos, fpos(0, 0.5f, 0)); // moved back out + UASSERTEQ_V3F(speed, fpos(0, 0, 0)); + UASSERT(res.collides); + UASSERT(res.touching_ground); + UASSERT(!res.standing_on_object); + UASSERT(res.collisions.size() == 1); + { + auto &ci = res.collisions.front(); + UASSERTEQ(int, ci.type, COLLISION_NODE); + UASSERTEQ(int, ci.axis, COLLISION_AXIS_Y); + UASSERTEQ(v3s16, ci.node_p, v3s16(0, 0, 0)); + } + + /* falling on ground */ + pos = fpos(0, 1.2345f, 0); + speed = fpos(0, -3.f, 0); + accel = fpos(0, -9.81f, 0); + res = collisionMoveSimple(env.get(), gamedef, box, 0.0f, 0.5f, + &pos, &speed, accel); + + UASSERT(res.collides); + UASSERT(res.touching_ground); + UASSERT(!res.standing_on_object); + // Current collision code uses linear collision, which incorrectly yields a collision at 0.741 here + // but usually this resolves itself in the next dtime, fortunately. + // Parabolic collision should correctly find this in one step. + // UASSERTEQ_V3F(pos, fpos(0, 0.5f, 0)); + UASSERTEQ_V3F(speed, fpos(0, 0, 0)); + UASSERT(res.collisions.size() == 1); + { + auto &ci = res.collisions.front(); + UASSERTEQ(int, ci.type, COLLISION_NODE); + UASSERTEQ(int, ci.axis, COLLISION_AXIS_Y); + UASSERTEQ(v3s16, ci.node_p, v3s16(0, 0, 0)); + } + + /* jumping on ground */ + pos = fpos(0, 0.5f, 0); + speed = fpos(0, 2.0f, 0); + accel = fpos(0, -9.81f, 0); + res = collisionMoveSimple(env.get(), gamedef, box, 0.0f, 0.2f, + &pos, &speed, accel); + UASSERT(!res.collides && !res.touching_ground && !res.standing_on_object); + + res = collisionMoveSimple(env.get(), gamedef, box, 0.0f, 0.5f, + &pos, &speed, accel); + + UASSERT(res.collides); + UASSERT(res.touching_ground); + UASSERT(!res.standing_on_object); + // Current collision code uses linear collision, which incorrectly yields a collision at 0.672 here + // but usually this resolves itself in the next dtime, fortunately. + // Parabolic collision should correctly find this in one step. + // UASSERTEQ_V3F(pos, fpos(0, 0.5f, 0)); + UASSERTEQ_V3F(speed, fpos(0, 0, 0)); + UASSERT(res.collisions.size() == 1); + { + auto &ci = res.collisions.front(); + UASSERTEQ(int, ci.type, COLLISION_NODE); + UASSERTEQ(int, ci.axis, COLLISION_AXIS_Y); + UASSERTEQ(v3s16, ci.node_p, v3s16(0, 0, 0)); + } + + /* moving over ground, no gravity */ + pos = fpos(0, 0.5f, 0); + speed = fpos(-1.6f, 0, -1.7f); + accel = fpos(0, 0.0f, 0); + res = collisionMoveSimple(env.get(), gamedef, box, 0.0f, 1.0f, + &pos, &speed, accel); + + UASSERT(!res.collides); + // UASSERT(res.touching_ground); // no gravity, so not guaranteed + UASSERT(!res.standing_on_object); + UASSERTEQ_V3F(pos, fpos(-1.6f, 0.5f, -1.7f)); + UASSERTEQ_V3F(speed, fpos(-1.6f, 0, -1.7f)); + UASSERT(res.collisions.empty()); + + /* moving over ground, with gravity */ + pos = fpos(5.5f, 0.5f, 5.5f); + speed = fpos(-1.0f, 0.0f, -0.1f); + accel = fpos(0, -9.81f, 0); + res = collisionMoveSimple(env.get(), gamedef, box, 0.0f, 1.0f, + &pos, &speed, accel); + + UASSERT(res.collides); + UASSERT(res.touching_ground); + UASSERT(!res.standing_on_object); + UASSERTEQ_V3F(pos, fpos(4.5f, 0.5f, 5.4f)); + UASSERTEQ_V3F(speed, fpos(-1.0f, 0, -0.1f)); + UASSERT(res.collisions.size() == 1); + { // first collision on y axis zeros speed and acceleration. + auto &ci = res.collisions.front(); + UASSERTEQ(int, ci.type, COLLISION_NODE); + UASSERTEQ(int, ci.axis, COLLISION_AXIS_Y); + UASSERTEQ(v3s16, ci.node_p, v3s16(5, 0, 5)); + } + /* not moving never collides */ pos = fpos(0, -100, 0); speed = fpos(0, 0, 0);