Torque/Networking/Making a Ghostable Object
From TDN
Overview
Making an object Ghostable is a very simple process, but you must be very careful in how you handle a few aspects.
- Inherit from a class that has NetObject in it's ancestry. This can be either NetObject itself, or one of the classes that inherit from it such as Player, Vehicle, or even ShapeBase
- Make sure that you have the appropriate flags set for mNetFlags. Possibilities include Ghostable, which simply indicates that this object will be ghosted, and ScopeAlways, which indicates that the scoping rules should be over-ridden and the object should always be scoped to all clients.
- Pay strict attention to your ::packUpdate() and ::unpackUpdate() method implementations. Roughly 85% of networking errors relate to improper implementation in these two methods. Important things to keep in mind:
Implementation Example
Ok, so how do we pull this off?
Well, here's an example of implementation for an object that accomplishes a very simple task: When the server modifies one of two messages in a networked object, the message change is transmitted to all clients:
// Using a NetObject is very simple; let's go through a simple example implementation:
(in a .h file, such as simpleNetObject.h)
class SimpleNetObject : public NetObject
{
public:
typedef NetObject Parent;
DECLARE_CONOBJECT(SimpleNetObject);
// Above is the standard boilerplate code for a Torque class. You can find out more about this in SimObject.
U32 packUpdate(NetConnection *, U32 mask, BitStream *stream);
void unpackUpdate(NetConnection *, BitStream *stream);
char message1[256];
char message2[256];
void setMessage1(const char *msg);
void setMessage2(const char *msg);
enum States {
Message1Mask = BIT(0),
Message2Mask = BIT(1)
};
};
(in a .cc file, for example simpleNetObject.cc)
// For our example, we're having two "states" that we keep track of, message1 and message2. In a real
// object, we might map our states to health and position, or some other set of fields. You have 32
// bits to work with, so it's possible to be very specific when defining states. In general, you
// should try to use as few states as possible (you never know when you'll need to expand your object's
// functionality!), and in fact, most of your fields will end up changing all at once, so it's not worth
// it to be too fine-grained. (As an example, position and velocity on Player are controlled by the same
// bit, as one rarely changes without the other changing, too.)
// Here is the constructor. Here, you see that we initialize our net flags to show that
// we should always be scoped, and that we're to be taken into consideration for ghosting. We
// also provide some initial values for the message fields.
SimpleNetObject::SimpleNetObject()
{
// in order for an object to be considered by the network system,
// the Ghostable net flag must be set.
// the ScopeAlways flag indicates that the object is always scoped
// on all active connections.
mNetFlags.set(ScopeAlways | Ghostable);
dStrcpy(message1, "Hello World 1!");
dStrcpy(message2, "Hello World 2!");
}
// Here's half of the meat of the networking code, the packUpdate() function. (The other half, unpackUpdate(),
// we'll get to in a second.) The comments in the code pretty much explain everything, however, notice that the
// code follows a pattern of if(writeFlag(mask & StateMask)) { ... write data ... }. The packUpdate()/unpackUpdate()
// functions are responsible for reading and writing the dirty bits to the bitstream by themselves.
U32 SimpleNetObject::packUpdate(NetConnection *, U32 mask, BitStream *stream)
{
// check which states need to be updated, and update them
if(stream->writeFlag(mask & Message1Mask))
{
stream->writeString(message1);
}
if(stream->writeFlag(mask & Message2Mask))
{
stream->writeString(message2);
}
// the return value from packUpdate can set which states still
// need to be updated for this object. In this case, we don't use it
return 0;
}
// The other half of the networking code in any NetObject, unpackUpdate(). In our simple example, all that
// the code does is print the new messages to the console; however, in a more advanced object, you might
// trigger animations, update complex object properties, or even spawn new objects, based on what packet
// data you unpack.
void SimpleNetObject::unpackUpdate(NetConnection *, BitStream *stream)
{
// the unpackUpdate function must be symmetrical to packUpdate
if(stream->readFlag())
{
stream->readString(message1);
Con::printf("Got message1: %s", message1);
}
if(stream->readFlag())
{
stream->readString(message2);
Con::printf("Got message2: %s", message2);
}
}
// Here are the accessors for the two properties. It is good to encapsulate your state
// variables, so that you don't have to remember to make a call to setMaskBits every time you change
// anything; the accessors can do it for you. In a more complex object, you might need to set
// multiple mask bits when you change something; this can be done using the | operator, for instance,
// setMaskBits( Message1Mask | Message2Mask ); if you changed both messages.
void SimpleNetObject::setMessage1(const char *msg)
{
setMaskBits(Message1Mask);
dStrcpy(message1, msg);
}
void SimpleNetObject::setMessage2(const char *msg)
{
setMaskBits(Message2Mask);
dStrcpy(message2, msg);
}
// Next, we use the NetObject implementation macro, IMPLEMENT_CO_NETOBJECT_V1(), to implement our
// NetObject. It is important that we use this, as it makes Torque perform certain initialization tasks
// that allow us to send the object over the network. IMPLEMENT_CONOBJECT() doesn't perform these tasks, see
// the documentation on [[Torque/AbstractClassRep|AbstractClassRep]] for more details.
/// IMPLEMENT_CO_NETOBJECT_V1(SimpleNetObject);
// Finally, let's create some ConsoleMethods so that our server user can simply change
// our messages in the console window
ConsoleMethod(SimpleNetObject, setMessage1, void, 3, 3, "(string msg) Set message 1.")
{
object->setMessage1(argv[2]);
}
ConsoleMethod(SimpleNetObject, setMessage2, void, 3, 3, "(string msg) Set message 2.")
{
object->setMessage2(argv[2]);
}
Ok, this looks like a lot of code, so let's break it down into sections, and explain exactly what is going on. Please keep in mind that an understanding of C++ and Object Oriented Coding theory is assumed.
- First, we declare our SimpleNetObject, and also declare the accessor methods.
- Second, we implement the constructor for this object. Note that this is where we set the mNetFlags (using mNetFlags.set()) to control the behaviour of our object's networking. In this case, it is flagged as both Ghostable, and ScopeAlways, so every client will always get updates to this object regardless of scoping rules. We also initialize starter content for our member variables message1 and message2.
- Third, we implement the packUpdate() and unpackUpdate() methods for our object. It is critical that you be very focused on how these two functions will interact. In our example we are only dealing with simple strings, so we are using the optimized versions of Bitstream::writeString() and Bitstream::readString().
- Fourth, we implement accessor methods for modifying our two member variables. While outside the scope of this article, proper data encapsulation is a critical part of successful OOP development, so (external links would be outstanding here, Dear Reader!) you should have a basic understanding of OOP Development.
- Finally, we provide ConsoleMethods for allowing console users to modify our object. Proper techniques for implementing ConsoleMethods are a good follow-on topic once you have absorbed this example.
[edit]Using our Example
Ok, so we see the code, but what is it doing?
Well, first we have to understand what a BitStream is, and how it works. Without going into painful detail, a BitStream is simply that: a stream of bits that are transmitted across an underling networking protocol (in our case, Enhanced UDP). It's not critical that you understand the underlying implementation and operation of the transport protocol, but what is important to understand:- The Torque server keeps track of all objects that are ghosted to each client.
- Clients only know about objects that are ghosted to them, and each client has a separate and unique ID for every object ghosted to them.
- The Client and Server can communicate about specific objects due to a mapping the server stores for each client that links a Client side ObjectID with the server side authoritative SimID (just note that...don't worry about it right now).
- The GameConnection class, along with its ancestor objects, are responsible for making sure updates transmitted via the BitStream are applied to the correct objects on the client.
So here is our sequence of events with our SimpleNetObject:- The server operator opens the console, and instantiates a SimpleNetObject:
$mySimpleNetObject = new SimpleNetObject(PracticeNetObject);
- The server instantiates our SimpleNetObject due to the above command.
- Every connected client (and any clients that connect after our object has been instantiated on the server) is directed to instantiate a client side replication of the object.
- Our server operator uses the console to change a string:
$mySimpleNetObject.setMessage1("Boo! Bet you didn't know I was here!"); - The server calls the ConsoleMethod to set message1.
- mySimpleNetObject->message1 is changed to the new message, and the Message1Mask is set on the object. Additionally (part of the underlying NetObject implementation) the mDirtyMaskBits of our object is set.
- A very short time later, the network processing loop is run. Since our mDirtyMaskBits is set for our object, and each client has had this object scoped to them, SimpleNetObject::packUpdate() is called for each client.
- Since the mNetFlags for our object has Message1Mask set, the BitStream for each client is loaded up with our string "Boo! Bet you didn't know I was here!".
- The clients each receive this BitStream, and their local instantiation of SimpleNetObject::unpackUpdate() is called.
- Each client copies the new string into their local version of PracticeNetObject, and since ::unpackUpdate() also uses Con::printf() to output the new string, the console for each client is updated with the new string "Boo! Bet you didn't know I was here!".



