Golang获取游戏手柄输入及控制器类型的方法

Golang获取游戏手柄输入及控制器类型的方法 大家好 我目前正在使用 GitHub - 0xcafed00d/joystick: Go Joystick API 来获取手柄状态——在基本形式上,它可以在 Windows 上配合 Xbox360(兼容)手柄、DualShock 4 以及 Legion Go 手柄工作。

然而,DS4 和 Xbox 手柄的面部按钮(我确信摇杆轴也是如此)的按键映射并不相同,也就是说,我希望这些按钮能对应相同的(掩码)值:

  • Ⓐ/✕
  • Ⓑ/◯
  • Ⓧ/☐
  • Ⓨ/🛆

似乎没有办法识别控制器类型(使用底层的 Windows 多媒体(旧版)dll/api)。

有没有人知道有哪个包能更好地处理这个问题——而不需要采用整个框架?请注意,这不是用于游戏开发,而是用于一个可以响应游戏手柄输入的后台(操作系统级别)实用程序。

到目前为止,我考虑过的方案包括:

  1. 我可以猜测控制器类型,因为 DS4 返回的按钮和摇杆轴数量与 Xbox360 不同——但我找不到任何地方有相关文档,所以这只是基于 2/3 个控制器的猜测……
  2. 自己封装 Windows 调用——但这似乎有些过头了(而且是操作系统特定的),而且我在这方面没有任何经验,很可能是在重复造轮子……
  3. Windows Gaming Input——但我没有看到任何 Golang 访问该 API 的例子,而且看起来你可能需要购买整个 SDK。
  4. SDL——但这对于我的目的来说看起来太臃肿了,而且看起来也不太可能解决这个问题。

请注意,我更喜欢跨平台的解决方案,但主要目标是 Windows……

提前感谢 - Andy


更多关于Golang获取游戏手柄输入及控制器类型的方法的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

我尝试了一些不同的方法,可能对遇到类似问题的人有所帮助:

  1. 我直接访问了 Windows 的 xinput dll。这仅对 XBox 360 兼容的摇杆有效。不幸的是,摇杆编号与多媒体编号不匹配,因此无法用于识别 Xbox 摇杆。

  2. 我尝试访问(新的)GameInput,并设法获取了 dll 以及访问了第一个 API 调用。但我的 Windows C++ 知识为零,所以当我遇到指针的指针时放弃了——将 C++ 代码转换到 Go 需要学习的东西太多了。

我现在正在使用 XInput,具体是通过 xinput1_3.dll 中的 XInputGetState 函数来“自己动手”实现。

我确实曾回退到使用 GitHub - 0xcafed00d/joystick: Go Joystick API,但在 Legion Go 上使用并看到非常奇怪的手柄属性(我认为是 directinput 控制器)后就放弃了。

祝好 - Andy

更多关于Golang获取游戏手柄输入及控制器类型的方法的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


供参考,以下是我的解决方案,或许对其他人有用……

主要的调用将是 go gamepad.CheckChanged() 这将(在我的情况下)向任何监听的客户端发送一个套接字广播的JSON数据……

包的内容如下:

package gamepad

import (
	"encoding/json"
	"fmt"
	"quando/internal/server/socket"
	"syscall"
	"time"
	"unsafe"
)

const (
	MAX_GAMEPADS        = 4
	XINPUT_DLL_FILENAME = "xinput1_3.dll"
	XINPUT_GET_STATE    = "XInputGetState"
	// 按钮映射供参考
	// "UP" 0x0001
	// "DOWN" 0x0002
	// "LEFT" 0x0004
	// "RIGHT" 0x0008
	// "START" 0x0010
	// "BACK" 0x0020
	// "L_STICK" 0x0040
	// "R_STICK" 0x0080
	// "L_BUMPER" 0x0100
	// "R_BUMPER" 0x0200
	// "A" 0x1000
	// "B" 0x2000
	// "X" 0x4000
	// "Y" 0x8000
	// 注意:HOME/GUIDE 键在标准调用中未映射 - 应为 0x0400
)

