The past 7 days of coding has possibly been the most frustrating in the entire project. The fact of the matter is that writing really good, efficient real-time multiplayer code is hard if you've never done it before. I spent days ironing out the kinks and ended up reworking the protocol more than once. There were several times where I just threw my hands up and walked away from the computer. Major hurdles included: having to ditch TCP because of latency, finally getting around to learning NIO but having NIO problems on Android. All in all it was a tough week but at the end of it, I have a game that's at least playable at 30-60fps over WiFi without too many problems.
I originally tried to implement the data protocol using TCP. I figured that since the players are always on a LAN because of WiFi, that they will always be low-latency and therefor TCP should work. I was wrong. The round-trip ACK/SYN/whatever that TCP uses to ensure delivery adds a noticable amount of latency. I got everything working with it but wasn't happy, so I switched to UDP.
I had always heard that Java's NIO was better but had never implemented anything in it. I didn't need the concurrency stuff since this is fairly simple and straightforward so fortunately I didn't have to deal with selectors at all. I switched to UDP and implemented it using NIO channels. I had a major performance problem using DatagramChannel.receive() because Android does some DNS operation of some sort and logs it. I think it was maybe just the logging but suffice to say that logging 60 times per second is enough to start screwing things up when running a game. I had to instead use reciprocal connections - both the client and server "connect" to each other so that they can use read() and write() instead of send() and receive(). No, UDP does not truly "connect" but it's how Java's Datagram NIO works. This means that I need a unique port for each client but since this is a 3 player max game, that's not a problem.
Once I switched to UDP, I had to implement a heartbeat from the clients so that the host could track lost connections and such. Overall it works satisfactory. I'm surprised by the network lag sometimes because I wouldn't think it would be an issue on a LAN but since this is my first Real-time MP protocol, I feel like I can cut myself a little slack.
One thing I REALLY like about NIO is the ByteBuffer class. It makes it extremely easy to write simple, small, fast protocols. The only thing it's missing is a 3-byte int, but those are rarely used so it's not a huge deal.
Most of the days were spent testing and tweaking through much trial and error. It was very frustrating. I did eventually get things ironed out well enough to where by the end of the day today, I was playing several multiplayer games in a row against my fiancee. She's currently pretty bad at the game (don't tell her I said that) but the testing went well and I was happy with the playability of the multiplayer. I think people are going to enjoy it.
Day 45 - What was done:
Optimized object fields and related netcode to use bytes and shorts where appropriate.
Changed network client from blocking update to interpolation between updates.
Fixed Tie-game result
Changed lose/win states into a single result state
Added scores to world
Changed state update code to work better for the net client
Day 46 - What was done:
Made RealtimeNetworkClient and Game Objects netcode more memory efficient
Fixed stability problems when quitting multiplayer - was causing major testing headaches, found that always using read timeouts is essential with android sockets.
Spent hours finding a thread synchronization issue stemming from cached net objects
Flattened out trail data and wrote a very efficient object model updater on the client side
Day 47 - What was done:
Refactored items to remove obvious code duplication
Implemented item netcode
Got items tested and working correctly w/very efficient code
Day 48 - What was done:
Implemented all netcode for all map objects
All levels tested and working
Decimator working but a little buggy
Day 49 - What was done:
Added variable-length data size for player and for MP packets
Now the multiplayer game shouldn't crash because of running out of space on the stream.
Show scores between rounds
Show final results
End game after correct # of rounds
Stated are synced up better
Here's how packets are laid out, assuming 1024 bytes available:
short packet length (2)
byte net objects (1)
-- Net Object
- Header (7)
byte obj type (1)
int obj id (4)
short data length (2)
-- World max data length (93)
-- item data length (5)
data length totals with header data for largest case scenario
world total = 100
items total (12 * 3) = 36
map objects max total (powerbox) = 67
187 before players = 837 for all players or
279 per player for 3 players or
418 per player for 2 players
It will take over 50 trail segments before increasing the packet size beyond 1024 bytes
Day 50 - What was done:
Tried using running average for host latency - didn't work well
Switched to NIO-based UDP for real time protocol
Day 51 - What was done:
Implemented much of the new menu styles
Switched to full use of ByteBuffers for real time multiplayer
Tried to add connection gained/lost detection
Day 52 - What was done:
Finished NIO UDP implementation
Fixed multiplayer sound issues
Got 3 player MP game working
Fixed client heartbeat
Fixed MP default input
Added MP host detection to main menu
Play tested several games with 2 and 3 players
3 player games work, though there are still a few bugs and it does sometimes get a little bit jittery and laggy. 2 player games are fairly smooth, though and since that's what most of the games are going to be, I'm happy with that for now. I just want to get the game finished since it's over a month delayed and I can come back to work on the multiplayer code later.
Here's a video of the multiplayer code at the end of Day 45
Things are MUCH better than that at day 52.
I haven't made a more recent video because it's really a pain to set up the camera, but I'll do one for 3-player action when Multiplayer is totally complete.