Skip to content

3D 单位移动控制器与 Locomotion ​

本文档说明 SCE 3D 场景归一化、GameGraph 物理角色 motor、上层 UnitMovementController、以及客户端 locomotion 状态机如何串成一个可用闭环。

创建 3D 项目/场景、选择 SpaceModel、生成 3D 体素/导航/碰撞产物的编辑器流程,见 3D 场景创建、SpaceModel 与场景产物。

当前状态判断 ​

这套能力已经可以作为 3D 单位控制器的第一版公开入口使用,适合角色、怪物、玩家主控单位在 3D 场景中做寻路、物理角色移动、跳跃、空中追加跳跃和移动动画状态机。

需要同时说明边界:

能力状态说明
3D 场景空间模型可用GameDataScene.SpaceModel 区分 Surface2D 与 Volumetric3D。物理角色和多层 3D 导航应使用 Volumetric3D。
3D 场景产物可用修改 3D 地形、静态阻挡或导航内容后,需要重新生成 3D 场景产物。
3D 寻路移动可用VolumetricNavigation 保留单位寻路语义,不使用物理 capsule motor。
物理角色移动可用PhysicalCharacter 使用 KinematicCharacterMotor 胶囊角色,由 node 驱动 Unit transform。
跳跃/空中跳可用TryJumpUnit、SetUnitExtraAirJumpCount、JumpLaunchSpeed、ExtraAirJumps 已有高层入口。
Locomotion 动画可用GameDataUnitLocomotionAnimationProfile 驱动 idle / move / jump / rise / fall / land 状态机。
角色推动动态刚体无内建配置KCC 当前没有角色质量或自动推力语义;框架可能在后续评估基于接触的有限冲量,但尚未确定 API、行为或交付时间。
Rigidbody 单位模式保留项UnitMovementMode.Rigidbody 还不是公开可用的单位控制器模式。
Scripted 单位模式保留项需要显式脚本运动 owner,目前不要当作 SDK 推荐入口。

架构链路 ​

text
GameDataScene.SpaceModel
  -> SCE Scene Load / 3D nav / physics scene products
  -> GameDataUnit.MovementController
  -> UnitMovementController
  -> UnitNodeBinding(role: PhysicalCharacter, mode: NodeDriven)
  -> KinematicCharacterMotor
  -> UnitMovementSignal
  -> GameDataUnitLocomotionAnimationProfile
  -> 客户端模型 locomotion 状态机

核心原则是:玩法仍以 Unit 为主,物理角色只接管连续位移。生命值、技能、AI、目标过滤、视野、玩家主控等仍走 Unit 系统;当进入 PhysicalCharacter 模式时,底层由绑定的 GameGraph node 和 KinematicCharacterMotor 写回 Unit 坐标。

3D 场景前置条件 ​

本节只列出 Unit 控制器需要的场景前置条件。编辑器里的完整创建与生成流程见 3D 场景创建、SpaceModel 与场景产物。

SpaceModel ​

GameDataScene.SpaceModel 决定场景移动语义:

SpaceModel语义适用场景
Surface2D2.5D 单层可走面传统俯视角、地形有高度但同一 XY 只有一层可走面。
Volumetric3D真 3D 空间模型多层地表、桥洞、高低平台、物理角色、3D 导航。

物理角色模式只应在 Volumetric3D 场景中启用。若在 Surface2D 场景调用 ActivateUnitPhysicalCharacterMovement,控制器会拒绝切换并记录失败原因。

3D 场景产物 ​

3D 场景依赖导航、体素、静态碰撞等生成产物。修改以下内容后,需要在编辑器中重新生成 3D 场景产物:

  • 3D 地形高度或尺寸
  • 静态阻挡、可走面、碰撞角色
  • Nav3D 相关内容
  • 会影响物理角色落地、台阶或坡度判断的静态场景内容