var getState *syscall.Proc

type Gamepad struct {
	_             uint32 // packet - 更新过于频繁且无用
	button_mask   uint16
	left_trigger  uint8
	right_trigger uint8
	left_x        int16
	left_y        int16
	right_x       int16
	right_y       int16
}

var gamepads [MAX_GAMEPADS]*Gamepad // 存储上次返回的状态以识别变化 - 或为 nil

type gamepadJSON struct {
	Id       int8   `json:"id"`
	Drop     bool   `json:"drop,omitempty"`
	Mask     uint16 `json:"mask,omitempty"`
	Ltrigger uint8  `json:"l_trigger,omitempty"`
	Rtrigger uint8  `json:"r_trigger,omitempty"`
	Lx       int16  `json:"l_x,omitempty"`
	Ly       int16  `json:"l_y,omitempty"`
	Rx       int16  `json:"r_x,omitempty"`
	Ry       int16  `json:"r_y,omitempty"`
}

func triggersChanged(gamepad_old, gamepad_new Gamepad) bool {
	// 一旦检测到变化就返回
	if gamepad_old.left_trigger != gamepad_new.left_trigger {
		return true
	}
	return gamepad_old.right_trigger != gamepad_new.right_trigger
}

func axesChanged(gamepad_old, gamepad_new Gamepad) bool {
	// 一旦检测到变化就返回
	if gamepad_old.left_x != gamepad_new.left_x {
		return true
	}
	if gamepad_old.left_y != gamepad_new.left_y {
		return true
	}
	if gamepad_old.right_x != gamepad_new.right_x {
		return true
	}
	return gamepad_old.right_y != gamepad_new.right_y
}

func gamepadUpdated(num uint) bool {
	changed := false
	var gamepad Gamepad
	result, _, _ := getState.Call(uintptr(num), uintptr(unsafe.Pointer(&gamepad)))
	if result == 0 { // 成功
		if gamepads[num] == nil {
			fmt.Println("Gamepad connected : ", num)
			changed = true
		} else {
			var last_gamepad = gamepads[num]
			if last_gamepad.button_mask != gamepad.button_mask {
				changed = true
			} else if triggersChanged(*last_gamepad, gamepad) {
				changed = true
			} else if axesChanged(*last_gamepad, gamepad) {
				changed = true
			}
		}
		gamepads[num] = &gamepad // 即使状态未改变也始终更新
	} else if gamepads[num] != nil { // 刚刚断开连接
		changed = true
		gamepads[num] = nil
	}
	return changed
}

func addPostJSON(gamepad *Gamepad, num int, gamepad_json *gamepadJSON) {
	gamepad_json.Id = int8(num)
	if gamepad == nil { // 已断开
		gamepad_json.Drop = true
	} else {
		gamepad_json.Mask = gamepad.button_mask
		gamepad_json.Ltrigger = gamepad.left_trigger
		gamepad_json.Rtrigger = gamepad.right_trigger
		gamepad_json.Lx = gamepad.left_x
		gamepad_json.Ly = gamepad.left_y
		gamepad_json.Rx = gamepad.right_x
		gamepad_json.Ry = gamepad.right_y
	}
}

func CheckChanged() {
	if getState == nil {
		fmt.Println("** XInput joystick not being checked...")
		return
	} // else
	for {
		updated := false
		gamepad_json := gamepadJSON{}
		for num := range MAX_GAMEPADS { // 注意这将是 0..3
			if gamepadUpdated(uint(num)) {
				updated = true
				var gamepad *Gamepad // 为 nil
				if gamepads[num] != nil {
					gamepad = gamepads[num]
				}
				addPostJSON(gamepad, num, &gamepad_json)
			}
		}
		if updated {
			bout, err := json.Marshal(gamepad_json)
			if err != nil {
				fmt.Println("Error marshalling gamepad", err)
			} else {
				str := string(bout)
				prefix := `{"type":"gamepad"`
				if str != "{}" {
					prefix += ","
				}
				str = prefix + str[1:]
				socket.Broadcast(str)
			}
		}
		time.Sleep(time.Second / 60) // 每秒 60 次
	}
}

