掘金:https://juejin.cn/post/7528405614440726582
最近用手柄玩星铁和绝区零的时候,发现他们的手柄适配做的很舒服,就突发奇想,web这边有没有现成的手柄库。但是搜了一圈,竟然没能找到一个像样的库(都是简单调用api,没有封装),难道没这样的需求吗😅。于是打算自己手撸一个了💢。
不说废话,先贴仓库链接,web-gamepad
需求不高,能像游戏一样,支持以下功能
俗话说,技术为需求服务,那么,设计也得围绕需求来,下面是对以上需求的设计思路。这里就不写api方法了(网上都能查到),只讲思路
众所周知,市面上有很多手柄厂商,每个厂商生产出来的手柄都不太一样,但大多遵守一套手柄按键标准。左右两侧四个按钮、顶部四个按钮、中间三个按钮、左右两个摇杆。
既然有标准,那就很简单了,可以用标准的key值做为参数去绑定事件。不遵守标准的,可以设计成自定义传参兼容。
在我的库里,提供了xbox和ps5的手柄按键映射表,如下:
/** xbox key map */
export const XBOX_KEY_MAP = {
A: 0,
B: 1,
X: 2,
Y: 3,
LB: 4,
RB: 5,
LT: 6,
RT: 7,
view: 8,
menu: 9,
LS: 10,
RS: 11,
up: 12,
down: 13,
left: 14,
right: 15,
home: 16
} as const
/** ps5 key map */
export const PS5_BUTTON_MAP = {
cross: 0,
circle: 1,
square: 2,
triangle: 3,
L1: 4,
R1: 5,
L2: 6,
R2: 7,
share: 8,
options: 9,
L3: 10,
R3: 11,
PS: 12,
touchpad: 13,
microphone: 14,
up: 15,
down: 16,
left: 17,
right: 18
} as const
// 绑定按钮监听事件,传入对应手柄按键的key值和输入事件类型
controller.addBtnEvents(XBOX_KEY_MAP.A, INPUT_TYPE.down, () => {})
这个需求是最核心的一个,要怎么理解呢,这里用星铁来举例:
在地图中,RB是跑步
但切换到每日任务中,RB是切换tab栏
也就是说,同一个按钮,会存在多个不同ui场景组件的事件绑定,但是因为按键监听是全局的,就不能简单在监听事件里面绑定,组件多了,管理起来会很混乱。
我的设计思路如下:
全局一个manager管理多个ui组件的事件(controller)切换,切换某个controller,就只触发该controller下的事件,这样就能实现像星铁一样切换事件
怎么理解呢,还是看图吧(以下是我自己的拆分)
如图,可以拆分多个组件,在前端中也是很常见的应用场景,不同组件分别控制手柄不同按钮。
这个其实也很好解决,controller继续细分控制每个按钮事件禁用就行了。
代码:
import {
createGamepadController,
XBOX_KEY_MAP,
INPUT_TYPE,
switchGamepadController
} from 'web-gamepad'
// 创建多个控制器
const controller = createGamepadController('btn1')
const menuController = createGamepadController('drawer', false)
const menu = ref(false)
controller.addBtnEvents(XBOX_KEY_MAP.menu, INPUT_TYPE.down, () => {
menu.value = true // 打开菜单栏
// 切换菜单Controller
menuGamepadController(drawerController.id)
})
controller.addBtnEvents(XBOX_KEY_MAP.menu, INPUT_TYPE.down, () => {
menu.value = true // 打开菜单栏
// 切换菜单Controller
menuGamepadController(menuGamepadController.id)
})
有多个场景ui切换,必然要有回溯之前ui场景的功能。比如:
一个提示弹窗,可以由多个组件触发,当关闭弹窗后,得切换回之前的ui场景(controller)。
如果没有回溯功能,那么关闭弹窗就得写很多if-else判断之前是哪个组件(controller)触发的
这里就用栈去存储之前的controller id,当触发回溯时,取栈的第一个id
代码:
const idStack: string[] = []
// 记录之前激活的controller id,用setTimeout是为了收集同一时间激活的controllerid
function recordActiveIdStack() {
if (recordTimeout) {
return
}
recordTimeout = setTimeout(() => {
// 获取全部激活的controller id
const ids = getActiveControllers()
.map((item) => item.id)
.join(',')
ids && idStack.push(ids)
clearTimeout(recordTimeout)
recordTimeout = null
})
}
function recallController(offset: number) {
if (offset <= 0) {
return
}
const [recallId] = idStack.splice(
-clamp(Math.abs(offset) + 1, 0, idStack.length)
)
switchGamepadController(recallId.split(',').filter((id) => id))
}
// ...省略业务代码
// 当关闭弹窗是,回退前1位激活的controllers
function handleClose() {
recallController(1)
}
这个需求也很简单,web的gamepad本身就支持监听多个手柄输入,在根据上面的manager设计,就很好实现管理
代码:
const GamepadManager: {
[key: number]: Gamepad
} = {}
function addGamepad(gamepad: Gamepad) {
GamepadManager[gamepad.index] = gamepad
}
window.addEventListener(
'gamepadconnected',
function ({ gamepad }: GamepadEvent) {
addGamepad(gamepad)
}
)
至此,核心需求算是解决了,当然,还有很多细节问题这里就不细说了。
参考文档:https://w3c.github.io/gamepad/#remapping
欢迎大家来提issue,之后会开发更多有趣的库~
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。