Preface
Sorry for the weird order, this is partly chronological and partly what came out of my mind.
UI
I'm not really a frontend guy, give me some backend service nobody will ever look at, and I'm happy. But since RPGs usually have a rich GUI (Chat, Party, some kinds of Maps, Options etc.) and I'm doing it alone, I'll have to do it.
After messing around a lot with Urho3D's UI, I think slowly I get used to it. I even tried Nuklear but realized it's not what I needed due to its immediate mode nature, and the code ends up to be a bit spaghettiish.
With Urho3D I can create the UI with the Editor, save it as XML file and load it at runtime, subscribe to some events and it's done. It's even possible to combine several windows into one complex window. For example, the Options window consists of six windows: The container with just the tabs (taken from Urho3D-UI-Components) and each tab is a separate window.
So far we have:
- a fully working Chat window with the different channels,
- a partly working Options window with fully customizeable shortcuts and multiple shortcuts per action,
- a Party window and Mission Map that's not working at all,
- a fully working Mail window and compose New Mail window (except that the multiline edit still bothers me a lot),
- a fully working Game menu,
- something that shows the Ping to the server and other things.
Network
Some details about the network layer. As mentioned earlier this is not a fast game, like an action game or a shooter. That's why I chose TCP over UDP. Using TCP takes out a lot of problems, like packet losses, wrong order of packets. With TCP, packets are always received in the right order, or something goes terribly wrong, e.g. disconnects. Of course this comes at a price (no advantage without disadvantage!), higher latency. But for slow games it may not matter so much.
Update rate
You know that interactive programs execute an endless loop. An interactive console program could look like this:
void main()
{
bool running = true;
while (running)
{
std::string input;
std::getline(std::cin, input);
if (input.compare("quit") == 0)
running = false;
else if ...
}
}
A Windows program looks usually like this:
void main()
{
MSG msg = { 0 };
while (msg.message != WM_QUIT)
{
if (::PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE))
{
::TranslateMessage(&msg);
::DispatchMessageW(&msg);
}
}
}
A game isn't much different:
void main()
{
while (the_game_is_running)
{
// Update game state
Update();
// Render the frame
Render();
// Wait to meet the desired frame rate.
// If vertical synchronization is on, the Graphics API may do this.
WaitSomeTime();
}
}
This game server does it similar, the games have an Update() method which is executed from time to time. The difference is that a game server does not update the game state in an endless loop and waits most of the time, but it uses a Scheduler. It would be a waste of time when it sleeps most of the time, it can simulate other games (or do something completely different).
The game update method of this game server looks a bit like this (simplified):
void Game::Update()
{
// Dispatcher Thread
int64_t tick = Utils::AbTick();
if (lastUpdate_ == 0)
lastUpdate_ = tick - NETWORK_TICK;
uint32_t delta = static_cast<uint32_t>(tick - lastUpdate_);
lastUpdate_ = tick;
// First Update all objects
for (const auto& o : objects_)
{
o->Update(delta);
}
// Send game status to players
SendStatus();
// Schedule next update
const int64_t end = Utils::AbTick();
const uint32_t duration = static_cast<uint32_t>(end - lastUpdate_);
// At least SCHEDULER_MINTICKS
const int32_t sleepTime = std::max<int32_t>(SCHEDULER_MINTICKS, NETWORK_TICK - duration);
Asynch::Scheduler::Instance.Add(
Asynch::CreateScheduledTask(sleepTime, std::bind(&Game::Update, this))
);
}
The server runs the simulations every 50ms (NETWORK_TICK = 50), this are 20 simulations per second. The client runs updates every ~16ms (60FPS) or ~8ms (140FPS) or whatever frame rate it is running.
The client sends all collected packets (e.g. player inputs) all 16ms (60 times per second) to the server, no matter if it runs at a higher frame rate (lower frame rates are a problem at the moment, that needs to be addressed...). The server collects all this inputs from the client and puts it into a queue, which is processed in the next update to calculate the next game state.
Delta compression
Delta compression means the server does not always send the whole game state to all clients, but only what has changed since the last state. This reduces bandwidth usage drastically. Since we use TCP we can assume that the client always knows the previous game state, so it's safe to send only the difference.
This is the only compression it uses. The packets are small binary packets and may not compress well, so it's probably not worth the CPU cycles.
Client/server asynchronicity
There are mainly three methods to address the problem that client and server are asynchronous, which is caused by different update rates (20 updates/sec on server, 60 updates/sec on client), network latency, server load etc.:
- Entity interpolation is already done with Entity Position Interpolation.
- Input prediction is not yet done.
- Lag compensation/Server reconciliation will not be done. This is more important for fast games.
Further reading
In no particluar order.