func init() {
	dll, err := syscall.LoadDLL(XINPUT_DLL_FILENAME) // 暂时使用旧版本
	if err != nil {
		fmt.Println("** Failed to find", XINPUT_DLL_FILENAME)
	} else {
		getState, err = dll.FindProc(XINPUT_GET_STATE)
		if err != nil {
			fmt.Println("** Failed to find proc :", XINPUT_GET_STATE)
		}
	}
}

在Go中处理游戏手柄输入并识别控制器类型确实是一个挑战,特别是需要跨平台兼容时。虽然joystick包提供了基础功能,但缺乏控制器类型识别和标准化映射。以下是几种可行的技术方案:

1. 使用SDL2的Go绑定(推荐)

SDL2内置了完善的控制器类型识别和标准化映射功能。虽然你担心它"臃肿",但实际上可以只使用控制器相关的子系统:

package main

import (
    "fmt"
    "github.com/veandco/go-sdl2/sdl"
)

func main() {
    if err := sdl.Init(sdl.INIT_GAMECONTROLLER); err != nil {
        panic(err)
    }
    defer sdl.Quit()

    // 加载游戏控制器数据库
    sdl.GameControllerAddMappingsFromFile("gamecontrollerdb.txt")

    // 检测连接的控制器
    for i := 0; i < sdl.NumJoysticks(); i++ {
        if sdl.IsGameController(i) {
            controller, err := sdl.GameControllerOpen(i)
            if err != nil {
                continue
            }
            defer controller.Close()

            // 获取控制器信息
            name := controller.Name()
            fmt.Printf("控制器 %d: %s\n", i, name)

            // 标准化按钮映射(SDL会自动处理)
            // Xbox: A=0, B=1, X=2, Y=3
            // PS4: Cross=0, Circle=1, Square=2, Triangle=3
        }
    }

    // 事件循环
    running := true
    for running {
        for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
            switch e := event.(type) {
            case *sdl.ControllerButtonEvent:
                // 标准化按钮事件
                fmt.Printf("按钮: %v 状态: %v\n", 
                    sdl.GameControllerGetStringForButton(e.Button),
                    e.State)
            case *sdl.ControllerAxisEvent:
                // 标准化摇杆事件
                fmt.Printf("摇杆: %v 值: %v\n",
                    sdl.GameControllerGetStringForAxis(e.Axis),
                    e.Value)
            case *sdl.QuitEvent:
                running = false
            }
        }
        sdl.Delay(16)
    }
}

2. 使用glfw的Go绑定

GLFW也提供了游戏手柄支持,比SDL更轻量:

package main

import (
    "fmt"
    "github.com/go-gl/glfw/v3.3/glfw"
)

func main() {
    if err := glfw.Init(); err != nil {
        panic(err)
    }
    defer glfw.Terminate()

    // 检测游戏手柄
    for joy := glfw.Joystick1; joy <= glfw.JoystickLast; joy++ {
        if glfw.JoystickPresent(joy) {
            name := glfw.GetJoystickName(joy)
            guid := glfw.GetJoystickGUID(joy)
            axes := glfw.GetJoystickAxes(joy)
            buttons := glfw.GetJoystickButtons(joy)
            
            fmt.Printf("手柄 %d: %s (GUID: %s)\n", joy, name, guid)
            fmt.Printf("  摇杆数: %d, 按钮数: %d\n", len(axes), len(buttons))
            
            // 根据GUID或按钮数量判断控制器类型
            if isXboxController(guid) {
                fmt.Println("  类型: Xbox控制器")
            } else if isPS4Controller(guid) {
                fmt.Println("  类型: PS4控制器")
            }
        }
    }
}

