鸿蒙分布式游戏手柄控制器设计与实现

一、系统架构设计

基于HarmonyOS的分布式设备虚拟化能力,我们设计了一个将手机转变为游戏控制器的系统,通过手机传感器和触摸屏模拟专业游戏手柄输入,实现跨设备的游戏控制体验。

https://example.com/distributed-gamepad-arch.png

系统包含三个核心模块:

  1. ​虚拟输入设备模块​​ - 使用@ohos.distributedHardware创建虚拟手柄
  2. ​传感器控制模块​​ - 通过@ohos.sensor获取手机运动数据
  3. ​分布式通信模块​​ - 利用@ohos.distributedDeviceManager建立设备间连接

二、核心代码实现

1. 虚拟手柄服务(ArkTS)

// VirtualGamepadService.ets
import distributedHardware from '@ohos.distributedHardware';
import sensor from '@ohos.sensor';
import deviceManager from '@ohos.distributedDeviceManager';

const GAMEPAD_SYNC_CHANNEL = 'virtual_gamepad_input';

class VirtualGamepadService {
  private static instance: VirtualGamepadService = null;
  private deviceManager: deviceManager.DeviceManager;
  private virtualGamepadId: string = '';
  private targetDeviceId: string = '';
  private listeners: GamepadListener[] = [];
  
  // 手柄状态
  @State leftStick: { x: number, y: number } = { x: 0, y: 0 };
  @State rightStick: { x: number, y: number } = { x: 0, y: 0 };
  @State buttons: { [key: string]: boolean } = {
    A: false, B: false, X: false, Y: false,
    L1: false, R1: false, L2: 0, R2: 0,
    Start: false, Select: false, DPadUp: false,
    DPadDown: false, DPadLeft: false, DPadRight: false
  };
  
  private constructor() {
    this.initDeviceManager();
    this.initSensors();
  }
  
  public static getInstance(): VirtualGamepadService {
    if (!VirtualGamepadService.instance) {
      VirtualGamepadService.instance = new VirtualGamepadService();
    }
    return VirtualGamepadService.instance;
  }
  
  private initDeviceManager() {
    this.deviceManager = deviceManager.createDeviceManager('com.example.virtualgamepad');
    this.deviceManager.on('deviceOnline', (device) => {
      this.listeners.forEach(listener => {
        listener.onDeviceConnected(device);
      });
    });
  }
  
  private initSensors() {
    // 加速度计用于方向控制
    sensor.on(sensor.SensorId.ACCELEROMETER, (data) => {
      this.handleAccelerometer(data);
    });
    
    // 陀螺仪用于精细控制
    sensor.on(sensor.SensorId.GYROSCOPE, (data) => {
      this.handleGyroscope(data);
    });
  }
  
  public async connectToDevice(deviceId: string): Promise<boolean> {
    try {
      // 创建虚拟游戏手柄
      const virtualDevice: distributedHardware.VirtualDevice = {
        deviceName: 'HarmonyVirtualGamepad',
        deviceType: distributedHardware.DeviceType.GAMEPAD,
        deviceId: ''
      };
      
      this.virtualGamepadId = await distributedHardware.createVirtualDevice(virtualDevice);
      this.targetDeviceId = deviceId;
      
      // 激活虚拟设备
      await distributedHardware.activateVirtualDevice(
        this.virtualGamepadId, 
        deviceId,
        (event) => {
          console.log('Virtual device event:', event);
        }
      );
      
      return true;
    } catch (err) {
      console.error('连接设备失败:', JSON.stringify(err));
      return false;
    }
  }
  
  public disconnect(): void {
    if (this.virtualGamepadId) {
      distributedHardware.deactivateVirtualDevice(this.virtualGamepadId);
      distributedHardware.releaseVirtualDevice(this.virtualGamepadId);
      this.virtualGamepadId = '';
      this.targetDeviceId = '';
    }
  }
  
