HarmonyOS开发:游戏输入——触控、陀螺仪与手柄适配

举报
Jack20 发表于 2026/06/28 21:00:15 2026/06/28
【摘要】 HarmonyOS开发:游戏输入——触控、陀螺仪与手柄适配📌 核心要点:游戏输入是玩家和游戏之间的唯一桥梁,多点触控要防误触、陀螺仪要滤噪声、手柄要处理死区,输入映射层把硬件差异屏蔽掉,游戏逻辑只管"前进"不管"按了哪个键"。 背景与动机你有没有遇到过这种体验——明明点了右边,角色却往左跑?明明摇杆推到底了,人物却像散步一样慢?输入体验差,游戏直接判死刑。画面可以糙,玩法可以简,但输入必...

HarmonyOS开发:游戏输入——触控、陀螺仪与手柄适配

📌 核心要点:游戏输入是玩家和游戏之间的唯一桥梁,多点触控要防误触、陀螺仪要滤噪声、手柄要处理死区,输入映射层把硬件差异屏蔽掉,游戏逻辑只管"前进"不管"按了哪个键"。

背景与动机

你有没有遇到过这种体验——明明点了右边,角色却往左跑?明明摇杆推到底了,人物却像散步一样慢?

输入体验差,游戏直接判死刑。画面可以糙,玩法可以简,但输入必须跟手。

鸿蒙设备形态多样:手机有触屏、平板有触屏+手柄、智慧屏只有遥控器。你的游戏要适配所有这些输入方式,总不能每个设备写一套逻辑吧?

所以你需要一个输入映射层——把触控、陀螺仪、手柄的原始输入统一转换成游戏动作。游戏逻辑只关心"玩家要前进",不关心"玩家按了W还是推了摇杆还是倾斜了手机"。

核心原理

游戏输入处理流程

从手指碰到屏幕到角色做出反应,中间经历了什么?

graph LR
    A[原始输入] --> B[输入采集]
    B --> C[噪声过滤]
    C --> D[死区处理]
    D --> E[输入映射]
    E --> F[游戏动作]
    
    subgraph 输入源
        A1[触控屏]
        A2[陀螺仪]
        A3[手柄摇杆]
        A4[手柄按键]
        A5[键盘]
    end
    
    subgraph 游戏动作
        F1[移动]
        F2[攻击]
        F3[跳跃]
        F4[瞄准]
        F5[菜单]
    end
    
    A1 --> A
    A2 --> A
    A3 --> A
    A4 --> A
    A5 --> A
    
    F --> F1
    F --> F2
    F --> F3
    F --> F4
    F --> F5
    
    classDef inputStyle fill:#E74C3C,stroke:#C0392B,color:#fff
    classDef processStyle fill:#3498DB,stroke:#2980B9,color:#fff,font-weight:bold
    classDef actionStyle fill:#27AE60,stroke:#229954,color:#fff
    
    class A1,A2,A3,A4,A5 inputStyle
    class A,B,C,D,E,F processStyle
    class F1,F2,F3,F4,F5 actionStyle

关键步骤是噪声过滤死区处理。陀螺仪数据有高频抖动,手柄摇杆有漂移,不处理的话,角色会自己乱动。

多点触控的状态机

触控不是简单的"按下-抬起"。一根手指按下、另一根手指按下、第一根手指移动、第二根手指抬起——这些事件交织在一起,你得用状态机来管理。

触控状态 说明
IDLE 无触控
TAP_DOWN 手指按下
TAP_MOVE 手指移动
TAP_UP 手指抬起
MULTI_TOUCH 多指触控
GESTURE_PINCH 捏合手势
GESTURE_SWIPE 滑动手势

陀螺仪数据滤波

陀螺仪原始数据噪声很大,直接用的话画面会抖。常用滤波方法:

滤波方法 延迟 平滑度 适用场景
低通滤波 通用
卡尔曼滤波 精确瞄准
互补滤波 姿态估计
移动平均 简单场景

游戏里最常用的是低通滤波,简单有效:

filtered = filtered * alpha + raw * (1 - alpha)

alpha越大越平滑,但延迟也越大。通常取0.8-0.95。