调试时可以打开 DebugOverlayMode.Nav3DMesh、Nav3DVoxels、PhysicsWorld 或 MotorProbe 检查导航网格、体素、物理世界和 character motor。需要逐帧比较着地、速度与碰撞命中时,通过 Runtime MCP 用 debug.overlay 订阅 MotorProbe,再用 motor.snapshot 读取服务端物理帧序列;完整参数与状态语义见Runtime MCP 调试指南。

推荐数据配置 ​

优先通过 GameDataUnit.MovementController 声明单位移动控制器。GameSparkCore 已提供通用 3D 控制器预设:

csharp
// gamesparkcore.ScopeData 通常通过 csproj 全局别名为 GSC。
_ = new GameDataUnit(MyLinks.Hero)
{
    UpdateFlags = new UpdateFlags
    {
        Walkable = true,
        Turnable = true,
        AllowMover = true,
    },
    MovementController = GSC.GameDataUnitMovementController.Generic3DMovementController,
    Properties = new UnitPropertyInitialData
    {
        { GSC.GameDataUnitProperty.MoveSpeed, 300 },
        { GSC.GameDataUnitProperty.JumpLaunchSpeed, 650 },
        { GSC.GameDataUnitProperty.ExtraAirJumps, 1 },
    },
};

如果需要自定义胶囊尺寸、坡度、台阶或碰撞掩码,可以创建自己的 GameDataUnitMovementController:

csharp
public static readonly GameLink<GameDataUnitMovementController, GameDataUnitMovementController> HeroMovement =
    new("HeroMovement"u8);

_ = new GameDataUnitMovementController(HeroMovement)
{
    InitialMode = UnitMovementMode.VolumetricNavigation,
    ApplyInitialMode = true,
    ExtraAirJumpsProperty = GSC.GameDataUnitProperty.ExtraAirJumps,
    PhysicalCharacter = new UnitPhysicalCharacterMovementSettings
    {
        EnsureUnitNodeBinding = true,
        EnsureStaticMeshColliders = true,
        EnsureTerrainCollider = false,
        UseCharacterCollisionRoles = false,
        Radius = 40f,
        Height = 180f,
        StepHeight = 45f,
        MaxSlopeDegrees = 45f,
        CollisionLayer = 1,
        CollisionMask = uint.MaxValue,
    },
};

_ = new GameDataUnit(MyLinks.Hero)
{
    MovementController = HeroMovement,
    UpdateFlags = new UpdateFlags { Walkable = true, Turnable = true, AllowMover = true },
};

InitialMode 推荐先用 VolumetricNavigation,在需要跳跃或物理角色手感时由服务器切换到 PhysicalCharacter。如果项目明确从出生开始就是物理角色,也可以设为 PhysicalCharacter,但场景必须是 Volumetric3D 且 3D 场景产物完整。

运行时控制 ​

移动模式切换、跳跃、空中跳次数修改都必须由服务端权威执行。客户端可以读状态、发请求、做 UI,但不要直接切换模式。

csharp
#if SERVER
using TriggerEncapsulation;

var unit = player.MainUnit;
if (unit == null)
{
    return;
}

// 切到物理角色模式。若 GameDataUnit.MovementController 已配置,会复用其中的物理角色参数。
var activated = unit.ActivateUnitPhysicalCharacterMovement();
if (!activated)
{
    var controller = unit.GetUnitMovementControllerInfo();
    Game.Logger.LogWarning("切换物理角色失败: {Reason}", controller?.LastTransitionFailureReason);
}

// 请求跳跃。launchSpeed 为空时使用 JumpLaunchSpeed 属性或默认值。
var jumped = unit.TryJumpUnit();
if (!jumped)
{
    var controller = unit.GetUnitMovementControllerInfo();
    Game.Logger.LogWarning("跳跃失败: {Reason}", controller?.LastJumpFailureReason);
}

// 允许 1 次额外空中跳跃。
unit.SetUnitExtraAirJumpCount(1);

// 切回当前场景适用的寻路模式:Surface2D 场景回 SurfaceNavigation,Volumetric3D 场景回 VolumetricNavigation。
unit.ReturnUnitToSceneNavigation();
#endif

常用查询:

csharp
using TriggerEncapsulation;

