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

玩 FPS 你肯定有这体感:斜着撞上一堵墙,人不会当场钉死在那,而是顺着墙面滑走,还能继续往前蹭。这个手感太理所当然了,以至于没人觉得它是个需要设计的东西。但你要是自己写移动,第一版八成就是"撞到就停",玩起来像被墙粘住,难受得要命。

这篇讲的就是从"撞到停死"到"贴墙滑走"这中间发生了什么。这套东西业内叫 Collide-and-Slide,碰撞加滑动,是几乎所有 FPS 角色移动的底层。照旧,全程定点数,坐标单位米。

先看"停死"版本错在哪

最朴素的移动是这样:算出这一帧想去的位置,检测一下路上撞没撞到东西,撞到了就别动。

void Move(ref FixVector3 pos, FixVector3 velocity, Fix dt) {
    FixVector3 target = pos + velocity * dt;   // 这帧想去的地方
    if (SweepHit(pos, target, out var hit)) {
        // 撞到了……那就别动了?
        return;                                 // ← 停死,罪魁祸首
    }
    pos = target;
}

问题出在 return 这一下。玩家明明是斜着撞墙的,他的运动里其实一部分是"往墙里钻",另一部分是"沿着墙走"。往墙里钻的那部分该拦,没问题;但沿着墙走的那部分是完全合法的,你一个 return 把两部分一起毙了,人就贴墙上不动了。

所以关键不是"撞到了要不要停",而是"撞到了,把运动拆开,只砍掉不合法的那部分,合法的那部分放行"。

核心就一个动作:把速度拍到墙面上

想通了上面那点,解法就浮出来了。撞墙的时候,墙会给你一个法线——就是垂直于墙面、指向外面的那个方向。玩家的速度向量,可以沿着这个法线拆成两块:

          墙面法线 N
             ↑
             |          玩家速度 V
             |         ↗
             |       ╱
    ─────────┼──────╱──────── 墙面
             |    ╱
             |  ╱
             |╱
   
   V 拆成两块:
   一块顺着 N(钻进墙里的分量)—— 要砍掉
   一块沿着墙面(滑动的分量)  —— 要保留

怎么拆?靠点积。速度 V 在法线 N 上的投影长度,就是 Dot(V, N)。这个值代表 V 有多少是朝着法线方向去的——也就是有多少在往墙里钻。

拆出来之后,把"钻墙"那部分从 V 里减掉,剩下的自然就是"沿墙滑"那部分:

// 把速度投影到墙面上,去掉钻进墙里的分量
static FixVector3 ProjectOnPlane(FixVector3 v, FixVector3 normal) {
    Fix dist = FixVector3.Dot(v, normal);   // v 有多少朝着法线方向
    return v - normal * dist;                // 减掉这部分,剩下的贴着墙面
}

就这三行,是整个滑动手感的心脏。normal * dist 是"钻墙"分量,v 减掉它,留下的就是平行于墙面的滑动速度。这个操作数学上叫"把向量投影到平面上",名字唬人,干的事就是"把往墙里去的劲儿抽掉"。

这里要提醒一句,Dot 内部是三个分量各自相乘再相加,每个乘法都是定点乘法,中间值会暴涨,前面反复讲过的那个 64 位防溢出的坑,这儿照样存在。你的 FixVector3.Dot 底层必须把中间乘积提到 long,不然大坐标下点积直接溢出,滑动方向瞬间乱套。

一次滑动的完整流程

有了投影,把移动逻辑重写一遍:

void Move(ref FixVector3 pos, ref FixVector3 velocity, Fix dt) {
    FixVector3 remaining = velocity * dt;      // 这帧还剩多少位移要走

    if (SweepHit(pos, pos + remaining, out var hit)) {
        // 走到撞墙点,先把撞之前那段路走完
        pos = hit.point;

        // 剩下没走完的位移,拍到墙面上,改方向继续滑
        FixVector3 leftover = (pos + remaining) - hit.point;
        remaining = ProjectOnPlane(leftover, hit.normal);

        // 顺便把速度本身也拍一下,不然下一帧又直直撞上来
        velocity = ProjectOnPlane(velocity, hit.normal);
    }

    pos += remaining;
}

