以玩家为中心的网页游戏: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>
关键修改点说明
- 新增
screenToWorld函数,实现屏幕坐标到游戏世界坐标的转换 - 鼠标移动事件中,将
clientX/Y转换为世界坐标后存储 - 瞄准函数使用世界坐标计算角度,确保角度与鼠标位置完全匹配
- 红点绘制使用世界坐标,位置与鼠标实际瞄准点一致
- 修正子弹移除逻辑,改为判断子弹是否在玩家视野对应的世界范围内
内容的提问来源于stack exchange,提问作者Jara
相关产品推荐
相关产品推荐

