PAYATHON 2026

Roblox ViewModel 在躯干快速移动时手臂抖动,如何解决?

数据小许

先修正渲染优先级,再关闭所有附加摆动进行对照。代码里有两处值得排查:真实手臂通过肩关节连接到 ViewModel,但位置由渲染回调中的 PivotTo() 驱动;UseOffset 还会将真实 Torso 的旋转变化叠加到 ViewModel 上。

仅凭这段代码无法确定唯一根因。一种更容易控制的实现方式是保留角色原有肩关节,使用独立的第一人称手臂模型,分别处理动画姿态和相机跟随。以下示例以原代码使用的 R6、Motor6D 为前提。

先修正渲染优先级

原代码写的是:

Enum.RenderPriority.Camera + 1

应改为:

Enum.RenderPriority.Camera.Value + 1

BindToRenderStep() 的优先级参数需要数值。使用默认相机脚本时,Camera.Value + 1 可以让 ViewModel 在默认相机更新之后更新。如果项目中还有自定义相机回调,需要按实际执行顺序安排。参见 Roblox 官方 RunService 文档源码。

按贴出的代码,缺少 .Value 是需要修正的错误。如果实际运行时仍能看到 ViewModel 更新,应核对正在执行的脚本是否与这里一致。

关闭附加效果,区分整体抖动与手臂抖动

暂时将第一人称分支替换为下面的最小逻辑,跳过原来的 offset、bobbing、ROTOFFSET 和 sway 计算结果:

if firstPerson then
    self.model.PrimaryPart = self.headPart
    self.model:PivotTo(cam.CFrame)
else
    -- Keep the existing third-person branch during this check.
end

观察抖动发生的位置:

  • 如果 ViewModel 的 Head、Torso 和手臂一起抖,优先检查相机、模型根部和其他修改位置的脚本。
  • 如果 Head、Torso 稳定,只有手臂相对 Torso 抖,优先检查肩关节、动画和跨模型连接。

只设置 UseOffset = false 不能关闭全部摆动,因为代码会转而启用另一套 bobbing;鼠标 spring 也仍然运行。

当前实现还通过 spring 和 Lerp() 平滑部分效果。如果希望旋转立即跟随,需要区分这些预期的缓动和不规则抖动。

为什么真实手臂的连接方式值得排查

这两行改变了真实角色关节的连接对象:

leftShoulder.Part0 = self.torsoPart
rightShoulder.Part0 = self.torsoPart

它们没有把手臂移动到 ViewModel 的对象层级中。真实手臂仍属于 Character,却通过关节连接到了相机下的另一个模型。

与此同时,PivotTo() 更新 ViewModel,角色动画仍可能更新肩关节姿态。Roblox 文档说明,Motor6D.Transform 在 PreSimulation 之后、物理步骤之前批量应用;Animator 通常在 PreAnimation 之后、PreSimulation 之前写入该属性。关节姿态更新和渲染阶段移动模型是不同环节,不能因为执行了 PivotTo(),就认定外部手臂也在同一次调用中完成了同步。参见 Roblox 官方 Motor6D 文档源码。

跨模型关节和更新时序因此值得排查,但目前没有运行结果证明它们是唯一原因。

使用独立手臂模型同步动画

可以将 ViewmodelRigs.ViewModel 配置为独立的 R6 展示模型,包含 Head、Torso、左右手臂和对应的肩关节。所有关节只能连接该 ViewModel 内的部件。

配置时注意:

  • 固定一个根部,例如 Head;由肩关节驱动的手臂保持未锚定。
  • 展示部件关闭 CanCollide、CanTouch 和 CanQuery。
  • Head 与 Torso 之间只保留所需的一套连接,避免模板已有连接时又重复创建 Weld。
  • 不让另一个 Animator 或脚本同时驱动同一组展示肩关节。

然后删除修改真实肩关节 Part0 的两行,在 _onCharacter() 获取真实肩关节之后加入姿态同步。下面假设模板已经包含左右手臂和 Motor6D 肩关节:

