Making a Multiplayer FPS in C++ Part 4: Rebuilding The Client in C++
In the previous part of this series we had an outline of an online game with a fixed tick rate, and support for multiple clients to connect and move around. In this part we bin the horrible client made in Unity, and implement one in C++. You can browse the repository at this stage of development here.
The reason I want to rebuild the client in C++ is that for upcoming work on client-side prediction, we’ll need the server and client to have a certain amount of shared code. It’s possible to re-implement this shared code in C# for the client, but it would be easier to maintain if we just had one codebase written in C++. Plus I just prefer doing stuff in C++, so there.
Unlike the server, the client will need to show visual stuff, which means it’ll need to be a windowed application. I’m not a fan of making things by plumbing together a bunch of frameworks, and opening a window is actually deceptively simple. I won’t go in to details here, there are plenty of guides out there for opening a window, I recommend the first few days of Casey Muratori’s Handmade Hero series.
The other thing we’ll need for ‘visual stuff’ is a way of getting our ‘stuff’ on screen. I’m intending for this to be a 3D game of some sort, so we’ll need a way of displaying 3D graphics. Again, there are a lot of frameworks out there, but I have yet to find one I really like. So I’ll be using Vulkan, a relatively new low-overhead, cross-platform (my main reason for choosing it over DirectX 12) graphics API. The code for this lives in client_graphics.cpp, I won’t go in to detail about this (it’s not really the point of this series), if you want to learn about Vulkan then I’d recommend looking at some of these learning resources.
So, I’ll start from client.cpp line 105, this is where the windows set up stuff ends. We start with initialising the state we need for graphics. For the Unity client, each player was represented by a capsule, for this new client each player is represented by a rectangle (no actual 3D yet, that’s a job for future Joe). For now I’ve implemented this with a single vertex buffer with 4 vertices per player to form each rectangle, each frame the vertices are updated and drawn.
|
|
We have our array of vertices, we know the maximum vertices we’ll need is the maximum number of clients multiplied by 4.
|
|
The Graphics::Vertex struct is defined in client_graphics.cpp. Moving on in client.cpp:
|
|
For each player we generate a random fully saturated colour, it looks a bit ugly, a proper HSV implementation would make it a lot cleaner. The colour is applied to all 4 of the players verts, and all verts will have position (0, 0) if there is no player to show.
|
|
We create the indices which will be uploaded to the index buffer to draw the rectangles. This won’t need to change after it’s set initially.
|
|
Finally we initialise the graphics system, which stores some relevant state in a Graphics::State struct which is used later to draw.
|
|
I’ve taken the winsock code needed by both the client and the server, and moved it to common_net.cpp. This just has some basic utlities for creating sockets, sending/receiving packets, etc. Some client/server specific netcode is in client_net.cpp and server_net.cpp respectively, this is just to stop the compiler complaining that I never call some of those functions when compiling one application or the other.
|
|
Client_Message::Join packet layout
This is the buffer we’ll use for reading/writing packets. The first thing to do is tell the server we want to join.
|
|
The client has it’s own version of Player_State which only contains the state which the server sends to the client (on the server there is an additional speed field), and an array of these to store the state of all the player objects. We store the slot assigned to us by the server, as this is needed for us to send input packets. Lastly the Timing_Info struct contains some data about the frequency of the performance counter, and whether or not our desired sleep granularity was set - these are both used to wait for the end of the tick at the end of each main loop iteration (more detail on this in part 2).
|
|
At the beginning of each tick we sample the clock.
|
|
Standard windows message pump stuff (look at win32 programming resources for further information on what this is).
|
|
This loop will keep grabbing packets from the socket for as long as there are any available.
|
|
Server_Message::Join_Result packet layout on failure Server_Message::Join_Result packet layout on success
See server.cpp for corresponding code where this message is sent. The first byte is a 0 or 1 indicating whether we were allowed to join. If that byte is 1, then the next 2 bytes are a uint16 for our slot.
|
|
Server_Message::State packet layout
A state packet contains all the player objects, so we just read until we run out of bytes. The server sends the slot (called id here) which we currently don’t use, in future we will use this field to figure out which object the player actually owns.
|
|
Client_Message::Input packet layout
Each tick we send our input. When key events are sent to the window, relevant information is stored in this global g_input struct. This packet just has the four buttons encoded in a bitfield.
|
|
Here we run through each object we read from the most recent state packet, and compute the 4 vertex positions to represent it.
|
|
Then zero the x and y positions of all vertices for player objects which have no actual player to show.
|
|
Then pass the updated vertices to the graphics system to update and draw, and finally wait for the tick to end, before starting all over again!
|
|
Client_Message::Leave packet layout
Just before the application exits, the client sends a message to the server to say we’re going. If we don’t send this, the server will time us out after some time, but it’s preferable to notify the server properly so the slot that was allocated to this client can be re-used immediately.
This part doesn’t feel like much of an improvement, just rebuilding what we already had. However, it’s a necessary step for things we’ll want to do later.