流程说白了三步:走到撞墙那一点停下,把没走完的路折向墙面,顺着新方向把剩下的路走完。速度也得跟着拍一下,否则这帧滑走了,下一帧速度还是原来那个直冲墙的方向,又撞上,白忙活。

一次滑动不够,要能连着滑

上面这版有个隐患。你斜着撞墙滑走,滑着滑着可能又撞上另一堵墙——比如墙角。这时候你只做了一次投影就 pos += remaining 收工,那第二堵墙就漏检了,人会直接穿进墙角里。

所以真正的 Collide-and-Slide 是循环的:滑动之后再检测,又撞到就再滑,直到这帧的位移全部走完,或者撞够了次数为止。

void Move(ref FixVector3 pos, ref FixVector3 velocity, Fix dt) {
    FixVector3 remaining = velocity * dt;

    const int MAX_SLIDE = 4;                   // 最多滑 4 次,够用且防死循环
    for (int i = 0; i < MAX_SLIDE; i++) {
        if (!SweepHit(pos, pos + remaining, out var hit)) {
            pos += remaining;                  // 一路无阻,走完收工
            return;
        }

        pos = hit.point;                       // 走到这次的撞墙点
        FixVector3 leftover = (pos + remaining) - hit.point;
        remaining = ProjectOnPlane(leftover, hit.normal);   // 折向墙面
        velocity  = ProjectOnPlane(velocity, hit.normal);
    }

    // 滑够 4 次还在撞(墙角、夹缝),剩下的位移就不走了,防止卡进几何体
    pos += remaining;
}

MAX_SLIDE 这个上限很重要。墙角、V 字形夹缝这种地方,你滑向 A 墙又撞 B 墙,滑向 B 墙又撞 A 墙,不设上限能无限循环下去,一帧卡死。设个 4 次,既够处理绝大多数复杂几何,又能兜住死循环。撞满 4 次还在撞,说明玩家钻进了个特别刁钻的角落,这时候宁可让他停一下,也不能让游戏卡住。

为什么这套东西非定点不可

你可能想,滑动手感是表现层的东西,用 float 算算得了,反正玩家又看不出那点误差。

在竞技 FPS 里这想法要命。角色位置是权威状态,它要参与服务器校验、参与延迟补偿回放、参与命中判定。你在客户端用浮点滑出来一个位置,服务器用浮点滑出另一个略微不同的位置,两边一对不上,轻则位置回弹(玩家看着自己被拽回去,俗称橡皮筋),重则整局 desync。

而滑动这套逻辑里全是点积、向量减法、乘法——正好都是定点数逐比特一致的强项。同样的输入、同样的墙、同样的法线,红米和 iPhone 和服务器滑出来的位置,连最低位都一样。这样服务器校验才有意义,延迟补偿倒带才能精确复现"你当时到底贴在墙的哪个位置"。

这也是为什么滑动逻辑必须整个待在定点模拟层里,连那个 ProjectOnPlane 都不能图省事转 float 算。一处泄漏,满盘皆输——这话在这个系列里说过好几遍了,但每一处都是真金白银的教训。

几个手感上的延伸

基础的贴墙滑讲完了,顺带说几个让手感更真实的常见处理,不展开写代码,给你个方向:

地面和斜坡。走斜坡本质也是滑动,只不过法线朝上。你走上一个缓坡,速度被投影到坡面上,人就顺着爬上去了;坡太陡,投影完向上的分量太小,人就爬不动往下出溜——滑动这套逻辑天然就把斜坡处理了,不用额外写。

台阶。想让玩家能自动迈上小台阶而不是被矮墙挡住,得在撞到矮障碍时先试着抬高一点再往前探,探到了就踩上去。这叫 step offset,是 Collide-and-Slide 之上加的一层。

保留下落。人在空中斜撞墙,水平方向该被墙挡住并滑走,但竖直方向的重力下落不该被影响。所以有些实现会把水平滑动和竖直下落分开算,别让投影把重力也给抹了。

收尾

