本地分屏合作听起来像复古功能,但做起来一点都不简单。两个玩家共用一台设备,意味着输入设备要分配,镜头要分屏或动态合并,UI 焦点不能互相抢,暂停菜单要知道是谁打开的,性能预算还要乘以多个视口。Godot 的 Viewport 和输入系统能支持这些需求,但需要清晰架构。
很多项目在原型期会直接复制一个玩家角色,再复制一台 Camera。画面很快能跑起来,但问题也随之而来:手柄 A 控制了玩家 B,暂停菜单抢走所有焦点,两个 Viewport 都渲染完整特效导致掉帧,掉落物提示不知道显示在哪个玩家视口。
这篇文章讲 Godot 本地分屏协作的客户端做法。重点是把“玩家槽位”作为核心概念,再围绕它组织输入、相机、UI、音频和性能预算。只要槽位清楚,后面的系统才不会互相误伤。
项目里的真实问题
一个双人合作项目中,单人模式已经稳定。加入本地双人后,团队只是把 Player 场景实例化两份,并给第二个玩家换了一套输入 action。结果测试时发现:键盘和第一个手柄会同时驱动 P1,第二个手柄偶尔抢到 UI 焦点;P2 打开背包时,P1 的移动也被暂停;分屏后小地图仍然只跟随 P1。
这些问题来自单人假设。原来的输入系统默认只有一个 active player,UI 默认只有一个焦点 owner,相机默认只有一个目标。分屏不是复制角色,而是把客户端许多全局概念改成按玩家槽位隔离。
同时,分屏会放大性能成本。两个 Viewport 可能让渲染成本接近翻倍,尤其 3D 场景里的阴影、后处理和透明特效。客户端需要为本地合作准备单独画质策略,而不是沿用单人设置。
目标和边界
- 槽位优先:用 PlayerSlot 管理输入设备、角色、相机和 UI 上下文。
- 输入隔离:每个设备明确归属,UI 和 gameplay 输入按槽位路由。
- 视口可控:分屏、合屏和动态布局都有统一管理。
- 预算明确:本地合作模式下单独调整渲染和特效成本。
这些边界看起来像流程约束,实际是在保护客户端团队的节奏。Godot 项目一旦进入内容量增长阶段,很多问题并不是某个脚本写错了,而是编辑器、资源、运行时和发布流程之间没有明确交接点。把边界提前写清楚,可以减少临近提测时的争论,也能让新人知道应该在哪一层补逻辑。
推荐架构
flowchart TD
A["DeviceManager"] --> B["PlayerSlot 1"]
A --> C["PlayerSlot 2"]
B --> D["角色/输入上下文"]
C --> E["角色/输入上下文"]
B --> F["Viewport + Camera"]
C --> G["Viewport + Camera"]
F --> H["SplitScreenLayout"]
G --> H
B --> I["Slot UI Focus"]
C --> J["Slot UI Focus"]
这张图不是为了追求复杂,而是把责任拆开。Godot 的便利之处在于 Node、Resource、信号和编辑器扩展都很轻,但便利也会诱导大家把判断写在任意脚本里。我的经验是,只要某个能力要被两个以上场景复用,就应该把它提升为一条稳定链路:输入是什么、谁负责校验、失败怎么回滚、日志如何被带出去。
PlayerSlot 是核心对象
PlayerSlot 不只是编号,它应该持有输入设备 id、玩家实体、相机、Viewport、UI 根、当前控制模式和暂停状态。游戏里的系统不要问“当前玩家是谁”,而要接收 slot 参数。比如打开背包、显示交互提示、播放本地震动,都应该知道来自哪个槽位。
设备分配要有明确流程。进入本地合作大厅时,玩家按键加入,DeviceManager 把设备绑定到空 slot。绑定后,gameplay 输入只路由到对应角色。键盘是否允许和手柄共用,要提前决定。不要在游戏中途让设备自动漂移到另一个玩家。
断开设备也要处理。手柄断开时,对应 slot 进入等待重连状态,只暂停该玩家还是暂停全局,要看游戏规则。UI 上要显示是哪个玩家的设备断开,而不是弹一个全局错误。
Viewport 布局要服务玩法
分屏不一定永远二等分。玩家距离近时可以合屏,距离远时切分屏;Boss 战可以固定横向分屏;解谜区域可以一大一小。无论策略如何,都应该由 SplitScreenLayout 统一控制 ViewportRect、Camera 目标和 UI 安全区。
每个 Viewport 的相机要有自己的后处理和裁剪策略。分屏时可以降低阴影距离、关闭昂贵后处理或减少粒子密度。不要让两个 Viewport 都用单人最高画质。Godot 中多个 SubViewport 渲染时,成本是真实存在的。
UI 要按 slot 放置。P1 的交互提示出现在 P1 视口内,P2 的背包不应该挡住 P1 的准星。全局菜单可以覆盖全屏,但要显示是哪个玩家打开,并且焦点归属清楚。
焦点和暂停是最容易乱的
Godot Control 焦点默认偏全局,本地多人需要建立 SlotFocusManager。每个玩家有自己的 UI 根和焦点栈,gameplay 输入和 UI 输入先按设备找到 slot,再交给该 slot 的焦点系统。这样 P2 浏览菜单时,P1 仍然可以移动,除非游戏规则明确暂停全局。
暂停分两类:系统暂停和玩家局部暂停。系统暂停会冻结世界,适合设置菜单;局部暂停只锁定某个玩家输入,适合背包或表情轮盘。Timer、动画和音频是否暂停,也要按类型处理。
音频和震动也要按玩家处理。P1 受到伤害时,不应该震动 P2 的手柄;分屏中位置音频可以仍然以全局听者为主,但关键提示音最好按 slot 做 UI 音效反馈。
GDScript 落地片段
class_name PlayerSlot
extends Node
var slot_id: int
var device_id: int
var actor: Node
var viewport: SubViewport
var camera: Camera3D
var ui_root: Control
func route_input(event: InputEvent) -> void:
if not DeviceRouter.event_belongs_to_device(event, device_id):
return
if SlotFocusManager.has_ui_focus(slot_id):
SlotFocusManager.dispatch_ui(slot_id, event)
else:
actor.handle_player_input(event)
这段代码不一定要原样放进项目,它更像接口形状的草图。真正落地时,我会先写成 Autoload 或 EditorPlugin 里的一个薄服务,让业务脚本只依赖稳定方法,不直接知道文件路径、远端地址、调试开关或平台差异。这样后续换实现时,场景脚本和 UI 脚本不需要跟着大面积调整。
排查指标
- 本地合作模式下每个 Viewport 的渲染耗时。
- 输入设备误路由或未识别次数。
- UI 焦点丢失、焦点抢占和菜单误操作次数。
- 分屏切换时帧率波动和相机跳变次数。
指标不要只在出问题后临时加。Godot 客户端经常遇到“编辑器里没事,导出包里才出问题”的情况,如果日志字段、采样频率和错误码命名没有提前约定,复盘时就只能靠截图和口头描述。建议把关键指标打印到本地日志,同时在内测包里接入轻量上报,至少保留设备、平台、场景、资源版本和玩家操作入口。
上线前检查清单
- 所有玩家相关系统都通过 PlayerSlot 访问上下文。
- 设备加入、断开、重连和重新绑定流程明确。
- 每个 slot 有独立 UI 根和焦点栈。
- 分屏模式使用单独画质预算。
- 暂停逻辑区分系统暂停和玩家局部暂停。
清单的价值不在于证明大家都很谨慎,而是把隐性经验变成团队共识。每次事故后都应该补一条能自动检查的规则,不能自动检查的也要变成明确的人工步骤。等同类问题第二次出现时,团队应该问的不是“谁又忘了”,而是“为什么流程还允许它被忘掉”。
分阶段落地和团队协作
第一阶段不要急着做华丽分屏,先做玩家槽位大厅。两个手柄能稳定加入、退出、重连,屏幕上能清楚显示 P1 和 P2 的设备归属。只有设备归属稳定,后续角色、UI 和相机才有可靠基础。
第二阶段接入双角色和固定分屏。固定上下或左右分屏虽然简单,但最适合验证输入隔离、Viewport 成本和 UI 焦点。动态合屏、镜头融合和花式布局都可以之后再做。先把最朴素的模式跑稳。
第三阶段再做体验优化。根据距离动态合屏、根据场景切换布局、根据玩家状态调整 UI 安全区。每个优化都要以 PlayerSlot 为入口,不要让相机或 UI 自己猜当前是哪位玩家。
自动化验证和回归样本
自动化验证可以用模拟输入事件。给 device 0 发送移动,断言只有 P1 位移;给 device 1 打开菜单,断言 P2 获得 UI 焦点且 P1 仍能移动;断开 device 1,断言 P2 进入等待重连状态。输入隔离必须可测。
性能样本要包含单人、固定双分屏、动态合屏三种模式。同一场景下记录 Viewport 渲染耗时、阴影耗时、粒子数量和 UI 绘制耗时。不要等玩家反馈卡顿后才发现双分屏成本接近翻倍。
本地合作相关 PR review 要特别警惕全局单例。任何 CurrentPlayer、active_camera、focused_ui 之类全局字段,都可能在分屏下出问题。更稳的写法是通过 slot id 查询上下文。
灰度观察和事故复盘
灰度期可以记录设备加入失败、输入未识别、焦点丢失和分屏切换次数。很多本地合作问题只在真实手柄和电视环境下出现,桌面键盘测试覆盖不了。
如果玩家反馈“我控制了另一个角色”,优先检查设备路由和 slot 绑定日志,而不是角色脚本。角色通常只是执行输入,真正的错误多半发生在输入事件归属阶段。日志里必须能看到事件来自哪个设备、被路由到哪个 slot。
长期维护本地合作,要把单人功能都当成按 slot 扩展来设计。背包、商店、对话、震动、教程提示、拍照模式都可能涉及玩家归属。只要 PlayerSlot 边界稳定,新功能接入分屏就不会每次都像重做一遍。
现场演练
分屏演练要用真实手柄。让 P1 用键盘,P2 用手柄,再热插拔另一个手柄,观察设备归属是否稳定。随后让 P2 打开背包,P1 继续移动,确认 UI 焦点没有抢走 P1 的 gameplay 输入。这个演练能快速发现全局焦点和全局当前玩家的问题。
性能演练则要在最重的战斗场景里做。单人、双人固定分屏、双人动态合屏分别跑同一段战斗,记录帧率、Viewport 渲染耗时和粒子峰值。分屏功能如果只在空场景里测试,正式玩法里一定会暴露预算问题。
小团队接入版本
小团队做本地合作时,建议先只支持两个玩家和固定布局。四人、动态合屏、独立菜单和复杂相机都可以排到后面。两个玩家已经足够暴露输入归属、UI 焦点、Viewport 成本和暂停语义问题。
此外,所有新功能都要问一句:它属于哪个 slot?如果答案是“全局”,再确认它真的应该全局。教程提示、交互气泡、手柄震动、背包菜单、拍照模式都可能需要按玩家区分。这个习惯越早建立,后续扩展越轻松。
交付标准
交付标准是两名玩家能同时完成一段真实核心玩法,而不只是站在测试场景里移动。要包含战斗、交互、打开菜单、死亡复活、切场景和设备断开。分屏问题常常出现在系统交叉点,单独测移动无法说明功能稳定。
还要给设计团队一份限制说明。比如本地合作模式下某些高成本后处理关闭,某些 UI 弹窗改为半屏,某些镜头演出不允许强制夺走全屏。提前说明限制,设计就能围绕真实客户端能力做方案。
交付补充
补充一点:本地合作的教程也要按 slot 设计。P1 学会的操作不代表 P2 已经学会,提示如果只显示一次,第二名玩家可能完全错过。教程状态可以共享进度,但输入提示和确认最好按玩家独立处理。
结语
本地分屏协作的难点,是把单人客户端里的全局假设拆开。Godot 的 Viewport、Control 和输入事件都足够灵活,但需要 PlayerSlot 这样的中间层把它们组织起来。只要槽位、输入、视口和焦点边界清楚,本地合作就不会变成一堆互相抢状态的特殊逻辑。
补充落地笔记
建议从大厅加入流程开始做,而不是先复制角色。先让两个设备稳定绑定到两个 slot,再让 UI 能显示各自焦点,最后接入角色和 Viewport。这样每一步都能独立验证。很多分屏 bug 不是渲染问题,而是最早的设备归属没有定义清楚。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。