注意
在Dropbox上,我在Hand tropbox上演示问题的视频的链接是:https://www.dropbox.com/scl/fo/p19oite64o22ssh9bl8s1b/alnnuvrjznbkk7qhrglggdy = gpxahqury = gpxahqur1kmffddqrw4bbkny = gpxahqurkey = gpxahqury = gpxahqur1km1ulow4bbkyb。
背景我正在尝试创建一款 agario 类型的游戏,其中玩家以圆圈形式生成,并且能够吃小而圆形的食物来变大。我在客户端使用 HTML canvas,在服务器端使用 Node.js。
问题当玩家吃更多的食物时,我一直很难弄清楚如何正确地缩放世界。在这个游戏中,当玩家触摸食物时,他们就会吃掉它。我使用 .scale() 方法慢慢缩小,这样当玩家变大时,他们最终不会完全占据屏幕,这样他们除了自己就看不到任何东西。然而,随着玩家变大,命中检测会变得更差 - 玩家会在食物上面并且不会吃它,或者会在触摸它之前吃掉它。似乎这种糟糕的命中检测对应的方向是:当玩家向上移动时,食物被吃得较晚,因为食物会与玩家重叠而不会被吃掉。当玩家向左移动时也会发生同样的情况,食物将与玩家重叠而不会被吃掉。相反,当玩家向右或向下移动时,食物将在玩家接触食物之前被吃掉。就好像我只需将玩家向右移动一定的距离,但我不知道要更改哪些代码,也不知道为什么它首先会导致问题。
代码我删除了似乎与该问题无关的代码。
Node.js 服务器目前处理所有事情(生成食物、计算玩家之间的碰撞、食物等),除了提供玩家坐标之外,因为客户端在每一帧上将其坐标发送到服务器。
“Player”类创建玩家对象的结构。在 Player 类中,我有 draw() 方法,如下所示:
draw(viewport) {
if (this.isLocal) { //if it's the local player, send coordinates to server
socket.emit('currentPosition', {
x: this.x / zoom,
y: this.y / zoom,
radius: this.radius / zoom,
speedMultiplier: this.speedMultiplier,
vx: this.vx, //update these on server side?
vy: this.vy
});
}
ctx.save();
ctx.translate(-viewport.x, -viewport.y);
//scale the player
ctx.scale(zoom, zoom);
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fillStyle = this.color;
ctx.fill();
ctx.strokeStyle = this.strokeColor;
ctx.lineWidth = 5;
ctx.stroke();
ctx.closePath();
ctx.restore();
}
在此draw()方法中,我使用
ctx.scale(zoom, zoom)
来缩放播放器。我的理解是,这本质上是将 x/y 位置和半径乘以缩放系数。 u2028u2028我还有一个 Food 类,它创建食物对象。 Food 类的 draw() 方法如下所示:
draw(viewport) {
ctx.save();
ctx.translate(-viewport.x, -viewport.y);
ctx.scale(zoom, zoom); //scale foods
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fillStyle = this.color;
ctx.fill();
ctx.strokeStyle = this.strokeColor;
ctx.lineWidth = 3.5;
ctx.stroke();
ctx.closePath();
ctx.restore();
}
这两个draw()方法都是为了根据玩家吃了多少食物来缩放玩家和食物。当玩家吃掉单个食物时,缩放率会降低。服务器告诉玩家何时吃过食物:
socket.on('foodEaten', (data) => {
const player = gamePlayers.get(data.playerId);
if (player.id == localPlayer.id) {
zoom *= 0.9999;
}
…
因此,对于玩家吃的每一种食物,他们都会缩小 0.9999。
玩家的移动由鼠标移动的位置决定。当他们移动鼠标时,此事件侦听器会计算鼠标指向的方向并将玩家设置在该路径上:
canvas.addEventListener('mousemove', (event) => {
const rect = canvas.getBoundingClientRect();
mouse.x = (event.clientX - rect.left) / zoom;
mouse.y = (event.clientY - rect.top) / zoom;
// Update player's target position
localPlayer.targetX = (mouse.x + viewport.x / zoom);
localPlayer.targetY = (mouse.y + viewport.y / zoom);
const dx = (localPlayer.targetX - localPlayer.x) * zoom;
const dy = (localPlayer.targetY - localPlayer.y) * zoom;
const distance = Math.sqrt(dx * dx + dy * dy);
...does some other stuff in between...
const xMovingDirection = dx / distance;
const yMovingDirection = dy / distance;
localPlayer.movingDirection = { x: xMovingDirection, y: yMovingDirection};
});
我在 Player 类的 draw() 方法中提到,它们在绘制玩家之前将当前位置发送到服务器:
socket.emit('currentPosition', {
x: this.x / zoom,
y: this.y / zoom,
radius: this.radius / zoom,
speedMultiplier: this.speedMultiplier,
vx: this.vx,
vy: this.vy
});
我将 x 和 y 坐标以及半径除以缩放级别,以允许服务器忽略各个玩家的缩放级别,以便游戏中的每个其他玩家都会发送非缩放坐标。
服务器收到此信息后,会根据游戏中的每种食物评估玩家的位置,检查它们是否发生碰撞:
socket.on('currentPosition', (data) => { //get player's current x/y coordinates and update them
const player = room.players.get(socket.id);
if (player) {
player.x = data.x; //update player x position
player.y = data.y; //update player y position
room.foods.forEach((food, foodId) => { //check for foods being eaten
if (checkCollision(player, food)) {
player.radius += food.radius * normalRadiusIncreaseRate; //increase player radius
let newFood; //add food back in
newFood = new Food(); //add new food item
room.foods.set(newFood.id, newFood);
//let player know that they ate food
io.to(room.roomId).emit('foodEaten', {
food: food,
playerId: player.id,
radius: player.radius,
newFood: newFood
});
room.foods.delete(food.id); //delete eaten food
}
//send this player’s data to other players
socket.to(room.roomId).emit('updatePlayerTarget', {
id: socket.id,
x: player.x,
y: player.y
// radius: player.radius
});
}
});
如果玩家碰撞到食物,他们应该吃掉它。 Node.js 向客户端发出“foodEaten”,这允许客户端更新吃掉食物的玩家的半径。它还给出了玩家在方块末端的 x 和 y 坐标。问题
为什么当使用 .scale() 时,玩家和食物之间的同步会随着时间的推移而变得更差?
socket.emit('currentPosition', {
x: this.x / zoom,
y: this.y / zoom,
radius: this.radius / zoom
});
相反,我应该按原样发送玩家的坐标,而不除以缩放系数。这是因为缩放系数仅影响客户端的视觉表示,而不影响实际的坐标值。
当我使用 .scale(zoom, Zoom) 时,它会改变坐标在屏幕上的显示方式,但底层坐标本身保持不变。因此,为了保持客户端和服务器之间的一致性,发送到服务器的坐标应该是原始世界坐标,不受缩放的影响。