当你插入控制器时,你会按下按钮、移动操纵杆、扣动扳机……而作为开发人员,你看不到这些。当然,浏览器会识别它,但除非您在控制台中记录数字,否则它是不可见的。这就是 Gamepad API 令人头疼的地方。 它已经存在很多年了,而且实际上非常强大。你可以阅读按钮、操纵杆、触发器和作品。但大多数人都不会碰它。为什么?因为没有反馈。开发者工具中没有面板。没有明确的方法可以知道控制器是否按照您的想法进行操作。感觉就像盲目飞行。 这让我很烦恼,因此我构建了一个小工具:Gamepad Cascade Debugger。您无需盯着控制台输出,而是可以看到控制器的实时交互式视图。按下某个东西,它就会在屏幕上做出反应。通过 CSS Cascade Layers,样式保持井井有条,因此调试起来更加干净。 在这篇文章中,我将向您展示为什么调试控制器如此痛苦,CSS 如何帮助清理它,以及如何为您自己的项目构建可重用的可视化调试器。
即使您能够将它们全部记录下来,您很快也会收到无法读取的控制台垃圾邮件。例如: [0,0,1,0,0,0.5,0,...] [0,0,0,0,1,0,0,...] [0,0,1,0,0,0,0,...]
你能看出按下的是哪个按钮吗?也许吧,但前提是你睁大眼睛并错过了一些输入。所以,不,在读取输入时调试并不容易。 问题三:缺乏结构 即使您组合了一个快速的可视化工具,样式也会很快变得混乱。默认、活动和调试状态可能会重叠,如果没有清晰的结构,您的 CSS 就会变得脆弱且难以扩展。 CSS 级联层可以提供帮助。他们将样式分组为按优先级排序的“层”,因此您无需再与特异性作斗争并猜测“为什么我的调试样式没有显示?”相反,您维护单独的关注点:
底座:控制器的标准初始外观。 活动:突出显示按下的按钮和移动的操纵杆。 调试:为开发人员提供的叠加层(例如数字读数、指南等)。
如果我们根据这个在 CSS 中定义图层,我们会有: /* 优先级从低到高 */ @层基础,活动,调试;
@层基础{ /* ... */ }
@层活跃{ /* ... */ }
@层调试{ /* ... */ }
由于每一层的堆叠都是可预测的,因此您始终知道哪些规则获胜。这种可预测性不仅使调试变得更加容易,而且实际上易于管理。 我们已经讨论了问题(不可见、混乱的输入)和方法(使用级联层构建的可视化调试器)。现在我们将逐步完成构建调试器的过程。 调试器概念 使隐藏的输入可见的最简单方法是将其绘制在屏幕上。这就是这个调试器的作用。按钮、触发器和操纵杆都具有视觉效果。
按 A:一个圆圈亮起。 轻推棍子:圆圈会滑动。 半扣动扳机:条形填满一半。
现在您不再盯着 0 和 1,而是实际观看控制器的实时反应。 当然,一旦你开始堆积默认、按下、调试信息,甚至记录模式等状态,CSS 就会开始变得更大、更复杂。这就是级联层派上用场的地方。这是一个精简的示例: @层基础{ .按钮{ 背景:#222; 边界半径:50%; 宽度:40px; 高度:40px; } }
@层活跃{ .button.pressed { 背景:#0f0; /* 亮绿色 */ } }
@层调试{ .button::之后{ 内容:attr(数据值); 字体大小:12px; 颜色:#fff; } }
层顺序很重要:基础→活动→调试。
底座绘制控制器。 活动处理按下状态。 调试会抛出覆盖层。
像这样分解它意味着你不会打奇怪的特异性战争。每一层都有它的位置,你总是知道什么会获胜。 构建它 让我们先在屏幕上显示一些内容。它不需要看起来很好——只需要存在,这样我们就有了可以使用的东西。
游戏手柄级联调试器
这实际上只是盒子。虽然还不令人兴奋,但它为我们提供了稍后使用 CSS 和 JavaScript 获取的句柄。 好的,我在这里使用级联层,因为一旦添加更多状态,它就能使内容保持井井有条。这是一个粗略的过程:
/* ===================================== 级联层设置 顺序很重要:基础 → 活动 → 调试 ===================================== */
/* 预先定义层顺序 */ @层基础,活动,调试;
/* 第 1 层:基本样式 - 默认外观 */ @层基础{ .按钮{ 背景:#333; 边界半径:50%; 宽度:70 像素; 高度:70 像素; 显示:柔性; 调整内容:居中; 对齐项目:居中; }
.暂停{ 宽度:20px; 高度:70 像素; 背景:#333; 显示:内联块; } }
/* 第 2 层:活动状态 - 处理按下的按钮 */ @层活跃{ .button.active { 背景:#0f0; /* 按下时呈亮绿色 */ 变换:缩放(1.1); /* 稍微放大按钮 */ }
.pause.active { 背景:#0f0; 变换:scaleY(1.1); /* 按下时垂直拉伸 */ } }
/* 第 3 层:调试覆盖 - 开发人员信息 */ @层调试{ .button::之后{ 内容:attr(数据值); /* 显示数值 */ 字体大小:12px; 颜色:#fff; } }
这种方法的优点在于每一层都有明确的目的。无论特殊性如何,基础层永远不能覆盖 active,并且 active 永远不能覆盖 debug。这消除了通常困扰调试工具的 CSS 特异性之争。 现在看起来有些簇位于黑暗的背景上。老实说,还不错。
添加 JavaScript JavaScript 时间。这是控制器实际做某事的地方。我们将逐步构建这个。 第 1 步:设置状态管理 首先,我们需要变量来跟踪调试器的状态: // ===================================== // 状态管理 // =====================================
让运行= false; // 跟踪调试器是否处于活动状态 让拉菲尔德; // 存储用于取消的 requestAnimationFrame ID
这些变量控制连续读取游戏手柄输入的动画循环。 第 2 步:获取 DOM 引用 接下来,我们获取对要更新的所有 HTML 元素的引用: // ===================================== // DOM 元素引用 // =====================================
const btnA = document.getElementById("btn-a"); const btnB = document.getElementById("btn-b"); const btnX = document.getElementById("btn-x"); constpause1 = document.getElementById("pause1"); constpause2 = document.getElementById("pause2"); const status = document.getElementById("status");
预先存储这些引用比重复查询 DOM 更有效。 第 3 步:添加键盘后备 为了在没有物理控制器的情况下进行测试,我们将键盘按键映射到按钮: // ===================================== // 键盘回退(用于没有控制器的测试) // =====================================
常量键映射 = { “a”:btnA, “b”:btnB, “x”:btnX, "p": [pause1,pause2] // 'p'键控制两个暂停条 };
这让我们可以通过按键盘上的按键来测试 UI。 第 4 步:创建主更新循环 这就是奇迹发生的地方。该函数连续运行并读取游戏手柄状态: // ===================================== // 主游戏手柄更新循环 // =====================================
函数更新游戏手柄(){ // 获取所有连接的游戏手柄 const gamepads = navigator.getGamepads(); if (!gamepads) 返回;
// 使用第一个连接的游戏手柄 const gp = 游戏手柄[0];
如果(GP){ // 通过切换“活动”类来更新按钮状态 btnA.classList.toggle("活动", gp.buttons[0].pressed); btnB.classList.toggle("活动", gp.buttons[1].pressed); btnX.classList.toggle("active", gp.buttons[2].pressed);
// 处理暂停按钮(大多数控制器上的按钮索引为 9) constpausePressed = gp.buttons[9].pressed; 暂停1.classList.toggle(“活动”,pausePressed); 暂停2.classList.toggle(“活动”,pausePressed);
// 构建当前按下的按钮列表以用于状态显示 让按下= []; gp.buttons.forEach((btn, i) => { if (btn.pressed)Pressed.push("按钮" + i); });
// 如果按下任何按钮则更新状态文本 if (pressed.length > 0) { status.textContent = "按下:" + Pressed.join(", "); } }
// 如果调试器正在运行则继续循环 如果(运行){ rafId = requestAnimationFrame(updateGamepad); } }
classList.toggle() 方法根据按钮是否被按下来添加或删除活动类,这会触发我们的 CSS 图层样式。 第 5 步:处理键盘事件 这些事件侦听器使键盘后备工作: // ===================================== // 键盘事件处理程序 // =====================================
document.addEventListener("keydown", (e) => { if (keyMap[e.key]) { // 处理单个或多个元素 if (Array.isArray(keyMap[e.key])) { keyMap[e.key].forEach(el => el.classList.add("active")); } 否则{ keyMap[e.key].classList.add("active"); } status.textContent = "按下的键:" + e.key.toUpperCase(); } });
document.addEventListener("keyup", (e) => { if (keyMap[e.key]) { // 释放按键时移除活动状态 if (Array.isArray(keyMap[e.key])) { keyMap[e.key].forEach(el => el.classList.remove("active")); } 否则{ keyMap[e.key].classList.remove("active"); } status.textContent = "密钥已释放:" + e.key.toUpperCase(); } });
第 6 步:添加启动/停止控制 最后,我们需要一种方法来打开和关闭调试器: // ===================================== // 打开/关闭调试器 // =====================================
document.getElementById("toggle").addEventListener("点击", () => { 运行=!运行; // 翻转运行状态
如果(运行){ status.textContent = "调试器正在运行..."; 更新游戏手柄(); // 启动更新循环 } 否则{ status.textContent = "调试器处于非活动状态"; 取消动画帧(rafId); // 停止循环 } });
所以是的,按下一个按钮,它就会发光。推动棍子,它就会移动。就是这样。 还有一件事:原始值。有时您只想看到数字,而不是灯光。
在此阶段,您应该看到:
一个简单的屏幕控制器, 当您与按钮交互时会做出反应的按钮,以及 显示按下按钮索引的可选调试读数。
为了让这个不那么抽象,这里有一个屏幕控制器实时反应的快速演示:
现在,按“开始录制”会记录所有内容,直到单击“停止录制”为止。 2. 将数据导出为 CSV/JSON 一旦我们有了日志,我们就会想要保存它。
第 1 步:创建下载助手 首先,我们需要一个帮助函数来处理浏览器中的文件下载: // ===================================== // 文件下载助手 // =====================================
函数 downloadFile(文件名, 内容, 类型 = "text/plain") { // 根据内容创建一个 blob const blob = new Blob([内容], { 类型 }); const url = URL.createObjectURL(blob);
// 创建一个临时下载链接并点击它 const a = document.createElement("a"); a.href = 网址; a.下载=文件名; a.点击();
// 下载后清理对象URL setTimeout(() => URL.revokeObjectURL(url), 100); }
此函数的工作原理是根据数据创建 Blob(二进制大对象),为其生成临时 URL,然后以编程方式单击下载链接。清理确保我们不会泄漏内存。 第 2 步:处理 JSON 导出 JSON 非常适合保留完整的数据结构:
// ===================================== // 导出为 JSON // =====================================
document.getElementById("export-json").addEventListener("click", () => { // 检查是否有需要导出的内容 if (!frames.length) { console.warn("没有可导出的录音。"); 返回; }
// 使用元数据和帧创建有效负载 常量负载 = { 创建时间:new Date().toISOString(), 框架 };
// 下载为 JSON 格式 下载文件( “游戏手柄-log.json”, JSON.stringify(有效负载, null, 2), “应用程序/json” ); });
JSON 格式使所有内容保持结构化且易于解析,非常适合加载回开发工具或与团队成员共享。 第 3 步:处理 CSV 导出 对于 CSV 导出,我们需要将分层数据展平为行和列:
//===================================== // 导出为 CSV // =====================================
document.getElementById("export-csv").addEventListener("click", () => { // 检查是否有需要导出的内容 if (!frames.length) { console.warn("没有可导出的录音。"); 返回; }
// 构建 CSV 标题行(时间戳列、所有按钮、所有轴) const headerButtons =frames[0].buttons.map((_, i) => btn${i}); const headerAxes =frames[0].axes.map((_, i) => axis${i}); const header = ["t", ...headerButtons, ...headerAxes].join(",") + "\n";
// 构建 CSV 数据行 const rows =frames.map(f => { const btnVals = f.buttons.map(b => b.value); 返回 [f.t, ...btnVals, ...f.axes].join(","); }).join("\n");
// 下载为 CSV downloadFile("gamepad-log.csv", header + rows, "text/csv"); });
CSV 非常适合数据分析,因为它可以直接在 Excel 或 Google Sheets 中打开,让您可以直观地创建图表、筛选数据或发现模式。 现在导出按钮已出现,您将在面板上看到两个新选项:导出 JSON 和导出 CSV。如果您想将原始日志返回到您的开发工具中或探索结构,那么 JSON 是很好的选择。另一方面,CSV 可以直接打开 Excel 或 Google Sheets,以便您可以绘制图表、过滤或比较输入。下图显示了带有这些额外控件的面板的外观。
3. 快照系统 有时您不需要完整的记录,只需要输入状态的快速“屏幕截图”。这就是“拍摄快照”按钮的用处。
和 JavaScript:
// ===================================== // 拍摄快照 // =====================================
document.getElementById("快照").addEventListener("点击", () => { // 获取所有连接的游戏手柄 const pads = navigator.getGamepads(); 常量 activePads = [];
// 循环并捕获每个连接的游戏手柄的状态 for (const gp of pads) { if (!gp) 继续; // 跳过空槽
activePads.push({ id: gp.id, // 控制器名称/型号 时间戳:performance.now(), 按钮: gp.buttons.map(b => ({ 按下:b.按下, 值:b.值 })), 轴:[...gp.axes] }); }
// 检查是否找到任何游戏手柄 if (!activePads.length) { console.warn("没有连接游戏手柄进行快照。"); Alert("未检测到控制器!"); 返回; }
// 记录并通知用户 console.log("快照:", activePads); 警报(已拍摄快照!捕获 ${activePads.length} 控制器。); });
快照会冻结控制器在某一时刻的确切状态。 4. 幽灵输入重放 现在有趣的是:幽灵输入重放。这需要一个日志并以视觉方式回放它,就像幻影玩家正在使用控制器一样。
用于重放的 JavaScript: // ===================================== // 幽灵重播 // =====================================
document.getElementById("重播").addEventListener("点击", () => { // 确保我们有录音可以重播 if (!frames.length) { Alert("没有录音可重播!"); 返回; }
console.log("正在开始幽灵重播...");
// 跟踪同步播放的时间 让 startTime = Performance.now(); 让帧索引 = 0;
// 重放动画循环 函数步骤() { const now = Performance.now(); const elapsed = 现在 - 开始时间;
// 处理现在应该发生的所有帧
while (frameIndex // 使用记录的按钮状态更新 UI
btnA.classList.toggle("活动",frame.buttons[0].pressed);
btnB.classList.toggle("活动",frame.buttons[1].pressed);
btnX.classList.toggle("活动",frame.buttons[2].pressed); // 更新状态显示
让按下= [];
frame.buttons.forEach((btn, i) => {
if (btn.pressed) Pressed.push("按钮" + i);
});
if (pressed.length > 0) {
status.textContent = "幽灵:" + Pressed.join(", ");
} 框架索引++;
} // 如果还有更多帧则继续循环
if (frameIndex < 帧长度) {
请求动画帧(步骤);
} 否则{
console.log("重播完成了。”);
status.textContent = "重播完成";
}
} // 开始重播
步骤();
}); 为了使调试更加实际,我添加了幽灵重播。录制完会话后,您可以点击重播并观看 UI 表演,就像幻影玩家正在运行键盘一样。为此,面板中会显示一个新的“重播幽灵”按钮。 点击“录制”,稍微调整一下控制器,停止,然后重播。用户界面只会回显您所做的一切,就像幽灵跟随您的输入一样。
为什么要费心这些额外的事情呢? 记录/导出使测试人员可以轻松地准确显示发生的情况。
快照会冻结某个时刻,当您追踪奇怪的错误时非常有用。
幽灵重播非常适合教程、可访问性检查或只是并排比较控制设置。 现在,它不再只是一个简洁的演示,而是您可以真正投入使用的东西。
现实世界的用例
现在我们有了这个可以做很多事情的调试器。它显示实时输入、记录日志、导出日志,甚至重播内容。但真正的问题是:谁真正关心?这对谁有用?
游戏开发商
控制器是工作的一部分,但是调试它们呢?通常是一种疼痛。想象一下,您正在测试格斗游戏组合,例如 ↓ → + 拳击。你不用祈祷,而是以同样的方式按两次,记录一次,然后重播。完成。或者,您与队友交换 JSON 日志,以检查您的多人游戏代码在他们的计算机上是否有相同的反应。那是巨大的。
无障碍从业者
这个很贴近我的心。并非每个人都使用“标准”控制器进行游戏。自适应控制器有时会发出奇怪的信号。使用此工具,您可以准确地看到正在发生的情况。教师、研究人员,无论是谁。他们可以抓取日志,进行比较,或者并排重放输入。突然间,看不见的东西变得显而易见。
质量保证测试
测试人员通常会写下诸如“我在这里捣碎了按钮,它坏了”之类的注释。不是很有帮助。现在?他们可以捕获准确的按下情况、导出日志并将其发送出去。无需猜测。
教育工作者
如果您正在制作教程或 YouTube 视频,幽灵重播就是黄金。你可以从字面上说,“这就是我对控制器所做的事情”,而 UI 则显示它正在发生。使解释更加清晰。
超越游戏
是的,这不仅仅是游戏。人们已经将控制器用于机器人、艺术项目和无障碍界面。每次都会遇到同样的问题:浏览器实际看到的是什么?有了这个,你就不必猜测了。
结论
调试控制器输入总是感觉就像盲目飞行。与 DOM 或 CSS 不同,游戏手柄没有内置检查器;它只是控制台中的原始数字,很容易在噪音中丢失。
我们用几百行 HTML、CSS 和 JavaScript 构建了一些不同的东西: 可视化调试器,使不可见的输入变得可见。
分层 CSS 系统,保持 UI 干净且可调试。
一组增强功能(记录、导出、快照、重放)将其从演示提升为开发人员工具。 该项目展示了通过将 Web 平台的强大功能与 CSS 级联层中的一点创造力相结合,您可以走多远。
我刚刚完整解释的工具是开源的。您可以克隆 GitHub 存储库并亲自尝试。
但更重要的是,您可以将其变成您自己的。添加您自己的图层。构建您自己的重播逻辑。将其与您的游戏原型集成。或者甚至以我想象不到的方式使用它。用于教学、可访问性或数据分析。
归根结底,这不仅仅是调试游戏手柄。它旨在揭示隐藏的输入,并让开发人员有信心使用网络尚未完全接受的硬件。
因此,插入控制器,打开编辑器,然后开始试验。您可能会对您的浏览器和 CSS 真正实现的功能感到惊讶。