Torque 2D/RealTimeNetworking

From TDN

This page is a Work In Progress.

Contents

Introduction

This article explains what is necessary to get TGE style real time networking into TGB. This is not the holy grail. It gives you the building blocks you need to implement it yourself, but it stops short of presenting a generic framework. I will hint at the game specific challenges, but solving those is beyond the scope of this article.

In order to get anything at all out of this article, you will need the following pre-requisites:

  • You must have a TGE license. This is because some of the code you need is missing from TGB, and you need to get it from TGE.
  • You need to be familiar with TGE's networking code. See this article for more information on Torque networking.
  • You must be proficient with C++. This requires extensive engine hacking. Ideally you should also be experienced with debugging netcode.
  • A decent merge tool such as Beyond Compare or WinMerge will speed up the process immensely, but is not required.

Datablocks

Due to an early design decision, datablocks were broken in TGB and repurposed as a generic data structure. This means that none of the TGB datablocks have network code, and none of the code to send datablocks over the network exists in TGB.

Replacing the Datablock Netcode

In order to restore datablocks to a working state, you will need to copy the code from TGE.

From gameConnection.cc/h, you will need the relevant parts of the following methods: GameConnection::handleConnectionMessage() GameConnection::preloadDataBlock() GameConnection::fileDownloadSegmentComplete() GameConnection::preloadNextDataBlock()

You will also need the following fields: GameConnection::mDataBlockModifiedKey GameConnection::mMaxDataBlockModifiedKey

And the following ConsoleMethods: GameConnection::transmitDataBlocks() GameConnection::listClassIDs() (optional, but handy for debugging)

You will also need SimDataBlockEvent from gameConnectionEvents.cc/h. I just copied the files and removed the unneeded code.

Datablock Names

T2D refers to datablocks internally by name, even in the C++ code. When datablocks are transmitted over the network, the client does not know the names of the datablocks. The ideal solution here would be to rewrite T2D's internal APIs to pass pointers to datablocks rather then by name. This could be done with an overloaded method, but T2D would still need to be modified to store pointers to datablocks rather then their names.

Unfortunately, this is a big job. Therefore, I present the following kludge to plug the gap until T2D's API is fixed. In simBase.cc, I modified the SimDataBlock::packData() and unpackData() to send the datablock name. This ensures the name is available on both the client and the server, at the cost of using more bandwidth. Here's the code:

void SimDataBlock::packData(BitStream *stream)
{
   stream->writeString(getName());
}

void SimDataBlock::unpackData(BitStream *stream)
{
   const char *foo = stream->readSTString();
   assignName(foo);
}

Networking an existing T2D datablock

As an example of what you need to do, I'll show you how to add netcode for t2dImageMapDatablock. I will provide the C++ code, but for brevity updating the header will be left as an exercise for the reader.

The first job is to add a preload() method and move the contents of onAdd() to preload().

bool t2dImageMapDatablock::preload(bool server, char errorBuffer[256])
{
   if(! Parent::preload(server, errorBuffer))
      return false;

   // Preload?
   if ( mPreload )
   {
      // Yes, so set lock-reference so that we're always active!
      mLockReference = 1;
   }

   // Compile ImageMap.
   if ( !compileImageMap() )
   {
      Con::errorf("t2dImageMapDatablock::preload - Failed to compile image map");
      return false;
   }

   return true;
}

Since onAdd() is now just a call to Parent, you can just remove it.

The implementations for packData() and unpackData() are as follows:

void t2dImageMapDatablock::packData(BitStream* stream)
{
    // Parent packing.
    Parent::packData(stream);

    // Write Datablock.
    stream->writeString(mSrcBitmapName);
    stream->write(mImageMode);

    stream->write(mExpectedFrameCount);

    stream->write(mFilterMode);
    stream->write(mFilterPad);

    stream->write(mPreferPerf);

    stream->write(mCellRowOrder);
    stream->write(mCellOffsetX);
    stream->write(mCellOffsetY);
    stream->write(mCellStrideX);
    stream->write(mCellStrideY);
    stream->write(mCellCountX);
    stream->write(mCellCountY);
    stream->write(mCellWidth);
    stream->write(mCellHeight);

    stream->writeString(mLinkedImageMaps);
    stream->write(mPreload);
    stream->write(mAllowUnload);
}

void t2dImageMapDatablock::unpackData(BitStream* stream)
{
    // Parent unpacking.
    Parent::unpackData(stream);

    // Read Datablock.
    mSrcBitmapName = stream->readSTString();
    stream->read((S32*)&mImageMode);

    stream->read(&mExpectedFrameCount);

    stream->read((S32 *)&mFilterMode);
    stream->read(&mFilterPad);

    stream->read(&mPreferPerf);

    stream->read(&mCellRowOrder);
    stream->read(&mCellOffsetX);
    stream->read(&mCellOffsetY);
    stream->read(&mCellStrideX);
    stream->read(&mCellStrideY);
    stream->read(&mCellCountX);
    stream->read(&mCellCountY);
    stream->read(&mCellWidth);
    stream->read(&mCellHeight);

    mLinkedImageMaps = stream->readSTString();
    stream->read(&mPreload);
    stream->read(&mAllowUnload);
}

The above code is for T2D 1.1 Beta 1.1, if you are not using that version you should check for any changed, added or removed fields and update the code accordingly.

It is perfectly safe to only add netcode to the T2D datablocks that you need on both the server and the client. Any datablocks that dont have packData() and unpackData() implemented will still ghost across the network, but none of their data will be transmitted. This allows you to slowly add netcode where you need it, and just create the datablocks you only need on the client in the same way as you would before.

NetObjects and NetEvents

Fortunately, NetObjects and NetEvents remain intact. You can test easily this by writing a ScopeAlways NetObject or a NetEvent. Note that no T2D objects derive from NetObject.

GameBase

Torque provides a GameBase object which is the base class of all objects that require datablocks. GameBase provides some additional code that aids with the transmission of datablock IDs over the network. You can get by without it, but you will have problems reliably sending a pointer to a datablock over the network.

You should either create a new t2dGameBase based on Torque's GameBase. You probably won't need the tick processing or move related code.

When writing your own NetObjects that require a datablock, you should derive from GameBase instead of NetObject and from GameBaseData instead of SimDataBlock.

Game Load Sequence

In TGE, this is referred to as the Mission Load Sequence. I renamed it here as T2D has no concept of missions. You will need to copy the mission load code for both the server and the client from TGE's common.

Scoping

Scoping is game specific and will not be covered in detail here. You will need to write a NetObject that overrides the onCameraScopeQuery() method to implement scoping. The camera information passed to this method will be incorrect, so if you need it you will have to modify the code to get the camera information from T2D.

Setting the Scope Object

TGE has no method of setting the scope object from script. I added this console method to netGhost.cc to expose that behavior to script for convenience when testing.

ConsoleMethod(NetConnection, setScopeObject, void, 3, 3, "(NetObject obj)")
{
   NetObject *obj = dynamic_cast<NetObject *>(Sim::findObject(argv[2]));
   if(obj)
      object->setScopeObject(obj);
}

In your final code you may find it easiest to set the scope object in C++, but it's nice to have the option of setting it in script.