手柄死区

手柄摇杆在中心位置时,读数不是精确的0,而是0附近的一个小范围波动。这个范围就是"死区"。死区内的输入要忽略,否则角色会自己漂移。

if (abs(input) < deadzone) input = 0

死区大小通常取0.1-0.2。太小过滤不干净,太大摇杆不灵敏。

代码实战

基础用法:多点触控处理

先搞定最基础的——触控输入。

// TouchInput.ets - 多点触控处理

// 触控点信息
interface TouchPoint {
  id: number        // 触控点ID
  x: number         // X坐标
  y: number         // Y坐标
  startX: number    // 按下时X坐标
  startY: number    // 按下时Y坐标
  timestamp: number // 时间戳
  isMoving: boolean // 是否在移动
}

// 虚拟摇杆区域
interface JoystickZone {
  centerX: number
  centerY: number
  radius: number
}

// 触控输入管理器
class TouchInputManager {
  // 活跃触控点
  private activePoints: Map<number, TouchPoint> = new Map()

  // 虚拟摇杆
  private joystickZone: JoystickZone = { centerX: 100, centerY: 550, radius: 60 }
  private joystickPointId: number = -1
  private joystickX: number = 0  // -1到1
  private joystickY: number = 0  // -1到1

  // 按钮区域
  private buttonZones: Map<string, { x: number; y: number; radius: number }> = new Map([
    ['attack', { x: 300, y: 550, radius: 35 }],
    ['jump', { x: 260, y: 480, radius: 30 }],
    ['skill', { x: 320, y: 470, radius: 30 }]
  ])
  private pressedButtons: Set<string> = new Set()

  // 触控回调
  private onJoystickUpdate?: (x: number, y: number) => void
  private onButtonPress?: (button: string) => void
  private onButtonRelease?: (button: string) => void
  private onTap?: (x: number, y: number) => void

  // 设置回调
  setCallbacks(
    onJoystick: (x: number, y: number) => void,
    onPress: (button: string) => void,
    onRelease: (button: string) => void,
    onTap?: (x: number, y: number) => void
  ): void {
    this.onJoystickUpdate = onJoystick
    this.onButtonPress = onPress
    this.onButtonRelease = onRelease
    this.onTap = onTap
  }

  // 处理触控事件
  handleTouchEvent(event: TouchEvent): void {
    for (const touch of event.touches) {
      switch (event.type) {
        case TouchType.Down:
          this.onTouchDown(touch.x, touch.y, touch.id)
          break
        case TouchType.Move:
          this.onTouchMove(touch.x, touch.y, touch.id)
          break
        case TouchType.Up:
        case TouchType.Cancel:
          this.onTouchUp(touch.x, touch.y, touch.id)
          break
      }
    }
  }

  // 手指按下
  private onTouchDown(x: number, y: number, id: number): void {
    const point: TouchPoint = {
      id, x, y, startX: x, startY: y,
      timestamp: Date.now(), isMoving: false
    }
    this.activePoints.set(id, point)

    // 检查是否在摇杆区域
    const dx = x - this.joystickZone.centerX
    const dy = y - this.joystickZone.centerY
    if (Math.sqrt(dx * dx + dy * dy) <= this.joystickZone.radius * 1.5) {
      this.joystickPointId = id
      this.updateJoystick(x, y)
      return
    }

    // 检查是否在按钮区域
    for (const [name, zone] of this.buttonZones) {
      const bdx = x - zone.x
      const bdy = y - zone.y
      if (Math.sqrt(bdx * bdx + bdy * bdy) <= zone.radius) {
        this.pressedButtons.add(name)
        if (this.onButtonPress) this.onButtonPress(name)
        return
      }
    }

    // 其他区域视为点击
    if (this.onTap) this.onTap(x, y)
  }

  // 手指移动
  private onTouchMove(x: number, y: number, id: number): void {
    const point = this.activePoints.get(id)
    if (!point) return

    point.x = x
    point.y = y
    point.isMoving = true

    // 更新摇杆
    if (id === this.joystickPointId) {
      this.updateJoystick(x, y)
    }
  }