  public setButtonState(button: string, pressed: boolean | number): void {
    if (button in this.buttons) {
      this.buttons[button] = pressed;
      this.syncGamepadState();
    }
  }
  
  public setStick(stick: 'left' | 'right', x: number, y: number): void {
    if (stick === 'left') {
      this.leftStick = { x, y };
    } else {
      this.rightStick = { x, y };
    }
    this.syncGamepadState();
  }
  
  private handleAccelerometer(data: sensor.AccelerometerResponse) {
    // 将加速度计数据转换为左摇杆输入
    const sensitivity = 0.1;
    const deadZone = 0.2;
    
    let x = -data.x * sensitivity;
    let y = data.y * sensitivity;
    
    // 死区处理
    if (Math.abs(x) < deadZone) x = 0;
    if (Math.abs(y) < deadZone) y = 0;
    
    this.setStick('left', x, y);
  }
  
  private handleGyroscope(data: sensor.GyroscopeResponse) {
    // 将陀螺仪数据转换为右摇杆输入
    const sensitivity = 0.05;
    const deadZone = 0.1;
    
    let x = data.z * sensitivity;
    let y = -data.x * sensitivity;
    
    // 死区处理
    if (Math.abs(x) < deadZone) x = 0;
    if (Math.abs(y) < deadZone) y = 0;
    
    this.setStick('right', x, y);
  }
  
  private syncGamepadState(): void {
    if (!this.targetDeviceId || !this.virtualGamepadId) return;
    
    const state: GamepadState = {
      leftStick: this.leftStick,
      rightStick: this.rightStick,
      buttons: this.buttons,
      timestamp: Date.now()
    };
    
    // 通过虚拟设备发送输入状态
    distributedHardware.sendVirtualDeviceData(
      this.virtualGamepadId,
      this.targetDeviceId,
      JSON.stringify(state)
    );
    
    // 同时通过分布式数据同步用于UI显示
    distributedData.sync(GAMEPAD_SYNC_CHANNEL, state);
  }
  
  public addListener(listener: GamepadListener): void {
    if (!this.listeners.includes(listener)) {
      this.listeners.push(listener);
    }
  }
  
  public removeListener(listener: GamepadListener): void {
    this.listeners = this.listeners.filter(l => l !== listener);
  }
  
  public getConnectedDevices(): deviceManager.DeviceInfo[] {
    try {
      return this.deviceManager.getAvailableDeviceListSync();
    } catch (err) {
      console.error('获取设备列表失败:', JSON.stringify(err));
      return [];
    }
  }
}

interface GamepadState {
  leftStick: { x: number, y: number };
  rightStick: { x: number, y: number };
  buttons: { [key: string]: boolean | number };
  timestamp: number;
}

interface GamepadListener {
  onDeviceConnected(device: deviceManager.DeviceInfo): void;
  onGamepadStateChanged(state: GamepadState): void;
}

export const gamepadService = VirtualGamepadService.getInstance();

2. 游戏手柄界面(ArkUI)

// VirtualGamepadUI.ets
import { gamepadService } from './VirtualGamepadService';

@Entry
@Component
struct VirtualGamepadUI {
  @State connectedDevices: deviceManager.DeviceInfo[] = [];
  @State connectedDevice: deviceManager.DeviceInfo | null = null;
  @State showControls: boolean = false;
  @State buttonLayout: 'default' | 'compact' = 'default';
  
  private gamepadListener: GamepadListener = {
    onDeviceConnected: (device) => {
      this.connectedDevices = [...this.connectedDevices, device];
    },
    onGamepadStateChanged: (state) => {
      // 可以用于显示输入状态
    }
  };
  
  aboutToAppear() {
    gamepadService.addListener(this.gamepadListener);
    this.connectedDevices = gamepadService.getConnectedDevices();
  }
  
  aboutToDisappear() {
    gamepadService.removeListener(this.gamepadListener);
  }
  
