This tutorial aims to go through the steps necessary to create an entirely new sceneObject in TGE. It will demonstrate the basics of ghosting, how to get something rendered (and various options), demonstrate some fun OpenGL stuff, and using ghosting changing parameters from server to client.

What specifically does this rather length tutorial touch on ?
  • Creating a new class in the TGE class heirarchy.
  • Adding a new class to the World Editor.
  • The basics of render images / prepRenderImage().
  • Some basics of OpenGL rendering.
  • Fun with sine waves.
  • Networking / Ghosting / Network Scope / PackUpdate / NetMasks.
  • Exposing C++ fields to script as member fields.
  • Exposing C++ fields to script via ConsoleMethods.

Who might profit from this tutorial ?

This is probably most useful for intermediate Torque programmers. That is, folks who've got torque scripting pretty much under the their belt, and are starting to get interested in what's under the covers on the C side. For OpenGL, this isn't going to explain the ZBuffer or why it's important to sort translucent objects, but it will introduce TGE's Render Images and various ways to queue them up. For C++, it's not going to explain why some fields are private and some public, and it will ask you to do things like "add this to the class declaration, and set it to zero in the constructor". For networking, it's not going to explain what a ghost is, but will go into some detail on making ghosting work for you.

This is built on a clean TGE 1.4 codebase, but will work with TGE 1.3, and will very, very probably work with TGE 1.5 and beyond. The networking parts are almost surely applicable to TGEA, but i can't vouch for the relevance of the scenegraph and rendering stuff to TGEA.

The tutorial is segmented into several parts. Each part has a bit of explanatory text, some pertinent code segments, and the source files representing the tutorial at that stage. There's only two source files throughout. Inside the code segments, some comments are called out in text like this, and those might be fun to read as you read the tutorial.

Thanks to Novack, Bruce Wallace, and Ben Garney for proofreading and advice on formatting and voice.

I just noticed that my CSS foo is not what i thought it was, and this doesn't render quite right in Internet Explorer. Sorry, ie!

Okay, on with the stuff!

First, make two blank files HappyFunSquiggleBall.cc and HappyFunSquiggleBall.h, and place them in engine/game. Arguably these might belong in engine/game/fx. Your call. Add them to your compiler project file, and recompile! If it doesn't recompile, or these steps aren't immediately obvious to you, you might find the rest of the tutorial frustrating.

