Now that I’ve decided to use Flecs to handle my reliability layer, it was time to wire up the server code and start getting acclimated with how flecs works. One of the things I always struggle with when starting to work with a new language or framework is figuring how to pass objects around in the framework specific, idiomatic way.
Before we get into that, let’s take a look at what the server loop looks like now. If you remember previously I was calculating the server tick, but it turns out flecs has a tick rate built in!
All we have to do is call set_target_fps and start our server loop:
flecs::world GameWorld;
// Set our tick rate to 30
GameWorld.set_target_fps(30);
// Import the player module
GameWorld.import<player::Player>();
// Inject the server socket
GameWorld.get_mut<player::Player>()->SetSocket(&ServerSocket);
// Run server loop, which ticks at 30hz
while(GameWorld.progress())
{
...
}
With that settled, we now need to figure out what our player entity and components will look like. So far I’ve come up with the following:
namespace player
{
// NetworkComponent Holds the user id/address and the last 24 sent commands
struct NetworkComponent {
uint32_t UserId;
struct sockaddr_in Address;
std::array<uint8_t, 24> CommandBuffer;
};
// NetworkStatsComponent will hold network related stats
struct NetworkStatsComponent {
uint32_t Retries;
uint16_t Latency;
};
// NetworkStateUpdate to be sent to the client
struct NetworkStateUpdate {
// will hold the state to be sent to client
std::array<unsigned char, 200> Buffer;
size_t Len{0};
};
/**
* @brief Our Player module, holds a reference to our server socket for SendTo.
*
*/
struct Player {
Player(flecs::world &GameWorld);
void SetSocket(net::Socket *Socket) { ServerSocket = Socket; };
private:
net::Socket *ServerSocket;
};
}
This namespace player defines all the components as well as the Player module. The Player constructor will configure all the systems necessary to interact with the Player entities once they join the world.
namespace player
{
Player::Player(flecs::world &GameWorld)
{
// Register the player module
GameWorld.module<Player>();
// Scope the components to this module
GameWorld.component<NetworkComponent>();
GameWorld.component<NetworkStatsComponent>();
// Create a system that has access to our client addr and server socket
GameWorld.system<NetworkComponent, NetworkStatsComponent>("Connect")
.each([&](NetworkComponent& Network, NetworkStatsComponent& Stats) {
fmt::print("Sending packets to {} FD: {}\n", Network.UserId, ServerSocket->GetFD());
ServerSocket->SendTo(NetworkComponent.Addr, ..., ...);
});
}
As a side note, these systems will execute for every entity that is comprised of the NetworkComponent and the NetworkStatsComponent.
I must say I’m already starting to see the power of the ECS system and in particular Flecs, it has a really nice API to work with (so far…)! It’s still taking a bit to understand how it’s memory management system will interact with normal C++. I’m also a bit worried about how to unit test these systems. I should probably make the Socket be an interface so I can mock it easier.
As you can see in the second code sample inside the struct Player, I defined a setter: SetSocket, this was not my first attempt to inject my Socket object. My first attempt was to pass in ServerSocket as a parameter to the module, but unforutnately when you call GameWorld.import<some::mode>() you can’t pass any arguments to the constructor.
My second attempt was to create a singleton, and then look it up inside the “Connect” system. It looked like this:
// main.cpp
flecs::world GameWorld;
net::Socket ServerSocket{"127.0.0.1", 4242};
GameWorld.import<player::Player>();
// If you try to use GameWorld.set<net::Socket>(..) it will call a default constructor
// So we have to use 'emplace' with already created objects
GameWorld.emplace<net::Socket>(ServerSocket);
// player.cpp
//Player::Player module constructor ..., not we use GameWorld.get<> to get the Socket
GameWorld.system<NetworkComponent, NetworkStatsComponent>("Connect")
.each([&](NetworkComponent& Network, NetworkStatsComponent& Stats) {
const net::Socket *ServerSocket = GameWorld.get<net::Socket>();
ServerSocket->SendTo(...);
});
This isn’t a good design because it’s a leaky abstraction, any module or system could then get access to ServerSocket, I only want the player systems to have access.
I should note that the Setter method was recommended by the fine folks over in the flecs discord channel!
So this will be my loop now:
- Read packets from our network thread
- Put them in the LockFreeQueue,
- Iterate the game loop
- Pop the packets off the queue
- Run queries against ECS world with the packets
- GameWorld.progress game loop processes the incoming packets
- Do game logic
- Capture world state for each client and send it using the system to send packets from the Player module
I think the next step is creating a player system that tags any player entity with a custom tag of ‘NeedsToSendSymmetricalKey’, then after N number of ticks keeps checking if it gets an Acknowledgement message back. If it does, removes the tag and then can start processing game packets from the user!