Specialization: Networked shooter
A basic multiplayer third-person shooter with client prediction and networked gameplay elements.
- A dedicated server that handles multiple clients, where each client controls a third-person player that can move/fire at other players seamlessly in a small arena.
- Dedicated to enhancing my knowledge in netcode through means such as: handling packet loss, server authority and client prediction.
- Build experience writing netcode for core gameplay mechanics and developing OneNet, a network library with focus on ease of use for further development and scalability with our in-house Engine.
- Built with Winsock and UDP as protocol.
- Character and animation based on our most recent school project, Serpentine Estate.
3d multiplayer shooter
5 weeks part-time
- Simulating movement without lag on clients.
- Server controlled physics and reconciliation of movement.
- Hit registration.
- Synchronizing animation states.
- Optimizing packet size and packet transfers.
- During the first week I focused on integrating my network library, which was in its very early stages, into our engine OneEngine.
- In order to prepare I did some research on upcoming challenges I would face such as client prediction and packet loss. Taking inspiration from Unreal Engine and GafferOnGames.com.
- Setting up the core foundation of all data I would be sending between clients/server.
For this I created 3 projects, shared (which would contain functions that are shared between client and server), UDPServer (server code) and UDPClient (client code).
- Packets are sent through a base class, NetMessage, which all types of data I sent, would inherit from.
- For ease of use, each message is defined by; the type of message, whom to send to, and if it's guaranteed or not.
- RPCs (Remote Procedure call), meaning calling a function locally to send a message to call that function on another instance that is not our instance.
- Using our engine, creating a prefab (which works very similar to Unity) for both our server, client and the simulated client.
- Moving clients locally and updating server with their position.
- Below is a gif showing the server first, and the client second.
- This week I wanted to add some simple gameplay, hence adding a way to fire at other players.
- This could be accomplished by:
1. Player receives input.
2. Tells server to do a raycast with PhysX at given position and direction the player is aiming.
3. Server receives packet and does a raycast against all simulated clients from that position and checks if we hit something.
4. If a simulated client has been hit, send a TakeDamage message to that client.
4.5 (If the player would've died with that shot, kill the player instead and respawn at one of our spawn points.)
- In order to pack data more efficiently, I added so each message keeps track of its buffer length (bytes) in order to not always send a full buffer (576 bytes) when in this case, 24 bytes is sufficient.
- Then whenever I send a message, I add the DEFAULT_MESSAGE_SIZE (12 bytes, wich is the overhead of NetMessage) + the size of the message we're sending.
- Below is a gif illustrating a client shooting another client.
- I wanted to prioritize optimization as I realized when I had 3 clients and the server running at the same time, the server would become bottlenecked by the sheer number of messages it had to process each frame.
- To lower the amount of data being sent each frame, I created a fixed time rate of Tick() for Client/Server, clients ticking at 30 frames/s and the server at 20frames/s.
- In order to keep movement smooth, every simulated client on each instance now lerp to their new target position, whenever a message was received of their most recent position. This helped with moments of packet loss, hindering teleportation of simulated clients.
- I realized I could optimize the data sent by using UINT8 instead of short/int. Hence only using 1 byte instead of 2/4 bytes respectively, where possible.
- Added animation states to be synchronized between clients. Most animations of simulated clients are updated locally, depending on their velocity/direction, but some had to be sent through the server. Such as firing.
- In order to serialize data further, I created BigPacketMessage.
- To do this I would have to do the following:
1. Have a list of all messages we want to send this frame.
2. Create a BigPacketMessage, containing all messages we want to send. (Without exceeding our MAX_MESSAGE_SIZE of 576 bytes).
3. Keep creating as many BigPacketMessages as necessary until our list of messages to send this frame were empty.
- I did this to reduce the overhead of each call to Winsock when sending UDP packets, now we would only call Send() for x number of BigPacketMessages needed to be sent.
- Created a small arena for the players to walk around in in our editor.
- In order to have server authorative movement, it was time to add Client prediction and Server reconciliation.
- To do this I needed to add a new message, MoveMessage.
MoveMessage contains a struct of MoveData:
- the timestamp
- the id of the packet
- the position of the client.
- the rotation of the client.
- More on this during week 5!
Week 5 & Final Result
- This week included final polish and finishing Client prediction.
Here's how I handled it:
1. Client adds input, and creates a Move which we add to our queue.
2. Client sends all moves made to Server.
3. Server receives and moves client.
4. Client receives server move and removes all previous local moves made up until the move with the server received ID.
5. Client replays all moves from the last received server move, correcting our position and adding the correction that to our next move to make, resulting in a corrected position.
- I'm quite happy with the progress I've made, I was hoping to add a lobby system to handle connecting to the server. Due to time constraints that had to be cut.
- I would've liked to add more gameplay elements such as sound and a K/D score system in order to make the gameplay more polished.
- There were some obstacles I faced that I struggled to take into consideration and plan accordingly to. Handling issues in our engine that impacted network performance, I had to remove a lot of threaded processes that stalled the server.
- However, I wanted to learn more about network programming and it's fundamentals and I wish to learn more going forward. This proved to be good stepping stone for my netcode knowledge, and I really felt as if I learned a lot!