Permalink
Browse files

add USAGE.md

  • Loading branch information...
FredGithub committed Sep 21, 2018
1 parent 5c3d6cb commit bf659622350a203d4d76345c760c62c31cd29bc7
Showing with 275 additions and 0 deletions.
  1. +275 −0 USAGE.md
View
275 USAGE.md
@@ -0,0 +1,275 @@
This guide is made to help you getting started with Yojimbo, integrate it into your game and cover some of the basic usages of the library.
Yojimbo is meant to be used in a client/server architecture. The first thing you will need is a way to run your game "headless" (with no window / rendering context) for the server. Game servers usually run in VPS machines where there is no real use for a window or rendering to the screen. Let's create our main server class called `GameServer` and let it handle everything the server needs to do. The different parts of Yojimbo you need on the server are `Server`, `ClientServerConfig` and `Adapter`.
The `Server` is the main part, used to receive and send messages. The `ClientServerConfig` allows you to configure the server: connection timeout, memory used or the different channels (reliable, unreliable). The `Adapter` allows you to get callbacks when a client connects or disconnects and is also responsible for providing Yojimbo with the `MessageFactory`. Both the `ClientServerConfig` and the `Adapter` are shared between client and server. Here is what it could look like:
```
// a simple test message
enum class GameMessageType {
TEST,
COUNT
};
// two channels, one for each type that Yojimbo supports
enum class GameChannel {
RELIABLE,
UNRELIABLE,
COUNT
};
// the client and server config
struct GameConnectionConfig : yojimbo::ClientServerConfig {
GameConnectionConfig() {
numChannels = 2;
channel[(int)GameChannel::RELIABLE].type = yojimbo::CHANNEL_TYPE_RELIABLE_ORDERED;
channel[(int)GameChannel::UNRELIABLE].type = yojimbo::CHANNEL_TYPE_UNRELIABLE_UNORDERED;
}
};
// the adapter
class GameAdapter : public yojimbo::Adapter {
public:
explicit GameAdapter(GameServer* server = NULL) :
m_server(server) {}
yojimbo::MessageFactory* CreateMessageFactory(yojimbo::Allocator& allocator) override {
return YOJIMBO_NEW(allocator, GameMessageFactory, allocator);
}
void OnServerClientConnected(int clientIndex) override {
if (m_server != NULL) {
m_server->ClientConnected(clientIndex);
}
}
void OnServerClientDisconnected(int clientIndex) override {
if (m_server != NULL) {
m_server->ClientDisconnected(clientIndex);
}
}
private:
GameServer* m_server;
};
// the message factory
YOJIMBO_MESSAGE_FACTORY_START(GameMessageFactory, (int)GameMessageType::COUNT);
YOJIMBO_DECLARE_MESSAGE_TYPE((int)GameMessageType::TEST, TestMessage);
YOJIMBO_MESSAGE_FACTORY_FINISH();
```
Note that the adapter will have a null `GameServer` pointer when used on the client. If you prefer, you can also create a different adapter for the client, but it needs to provide Yojimbo with the same `MessageFactory` as the server.
Let's take a look at the `TestMessage` class and briefly cover basic serialization:
```
class TestMessage : public yojimbo::Message {
public:
int m_data;
TestMessage() :
m_data(0) {}
template <typename Stream>
bool Serialize(Stream& stream) {
serialize_int(stream, m_data, 0, 512);
return true;
}
YOJIMBO_VIRTUAL_SERIALIZE_FUNCTIONS();
};
```
Yojimbo uses a unified serialization system for both reading from and writing to a `Stream`. It might take a bit of getting used to, and for more complex messages, you will sometimes have to split the code path, but we won't get into details here. For now we just want to read/write a simple int, so we use the `serialize_int` helper. There are several serialization helpers provided by Yojimbo and you will probably need to define several more to cover the needs of your game.
The `GameServer` will then have a `GameConnectionConfig`, a `GameAdapter` and a `Server` and initialize everything like so:
```
static const uint8_t DEFAULT_PRIVATE_KEY[yojimbo::KeyBytes] = { 0 };
static const int MAX_PLAYERS = 64;
GameServer::GameServer(const yojimbo::Address& address) :
m_adapter(this),
m_server(yojimbo::GetDefaultAllocator(), DEFAULT_PRIVATE_KEY, address, m_connectionConfig, m_adapter, 0.0)
{
// start the server
m_server.Start(MAX_PLAYERS);
if (!m_server.IsRunning()) {
throw std::runtime_error("Could not start server at port " + std::to_string(address.GetPort()));
}
// print the port we got in case we used port 0
char buffer[256];
m_server.GetAddress().ToString(buffer, sizeof(buffer));
std::cout << "Server address is " << buffer << std::endl;
// ... load game ...
}
void GameServer::ClientConnected(int clientIndex) {
std::cout << "client " << clientIndex << " connected" << std::endl;
}
void GameServer::ClientDisconnected(int clientIndex) {
std::cout << "client " << clientIndex << " disconnected" << std::endl;
}
```
Note the use of a null private key. How to make secure connections to the server is a topic on its own that won't be covered here. But for development, you will be running both the server and the client on your own machine so you're fine with insecure connections.
Also note that the `GameServer` takes an `Address`. The IP part of the address must be set to the external IP address of the machine. You are free to choose the port. For development, just use "127.0.0.1" and a port of your liking. For several LAN machines connecting together, use the local IP address of the server.
The server will need a custom game loop, running at a fixed timestep. Game loops and whether to use fixed timestep or not is also a topic on its own, but for netcode, fixed timestep, for both server and client, is often a good idea. We chose to run the server at 60Hz. Here is what it looks like:
```
void GameServer::Run() {
float fixedDt = 1.0f / 60.0f;
while (m_running) {
double currentTime = yojimbo_time();
if (m_time <= currentTime) {
Update(fixedDt);
m_time += fixedDt;
} else {
yojimbo_sleep(m_time - currentTime);
}
}
}
```
In the `Update` method, there are 3 methods of the `Server` class that you need to call: `AdvanceTime`, `ReceivePackets` and `SendPackets`. Note that the order is very important. Using another order might add a couple frames of delay to your server's latency. Here is what it looks like:
```
void GameServer::Update(float dt) {
// stop if server is not running
if (!m_server.IsRunning()) {
m_running = false;
return;
}
// update server and process messages
m_server.AdvanceTime(m_time);
m_server.ReceivePackets();
ProcessMessages();
// ... process client inputs ...
// ... update game ...
// ... send game state to clients ...
m_server.SendPackets();
}
```
The `ProcessMessage` function consists in looping over all the connected clients and read the messages received from each of them:
```
void GameServer::ProcessMessages() {
for (int i = 0; i < MAX_PLAYERS; i++) {
if (m_server.IsClientConnected(i)) {
for (int j = 0; j < m_connectionConfig.numChannels; j++) {
yojimbo::Message* message = m_server.ReceiveMessage(i, j);
while (message != NULL) {
ProcessMessage(i, message);
m_server.ReleaseMessage(i, message);
message = m_server.ReceiveMessage(i, j);
}
}
}
}
}
void GameServer::ProcessMessage(int clientIndex, yojimbo::Message* message) {
switch (message->GetType()) {
case (int)GameMessageType::TEST:
ProcessTestMessage(clientIndex, (TestMessage*)message);
break;
default:
break;
}
}
void GameServer::ProcessTestMessage(int clientIndex, TestMessage* message) {
// ... process test message ...
}
```
Let's leave the server aside for a moment and take a look at the client. You need a `Client` and the same `ClientServerConfig` and `Adapter` as the server. In this example, the game is divided into "screens" and the screen handling the online game is called `OnlineGameScreen`. It looks like this:
```
OnlineGameScreen::OnlineGameScreen(const yojimbo::Address& serverAddress) :
m_client(yojimbo::GetDefaultAllocator(), yojimbo::Address("0.0.0.0"), m_connectionConfig, m_adapter, 0.0)
{
uint64_t clientId;
yojimbo::random_bytes((uint8_t*)&clientId, 8);
m_client.InsecureConnect(DEFAULT_PRIVATE_KEY, clientId, m_serverAddress);
}
```
Yojimbo requires each client to have a unique `clientId`. In a game with user accounts, this would typically be the user id. While in development or if you don't have user accounts in your game, you can just pass a random `uint64_t` number. Note that with a secure connection, the `clientId` wouldn't be set by the client, but would come inside a connection token received from the web backend, so clients cannot spoof their identity. But this is out of the scope of this guide.
The client is now connecting. You can check the state of the connection in your game loop to know when the connection is established. Note that later on, you will also want to detect disconnections and try to reconnect the client. Just like on the server, you also need to update the `Client` by calling the `AdvanceTime`, `ReceivePackets` and `SendPackets` methods.
For testing purposes, let's also send a `TestMessage` when the player presses a key:
```
void OnlineGameScreen::Update(float dt) {
// update client
m_client.AdvanceTime(m_client.GetTime() + dt);
m_client.ReceivePackets();
if (m_client.IsConnected()) {
ProcessMessages();
// ... do connected stuff ...
// send a message when space is pressed
if (KeyIsDown(Key::SPACE)) {
TestMessage* message = (TestMessage*)m_client.CreateMessage((int)GameMessageType::TEST);
message->m_data = 42;
m_client.SendMessage((int)GameChannel::RELIABLE, message);
}
}
m_client.SendPackets();
}
void OnlineGameScreen::ProcessMessages() {
for (int i = 0; i < m_connectionConfig.numChannels; i++) {
yojimbo::Message* message = m_client.ReceiveMessage(i);
while (message != NULL) {
ProcessMessage(message);
m_client.ReleaseMessage(message);
message = m_client.ReceiveMessage(i);
}
}
}
void OnlineGameScreen::ProcessMessage(yojimbo::Message* message) {
switch (message->GetType()) {
case (int)GameMessageType::TEST:
ProcessTestMessage((TestMessage*)message);
break;
default:
break;
}
}
void OnlineGameScreen::ProcessTestMessage(TestMessage* message) {
std::cout << "Test message received from server with data " << message->m_data << std::endl;
}
```
The client now sends a `TestMessage` when a key is pressed and will also log to the console when receiving one. Let's change the server so that when it receives a `TestMessage` it will log to the console and answer the same message back to the client:
```
void GameServer::ProcessTestMessage(int clientIndex, TestMessage* message) {
std::cout << "Test message received from client " << clientIndex << " with data " << message->m_data << std::endl;
TestMessage* testMessage = (TestMessage*)m_server.CreateMessage(clientIndex, (int)GameMessageType::TEST);
testMessage->m_data = message->m_data;
m_server.SendMessage(clientIndex, (int)GameChannel::RELIABLE, testMessage);
}
```
Note that we used the `RELIABLE` channel here. We won't get into details but you will want to use the reliable channel for important and unfrequent messages such as initialization or chat messages and the unreliable channel for messages sent every frame like the game state.
You should now be able to run the server, run the client, have it connect to the server and send and receive test messages. Hopefully this guide gave you a better idea on how to get started using Yojimbo!

0 comments on commit bf65962

Please sign in to comment.