local vmTorso = self.torsoPart
local vmLeftShoulder = vmTorso:WaitForChild("Left Shoulder")
local vmRightShoulder = vmTorso:WaitForChild("Right Shoulder")
local vmLeftArm = self.model:WaitForChild("Left Arm")
local vmRightArm = self.model:WaitForChild("Right Arm")

assert(leftShoulder:IsA("Motor6D"))
assert(rightShoulder:IsA("Motor6D"))
assert(vmLeftShoulder:IsA("Motor6D"))
assert(vmRightShoulder:IsA("Motor6D"))

vmLeftShoulder.Part0 = vmTorso
vmLeftShoulder.Part1 = vmLeftArm
vmRightShoulder.Part0 = vmTorso
vmRightShoulder.Part1 = vmRightArm

-- Assumes the viewmodel matches the character's R6 proportions.
vmLeftShoulder.C0 = leftShoulder.C0
vmLeftShoulder.C1 = leftShoulder.C1
vmRightShoulder.C0 = rightShoulder.C0
vmRightShoulder.C1 = rightShoulder.C1

if self.poseConn then
    self.poseConn:Disconnect()
end

self.poseConn = RunService.PreSimulation:Connect(function()
    vmLeftShoulder.Transform = leftShoulder.Transform
    vmRightShoulder.Transform = rightShoulder.Transform
end)

这样,真实角色保留原有关节,ViewModel 只复制肩部动画姿态。相机跟随继续放在 Camera.Value + 1 的渲染回调中,通过 PivotTo() 移动完整的独立模型。不要把上面的 Transform 同步直接放到渲染回调里,再期待关节批处理立即执行。

第一人称中还要隐藏真实手臂、显示展示手臂;第三人称时反过来。原代码每帧将真实手臂的 LocalTransparencyModifier 设为 0,使用独立模型后需要调整这部分,并在销毁时恢复之前的值。

如果仍要复用真实手臂,应保存肩关节原来的 Part0,并在退出该模式或销毁 ViewModel 前恢复。该方案仍需验证动画、碰撞、Tool 连接和更新时序,重新指定 Part0 不能作为完整修复。

再检查 Torso 旋转偏移

当前 UseOffset = true,会读取:

local relative = torso.CFrame:ToObjectSpace(char.HumanoidRootPart.CFrame)
local rotX, rotY, rotZ = relative:ToOrientation()

这里的 torso 是真实角色的 Torso,因此真实躯干相对 HumanoidRootPart 的旋转变化会进入 ViewModel 的最终姿态。这可能把角色动画或其他脚本产生的变化带到第一人称画面中。

基础跟随稳定后,依次恢复固定偏移、肩部动画、ROTOFFSET、bobbing 和 sway。若只恢复 ROTOFFSET 就重新出现抖动,应检查其输入旋转是否连续,以及其他脚本是否也在修改 Torso。仅凭“主要发生在 X 和 Z 方向”,还不足以判定为某种轴向或欧拉角问题。

清理回调并核对模型配置

self.renderConn 保存的是一个空的 RenderStepped 连接,而 BindToRenderStep() 创建了单独的绑定。断开前者不会停止真正更新 ViewModel 的回调,应在重新绑定前和销毁时明确调用:

RunService:UnbindFromRenderStep("viewmodel")

新增的动画同步连接也需要在销毁模型前断开:

if self.poseConn then
    self.poseConn:Disconnect()
    self.poseConn = nil
end

原代码中设置 head.Anchored 和碰撞属性的部分已经被注释,实际配置取决于 Viewmodelr6 模板,不能据此认定这些属性已经正确设置。Init() 还在检查 Character 是否存在之前访问了它,应先取得 self.player.Character or self.player.CharacterAdded:Wait(),再获取 Humanoid。

这些修改分别针对代码错误、模型连接风险和排查干扰项。能否消除抖动,仍需在项目中以关闭附加效果的状态为基线进行验证。

备注:内容仅供参考。