func isXboxController(guid string) bool {
    // 根据GUID判断Xbox控制器
    return len(guid) > 0 && (guid[0] == '7' || guid[0] == '8')
}

func isPS4Controller(guid string) bool {
    // 根据GUID判断PS4控制器
    return len(guid) > 0 && guid[0] == '0'
}

3. 使用go-joycon包(支持多种控制器)

package main

import (
    "fmt"
    "github.com/dylan-mitchell/go-joycon"
)

func main() {
    // 获取所有连接的控制器
    devices, err := joycon.GetDevices()
    if err != nil {
        panic(err)
    }

    for _, device := range devices {
        fmt.Printf("设备: %s\n", device.Path)
        
        // 打开设备
        jc, err := joycon.NewJoycon(device.Path, joycon.DeviceTypeAuto)
        if err != nil {
            continue
        }
        defer jc.Close()

        // 获取设备信息
        info := jc.GetDeviceInfo()
        fmt.Printf("  类型: %v\n", info.Type)
        fmt.Printf("  名称: %s\n", info.Name)
        
        // 读取输入
        state, err := jc.ReadState()
        if err != nil {
            continue
        }
        
        // 根据控制器类型映射按钮
        switch info.Type {
        case joycon.TypeXbox:
            // Xbox按钮映射
            if state.Buttons&0x1000 != 0 { // A按钮
                fmt.Println("  A按钮按下")
            }
        case joycon.TypePS4:
            // PS4按钮映射
            if state.Buttons&0x1000 != 0 { // Cross按钮
                fmt.Println("  Cross按钮按下")
            }
        }
    }
}

4. Windows专用方案:使用syscall调用XInput

如果只需要Windows支持,可以直接调用XInput API:

package main

import (
    "fmt"
    "syscall"
    "unsafe"
)

var (
    xinput = syscall.NewLazyDLL("xinput1_4.dll")
    procXInputGetState = xinput.NewProc("XInputGetState")
)

type XInputState struct {
    dwPacketNumber uint32
    Gamepad        XInputGamepad
}

type XInputGamepad struct {
    wButtons      uint16
    bLeftTrigger  byte
    bRightTrigger byte
    sThumbLX      int16
    sThumbLY      int16
    sThumbRX      int16
    sThumbRY      int16
}

func GetControllerState(userIndex uint32) (*XInputState, error) {
    var state XInputState
    ret, _, _ := procXInputGetState.Call(
        uintptr(userIndex),
        uintptr(unsafe.Pointer(&state)),
    )
    
    if ret != 0 {
        return nil, fmt.Errorf("XInput error: %d", ret)
    }
    return &state, nil
}

func main() {
    // 检测最多4个Xbox控制器
    for i := uint32(0); i < 4; i++ {
        state, err := GetControllerState(i)
        if err == nil {
            fmt.Printf("Xbox控制器 %d 已连接\n", i)
            
            // 标准化按钮(Xbox布局)
            buttons := state.Gamepad.wButtons
            if buttons&0x1000 != 0 { // A按钮
                fmt.Println("  A按钮按下")
            }
            if buttons&0x2000 != 0 { // B按钮
                fmt.Println("  B按钮按下")
            }
            // ... 其他按钮
        }
    }
}

推荐方案

对于你的需求,我建议:

  1. 首选SDL2方案:虽然需要C依赖,但提供了最完整的跨平台控制器支持和标准化映射
  2. 次选GLFW方案:更轻量,适合后台程序,但需要自己处理控制器类型识别
  3. Windows专用:如果只需要Windows,直接使用XInput是最直接的

SDL2的gamecontrollerdb.txt包含了数百种控制器的映射配置,能自动处理不同控制器的按钮映射问题,这是其他方案难以比拟的优势。

回到顶部