I'll be implementing multiplayer in Carnage soon too. It's definitely a challenge. Here are some of my observations and thoughts that may help.
Client
The client handles almost everything locally. So all controls cause local movements, firing etc. as if it's a local game. The client also handles it's own collision with enemy units. So when the player runs into a multiplayer enemy, I use my local collision functions to move the player accordingly. All this arrives at positional data and projectile creation data that is sent to the server. So packets will be something like:
Routinely sent positional data packet (maybe every 100ms):
- X,Y,Z player position
- X,Y,Z player speed
- X,Y,Z player angle
Special packets sent as soon as the event occurs:
1. Fire button pressed. Packet contains information on the projectile created and it's data, e.g:
- Bullet created
- X,Y,Z bullet pos, speed, angle
2. Movement key pressed. As well as the routine position updates, whenever a movement key is pressed or release, that would result in a sudden shift of direction/velocity, an additional position packet is sent.
Server
The server is responsible for predominantly handling game logic and damage as millenium7 said. So it's running it's local version of the game, receiving packets from players describing their positions, speeds and angles, and also their creation of projectile entities. It's main purpose is to forward all these packets onto the other player clients, so they can run their own "position other player" and "create bullet projectile" functions, based on the packet data.
To keep things fair, it does the calculations on projectile to player damage, and sends packets back to the clients with that information. A list of possible packets:
1. Client position packets
- The player ID
- X,Y,Z position, angle, speed of this player
2. Projectile to player collision packets:
- The projectile ID that is hitting a player
- The client ID being hit by the projectile
- The X,Y,Z positional data of projectile/clients at point of collision
3. Game logic packets like:
- A weapon has spawned in X,Y,Z of a certain type
- The timer is currently at 2:23 secs (to ensure game synchronisation)
4. Client state packets like:
- Client 3 has just dropped, so remove him from the game
- Client 5 has just joined the arena, so create a new player
What I'll end up with is a load of unique functions for the client, and special functions for the server, which handle all these potential packets and handle the logic appropriately. So for example:
- CreateBullet(x,y,z,xspd,yspd,zspd,xang,yang,zang,owner_id,timestamp)
-PositionPlayer(PlayerID,x,y,z,xspd,yspd,zspd,xang,yang,zang,timestamp)
- InjurePlayer(PlayerID,damage,timestamp)
- NewPlayer(playerID,x,y,z,timestamp)
- DropPlayer(playerID,timestamp)
A top level HandleReceivedPacket() function analyses the packet and it's contents and calls the appropriate function.
Similarly, there will be sending functions:
- SendPosition(myPlayerID,x,y,z,xspd,yspd,zspd,timestamp)
- KeyPressed(myPlayerID,keypressed,timestamp)
- QuittingGame(myPlayerID)
And all those functions call a top level HandleSendingPacket() function which packages the data up with the correct headers and sends it off to the client or the server.
You've mentioned issues to do with object number ID and things, but those have to be completely unrelated to the game logic in multiplayer. It should be irrelevant if client1 has player 2s character model as object number 3, for example. You need to be writing functions that create player models, projectiles, pick-ups etc. without regard to DBPs object ID system. Instead, they need to have an ID you program yourself, as part of a Type structure. Since the games will never be synchronised 100% across clients, because of latency, bullets, players, particles etc. will all be generated out of order with each other, so you can't rely on object IDs ever being consistent.
The key thing mention above is latency, and you'll notice timestamps mentioned in all the functions. As Millenium7 mentioned, it's all about interpolation based on speeds and latency.
The first step is to synchronise the client and server. So before a game starts, the client sends some ping packets to the server, which the server receives and sends back straight away. The client includes a timestamp in that packet, and waits to get it back from the server. Once it receives it, it compares that packet timestamp to the current time, and can determine a ping from that. If 10 ping packets take an average of 100ms to do the round trip, the client can assume it take 50ms for his data to reach the server. The server does the same thing, and keeps a table of how long it takes to send/receive data from each client. This is performed routinely throughout the game, so this table of latency is up to date.
You can use this ping to ensure clients all start at exactly the same time. So when the server sends a "GameStartsIn1Second" packet to the client, he knows it takes 50ms to get data from the server to him, so he starts the game in 950ms. This ensures everyone is synchronised.
When you know the ping between each client and the server, you can start to figure out where things should be, and interpolate positional values. Here's a step by step of a latency example:
1. Client1 pressed Fire at an imaginary time of 0ms.
2. A FireBullet packet is sent to the server at 0ms.
3. Server receives this packet at 30ms.
4. Server knows he has a 30ms ping to Client1, so he runs his CreateBullet() function, passing it a value of 30ms.
5. The CreateBullet() function creates the bullet, then moves it along in space whatever distance it should travel in 30ms. Now Client1 and the Server have synchronised positions for that bullet.
6. The server sends a FireBullet() packet to each of the other clients, using this new position (or alternatively the original position, but with a message describing Client1s latency)
7. Client 2 receives this packet at 100ms.
8. Client 2 runs its createBullet() function, moving the bullet on by whatever distance it travels in 100ms.
In that example, even though the data takes 100ms to go from Client1 to Client2, the world will still be in perfect synchronisation. The only problem is the bullet starts further ahead in space on Client2 than is ideal, but there is NOTHING you can do about that. That is why online games require low pings (sub 50ms ideally), so the effect of latency is negligible, and thar's why your server should kick players with high ping.
Anyway, that's the crux of it! Key steps are:
1. Write code that is independent of DBP object IDs etc. and use your own IDs to describe entities in the game.
2. Write a load of functions to handle positioning and creation of game entities, with the ability to reposition them based on latency of the data received.
3. Write some top level functions which handle packets and call the appropriate creation/position functions.
4. Keep the games perfectly synchronised by sending ping packets regularly so the clients/server know how long it takes to send data back and forth.
5. Time stamp everything
6. Pray to God it works
Multiplayer coding is a mega headache and will take ages to tweak and get right with a complex game, but it's worth it.
I hope that helps a bit. It helps me to write it down at least!