高速碰撞盒穿过薄墙:接触时刻必须沿整段位移求解
一个宽一单位的碰撞盒从中心 (0,0) 向右移动十单位,墙位于 x=[4,4.2]。移动前,盒在墙左边;移动后,盒在墙右边。两次重叠检测都返回否,角色却已经穿过墙。墙再厚一点可能暂时掩盖问题,减小步长也只能改变触发条件:只看端点,始终没有回答这一段运动经过了哪里。
这里用独立 C# 研究工程固定输入与接触约定,研究二维、尺寸不变、无旋转的轴对齐盒。坐标使用同一世界长度单位,墙保持静止,位移在本次步进内按直线发生。它不是 Unity 或 Godot 物理引擎的实测报告。

墙的厚度变成一段时间窗口
令 t 表示本次位移的完成比例,取值为 [0,1],不是秒数。移动盒的中心为 (10t,0),右边缘先在 10t+0.5=4 时触墙,因此进入时刻是 0.35;左边缘在 10t-0.5=4.2 时离墙,退出时刻是 0.47。Y 方向始终重叠,所以首次接触只由 X 决定,接触中心为 (3.5,0),朝向移动盒的墙面法线为 (-1,0)。
这个计算也可以理解为把墙按移动盒半尺寸扩张,再让盒中心沿位移做射线区间查询。代码直接使用两盒端点相减,避免额外构造扩张盒。每个轴给出进入与退出时间;跨轴取最晚进入、最早退出,两者之间才是全部轴同时重叠的区间。位移为负时交换轴上的两个时间,并反转接触法线。

