· 6 分钟阅读 · 2164 字
墙面迎着投射物移动时,反弹不能直接反射世界速度
投射物以 (-3,4) m/s 撞向一面向右移动的墙,墙速为 (2,0) m/s,法线从墙面指向投射物,取 (1,0)。把恢复系数设为 0.5,直接翻转并衰减世界速度的 X 分量,会得到 (1.5,4)。投射物看似已经朝右反弹,却仍然比墙慢,墙会继续追上它。正确结果是 (4.5,4):反弹后相对墙面的法向速度应当是 +2.5 m/s。
这类错误会出现在移动挡板、机关墙与运动学平台上的投射物响应。下面的 C# 纵向切片只求接触发生后的速度,不负责发现接触,也不把运动学墙面当成会被撞退的动态刚体。这个边界决定了恢复系数该作用在哪个速度上,以及世界动能为何可以在碰撞后增加。

反弹后朝右,仍可能没有离开墙
令投射物世界速度为 v,墙面接触点世界速度为 u,单位法线为 n。相对速度是 w=v-u,接近速度是 s=dot(w,n)。当前法线约定下,s<0 表示正在接近表面;s≥0 表示分离或切向运动,应保持输入速度。
恢复系数 e 的合同是 dot(w',n)=-e·s,无摩擦则要求切向相对速度不变。满足两项条件的增量沿法线,大小为 -(1+e)·s,所以世界速度可以直接写成 v'=v-(1+e)·s·n。只有计算 s 时需要减去表面速度,最终表达式已经包含了回到世界空间的变换。
固定案例中,接近速度为 -3-2=-5,反弹后是 -0.5×(-5)=2.5,加回墙速后得到 4.5。错误实现得到的 1.5 与墙速相减仍为 -0.5,连“接触后正在分离”的条件都没有满足。它的问题不只是反弹力度偏小,还会把同一接触留给下一次响应,再次修改速度。
世界速度为正也不代表正在分离。把投射物改成 (1,4),它已经向右运动,但墙以 2 向右推进,相对法向速度仍为 -1;正确响应把 X 改为 2.5。入口若先判断 dot(v,n)>=0 就跳过,会漏掉墙面追上投射物的情况。
输入的是接触点速度
Bounce.cs 的 Resolve 消费世界空间速度、单位法线和区间 [0,1] 内的恢复系数,返回新的世界速度。二维向量只用于缩小验证切片,同一推导可以扩展到三维点质量。
public readonly record struct V(double X, double Y)
{
public static V operator +(V a, V b) => new(a.X + b.X, a.Y + b.Y);
public static V operator -(V a, V b) => new(a.X - b.X, a.Y - b.Y);
public static V operator *(double s, V v) => new(s * v.X, s * v.Y);
public double Dot(V b) => X * b.X + Y * b.Y;
public bool Finite => double.IsFinite(X) && double.IsFinite(Y);
}
public static class Bounce
{
public static V Resolve(V velocity, V surfaceVelocity, V unitNormal, double restitution)
{
if (!velocity.Finite || !surfaceVelocity.Finite || !unitNormal.Finite ||
!double.IsFinite(restitution) || restitution < 0 || restitution > 1 ||
Math.Abs(unitNormal.Dot(unitNormal) - 1) > 1e-12)
throw new ArgumentOutOfRangeException(nameof(restitution));
V relative = velocity - surfaceVelocity;
double normalSpeed = relative.Dot(unitNormal);
if (!relative.Finite || !double.IsFinite(normalSpeed))
throw new ArithmeticException("Relative velocity overflow.");
// 只处理朝向表面的相对法向速度;切向分量不施加摩擦。
if (normalSpeed >= 0) return velocity;
V result = velocity - ((1 + restitution) * normalSpeed) * unitNormal;
if (!result.Finite) throw new ArithmeticException("Resolved velocity overflow.");
return result;
}
}
法线长度错误会改变增量的平方尺度,因此入口拒绝非单位法线;它不在响应函数内悄悄归一化,也不能从速度猜测法线朝向。碰撞查询负责提供从表面指向投射物的法线,并在所有空间变换完成后保证方向与单位长度。当前 1e-12 单位长度容差属于 double 探针;接入使用 float 的引擎时,需要依据法线生产过程另定精度合同。
表面速度也不能一概取刚体质心速度。墙面若发生旋转,调用方需提供接触位置处的 u=v_center+ω×r;刚体的角速度、接触点相对质心的位置和长度单位必须匹配。当前运行样本只覆盖平移表面,没有验证旋转接触的生成过程。
无摩擦还意味着切向移动的墙不会拖动投射物。主图右侧的墙沿自身切线运动,改变的是 u 的切向分量,dot(v-u,n) 不变,响应也不变。若玩法要求传送带带动物体,必须引入摩擦或明确的附着规则,不能把整条相对速度都乘上恢复系数;那样会连切向运动一起衰减。

恢复系数小于一,世界动能仍会增加
以单位质量计算,固定案例碰撞前的世界动能是 ((-3)²+4²)/2=12.5 J,碰撞后是 (4.5²+4²)/2=18.125 J。e=0.5 没有保证世界动能下降;它保证的是相对法向速度减半,对应相对法向动能变为原来的四分之一。运动学墙的速度由外部驱动维持,它可以向投射物做功。
因此不能把“碰撞后世界速度不得变大”当作通用保护,否则会删掉移动机关应有的能量传递。若两侧都具有有限质量,墙面也应获得反冲,就需要用质量、转动惯量和接触臂求冲量,并一起更新两侧状态。当前函数把表面速度视为给定值,单独用于两颗动态刚体会破坏动量合同。
e=0 时,投射物的法向速度匹配墙面,切向速度依旧保留;这不是全速度归零,也不是自动粘住墙。持续接触中的重力、穿透修正与下一次接触约束需要单独处理。对已经分离的接触重复调用会保持结果,测试锁住了这一点;它不等于完整接触求解器能够免除接触缓存或迭代策略。
用参考系变换检查同一条响应
Program.cs 的 Check 与 Reject 覆盖固定结果、静止墙、e 的两个端点、切向运动、分离、重复调用以及非法法线、非有限值和算术溢出。以下回归保留了容易被世界速度方向误导的输入:
V n = new(1,0), incoming = new(-3,4), wall = new(2,0);
V actual = Bounce.Resolve(incoming,wall,n,.5);
Check(Close(actual,new(4.5,4)));
Check(Close(Bounce.Resolve(new(1,4),wall,n,.5),new(2.5,4)));
Check(Close(Bounce.Resolve(actual,wall,n,.5),actual));
另有 10000 组固定样本旋转法线并改变法向速度、切向速度、表面速度与 e,分别检查恢复定律和切向分量。每组再给投射物与表面同时增加 (17,-23),响应结果减回这个共同速度后应保持不变。这个惯性系平移检查能够识别直接使用世界法向速度的错误;只对静止墙测试无法区分两种实现。
源码与运行材料保存在 $WORK_DIR/.tmp/daily-labs/2026-10-25-2000/。2026-10-25 使用 .NET 9 Release 执行,命令与决定性输出如下;耗时只记录此次功能检查。
cd "$WORK_DIR"
dotnet build .tmp/daily-labs/2026-10-25-2000/BounceLab.csproj -c Release
dotnet run --project .tmp/daily-labs/2026-10-25-2000/BounceLab.csproj -c Release --no-build
build: exit=0, warnings=0, errors=0
run: exit=0, passed=22, failed=0, skipped=0, elapsed_ms=64.8069
samples=10000
max_normal_law_error=3.552713678800501e-15
max_tangent_error=2.6645352591003757e-15
max_boost_error=7.105427357601002e-15
input=(-3,4), surface=(2,0), output=(4.5,4)
wrong_world_reflection=(1.5,4)
world_positive_but_approaching: input_x=1, output_x=2.5
投射物系统应把碰撞时刻与接触点速度一起交给响应层:连续检测给出命中位置和时刻,响应层在该时刻计算新速度,剩余时间继续推进并按需再次检测。用帧末墙速替代命中时刻墙速,或在反弹后沿旧速度推进余下时间,都会引入这里的速度公式无法修复的误差。对无摩擦运动学表面,最小接口应包含接触法线与接触点速度;只有一个世界反射方向的接口,已经丢掉了完成正确响应所需的信息。