You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何创建两个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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 23:31:00