
理解事件类型与事件对象属性
在开发javascript游戏时,将原本基于键盘输入的交互逻辑适配到触控设备是一个常见需求。然而,许多开发者在尝试将keypress或keydown事件直接替换为touchstart时,会遇到意料之外的问题。这通常源于对不同事件类型所附带的事件对象(event object)属性的误解。
以Flappy Bird为例,当我们需要将小鸟的跳跃动作从按下空格键(Space)改为触摸屏幕时,直观地将document.addEventListener("keypress", handleJump)改为document.addEventListener("touchstart", handleJump),并将handleJump函数中的条件判断从e.code !== "Space"改为e.code !== "touchstart",看似合理,实则不然。
问题的核心在于e.code属性。e.code是KeyboardEvent(键盘事件)对象特有的属性,用于表示触发事件的物理按键代码,例如"Space"、"KeyA"等。而touchstart事件属于TouchEvent(触控事件)类型,其事件对象不包含code属性。因此,当touchstart事件触发时,e.code将是undefined。将undefined !== "touchstart"作为条件判断,实际上是试图比较一个不存在的属性与一个字符串,这会导致条件始终为真(或在严格模式下抛出错误),从而阻止了后续逻辑的执行。
正确处理触控事件
对于touchstart这类触控事件,我们通常不需要检查特定的“代码”来判断事件的来源,因为事件本身就代表了用户在屏幕上的触摸行为。如果事件监听器已经明确绑定到touchstart事件,那么当handleJump函数被调用时,我们就可以确信这是一个触摸事件。
因此,解决问题的关键在于移除或修改不适用于触控事件的条件判断。
立即学习“Java免费学习笔记(深入)”;
原始(错误)的 handleJump 函数:
function handleJump(e) {
if (e.code !== "touchstart") return; // 错误:e.code 不适用于 touchstart 事件
timeSinceLastJump = 0;
}修正后的 handleJump 函数:
function handleJump(e) {
// 对于 touchstart 事件,无需检查 e.code
// 如果需要防止默认的滚动/缩放行为,可以添加 e.preventDefault();
// e.preventDefault();
timeSinceLastJump = 0;
}通过移除if (e.code !== "touchstart") return;这一行,handleJump函数在touchstart事件触发时将直接执行timeSinceLastJump = 0;,从而正确地触发小鸟的跳跃动作。
示例代码:Flappy Bird 触控适配
以下是基于原始代码进行修改的示例,展示了如何正确地将Flappy Bird游戏适配为触控友好型。
f.js (主页面逻辑):
import { updateBird, setupBird, getBirdRect } from "./Bird.js";
import {
updatePipes,
setupPipes,
getPassedPipesCount,
getPipesRects,
} from "./Pipe.js";
// 监听 touchstart 事件来启动游戏
document.addEventListener("touchstart", handleStart, { once: true });
const title = document.querySelector("[data-title]");
const subtitle = document.querySelector("[data-subtitle]");
let lastTime;
function updateLoop(time) {
if (lastTime == null) {
lastTime = time;
window.requestAnimationFrame(updateLoop);
return;
}
const delta = time - lastTime;
updateBird(delta);
updatePipes(delta);
if (checkLose()) return handleLose();
lastTime = time;
window.requestAnimationFrame(updateLoop);
}
function checkLose() {
const birdRect = getBirdRect();
const insidePipe = getPipesRects().some((rect) =>
isCollision(birdRect, rect)
);
const outsideWorld = birdRect.top < 0 || birdRect.bottom > window.innerHeight;
return outsideWorld || insidePipe;
}
function isCollision(rect1, rect2) {
return (
rect1.left < rect2.right &&
rect1.top < rect2.bottom &&
rect1.right > rect2.left &&
rect1.bottom > rect2.top
);
}
function handleStart() {
title.classList.add("hide");
setupBird();
setupPipes();
lastTime = null;
window.requestAnimationFrame(updateLoop);
}
function handleLose() {
setTimeout(() => {
title.classList.remove("hide");
subtitle.classList.remove("hide");
subtitle.textContent = `${getPassedPipesCount()} Pipes`;
// 游戏结束后,再次监听 touchstart 来重新开始游戏
document.addEventListener("touchstart", handleStart, { once: true });
}, 100);
}Bird.js (小鸟逻辑):
const birdElem = document.querySelector("[data-bird");
const BIRD_SPEED = 0.5;
const JUMP_DURATION = 120;
let timeSinceLastJump = Number.POSITIVE_INFINITY;
export function setupBird() {
setTop(window.innerHeight / 2);
// 移除旧的事件监听器,确保只添加一次
document.removeEventListener("touchstart", handleJump);
// 监听 touchstart 事件来触发跳跃
document.addEventListener("touchstart", handleJump);
}
export function updateBird(delta) {
if (timeSinceLastJump < JUMP_DURATION) {
setTop(getTop() - BIRD_SPEED * delta);
} else {
setTop(getTop() + BIRD_SPEED * delta);
}
timeSinceLastJump += delta;
}
export function getBirdRect() {
return birdElem.getBoundingClientRect();
}
function setTop(top) {
birdElem.style.setProperty("--bird-top", top);
}
function getTop() {
return parseFloat(getComputedStyle(birdElem).getPropertyValue("--bird-top"));
}
function handleJump(e) {
// 修正:移除对 e.code 的错误判断
// 建议:对于游戏中的触控事件,通常需要阻止默认行为以防止页面滚动或缩放
e.preventDefault();
timeSinceLastJump = 0;
}注意事项与最佳实践
- e.preventDefault(): 在处理触控事件时,尤其是在游戏中,强烈建议调用e.preventDefault()。这可以阻止浏览器执行与触摸相关的默认行为,例如页面滚动、缩放或触发上下文菜单。如果您的游戏不阻止这些默认行为,用户可能会在尝试玩游戏时意外地滚动页面。
- 事件监听器的管理: 在setupBird函数中,先removeEventListener再addEventListener是一个好习惯,可以防止重复添加相同的事件监听器,尤其是在游戏重置或重新开始时。
-
click事件与touchstart/touchend:
- touchstart:当用户触摸屏幕时立即触发。适合需要即时响应的动作(如跳跃、射击)。
- touchend:当用户从屏幕上抬起手指时触发。适合需要释放动作的场景。
- click:在touchstart和touchend之后,如果触摸没有移动且持续时间短,浏览器会模拟触发click事件。对于简单的“点击”交互,click事件可能更方便,因为它已经处理了触摸和鼠标事件的兼容性。但对于需要精确控制触摸生命周期的游戏,touchstart和touchend更合适。
- 多点触控: 如果您的游戏需要支持多点触控(例如,同时控制多个角色或执行组合操作),您需要利用TouchEvent对象的e.touches、e.targetTouches和e.changedTouches属性来获取当前所有触摸点的信息。
- 性能优化: 频繁触发的触控事件(如touchmove)可能会影响性能。如果不需要实时追踪触摸点的移动,尽量避免在这些事件上执行复杂的计算。
总结
将JavaScript游戏从键盘控制转换为触控控制时,理解不同事件类型(如KeyboardEvent和TouchEvent)的事件对象属性至关重要。e.code是键盘事件特有的属性,不适用于触控事件。对于touchstart事件,我们只需监听事件并执行相应的逻辑,同时考虑使用e.preventDefault()来优化用户体验。通过遵循这些原则和最佳实践,您可以确保您的JavaScript游戏在移动设备上提供流畅、响应迅速的触控交互。