  build() {
    Column() {
      if (!this.connectedDevice) {
        this.buildDeviceList()
      } else {
        this.buildGamepadControls()
      }
    }
    .width('100%')
    .height('100%')
  }
  
  @Builder
  buildDeviceList() {
    Column() {
      Text('选择游戏设备')
        .fontSize(20)
        .margin({ bottom: 20 })
      
      if (this.connectedDevices.length === 0) {
        Text('正在搜索设备...')
          .fontSize(16)
      } else {
        List() {
          ForEach(this.connectedDevices, (device) => {
            ListItem() {
              Column() {
                Text(device.deviceName)
                  .fontSize(18)
                Text(device.deviceId)
                  .fontSize(12)
                  .margin({ top: 4 })
              }
              .width('100%')
              .padding(10)
              .onClick(async () => {
                const success = await gamepadService.connectToDevice(device.deviceId);
                if (success) {
                  this.connectedDevice = device;
                  this.showControls = true;
                }
              })
            }
          })
        }
        .layoutWeight(1)
      }
    }
    .padding(20)
  }
  
  @Builder
  buildGamepadControls() {
    Stack() {
      // 背景
      Column()
        .width('100%')
        .height('100%')
        .backgroundColor('#222222')
      
      // 方向控制区
      this.buildDirectionalControls()
      
      // 动作按钮区
      this.buildActionButtons()
      
      // 功能按钮区
      this.buildFunctionButtons()
      
      // 设置按钮
      this.buildSettingsButton()
    }
    .width('100%')
    .height('100%')
  }
  
  @Builder
  buildDirectionalControls() {
    Column() {
      // 左摇杆区域
      Stack() {
        Circle()
          .width(120)
          .height(120)
          .fill('#444444')
          .opacity(0.5)
        
        Circle()
          .width(60)
          .height(60)
          .fill('#FFFFFF')
          .position({
            x: 60 + gamepadService.leftStick.x * 30 - 30,
            y: 60 + gamepadService.leftStick.y * 30 - 30
          })
      }
      .margin({ bottom: 40 })
      
      // 右摇杆区域
      Stack() {
        Circle()
          .width(120)
          .height(120)
          .fill('#444444')
          .opacity(0.5)
        
        Circle()
          .width(60)
          .height(60)
          .fill('#FFFFFF')
          .position({
            x: 60 + gamepadService.rightStick.x * 30 - 30,
            y: 60 + gamepadService.rightStick.y * 30 - 30
          })
      }
    }
    .position({ x: '50%', y: '50%' })
    .width('100%')
    .height('60%')
    .gesture(
      GestureGroup(GestureMode.Exclusive,
        PanGesture({ fingers: 2 })
          .onActionUpdate((event: GestureEvent) => {
            // 双指分别控制左右摇杆
            if (event.fingerList.length >= 2) {
              const finger1 = event.fingerList[0];
              const finger2 = event.fingerList[1];
              
              // 判断哪个手指在左半屏,哪个在右半屏
              const leftFinger = finger1.x < finger2.x ? finger1 : finger2;
              const rightFinger = finger1.x < finger2.x ? finger2 : finger1;
              
              // 左摇杆控制
              const leftX = (leftFinger.x / this.width) * 2 - 1;
              const leftY = (leftFinger.y / (this.height * 0.6)) * 2 - 1;
              gamepadService.setStick('left', leftX, leftY);
              
              // 右摇杆控制
              const rightX = (rightFinger.x / this.width) * 2 - 1;
              const rightY = (rightFinger.y / (this.height * 0.6)) * 2 - 1;
              gamepadService.setStick('right', rightX, rightY);
            }
          })
          .onActionEnd(() => {
            gamepadService.setStick('left', 0, 0);
            gamepadService.setStick('right', 0, 0);
          })
      )
    )
  }
  
