为何创建两个Sprite实例后仅最后一个可通过键盘控制移动?
问题描述
跟着Chris Courses的YouTube教程实现格斗游戏,创建了Sprite类并实例化了player和enemy两个角色。单独声明其中一个角色时,能正常响应键盘移动;但同时声明两个时,只有最后创建的角色能响应操作,另一个完全无反应。原本预期每个Sprite实例拥有独立的控制数据,按键时两个角色应同时移动。
相关代码
HTML
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Fighting Game inspired by Chris Courses</title> <link rel="stylesheet" href="styles.css"> </head> <body> <canvas id="canvas"></canvas> </body> <script src="script.js"></script> </html>
CSS
* { padding: 0px; margin: 0px; overflow: hidden; background-color: darkgrey; } body { display: flex; justify-content: center; align-items: center; height: 100vh; } #canvas { background-color: black; }
JavaScript
"use strict" // Sprite类 class Sprite { constructor({ character, position, velocity, width, height, gravity = 0.2 }) { this.character = character; this.position = position; this.velocity = velocity; this.width = width; this.height = height; this.gravity = gravity; this.controls = new Controls(); } draw() { context.beginPath(); context.fillStyle = "red"; context.fillRect( this.position.x, this.position.y, this.width, this.height ); context.closePath(); } update() { this.position.y += this.velocity.y; this.position.x += this.velocity.x; this.#applyGravity(); this.#move(); } #applyGravity() { if(this.position.y + this.height + this.velocity.y >= canvas.height) { this.velocity.y = 0; } else { this.velocity.y += this.gravity; } } #move() { if(this.controls) { if(this.controls.up && this.controls.lastKey === "up") { this.velocity.y = -1; } else if(this.controls.down && this.controls.lastKey === "down") { this.velocity.y = 1; } else if(this.controls.left && this.controls.lastKey === "left") { this.velocity.x = -1; } else if(this.controls.right && this.controls.lastKey === "right") { this.velocity.x = 1; } } } } // Controls类 class Controls { constructor() { this.up = false; this.down = false; this.left = false; this.right = false; this.lastKey = ""; this.#addEventListeners(); } #addEventListeners() { document.onkeydown = (event => { switch(event.key) { case "ArrowUp": this.up = true; this.lastKey = "up"; break; case "ArrowDown": this.down = true; this.lastKey = "down"; break; case "ArrowLeft": this.left = true; this.lastKey = "left"; break; case "ArrowRight": this.right = true; this.lastKey = "right"; break; } }); document.onkeyup = (event => { switch(event.key) { case "ArrowUp": this.up = false; break; case "ArrowDown": this.down = false; break; case "ArrowLeft": this.left = false; break; case "ArrowRight": this.right = false; break; } }); } } // 初始化代码 const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); const player = new Sprite({ position: { x: 100, y: 100 }, velocity: { x: 0, y: 0 }, width: 50, height: 150 }); const enemy = new Sprite({ position: { x: 800, y: 100 }, velocity: { x: 0, y: 0 }, width: 50, height: 150 }); let animationID; function animate() { animationID = window.requestAnimationFrame(animate); canvas.width = 1024; canvas.height = 576; player.draw(); player.update(); enemy.draw(); enemy.update(); } animate();
问题原因
核心问题出在Controls类的事件绑定方式:
- 使用
document.onkeydown = ...和document.onkeyup = ...这种赋值式绑定,后绑定的事件处理函数会直接覆盖之前的。 - 创建第二个
Sprite实例时,其Controls构造函数会重新给document.onkeydown和document.onkeyup赋值,第一个实例的事件处理逻辑被完全覆盖,导致只有最后一个实例的控制能生效。
解决方案
方案1:改用addEventListener绑定事件(快速修复)
将Controls类中的赋值式事件绑定改为addEventListener,该方法支持同时绑定多个事件处理函数,不会互相覆盖:
#addEventListeners() { document.addEventListener('keydown', (event) => { switch(event.key) { case "ArrowUp": this.up = true; this.lastKey = "up"; break; case "ArrowDown": this.down = true; this.lastKey = "down"; break; case "ArrowLeft": this.left = true; this.lastKey = "left"; break; case "ArrowRight": this.right = true; this.lastKey = "right"; break; } }); document.addEventListener('keyup', (event) => { switch(event.key) { case "ArrowUp": this.up = false; break; case "ArrowDown": this.down = false; break; case "ArrowLeft": this.left = false; break; case "ArrowRight": this.right = false; break; } }); }
方案2:全局输入管理器(适合双人对战扩展)
如果后续要实现双人对战(给两个角色分配不同按键),更合理的做法是用全局单例的输入管理器统一处理键盘事件,避免重复绑定:
// 全局输入管理器 class InputManager { constructor() { this.keys = {}; this.#initListeners(); } #initListeners() { document.addEventListener('keydown', (e) => { this.keys[e.key] = true; }); document.addEventListener('keyup', (e) => { this.keys[e.key] = false; }); } isPressed(key) { return this.keys[key] || false; } } // 创建全局实例 const inputManager = new InputManager(); // 修改Sprite类,移除内部Controls实例 class Sprite { constructor({ position, velocity, width, height, gravity = 0.2, keys }) { this.position = position; this.velocity = velocity; this.width = width; this.height = height; this.gravity = gravity; this.keys = keys; // 每个角色的按键映射 this.lastKey = ""; } #move() { // 处理Y轴移动 if(inputManager.isPressed(this.keys.up)) { this.velocity.y = -1; this.lastKey = "up"; } else if(inputManager.isPressed(this.keys.down)) { this.velocity.y = 1; this.lastKey = "down"; } else { // 松开按键后Y轴速度归0(重力会单独处理) this.velocity.y = 0; } // 处理X轴移动 if(inputManager.isPressed(this.keys.left)) { this.velocity.x = -1; this.lastKey = "left"; } else if(inputManager.isPressed(this.keys.right)) { this.velocity.x = 1; this.lastKey = "right"; } else { // 松开按键后X轴速度归0 this.velocity.x = 0; } } // 保留其他方法(draw、update、#applyGravity) } // 实例化时给两个角色分配不同按键 const player = new Sprite({ position: { x: 100, y: 100 }, velocity: { x: 0, y: 0 }, width: 50, height: 150, keys: { up: "ArrowUp", down: "ArrowDown", left: "ArrowLeft", right: "ArrowRight" } }); const enemy = new Sprite({ position: { x: 800, y: 100 }, velocity: { x: 0, y: 0 }, width: 50, height: 150, keys: { up: "w", down: "s", left: "a", right: "d" } });
额外优化建议
- 当前
#move方法未处理按键松开后的速度归零,建议在没有对应按键按下时,将velocity.x和非重力影响的velocity.y设为0,避免角色一直移动。 animate函数中每次重置canvas.width和canvas.height会造成不必要的性能消耗,建议将这两行移到初始化代码中,不要在每一帧执行。
内容的提问来源于stack exchange,提问作者GithubUser699
相关产品推荐
相关产品推荐

