Any game that has objects that are created and destroyed while the game is in progress, almost all non-trivial games, faces a minimum of 3 problems:
- How to broadcast existing game objects to new players
- How to broadcast new game objects to existing players
- How to broadcast deleted game objects to existing players
Additional potential problems, depending on complexity and optimization
- How to create and destroy objects dynamically as the player moves around the world
- How to allow the client to create objects locally when this is necessary right away for programming or graphical reasons (such as shooting a bullet).
- How to update objects as they change over time
The solution to most of these problems is usually straightforward, yet still requires a significant amount of work and debugging, with several dozen lines of code per object.
ReplicaManager3 is designed to be a generic, overridable plugin that handles as many of these details as possible automatically. ReplicaManager3 automatically creates and destroys objects, downloads the world to new players, manages players, and automatically serializes as needed. It also includes the advanced ability to automatically relay messages, and to automatically serialize your objects when the serialized member data changes.
Methods of object serialization:
Manual sends on dirty flags
Description: When a variable changes, it's up to you to set some flag that the variable has changed. The next Serialize() tick, you send all variables with dirty flags set
Pros: fast, memory efficient
Cons: All replicated variables must change through accessors so the flags can be set. So it's labor intensive on the programmer as you have to program it to set the dirty flags, and bugs will likely be made during the process
Example
void SetHealth(float newHealth) {if (health==newHealth) return; health=newHealth; serializeHealth=true;}
void SetScore(float newScore) {if (score==newScore) return; score=newScore; serializeScore=true;}
virtual RM3SerializationResult Serialize(SerializeParameters *serializeParameters)
{
bool anyVariablesNeedToBeSent=false;
if (serializeHealth==true)
{
serializeParameters->outputBitstream[0]->Write(true);
serializeParameters->outputBitstream[0]->Write(health);
anyVariablesNeedToBeSent=true;
}
else
{
serializeParameters->outputBitstream[0]->Write(false);
}
if (serializeScore==true)
{
serializeParameters->outputBitstream[0]->Write(true);
serializeParameters->outputBitstream[0]->Write(score);
anyVariablesNeedToBeSent=true;
}
else
{
serializeParameters->outputBitstream[0]->Write(false);
}
if
(anyVariablesNeedToBeSent==false)
serializeParameters->outputBitstream[0]->Reset();
// Won't send anything if the bitStream is empty (was Reset()).
RM3SR_SERIALIZED_ALWAYS skips default memory compare
return
RM3SR_SERIALIZED_ALWAYS;
}
virtual void Deserialize(RakNet::DeserializeParameters *deserializeParameters)
{
bool healthWasChanged, scoreWasChanged;
deserializeParameters->serializationBitstream[0]->Read(healthWasChanged);
if (healthWasChanged)
deserializeParameters->serializationBitstream[0]->Read(health);
deserializeParameters->serializationBitstream[0]->Read(scoreWasChanged);
if (scoreWasChanged)
deserializeParameters->serializationBitstream[0]->Read(score);
}
Serializing based on the object changing
Description: This is what ReplicaManager3 comes with. If the object's state for a bitStream channel change at all, the entire channel is resent
Pros: Easy to use for the programmer
Cons: Some variables will be sent unneeded, using more bandwidth than necessary. Moderate CPU and memory usage
Example
void SetHealth(float newHealth) {health=newHealth;}
virtual RM3SerializationResult Serialize(SerializeParameters *serializeParameters)
{
serializeParameters->outputBitstream[0]->Write(health);
serializeParameters->outputBitstream[0]->Write(score);
// Memory compares against last outputBitstream write. If changed, writes everything on the changed channel(s), which can be wasteful in this case if only health or score changed, and not both
ieturn
RM3SR_BROADCAST_IDENTICALLY;
}
virtual void Deserialize(RakNet::DeserializeParameters *deserializeParameters)
{
deserializeParameters->serializationBitstream[0]->Read(health);
deserializeParameters- >serializationBitstream[0]->Read(score);
}
Serializing per-variable
Description: The optional module I was talking about. Every variable is copied internally and compared to the last state
Pros: Maximum bandwidth savings
Cons: Heavy CPU and memory usage
Example (also see ReplicaManager3 sample project)
virtual RM3SerializationResult Serialize(SerializeParameters *serializeParameters) {
VariableDeltaSerializer::SerializationContext serializationContext;
// All variables to be sent using a different mode go on different channels
serializeParameters->pro[0].reliability=RELIABLE_ORDERED;
variableDeltaSerializer.BeginIdenticalSerialize(
&serializationContext,
serializeParameters->whenLastSerialized==0,
&serializeParameters->outputBitstream[0]
);
variableDeltaSerializer.SerializeVariable(&serializationContext, var3Reliable);
variableDeltaSerializer.SerializeVariable(&serializationContext, var4Reliable);
variableDeltaSerializer.EndSerialize(&serializationContext);
return RM3SR_SERIALIZED_ALWAYS_IDENTICALLY;
}
virtual void Deserialize(RakNet::DeserializeParameters *deserializeParameters) {
VariableDeltaSerializer::DeserializationContext deserializationContext;
variableDeltaSerializer.BeginDeserialize(&deserializationContext, &deserializeParameters->serializationBitstream[0]);
if (variableDeltaSerializer.DeserializeVariable(&deserializationContext, var3Reliable))
printf("var3Reliable changed to %i\n", var3Reliable);
if (variableDeltaSerializer.DeserializeVariable(&deserializationContext, var4Reliable))
printf("var4Reliable changed to %i\n", var4Reliable);
variableDeltaSerializer.EndDeserialize(&deserializationContext);
}
Quick start:
- Derive from Connection_RM3 and implement Connection_RM3::AllocReplica(). This is a factory function where given an identifier for a class (such as name) return an instance of that class. Should be able to return any networked object in your game.
- Derive from ReplicaManager3 and implement AllocConnection() and DeallocConnection() to return the class you created in step 1.
- Derive your networked game objects from Replica3. All pure virtuals have to be implemented, however defaults are provided for Replica3::QueryConstruction() and Replica3::QueryRemoteConstruction() depending on your network architecture.
- When a new game object is created on the local system, pass it to ReplicaManager3::Reference().
- When a game object is destroyed on the local system, and you want other systems to know about it, call Replica3::BroadcastDestruction()
- Attach ReplicaManager3 as a plugin
For a full list of functions with detailed documentation on each parameter, see ReplicaManager3.h.
The primary sample is located at Samples\ReplicaManager3.
|