  @Builder
  buildActionButtons() {
    // ABXY按钮布局
    Row() {
      // 左侧按钮 (X, Y)
      Column() {
        Button('X')
          .width(60)
          .height(60)
          .backgroundColor(gamepadService.buttons.X ? '#FF0000' : '#880000')
          .onTouch((event: TouchEvent) => {
            gamepadService.setButtonState('X', event.type === TouchType.Down);
          })
          .margin({ bottom: 20 })
        
        Button('Y')
          .width(60)
          .height(60)
          .backgroundColor(gamepadService.buttons.Y ? '#FFFF00' : '#888800')
          .onTouch((event: TouchEvent) => {
            gamepadService.setButtonState('Y', event.type === TouchType.Down);
          })
      }
      .margin({ right: 80 })
      
      // 右侧按钮 (A, B)
      Column() {
        Button('A')
          .width(60)
          .height(60)
          .backgroundColor(gamepadService.buttons.A ? '#00FF00' : '#008800')
          .onTouch((event: TouchEvent) => {
            gamepadService.setButtonState('A', event.type === TouchType.Down);
          })
          .margin({ bottom: 20 })
        
        Button('B')
          .width(60)
          .height(60)
          .backgroundColor(gamepadService.buttons.B ? '#0000FF' : '#000088')
          .onTouch((event: TouchEvent) => {
            gamepadService.setButtonState('B', event.type === TouchType.Down);
          })
      }
    }
    .position({ x: '50%', y: '80%' })
    .translate({ x: '-50%', y: '-50%' })
  }
  
  @Builder
  buildFunctionButtons() {
    // L1/R1/L2/R2按钮
    Row() {
      // 左侧肩键
      Column() {
        Button('L1')
          .width(80)
          .height(40)
          .backgroundColor(gamepadService.buttons.L1 ? '#FFFFFF' : '#888888')
          .onTouch((event: TouchEvent) => {
            gamepadService.setButtonState('L1', event.type === TouchType.Down);
          })
          .margin({ bottom: 10 })
        
        Button('L2')
          .width(80)
          .height(40)
          .backgroundColor('#888888')
          .gesture(
            PanGesture()
              .onActionUpdate((event: GestureEvent) => {
                const value = Math.min(1, Math.max(0, event.offsetY / 40));
                gamepadService.setButtonState('L2', value);
              })
              .onActionEnd(() => {
                gamepadService.setButtonState('L2', 0);
              })
          )
      }
      .margin({ right: 60 })
      
      // 右侧肩键
      Column() {
        Button('R1')
          .width(80)
          .height(40)
          .backgroundColor(gamepadService.buttons.R1 ? '#FFFFFF' : '#888888')
          .onTouch((event: TouchEvent) => {
            gamepadService.setButtonState('R1', event.type === TouchType.Down);
          })
          .margin({ bottom: 10 })
        
        Button('R2')
          .width(80)
          .height(40)
          .backgroundColor('#888888')
          .gesture(
            PanGesture()
              .onActionUpdate((event: GestureEvent) => {
                const value = Math.min(1, Math.max(0, event.offsetY / 40));
                gamepadService.setButtonState('R2', value);
              })
              .onActionEnd(() => {
                gamepadService.setButtonState('R2', 0);
              })
          )
      }
    }
    .position({ x: '50%', y: '10%' })
    .translate({ x: '-50%', y: '0%' })
  }
  
  @Builder
  buildSettingsButton() {
    Row() {
      Button('断开连接')
        .width(120)
        .height(40)
        .onClick(() => {
          gamepadService.disconnect();
          this.connectedDevice = null;
          this.showControls = false;
        })
        .margin({ right: 20 })
      
      Button(this.buttonLayout === 'default' ? '紧凑布局' : '默认布局')
        .width(120)
        .height(40)
        .onClick(() => {
          this.buttonLayout = this.buttonLayout === 'default' ? 'compact' : 'default';
        })
    }
    .position({ x: '50%', y: '95%' })
    .translate({ x: '-50%', y: '-50%' })
  }
}

3. 游戏主机端接收器(ArkTS)

