巧用触控打造出色的游戏
Make your game great with touch
2026年6月10日
一句话判断
这场 Session 最值得关注的一件事是:Apple 终于把“虚拟按键”提升到了和物理手柄同等的一等公民地位,TCTouchController 让你写一套输入逻辑就能同时搞定手柄和屏幕触控。
这场 Session 讲了什么
以前做 iOS 游戏,处理物理手柄用 GCController,处理屏幕虚拟按键得自己写一堆 UITouch 状态机,两套逻辑完全割裂。现在 Apple 推出了 Touch Controller 框架(TCTouchController),直接把屏幕上的虚拟按键包装成了标准的 GCController 对象。
你只需要通过描述符(Descriptor)配置好按键布局和样式,把 UIKit 的触摸事件喂给它,它就能在 Metal 渲染管线里画出按键,并触发和物理手柄一模一样的回调。这对用 Game Porting Toolkit 4 把 Mac/PC 游戏移植到 iOS 的团队,以及想优化原生手游盲操体验的开发者来说,是个巨大的减负。
值得深挖的点
输入逻辑的“大一统”与渲染绑定
Apple 做 TCTouchController 的动机很明确:GPTK 4 让游戏移植变简单了,但 iOS 设备不一定外接手柄,虚拟按键是刚需。如果让开发者自己把虚拟按键的输入映射到 GCController 协议上,工作量巨大且容易出 Bug。现在框架底层直接帮你把触控事件转换成了 GCControllerButtonInput 和 GCControllerDirectionPad,你的游戏逻辑层只需要监听 valueChangedHandler,完全不用关心输入源是屏幕还是 Xbox 手柄。
代价是框架强绑定了 Metal 渲染(通过 render(using:) 方法)。如果你的游戏是用 SpriteKit 或纯 UIKit 写的,接入成本会变高,你可能需要创建一个离屏的 Metal 视图来专门渲染这些控件,或者只白嫖它的输入转换逻辑而自己画 UI。另外,它接管了触摸事件,如果你的游戏里还有非游戏性的 UI(比如聊天框、商店列表),需要仔细处理 UITouch 的事件拦截,避免虚拟按键和 UIKit 控件抢事件。
抛弃“物理映射”,拥抱“触控原生”交互
很多移植游戏最容易犯的错,就是把 Xbox 手柄的按键 1:1 搬到屏幕上,导致屏幕下半部分密密麻麻全是按钮,不仅遮挡视野,多指操作也极其反人类。Session 里给出的解法非常务实:利用触控屏没有物理边界的特点,把虚拟摇杆的碰撞体(colliderShape)设为 leftSide(左半屏),让玩家随便戳左半边都能移动。
更精妙的是用摇杆的“倾斜幅度”(tilt magnitude)来代替“冲刺键”。物理手柄上“推摇杆+按 L3”很自然,但在屏幕上同时做这两个动作手指会打结。通过判断摇杆推到底的幅度直接触发冲刺,直接砍掉了一个屏幕按钮。这种“做减法”的设计思路,比单纯提供一套好看的皮肤有价值得多。
代码片段
1. 初始化触控控制器与事件转发
场景:在 Metal 视图中创建 TCTouchController,并将 UIKit 的触摸事件桥接给它。
// 初始化触控控制器
let descriptor = TCTouchControllerDescriptor(mtkView: mtkView)
if TCTouchController.isSupported {
touchController = TCTouchController(descriptor: descriptor)
}
touchController?.connect()
// 在 Metal 渲染循环中绘制控件
touchController?.render(using: renderEncoder)
// 重写 UIKit 触摸事件,喂给触控控制器
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
// 将触摸坐标和唯一标识传给框架
touchController?.handleTouchBegan(at: touch.location(in: view), index: touch.hash)
}
}
坑:index 参数必须传 touch.hash 或唯一 ID,如果传错会导致多指操作时框架无法区分手指,摇杆和按键会串线。
2. 全屏摇杆与倾斜幅度检测冲刺
场景:配置左半屏虚拟摇杆,并通过倾斜幅度判断玩家是否在“推到底”以触发冲刺。
// 配置左摇杆描述符
let leftStickDesc = TCThumbstickDescriptor()
// 碰撞区域设为屏幕左半边,实现盲操
leftStickDesc.colliderShape = .leftSide
// 没按下时隐藏,保持屏幕干净
leftStickDesc.hidesWhenNotPressed = true
touchController?.addThumbstick(descriptor: leftStickDesc)
// 在轮询逻辑中检测冲刺
func pollInput() {
guard let leftStick = touchController?.extendedGamepad?.leftThumbstick else { return }
// 获取摇杆的倾斜幅度 (0.0 到 1.0)
let tiltMagnitude = leftStick.magnitude
// 幅度超过 0.9 视为冲刺,无需额外的冲刺按键
if tiltMagnitude > 0.9 {
player.sprint()
} else {
player.move(with: leftStick.value)
}
}
坑:magnitude 是向量长度,不要自己去算 x*x + y*y 开根号,直接用框架提供的属性,且记得处理死区(Deadzone)避免摇杆回中时的微小抖动误触冲刺。
最佳实践
老项目迁移时,千万别急着把现有的虚拟按键 UI 删掉。先把现有的输入逻辑抽象成标准的 GCController 接口,跑通物理手柄,然后再引入 TCTouchController 替换掉自定义的触控 UI。
新项目布局时,死磕框架提供的 9 个锚点(Anchor)和 safeAreaInsets。不要自己硬编码坐标,iPad 和 iPhone 的屏幕比例差异巨大,灵动岛和 Home Indicator 会吃掉你边缘的 20-30 像素。把高频操作放在拇指热区(底部锚点),低频菜单扔给屏幕顶部。
实战中最容易踩的坑是“上下文状态不同步”。物理按键的盲文是固定的,但屏幕按键可以变。当角色切换武器或魔法时,务必通过 button.contents 动态更新按键图标,并在技能 CD 或不可用时,直接把 isEnabled 设为 false 隐藏按键,别让玩家点了一个没反应的灰色按钮。
还有什么值得关注
TCTouchpad替代右摇杆:用触控板(Touchpad)控制相机视角,手指移动距离和相机转动 1:1 映射,彻底解决了传统虚拟右摇杆“松手后相机还在漂移”和“转动过度”的顽疾。- QTE(快速反应事件)降维打击:把物理手柄上反人类的“同时按下 L1+R1”组合键,在屏幕上直接合并成一个巨大的单点触控按钮,降低多指操作门槛。
- 技能轮盘直接触控化:按下 X 键不再弹出一个需要摇杆选择的 UI 覆盖层,而是直接在屏幕上展开技能触控按钮,3 秒不选自动收起,交互更连贯。