I recently started working on my first multiplayer game and learning multiple game server mechanics. The game itself uses mouse movement.
For the first version I used a very simple approach, where the physics are run both on the client and the server side, and the client and the server both run at 60fps. The client only renders what it gets from the server and every collision/movement is processed server side.
Now every frame the client sends his mouse position to the server, where the players next position is processed and sent back to the player. This approach worked for some time, but at the time of writing the code, I didn't take the latency in account. For really good internet connections or locally for me, there isn't any issues, but for not so good internet connections there's lag obviosly.
So I started searching for a solution to my problem and I found this article - http://www.gabrielgambetta.com/client-side-prediction-server-reconciliation.html
I implemented the Client Side Prediction to my game with the Server reconciliation but now I noticed another problem. Because the collision detection happens server side, I can see that sometimes the collision happens much later than it should. Pretty obvious because the server is in the past.
So I was thinking, what would be the best solution to this problem? I could move the collision detection to the client side, but we all know, that the player can't be trusted, so I would need a way to verify this collision server side. And what if the player tampers with the code and disables collision detection (because some of these collisions are deadly), how could I known when the collision is happening?
Another issue I though of with the client side prediction, is if the player throws a grenade at some other player, but in reality the other player isn't at the specific position yet, because his position was also predicted. What would be the best way to handle this?
You can see my current approach here, which doesn't use the client side prediction at all
The client side of my game uses Phaser, but the server P2.js physics library.
This is how the movement happens without client side prediction
Client Side
let pointer = game.input.activePointer;
//Send a new position data to the server
socket.emit('move-pointer', {
pointer_x: pointer.x,
pointer_y: pointer.y,
pointer_worldx: pointer.worldX,
pointer_worldy: pointer.worldY,
});
Server Side
//Make a new pointer with the new inputs from the client.
//contains player positions in server
let serverPointer = {
x: data.pointer_x,
y: data.pointer_y,
worldX: data.pointer_worldx,
worldY: data.pointer_worldy
};
//moving the player to the new inputs from the player
if (PositionService.distanceToPointer(movePlayer, serverPointer) <= 30) {
movePlayer.body.angle = PositionService.moveToPointer(movePlayer, 0, serverPointer, 100);
} else {
movePlayer.body.angle = PositionService.moveToPointer(movePlayer, movePlayer.speed, serverPointer);
}
let x = movePlayer.body.position[0];
let y = movePlayer.body.position[1];
let radius = movePlayer.size + (movePlayer.shield / 2);
if (x <= (1000 + radius)) {
movePlayer.body.position[0] = 1000 + radius;
}
if (y <= (1000 + radius)) {
movePlayer.body.position[1] = 1000 + radius;
}
if (x >= (game.properties.height - radius)) {
movePlayer.body.position[0] = game.properties.height - radius;
}
if (y >= (game.properties.width - radius)) {
movePlayer.body.position[1] = game.properties.width - radius;
}
movePlayer.x = movePlayer.body.position[0];
movePlayer.y = movePlayer.body.position[1];
//new player position to be sent back to client.
let info = {
x: movePlayer.body.position[0],
y: movePlayer.body.position[1],
angle: movePlayer.body.angle,
speed: movePlayer.speed,
ts: data.ts
};
//send to sender (not to every clients).
this.emit('input-received', info);
Client Side, on input-received
//we're forming a new pointer with the new position
let newPointer = {
x: data.x,
y: data.y,
worldX: data.x,
worldY: data.y,
};
let distance = PositionService.distanceToPointer(player, newPointer);
let speed = distance / 0.06;
player.rotation = PositionService.moveToPointer(player, speed, newPointer);
if (this.map_group) {
player.map.x = (player.x / (this.properties.server_width / 220)) - 20;
player.map.y = (player.y / (this.properties.server_height / 220)) - 20;
}
PositionService class
class PositionService {
/**
* @param displayObject
* @param speed
* @param pointer
* @param maxTime
* @param angle
* @returns {number}
*/
static moveToPointer(displayObject, speed, pointer, maxTime, angle) {
if (maxTime === undefined) { maxTime = 0; }
if(typeof angle === 'undefined'){
angle = PositionService.angleToPointer(displayObject, pointer);
}
if (maxTime > 0) {
// We know how many pixels we need to move, but how fast?
speed = PositionService.distanceToPointer(displayObject, pointer) / (maxTime / 1000);
}
displayObject.body.velocity[0] = Math.cos(angle) * speed;
displayObject.body.velocity[1] = Math.sin(angle) * speed;
return angle;
}
/**
* @param displayObject
* @param pointer
* @param world
* @returns {number}
*/
static distanceToPointer (displayObject, pointer, world) {
if (world === undefined) { world = false; }
let dx = (world) ? displayObject.world.x - pointer.worldX : displayObject.body.position[0] - pointer.worldX;
let dy = (world) ? displayObject.world.y - pointer.worldY : displayObject.body.position[1] - pointer.worldY;
return Math.sqrt(dx * dx + dy * dy);
}
/**
* @param displayObject
* @param pointer
* @param world
* @returns {number}
*/
static angleToPointer (displayObject, pointer, world) {
if (world === undefined) { world = false; }
if (world) {
return Math.atan2(pointer.worldY - displayObject.world.y, pointer.worldX - displayObject.world.x);
}
return Math.atan2(pointer.worldY - displayObject.body.position[1], pointer.worldX - displayObject.body.position[0]);
}
}