Network programming adds a lot of complexity to a game engine, as you will soon see. If you are serious about writing a game engine, though, you're already well aware of the dedication it takes to see it through. After all, you wouldn't be doing this if you didn't know the end result would more than make up for the effort! Before we begin, I'll tell you what I think should go into a well laid out networking component, and then I will mention the challenges you can expect to deal with when you upgrade to multiplayer programming!
A flexible networking component should have these capabilities:
Access to Networking API
Complete Session Options
Query Up Existing Sessions
Ability to Host or Join a Session
Track All Players In the Session
Handle Connections and Disconnects
Monitor Message Data Stream
Support Custom Game Messages
Naturally, the Network component will need access to a networking API, like DirectPlay, a part of DirectX. As with the other game engine components, the calls to this API are entirely hidden within the Network object, protecting the programmer from needing to understand all of its workings.
The network component should support being able to create and join sessions using the various favorite protocols: LAN (local area network), TCP/IP (direct connection to another computer by entering an address), and Internet-based TCP/IP (from a lobby of some sort where many sessions are listed).
It should also support both client-server and peer-to-peer sessions. A client-server session is where primarily ONE machine makes the decisions about game object behaviors and forwards those notifications to all clients. A peer-to-peer session allows all machines to share the work of reporting on the objects that each of them have the responsibility to report on. The actual implementation of that logic is not done within the network object itself, but in what messages are sent across the network and how they are handled. However, there may be some special options available from the network API that optimize either possibility.
>>> Not done yet, sorry!! :)
Upgrading from Single Player to Multi-Player Design
The move from single player gaming to multiplayer gaming introduces the following challenges:
Even if you are only doing single-player programming right now, if you plan to do any network programming in the future, it may be worth your while to take a look at this section. Especially the section on session time, where it mentions the concept of session time and how that value is used to coordinate object movement and animations. If you incorporate its design into your single player code, it will help to avoid doing a large revamp of your game object behavior routines when you finally decide to tackle multiplayer.
Machines will send packets of information, or messages, to each other in order to maintain a common state across all machines in the session. This helps to ensure that everyone is playing the game according to the same circumstances. A message can be sent whenever a player changes their information, a connection or disconnection occurs (which may or may not be automatic in the API used), or a game object that a machine is responsible for changes its behavior or movement.
Generally, a message is stored in a structure or a class. The first few bytes of the message will contain its type. Once the message type is determined, the rest of the message can be "cracked open" to get the rest of the information. Examples of messages sent would include:
Messages that contain no data: "Synchronize your session time with me", "You are kicked out".
Messages that include data: "I am pinging you at time T" ,"A missile was fired at position P, in direction D with velocity V at time T, locked on to object O".
Some messages are standard for every game engine and might handle tasks like synchronization, watching for connection timeouts, connections and disconnects. Other messages are custom to each game and include game object behavior, scoring values, and special game events, like a scored goal or a weapon strike.
When a message is received, the data items are read and the appropriate action taken. For example, if "You are kicked" is the message received, it means the host has booted you from the session, so the program forcibly disconnects you. This in turn sends the automatic "I have disconnected" to the other machines to perform whatever maintenance is necessary to accommodate your departure. If information about a game object is reported, that object is located in the game list and updated with the new information.
Sometimes, one message will kick off an entire exchange between two machines. The simplest example is the ping. One machine sends the message "I am pinging you at time T" to another, where T is the current session time. The receiving machine takes that message and replies with "You pinged me at time T." When the first machine receives that message back, it takes its current session time and subtracts T to get the round-trip time, or ping. Message logic systems can get much more complicated than this. Other examples include synchronization of session time, and requesting the game state from the server and/or the other clients.
This part gets messy. When you send a message across the network, you might assume you can just trust the system to deliver it. This is not always the case, especially when it comes to the internet. You have to be wary of the problems that can occur and code for them, otherwise your players will get frustrated from the bad things that can happen from an unreliable connection.
A message failure falls into one of the following possibilities:
Received out of order - by nature of the internet, delivery times vary and messages can be received in a different order than they were sent. If you code in a way that requires AND assumes the order will always be correct, you will run into problems, unless you code specifically to wait for the first message to arrive before you send the next one. This would be too slow for gaming, so it is best to code in a way that makes receiving messages out of order non-destructive. Rare, important messages can be coded to force the right order, but don't do it with all of them. In other cases, a sequential ID can be sent with the message, such that if a message is received with an ID that is smaller than the last message received, it is definitely out of order and may or may not be discarded.
Packet loss - It's the internet. It happens. And it gets in the way of being able to depend only upon very small packets that update only those parts of a game object that have actually changed. Usually the way this is handled is to occasionally send a full game object update every now and then to make sure all objects are fully current. For the very important messages, you can wait for a confirmation from the destination that the message actually arrived, and if there is none after a short time, send it again. Do NOT do this with all types of messages!!
Double receives - It might not happen a lot, but it happens. Code to handle the same message received more than once. Make sure a double send isn't destructive in your game. One example would be "Rotate 10 degrees." If this message is received twice, it will throw off the gamestate. This kind of packet would avoid that: "Place object at angle A, rotate at rate R, at this time T." But that packet is much larger. Another possibility to is send an ID with every message, and if the same ID is received more than once within a given amount of time, the message could be discarded.
Developing the strategy behind handling these possibilities AND keeping the packets small is one of the biggest challenges of network coding. And the challenge will be different for every game. Keep all of these factors in mind with every message logic system you design. You will need to find a balance between keeping packets small and ensuring enough accuracy across all machines to maintain an adequate (NOT perfect) gamestate.
In a single player game, every event happens at that given CPU clock time. The system clock is fine for tracking those times, but events occur in multiplayer in different time spaces. Not every event will occur at the same time, and not all objects will be caught up to the last frame rendered, especially after some network messages resulted in the creation of some new objects. So, I'd like to introduce the concept of session time.
In my own designs, once a session is started, I prefer to set the session time to zero and let it proceed upwards at the same rate as the system clock. This session time becomes the base for all operations. It will be used to represent the last time an object was moved, the last time an animation was rendered, and when other game effects have occurred. As the session time increments with each frame or tick, the amount of time passed since the last time any object was rendered or moved is used to make those objects behave up to that point in time. This allows all behaviors and animations to ultimately move with the system clock, as they did with a single player game.
It also provides the base for synchronizing all events on all machines connected to the same session. When a game object is generated as traveling in a certain direction at a certain session time, knowing that all machines are operating on a similar session time allows the physics and animation routines on this machine to catch the object up to the current time in this machine's time space.
It isn't always important to make sure every machine on the network has the same session time. Simple chat programs and games that don't allow real time dodging of shots do not depend on a synchronized session time. The message gets there when it gets there, and the effect just appears in the other machines' game states.
However, there may be times, in the event of first person shooters, or real time strategies, that the actual time something occurred is important. In that case, a session time is submitted with every message, and the effect is calculated to have occurred at that time on everyone's machine to allow for a more accurate representation.
If you are going to make use of session time in your calculations, the clients must sync up with the host when they join a session before they can be allowed full participation. Once the session time is approximated, however, it rarely needs to change again.
When a client joins a session that has a session time, it has to synchronize its session time with the host so that similar game states can be maintained. First, the host sends its current session time to the client, as an initial approximation. Then, the client will send its current session time to the host repeatedly, and the host will reply with its own session time to approximate a ping value. When a smallest travel time is acquired, the client session time is adjusted such that the host's session time is at the midpoint of the message's travel time. This method of approximation assumes that the travel time from client to host, and from host to client, are the same. This is not always the case, but it is close.
There are some potential problems that can occur with synched session times:
Negative ping - The host and a given client may have a good approximation of session time with each other, but that doesn't mean all the clients are just as well synced. I can recall a few instances while playing Battlezone that my opponent was firing at me, but his shots were striking his tank in the backside. I figure this was because our session times were badly synced, and his machine was reporting to me that he had fired a shot at a time that in my game state hadn't even occurred yet. So the program back-calculated the position of his shot along its trajectory, placing it behind the tank. The shot would then strike his tank in its backside. To correct the problem, the program should have held the message in limbo until that session time occurred on my machine, and THEN applied the message. Watch out for this possibility in your coding. You may even want a client to complain to the host that another client had sent it a "negative ping", and let the host keep track of everyone's session times in comparison to each other to decide who should forcibly adjust their session time by a given amount to correct the error.
Host Disconnect - If the host disconnects and migration is allowed (meaning another client gets the host responsibilities), all clients that were busy syncing must be told by the new host to restart their synchronization. If the new host is a machine that was busy syncing, the program should forcibly transfer hosting to a different machine. If the API being used doesn't support transferring the host responsbility, then the program should forcibly kick the client out to cause the migration to occur on its own. If all clients were busy syncing, either one client must be chosen to host or the whole session should be terminated. Your own designs may be a little more flexible, depending on the needs for accuracy and the complexity of the data handling during a connection. In any case, forced termination should be accompanied by a message stating the reason.
To generate an accurate representation for all machine involved in a session, the nature of how messages are handled must be such that it promotes a similar game state on all of them. It doesn't have to be absolutely perfect, just close enough.
Only the most important events need to have their accuracy maintained. The position, time and direction of a weapon shot is one example. The position of a target and its velocity is another. It is not necessary, and very discouraged, to send updates of non-crucial elements across the network. Examples would be the smoke that comes off a fire, and particles that fly away from an explosion. Instead of sending these updates across the network, send a message that there is a fire in a given position, or that an explosion took place, and let the client at the other end generate its own objects to represent the fire and explosion.
(Need more here! Not done yet!!)