Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Collision: more accurate computation with acceleration and long dtime #15408

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
245 changes: 130 additions & 115 deletions src/collision.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
}
}
kno10 marked this conversation as resolved.
Show resolved Hide resolved

// Helper function:
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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
*/
Expand All @@ -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<NearbyCollisionInfo> 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
Expand All @@ -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;
Expand All @@ -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.
Expand All @@ -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;

Expand All @@ -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));

// 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;
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;

// 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;
}

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) {
kno10 marked this conversation as resolved.
Show resolved Hide resolved
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 {
*pos_f += truncate(*speed_f * nearest_dtime, 100.0f);
dtime -= nearest_dtime;
speed_f->Z = 0;
accel_f.Z = 0; // avoid colliding in the next interations
}
}

bool is_collision = true;
if (nearest_info.is_unloaded)
is_collision = false;

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);
}

/*
Expand Down Expand Up @@ -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;
}

Expand Down
Loading
Loading