Page 56 - MSDN Magazine, October 2017
P. 56
and send as little data as is absolutely required. For example, an object that hasn’t moved or changed shouldn’t be broadcast. On the other hand, if a new player has just connected, that player does need a full description of all the game objects to bootstrap the game state.
Last, the client-server communication will need to emit and capture out-of-band events, which describe game events that don’t fit as part of the normal game progress. For example, the server may want to communicate to a specific player that he’s reached a special achievement. This mechanism can also be used to report a message to all players.
Here’s the implementation. On the server side, I bring up an express node.js server and a socket.io service. The code shown in Figure 3 configures the HTTP server on port 3000, initializes the server engine and the game engine classes, and routes all HTTP requests to the dist sub-directory where all the HTML and assets are placed. This means that the node.js server has two roles. In the first role, it serves as an HTML server that serves HTML content, including CSS files, images, audio and the 3D models in GLTF for- mat. In the second role, the HTTP server acts as a game server that accepts incoming connections on socket.io. Note that best prac- tice is to cache all static assets behind a content-delivery network (CDN) for good performance. This configuration isn’t shown in the example in Figure 3, but the use of a CDN dramatically increased the game bring-up performance, while reducing the unnecessary load from the game server. Finally, at the last line of the code, the game is started.
The server has started, but has yet to handle new connections. The base ServerEngine class declares a handler method for new connections called onPlayerConnected and a corresponding onPlayerDisconnected handler. This is the place where the author- itative server subclass methods can instruct the game engine to
Figure 3 Server Entry Point
create a new car or remove an existing car. The code snippet in Figure 4 from the base class shows how socket.io is used to ensure those handlers are called when a new player has connected.
When a player connects, this code emits the playerJoined event twice. The first event is emitted on the game engine’s event controller, where other parts of the game code may have registered listeners to this specific event. The second event is sent over the player’s socket. In this case, the event can be captured on the client side of the socket, and acts as an acknowledgement that the server has allowed this player to join the game.
Note the server engine’s step method—here, the game state is broadcast to all connected players. This broadcast only occurs at a fixed interval. In my game I chose to configure the schedule such that 60 steps are executed per second, and a broadcast is sent every sixth step, or 10 times per second.
Now I can implement the methods in the subclass, as they apply specifically to the game. As shown in Figure 5, this code handles new connections by creating a new car and joining that car to
Figure 4 Connection Handler Implementation with socket.io
class ServerEngine {
// The server engine constructor registers a listener on new connections constructor(io, gameEngine, options) {
this.connectedPlayers = {};
io.on('connection', this.onPlayerConnected.bind(this)); }
// Handle new player connection onPlayerConnected(socket) {
let that = this;
// Save player socket and state this.connectedPlayers[socket.id] = {
socket: socket,
state: 'new' };
let playerId = socket.playerId = ++this.gameEngine.world.playerCount;
// Save player join time, and emit a `playerJoined` event socket.joinTime = (new Date()).getTime(); this.resetIdleTimeout(socket);
let playerEvent = { id: socket.id, playerId,
joinTime: socket.joinTime, disconnectTime: 0 }; this.gameEngine.emit('playerJoined', playerEvent); socket.emit('playerJoined', playerEvent);
// Ensure a handler is called when the player disconnects socket.on('disconnect', function() {
playerEvent.disconnectTime = (new Date()).getTime(); that.onPlayerDisconnected(socket.id, playerId); that.gameEngine.emit('playerDisconnected', playerEvent);
});
}
// Every server step starts here step() {
// Run the game engine step this.gameEngine.step();
// Broadcast game state to all players
if (this.gameEngine.world.stepCount % this.options.updateRate === 0) {
for (let socketId of Object.keys(this.connectedPlayers)) { this.connectedPlayers[socketId].socket.emit(
'worldUpdate', this.serializeUpdate()); }
} }
}
const express = require('express'); const socketIO = require('socket.io');
// Constants
const PORT = process.env.PORT || 3000;
const INDEX = path.join(__dirname, './dist/index.html');
// Network servers
const server = express();
const requestHandler = server.listen(PORT, () => console.log(`Listening on ${PORT}`));
const io = socketIO(requestHandler);
// Get game classes
const SLServerEngine = require('./src/server/SLServerEngine.js'); const SLGameEngine = require('./src/common/SLGameEngine.js');
// Create instances
const gameEngine = new SLGameEngine({ traceLevel: 0 }); const serverEngine =
new SLServerEngine(io, gameEngine, { debug: {}, updateRate: 6, timeoutInterval: 20 });
// HTTP routes
server.get('/', (req, res) => { res.sendFile(INDEX); }); server.use('/', express.static(path.join(__dirname, './dist/')));
// Start the game serverEngine.start();
52 msdn magazine
Game Development