  // 手指抬起
  private onTouchUp(_x: number, _y: number, id: number): void {
    const point = this.activePoints.get(id)
    if (!point) return

    // 释放摇杆
    if (id === this.joystickPointId) {
      this.joystickPointId = -1
      this.joystickX = 0
      this.joystickY = 0
      if (this.onJoystickUpdate) this.onJoystickUpdate(0, 0)
    }

    // 释放按钮
    for (const name of this.pressedButtons) {
      if (this.onButtonRelease) this.onButtonRelease(name)
    }
    this.pressedButtons.clear()

    this.activePoints.delete(id)
  }

  // 更新摇杆值
  private updateJoystick(touchX: number, touchY: number): void {
    let dx = touchX - this.joystickZone.centerX
    let dy = touchY - this.joystickZone.centerY
    const dist = Math.sqrt(dx * dx + dy * dy)

    // 限制在摇杆范围内
    if (dist > this.joystickZone.radius) {
      dx = dx / dist * this.joystickZone.radius
      dy = dy / dist * this.joystickZone.radius
    }

    // 归一化到-1~1
    this.joystickX = dx / this.joystickZone.radius
    this.joystickY = dy / this.joystickZone.radius

    // 应用死区
    const deadzone = 0.1
    if (Math.abs(this.joystickX) < deadzone) this.joystickX = 0
    if (Math.abs(this.joystickY) < deadzone) this.joystickY = 0

    if (this.onJoystickUpdate) this.onJoystickUpdate(this.joystickX, this.joystickY)
  }

  // 获取摇杆值
  getJoystick(): { x: number; y: number } {
    return { x: this.joystickX, y: this.joystickY }
  }

  // 是否按下指定按钮
  isButtonPressed(name: string): boolean {
    return this.pressedButtons.has(name)
  }

  // 绘制虚拟控制器(调试用)
  drawVirtualControls(ctx: CanvasRenderingContext2D): void {
    // 绘制摇杆底座
    ctx.beginPath()
    ctx.arc(this.joystickZone.centerX, this.joystickZone.centerY, this.joystickZone.radius, 0, Math.PI * 2)
    ctx.fillStyle = 'rgba(255,255,255,0.1)'
    ctx.fill()
    ctx.strokeStyle = 'rgba(255,255,255,0.3)'
    ctx.lineWidth = 2
    ctx.stroke()

    // 绘制摇杆手柄
    const handleX = this.joystickZone.centerX + this.joystickX * this.joystickZone.radius
    const handleY = this.joystickZone.centerY + this.joystickY * this.joystickZone.radius
    ctx.beginPath()
    ctx.arc(handleX, handleY, 20, 0, Math.PI * 2)
    ctx.fillStyle = 'rgba(255,255,255,0.4)'
    ctx.fill()

    // 绘制按钮
    for (const [name, zone] of this.buttonZones) {
      const pressed = this.pressedButtons.has(name)
      ctx.beginPath()
      ctx.arc(zone.x, zone.y, zone.radius, 0, Math.PI * 2)
      ctx.fillStyle = pressed ? 'rgba(255,100,100,0.5)' : 'rgba(255,255,255,0.15)'
      ctx.fill()
      ctx.strokeStyle = 'rgba(255,255,255,0.3)'
      ctx.lineWidth = 2
      ctx.stroke()

      // 按钮文字
      ctx.fillStyle = '#ffffff'
      ctx.font = '12px sans-serif'
      ctx.textAlign = 'center'
      ctx.fillText(name, zone.x, zone.y + 4)
    }
    ctx.textAlign = 'start'
  }
}

进阶用法:陀螺仪与手柄输入

陀螺仪和手柄是3D游戏和高级游戏的重要输入方式。

// SensorInput.ets - 陀螺仪与手柄输入
import { sensor } from '@kit.SensorServiceKit'
import { inputConsumer } from '@kit.InputKit'

// 陀螺仪输入管理器
class GyroInputManager {
  private enabled: boolean = false
  private sensitivity: number = 2.0    // 灵敏度
  private deadzone: number = 0.05      // 死区
  private alpha: number = 0.85         // 低通滤波系数

  // 滤波后的值
  private filteredX: number = 0
  private filteredY: number = 0
  private filteredZ: number = 0