不能把 delta 归一化后仍把返回值当作位移比例。归一化会改变参数的尺度,让 [0,1] 从整段运动变成一个长度单位;位移的长度是这份接口合同的一部分。
接触有方向,初始重叠没有唯一入射面
研究目录中的 SweepAabb.cs 定义完整核心,调用链是 Program.cs 的固定样本和回归断言进入 SweepAabb.Cast,再逐轴进入 Axis。返回的 Kind 区分 Hit、Miss 和 Overlap;调用者必须先看分类,不能单凭 Time 决定移动。
public readonly record struct V(double X, double Y);
public readonly record struct Box(V Min, V Max);
public readonly record struct Contact(string Kind, double Time, V Normal);
public static class SweepAabb
{
public static Contact Cast(Box moving, V delta, Box target)
{
Validate(moving);
Validate(target);
if (!double.IsFinite(delta.X) || !double.IsFinite(delta.Y))
throw new ArgumentException("Displacement must be finite.");
if (moving.Min.X < target.Max.X && moving.Max.X > target.Min.X
&& moving.Min.Y < target.Max.Y && moving.Max.Y > target.Min.Y)
return new("Overlap", 0, default);
double enter = double.NegativeInfinity, exit = double.PositiveInfinity;
V normal = default;
if (!Axis(moving.Min.X, moving.Max.X, target.Min.X, target.Max.X,
delta.X, new V(-1, 0), ref enter, ref exit, ref normal)
|| !Axis(moving.Min.Y, moving.Max.Y, target.Min.Y, target.Max.Y,
delta.Y, new V(0, -1), ref enter, ref exit, ref normal)
|| enter < 0 || enter > 1 || enter >= exit)
return new("Miss", 1, default);
return new("Hit", enter, normal);
}
private static bool Axis(double lo, double hi, double wallLo,
double wallHi, double d, V negativeNormal,
ref double enter, ref double exit, ref V normal)
{
// 平行时要求严格重叠,沿边滑动不作为阻挡。
if (d == 0) return lo < wallHi && hi > wallLo;
double near = (wallLo - hi) / d, far = (wallHi - lo) / d;
V n = negativeNormal;
if (near > far)
{
(near, far) = (far, near);
n = new(-n.X, -n.Y);
}
// 相同进入时刻固定选 X 轴,避免角点法线不确定。
if (near > enter) { enter = near; normal = n; }
exit = Math.Min(exit, far);
return enter < exit;
}
private static void Validate(Box b)
{
if (!double.IsFinite(b.Min.X) || !double.IsFinite(b.Min.Y)
|| !double.IsFinite(b.Max.X) || !double.IsFinite(b.Max.Y)
|| b.Min.X >= b.Max.X || b.Min.Y >= b.Max.Y)
throw new ArgumentException("Box must have finite positive extents.");
}
}
本文把阻挡定义为继续沿该方向运动会进入盒内部。平行轴必须严格重叠;只沿墙边滑动不阻挡,仅在角点相切且立即离开的轨迹也不阻挡。已经贴墙时,向墙内移动返回 Hit,t=0,向外离开返回 Miss。在本步终点刚好接触、且继续运动将进入内部时,保留 Hit,t=1,让下一步可以继承接触信息。
若起点已经严格重叠,时间区间无法替调用者选择一个合理的退出方向。代码返回 Overlap 与零法线,不把它伪装成普通命中。出生位置修复、传送校验和穿透分离应走明确的恢复分支。两个轴同刻进入时固定选 X 法线,这让单接触输出确定,但不提供角点的完整接触流形;滑动求解器不能把它当成唯一正确的物理法线。
用端点失败样本锁住时间语义
Program.cs 同时保留了失败参照:把移动盒直接放到中心 (10,0) 再检测,确实得到 Miss。扫掠结果则必须等于 0.35,不能仅断言“检测到了某个碰撞”。同一个测试还检查接触法线,因此把时间算对、方向翻错也无法通过。
其余边界包含反向运动、零位移、平行错开、初始重叠、贴边向内和向外移动、沿边滑动、终点接触、角点进入与瞬时擦角,以及零尺寸、NaN、无限坐标拒绝。额外以整数间隙 1..20、位移 1..40、平移 -3..3 枚举 5600 组样本,用独立的一维间隙除位移作为预期,再把几何与位移同时放大三倍,检查接触比例不变。这批枚举覆盖水平运动,不代表已经穷举二维方向。
2026-09-22 的 .NET 9 Release 构建为零警告、零错误,构建耗时 13.86 秒。下列命令在研究工程目录执行;测试耗时只表示本机断言执行时间,不是引擎物理帧成本。
$ dotnet build SweepAabbLab.csproj -c Release --nologo -p:NuGetAudit=false
exitCode=0 warnings=0 errors=0
$ dotnet run --project SweepAabbLab.csproj -c Release --no-build --no-restore
exitCode=0
passed=17 failed=0 skipped=0 elapsedMs=19.7313
samples=5600
endOverlap=false
kind=Hit time=0.35 contactCenter=(3.5,0) normal=(-1,0)
有限输入检查只排除了明显非法值,并不保证极大坐标相减不溢出,也没有解决极小间隙的浮点消失。当前证据限于所列尺度;接入大世界时应先转到受控的局部坐标范围,再定义长度容差和接触皮肤。随意给所有时间比较加一个 epsilon,会改变擦边规则,不能代替尺度合同。
查询到接触之后,还剩下谁的工作
运动系统可以先移动 delta*t,再决定丢弃、投影或反射剩余位移;检测器不直接修改角色位置。若要继续滑动,需要重新扫掠剩余段并限制迭代次数,还要处理同时接触多面,当前实现没有代办这些响应。移动墙可在保持平移假设时转成相对位移,但接触点的世界位置仍须还原;旋转、缩放或弯曲轨迹不能直接沿用这份区间。
宽阶段也必须提供覆盖整段运动的候选,例如起终点 AABB 的并盒。只查询终点附近,即使窄阶段扫掠完全正确,也不会拿到途中薄墙。面对多个候选时,应比较合法命中的最小 t,并明确相同时刻的多接触政策。
开头的盒可以由控制器截停在墙前的中心 3.5,依据来自整段位移的交集时间。对运动控制器而言,值得固定的是“期望位移进入查询,接触分类与比例返回控制器”的接口:它使穿墙检测、初始穿透修复和碰撞响应各自拥有可测试的输入,而不会让一个帧末布尔值承担整段运动的安全保证。