var mode = unit.GetUnitMovementMode();
var controller = unit.GetUnitMovementControllerInfo();
var signal = unit.GetUnitMovementSignal();

var isPhysical = mode == UnitMovementMode.PhysicalCharacter;
var canJump = unit.CanUnitJump();
var grounded = unit.IsUnitMovementGrounded();
var remainingAirJumps = unit.GetUnitRemainingAirJumps();

输入与移动意图 ​

常规玩家控制仍推荐使用 GameSystemUI 的默认移动控件和主控单位绑定。移动控件记录的是单位移动意图,服务端根据当前 movement mode 选择后端:

  • SurfaceNavigation:走 2.5D 寻路/Walkable。
  • VolumetricNavigation:走 3D 导航移动。
  • PhysicalCharacter:Host 会把 vector move 路由给 KinematicCharacterMotor,由 motor 处理重力、着地、台阶和坡度。

如果只是想读取输入控制相机、载具或纯 SceneGraph 节点,不要绑定 MainUnit,改用 MoveControlBase.OnMoveInput。详见 GameSystemUI 移动控件。

底层物理角色 motor ​

PhysicalCharacter 模式底层使用 KinematicCharacterMotor:

参数/方法说明
Radius / Height胶囊半径和总高度。
StepHeight可自动跨上的最大台阶高度。
MaxSlopeDegrees可站立/行走的最大坡度。
Gravity可选自定义重力;为空时使用场景物理世界重力。
CollisionLayer / CollisionMask胶囊碰撞层和碰撞掩码。
Move(Vector3 velocity)提交期望速度。Unit 控制器会自动调用,一般不用业务手写。
Jump(float launchSpeed)给 motor 垂直跳跃初速。推荐通过 TryJumpUnit 调用。
Warp(Vector3 position)物理角色瞬移。业务通常优先使用 Unit 的传送语义。

直接使用 unit.UsePhysicalCharacterMovement(...) 属于较低层 GameGraph/Node API。普通单位玩法优先用 GameDataUnit.MovementController 和 unit.ActivateUnitPhysicalCharacterMovement(...),这样 Unit movement signal、空中跳、客户端状态同步和 locomotion 才会一起生效。

UseCharacterCollisionRoles 当前默认关闭。需要按静态碰撞角色区分 ground/blocker 时再打开,并确保场景的碰撞角色产物已生成且经过项目验证。

KinematicCharacterMotor 不等于 Kinematic RigidBody ​

KinematicCharacterMotor 名称中的 Kinematic 表示“由控制器按移动意图驱动的运动学角色”,不表示角色绑定 Node 上存在 RigidBody.Kinematic = true。它底层使用 KCC ghost object 和胶囊形状完成 sweep、接触、台阶、坡度、重力与着地处理:

  • 它不是参与刚体动力学求解的 RigidBody,因此没有可配置的真实 Mass。
  • 它可以被静态或动态碰撞体阻挡,但当前不会自动按角色质量向动态刚体施加冲量。
  • 不要在 PhysicalCharacter 的绑定 Node 上再添加 Kinematic RigidBody 和第二套碰撞形状。这样会造成重复接触、自碰撞或双重位移所有权。
  • UnitMovementMode.Rigidbody 是另一种尚未公开实现的移动模式,不能把它与当前 PhysicalCharacter / KCC 混为一谈。

框架今后可能评估由 UnitMovementController 在 KCC contact / sweep 命中动态刚体后施加有限冲量,但这只是后续方向,不代表确定的公开 API、参数语义或发布时间。项目当前不应依赖该能力做玩法排期。

临时 PushProxy Node ​

项目现在确实需要“角色推箱子”时,可以自行实现一个 PushProxy Node。PushProxy 只是建议的项目侧命名,不是 SDK 内置类型:

