// 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 <stdio.h>
#include <conio.h>
#include <string.h>
#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 <windows.h> // Sleep
#else
#include <unistd.h> // usleep
#include <cstdio>
#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");
}
syntax highlighted by Code2HTML, v. 0.9.1