Controllers in Multiplayer
From C4 Engine Wiki
Contents |
Setup
Disable Mutex
In C4Main.cpp, comment out the two references to the 'mutex' variable. This will allow you to open multiple copies of C4 at the same time, which makes testing multiplayer easier. This is not a necessary step if you will only be testing between two different machines.
Debugging Output
To help determine what's happening behind the scene, add the following to your Application class.
void HandlePlayerEvent(PlayerEvent event, Player *player, const void *param); void HandleConnectionEvent(ConnectionEvent event, const NetworkAddress& address, const void *param);
void Game::HandlePlayerEvent(PlayerEvent event, Player *player, const void *param){ switch(event){ case kPlayerConnected: TheEngine->Report("A new player connected to the game"); break; case kPlayerDisconnected: TheEngine->Report("A player disconnected from the game"); break; case kPlayerTimedOut: TheEngine->Report("A player timed out and was disconnected from the game"); break; case kPlayerInitialized: TheEngine->Report("A new player has been initialized and is ready to receive game state"); break; case kPlayerChatReceived: TheEngine->Report("A player has sent a chat message"); break; case kPlayerRenamed: TheEngine->Report("A player has changed his name"); break; } } void Game::HandleConnectionEvent(ConnectionEvent event, const NetworkAddress& address, const void *param){ const long * failReason = static_cast<const long*>(param); switch(event){ case kConnectionQueryReceived: TheEngine->Report("A request for server information has been received"); break; case kConnectionAttemptFailed: TheEngine->Report("An attempt to connect to a server has failed. "); switch(*failReason){ case kNetworkFailTimeout: TheEngine->Report("The remote machine did not respond and the connection attempt timed out. "); break; case kNetworkFailWrongProtocol: TheEngine->Report("The remote machine is not using the same communications protocol that the local machine is using. "); break; case kNetworkFailNotServer: TheEngine->Report("The remote machine is using the correct protocol, but it is not a server. "); break; case kNetworkFailServerFull: TheEngine->Report("The remote machine did not accept the connection because the maximum number of clients have already connected. "); break; } break; case kConnectionClientOpened: TheEngine->Report("A client machine has opened a connection to the server. "); break; case kConnectionServerAccepted: TheEngine->Report("The server has accepted a connection request. "); break; case kConnectionServerClosed: TheEngine->Report("The server has closed the connection. "); break; case kConnectionClientClosed: TheEngine->Report("A client has closed the connection. "); break; case kConnectionServerTimedOut: TheEngine->Report("The server connection has timed out. "); break; case kConnectionClientTimedOut: TheEngine->Report("A client connection has timed out. "); break; } }
These two methods will alert you to basic player events like joining, which lets you at least confirm that your basic network connections are working.
Server Setup
Using a console command, an interface (panel/widget) or by some other method, set up one method to be called to start hosting as a server, and one method to join as a client. In the code for hosting, set up the server with something like this.
TheNetworkMgr->SetProtocol(1); TheNetworkMgr->SetBroadcastPortNumber(6667); TheNetworkMgr->SetPortNumber(6666); NetworkResult result = TheMessageMgr->BeginMultiplayerGame(true); switch(result){ case kEngineOkay: TheEngine->Report("The Network Manager was successfully initialized and the connection request was submitted. "); break; case kNetworkInitFailed: TheEngine->Report("The Network Manager could not be initialized because the operating system returned an error. "); break; default: TheEngine->Report("Unknown network result"); } TheWorldMgr->LoadWorld("Worlds/Test_01");
The protocol number is an arbitrary number you can use to make sure that the client and server are compatible versions. This will start put the server into 'host' mode and load a default world.
Client Setup
The client can use the following code to connect to the server as started above:
TheNetworkMgr->SetProtocol(1); TheNetworkMgr->SetPortNumber(0); TheNetworkMgr->SetBroadcastPortNumber(6667); NetworkAddress address = MessageMgr::StringToAddress(hostnameWidget->GetText()); address.SetPort(6666); // Now we're just going to (try to) connect to the entered address. NetworkResult result = TheMessageMgr->Connect(address); TheWorldMgr->LoadWorld("Worlds/Test_01"); switch(result){ case kEngineOkay: TheEngine->Report("The Network Manager was successfully initialized and the connection request was submitted. "); break; case kNetworkInitFailed: TheEngine->Report("The Network Manager could not be initialized because the operating system returned an error. "); break; default: TheEngine->Report("Unknown network result"); }
NOTE!!!! For the client, we call TheNetworkMgr->SetPortNumber(0);, and then set the port to 6666 on the address object. This differs between the client and the server.
=Message Manager
http://www.terathon.com/c4engine/doco/MessageMgr/MessageMgr.html
Client
Clients cannot communicate directly to each other, so when they wish to notify others of something, they must do so through the server.
Server
Networking
Message Structure
ClientMessage
This encapsulates all messages that are sent from a client to a server. This is usually done when the client wants to do something that the server has to OK, like spawning.
TankPositionMessage
This is used to communicate the position and orientation of a tank
Player Spawning
When a client wants to spawn, they send a ClientMessage to the server. The kMessageClientSpawn type is used to indicate the intent of the ClientMessage.
TheMessageMgr->SendMessage(kPlayerServer, ClientMessage(kMessageClientSpawn));
Note that kPlayerServer is used to tell the Message Manager that the message should only be sent to the player who is the server. The server receives this message and calls Application::ConstructMessage(). This method is overridden in the Game class, and a switch statement is used to check the type to determine what to do with the message. This method receives the message type and the decompressed data, so the switch statement unpacks the data based on the message type.
In the case of the client spawn message, the ConstructMessage does the following:
return (new ClientMessage(type));
The type variable in this case has a value of kMessageClientSpawn. The engine then calls HandleMessage on the message object (in this case a ClientMessage) returned from ConstructMessage. Since ClientMessage is meant to receive various types of requests from clients, we have to check the actual message type to figure out what to do.
bool ClientMessage::HandleMessage(Player *sender) const
{
switch (GetMessageType())
{
case kMessageClientSpawn:
//Not shown:make sure the player isn't already spawned
TheGame->SpawnPlayer(player);
. . .
The call to SpawnPlayer is responsible for informing all the clients
void Game::SpawnPlayer(Player *player)
{
GameWorld *world = static_cast<GameWorld *>(TheWorldMgr->GetWorld());
if (world)
{
long index = TheWorldMgr->GetWorld()->NewControllerIndex();
TheMessageMgr->SendMessageAll(
CreateTankMessage(index,Point3D goes here,player->GetPlayerKey()));
This tells all clients what the next controller index is, where to put the tank, and which player controls it. Since the call is SendMessageAll, the server recieves the message as well, but the actual handling of the message is slightly different between client and server.
On the client, it receives a new message and so calls Application::ConstructMessage(). The CreateTankMessage identifies itself as type kMessageCreateModel to the base Message class, and so this is the type that's received by ConstructMessage. So, in the ConstructMessage body we have the follwing:
case kMessageCreateModel:
{
unsigned char modelType;
data >> modelType;
return (CreateModelMessage::ConstructMessage(modelType));
}
Remember that ConstructMessage accepts decompressed data. Our CreateModelMessage has a modelType member that is used to indicate which model we want to create. When the message is received, this data is passed into the data paramtere. So, now that we know what type of model to create, ConstructMessage returns a CreateModelMessage with the specified model type, and the client has a complete CreateModelMessage including the model type to create.
This process occurs differently on the server. Since it already has the complete message (it was created in the call to SendMessageAll, it simply skips to the next part, which is the same as on the client. It's worth noting that the the local computer sending the message will never call Decompress since it already has the message and won't be creating one. However, other receiving the message will call Decompress.
The engine now calls HandleMessage() on the new message, which in this case is CreateModelMessage::HandleMessage(). The actual functionality is rather plain, as the heavy lifting is moved elsewhere.
bool CreateModelMessage::HandleMessage(Player *sender) const
{
TheGame->CreateModel(this);
return (true);
}
So, CreateModel is called and passed the message itself. It examines the model type, adds the appropriate model and controller. kModelMessageTank01 is the model type I was speaking about earlier.
void Game::CreateModel(const CreateModelMessage *message)
{
C4::Engine::Report("CreateModel()");
GameWorld *world = static_cast<GameWorld *>(TheWorldMgr->GetWorld());
if (world)
{
MessageType type = message->GetModelMessageType();
switch (type)
{
case kModelMessageTank01:
{
const CreateTankMessage *m =
static_cast<const CreateTankMessage *>(message);
TankController *controller = new TankController();
Model *model = Model::Get(kModelTank);
model->SetNodeFlags(kNodeNonpersistent);
GamePlayer *player =
static_cast<GamePlayer *>(
TheMessageMgr->GetPlayer(m->GetPlayerKey())
);
if (player){
player->SetPlayerController(controller);
}
InitializeModel(message, world, model , controller);
break;
}
State When Joining
SendInitialStateMessages is responsible for bringing a newly joined client up to date with what all the controllers are doing. “The SendInitialStateMessages function is called for every controller in a world when a new client machine joins a multiplayer game.†For example, when a player joins, they have to be made aware of all the existing tanks. This involves first creating a tank with the CreateTankMessage, and then setting its position with TankPositionMessage.
void TankController::SendInitialStateMessages(Player *player) const{
//We need to know who this controller belongs to
//so we can tell the new message
const GamePlayer *tankPlayer = GetTankPlayer();
//Create the tank
player->SendMessage(CreateTankMessage(GetControllerIndex(), GetTargetNode()->GetWorldPosition(), tankPlayer->GetPlayerKey()));
//Now update its position
player->SendMessage(
TankPositionMessage(
this->GetControllerIndex(),
this->chassis->GetNodeTransform(),
this->cannonAzimuth,
this->cannonAltitude
)
);
}
Here, the player parameter is the newly joined player, so all the messages are sent to this player. Note that for the CreateTankMessage, controlling player is specified by GetTankPlayer.
Updating Clients
When the tank moves around, there are two seperate types of movement. One is a client can do for itself and inform the server (like rotating the tank turret), and another is one that it must ask the server to do (like move or fire). We'll start with something simple like changing the orientation.
When the orientation changes, the client tells the server its new orientation. The server is responsible for keeping track of what is current, and distributing that to clients. The dispurses information to clients using the SendSnapshot method int he controller class. Only client machines will call TankController::ReceiveMessage

