Page 58 - MSDN Magazine, October 2017
P. 58

Figure 5 Server Connection Handling
The Game Logic
The fundamental game logic involves the application of user inputs to the game state, as shown in Figure 6. For example, pressing on the gas should apply a forward-directed force on a car. Pressing the brakes should apply a backward-directed force on a car, potentially going in reverse. Turning the steering wheel applies an angular velocity, and so on.
These operations are implemented as calls to the physics engine, as time progresses. An interesting twist is that applying correct physical forces to a car provides a poor gaming experience. If you apply the cor- rect physical forces just as they work in the real world, the handling and control of the car doesn’t “feel right” in the hands of the player. The resulting game action is too slow. For the game to be enjoyable, I needed to apply an artificial boost when the car accelerates from very low velocities, otherwise the game action was just too slow.
Finally, the game logic must check if the ball passed the goal posts, and update the score.
Client-Side Prediction
Each multiplayer game has different requirements for client-side prediction and needs to be configured correspondingly. Game object types can also have a specific configuration. The code that follows shows a small subset of the game’s configuration. User inputs are delayed by three steps, or 50ms, to better match the time at which the inputs will be applied on the server. This is done by setting delayInputCount to 3. syncOptions.sync is set to “extrapo- late” to enable client-side prediction; localObjBending is set to 0.6, indicating that locally controlled object positions should be cor- rected by 60 percent before the next server broadcast is estimated to arrive; remoteObjBending is set higher, to 80 percent, because those objects are more likely to diverge significantly. Last, I set bendingIncrements to 6, indicating that each correction must not be applied at once, but rather over 6 increments of the render loop:
const options = { delayInputCount: 3, syncOptions: {
sync: 'extrapolate', localObjBending: 0.6, remoteObjBending: 0.8, bendingIncrements: 6
} };
The ball has its own velocity-bending parameter (not shown) set to zero. This is because when one team scores, the ball gets tele- ported back to the center of the field, and any attempt to bend the resulting velocity will result in undesired visual effects.
The client-side prediction logic itself is too large to include here, so I presented it as general pseudocode in Figure 7. It’s the secret sauce, which makes a multiplayer game possible. The first step scans the last server broadcast and checks if any new objects have been created. Existing objects remember their current state before they’re synced to the server state. The second step is the reenactment phase, which re-executes the game logic for all the steps that have occurred since the step described in the broadcast. Inputs need to be reapplied, and the game engine step is executed in reenactment mode. In the third step, each object records the required correc- tion, and reverts to the remembered state. Finally, in the last step, objects that are marked for destruction can be removed.
// Game-specific logic for player connections onPlayerConnected(socket) {
super.onPlayerConnected(socket);
let makePlayerCar = (team) => { this.gameEngine.makeCar(socket.playerId, team);
};
// Handle client restart requests
socket.on('requestRestart', makePlayerCar); socket.on('keepAlive', () => { this.resetIdleTimeout(socket); });
}
// Game-specific logic for player dis-connections onPlayerDisconnected(socketId, playerId) {
super.onPlayerDisconnected(socketId, playerId);
this.gameEngine.removeCar(playerId); }
either the blue team or the red team. When a player disconnects, I remove that player’s car.
Communication can also occur out-of-band, meaning that an event must sometimes be sent to one or more clients asynchro- nously, or data must be sent that’s not part of the game object states. The following sample shows how socket.io can be used on the server side:
// ServerEngine: send the event monsterAttack! with data // monsterData = {...} to all players this.io.sockets.emit('monsterAttack!', monsterData);
// ServerEngine: send events to specific connected players for (let socketId of Object.keys(this.connectedPlayers)) {
}
let player = this.connectedPlayers[socketId]; let playerId = player.socket.playerId;
let message = `hello player ${playerId}`; player.socket.emit('secret', message);
On the client side, listening to events is simple:
this.socket.on('monsterAttack!', (e) => { console.log(`run for your lives! ${e}`);
});
Figure 6 The GameEngine Step Method
// A single Game Engine Step step() {
super.step();
// Car physics this.world.forEachObject((id, o) => {
if (o.class === Car) { o.adjustCarMovement();
} });
// Check if we have a goal
if (this.ball && this.arena) {
// Check for ball in Goal 1
if (this.arena.isObjInGoal1(this.ball)) {
this.ball.showExplosion(); this.resetBall(); this.metaData.teams.red.score++;
}
// Check for ball in Goal 2
if (this.arena.isObjInGoal2(this.ball)) {
this.ball.showExplosion(); this.resetBall(); this.metaData.teams.blue.score++;
} }
}
54 msdn magazine
Game Development


































































































   56   57   58   59   60