ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

外推的真相:游戏中的猜谜艺术

外推的真相:游戏中的猜谜艺术 从一个熟悉的 bug 说起多人游戏里远程玩家的位置是从服务器收到的。假设 20 Hz也就是每 50 毫秒来一个包。但你要以 144 帧渲染。两个包之间有 7 帧没有数据。这 7 帧的位置你只能猜。猜出来的结果经常是这样对面玩家在拐角处冲出去一截然后被拉回来。你瞄准的是他冲出去的位置打空了。这就是外推。它不是某个高级技巧它是所有实时游戏都必须做的事因为数据永远不够。插值和外推的区别这两个词经常混用但它们的稳定性差了一个量级。插值在已知数据之间求值 ●━━━━━?━━━━━● 已知 求这里 已知 两侧都有约束答案被夹住了 外推在已知数据之外求值 ●━━━━━●━━━━━● ? ? ? 已知 已知 已知 求这里 只有单侧约束没有东西拦着插值的误差被两端的数据夹住有天然的上界。外推没有这个保护——你离最后一个数据点越远误差增长越快而且没有任何机制会把它拽回来。这正是前一篇里说的为什么龙格现象的振荡总出现在区间两端端点附近的多项式行为更接近外推。那里的邻域有一半在数据之外有效约束只剩单侧。所以外推不稳定不是实现问题是它的固有性质。你没法把外推做得正确只能做得够用。游戏案例一网络位置预测这是外推最典型的用途行话叫 dead reckoning航位推算。最简单的做法是用速度做一阶外推// 收到的最新状态vec3 lastPos;vec3 lastVel;floatlastTime;// 渲染时floatdtnow-lastTime;vec3 predictedlastPoslastVel*dt;就是position velocity * dt。用导数往前推。阶数越高越快失控你可能想加上加速度做二阶外推更准一点vec3 predictedlastPoslastVel*dt0.5f*lastAcc*dt*dt;在dt很小的时候这确实更准。但dt一变大那个dt2dt^2dt2项就开始主导误差爆炸式增长。外推阶数误差增长dt50msdt200ms丢包0 阶位置冻结线性偏一点偏得多但方向对1 阶用速度∝dt2\propto dt^2∝dt2很准明显超前2 阶用加速度∝dt3\propto dt^3∝dt3更准飞到墙外面这和龙格现象是同一个道理高阶在数据附近更准在远离数据处更快发散。实践上的结论很一致网络预测基本只用一阶顶多在短窗口内用二阶。没人用三阶。外推必须有闸因为外推一定会错所以关键不是猜得准而是限制猜错的代价。三道闸每一道都在实际项目里见过// 闸 1限制外推时长。超过这个时间就不猜了constfloatMAX_EXTRAP0.25f;dtfminf(dt,MAX_EXTRAP);// 闸 2外推结果必须过碰撞检测不能穿墙predictedResolveCollision(lastPos,predicted);// 闸 3真实数据到了之后平滑收敛而不是硬切floatt1.0f-expf(-k*frameDt);renderPoslerp(renderPos,authoritativePos,t);第三道闸尤其重要。外推错了之后真值到达时如果直接赋值玩家看到的就是瞬移。平滑收敛让错误被摊到几帧里消化掉。注意这里又用上了1 - exp(-k*dt)而不是常数 lerp——否则平滑速度会跟帧率绑定这是前面那篇讲过的坑。外推 vs 延迟渲染有个更根本的选择你可以干脆不外推。把远程玩家故意延迟 100 毫秒渲染这样你总是在两个已知数据点之间求值——变成了插值。外推方案渲染在最新数据之后必须猜 ●───●───● ?←渲染在这 可能错但没有额外延迟 插值方案渲染在数据中间不用猜 ●───●───↑───● 渲染在这 总是正确但慢了一个包这是个实打实的权衡外推延迟插值显示延迟低高一个包的时间位置正确性会错会被拉回总是准确适合快节奏射击MOBA、策略、回合制竞技射击倾向外推因为延迟直接影响手感。MOBA 和 RTS 倾向延迟插值因为位置稳定比 100 毫秒更重要。很多引擎两者混用自己的角色外推要响应别人的角色延迟插值要稳定。游戏案例二VR 的重投影VR 里的外推是刚需而且容错极低。渲染一帧要时间。你开始渲染时读的头部姿态等画面显示出来已经过了十几毫秒。如果直接用读到的姿态渲染画面永远落后于头部运动玩家会晕。所以必须外推预测画面显示那一刻头在哪。floatpredictAheadrenderTimedisplayLatency;Pose predictedExtrapolatePose(currentPose,angularVel,predictAhead);这里有两个值得注意的点。预测窗口极短所以外推很准。十几毫秒内头部运动近似匀速一阶外推足够。外推误差随dtdtdt增长窗口小就没事。代价不对称。渲染延迟会直接导致晕动症外推轻微超前只会让画面稍微过冲。后者比前者可接受得多所以 VR 宁可外推也不接受延迟。这是个很好的例子外推值不值得做取决于猜错的代价和不猜的代价哪个大。游戏案例三动画的边界动画曲线也会遇到外推位置是在时间轴的两端。动画师 K 了一串关键帧从 0 秒到 2 秒。如果有代码在 2.1 秒去采样就越界了。常见的几种处理方式每种都是一个外推策略模式行为适合Clamp停在最后一帧的值大多数情况最安全Loop回到开头循环动画走路跑步Ping-pong倒着播回去呼吸、摆动Linear按最后的切线继续延伸需要连续运动有风险默认用 Clamp。它是零阶外推——不猜趋势只保持最后已知值。最保守但永远不会跑飞。Linear模式就是一阶外推会按最后一帧的斜率无限延伸。如果最后一帧的切线很陡角色的手会越伸越远一秒后伸到屏幕外面去。这个 bug 看起来很诡异但原因就是外推没有闸。程序化动画里要特别小心。比如你用样条做一条武器挥击轨迹然后某个 combo 把播放速度调快了时间轴可能滑出曲线定义域。如果曲线的外推模式是 linear武器会飞出去。游戏案例四时间性渲染技术这类技术全都建立在外推上。TAA / 运动模糊用 motion vector 把上一帧的像素外推到当前帧的位置。motion vector 就是屏幕空间位置的导数。外推失效的地方会出现伪影遮挡关系变了disocclusion——上一帧被挡住的区域没有历史数据可用快速转镜——外推距离太远半透明和粒子——motion vector 不可靠所有 TAA 实现都要做历史拒绝history rejection检测外推结果是否合理不合理就丢掉历史退回当前帧的原始值。这又是一道闸。帧生成 / 插帧注意名字叫插但很多实现其实是外推——它没有未来帧可用只能从已有帧往前推。所以生成的帧会有轻微的橡皮感和额外的伪影。真正的插值需要等到下一帧那就引入延迟了又回到前面那个权衡。外推的通用处方把上面几个案例的共性提出来是五条一、降阶。阶数越高远处越快发散。能用一阶就别用二阶。这和不要用高次多项式是同一条规律。二、限窗。给外推时长设硬上限。超过就退化成零阶冻结在最后已知值而不是继续外推。dtfminf(dt,MAX_EXTRAP_TIME);三、约束。外推结果必须过一遍物理和逻辑检查。不能穿墙不能出界血量不能为负。这就是前一篇讲值域时说的那把尺子——知道答案该落在哪个区间就能拦掉不可能的猜测。四、平滑收敛。真值到了之后别硬切。用1 - exp(-k*dt)把误差摊到几帧里消化。五、能插值就别外推。愿意付出一点延迟就能把一个不稳定的问题换成稳定的问题。先确认延迟真的不可接受再去外推。这条线串起来了什么回头看这几篇其实是同一件事的不同切面。导数是外推的工具。位置加速度乘时间这就是一阶外推。没有导数就没法往前猜。阶数越高越不稳。二阶外推在短窗口内更准窗口一长就比一阶烂得多。龙格现象是这条规律在插值上的版本网络预测的dt3dt^3dt3发散是它在时间上的版本。值域是最后的拦网。外推一定会错。你没法保证它对但可以保证它不荒谬——知道合法区间就能在它跑飞之前拦住。端点就是外推。这是龙格现象那篇的核心解释。区间两端的邻域有一半在数据之外多项式在那里的行为更像外推而不是插值所以振荡总从两端开始。带走外推就是在没有数据的地方猜答案。它不稳定不是因为实现得不好而是因为单侧约束本身就不够。你没法让外推变准只能降阶、限窗、约束、平滑收敛。如果延迟一点就能换成插值先认真考虑那条路。
返回列表