场景镜头
场景镜头是一段由锚点、输入策略和有序关键帧组成的声明式时间轴。客户端会在每个渲染帧采样镜头位置,不接收任意脚本代码,也不需要服务端每 Tick 推送镜头位置。已有场景无需迁移,会自动使用新的平滑轨迹。
服务端文件放在 plugins/ChaEngine/camera/scenes/,支持子目录;场景 ID 是相对于该目录且不含 .yml 的路径。
完整配置
# plugins/ChaEngine/camera/scenes/intro.yml
anchor:
type: player # player、entity 或 world
tracking: live # live 持续跟随;snapshot 固定开始时状态
input:
perspective-locked: true # 播放时禁止玩家主动切换视角
look-mode: locked # player、camera_orbit 或 locked
movement-mode: block # allow 或 block
movement-reference: player_body # player_body 或 camera_yaw
recenter-mode: none # none、immediate 或 smooth
visibility:
render-player: true # 是否显示本地玩家模型
render-hand: false # 是否显示第一人称手部
hud: hide # keep 保留 HUD;hide 隐藏 HUD
skippable: true # true 时玩家可按 Esc 跳过
restore:
mode: previous # 当前请使用 previous;original 暂按 previous 处理
duration-ms: 500 # 旧配置回退值;新配置由 transitions.exit 接管
transitions:
enter:
duration-ms: 600 # 从当前画面移动到第一关键帧
bezier: [0.25, 0.1, 0.25, 1.0]
exit:
duration-ms: 500 # 正常播放结束或 stop 后的恢复衔接
bezier: [0.25, 0.1, 0.25, 1.0]
interrupt:
duration-ms: 300 # 玩家跳过或 interrupt 时的恢复衔接
bezier: [0.25, 0.1, 0.25, 1.0]
keyframes:
- name: establish # 关键帧唯一名称
hold-ms: 400 # 到达后停留时间
position: [0.0, 2.0, 6.0] # 相对于锚点的 X、Y、Z 位置
rotation: [180.0, 8.0, 0.0] # yaw、pitch、roll,单位为度
fov: 70.0 # 视野角,范围 1~170
collision: clamp # clamp 防穿墙;ignore 忽略碰撞
cinematic: [0.12, 0.12, 0.0, 0.0] # [上, 下, 左, 右] 黑边比例
- name: approach
transition-ms: 1600
hold-ms: 600
position: [1.2, 1.4, 3.0]
rotation: [165.0, 3.0, 5.0]
fov: 55.0
bezier: [0.42, 0.0, 0.58, 1.0]
collision: ignore
# 未填写 cinematic,自动继承 establish 的黑边cinematic 数组固定使用 [top, bottom, left, right],即 [上, 下, 左, 右],每个值必须是 0~0.5 的有限数。第一关键帧省略时从全零开始;后续关键帧省略时继承上一帧。黑边在关键帧之间平滑变化,不会因为窗口尺寸或分辨率改变而使用过期像素值。普通 HUD 位于黑边下方,聊天和字幕仍显示在黑边上方。
显式配置 transitions.enter 后,第一关键帧可省略 transition-ms 与 bezier,进入段只计算一次。旧场景仍可继续把第一段写在第一关键帧,并用 restore.duration-ms 控制恢复;新旧字段同时存在时,以 transitions 为准。
锚点类型
| 类型 | 额外字段 | 适用场景 |
|---|---|---|
player | 无 | 围绕正在观看场景的玩家播放 |
entity | dimension、entity-uuid | 跟随指定实体;实体消失或维度不匹配时中止 |
world | dimension、position、rotation | 以固定世界坐标和朝向播放 |
实体锚点示例:
anchor:
type: entity # 跟随指定实体
tracking: live # 每帧读取实体当前位置
dimension: "minecraft:overworld" # 实体所在维度
entity-uuid: "00000000-0000-0000-0000-000000000001" # 实体 UUID世界锚点示例:
anchor:
type: world # 固定世界坐标锚点
tracking: snapshot # 世界锚点通常保持固定
dimension: "minecraft:overworld" # 锚点所在维度
position: [120.5, 70.0, -35.5] # 世界 X、Y、Z
rotation: [90.0, 0.0] # 锚点 yaw、pitchlive 会持续读取玩家或实体的位置和朝向,适合跟随移动目标;snapshot 在开始时固定锚点,适合相对于开场位置播放。
关键帧与时间
- 第二帧及后续帧的
transition-ms是从上一关键帧移动到当前关键帧的时间。 hold-ms: 0表示镜头连续穿过当前关键帧,不会在该点刻意减速至静止。hold-ms大于0时,镜头会平滑减速并准确停在当前关键帧,停留结束后再平滑启动。- 第一帧由
transitions.enter从玩家当前看到的画面平滑进入,不会先瞬切到目标位置。 - 位置、旋转和 FOV 会在相邻关键帧间生成连续轨迹,yaw 与 roll 会走跨越 -180/180 的最短角路径,FOV 不会越过相邻关键帧给出的范围。
- 时间轴按渲染 FPS 更新;
live锚点仍使用游戏的partialTick渲染插值跟随玩家或实体,因此镜头轨迹和移动锚点都不会被 20 TPS 的更新频率限制。 bezier仍控制每段移动的快慢节奏。建议 Y 控制点保持在0~1;如果故意写成非单调曲线,镜头可能在该段内短暂倒退。collision: clamp会在镜头接近墙面时缩短最终可见距离,因此实际画面可能偏离理想曲线;障碍消失后会继续按当前时间轴位置渲染,不会把碰撞结果带入后续关键帧。- 场景至少需要 1 个、最多 512 个关键帧,总时长最多 30 分钟。
- 所有数值必须是有限数,贝塞尔曲线的第 1、3 个值必须在 0~1 范围内。
连续穿过多个关键帧
下面的场景会连续穿过 middle,只在最后的 finish 停下。三个关键帧方向一致时,镜头经过中间点不会出现突然减速或方向折断。
# plugins/ChaEngine/camera/scenes/smooth-pass.yml
anchor:
type: player
tracking: snapshot
input:
perspective-locked: true
look-mode: locked
movement-mode: block
movement-reference: player_body
recenter-mode: none
visibility:
render-player: true
render-hand: false
hud: hide
skippable: true
restore:
mode: previous
duration-ms: 500
keyframes:
- name: start
transition-ms: 0
hold-ms: 0
position: [0.0, 2.0, 8.0]
rotation: [180.0, 5.0, 0.0]
fov: 70.0
bezier: [0.0, 0.0, 1.0, 1.0]
collision: clamp
- name: middle
transition-ms: 1200
hold-ms: 0
position: [2.0, 2.4, 5.5]
rotation: [165.0, 3.0, 2.0]
fov: 62.0
bezier: [0.42, 0.0, 0.58, 1.0]
collision: clamp
- name: finish
transition-ms: 1200
hold-ms: 600
position: [4.0, 2.0, 3.0]
rotation: [150.0, 0.0, 0.0]
fov: 55.0
bezier: [0.42, 0.0, 0.58, 1.0]
collision: clamp在指定关键帧平滑停下
下面的场景会在 inspect 准确停留 800 毫秒,再移动到 leave。适合展示 NPC、建筑或任务目标。
# plugins/ChaEngine/camera/scenes/smooth-stop.yml
anchor:
type: player
tracking: live
input:
perspective-locked: true
look-mode: locked
movement-mode: block
movement-reference: player_body
recenter-mode: none
visibility:
render-player: true
render-hand: false
hud: keep
skippable: true
restore:
mode: previous
duration-ms: 500
keyframes:
- name: inspect
transition-ms: 1400
hold-ms: 800
position: [1.5, 2.2, 4.0]
rotation: [170.0, 4.0, 0.0]
fov: 48.0
bezier: [0.25, 0.1, 0.25, 1.0]
collision: clamp
- name: leave
transition-ms: 1000
hold-ms: 300
position: [-1.0, 3.0, 7.0]
rotation: [-170.0, 8.0, 0.0]
fov: 70.0
bezier: [0.42, 0.0, 0.58, 1.0]
collision: clamp播放、停止、打断与恢复
管理员可为在线玩家播放场景、停止最近场景、按会话 ID 停止指定场景,也可以独立打断当前场景。完整可复制命令见命令参考。Java 调用方应保存 playScene 成功返回的会话 ID,再用 stop 或 interrupt 精确处理自己的场景。
- 自然播放完成:使用
transitions.exit,衔接结束后报告FINISHED。 scene stop:使用退出衔接,衔接结束后报告ABORTED,原因是 stopped。- 玩家按 Esc 或执行
scene interrupt:使用transitions.interrupt,衔接结束后报告INTERRUPTED。 camera reset、断线和世界卸载:用于立即清理,不承诺播放完整过渡。
终态会在衔接动画完成后上报,因此剧情插件不会在镜头仍移动时提前开始下一段。重复打断同一个正在退出的场景不会创建第二段恢复动画。
previous 会恢复被场景暂时压住的预设或普通控制。配置解析和通信协议也接受 original,但当前客户端尚未实现它“直接恢复进入相机系统前原始状态”的独立语义,实际仍按 previous 处理;现阶段不要依赖两者差异。无论正常结束、按 Esc 跳过、管理员停止还是异常中止,输入限制都必须随场景一起解除。
与剧情逻辑配合
场景文件只负责相机,不执行命令、条件、循环或其他玩法逻辑。需要在开场、结束或中止时继续剧情时,由业务插件监听 CameraStatusEvent,根据 STARTED、FINISHED、ABORTED、INTERRUPTED、REJECTED 或 UNSUPPORTED 状态处理。
多人同一时刻调用只能保证服务端发起时机接近,不保证不同客户端逐帧完全同步。需要精确战斗判定时,仍应以服务端逻辑为准。
常见问题
场景一播放就结束:检查锚点是否合法;实体锚点必须使用目标维度和真实 UUID,且客户端需要能解析到该实体。
关键帧配置导致整次重载失败:检查关键帧名称是否重复、时间是否为非负整数、数组长度是否正确,以及 FOV、贝塞尔参数是否越界。
场景结束后没有回到肩射预设:使用 restore.mode: previous,并确认下层预设会话没有被单独停止或重置。
玩家无法跳过:确认 skippable: true;只有当前生效的可跳过场景会响应 Esc。
隐藏 HUD 后担心无法退出:HUD 隐藏不影响暂停菜单、聊天和恢复入口;异常结束也会恢复原状态。
猹件开发组