// GameConsoleReceiver.ets
import distributedHardware from '@ohos.distributedHardware';

class GameConsoleReceiver {
  private static instance: GameConsoleReceiver = null;
  private virtualGamepadId: string = '';
  private listeners: ConsoleListener[] = [];
  
  private constructor() {}
  
  public static getInstance(): GameConsoleReceiver {
    if (!GameConsoleReceiver.instance) {
      GameConsoleReceiver.instance = new GameConsoleReceiver();
    }
    return GameConsoleReceiver.instance;
  }
  
  public async registerVirtualGamepad(): Promise<boolean> {
    try {
      const callback = (deviceId: string, data: string) => {
        try {
          const state = JSON.parse(data) as GamepadState;
          this.listeners.forEach(listener => {
            listener.onGamepadInput(state);
          });
        } catch (err) {
          console.error('解析游戏手柄数据失败:', JSON.stringify(err));
        }
      };
      
      this.virtualGamepadId = await distributedHardware.registerVirtualDeviceListener(
        distributedHardware.DeviceType.GAMEPAD,
        callback
      );
      
      return true;
    } catch (err) {
      console.error('注册虚拟设备监听失败:', JSON.stringify(err));
      return false;
    }
  }
  
  public unregisterVirtualGamepad(): void {
    if (this.virtualGamepadId) {
      distributedHardware.unregisterVirtualDeviceListener(this.virtualGamepadId);
      this.virtualGamepadId = '';
    }
  }
  
  public addListener(listener: ConsoleListener): void {
    if (!this.listeners.includes(listener)) {
      this.listeners.push(listener);
    }
  }
  
  public removeListener(listener: ConsoleListener): void {
    this.listeners = this.listeners.filter(l => l !== listener);
  }
}

interface ConsoleListener {
  onGamepadInput(state: GamepadState): void;
}

export const consoleReceiver = GameConsoleReceiver.getInstance();

三、项目配置

1. 权限配置

// module.json5
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.DISTRIBUTED_DATASYNC",
        "reason": "跨设备同步游戏控制指令"
      },
      {
        "name": "ohos.permission.ACCELEROMETER",
        "reason": "获取加速度传感器数据"
      },
      {
        "name": "ohos.permission.GYROSCOPE",
        "reason": "获取陀螺仪传感器数据"
      },
      {
        "name": "ohos.permission.DISTRIBUTED_DEVICE_STATE_CHANGE",
        "reason": "发现和管理分布式设备"
      },
      {
        "name": "ohos.permission.MANAGE_VIRTUAL_DEVICE",
        "reason": "创建和管理虚拟设备"
      }
    ],
    "abilities": [
      {
        "name": "MainAbility",
        "type": "page",
        "visible": true
      }
    ],
    "distributedNotification": {
      "scenarios": [
        {
          "name": "virtual_gamepad",
          "value": "game_controller"
        }
      ]
    }
  }
}

2. 资源文件

<!-- resources/base/element/string.json -->
{
  "string": [
    {
      "name": "app_name",
      "value": "虚拟游戏手柄"
    },
    {
      "name": "select_device",
      "value": "选择游戏设备"
    },
    {
      "name": "searching_devices",
      "value": "正在搜索设备..."
    },
    {
      "name": "disconnect",
      "value": "断开连接"
    },
    {
      "name": "compact_layout",
      "value": "紧凑布局"
    },
    {
      "name": "default_layout",
      "value": "默认布局"
    }
  ]
}

四、功能扩展

1. 触觉反馈增强

// 在VirtualGamepadUI中添加触觉反馈
import vibrator from '@ohos.vibrator';

class VirtualGamepadUI {
  private playHapticFeedback(effect: 'light' | 'medium' | 'heavy'): void {
    const pattern = {
      intensity: effect === 'light' ? 50 : effect === 'medium' ? 75 : 100,
      duration: effect === 'light' ? 20 : effect === 'medium' ? 40 : 60
    };
    
    vibrator.vibrate(pattern, (err) => {
      if (err) console.error('触觉反馈失败:', JSON.stringify(err));
    });
  }
  