  // 回调
  private onGyroUpdate?: (x: number, y: number, z: number) => void

  // 设置回调
  setCallback(cb: (x: number, y: number, z: number) => void): void {
    this.onGyroUpdate = cb
  }

  // 启用陀螺仪
  async enable(): Promise<void> {
    if (this.enabled) return

    try {
      // 订阅陀螺仪数据
      sensor.on(sensor.SensorType.GYROSCOPE, (data: sensor.GyroscopeResponse) => {
        this.processGyroData(data.x, data.y, data.z)
      }, { interval: sensor.SensorRate.GAME }) // 游戏采样率

      this.enabled = true
    } catch (e) {
      console.error('陀螺仪启用失败: ' + e)
    }
  }

  // 禁用陀螺仪
  disable(): void {
    if (!this.enabled) return
    sensor.off(sensor.SensorType.GYROSCOPE)
    this.enabled = false
    this.filteredX = 0
    this.filteredY = 0
    this.filteredZ = 0
  }

  // 处理陀螺仪数据
  private processGyroData(rawX: number, rawY: number, rawZ: number): void {
    // 低通滤波
    this.filteredX = this.filteredX * this.alpha + rawX * (1 - this.alpha)
    this.filteredY = this.filteredY * this.alpha + rawY * (1 - this.alpha)
    this.filteredZ = this.filteredZ * this.alpha + rawZ * (1 - this.alpha)

    // 应用死区
    let fx = this.filteredX * this.sensitivity
    let fy = this.filteredY * this.sensitivity
    let fz = this.filteredZ * this.sensitivity

    if (Math.abs(fx) < this.deadzone) fx = 0
    if (Math.abs(fy) < this.deadzone) fy = 0
    if (Math.abs(fz) < this.deadzone) fz = 0

    if (this.onGyroUpdate) this.onGyroUpdate(fx, fy, fz)
  }

  // 设置灵敏度
  setSensitivity(s: number): void {
    this.sensitivity = s
  }

  // 设置死区
  setDeadzone(d: number): void {
    this.deadzone = d
  }

  isEnabled(): boolean {
    return this.enabled
  }
}

// 手柄输入管理器
class GamepadInputManager {
  private connected: boolean = false
  private deadzone: number = 0.15  // 摇杆死区

  // 摇杆值
  private leftStickX: number = 0
  private leftStickY: number = 0
  private rightStickX: number = 0
  private rightStickY: number = 0

  // 按键状态
  private buttons: Map<string, boolean> = new Map([
    ['A', false], ['B', false], ['X', false], ['Y', false],
    ['LB', false], ['RB', false], ['LT', false], ['RT', false],
    ['Start', false], ['Select', false],
    ['DUp', false], ['DDown', false], ['DLeft', false], ['DRight', false]
  ])

  // 回调
  private onStickUpdate?: (leftX: number, leftY: number, rightX: number, rightY: number) => void
  private onButtonPress?: (button: string) => void
  private onButtonRelease?: (button: string) => void

  // 设置回调
  setCallbacks(
    onStick: (lx: number, ly: number, rx: number, ry: number) => void,
    onPress: (btn: string) => void,
    onRelease: (btn: string) => void
  ): void {
    this.onStickUpdate = onStick
    this.onButtonPress = onPress
    this.onButtonRelease = onRelease
  }

  // 启用手柄监听
  async enable(): Promise<void> {
    try {
      // 监听手柄连接
      inputConsumer.on('change', (data: inputConsumer.InputDeviceData) => {
        if (data.type === inputConsumer.InputDeviceType.GAMEPAD) {
          this.connected = data.status === 'online'
          console.info(`手柄${this.connected ? '已连接' : '已断开'}`)
        }
      })

      // 监听手柄输入事件
      inputConsumer.on('inputEvent', (event: inputConsumer.InputEvent) => {
        this.processGamepadEvent(event)
      })

      this.connected = true
    } catch (e) {
      console.error('手柄监听启用失败: ' + e)
    }
  }

  // 禁用
  disable(): void {
    inputConsumer.off('inputEvent')
    inputConsumer.off('change')
    this.connected = false
  }

