ハイライト
Apple は Touch Controller framework を導入し、すでに Game Controller に対応しているゲームを touch input へ自然に拡張できるようにしました。動的な control、全画面 touch 領域、custom visual feedback により、mobile player に滑らかなゲーム体験を提供できます。
主要内容
ゲームを Mac から iOS に移すことは、すでにかなり簡単になっています。Game Porting Toolkit 4 によって、このプロセスはさらに扱いやすくなりました。プレイヤーは Mac、iPad、iPhone 上でさまざまなゲームコントローラーを使えます。
ただし問題があります。プレイヤーはいつでも iPhone を取り出してゲームに入る可能性があり、そのとき必ず controller を持っているとは限りません。ハードウェア controller がないときにも優れたゲーム体験を保つにはどうすればよいでしょうか。答えは、touch control をきちんと設計することです。
Touch Controller framework は Game Controller framework の上に構築されており、同じ input model を引き継ぎます。状態を polling することも、value change handler を設定することもできます。違いは、これらの control を touch screen 上に表示でき、見た目を完全に customize できる点です。
この framework は Metal と直接統合され、rendering performance を保ちます。有効にすると、touch controller は GCController object として現れるため、ゲームロジックを大きく変更する必要はありません。
詳細
Touch controller の設定
(02:04)
Game Controller framework の基本はシンプルです。GCController の接続と切断 notification を監視し、その後 device state を polling するか、change handler を設定します。
// Polling
if (button.isPressed) {
// ...
}
// Change handlers
pressedInput.pressedDidChangeHandler = { (element: any GCPhysicalInputElement,
input: any GCPressedStateInput,
pressed: Bool)
// ...
}
ポイント:
- Polling は毎 frame input state を確認する用途に向いています。
- Change handler は input event に反応する処理に向いています。
- Touch Controller はどちらの model もサポートします。
(03:14)
Touch controller を設定するコード構造は次のとおりです。
// TCTouchController を設定
private(set) var touchController: TCTouchController?
let descriptor = TCTouchControllerDescriptor(mtkView: mtkView)
if TCTouchController.isSupported {
touchController = TCTouchController(descriptor: descriptor)
}
touchController?.connect()
touchController?.render(using: renderEncoder)
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
touchControls.handleTouchBegan(at: touch.location(in: view), index: touch.hash)
}
}
buttonA?.valueChangedHandler = { (_ button: GCControllerButtonInput, _ value: Float,
_ pressed: Bool) in
// ...
}
ポイント:
TCTouchControllerDescriptorはMTKViewと結び付けて作成します。connect()で touch controller を有効化します。render(using:)は Metal render pass の中で control を描画します。UIViewの touch event は touch controller に転送する必要があります。
柔軟な layout 設計
(05:21)
Touch Controller framework は 9 種類の anchor layout を提供します。各 control は anchor と相対 offset を指定できます。
// 標準的な円形 button B を作成
let buttonBDesc = TCButtonDescriptor()
buttonBDesc.label = TCControlLabel.buttonB
buttonBDesc.anchor = .bottomRight
buttonBDesc.offset = adjustedOffset(CGPoint(x: -35, y: -106), for: buttonBDesc.anchor)
buttonBDesc.contents = .buttonContents(forSystemImageNamed: "b.circle",
size: buttonBDesc.size, shape: .circle,
controller: touchController)
touchController.addButton(descriptor: buttonBDesc)
func adjustedOffset(_ offset: CGPoint, for anchor: TCControlLayoutAnchor) -> CGPoint {
var x = offset.x, y = offset.y
let safeArea = view.safeAreaInsets
switch anchor {
case .bottomRight:
x -= safeArea.right
y -= safeArea.bottom
// ... その他の anchor
}
return CGPoint(x: x, y: y)
}
ポイント:
TCControlLabel.buttonBは物理 controller の B button に対応します。- Anchor で位置決めした後、offset は anchor からの相対位置として計算します。
safeAreaInsetsにより、control が角丸や Dynamic Island に隠れないようにします。- 関連する control は同じ anchor を使って group 化し、相対位置を保てます。
動的な control
(10:48)
Touch control の利点は、見た目を動的に変更できることです。
// Icon image を変更
buttonBDesc.contents = .buttonContents(forSystemImageNamed: "figure.fencing",
size: buttonBDesc.size,
shape: .circle,
controller: touchController)
(11:51)
ゲームの context に応じて control の内容を更新します。
func setButtonBContents(symbolName: String) {
for button in touchController.buttons {
if button.label == TCControlLabel.buttonB {
button.contents = .buttonContents(forSystemImageNamed: symbolName, size: buttonSize,
shape: .circle, controller: touchController)
}
}
}
func cyclePower() {
switch currentPower {
case .strike: touchControls?.setButtonBContents(symbolName: "figure.fencing")
case .fireball: touchControls?.setButtonBContents(symbolName: "flame.fill")
case .waterBlaster: touchControls?.setButtonBContents(symbolName: "drop.fill")
}
}
ポイント:
- Icon は control の実際の機能を反映しているべきです。
- Context が変わったら icon も同期して更新します。
- プレイヤーは設定画面を見なくても、各 control の役割を理解できます。
Context に応じた非表示
(13:01)
使用していない control は非表示にするべきです。
// 触れていないときは left thumbstick を隠す
let leftStickDesc = TCThumbstickDescriptor()
leftStickDesc.hidesWhenNotPressed = true
(13:19)
Pickup button を場面に応じて動的に表示・非表示にします。
func showPickupButton(at projectedPosition: CGPoint) {
// Pickup button の位置 (ptX, ptY) を計算
descriptor.offset = CGPoint(x: ptX, y: ptY)
touchController.addButton(descriptor: descriptor)
}
func hidePickupButton() {
for button in touchController.buttons {
if button.label == TCControlLabel.buttonY {
touchController.removeControl(button)
}
}
}
ポイント:
hidesWhenNotPressedにより、thumbstick は使われていないとき自動で隠れます。- Button は
isEnabledで表示を制御できます。 - 位置が動的に変わる button は
addButtonとremoveControlで管理します。 - 画面上の clutter を減らし、プレイヤーがゲームに集中できるようにします。
全画面 touch 領域
(15:34)
物理 thumbstick には固定サイズがありますが、touch screen では入力領域を広げられます。
// 画面左半分を character movement に使う
let leftStickDesc = TCThumbstickDescriptor()
leftStickDesc.colliderShape = .leftSide
ポイント:
.leftSideは thumbstick を画面左半分に広げます。- プレイヤーは任意の位置から入力を開始できます。
- 視覚的な control を正確に触る必要はありません。
(16:39)
Thumbstick の傾き量で sprint を発動します。
func pollInput() {
if let gamePad = gameController.extendedGamepad {
let gamePadLeft = gamePad.leftThumbstick
var moveInput = simd_make_float2(gamePadLeft.xAxis.value, -gamePadLeft.yAxis.value)
let magnitude = simd_length(moveInput)
if magnitude > 0.8 {
self.runModifier = 1.3
}
self.characterDirection = moveInput
}
}
ポイント:
- 小さな傾きは通常移動です。
- 大きな傾きは sprint を発動します。
- プレイヤーは 2 本目の指で sprint button を押す必要がありません。
Thumbstick を touchpad に置き換える
(17:36)
右側の thumbstick は touchpad に置き換えられます。これにより過度な回転を避けられます。
// Right thumbstick を touchpad に置き換える
let touchpadDesc = TCTouchpadDescriptor()
touchpadDesc.label = TCControlLabel.rightThumbstick
touchpadDesc.colliderShape = .rightSide
touchpadDesc.reportsRelativeValues = true
touchController.addTouchpad(descriptor: touchpadDesc)
ポイント:
reportsRelativeValuesにより、touchpad は相対移動を報告します。- 相対移動は過度な回転を防ぎます。
- Touchpad には視覚要素がないため、ゲーム画面を遮りません。
- 移動は即時に反応し、遅延がありません。
複雑な操作を単純化する
(19:30)
QTE(quick time event)は複数 button の同時押しを必要とする場合がありますが、単一 button に単純化できます。
// 2 つの QTE button を 1 つの button にまとめる
func setupControls() {
let desc = TCButtonDescriptor()
desc.label = TCControlLabel(name: "escape_button", role: .button)
touchController.addButton(descriptor: desc)
}
func showEscapeButton() {
escapeButton.isEnabled = true
}
func hideEscapeButton() {
escapeButton.isEnabled = false
}
ポイント:
- 固定位置の QTE button は
isEnabledで表示と非表示を切り替えます。 - プレイヤーは character を移動しながら、1 本指で操作できます。
- 複数指操作の複雑さを減らします。
(20:28)
照準と release を 1 つの action button にまとめられます。
buttonB?.valueChangedHandler = { (_ button: GCControllerButtonInput, _ value: Float,
_ pressed: Bool) -> Void in
self.releasePower(pressed: pressed)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let point = touch.location(in: metalView)
if let gc = gameController, gc.isAiming {
let prev = touch.previousLocation(in: metalView)
gc.aimTouchDelta += simd_float2(Float(point.x - prev.x), Float(point.y - prev.y))
}
}
}
ポイント:
- Button を長押しして照準を合わせ、drag で方向を調整し、離して発射します。
touchesMovedは touch delta を独立して追跡します。- 2 本指を使えば、移動と照準を同時に行えます。
Custom visual feedback
(21:52)
Sprint 中に left thumbstick へ glow effect を追加します。
let haloLayer = TCControlImage(texture: haloTexture, size: haloSize, highlight: nil,
offset: .zero, tintColor: tint)
let normalBgImages = TCControlContents.thumbstickStickBackgroundContents(size: bgSize,
controller: controller).images
haloThumbstickBg = TCControlContents(images: [haloLayer] + normalBgImages)
thumbstick.backgroundContents = active ? haloThumbstickBg : normalThumbstickBg
ポイント:
TCControlContentsは layer の配列です。- Halo layer は標準背景の上に重ねます。
- Sprint state に応じて background contents を切り替えます。
- 明確な visual feedback はゲーム体験を高めます。
主要な示唆
-
Skill wheel を直接 touch button として表示する — Pop-up menu を出すのではなく、現在利用可能な skill option を画面上に直接表示します。
addButtonで動的に追加し、3 秒の timeout で自動的に隠します。プレイヤーは menu を開かず直接選べます。 -
全画面 thumbstick 領域 — 移動 thumbstick の
colliderShapeを.leftSideに設定し、画面左半分のどこからでも移動を開始できるようにします。小さな thumbstick を正確に触る必要がなく、寝転がっていても遊びやすくなります。 -
傾き sprint — Thumbstick の傾き量を読み取り、閾値を超えたら自動的に sprint します。独立した sprint button が不要になり、指の負担を減らします。頻繁に sprint する action game に向いています。
-
Touchpad camera control — 右 thumbstick を
TCTouchpadに置き換え、reportsRelativeValuesを設定します。指が動いた分だけ camera が回転するため、過度な回転も遅延も起きにくくなります。 -
Context-sensitive button — ゲーム状態に応じて button を動的に表示します。Pickup button は item の近くでのみ表示し、attack button の icon は現在の weapon に合わせて変化します。画面上の clutter を減らし、プレイヤーの注意をゲーム内容に集中させます。
関連 Session
- Level up with Apple game technologies — ゲーム移植と Apple のゲーム技術の概要
- Design great interfaces for handheld games — 携帯型ゲーム interface の設計原則
- Game Porting Toolkit — ゲーム移植ツールの詳解
- SwiftUI graphics — カスタムゲーム UI に利用できる SwiftUI の描画技術
- Metal — Metal レンダリングのパフォーマンス最適化
コメント
GitHub Issues · utterances