text
PhysicalCharacter Binding Node
├── KinematicCharacterMotor       # KCC,负责角色运动和场景阻挡
└── PushProxy Node                # 独立的 server-local 子节点
    ├── Kinematic RigidBody
    └── Box / Capsule CollisionShape
  1. 在 PhysicalCharacter Binding Node 下创建独立的 server-local PushProxy 子节点。不要把 Kinematic RigidBody / CollisionShape 直接添加到 Binding Node 本身,也不要尝试复用 motor 内部的胶囊形状。
  2. 给 proxy 添加小型盒或胶囊 CollisionShape 与 Kinematic RigidBody。它会继承 Binding Node 的角色 transform;项目可通过 proxy 的 local transform 调整推动区域和朝向。
  3. 使用独立 collision layer / mask,让 proxy 只接触项目指定的可推动动态刚体,并排除角色 KCC、静态场景、trigger 和其他不应推动的对象。
  4. proxy 不需要复制给客户端;被推动物体仍应使用 replicated Node + server-local RigidBody,由客户端在同一 replicated Node 下挂 local 表现。
  5. 角色离开物理模式、死亡、传送、切场景或销毁时,同步停用、warp 或销毁 proxy,避免留下不可见碰撞体。

Kinematic proxy 对动态物体表现得接近“无限质量推动者”,并不等价于角色具有有限质量。proxy 体积过大、单帧位移过远、与静态场景发生接触或同时挤压多个刚体时,可能产生穿透、抖动或过强推力。应限制 proxy 尺寸和每帧位移,并针对轻重物体、墙角挤压、台阶、跳跃、传送和不同服务器帧率做专项回归。需要严格控制最大推力时,项目应改为查询目标 RigidBody 后调用 ApplyImpulse(...) / ApplyImpulseAtPosition(...),自行限制冲量,而不是依赖 Kinematic proxy 的接触求解。

Locomotion 动画状态机 ​

GameDataUnitLocomotionAnimationProfile 用来定义客户端模型的移动动画状态机。它不是服务端物理的一部分,而是消费 UnitMovementSignal 与客户端位置采样,驱动模型播放:

  • Idle
  • Move
  • JumpStart
  • AirJumpStart
  • AirRise
  • AirFall
  • Land

示例:

csharp
public static readonly GameLink<GameDataUnitLocomotionAnimationProfile, GameDataUnitLocomotionAnimationProfile> HeroLocomotion =
    new("HeroLocomotion"u8);

_ = new GameDataUnitLocomotionAnimationProfile(HeroLocomotion)
{
    Idle = Clip("anim/human/idle_01.ani", looping: true),
    Move = Clip("anim/human/run_01.ani", looping: true),
    JumpStart = Clip("anim/human/jump_start.ani", looping: false),
    AirJumpStart = Clip("anim/human/air_jump.ani", looping: false),
    AirRise = Clip("anim/human/air_rise.ani", looping: true),
    AirFall = Clip("anim/human/air_fall.ani", looping: true),
    Land = Clip("anim/human/land.ani", looping: false),
    EnterMoveSpeed = 20f,
    ExitMoveSpeed = 5f,
    FallSpeedThreshold = -50f,
    MinAirTimeForLand = TimeSpan.FromMilliseconds(120),
    LandSpeedThreshold = -120f,
    InterruptLandOnMove = true,
    MinLandTimeForMoveInterrupt = TimeSpan.FromMilliseconds(80),
    LandMoveInterruptSpeed = 20f,
    MoveSpeedScaleMode = UnitLocomotionAnimationSpeedScaleMode.HorizontalSpeed,
    MovementAnimationSpeedRatio = 0.003f,
};

static UnitLocomotionAnimationClip Clip(string animation, bool looping) => new()
{
    Animation = animation,
    LogicLayer = AnimationLogicLayer.Rollback,
    Priority = 2,
    BodyPart = AnimationBodyPart.FullBody,
    IsLooping = looping,
    BlendIn = TimeSpan.FromMilliseconds(120),
    Speed = 1f,
};

把 profile 挂到模型数据上:

csharp
_ = new GameDataModel(MyLinks.HeroModel)
{
    UnitLocomotionAnimationProfile = HeroLocomotion,
    MovementAnimationSpeedRatio = 0.003f,
};