  // 处理手柄事件
  private processGamepadEvent(event: inputConsumer.InputEvent): void {
    // 摇杆事件
    if (event.type === inputConsumer.InputEventType.AXIS) {
      this.leftStickX = this.applyDeadzone(event.axisX ?? 0)
      this.leftStickY = this.applyDeadzone(event.axisY ?? 0)
      this.rightStickX = this.applyDeadzone(event.axisZ ?? 0)
      this.rightStickY = this.applyDeadzone(event.axisRz ?? 0)

      if (this.onStickUpdate) {
        this.onStickUpdate(this.leftStickX, this.leftStickY, this.rightStickX, this.rightStickY)
      }
    }

    // 按键事件
    if (event.type === inputConsumer.InputEventType.KEY) {
      const buttonName = this.mapKeyCodeToButton(event.keyCode)
      if (buttonName) {
        const pressed = event.keyAction === inputConsumer.KeyAction.DOWN
        this.buttons.set(buttonName, pressed)

        if (pressed) {
          if (this.onButtonPress) this.onButtonPress(buttonName)
        } else {
          if (this.onButtonRelease) this.onButtonRelease(buttonName)
        }
      }
    }
  }

  // 应用死区
  private applyDeadzone(value: number): number {
    if (Math.abs(value) < this.deadzone) return 0
    // 将死区外的值重新映射到0~1
    const sign = value > 0 ? 1 : -1
    return sign * (Math.abs(value) - this.deadzone) / (1 - this.deadzone)
  }

  // 按键码映射
  private mapKeyCodeToButton(code: number): string {
    const map: Map<number, string> = new Map([
      [0x001, 'A'], [0x002, 'B'], [0x003, 'X'], [0x004, 'Y'],
      [0x005, 'LB'], [0x006, 'RB'], [0x007, 'LT'], [0x008, 'RT'],
      [0x009, 'Start'], [0x00A, 'Select'],
      [0x00B, 'DUp'], [0x00C, 'DDown'], [0x00D, 'DLeft'], [0x00E, 'DRight']
    ])
    return map.get(code) ?? ''
  }

  isConnected(): boolean {
    return this.connected
  }

  getLeftStick(): { x: number; y: number } {
    return { x: this.leftStickX, y: this.leftStickY }
  }

  getRightStick(): { x: number; y: number } {
    return { x: this.rightStickX, y: this.rightStickY }
  }

  isButtonPressed(name: string): boolean {
    return this.buttons.get(name) ?? false
  }
}

完整示例:统一输入映射系统

把触控、陀螺仪、手柄统一到一个映射层里,游戏逻辑只管"动作":

// InputMapping.ets - 统一输入映射系统

// 游戏动作枚举
enum GameAction {
  MOVE_X = 'move_x',       // 水平移动 -1~1
  MOVE_Y = 'move_y',       // 垂直移动 -1~1
  AIM_X = 'aim_x',         // 水平瞄准 -1~1
  AIM_Y = 'aim_y',         // 垂直瞄准 -1~1
  ATTACK = 'attack',       // 攻击
  JUMP = 'jump',           // 跳跃
  SKILL = 'skill',         // 技能
  INTERACT = 'interact',   // 交互
  PAUSE = 'pause'          // 暂停
}

// 动作值
interface ActionValue {
  axis: number    // 轴值 -1~1(用于移动、瞄准)
  pressed: boolean // 是否按下(用于按钮动作)
  justPressed: boolean  // 是否刚按下(本帧)
  justReleased: boolean // 是否刚释放(本帧)
}

// 统一输入管理器
class UnifiedInputManager {
  // 子管理器
  private touchMgr: TouchInputManager = new TouchInputManager()
  private gyroMgr: GyroInputManager = new GyroInputManager()
  private gamepadMgr: GamepadInputManager = new GamepadInputManager()

  // 动作值表
  private actionValues: Map<GameAction, ActionValue> = new Map()

  // 输入模式
  private inputMode: 'touch' | 'gyro' | 'gamepad' = 'touch'

  constructor() {
    this.initActionValues()
    this.setupSubManagers()
  }

