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

Canvas实现玩家朝向发射子弹:shoot函数无绘制问题排查与实现

问题描述

我已经开发出可以创建玩家并实现移动、旋转功能的程序,现在想绑定空格键生成小型子弹,让它沿玩家朝向发射。
我写了shoot()函数,本应该在玩家位置绘制小圆,但画布上没任何内容,控制台也没报错。代码片段如下:

shoot() {
  ctx.beginPath();
  ctx.arc(this.X, this.Y, 5, 0, 2 * Math.PI, false);
  ctx.fillStyle = 'white';
  ctx.fill();
},

请问哪里出错了?该怎么修复?另外,我还想实现子弹真正沿朝向发射的效果。

完整代码如下:

const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style = "position: absolute; top: 0px; left: 0px; right: 0px; bottom: 0px; margin: auto; border: 0px";

const kbd = {
  ArrowLeft: false, 
  ArrowUp: false, 
  ArrowRight: false,
  ArrowDown: false,
  Space: false,
};

const ship = {
  angle: 0,
  color: "white",
  x: canvas.width / 2,
  y: canvas.height / 2,
  width: 10,
  height: 12,
  drag: 0.9,
  accSpeed: 0.025,
  rotSpeed: 0.007,
  rotv: 0,
  ax: 0,
  ay: 0,
  vx: 0,
  vy: 0,
  rotateLeft() {
    this.rotv -= this.rotSpeed;
  },
  rotateRight() {
    this.rotv += this.rotSpeed;
  },
  accelerate() {
    this.ax += this.accSpeed;
    this.ay += this.accSpeed;
  },
  decelerate() {
    this.ax -= this.accSpeed;
    this.ay -= this.accSpeed;
  },
  shoot() {
    ctx.beginPath();
    ctx.arc(this.X, this.Y, 5, 0, 2 * Math.PI, false);
    ctx.fillStyle = 'white';
    ctx.fill();
  },
  move() {
    this.angle += this.rotv;
    this.rotv *= this.drag;
    this.vx += this.ax;
    this.vy += this.ay;
    this.ax *= this.drag;
    this.ay *= this.drag;
    this.vx *= this.drag;
    this.vy *= this.drag;
    this.x += Math.cos(this.angle) * this.vx;
    this.y += Math.sin(this.angle) * this.vy;
  },
  draw(ctx) {
    ctx.save();
    ctx.lineWidth = 3;
    ctx.translate(this.x, this.y);
    ctx.rotate(this.angle);
    ctx.beginPath();
    ctx.moveTo(this.height, 0);
    ctx.lineTo(-this.height, this.width);
    ctx.lineTo(-this.height, -this.width);
    ctx.closePath();
    ctx.strokeStyle = this.color;
    ctx.stroke();
    ctx.restore();
  }
};

document.addEventListener("keydown", event => {
  if (event.code in kbd) {
    event.preventDefault();
    kbd[event.code] = true;
  }
});
document.addEventListener("keyup", event => {
  if (event.code in kbd) {
    event.preventDefault();
    kbd[event.code] = false; 
  }
});

(function update() {
  ctx.fillStyle = "black";
  ctx.fillRect(0, 0, canvas.width, canvas.height);  

  const shipActions = {
    ArrowLeft: "rotateLeft",
    ArrowUp: "accelerate",
    ArrowDown: "decelerate",
    ArrowRight: "rotateRight",
    Space: "shoot",
  };

  for (const key in shipActions) {
    if (kbd[key]) {
      ship[shipActions[key]]();
    }
  }
  
  ship.move();
  ship.draw(ctx);
  requestAnimationFrame(update);
})();

问题分析与修复

1. 子弹不显示的直接原因

你的shoot()函数里用了this.X和this.Y,但玩家对象里存储位置的属性是小写的this.x和this.y,大小写不匹配导致取到undefined,绘制的圆不在画布可见范围内。

修复这个错误只需要把this.X、this.Y改成this.x、this.y:

shoot() {
  ctx.beginPath();
  ctx.arc(this.x, this.y, 5, 0, 2 * Math.PI, false);
  ctx.fillStyle = 'white';
  ctx.fill();
},

