← ALL ARTICLES

Character Controller

全库目录221
本文目录9

把玩家当普通刚体推,接触响应未必符合操作意图。Character Controller以胶囊扫掠寻找最早碰撞,再沿接触面滑动;台阶、坡面和移动平台分别定义规则。位移和碰撞的单位必须一致。

扫掠与滑动

常见角色控制器使用胶囊做扫掠,而不是把玩家当普通动态刚体。一次位移会先检测最早碰撞,再把剩余位移投影到接触平面形成滑动。跨台阶需要额外尝试“向上、向前、向下”三个阶段,并检查台阶高度和落点。

坡度限制通过地面法线与 Up 的夹角判断可站立区域。贴地检测需要容忍很小的间隙,但安全间距过大又会让角色悬浮。移动平台要明确继承平台位移还是速度,并处理平台旋转。

扫掠与滑动循环

cpp
Vec3 MoveWithCollision(Vec3 position, Vec3 displacement)
{
    for (int i = 0; i < maxSlideIterations && !IsNearlyZero(displacement); ++i) {
        SweepHit hit;
        if (!SweepCapsule(position, displacement, hit))
            return position + displacement;           // 全程无阻挡
        float distance = Length(displacement);
        float safeFraction = Max(0.0f, hit.fraction - skinWidth / Max(distance, 1e-6f));
        position += displacement * safeFraction;
        Vec3 remaining = displacement * (1.0f - hit.fraction);
        displacement = remaining - hit.normal * Min(0.0f, Dot(remaining, hit.normal));
    }
    return position;                                  // 迭代耗尽:停在夹角处
}

这是单平面滑动的算法伪代码。扫掠返回比例 hit.fraction,skinWidth 是世界空间长度,必须除以位移长度后才能相减。接触法线需归一化,初始穿透要先单独处理。

每次碰撞后将剩余位移投影到接触平面,再做下一次扫掠。本片段用碰撞比例计算剩余位移,安全间距会使一次移动保守缩短;需要精确保留切向距离时,必须连同接触偏移一起定义剩余位移。完整控制器还需保留多个接触平面,在两面夹角沿交线移动,在封闭角落停止。用不同位移长度推进同一墙面,确认安全间距始终相同,再测试楔形墙角和 90 度内角。

跳跃初速度可由目标高度 h 推出:

v0=2ghv_0=\sqrt{2gh}

这里 g 使用重力加速度的正值。为了手感,起跳和下落可以使用不同重力倍率,但要把它当玩法规则,而不是物理常量。

坡面、台阶与平台

地面法线n和单位向上轴u的点积给出坡度余弦。设最大可站立角为θ,dot(n,u)>=cos(theta) 才把接触记成可站立地面。垂直墙不能仅因胶囊接触就恢复跳跃资格;陡坡的横向滑动与重力滑落由玩法规则分别处理。

台阶尝试先向上扫掠不超过最大台阶高,再水平前进,最后向下寻找可站立落点。三次查询都保留胶囊体积;只射一条向下射线会漏掉头顶和身体侧面的阻挡。任一步受阻或落点超坡度上限,就退回普通滑动结果。

角色站在旋转平台上时,把脚点先变到平台上一帧的局部空间,再用当前平台矩阵变回世界空间,两位置之差就是应继承的平台位移。只加平台质心平移会漏掉旋转造成的切向运动。离开平台时是否继承速度由跳跃规则确定,并避免与角色自己的积分重复。

单面滑动只移除朝向障碍内部的分量:remaining -= n * min(0,dot(remaining,n)),其中n从障碍指向角色。接触法线若表示反方向,符号也随之改变。多个接触面需要共同限制剩余位移,不能在两面之间反复投影而越过已有接触。

多面接触与平台位移

只有两个接触面同时阻挡候选位移,且单面投影会重新进入另一面时,才将两者记为活动约束。候选位移离开两面时直接保留;仅朝向一面内部时先做单面投影并复查另一面。两个活动约束的单位法线n1、n2线性独立时,可求单位交线方向c=normalize(cross(n1,n2)),候选位移取c*dot(d,c),再检查它是否进入任一已有接触面。两法线近乎平行时不能归一化零叉积,沿单面处理;第三个独立平面封住交线时停止。该规则针对局部平面约束,弧形胶囊与动态障碍仍需每次重新扫掠。

例如平台绕世界Y轴旋转90度,脚点原来距轴1米,平台局部点(1,0,0)随平台变到(0,0,-1),继承位移为(-1,0,-1)。只加平台中心平移得到零,会让平台从角色脚下转走。角色自身移动随后在更新后的平台位置执行,离台速度规则另行应用,避免同一位移加两次。