  // 初始化动作值
  private initActionValues(): void {
    const actions = [
      GameAction.MOVE_X, GameAction.MOVE_Y,
      GameAction.AIM_X, GameAction.AIM_Y,
      GameAction.ATTACK, GameAction.JUMP,
      GameAction.SKILL, GameAction.INTERACT,
      GameAction.PAUSE
    ]
    for (const action of actions) {
      this.actionValues.set(action, { axis: 0, pressed: false, justPressed: false, justReleased: false })
    }
  }

  // 设置子管理器回调
  private setupSubManagers(): void {
    // 触控回调
    this.touchMgr.setCallbacks(
      (x, y) => {
        if (this.inputMode === 'touch') {
          this.setActionAxis(GameAction.MOVE_X, x)
          this.setActionAxis(GameAction.MOVE_Y, y)
        }
      },
      (button) => {
        if (this.inputMode === 'touch') {
          this.mapButtonToAction(button, true)
        }
      },
      (button) => {
        if (this.inputMode === 'touch') {
          this.mapButtonToAction(button, false)
        }
      }
    )

    // 陀螺仪回调
    this.gyroMgr.setCallback((x, y, _z) => {
      if (this.inputMode === 'gyro') {
        this.setActionAxis(GameAction.AIM_X, x)
        this.setActionAxis(GameAction.AIM_Y, y)
      }
    })

    // 手柄回调
    this.gamepadMgr.setCallbacks(
      (lx, ly, rx, ry) => {
        if (this.inputMode === 'gamepad') {
          this.setActionAxis(GameAction.MOVE_X, lx)
          this.setActionAxis(GameAction.MOVE_Y, ly)
          this.setActionAxis(GameAction.AIM_X, rx)
          this.setActionAxis(GameAction.AIM_Y, ry)
        }
      },
      (btn) => {
        if (this.inputMode === 'gamepad') {
          this.mapGamepadButtonToAction(btn, true)
        }
      },
      (btn) => {
        if (this.inputMode === 'gamepad') {
          this.mapGamepadButtonToAction(btn, false)
        }
      }
    )
  }

  // 按钮映射
  private mapButtonToAction(button: string, pressed: boolean): void {
    const mapping: Map<string, GameAction> = new Map([
      ['attack', GameAction.ATTACK],
      ['jump', GameAction.JUMP],
      ['skill', GameAction.SKILL]
    ])
    const action = mapping.get(button)
    if (action) this.setActionPressed(action, pressed)
  }

  // 手柄按钮映射
  private mapGamepadButtonToAction(button: string, pressed: boolean): void {
    const mapping: Map<string, GameAction> = new Map([
      ['A', GameAction.ATTACK],
      ['B', GameAction.JUMP],
      ['X', GameAction.SKILL],
      ['Y', GameAction.INTERACT],
      ['Start', GameAction.PAUSE]
    ])
    const action = mapping.get(button)
    if (action) this.setActionPressed(action, pressed)
  }

  // 设置轴值
  private setActionAxis(action: GameAction, value: number): void {
    const av = this.actionValues.get(action)
    if (av) av.axis = value
  }

  // 设置按下状态
  private setActionPressed(action: GameAction, pressed: boolean): void {
    const av = this.actionValues.get(action)
    if (av) {
      if (pressed && !av.pressed) av.justPressed = true
      if (!pressed && av.pressed) av.justReleased = true
      av.pressed = pressed
    }
  }

  // 每帧更新(在游戏循环中调用)
  update(): void {
    // 自动检测输入模式
    if (this.gamepadMgr.isConnected()) {
      this.inputMode = 'gamepad'
    }

    // 清除justPressed和justReleased(只保持一帧)
    for (const av of this.actionValues.values()) {
      av.justPressed = false
      av.justReleased = false
    }
  }

  // 获取动作值
  getAction(action: GameAction): ActionValue {
    return this.actionValues.get(action) ?? { axis: 0, pressed: false, justPressed: false, justReleased: false }
  }

  // 处理触控事件(从UI组件转发)
  handleTouchEvent(event: TouchEvent): void {
    this.inputMode = 'touch'
    this.touchMgr.handleTouchEvent(event)
  }

