// Client / Server test of the ReplicaManager class. See ReplicaManagerP2P for the peer to peer test. // The ReplicaManager is a very lightweight and efficient system that provides object creation, destruction, and serialization updates such that // You have all copies of all desired objects on all desired systems semi-automatically #include "StringTable.h" // Monster and player are the same, but monster implements the Replica virtual interfaces with a member object, while player derives from Replica #include "Monster.h" #include "Player.h" #include "RakPeerInterface.h" #include "RakNetworkFactory.h" #include #include #include #include "Rand.h" // RandomMT #include "ReplicaMember.h" #include "BitStream.h" #include "PacketEnumerations.h" #include "ReplicaEnums.h" #include "Replica.h" #include "ReplicaManager.h" void ShowStatus(Monster *monster, Player *player); bool isServer; RakPeerInterface *rakPeer; // ReplicaManager manages the object replication system ReplicaManager replicaManager; // My two game instances Monster *monster; Player *player; #ifdef _COMPATIBILITY_1 #include "Compatibility1Includes.h" // Developers of a certain platform will know what to do here. #elif defined(_WIN32) #include // Sleep #else #include // usleep #include #endif // This is a required callback I registered to get called when a remote participant calls ReplicaManager::Register on a new object. // It's equivalent to creating our local copy of an object when a new object is created on a remote system ReplicaReturnResult ConstructionCB(RakNet::BitStream *inBitStream, RakNetTime timestamp, NetworkID networkID, PlayerID senderId, ReplicaManager *caller) { char output[255]; // Security - The server needs to pay attention to what objects the client asks to create and reject those that are invalid. // In this case the client isn't allowed to create anything if (isServer) return REPLICA_PROCESSING_DONE; // I encoded all the data in inBitStream in Monster::SendConstruction and Player::SendConstruction // I am using the string table to send strings, which is a way to send pre-known strings in a single byte. // I registered "Player" and "Monster" in main(). // The stringTable system has the limitation that all systems must register all the same strings in the same order. // I could have also used stringCompressor, which would always work but is less efficient to use when we have known strings stringTable->DecodeString(output, 255, inBitStream); if (strcmp(output, "Player")==0) { // The system automatically prevents duplicate creations assert(player==0); player = new Player; // Calling SetNetworkID is important and necessary, because all calls other than construction rely on being able to lookup an object by its network ID // If we didn't do this, we would never get scope, serialize, or destruction calls because the ReplicaManager wouldn't be able to find this object // networkID was generated from ReplicaManager by calling GetNetworkID on the remote object and automatically including that value in this packet player->SetNetworkID(networkID); if (isServer==false) { // In order to get Replica::Receive packets the pointer must be registered. Otherwise these calls are ignored as a security measure // Note: If you wanted to actually perform sends on this object to remote systems, call Construct with the isCopy parameter as true instead. // replicaManager.ReferencePointer(player); // If we want to update an object created remotely, we can call Construct with the last parameter isCopy as true. replicaManager.Construct(player, true, senderId, false); // Since SendConstruction is not called for copies and we were calling SetScope there, we need to call it here instead. replicaManager.SetScope(player, true, senderId, false); } printf("New player created\n"); } else if (strcmp(output, "Monster")==0) { // The system automatically prevents duplicate creations assert(monster==0); monster = new Monster; // Calling SetNetworkID is important and necessary, because all calls other than construction rely on being able to lookup an object by its network ID // If we didn't do this, we would never get scope, serialize, or destruction calls because the ReplicaManager wouldn't be able to find this object // networkID was generated from ReplicaManager by calling GetNetworkID on the remote object and automatically including that value in this packet monster->replica->SetNetworkID(networkID); if (isServer==false) { // In order to get Replica::Receive packets the pointer must be registered. Otherwise these calls are ignored as a security measure // Note: If you wanted to actually perform sends on this object to remote systems, call Construct with the isCopy parameter as true instead. //replicaManager.ReferencePointer(monster->replica); // If we want to update an object created remotely, we can call Construct with the last parameter isCopy as true. replicaManager.Construct(monster->replica, true, senderId, false); // Since SendConstruction is not called for copies and we were calling SetScope there, we need to call it here instead. replicaManager.SetScope(monster->replica, true, senderId, false); } printf("New monster created\n"); } else { // Unknown string assert(0); } return REPLICA_PROCESSING_DONE; } // This is an optional callback I registered to get called when all objects are sent to a new system ReplicaReturnResult SendDownloadCompleteCB(RakNet::BitStream *outBitStream, RakNetTime currentTime, PlayerID senderId, ReplicaManager *caller) { // I could write data to outBitStream if I wanted, and it would arrive in inBitStream in ReceiveDownloadCompleteCB return REPLICA_PROCESSING_DONE; } // This is an optional callback I registered to get called when I got all objects sent to me from a new system. // This is called even if no objects were sent, so even though I only have the server sending objects here, // the server will get it from the client and the client from the server. ReplicaReturnResult ReceiveDownloadCompleteCB(RakNet::BitStream *inBitStream, PlayerID senderId, ReplicaManager *caller) { if (isServer==false) // Server doesn't care about this message, although we get it anyway printf("Object downloads complete\n"); return REPLICA_PROCESSING_DONE; } int main(void) { char ch; char userInput[256]; rakPeer = RakNetworkFactory::GetRakPeerInterface(); // You have to attach ReplicaManager for it to work, as it is one of the RakNet plugins rakPeer->AttachPlugin(&replicaManager); // Anytime we get a new connection, call AddParticipant() on that connection replicaManager.SetAutoParticipateNewConnections(true); // Anytime we get a new participant, automatically call Construct() for them with all known objects replicaManager.SetAutoConstructToNewParticipants(true); // Note there is also replicaManager.SetDefaultScope(true); which I'm not using just for illustration // It eliminates the need to call replicaManager.SetScope(this, true, playerId); in Replica::SendConstruction. // Set the function that will get messages from Replica::SendConstruction replicaManager.SetReceiveConstructionCB(ConstructionCB); // Set the optional callbacks to send and receive download complete notifications replicaManager.SetDownloadCompleteCB(SendDownloadCompleteCB, ReceiveDownloadCompleteCB); // Here I use the string table class to efficiently send strings I know in advance. // The encoding is done in the monster and player SendConstruction functions // The decoding is done in the ConstructionCB callback // The stringTable class will also send strings that weren't registered but this just falls back // to the stringCompressor and wastes 1 extra bit on top of that stringTable->AddString("Player", false); // 2nd parameter of false means a static string so it's not necessary to copy it stringTable->AddString("Monster", false); // 2nd parameter of false means a static string so it's not necessary to copy it // Just start the client, or the server printf("Demonstration of ReplicaManager for client / server\n"); printf("The replica manager provides a framework to make it easier to synchronize\n"); printf("object creation, destruction, and member object updates\n"); printf("Difficulty: Intermediate\n\n"); printf("Run as (s)erver or (c)lient? "); gets(userInput); if (userInput[0]=='s' || userInput[0]=='S') { isServer=true; rakPeer->Initialize(8,60000,0,0); rakPeer->SetMaximumIncomingConnections(8); printf("Server started.\n"); } else { isServer=false; rakPeer->Initialize(1,0,0,0); printf("Enter IP to connect to: "); gets(userInput); if (userInput[0]==0) { strcpy(userInput, "127.0.0.1"); printf("%s\n", userInput); } if (!rakPeer->Connect(userInput, 60000, 0, 0)) { printf("Connect call failed!\n"); return 1; } printf("Connecting...\n"); } printf("Commands:\n(Q)uit\n(Space) Show status\n(R)andomize health and position\n"); if (isServer) { printf("Toggle (M)onster\nToggle (p)layer\n"); printf("Toggle (S)cope of player\n"); } Packet *p; // Main loop, go until user signals to quit while (1) { p = rakPeer->Receive(); while (p) { if (p->data[0]==ID_DISCONNECTION_NOTIFICATION || p->data[0]==ID_CONNECTION_LOST) { if (isServer==false) { printf("Server connection lost. Deleting objects\n"); // Lets have the clients delete all their objects when they lose connection to the server if (monster) { delete monster; // Destructor of monster calls Dereplicate } if (player) { delete player; // Destructor of player calls Dereplicate } } } rakPeer->DeallocatePacket(p); p = rakPeer->Receive(); } if (kbhit()) { ch=getch(); if (ch=='q' || ch=='Q') { printf("Quitting.\n"); break; } else if (ch==' ') ShowStatus(monster, player); else if (ch=='r' || ch=='R') { // Randomize health and position of the player and monster if (player) { player->health=randomMT(); player->position=randomMT(); // This system requires that you tell the manager when object memory has changed, so it will queue up a serialize call replicaManager.SignalSerializeNeeded(player, UNASSIGNED_PLAYER_ID, true); // UNASSIGNED_PLAYER_ID, true means everybody } if (monster) { monster->health=randomMT(); monster->position=randomMT(); // This system requires that you tell the manager when object memory has changed, so it will queue up a serialize call replicaManager.SignalSerializeNeeded(monster->replica, UNASSIGNED_PLAYER_ID, true); // UNASSIGNED_PLAYER_ID, true means everybody } printf("Randomized player and monster health and position\n"); ShowStatus(monster, player); } else if (isServer) { if (ch=='m' || ch=='M') { if (monster==0) { printf("Creating monster\n"); monster = new Monster; } else { delete monster; printf("Deleted monster\n"); monster=0; } } else if (ch=='p' || ch=='P') { if (player==0) { printf("Creating player\n"); player = new Player; } else { delete player; printf("Deleted player\n"); player=0; } } else if (ch=='s' || ch=='S') { if (player) { bool currentScope; currentScope=replicaManager.IsInScope(player, rakPeer->GetPlayerIDFromIndex(0)); if (currentScope==false) printf("Setting scope for player to true for all remote systems.\n"); else printf("Setting scope for player to false for all remote systems.\n"); replicaManager.SetScope(player, !currentScope, UNASSIGNED_PLAYER_ID, true); } else { printf("No player to set scope for\n"); } } } } #ifdef _WIN32 Sleep(30); #else usleep(30 * 1000); #endif } delete monster; delete player; RakNetworkFactory::DestroyRakPeerInterface(rakPeer); return 1; } void ShowStatus(Monster *monster, Player *player) { printf("\nSTATUS:\n"); if (monster) printf("Monster is at position %i with health %i\n", monster->position, monster->health); else printf("There is no monster\n"); if (player) printf("Player is at position %i with health %i\n", player->position, player->health); else printf("There is no player\n"); if (isServer) { bool monsterInScope, playerInScope; if (monster) { // Note that when using a member object, I pass that member object to the ReplicaManager functions monsterInScope=replicaManager.IsInScope(monster->replica, rakPeer->GetPlayerIDFromIndex(0)); // If an object is in scope we will send memory updates for that object to that player. if (monsterInScope) printf("Monster in scope to system 0.\n"); else printf("Monster NOT in scope to system 0.\nChanges to monster variables will not be sent to that system.\n"); } if (player) { // For regular inheritance I don't have to do that. playerInScope=replicaManager.IsInScope(player, rakPeer->GetPlayerIDFromIndex(0)); if (playerInScope) printf("Player in scope to system 0.\n"); else printf("Player NOT in scope to system 0.\nChanges to player variables will not be sent to that system.\n"); } } printf("\n"); }