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

以玩家为中心的网页游戏:ctx.setTransform瞄准精度异常问题

问题根源

核心问题是坐标系统不匹配:

  • 鼠标事件获取的clientX/clientY是Canvas的屏幕坐标,而通过ctx.setTransform偏移后,玩家的posX/posY是游戏世界坐标,直接用两类坐标计算瞄准角度会出现偏差,且玩家离初始中心越远偏差越大。
  • 红点绘制使用屏幕坐标,但画布已经应用了世界变换,导致红点位置完全错位。
解决方案

需要将屏幕鼠标坐标转换为游戏世界坐标,再用于瞄准计算和红点绘制,同时修正子弹的移除逻辑。

1. 实现坐标转换函数

当前使用的变换公式为:屏幕坐标 = 世界坐标 + (transformX, transformY),因此逆变换(屏幕转世界)为:

function screenToWorld(screenX, screenY) {
    const transformX = -player.posX + canvasWidth / 2;
    const transformY = -player.posY + canvasHeight / 2;
    return {
        x: screenX - transformX,
        y: screenY - transformY
    };
}

2. 修改鼠标事件与瞄准逻辑

  • 鼠标移动时,将屏幕坐标转换为世界坐标后存储
  • 瞄准函数使用转换后的世界坐标计算角度

3. 修正红点绘制与子弹移除逻辑

  • 红点用转换后的世界坐标绘制,确保位置与鼠标瞄准点匹配
  • 子弹移除时,判断其是否在玩家视野对应的世界范围内
修改后的完整代码
var canvas = document.querySelector("canvas");
var ctx = canvas.getContext("2d");

canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;

function draw(posX, posY, width, height, color) {
  ctx.fillStyle = color;
  ctx.fillRect(posX, posY, width, height);
}

class Player {
  constructor(width, height, color) {
    this.posX = canvasWidth / 2;
    this.posY = canvasHeight / 2;
    this.width = width || 32;
    this.height = height || 32;
    this.color = color || "red";
    this.speed = 4; // lower is faster
    this.velocity = [0, 0];
    this.keys = {
      w: false,
      a: false,
      s: false,
      d: false,
    };
    this.bullets = [];
    this.lastShotTime = 0;
    this.fireRate = 750; // lower is faster
    this.gunWidth = 22;
    this.gunHeight = 10;
    this.gunColor = "black";
    this.gunAngle = 0;
  }

  position() {
    if (this.keys["a"]) {
      if (this.velocity[0] > -this.speed) {
        this.velocity[0] -= 0.5;
      }
    }
    if (this.keys["d"]) {
      if (this.velocity[0] < this.speed) {
        this.velocity[0] += 0.5;
      }
    }
    if (this.keys["w"]) {
      if (this.velocity[1] > -this.speed) {
        this.velocity[1] -= 0.5;
      }
    }
    if (this.keys["s"]) {
      if (this.velocity[1] < this.speed) {
        this.velocity[1] += 0.5;
      }
    }
    
    if (this.firing) {
      this.shoot();
    }

    this.posX += this.velocity[0];
    this.posY += this.velocity[1];
    if (this.keys["a"] === false && this.keys["d"] === false) {
      this.velocity[0] *= 0.9;
    }
    if (this.keys["w"] === false && this.keys["s"] === false) {
      this.velocity[1] *= 0.9;
    }
  }

  draw() {
    ctx.beginPath();
    ctx.fillStyle = this.color;
    ctx.arc(this.posX, this.posY, this.width / 2, 0, 2 * Math.PI);
    ctx.fill();

    // Draw the rotated gun
    ctx.save();
    ctx.beginPath();
    ctx.translate(this.posX, this.posY);
    ctx.rotate(this.gunAngle);
    ctx.fillStyle = this.gunColor;
    ctx.fillRect(0, -this.gunHeight / 2, this.gunWidth, this.gunHeight);
    ctx.restore();
  }

  shoot() {
    const currentTime = Date.now();
    if (currentTime - this.lastShotTime < this.fireRate) {
      return;
    }
    const bullet = new Bullet(
      this.posX + this.width / 10,
      this.posY + this.height / 10,
      this.gunAngle
    );
    this.bullets.push(bullet);
    this.lastShotTime = currentTime;
  }
}

class Bullet {
  constructor(posX, posY, angle) {
    this.posX = posX;
    this.posY = posY;
    this.speed = 15;
    this.angle = angle;
    this.width = 10;
    this.height = 10;
    this.color = "green";
  }
  move() {
    this.posX += this.speed * Math.cos(this.angle);
    this.posY += this.speed * Math.sin(this.angle);
  }
  draw() {
    draw(this.posX, this.posY, this.width, this.height, this.color);
  }
}

var player = new Player();
// 存储世界坐标下的鼠标位置
var worldMouseX = 0;
var worldMouseY = 0;

function screenToWorld(screenX, screenY) {
    const transformX = -player.posX + canvasWidth / 2;
    const transformY = -player.posY + canvasHeight / 2;
    return {
        x: screenX - transformX,
        y: screenY - transformY
    };
}

update();
function update() {
  requestAnimationFrame(update);
  ctx.clearRect(0, 0, canvasWidth, canvasHeight);

  const transformX = -player.posX + canvasWidth / 2;
  const transformY = -player.posY + canvasHeight / 2;
  ctx.setTransform(1, 0, 0, 1, transformX, transformY);

  if (player.firing) {
    player.shoot();
  }

  player.bullets.forEach((bullet) => {
    bullet.move();
    bullet.draw();
  });

  // 修正子弹移除逻辑:判断是否在玩家视野范围内
  player.bullets = player.bullets.filter(
    (bullet) =>
      bullet.posX > player.posX - canvasWidth / 2 &&
      bullet.posX < player.posX + canvasWidth / 2 &&
      bullet.posY > player.posY - canvasHeight / 2 &&
      bullet.posY < player.posY + canvasHeight / 2
  );

  // 用世界坐标绘制红点
  draw(worldMouseX, worldMouseY, 20, 20, "red");

  player.position();
  player.draw();

  aim(worldMouseX, worldMouseY);
}

document.addEventListener("keydown", function (e) {
  player.keys[e.key.toLowerCase()] = true;
});
document.addEventListener("keyup", function (e) {
  player.keys[e.key.toLowerCase()] = false;
});

document.addEventListener("mousemove", function (e) {
  // 将屏幕坐标转为世界坐标
  const worldPos = screenToWorld(e.clientX, e.clientY);
  worldMouseX = worldPos.x;
  worldMouseY = worldPos.y;
});

function aim(worldMouseX, worldMouseY) {
  // 使用世界坐标计算瞄准角度
  const angle = Math.atan2(worldMouseY - player.posY, worldMouseX - player.posX);
  player.gunAngle = angle;
}

document.addEventListener("mousedown", function (e) {
  player.firing = true;
});
document.addEventListener("mouseup", function (e) {
  player.firing = false;
});
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        body { margin: 0; overflow: hidden; }
        canvas { display: block; }
    </style>
</head>
<body>
    <canvas></canvas>
    <script src="minscript.js"></script>
</body>
</html>
关键修改点说明
  1. 新增screenToWorld函数,实现屏幕坐标到游戏世界坐标的转换
  2. 鼠标移动事件中,将clientX/Y转换为世界坐标后存储
  3. 瞄准函数使用世界坐标计算角度,确保角度与鼠标位置完全匹配
  4. 红点绘制使用世界坐标,位置与鼠标实际瞄准点一致
  5. 修正子弹移除逻辑,改为判断子弹是否在玩家视野对应的世界范围内

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 12:24:55