SimpleChar Multiplayer
From C4 Engine Wiki
Contents |
Introduction
The most common request I've seen in the C4 forums in regards to network programming is for a networked version of SimpleChar. Since I myself was working on a multiplayer game and would have greatly benefited from a multiplayer SimpleChar, I decided to go ahead and create just that. This tutorial will show you the basics of getting a multiplayer game running, with each player controlling his own soldier. This tutorial is heavily based on both SimpleChar and the Networked Custom Controllers sample in the wiki, so thank you to the respective writers of those for showing me how this networking stuff works in the first place.
Prerequisites
This tutorial assumes that you have worked with the C4 engine and are familiar with loading/unloading worlds, controllers, interface and have gone through either the Basic Network Tutorial or the Networked Custom Controllers wiki posts to understand the basics of networking. I will not be stepping through all of the code in this tutorial, only the parts related to networking.
Basics
The core components of a multiplayer game are the server and the clients. The server is where the games are started and stopped and where all game logic happens. When something happens on the server such as a player moving or a game starting, it then sends that message down to all clients so everybody is synced up with one another. It is possible to create a dedicated server that does nothing but handle all the game logic, and does not allow a player to actually play on that instance. In this example however, the host (server) will be controlling a player just like all of the clients.
Hosting a game
In our MainWindow class in Interface.h, there is a button entitled “Host Game”. Once clicked, it will call the function "HostGame" in the Game class. Let's take a look:
TheMessageMgr->BeginMultiplayerGame(true); TheEngine->Report(String<>("Initialized. Hosting on: ") + MessageMgr::AddressToString(TheNetworkMgr->GetLocalAddress(), true)); TheGame->LoadWorld("world/Inout"); TheMessageMgr->SendMessageAll(RequestMessage());
We begin by calling BeginMultiplayerGame(true), which lets the engine know that we are starting a game, and that by passing "true", that we are the server. The level is then loaded (you should already be familiar with this), and finally, we send the RequestMessage to all (this includes all clients and the server). It is very important to note that any message which inherits from the Message class (basically all non-controller messages) MUST be constructed in the Game::ConstructMessage function seen below:
Message *Game::ConstructMessage(MessageType type, Decompressor &data) const { switch(type) { case kMessageSpawn: return (new SpawnMessage); case kMessageRequest: return (new RequestMessage); case kMessageMovementBegin: case kMessageMovementEnd: return (new ClientMovementMessage(type)); case kMessageOrientation: return (new ClientOrientationMessage); } return nullptr; }
This function basically just lets the engine know how to construct all non-controller messages based on message type. If you forget to put a message constructor in this function, the message will work on the server, but not the clients. I know this from personal experience - do not forgot this part!
As you can see from examining the RequestMessage class, there is not much to it at all. Since there is no data actually being sent, it is very small.
RequestMessage::RequestMessage() : Message(C4::kMessageRequest) { } RequestMessage::~RequestMessage() { } void RequestMessage::Compress(Compressor &data) const { } bool RequestMessage::Decompress(Decompressor &data) { return true; }
You should note that we did not declare a function called HandleMessage for the RequestMessage class. When a non-controller message is received by a server or client, it can be handled in two different places. If you define the HandleMessage function of the message, then it will be called when the message is received, and you can do whatever you like on that machine at that time. The other place the message can be handled is in the Game::ReceiveMessage function. Again, this function will be called anytime a non-controller message is received by a client or server, so you can do whatever you like on that machine at this point.
For the RequestMessage message, we are handling the receipt of the message in the Game::ReceiveMessage function. Let’s take a look at this function.
void Game::ReceiveMessage(Player *sender, const NetworkAddress &address, const Message *message) { switch(message->GetMessageType()) { case C4::kMessageRequest: { if(TheMessageMgr->Server()) { Point3D loc; loc.x = -10.0F + Math::Random(20.0F); loc.y = -10.0F + Math::Random(20.0F); loc.z = 1.0F; long contIndex = TheWorldMgr->GetWorld()->NewControllerIndex(); TheMessageMgr->SendMessageAll(SpawnMessage(sender->GetPlayerKey(), contIndex, loc)); } break; } } }
The first thing you should notice is that we do a switch on the message type. As stated earlier, all non-controller messages will activate this function upon receipt, and can be handled here. Once we know we have received the message with type kMessageRequest, we then call MessageMgr::Server to ensure that the next bit of code is only run on the server, not on the client machines. Once we know that we are running on the server, a random location is picked as a spawn point, and the SpawnMessage is sent to the player that originally sent the RequestMessage.
One very important thing to take note of is the World::NewControllerIndex function. Anytime a new controller is created and added to the world (which it will be when the SpawnController message is received) you MUST get the controller index from this function running on the server. The point of this is to make sure that all controllers on all clients have the same index for all controllers. When a controller message is created and sent to the clients to handle, the controller on the client’s machine is looked up by it’s index. If the controller indexes get mixed up between clients, then the wrong controllers will be receiving messages that they can’t handle. Once the SpawnMessage message is received, it is handled via the SpawnMessage::HandleMessage function, which calls the Game::SpawnSoldier function. Let’s take a gander at that function:
void Game::SpawnSoldier(Player *player, Point3D location, long controllerIndex) { World *world = TheWorldMgr->GetWorld(); if(world) { GamePlayer *gPlayer = static_cast<GamePlayer *>(player); SoldierController *cont = gPlayer->GetController(); if(!cont) { cont = new SoldierController(); cont->SetControllerIndex(controllerIndex); gPlayer->SetController(cont); Model *soldier = Model::Get(C4::kModelSoldier); soldier->SetController(cont); Zone *zone = world->FindZone(location); soldier->SetNodePosition(zone->GetInverseWorldTransform() * location); zone->AddNewSubnode(soldier); } } }
This piece of code should be very familiar if you have used C4, it creates a model and controller, and adds the model to the world. The important part to take note of is the SetControllerIndex function being called. It uses the controllerIndex passed from the server and manually sets the index here on the client machine.
The next message that will be received is in the Game::HandlePlayerEvent function with an event type of kPlayerInitialized.
case C4::kPlayerInitialized: { TheEngine->Report("Player initialized."); GamePlayer *gp = nullptr; SoldierController *cont = nullptr; Node *node = nullptr; PlayerKey key = -1; long id = -1; Point3D loc; // This player just joined, so send him 1 message per // player to spawn a character Player *p = TheMessageMgr->GetFirstPlayer(); while(p) { gp = static_cast<GamePlayer *>(p); cont = gp->GetController(); if(cont) { node = cont->GetTargetNode(); key = gp->GetPlayerKey(); id = cont->GetControllerIndex(); loc = node->GetWorldPosition(); TheMessageMgr->SendMessage(player->GetPlayerKey(), SpawnMessage(key, id, loc)); } p = p->Next(); } break; }
This particular section of code will be ignored when the host is starting a game, but when clients join the game, this section of code will loop through all players in the game and send a SpawnMessage message to the player that just connected to create a soldier controller on the local machine for all players.
That’s it, now you’re hosting a game and you’re the server and only player.
Joining a game
Once a host has started a game, another player can then join. On the host machine, you can press the Esc button to bring up the main window, which will have the host machine’s IP address as the title of the window. On the client machine, put that IP address into the edit text box labeled IP and press the Join button. The join button will call the Game::JoinGame function, seen below:
void Game::JoinGame(String<> ipAddress) { TheMessageMgr->BeginMultiplayerGame(false); NetworkAddress addr = MessageMgr::StringToAddress(ipAddress); addr.SetPort(C4::kGamePort); NetworkResult result = TheMessageMgr->Connect(addr); }
In this function, the MessageMgr::BeginMultiplayerGame function is called to start the game, passing false to let the game know that we are not the server. The NetworkAddress object is creating using the IP that was passed in from the UI and the port that is hardcoded, then passed to the MessageMgr connect function to actually make the connection to the host.
From there, the code will then wait for the server to tell it that a connection has been made. Once the connection message is received in the client’s Game::HandleConnectionEvent function, we do a switch on the message type to determine what has occurred. If everything went as planned, the kConnectionServerAccepted event is received by the client that just connected, and we load the level and send the RequestMessage message. You can look back at the Hosting a game section to follow the flow of the RequestMessage message.
SoldierController messages
Now that we have some soldiers on the screen, lets start controlling them. If you’re familiar with the SimpleChar sample, you know that a movement flag is set and removed in the Begin and End functions of the MovementAction class. We use a similar method in this example, but instead of directly setting and removing the movement flag on the client machine, the movement functions are sent to the server.
void MovementAction::Begin(void) { const Player *player = TheMessageMgr->GetLocalPlayer(); if (player) { const SoldierController *controller = static_cast<const GamePlayer *>(player)->GetController(); if (controller) { ClientMovementMessage message(kMessageMovementBegin, movementFlag); TheMessageMgr->SendMessage(kPlayerServer, message); return; } } }
Here we see the ClientMovementMessage being sent to the server when the MovementAction is pressed.
bool ClientMovementMessage::HandleMessage(Player *sender) const { SoldierController *controller = static_cast<GamePlayer *>(sender)->GetController(); if (controller) { switch (GetMessageType()) { case kMessageMovementBegin: controller->BeginMovement(movementFlag); break; case kMessageMovementEnd: controller->EndMovement(movementFlag); break; } } return (true); }
When the message is received by the server, it calls the BeginMovement function on the local soldier controller. Take a look below.
void SoldierController::BeginMovement(unsigned long flag) { const Point3D& position = GetTargetNode()->GetWorldPosition(); Vector3D velocity = GetLinearVelocity(); SoldierMovementMessage message(kSoldierMessageBeginMovement, GetControllerIndex(), position, velocity, flag); TheMessageMgr->SendMessageAll(message); }
What this function does is sends a controller message to everyone (clients and server) with the movement flag. As with the non-controller messages from before being constructed in the Game class, all controller messages need to be constructed in that controller’s ConstructMessage function. In the SoldierController::ReceiveMessage function, the message will be received and we can handle it. Skip down to the kSoldierMessageBeginMovement case, and you should see something familiar.
case kSoldierMessageBeginMovement: { const SoldierMovementMessage *m = static_cast<const SoldierMovementMessage *>(message); unsigned long flag = m->GetMovementFlag(); movementFlags |= flag; break; }
Just like the SimpleChar example, the movement flag is set and the soldier movement is the same as the always. This should give you a basic overview of how controller messages work, but I will point out more more message. Each player will keep track of his own soldier’s orientation, but he still needs to tell everyone else what his orientation is. To do this, we send a ClientOrientationMessage every 30 seconds to the server. The server then calls BeginOrientation on the soldier controller, which sends a SoldierOrientationMessage to all players. When the SoldierOrientationMessage message is received by a client, there is a check to make sure the message didn’t come from our own soldier (since orientation of the local soldier is handled locally) and once passed, the orientation is set.
Final Thoughts
That’s it! If you went through this whole tutorial, you should now have a pretty good idea of how to host & join a game, how to send and receive messages and how to create a simple multiplayer game. The full source code is attached to this post, so download it and test it out. Note that it was created with C4 version 2.6. If you have any further question you may send me a PM on the forums (Username: smittix) or take a look at the original thread here: http://www.terathon.com/forums/viewtopic.php?f=7&t=11400&start=0&st=0&sk=t&sd=a
Full Source Code: http://www.applicachia.com/cs2/SimpleCharMultiplayer.zip
Future
While this messaging model works great for LAN games and games with super-low ping, you will notice that any latency at all will make all client movement “feel weird”. Basically they will press the move button and nothing will happen until the server responds and lets them know it’s OK to move. I hope to continue working on this sample and release a more advanced multiplayer character controller example in the future that focuses on latency correction.