Okay, let's put some boiler-plate code in there. /////////////////////////////////////////////////////////////////////// // happyFunSquiggleBall.h #ifndef _HAPPYFUNSQUIGGLEBALL_H_ #define _HAPPYFUNSQUIGGLEBALL_H_ #include "sim/sceneObject.h" // often, new classes will define a new datablock here. // datablocks are a large topic, and out of the scope of this tutorial, // and this simple object can get along without a new one. class HappyFunSquiggleBall : public SceneObject { typedef SceneObject Parent; public: HappyFunSquiggleBall(); ~HappyFunSquiggleBall(); // this registers our class with script. without this, our class would only be available in the C side. DECLARE_CONOBJECT(HappyFunSquiggleBall); static void initPersistFields(); // this will be called to make certain member variabled visible to script. void inspectPostApply (); // this gets calls when you modify the fields of an object in the World Editor // scene management routines bool onAdd (); void onRemove(); // rendering routines more on these in the .cc file just below bool prepRenderImage(SceneState* state, const U32 stateKey, const U32 startZone, const bool modifyBaseZoneState); virtual void renderObject (SceneState* state, SceneRenderImage *image); virtual void renderImage (SceneState* state, SceneRenderImage *image); // networking routines U32 packUpdate (NetConnection *conn, U32 mask, BitStream *stream); void unpackUpdate(NetConnection *conn, BitStream *stream); }; #endif // _HAPPYFUNSQUIGGLEBALL_H_ /////////////////////////////////////////////////////////////////////// // happyFunSquiggleBall.cc #include "core/bitStream.h" #include "math/mathIO.h" #include "sceneGraph/sceneState.h" #include "platform/profiler.h" #include "dgl/dgl.h" #include "game/happyFunSquiggleball.h" //---------------------------------------------------------------------------- // this corresponds to DECLARE_CONOBJECT in the header file. IMPLEMENT_CO_NETOBJECT_V1(HappyFunSquiggleBall); HappyFunSquiggleBall::HappyFunSquiggleBall() { mTypeMask |= StaticObjectType | StaticShapeObjectType | StaticRenderedObjectType; // (overkill? homework: eliminate as many as possible) mNetFlags.set(Ghostable); // without this, the clients never get the ghost // our object will start life as a unit cube who's origin is its center mObjBox.min.set(-0.5, -0.5, -0.5); mObjBox.max.set( 0.5, 0.5, 0.5); } HappyFunSquiggleBall::~HappyFunSquiggleBall() { } void HappyFunSquiggleBall::initPersistFields() { Parent::initPersistFields(); // nothing here for now, since HappyFunSquiggleBall has no new data members, // but it's good to get this boiler-plate stuff out of the way. addGroup("HappyFunSquiggleBall"); endGroup("HappyFunSquiggleBall"); } void HappyFunSquiggleBall::inspectPostApply() { // nothing here for now, since HappyFunSquiggleBall has no new data members, // but it's good to get this boiler-plate stuff out of the way. Parent::inspectPostApply(); } U32 HappyFunSquiggleBall::packUpdate(NetConnection *connection, U32 mask, BitStream *stream) { #ifdef BITSTREAMCOUNTER // you may not have the BITSTREAMCOUNTER resource implemented. // if not, that's fine, but it's a handy resource when the time comes that you want to // measure how much you're sending over the wire. // see http://www.garagegames.com/index.php?sec=mg&mod=resource&page=view&qid=12663 BITSTREAMCOUNTER(BSCounter, stream, "HappyFunSquiggleBall::packUpdate", connection, this) #endif // BITSTREAMCOUNTER U32 retMask = Parent::packUpdate(connection, mask, stream); // Write basic location info mathWrite(*stream, getTransform()); mathWrite(*stream, getScale ()); return retMask; } void HappyFunSquiggleBall::unpackUpdate(NetConnection *connection, BitStream *stream) { Parent::unpackUpdate(connection, stream); MatrixF mat; Point3F scale; mathRead (*stream, &mat ); mathRead (*stream, &scale); setScale (scale); setTransform(mat ); } bool HappyFunSquiggleBall::onAdd() { if (!Parent::onAdd()) return false; // addToScene() is critical. Without this, the object is not part of the scenegraph, // and so never gets ghosted on the server side, nor rendered on the client side. // If you put a breakpoint or debug print here, you'll see that this is called twice per instance: // once for the server-side object and once for the ghost. addToScene(); return true; } void HappyFunSquiggleBall::onRemove() { removeFromScene(); Parent::onRemove(); } bool HappyFunSquiggleBall::prepRenderImage(SceneState* state, const U32 stateKey, const U32 startZone, const bool modifyBaseZoneState) { // More on this in step 2 // For now, let's just to a debug print Con::printf("hello world, it's me, prepRenderImage()!"); return false; // not sure why/what to return here } void HappyFunSquiggleBall::renderObject(SceneState* state, SceneRenderImage *image) { // More on this in step 2 // For now, let's just do a debug print Con::printf("hello world, it's me, renderObject()!"); } void HappyFunSquiggleBall::renderImage(SceneState* state, SceneRenderImage* image) { // More on this in step 2 Con::printf("hello world, it's me, renderImage()!"); } At this point, torqueDemo should recompile and run. If you're feeling so inclined, you could now add a HappyFunSquiggleBall to the mission file by hand, or you could proceed to step 2..

.. Let's take a break from the hardcore stuff for a bit, and make it so that folks can add our new object to the scene right from the World Editor.

In example/creator/editor/EditorGui.cs, find this line: %Environment_Item[11] = "fxLight"; and right after it add this line: %Environment_Item[12] = "HappyFunSquiggleBall"; Then, in example/creator/editor/objectBuilderGui.gui, find the function ObjectBuilderGui::buildfxFoliageReplicator(), and add: function ObjectBuilderGui::buildHappyFunSquiggleBall(%this) { %this.className = "HappyFunSquiggleBall"; %this.process(); } That's sufficient for it to show up in the World Editor Creator (F11 | F4), in Mission Objects | Environment, but let's give it a cute icon in the World Editor Inspector (F11 | F3):

In guiTreeViewCtrl.cc, find the method GuiTreeViewCtrl::getIcon(), and add this: else if (!dStrcmp(iconString, "HappyFunSquiggleBall")) icon = fxSunLight; I chose the fxSunLight icon because it looks sort of like a happy fun squiggly ball itself.


Now if you recompile and run the mission editor, you can add a HappyFunSquiggleBall to the scene. And if you open up the console, you'll see a bunch of "hello world, it's me, prepRenderImage()!" lines. Notice that if you turn around so the object is no longer in view, you no longer get the debug prints. Nice!



But of course, nothing is actually rendering, and you'll notice that we don't have any debug prints from renderObject() or renderImage(). More on all of that in the next part! As a final part of this step, let's manually add the squiggle ball to the mission file, near the campfire: new HappyFunSquiggleBall(hfsb1) { position = "358 310 220"; rotation = "0 0 1 0"; scale = "1 1 1"; }; The "hello world" debug prints are actually telling us a lot. First, we know that rendering only happens on the client-side, so we know that our object has been successfully ghosted down, and added to the sceneGraph. Second, we know it's trying to render when it's in the view frustum, which is good. Third, we see that RenderObject() is not being called, so we know we have more wiring to hook up. Let's get started with the wiring. Check out the in-line comments for more narrative.

happyFunSquiggleBall.cc bool HappyFunSquiggleBall::prepRenderImage(SceneState* state, const U32 stateKey, const U32 startZone, const bool modifyBaseZoneState) { // Now things are heating up! // The scenegraph has a big list of objects which need to be rendered. // As you know, plain old opaque objects can be rendered any old time, and the magic of the ZBuffer will take care // of occlusion. But translucent objects need to be rendered from farthest to nearest, so the engine needs to sort them. // The engine could sort everything in the scene, but sorting is expensive, so instead it lumps all the translucent objects // together, and sorts and renders them as a bunch after all the opaque objects have been rendered. // prepRenderImage() is called to let the object declare how it would like to be sorted. // Note that a single object can add as many renderImages to the queue as it likes, to achieve multi-pass rendering. // Each frame, after all the objects have declared in which order they want to be rendered, then renderObject() is called. // // Let's set it up for translucency. SceneRenderImage* image = new SceneRenderImage; image->obj = this; image->isTranslucent = true; // tells the engine to render in a special pass image->sortType = SceneRenderImage::Point; // tells the engine which special pass to render in state->setImageRefPoint(this, image); // what 3D point to use for sorting this object state->insertRenderImage(image); Con::printf("hello world, it's me, prepRenderImage()!"); return false; // this has to do with Zone traversal; // since we're not an Interior or other zoneManager, we return false. } Now if you recompile and run (using the modified mission file, remember), you should see this in the console:

   hello world, it's me, prepRenderImage()!
   hello world, it's me, renderObject()!
   hello world, it's me, prepRenderImage()!
   hello world, it's me, renderObject()!
   hello world, it's me, prepRenderImage()!
   hello world, it's me, renderObject()!

Great! That means our object has been added to the render queue, and it's trying to actually render.

Let's take out the debug prints, and then flesh out renderObject() a little: void HappyFunSquiggleBall::renderObject(SceneState* state, SceneRenderImage *image) { // let's just set up the projection - renderImage() will be responsible for setting up the view matrix PROFILE_START(ShapeBaseRenderObject); AssertFatal(dglIsInCanonicalState(), "Error, GL not in canonical state on entry"); RectI viewport; // store the current projection matrix.. glMatrixMode(GL_PROJECTION); glPushMatrix(); // ..and viewport.. dglGetViewport(&viewport); // ..and build a new projection matrix. state->setupObjectProjection(this); // do rendering! renderImage(state, image); // restore the original projection matrix.. glMatrixMode(GL_PROJECTION); glPopMatrix(); // .. and viewport. dglSetViewport(viewport); // .. and OpenGL state dglSetCanonicalState(); AssertFatal(dglIsInCanonicalState(), "Error, GL not in canonical state on exit"); PROFILE_END(); } .. and then renderImage(): void HappyFunSquiggleBall::renderImage(SceneState* state, SceneRenderImage* image) { // store the original view matrix glMatrixMode(GL_MODELVIEW); glPushMatrix(); // .. push our object's position and orientation onto the view matrix dglMultMatrix(&mObjToWorld); // .. add in scaling glScalef(mObjScale.x, mObjScale.y, mObjScale.z); // and draw a lime-yellow cube! glColor3f(0.8f, 1.0f, 0.0f); dglWireCube(Point3F(1,1,1),Point3F(0,0,0)); // restore the original view matrix. glMatrixMode(GL_MODELVIEW); glPopMatrix(); } Woo-Hoo!


Let's replace that unit cube with a nice circle. In the class declaration: void drawUnitSquiggle(const ColorF& colorFill, const ColorF& colorOutline); And in the body: void HappyFunSquiggleBall::renderImage(SceneState* state, SceneRenderImage* image) { // store the original view matrix glMatrixMode(GL_MODELVIEW); glPushMatrix(); // .. push our object's position and orientation onto the view matrix dglMultMatrix(&mObjToWorld); // .. add in scaling glScalef(mObjScale.x, mObjScale.y, mObjScale.z); drawUnitSquiggle(ColorF(0.8f, 1.0f, 0.5f, 0.3f), ColorF(0.8f, 1.0f, 0.0f, 0.7f)); // Changed from a wireCube // restore the original view matrix. glMatrixMode(GL_MODELVIEW); glPopMatrix(); } void HappyFunSquiggleBall::drawUnitSquiggle(const ColorF& colorFill, const ColorF& colorOutline) { S32 numVertices = 64; F32 theta; F32 dTheta = M_2PI_F / (F32)numVertices; F32 r = 1.0f; // Tell OpenGL we're about to draw some transparent pixels glEnable (GL_BLEND); // Set the blend mode. (There are LOTS of blend modes, but this is the most standard one) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // Disable texturing glDisable (GL_TEXTURE_2D); // And let's draw slightly thick lines: glLineWidth(2.0f); // draw filled interior of the squiggle // using sines and cosines is a very inefficient but very flexible way of drawing a circle. // we'll make it slightly more efficient in later steps, but we're still going to do a bunch of trig calculations each frame. glBegin (GL_TRIANGLE_FAN); glColor4f (colorFill.red, colorFill.green, colorFill.blue, colorFill.alpha); glVertex3f (0.0f, 0.0f, 0.0f); theta = 0.0f; for (S32 n = 0; n <= numVertices; n++) { glVertex3f(cos(theta) * r, 0.0f, sin(theta) * r); theta += dTheta; } glEnd (); // draw outline glBegin (GL_LINE_LOOP); glColor4f (colorOutline.red, colorOutline.green, colorOutline.blue, colorOutline.alpha); theta = 0.0f; for (S32 n = 0; n <= numVertices; n++) { glVertex3f(cos(theta) * r, 0.0f, sin(theta) * r); theta += dTheta; } glEnd (); // Note that we don't bother restoring the texture mode, blend mode, etc, // since we're going to call dglSetCanonicalState() back in renderObject(). }
Thing of beauty!

First, let's make our squiggle drawing function a little more efficient by calculating the vertices once per render, instead of twice. Let's add two member variables, mVertices and mNumVertices to assist. HappyFunSquiggleBall::HappyFunSquiggleBall() { mTypeMask |= StaticObjectType | StaticShapeObjectType | StaticRenderedObjectType; mNetFlags.set(Ghostable); // our object will start life as a unit cube who's origin is its center mObjBox.min.set(-0.5, -0.5, -0.5); mObjBox.max.set( 0.5, 0.5, 0.5); // new code: mVertices = NULL; setNumVertices(64); } HappyFunSquiggleBall::~HappyFunSquiggleBall() { // new code: setNumVertices(0); } // new code: void HappyFunSquiggleBall::setNumVertices(U32 num) { if (mVertices != NULL) { delete [] mVertices; mNumVertices = 0; } if (num > 0) { mVertices = new Point3F[num]; mNumVertices = num; } } void HappyFunSquiggleBall::drawUnitSquiggle(const ColorF& colorFill, const ColorF& colorOutline) { // Tell OpenGL we're about to draw some transparent pixels glEnable (GL_BLEND); // Set the blend mode. (There are LOTS of blend modes, but this is the most standard one) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // Disable texturing glDisable (GL_TEXTURE_2D); // And let's draw slightly thick lines: glLineWidth(2.0f); calculateVertices(); // draw outline glBegin (GL_LINE_LOOP); glColor4f (colorOutline.red, colorOutline.green, colorOutline.blue, colorOutline.alpha); for (U32 n = 0; n < mNumVertices; n++) { glVertex3f(mVertices[n].x, mVertices[n].y, mVertices[n].z); } glEnd (); // draw filled interior of the squiggle glBegin (GL_TRIANGLE_FAN); glColor4f (colorFill.red, colorFill.green, colorFill.blue, colorFill.alpha); glVertex3f (0.0f, 0.0f, 0.0f); for (U32 n = 0; n < mNumVertices; n++) { glVertex3f(mVertices[n].x, mVertices[n].y, mVertices[n].z); } glVertex3f(mVertices[0].x, mVertices[0].y, mVertices[0].z); glEnd (); // Note that we don't bother restoring the texture mode, blend mode, etc, // since we're going to call dglSetCanonicalState() back in renderObject(). } void HappyFunSquiggleBall::calculateVertices() { F32 theta; F32 dTheta = M_2PI_F / (F32)mNumVertices; F32 r = 1.0f; // calculate the vertices theta = 0.0f; for (U32 n = 0; n < mNumVertices; n++) { mVertices[n].x = mCos(theta) * r; mVertices[n].y = 0.0f; mVertices[n].z = mSin(theta) * r; theta += dTheta; } } So far so good! Now let's really get squiggly. void HappyFunSquiggleBall::calculateVertices() { F32 theta; F32 dTheta = M_2PI_F / (F32)mNumVertices; F32 r = 1.0f; F32 t = (F32)(Sim::getCurrentTime()) * 0.001f * M_2PI_F; // Sine waves are your friend! F32 waveFreq1 = 1.0f; F32 waveRepeats1 = 5.0f; F32 waveAmp1 = 0.3f; F32 waveTheta1 = 0.0f; F32 waveDTheta1 = dTheta * waveRepeats1; F32 waveFreq2 = -1.7f; F32 waveRepeats2 = 23.0f; F32 waveAmp2 = 0.1f; F32 waveTheta2 = 0.0f; F32 waveDTheta2 = dTheta * waveRepeats2; // calculate the vertices theta = 0.0f; for (U32 n = 0; n < mNumVertices; n++) { r = 1.0f; r += mSin(t * waveFreq1 + waveTheta1) * waveAmp1; r += mSin(t * waveFreq2 + waveTheta2) * waveAmp2; mVertices[n].x = mCos(theta) * r; mVertices[n].y = 0.0f; mVertices[n].z = mSin(theta) * r; theta += dTheta; waveTheta1 += waveDTheta1; waveTheta2 += waveDTheta2; } }
Thing of squiggly beauty!

If you're interested in making this yet more optimized, you can dramatically reduce the number of calls into the OpenGL system by using glDrawArrays, but that's a topic for another tutorial.

This is another mini-step. It's definitely squiggley, but it's more of a Happy Fun Squiggle Disk than a ball. Let's use a cheap-and-dirty trick to make it look more like a ball: we'll have it always face the camera. This is the same method used by "Billboard" objects in other parts of Torque. We'll add the following function: void HappyFunSquiggleBall::faceCamera() { // make the rotation portion of the matrix be identity. MatrixF mat; dglGetModelview(&mat); mat.setColumn (0, Point3F(1.0f, 0.0f, 0.0f)); mat.setColumn (1, Point3F(0.0f, 1.0f, 0.0f)); mat.setColumn (2, Point3F(0.0f, 0.0f, 1.0f)); dglLoadMatrix (&mat); } and insert it in renderImage(): void HappyFunSquiggleBall::renderImage(SceneState* state, SceneRenderImage* image) { // store the original view matrix glMatrixMode(GL_MODELVIEW); glPushMatrix(); // .. push our object's position and orientation onto the view matrix dglMultMatrix(&mObjToWorld); if (mFaceCamera) // New Code (mFaceCamera added to .h, and set to true in the constructor) faceCamera(); // It's important to do this before the call to scale, since faceCamera() // resets the upper-left 3x3 portion of the matrix, which stores scale. // .. add in scaling glScalef(mObjScale.x, mObjScale.y, mObjScale.z); drawUnitSquiggle(ColorF(0.8f, 1.0f, 0.5f, 0.3f), ColorF(0.8f, 1.0f, 0.0f, 0.7f)); // Changed from a wireCube // restore the original view matrix. glMatrixMode(GL_MODELVIEW); glPopMatrix(); } This is a major part. The squiggle ball is pretty cool, but say you want to change its colors when someone walks into it ? Or change the behaviour of the squiggles ? That's where ghosting parameters comes in.

Torque's ghosting mechanism is great. Consider a single server with a bunch of players, and one happy squiggle ball. If the server decides it's time to change the colors on the ball, how should the clients be notified of the change ? Well, you could have the server send out a message when the change occurs; something like: onColorChanged() { for each client in network scope { sendMessageToClient(colorChanged) } } But suppose nobody was in network scope when the color changed ? (Being out of Network Scope means that a given client was too far away, or in a sealed space or something, and to save bandwidth the system has decided that they don't need to know about changes to the ball.) In that case, onColorChanged() would execute, and nobody would get notified. But then a few seconds or a minute later, some client does come in to network scope - they're not going to get the message! Well, you could perhaps add a "colorsHaveChanged" flag to the ball, and when a client comes into network scope, check that flag, and if it's true, then send the colorChanged notification. That's well and good, but the question remains: when would you clear that flag ? Say you clear it after sending the notification to client A. But then later client B also comes into network scope - the flag is not set, so client B would not be notified.

Torque's solution here is pretty cool: it can keep a "colorsHaveChanged" flag independently for each client. So when the colors change, the flag is set for all clients, and when client A comes into scope, the message is sent and client A's flag is cleared, and later, likewise for client B.

This mechanism is called "Ghosting", and the flags are called "Netmasks".

What sort of data is good for ghosting ?

Ghosting is not perfect for every type of information you might want to send to clients about an object. Suppose you want to send down a notification about some event, for example an event named "GainedAPoint". You could make a flag for "hasGainedAPoint", and send down that information for each client as above. But suppose a client is out of scope, and more than one GainedAPoint events occur ? When the client comes back in to scope, since the netmask is a boolean flag and not a counter, only one such notification will be sent, and the client will have an innacurate idea of the object. The way to ghost down this information would be to make a flag like "PointsHaveChanged", and send down the current points instead of the change in points.

In short, ghosting is good for sending state, not events.

Another good rule of thumb for whether to ghost or not to ghost is to ask yourself if the information in question should be known by all the clients in the game, or just one ? If the answer is "just one", you can achieve that through ghosting, but you probably want to use CommandToClient (in script), or a custom NetEvent (in C) instead.

Okay, enough theory! Back to happy fun squiggle ball! Let's start with something simple. Let's network the flag "mFaceCamera".

The first step is to create a NetMask. In the .h file, add: public: enum NetMasks { StateChangedMask = Parent::NextFreeMask, NextFreeMask = Parent::NextFreeMask << 1, }; Note the relationship with the parent class. Since they track dirty-state per-client, there are only 32 netmasks for any object, and a child class inherits all of its parent's netmasks. .. Let's pause and repeat that: there are only 32 netmasks for any object, and a child class inherits all of its parent's netmasks. So a class like AIPlayer inherits all the netmasks from Player, ShapeBase, GameBase, SceneObject, and so on. For this reason it's good to be parsimonious with netmasks. A common approach to conserving them is to use a single netmask for a group of properties which tend to all change at the same time. For example it would be silly to have one netmask for "fillColor" and another for "outlineColor", since one probably changes when the other does. On the other hand, putting all the properties in a single netmask can be inefficient. For example if you have lots and lots of data which changes only rarely, it would be a shame to ghost all that down every time some other high-frequency property changes. So it's a balance.

In this tutorial, i only use one netmask for all the properties of the ball.

Okay, back to the code.

Let's add some setter/getters for mFaceCamera, down near the bottom of the class declaration: // access routines public: void setFaceCamera (bool value); bool getFaceCamera () { return mFaceCamera; } .. and implement setFaceCamera() in the .cc file: void HappyFunSquiggleBall::setFaceCamera(bool val) { mFaceCamera = val; setMaskBits(StateChangedMask); // Sets this netmask for all clients } next, we'll change packUpdate() and unpackUpdate() to check if the StateChangedMask has been set for each client, and if so, send the current state down the wire: U32 HappyFunSquiggleBall::packUpdate(NetConnection *connection, U32 mask, BitStream *stream) { // The parameter "connection" is one particular client. // packUpdate() will be called in turn for each client in network scope. #ifdef BITSTREAMCOUNTER // you may not have the BITSTREAMCOUNTER resource implemented. // if not, that's fine, but it's a handy resource when the time comes that you want to // measure how much you're sending over the wire. // see http://www.garagegames.com/index.php?sec=mg&mod=resource&page=view&qid=12663 BITSTREAMCOUNTER(BSCounter, stream, "HappyFunSquiggleBall::packUpdate", connection, this) #endif // BITSTREAMCOUNTER U32 retMask = Parent::packUpdate(connection, mask, stream); mathWrite(*stream, getTransform()); mathWrite(*stream, getScale ()); // The following writes a single bit to the bitstream, and also returns the value of the bit.. if (stream->writeFlag(mask & StateChangedMask)) { // .. so if our mask had the StateChangedMask set, then the bitstream would now have a one written in it, // and code execution will be here. stream->writeFlag(mFaceCamera); // write the value of our boolean mFaceCamera } // else // { // .. and if our mask did not have the StateChangedMask set, then the bitstream would now have a zero written in it, // and code execution will be here. Where we will do nothing, so leave this commented out. // } return retMask; } void HappyFunSquiggleBall::unpackUpdate(NetConnection *connection, BitStream *stream) { Parent::unpackUpdate(connection, stream); MatrixF mat; Point3F scale; mathRead (*stream, &mat ); mathRead (*stream, &scale); setScale (scale); setTransform(mat ); // Here we need to be an expert on what was written by packUpdate(). // First, we read one bit, to see if we should read another: if (stream->readFlag()) // StateChangedMask { // .. yes, the netmask bit was set, so let's read the actual value: mFaceCamera = stream->readFlag(); } // else // { // .. nope, StateChangedMask was not set, so don't read the state. // } } OKAY! Techically, mFaceCamera is now networked!

But there's one last detail before we can really appreciate that, which is exposing it to script.

Add the following to the bottom of the .cc file: ConsoleMethod(HappyFunSquiggleBall, setFaceCamera, void, 3, 3, "(bool)") { object->setFaceCamera(dAtob(argv[2])); } ConsoleMethod(HappyFunSquiggleBall, getFaceCamera, bool, 2, 2, "") { return object->getFaceCamera(); } Check it out:



A quick note on more advanced use of packUpdate - if you wanted, you could shape the update for each client. ie, the ghosted data doesn't need to be identical for each client. You might use this in a team situation, where folks on the red team see one version of the state of red players, but folks on the blue team see something different.

Pseudoish-code for that might look like this: Player::packUpdate(connection) { ... if writeFlag(connection->player->team == this->team) sendInfoForFriends(); else sendInfoForEnemies(); } Player::unpackUpdate(connection) { ... if readFlag() // on the same team ? readInfoForFriends(); else readInfoForEnemies(); } Let's network a whole bunch more stuff than mFaceCamera; let's send colors and squiggle parameters!

In the last section, we used a getter/setter for mFaceCamera to allow us to set the netmask when the value changed, but using setter/getters for our eight squiggle parameters and two colors is a pain, so for those we'll just expose them as plain old script fields, and provide a single "touch" function to call when they're changed. Since we'll likely be editing these in the mission editor, we'll have the mission editor automatically call touch().

I believe that later versions of TGE (1.5?) provide a way to execute a function when script changes a fields.

Okay, first, let's move the squiggle parameters out of calculateVertices() and into the class declaration.

at the bottom of our class declaration: protected: // squiggle parameters: bool mFaceCamera; F32 mWaveFreq1; F32 mWaveRepeats1; F32 mWaveAmp1; F32 mWaveFreq2; F32 mWaveRepeats2; F32 mWaveAmp2; in our class constructor: mFaceCamera = true; mWaveFreq1 = 1.0f; mWaveRepeats1 = 5.0f; mWaveAmp1 = 0.3f; mWaveFreq2 = -1.7f; mWaveRepeats2 = 23.0f; mWaveAmp2 = 0.1f; and calculateVertices(): void HappyFunSquiggleBall::calculateVertices() { F32 theta; F32 dTheta = M_2PI_F / (F32)mNumVertices; F32 r = 1.0f; F32 t = (F32)(Sim::getCurrentTime()) * 0.001f * M_2PI_F; // Sine waves are your friend! F32 waveTheta1 = 0; F32 waveTheta2 = 0; F32 waveDTheta1 = dTheta * mWaveRepeats1; F32 waveDTheta2 = dTheta * mWaveRepeats2; // calculate the vertices theta = 0.0f; for (U32 n = 0; n < mNumVertices; n++) { r = 1.0f; r += mSin(t * mWaveFreq1 + waveTheta1) * mWaveAmp1; r += mSin(t * mWaveFreq2 + waveTheta2) * mWaveAmp2; mVertices[n].x = mCos(theta) * r; mVertices[n].y = 0.0f; mVertices[n].z = mSin(theta) * r; theta += dTheta; waveTheta1 += waveDTheta1; waveTheta2 += waveDTheta2; } } Next we'll expose the fields to script. In initPersistFields(): void HappyFunSquiggleBall::initPersistFields() { Parent::initPersistFields(); addGroup("HappyFunSquiggleBall"); addField("faceCamera" , TypeBool, Offset(mFaceCamera , HappyFunSquiggleBall)); addField("mWaveFreq1" , TypeF32 , Offset(mWaveFreq1 , HappyFunSquiggleBall)); addField("mWaveRepeats1", TypeF32 , Offset(mWaveRepeats1 , HappyFunSquiggleBall)); addField("mWaveAmp1" , TypeF32 , Offset(mWaveAmp1 , HappyFunSquiggleBall)); addField("mWaveFreq2" , TypeF32 , Offset(mWaveFreq2 , HappyFunSquiggleBall)); addField("mWaveRepeats2", TypeF32 , Offset(mWaveRepeats2 , HappyFunSquiggleBall)); addField("mWaveAmp2" , TypeF32 , Offset(mWaveAmp2 , HappyFunSquiggleBall)); endGroup("HappyFunSquiggleBall"); } Next we'll provide a Touch() method to call after a field is changed.

In the class declaration: // access routines public: void setFaceCamera (bool value); bool getFaceCamera () { return mFaceCamera; } void touchStateChanged() { setMaskBits(StateChangedMask); } // New code And now we'll send the values down in packUpdate(): U32 HappyFunSquiggleBall::packUpdate(NetConnection *connection, U32 mask, BitStream *stream) { #ifdef BITSTREAMCOUNTER // you may not have the BITSTREAMCOUNTER resource implemented. // if not, that's fine, but it's a handy resource when the time comes that you want to // measure how much you're sending over the wire. // see http://www.garagegames.com/index.php?sec=mg&mod=resource&page=view&qid=12663 BITSTREAMCOUNTER(BSCounter, stream, "HappyFunSquiggleBall::packUpdate", connection, this) #endif // BITSTREAMCOUNTER U32 retMask = Parent::packUpdate(connection, mask, stream); mathWrite(*stream, getTransform()); mathWrite(*stream, getScale ()); // The following writes a single bit to the bitstream, and also returns the value of the bit.. if (stream->writeFlag(mask & StateChangedMask)) { // .. so if our mask had the StateChangedMask set, then the bitstream would now have a one written in it, // and code execution will be here. stream->writeFlag (mFaceCamera) ; // write the value of our boolean mFaceCamera stream->writeSignedFloat(mWaveFreq1 * 0.001f, 16); // writeSignedFloat() only accepts values between -1 and 1. stream->writeSignedInt (mWaveRepeats1 , 10); stream->writeSignedFloat(mWaveAmp1 * 0.001f, 16); stream->writeSignedFloat(mWaveFreq2 * 0.001f, 16); stream->writeSignedInt (mWaveRepeats2 , 10); stream->writeSignedFloat(mWaveAmp2 * 0.001f, 16); } // else // { // .. and if our mask did not have the StateChangedMask set, then the bitstream would now have a zero written in it, // and code execution will be here. Where we will do nothing, so leave this commented out. // } return retMask; } void HappyFunSquiggleBall::unpackUpdate(NetConnection *connection, BitStream *stream) { Parent::unpackUpdate(connection, stream); MatrixF mat; Point3F scale; mathRead (*stream, &mat ); mathRead (*stream, &scale); setScale (scale); setTransform(mat ); // Here we need to be an expert on what was written by packUpdate(). // First, we read one bit, to see if we should read another: if (stream->readFlag()) // StateChangedMask { // .. yes, the netmask bit was set, so let's read the actual value: mFaceCamera = stream->readFlag ( ); mWaveFreq1 = stream->readSignedFloat(16) * 1000.0f; mWaveRepeats1 = stream->readSignedInt (10); mWaveAmp1 = stream->readSignedFloat(16) * 1000.0f; mWaveFreq2 = stream->readSignedFloat(16) * 1000.0f; mWaveRepeats2 = stream->readSignedInt (10); mWaveAmp2 = stream->readSignedFloat(16) * 1000.0f; } // else // { // .. nope, StateChangedMask was not set, so don't read the state. // } } .. and finally a bit of code in inspectPostApply(), which gets called when you change a field in an object from the mission editor: void HappyFunSquiggleBall::inspectPostApply() { // nothing here for now, since HappyFunSquiggleBall has no new data members, // but it's good to get this boiler-plate stuff out of the way. Parent::inspectPostApply(); touchStateChanged(); // new code } and voila, more fun with sine waves:


This tutorial has been pretty thin on homework, and i apologize, so here's a little bit:

Network the fill and outline colors of Happy Fun Squiggle Ball, and network the number of vertices it uses.

That's all i've got, i hope this has been some help!

Orion