PongOut: 2 player Pong-meets-BreakOut
The game starts off with 2 players essentially simultaneously playing BreakOut, but once enough bricks are broken the strategy changes. If I as the red player hit the blue player's ball into a brick, the blue player is still the one to get the point. So you want to try and keep your ball on your side so that you can aim it at bricks, and whenever the opponent's ball gets through you want to either send it right back (without hitting any bricks), or ideally send it off their side of the screen. The game ends when all the bricks have been broken.
Network Architecture
Since PongOut is a game for two players, the server will accept connections from up to two clients and gracefully reject additional ones. The game won't start with only one client. If one of the clients drops out once the game has begun, another client (or the same client) can reconnect and resume control of the forlorn paddle.
I used SFML v2.1 for graphics, but I opted not to use their networking capabilities. For the networking I decided to do event-based I/O using the WinSock library and WSAAsyncSelect. This was a more hands-on approach than using SFML, meaning I learnt more about how network programming actually works.
Application Protocol
- Message id: each program tracks how many messages it has sent, and sets the message id accordingly. This means the order of messages from a certain sender can be determined (though this is somewhat superfluous here as PongOut uses a TCP connection, rather than UDP).
- Connection id: this is sent by the server when it accepts a new client. The client then sends this back to the server with every subsequent message.
- Message type: an integer value stating what kind of message this is. See below.
- Data: there are a series of structs containing the different data for each message type, ranging from 0 to 20 bytes. Since the amount of data is so small, these structs are all stored as part of a union in the message structure. This simplifies things as it means every message is the same length.
- MT_WELCOME. Sent from the server upon acceptance of a new client. This tells the client what it's connection id is, which the client then includes in all subsequent messages to the server.
- MT_REJECT. Sent from the server when at max capacity.
- MT_TIME_SCALE. Sent periodically by the client. Immediately sent back by the server with what time it thinks it is. The client can then calculate the amount of latency and the required simulation time scale/offset. See below.
- MT_START. Sent by the server when enough clients connect to start the game.
- MT_BRICK. Sent by the server when a ball hits a brick. Tells the clients which brick to stop rendering.
- MT_POSITION. Sent by the client when their player moves; the server then sends one to the other client to move their opponent. Also sent periodically by the server for each ball.
- MT_SCORE. The score for both players, sent by the server when either changes.
- MT_SERVE. Sent by the server to enable a client’s serve controls, and sent by the client to signal that they've served.
- MT_GAME_OVER. Sent by the server to signal the game’s end and tell each client if they won, lost or drew.
Establishing a time scale
So in the example we got a message from the server saying it's 12:05, but we know that it takes 10 minutes for a message to get to us, so we know that currently the server thinks it's 12:15. Client-side we think it's 12:20, so now we know to offset any message we get from the server by 5 minutes. Network conditions can change over time, thereby changing the amount of latency in the connection. This is why the client keeps periodically redoing this calculation.
Prediction and interpolation
But what happens if we get a new update and find that our predicted (and therefore rendered) position is incorrect? This is where interpolation comes in. Rather than simply teleport to the correct location, we want to smoothly interpolate the object's position over a short period of time. Consider the following:
Since we're interpolating over 3 frames we move to 1/3 of the way between our previous prediction (current position) and our new prediction (based on new data). The following frame we move to 2/3 of the way along from our most recent prediction (current position) to our newest prediction. In the third frame we complete the interpolation and in every subsequent frame we no longer interpolate, rather we just move to our newest predicted position. This smooths out what may otherwise be a jerky change of direction.
Mirroring
State Machines
Download
But if you wanted to download the applications and see it working, you can do so here.