  // 在按钮触摸事件中添加反馈
  @Builder
  buildActionButtons() {
    Button('A')
      // ...其他属性
      .onTouch((event: TouchEvent) => {
        gamepadService.setButtonState('A', event.type === TouchType.Down);
        if (event.type === TouchType.Down) {
          this.playHapticFeedback('medium');
        }
      })
    // ...其他按钮
  }
}

2. 自定义控制布局

// 在VirtualGamepadService中添加布局配置
class VirtualGamepadService {
  private buttonMapping: { [key: string]: string } = {
    A: 'A', B: 'B', X: 'X', Y: 'Y',
    L1: 'L1', R1: 'R1', L2: 'L2', R2: 'R2'
  };
  
  public remapButton(virtualButton: string, targetButton: string): void {
    if (targetButton in this.buttons) {
      this.buttonMapping[virtualButton] = targetButton;
    }
  }
  
  public setButtonState(button: string, pressed: boolean | number): void {
    const targetButton = this.buttonMapping[button] || button;
    if (targetButton in this.buttons) {
      this.buttons[targetButton] = pressed;
      this.syncGamepadState();
    }
  }
}

// 在UI中添加布局编辑器
@Builder
buildLayoutEditor() {
  Column() {
    Text('按钮映射配置')
      .fontSize(18)
      .margin({ bottom: 20 })
    
    ForEach(Object.entries(gamepadService.buttonMapping), ([virtual, target]) => {
      Row() {
        Text(`${virtual} →`)
          .width(60)
        
        Picker({
          selected: target,
          range: ['A', 'B', 'X', 'Y', 'L1', 'R1', 'L2', 'R2']
        })
          .onChange((value: string) => {
            gamepadService.remapButton(virtual, value);
          })
          .width(120)
      }
      .margin({ bottom: 10 })
    })
  }
  .padding(20)
}

3. 多手柄支持

// 在GameConsoleReceiver中增强多手柄支持
class GameConsoleReceiver {
  private gamepadStates: { [deviceId: string]: GamepadState } = {};
  
  private handleGamepadInput(deviceId: string, state: GamepadState): void {
    this.gamepadStates[deviceId] = state;
    
    this.listeners.forEach(listener => {
      listener.onGamepadInput(deviceId, state);
    });
  }
  
  public getGamepadState(deviceId: string): GamepadState | null {
    return this.gamepadStates[deviceId] || null;
  }
  
  public getAllGamepadStates(): { [deviceId: string]: GamepadState } {
    return { ...this.gamepadStates };
  }
}

// 游戏逻辑中可以区分不同玩家
consoleReceiver.addListener({
  onGamepadInput: (deviceId, state) => {
    if (deviceId === player1DeviceId) {
      // 玩家1逻辑
    } else if (deviceId === player2DeviceId) {
      // 玩家2逻辑
    }
  }
});

五、总结

通过这个分布式游戏手柄控制器的实现,我们学习了:

  1. 使用HarmonyOS虚拟设备能力创建虚拟游戏手柄
  2. 将手机传感器数据转换为游戏控制输入
  3. 设计直观的游戏控制界面
  4. 实现低延迟的跨设备控制指令传输
  5. 添加触觉反馈增强用户体验

系统特点:

  • 将普通手机转变为专业游戏控制器
  • 支持多种输入方式(触摸、传感器)
  • 可自定义的控制布局和按钮映射
  • 低延迟的分布式通信
  • 支持多手柄同时连接

这个系统可以进一步扩展为功能更完善的游戏控制平台,如:

  • 添加更多控制器类型支持(方向盘、跳舞毯等)
  • 实现控制配置云同步
  • 添加游戏内快捷指令
  • 支持控制器固件OTA升级
  • 开发配套的游戏中心应用
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