单步提交与失效恢复

一个固定步先保存步前胶囊位置P0、脚点F0与所属平台上一帧变换T0,用fLocal=inverse(T0)*F0保留同一附着点。平台更新到T1以后,目标脚点为Fcarry=T1*fLocal,目标携带姿态的中心取Pcarry=Fcarry+(P0-F0);竖直角色胶囊保留世界Up,不把平台横滚直接应用到胶囊轴。先算出这个目标,但此时不从P0立即扫掠,因为已移动的平台可能使P0处于穿透状态。

在当前碰撞世界按下一段规则恢复初始穿透,得到无穿透位置Precovered;失败时停止本步的携带、输入与台阶查询,进入安全恢复分支。恢复成功后只扫掠剩余携带位移Pcarry-Precovered,以滑动约束取得Pcarried。恢复已经完成的那部分位移不会再次加入。例如忽略skin,角色中心原为高度1米,平台上移1米,目标中心是2米;初始恢复已经将中心推到2米,剩余携带位移是0,再加原平台增量会错误地到3米。

携带扫掠不能穿过静态墙;被夹住时保留可行位置并报告挤压状态。从Pcarried处理输入、跳跃、重力、普通滑动和台阶,最后提交一份最终胶囊位置。跳离时只把所选平台速度加进角色自身初速度一次,离台后清除平台身份;仍附着时平台携带位移单独处理,不从包含平台运动的世界实际速度再次积分携带。最终接地查询确认实际接触的平台,并用最终脚点更新其局部附着点,避免下一步继续拉向已经被墙阻挡的旧目标。

初始穿透查询返回宿主物理系统的最小平移方向n与深度d,候选位置沿n移动d+skinWidth后重新查询全部重叠;每步最多执行固定次数,例如4次。多物体重叠可能使修正来回震荡,达到上限仍穿透就把最近保存的无穿透位置拿到当前碰撞世界重新查询,仍安全才回退;动态墙体或平台可能使旧安全点也失效,此时保持停滞并请求一个经过完整胶囊查询的传送恢复点。保存时有效不能替代当前世界中的有效性。不能在仍穿透状态下把比例为零的扫掠当作正常碰撞继续投影。

普通滑动和台阶都从同一个完成初始恢复与剩余平台携带后的Pcarried起点计算候选,查询不直接提交位置。台阶候选必须完成向上、水平、向下三次胶囊查询,落点可站立且水平进度大于普通滑动结果,才选台阶;否则提交普通滑动结果。这样向上成功而水平失败时不会把角色永久抬高。下降查询忽略起跳阶段,防止刚设置正向上速度就被贴地拉回。

可站立坡面把地面输入方向投到坡面切向,并按玩法选择是否保持原输入速度;例如保持速度时先投影再归一化,投影近零则取零。陡坡不提供地面身份。把Up投到接触面得到h0=u-n*dot(u,n),h0长度近零时按普通接触处理;否则归一化为上坡方向h。输入切向d若dot(d,h)>0则减去h*dot(d,h),只保留下坡和横坡分量。重力沿接触面滑落;墙面法线不能恢复地面跳跃权限。

最终位置确定以后分别保存穿透恢复、平台携带和角色主动移动三段位移。各段位移除以步长得到本步平均运动,供动画、路线进度与阻塞诊断使用;穿透纠偏单独标记。请求速度仍为每秒3米而实际主动位移为零时,跟随器据此观察到阻塞。

制动距离、加速度约束和停止判断读取接触响应后的实际步末速度。控制器另保存步末世界速度、有效支撑身份和该接触点的支撑速度,相减得到相对支撑的步末速度;旋转支撑按下一时刻的朝向搬运旧相对速度,再比较主动变化。连续加速或制动期间,位移平均值与步末速度分别反馈。只提供位移的接口将步末速度标为未知,在取得有效速度前由原控制器保持制动或报告无法确认。换算与0.15米/秒平均速度、0.2米/秒步末速度的数例见移动支撑与速度反馈。

验证方法

楔形内角、台阶、斜坡与平台旋转分别测试。记录扫掠比例和skin宽度,不可达目标限迭代,初始穿透单独解出。与固定步长参考比较跳跃高度。

实现与证据

2026-10-22,以本机 Python 3.11 运行 python 知识库/examples/revision_16_21/test_reference.py,17项检查通过。参考实现与测试输入可复现本页注明的机制。 本页只检查单面滑动保留切向和离开分量。胶囊扫掠、台阶与旋转平台依赖宿主物理查询,尚未接入运行。

相关主题

参考资料