不过改完后你会发现子弹只会闪一下就消失——因为update()函数每次都会用黑色矩形清空整个画布,且当前shoot()只是单次绘制,没有保存子弹状态让它持续存在并移动。

2. 实现子弹沿朝向发射的完整方案

要让子弹真正发射并移动,需要维护一个子弹数组存储每个子弹的位置、速度等状态,然后在每一帧更新并绘制所有子弹。

步骤1:添加子弹数组和配置

在const kbd之后添加:

// 存储所有子弹的数组
const bullets = [];
// 子弹基础配置
const bulletConfig = {
  radius: 5,
  speed: 5,
  color: 'white'
};

步骤2:修改shoot()函数生成子弹对象

不再直接绘制,而是生成子弹对象加入数组,让子弹从玩家头部发射并继承玩家移动速度:

shoot() {
  // 计算子弹初始位置:飞船头部(沿朝向偏移飞船height距离)
  const bulletX = this.x + Math.cos(this.angle) * this.height;
  const bulletY = this.y + Math.sin(this.angle) * this.height;
  
  // 计算子弹速度:朝向方向的基础速度 + 玩家当前移动速度
  const bulletVX = Math.cos(this.angle) * bulletConfig.speed + this.vx;
  const bulletVY = Math.sin(this.angle) * bulletConfig.speed + this.vy;
  
  // 将子弹加入数组
  bullets.push({
    x: bulletX,
    y: bulletY,
    vx: bulletVX,
    vy: bulletVY,
    radius: bulletConfig.radius,
    color: bulletConfig.color
  });
},

步骤3:添加子弹更新与绘制逻辑

在update()函数的ship.draw(ctx)之后添加:

// 更新并绘制所有子弹
for (let i = bullets.length - 1; i >= 0; i--) {
  const bullet = bullets[i];
  
  // 更新子弹位置
  bullet.x += bullet.vx;
  bullet.y += bullet.vy;
  
  // 绘制子弹
  ctx.beginPath();
  ctx.arc(bullet.x, bullet.y, bullet.radius, 0, 2 * Math.PI);
  ctx.fillStyle = bullet.color;
  ctx.fill();
  
  // 移除超出画布的子弹,节省性能
  if (bullet.x < 0 || bullet.x > canvas.width || bullet.y < 0 || bullet.y > canvas.height) {
    bullets.splice(i, 1);
  }
}

步骤4:优化按键逻辑(可选,防止按住空格连发)

如果不想按住空格时连续发射子弹,可修改键盘事件和触发逻辑:

// 修改kbd对象
const kbd = {
  ArrowLeft: false, 
  ArrowUp: false, 
  ArrowRight: false,
  ArrowDown: false,
  Space: { pressed: false, triggered: false },
};

// 修改keydown事件
document.addEventListener("keydown", event => {
  if (event.code === "Space") {
    event.preventDefault();
    if (!kbd.Space.pressed) {
      kbd.Space.pressed = true;
      kbd.Space.triggered = false;
    }
  } else if (event.code in kbd) {
    event.preventDefault();
    kbd[event.code] = true;
  }
});

// 修改keyup事件
document.addEventListener("keyup", event => {
  if (event.code === "Space") {
    event.preventDefault();
    kbd.Space.pressed = false;
  } else if (event.code in kbd) {
    event.preventDefault();
    kbd[event.code] = false; 
  }
});

// 修改update()里的触发逻辑
for (const key in shipActions) {
  if (key === "Space") {
    if (kbd.Space.pressed && !kbd.Space.triggered) {
      ship[shipActions[key]]();
      kbd.Space.triggered = true;
    }
  } else if (kbd[key]) {
    ship[shipActions[key]]();
  }
}

最终效果

修改完成后,按空格键会从飞船头部发射白色子弹,子弹沿飞船朝向飞行,超出画布后自动被清理;若启用按键优化,按住空格只会发射一颗子弹,否则会连续发射。


内容的提问来源于stack exchange,提问作者gamer merch

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.21 19:45:44