回到最开始那个问题:为什么撞墙不停死、要滑走?

因为玩家斜着撞墙时,他的运动里"往墙里钻"和"沿墙走"是两码事。停死是把两者一起毙了,粗暴。正确做法是用点积把速度沿墙面法线拆开,只砍掉钻墙那部分,保留沿墙那部分——这就是 ProjectOnPlane 那三行干的事。再套上一个"滑完再检测"的循环处理墙角,配一个次数上限防死循环,一套顺滑的角色移动就成了。

而这一切之所以能放心用,是因为底下全是定点数的点积和向量运算,逐比特一致,服务器和每个客户端滑出来的位置分毫不差。手感是玩家感受到的,确定性是玩家感受不到、但一旦缺了就满屏橡皮筋的东西。好的移动手感,就是这两样在底下悄悄咬合的结果。


内容概要:本文档是一份针对全国大学生电子设计竞赛(NUEDC)的“保姆级”实战指导手册,系统涵盖赛题解析与方案库、模块化代码与电路实现、以及测试报告范例三大核心部分。手册深入剖析了电赛七大赛题类别及其命题规律,强调“基本要求+发挥部分”的结构特点、指标逐年收紧趋势及测量与控制复合型题目的增加。通过数控直流电流源和频率特性测试仪两个典型案例,展示了从系统方案设计、关键器件选型到软硬件实现的完整路径。同时,提供了基于STM32 HAL库的ADC采样、PWM生成、OLED显示、无线通信等常用模块的详细电路原理与驱动代码,并辅以测试报告范例和评分标准解析,帮助参赛者规范撰写高质量设计报告。; 适合人群:参加全国大学生电子设计竞赛的本科生及指导教师,尤其适合有一定单片机和电路基础、希望在短时间内高效备赛并提升获奖概率的团队。; 使用场景及目标:①帮助参赛者快速掌握电赛命题规律与主流技术方案,精准应对电源类、控制类、仪器仪表类等高频赛题;②提供可复用的模块化代码与电路设计,加速硬件搭建与软件开发进程;③指导撰写符合评审标准的设计报告,强化误差分析与测试数据呈现,提升综合得分。; 阅读建议:建议按照“赛题分析→方案设计→模块实现→报告撰写”的流程顺序阅读,重点学习典型案例的整体设计思路与关键器件选型依据。对于代码与电路部分,应在实际开发板上动手验证,结合示波器、逻辑分析仪等工具进行调试。撰写报告时,务必参考文中测试表格与误差分析模板,确保数据完整、分析定量,避免因报告不规范而失分。;
内容概要:本文系统介绍了基于投资组合CVaR(条件风险价值)对象的金融投资组合优化方法,重点阐述了利用Matlab代码实现CVaR风险度量下的资产配置优化过程。相较于传统VaR仅衡量特定置信水平下的最大损失,CVaR进一步评估超出该阈值的平均尾部损失,具有更好的数学性质如凸性和次可加性,更适用于构建可优化的数学模型。文中详细讲解了CVaR优化模型的理论基础、目标函数设计、约束条件设置以及Matlab金融工具箱中PortfolioCVaR类的具体应用步骤,并结合实证案例演示了如何加载资产数据、设定预期收益率与风险偏好、执行优化求解及分析有效前沿,帮助投资者在控制极端下行风险的前提下实现最优资产配置。; 适合人群:具备一定金融工程、数量经济学或风险管理背景,熟悉Matlab编程环境,正在从事量化投资、资产配置建模、金融产品设计等相关工作的研究人员、高校师生及金融机构从业人员。; 使用场景及目标:①用于金融机构构建高阶风险管理导向的投资组合,提升对尾部风险的防控能力;②支持学术研究中对不同风险度量模型(如VaR与CVaR)在组合优化中表现差异的实证比较;③辅助教学实践中开展现代投资组合理论与高级风险控制技术相结合的编程实训课程。; 阅读建议:建议读者结合Matlab平台动手复现文中的代码示例,深入理解CVaR优化模型的构建逻辑与求解流程,并尝试调整资产数据、置信水平和约束条件以观察优化结果的变化,从而掌握其在真实投资决策中的灵活应用技巧。
标题基于SpringBoot的学生读书笔记共享平台设计研究AI更换标题第1章引言介绍学生读书笔记共享平台的研究背景、意义、国内外研究现状、论文方法以及创新点。1.1研究背景与意义阐述学生读书笔记共享平台在当前教育环境下的重要性。1.2国内外研究现状分析国内外学生读书笔记共享平台的研究进展与现状。1.3研究方法及创新点概述本文的研究方法与平台设计的创新点。第2章相关理论总结和评述与SpringBoot及读书笔记共享平台相关的理论。2.1SpringBoot框架介绍阐述SpringBoot框架的特点、优势及其在Web开发中的应用。2.2读书笔记共享平台相关理论介绍读书笔记共享平台的设计原则、功能需求及用户体验理论。2.3数据库设计与优化理论简述数据库设计的基本原则及优化策略。第3章平台设计详细介绍基于SpringBoot的学生读书笔记共享平台的设计方案。3.1平台架构设计平台的整体架构,包括前端、后端及数据库的设计。3.2功能模块设计阐述平台的主要功能模块,如用户管理、笔记上传、笔记分享等。3.3数据库设计介绍数据库的设计方案,包括表结构、索引及关系设计。第4章平台实现详细描述平台的具体实现过程,包括技术选型、开发环境搭建等。4.1技术选型与开发环境介绍开发平台所采用的技术栈及开发环境配置。4.2关键代码实现展示平台实现过程中的关键代码片段,如用户登录、笔记上传等功能的实现。4.3平台测试与优化平台的测试过程及优化策略,确保平台的稳定性和性能。第5章平台应用与分析对平台的应用效果进行分析,包括用户反馈、使用数据等。5.1用户反馈收集与分析收集用户反馈,分析用户对平台的满意度及改进建议。5.2使用数据分析通过数据分析工具,分析平台的使用情况,如用户活跃度、笔记分享量等。5.3对比方法分析对比其他类似平台,分析本平台的优势与不足。第6章结论与展望总结本文的研究成果,并对未来研究方向
上市公司人工智能技术应用水平主要用于衡量企业在人工智能技术研发、应用部署、业务融合以及战略布局方面的程度 学术界主要采用以下方法测度上市公司人工智能技术应用水平: 第一,人工智能专利测度法:基于企业技术创新产出视角,通过识别上市公司专利申请或授权信息中的人工智能相关专利,利用企业年度人工智能专利数量衡量其人工智能技术研发能力与技术积累水平 第二,年报文本分析法:基于企业信息披露视角,通过构建人工智能关键词词典,提取上市公司年度报告、管理层讨论与分析(MD&A)等文本中人工智能相关词汇出现频次,并对词频进行对数化处理,以衡量企业人工智能技术关注程度和应用水平 第三,机器人渗透度测度法:主要从智能化生产应用角度出发,利用行业层面的工业机器人安装密度,并结合企业所在行业特征、就业结构等信息,推算企业层面的自动化和人工智能技术渗透程度 第四,综合指数法:从人工智能投资、专利、关键词词频、机器人应用、人工智能项目等多维度构建指标体系,构建综合指数 第五,智能化投资测度法:基于人工智能软件投资额、人工智能硬件投资额之和占总资产的比例来衡量企业人工智能基础设施建设和技术应用水平 参考李果和白云朴(2024)、闫文影和陈雨生(2026)的研究思路,本文从企业人工智能技术实际投入角度衡量上市公司人工智能应用水平。具体而言,基于上市公司年度报告财务附注信息,通过关键词识别方法提取人工智能相关软件投资和硬件投资,并将二者加总形成企业人工智能投资规模,进一步以人工智能投资额占企业总资产的比例衡量企业人工智能技术应用水平 一、数据介绍 数据名称:上市公司人工智能技术应用水平 数据范围:上市公司企业 时间范围:2007-2025年 样本数量:78325条 数据来源:上市公司年报 二、数据指标 年份 股票代码 股票简称 行业名称 行业代码 省份
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

你一身傲骨怎能输

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值