Make your game great with touch
Graphics, Games & Media 进阶 24m

巧用触控打造出色的游戏

Make your game great with touch

2026年6月10日

在 Apple 官方观看视频

一句话判断

这场 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。现在框架底层直接帮你把触控事件转换成了 GCControllerButtonInputGCControllerDirectionPad,你的游戏逻辑层只需要监听 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 秒不选自动收起,交互更连贯。
TCTouchController GCController Metal Game Porting Toolkit UIKit