  // 启用陀螺仪
  async enableGyro(): Promise<void> {
    await this.gyroMgr.enable()
    this.inputMode = 'gyro'
  }

  // 启用手柄
  async enableGamepad(): Promise<void> {
    await this.gamepadMgr.enable()
  }

  // 获取触控管理器(用于绘制虚拟控制器)
  getTouchManager(): TouchInputManager {
    return this.touchMgr
  }

  // 获取当前输入模式
  getInputMode(): string {
    return this.inputMode
  }
}

踩坑与注意事项

坑1:触控事件被ArkUI组件拦截

Canvas上的触控事件可能被父级Column、Row等容器拦截。你要确保Canvas的hitTestBehavior设置为HitTestMode.Default,否则触控事件到不了Canvas。

坑2:陀螺仪权限

访问陀螺仪需要ohos.permission.GYROSCOPE权限。你在module.json5里声明了还不够,运行时还得动态申请。用户拒绝授权的话,陀螺仪数据拿不到,你得有降级方案。

坑3:手柄兼容性

不同品牌的手柄,按键码可能不一样。Xbox手柄和PS手柄的A/B/X/Y位置是反的,Switch Pro手柄又有自己的映射。你最好让玩家可以自定义按键映射。

坑4:虚拟摇杆的触控ID追踪

多点触控时,虚拟摇杆必须追踪正确的触控ID。如果玩家先用大拇指控制摇杆,再用食指点按钮,摇杆的触控ID不能变。上面的代码用joystickPointId来追踪,就是解决这个问题的。

坑5:输入延迟

从触控事件触发到游戏逻辑响应,中间有ArkUI事件分发→JS线程处理→游戏循环读取的延迟。通常在30-50ms左右。对于休闲游戏够用,但对格斗、音乐节奏类游戏可能不够。降低延迟的方法:减少事件处理链路、用Native层直接读取输入。

HarmonyOS 6适配说明

HarmonyOS 6在输入方面有几个重要更新:

  1. 低延迟触控API:新增了@ohos.input.lowLatency模块,可以绕过ArkUI事件分发,直接从驱动层读取触控数据,延迟降低到5ms以内。
// HarmonyOS 6 低延迟触控
import { lowLatency } from '@ohos.input.lowLatency'

// 注册低延迟触控监听
lowLatency.on('touch', (data: lowLatency.TouchData) => {
  // data.x, data.y, data.action
  // 延迟 < 5ms
  this.processTouch(data.x, data.y, data.action)
})
  1. 手柄振动反馈:新增了手柄振动API,可以根据游戏事件触发手柄振动。射击后振一下、被击中时强振,沉浸感拉满。

  2. 触控采样率提升:HarmonyOS 6将触控采样率从120Hz提升到240Hz,触控精度更高。对绘图类、音乐节奏类游戏帮助很大。

  3. 跨设备输入流转:手机上的触控可以流转到平板或智慧屏上。你可以用手机当手柄,在大屏上玩游戏。

总结

游戏输入是玩家和游戏之间的桥梁。触控、陀螺仪、手柄,每种输入方式都有自己的坑。多点触控要追踪ID,陀螺仪要滤波降噪,手柄要处理死区。

核心思路:用统一输入映射层屏蔽硬件差异。游戏逻辑只管"前进"“攻击”,不关心"按了W还是推了摇杆"。这样不管玩家用什么设备,你的游戏都能正确响应。

输入体验是游戏体验的基石。画面再好看,输入不跟手,玩家直接卸载。

评估维度 学习难度 使用频率 重要程度
多点触控处理 ★★★☆☆ ★★★★★ ★★★★★
虚拟摇杆 ★★★☆☆ ★★★★★ ★★★★★
陀螺仪数据滤波 ★★★★☆ ★★★☆☆ ★★★☆☆
手柄适配 ★★★★☆ ★★★☆☆ ★★★★☆
死区处理 ★★★☆☆ ★★★★☆ ★★★★★
输入映射系统 ★★★★☆ ★★★★★ ★★★★★

下一篇讲游戏网络——多人联机与实时同步。单机游戏玩腻了?那就让玩家一起玩。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。