尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

FPS角色移动:从撞墙卡死到顺滑贴墙

FPS角色移动:从撞墙卡死到顺滑贴墙 玩 FPS 你肯定有这体感:斜着撞上一堵墙,人不会当场钉死在那,而是顺着墙面滑走,还能继续往前蹭。这个手感太理所当然了,以至于没人觉得它是个需要设计的东西。但你要是自己写移动,第一版八成就是撞到就停,玩起来像被墙粘住,难受得要命。这篇讲的就是从撞到停死到贴墙滑走这中间发生了什么。这套东西业内叫 Collide-and-Slide,碰撞加滑动,是几乎所有 FPS 角色移动的底层。照旧,全程定点数,坐标单位米。先看停死版本错在哪最朴素的移动是这样:算出这一帧想去的位置,检测一下路上撞没撞到东西,撞到了就别动。voidMove(refFixVector3pos,FixVector3velocity,Fixdt){FixVector3targetposvelocity*dt;// 这帧想去的地方if(SweepHit(pos,target,outvarhit)){// 撞到了……那就别动了return;// ← 停死罪魁祸首}postarget;}问题出在return这一下。玩家明明是斜着撞墙的,他的运动里其实一部分是往墙里钻,另一部分是沿着墙走。往墙里钻的那部分该拦,没问题;但沿着墙走的那部分是完全合法的,你一个return把两部分一起毙了,人就贴墙上不动了。所以关键不是撞到了要不要停,而是撞到了,把运动拆开,只砍掉不合法的那部分,合法的那部分放行。核心就一个动作:把速度拍到墙面上想通了上面那点,解法就浮出来了。撞墙的时候,墙会给你一个法线——就是垂直于墙面、指向外面的那个方向。玩家的速度向量,可以沿着这个法线拆成两块:墙面法线 N ↑ | 玩家速度 V | ↗ | ╱ ─────────┼──────╱──────── 墙面 | ╱ | ╱ |╱ V 拆成两块 一块顺着 N钻进墙里的分量—— 要砍掉 一块沿着墙面滑动的分量 —— 要保留怎么拆?靠点积。速度 V 在法线 N 上的投影长度,就是Dot(V, N)。这个值代表 V 有多少是朝着法线方向去的——也就是有多少在往墙里钻。拆出来之后,把钻墙那部分从 V 里减掉,剩下的自然就是沿墙滑那部分:// 把速度投影到墙面上去掉钻进墙里的分量staticFixVector3ProjectOnPlane(FixVector3v,FixVector3normal){FixdistFixVector3.Dot(v,normal);// v 有多少朝着法线方向returnv-normal*dist;// 减掉这部分剩下的贴着墙面}就这三行,是整个滑动手感的心脏。normal * dist是钻墙分量,v减掉它,留下的就是平行于墙面的滑动速度。这个操作数学上叫把向量投影到平面上,名字唬人,干的事就是把往墙里去的劲儿抽掉。这里要提醒一句,Dot内部是三个分量各自相乘再相加,每个乘法都是定点乘法,中间值会暴涨,前面反复讲过的那个 64 位防溢出的坑,这儿照样存在。你的FixVector3.Dot底层必须把中间乘积提到 long,不然大坐标下点积直接溢出,滑动方向瞬间乱套。一次滑动的完整流程有了投影,把移动逻辑重写一遍:voidMove(refFixVector3pos,refFixVector3velocity,Fixdt){FixVector3remainingvelocity*dt;// 这帧还剩多少位移要走if(SweepHit(pos,posremaining,outvarhit)){// 走到撞墙点先把撞之前那段路走完poshit.point;// 剩下没走完的位移拍到墙面上改方向继续滑FixVector3leftover(posremaining)-hit.point;remainingProjectOnPlane(leftover,hit.normal);// 顺便把速度本身也拍一下不然下一帧又直直撞上来velocityProjectOnPlane(velocity,hit.normal);}posremaining;}流程说白了三步:走到撞墙那一点停下,把没走完的路折向墙面,顺着新方向把剩下的路走完。速度也得跟着拍一下,否则这帧滑走了,下一帧速度还是原来那个直冲墙的方向,又撞上,白忙活。一次滑动不够,要能连着滑上面这版有个隐患。你斜着撞墙滑走,滑着滑着可能又撞上另一堵墙——比如墙角。这时候你只做了一次投影就pos remaining收工,那第二堵墙就漏检了,人会直接穿进墙角里。所以真正的 Collide-and-Slide 是循环的:滑动之后再检测,又撞到就再滑,直到这帧的位移全部走完,或者撞够了次数为止。voidMove(refFixVector3pos,refFixVector3velocity,Fixdt){FixVector3remainingvelocity*dt;constintMAX_SLIDE4;// 最多滑 4 次够用且防死循环for(inti0;iMAX_SLIDE;i){if(!SweepHit(pos,posremaining,outvarhit)){posremaining;// 一路无阻走完收工return;}poshit.point;// 走到这次的撞墙点FixVector3leftover(posremaining)-hit.point;remainingProjectOnPlane(leftover,hit.normal);// 折向墙面velocityProjectOnPlane(velocity,hit.normal);}// 滑够 4 次还在撞墙角、夹缝剩下的位移就不走了防止卡进几何体posremaining;}MAX_SLIDE这个上限很重要。墙角、V 字形夹缝这种地方,你滑向 A 墙又撞 B 墙,滑向 B 墙又撞 A 墙,不设上限能无限循环下去,一帧卡死。设个 4 次,既够处理绝大多数复杂几何,又能兜住死循环。撞满 4 次还在撞,说明玩家钻进了个特别刁钻的角落,这时候宁可让他停一下,也不能让游戏卡住。为什么这套东西非定点不可你可能想,滑动手感是表现层的东西,用 float 算算得了,反正玩家又看不出那点误差。在竞技 FPS 里这想法要命。角色位置是权威状态,它要参与服务器校验、参与延迟补偿回放、参与命中判定。你在客户端用浮点滑出来一个位置,服务器用浮点滑出另一个略微不同的位置,两边一对不上,轻则位置回弹(玩家看着自己被拽回去,俗称橡皮筋),重则整局 desync。而滑动这套逻辑里全是点积、向量减法、乘法——正好都是定点数逐比特一致的强项。同样的输入、同样的墙、同样的法线,红米和 iPhone 和服务器滑出来的位置,连最低位都一样。这样服务器校验才有意义,延迟补偿倒带才能精确复现你当时到底贴在墙的哪个位置。这也是为什么滑动逻辑必须整个待在定点模拟层里,连那个ProjectOnPlane都不能图省事转 float 算。一处泄漏,满盘皆输——这话在这个系列里说过好几遍了,但每一处都是真金白银的教训。几个手感上的延伸基础的贴墙滑讲完了,顺带说几个让手感更真实的常见处理,不展开写代码,给你个方向:地面和斜坡。走斜坡本质也是滑动,只不过法线朝上。你走上一个缓坡,速度被投影到坡面上,人就顺着爬上去了;坡太陡,投影完向上的分量太小,人就爬不动往下出溜——滑动这套逻辑天然就把斜坡处理了,不用额外写。台阶。想让玩家能自动迈上小台阶而不是被矮墙挡住,得在撞到矮障碍时先试着抬高一点再往前探,探到了就踩上去。这叫 step offset,是 Collide-and-Slide 之上加的一层。保留下落。人在空中斜撞墙,水平方向该被墙挡住并滑走,但竖直方向的重力下落不该被影响。所以有些实现会把水平滑动和竖直下落分开算,别让投影把重力也给抹了。收尾回到最开始那个问题:为什么撞墙不停死、要滑走?因为玩家斜着撞墙时,他的运动里往墙里钻和沿墙走是两码事。停死是把两者一起毙了,粗暴。正确做法是用点积把速度沿墙面法线拆开,只砍掉钻墙那部分,保留沿墙那部分——这就是ProjectOnPlane那三行干的事。再套上一个滑完再检测的循环处理墙角,配一个次数上限防死循环,一套顺滑的角色移动就成了。而这一切之所以能放心用,是因为底下全是定点数的点积和向量运算,逐比特一致,服务器和每个客户端滑出来的位置分毫不差。手感是玩家感受到的,确定性是玩家感受不到、但一旦缺了就满屏橡皮筋的东西。好的移动手感,就是这两样在底下悄悄咬合的结果。
返回列表