也可以在客户端手动应用:

csharp
#if CLIENT
unit.UseUnitLocomotionAnimation(HeroLocomotion);
#endif

动画优先级有效范围是 0 到 31。locomotion 的 idle / move / jump 通常使用较低优先级,例如 2;普攻、技能、受击等短动作使用更高优先级覆盖。

测试模板参考 ​

map_templates/unit_movement_3d_template 中的 PhysicalCharacterMotorTest.cs 是当前 3D 单位控制器联调模板,覆盖:

  • 物理角色 / 3D 寻路模式切换
  • Space 跳跃
  • 额外空中跳跃次数调整
  • 移动速度调整
  • 服务端状态快照回传
  • 向下 raycast 地面探测
  • Nav3DMesh、Nav3DVoxels、PhysicsWorld、MotorProbe 调试 overlay
  • 项目侧 patch Controlled3DHeroSample 的 locomotion profile

该模板当前仍是内部/测试入口,不应在文档里当作普通公开模板推荐给最终用户。写项目代码时可以参考其中的 API 用法,但正式数据仍应放在项目自己的 GameData 初始化或数据编辑器源数据里。

常见问题 ​

切换物理角色失败 ​

先检查:

  • 当前场景是否为 SceneSpaceModel.Volumetric3D
  • 是否重新生成过 3D 场景产物
  • GameDataUnit.UpdateFlags.Walkable 是否启用
  • 是否在服务端调用
  • unit.GetUnitMovementControllerInfo()?.LastTransitionFailureReason

能跳但落不到地面 ​

先打开 DebugOverlayMode.PhysicsWorld 和 MotorProbe。如果没有静态碰撞或地形碰撞,角色会一直下落。EnsureStaticMeshColliders 默认应保持 true;如果依赖地形 collider,再打开 EnsureTerrainCollider 并验证场景资产。

偶发问题不要只看某一帧画面。用 Runtime MCP 连续读取 motor.snapshot,对照 grounded、selected_ground.normal、velocity、forward_sweep 和 penetration_contact。如果只在替换碰撞根的物理帧中断,优先检查碰撞提交顺序;稳定静态盒也持续失败时,再进入 KCC 算法排查。

客户端动画没有进入跳跃/落地 ​

检查:

  • 模型是否配置 UnitLocomotionAnimationProfile
  • profile 的动画路径是否存在
  • 客户端 Unit 是否能读到 GetUnitMovementSignal()
  • 是否使用了过高优先级的手动动画长期覆盖 locomotion
  • JumpSequence、GroundedChangeSequence 是否随跳跃和落地变化

物理角色移动时普通寻路行为不一致 ​

这是预期差异。PhysicalCharacter 是 node-driven 物理角色,能处理重力、台阶、坡度和跳跃;VolumetricNavigation 是 3D 导航移动,不会使用胶囊 motor。需要跳跃和平台手感时使用物理角色;需要自动找路和 AI 路径时使用 3D 导航。

API 速查 ​

API运行端用途
GameDataUnit.MovementController双端数据为单位声明移动控制器配置。
GameDataUnitMovementController双端数据配置初始模式、额外空中跳属性和物理角色参数。
UnitMovementMode双端SurfaceNavigation、VolumetricNavigation、PhysicalCharacter 等模式枚举。
unit.ActivateUnitPhysicalCharacterMovement(...)服务端切换到物理角色模式。
unit.ReturnUnitToSceneNavigation()服务端返回当前场景适用的寻路模式。
unit.TryJumpUnit(...)服务端请求物理角色跳跃。
unit.SetUnitExtraAirJumpCount(...)服务端设置额外空中跳跃次数。
unit.GetUnitMovementSignal()双端读取移动模式、速度、跳跃序列、着地序列等状态。
unit.GetPhysicalCharacterMovement()双端读取底层物理角色 handle。
GameDataUnitLocomotionAnimationProfile客户端表现数据配置模型 locomotion 状态机。
unit.UseUnitLocomotionAnimation(...)客户端手动应用或刷新 locomotion profile。