RakNet and Cinema4D Part 1: Connecting 2 players

Introduction

This tutorial will demonstrate how to connect 2 or more copies of Cinema4D together and let each copy of Cinema4D have its own "player" cube object that they can move around in the scene. All other copies of Cinema4D that are also connected will also see the cube moving around the screen. This could be thought of as a very simple Multiplayer room where the each Cinema4D user has a cube as their "avatar" that they can move around the world.

This tutorial will use Visual Studio 2008 C++ Express Edition, Cinema4D 11.5 and RakNet for the Game Networking Engine.

If you want to know more then just read on...

What is RakNet

The aim of this first tutorial is to get 2 copies of Cinema4D connect together using RakNet. What is RakNet I hear you ask? Well here is the snippet from the RakNet website...  "Raknet is a cross-platform C++ game networking engine. It is designed to be a high performance, easy to integrate, and complete solution for games and other applications.". As seen there RakNet is a game networking library. It can be used as the back bone of a game to support Massively Multiplayer Online games (MMO's).
So why are we going to be using RakNet when we could do something similar by using something like Sockets? RakNet also comes with the ability to do Voice chatting between players, call Remote Procedure Calls, has object replication to automatically create, destroy and serialize changes to your "game objects". It also comes with NAT Punchthrough for connecting peer to peer applications (more about this to follow in this tutorial) as well as more advanced MMO type features like a full Lobby System and more...
OK now your thinking... do you work for this guy? Why all the hoopla and hype about it? The answer to that is, No. No I do not work for JenkinsSoftware I just think RakNet is cool and affordable (you don't pay a cent unless you earn more than $250,000 USD).  
Another reason for choosing a "Game Networking Engine" to connect Cinema4D copies together... well... lets see if we can make a game that runs in Cinema4D. Hey, it might not be the next killer game but maybe we will make something fun and inspire others to do the same.
One thing to mention about this tutorial is that its not possible to run 2 copies of Cinema4D on the same machine at the same time. So unfortunately to really make use of this tutorial you will need 2 machines that can run Cinema4D. You can use the 42 day trial for your second machine if you only have 1 copy. Well you could use the 42 day trial for both machines. The problem is that you need 2 machines. My current setup is a laptop and a pc.
NOTE: Since publishing this article I have been informed that you can run 2 copies of Cinema4D at the same time by running it via the command line with the -parallel flag. For example, just open up a command dialog, navigate to where cinema4d is installed and type "Cinema4D -parallel". I will update this tutorial at a later date to include the use of this flag in the tutorial. But for now you should be able to use this method to run it on the same machine, without the need of copying the plugin to another computer.
Lets get started.

Before we start

Heh, and you thought we were going to leap straight into some fancy coding there didn't you. Well unfortunately we need to get a couple of things sorted out first before we get started. To work with this Tutorial you will need to have a copy of Visual Studio C++ 2008 Express Edition (the Standard and Professional Editions should also work just as well). You will also need a copy of Cinema4D and have it setup properly to work with. These tutorials were developed using Visual Studio 2008 C++ Express Edition and Cinema4D 11.5 on a Windows 7 machine.
Now before you rush away to google and start typing in keywords like a mad man you should instead just follow through this tutorial

This will take you through all the steps needed to get up and running with Visual Studio 2008 C++ Express Edition and Cinema4D plugin development.

Getting the Code

This tutorial is actually going to skip on a few of the inner working details of how RakNet works and jump straight into the fun of seeing something actually happen. To do this you should first download the project file located at the end of this article.
Unzip this to your plugins folder in Cinema4D. It will be something like C:\Program Files\MAXON\CINEMA 4D R11.5\plugins
You should now have a folder called raknetcinemapart1 and directly beneath that the  gameengine.vcproj file as well as a bunch of other stuff. If instead you have unzipped it and now have a raknetcinemapart1 folder inside a raknetcinemapart1 folder, then you will need to move the files down. I note this because sometimes unzipping files you can accidentally unzip to a new folder and this will break it. Relative paths are very important to Cinema4D plugins.

Getting RakNet

Downloading and installing.

Next we need to get RakNet. Go to  http://www.jenkinssoftware.com/ and download the latest version using the "Free Download" link at the top right. As of writting the version I am using is verison 3.7.
Unzip this somewhere on your harddrive. I have mine stored in c:\Projects\3rdparty\raknet\raknet-3.7.  Its good to keep all your 3rd party libraries in the same place for code management purposes. I also have multiple versions of raknet which is why I have a different folder for each version. And in my particular setup everything is also added to a personal subversion repository which is backed up hourly.
With RakNet installed you will now need to compile it.

Compiling RakNet

Now that we have the code lets compile it. Right click on your Visual Studio 2008 C++ Express Edition and choose "Run as Administrator". When Visual Studio is running open the solution file RakNet_VS2005. Mine is located in the following location C:\Projects\3rdParty\racknet\RakNet-3.7\RakNet_VS2005.sln
You will now be prompted with the "Welcome to Visual Studio Conversion Wizard" dialog. Just press Finish and let it do its thing.
It might then tell you "Some of the properties associated with the solution could not be read". This is fine... just press OK.
Finally close the "Conversion Complete" dialog.
Now go up to the Build menu and choose "Build Solution". Go make your self a cup of tea, or a coffee if you prefer and when you come back all the projects will be compiled.

Compiling and Cinema4D sample Code

Now that we raknet in place we can compile our cinema4d sample code. Right click on your Visual Studio 2008 C++ Express Edition and choose "Run as Administrator". When Visual Studio is running open the solution file gameengine.sln in the Cinema4D plugins directory, mine is located here C:\Program Files\MAXON\CINEMA 4D R11.5\plugins\raknetcinemapart1\gameengine.sln

Fixing up any missing projects

Since you most likely installed RakNet in a different location to mine you will notice that one of the projects "NATpunchthrough" might be greyed out. Right click on this project and click "remove". You don't actually need this here but I like to have it there for convenience when running my projects. To get this back you need to right click on the "Solution 'game engine' " at the top of the Solution Explorer and choose "Add->Existing Project". Navigate to your raknet folder and find the NATPunchthrough project in the Samples folder, mine is located at C:\Projects\3rdParty\racknet\RakNet-3.7\Samples\NATPunchthrough\NATPunchthrough_vc8.vcproj.
If the _api folder is also greyed out that means that you didn't follow the instructions from the tutorial mentioned at the top of this article.  Go now to that article and follow the section "Converting the _api library project to VS 2008". After that reload this project and the _api folder should now be ok.

Setting up Visual Studio Directory Paths

One last thing to do before we can compile is setup a couple of directory paths that visual studio needs to find some RakNet files.
From the file menu choose "Tools->Options...". The Options dialog will now be displayed. Expand the "Projects and Solutions" tree and choose "VC++ Directories".
Next change the "Show directories for:" drop down from "Executable Files" to "Include files".
Now press the little folder icon. A new entry will be added to the list in the window. Double click in this and then select the little "..." at the end. This will let us choose a directory to add to the include list. When the "Select Directory" window appears navigate your RakNet source directory, mine is located here C:\Projects\3rdParty\racknet\RakNet-3.7\Source.
With that directory open press the "Select Folder" button at the bottom of the screen.  
Next change the "Show directories for:" dialog to "Library Files".
Again create a new entry and then navigate to the RakNet Lib folder, mine is located here... C:\Projects\3rdParty\racknet\RakNet-3.7\Lib
Now that you have these 2 setup press the OK button to close the Options dialog.

Compile the plugin

Now we can compile. Go to the Build menu and choose "Build Solution". This will build all the projects including the _api, gamegine and NATpunchthrough projects.
Finally make sure you have the gameengine project set as your start project. First right click on the "gameengine" project and then choose "Set as Startup Project"from the menu.

Running the Cinema4D sample Code

Now we are ready to run the project.
HA... almost had you there for a second. You really didn't think I would make it that easy did you. Well sadly not. Before we can begin we need to find out your network IP address and do a couple of changes.

Finding your network address

First we need your IP address. The easiest way for us to do this is simply to just run the NATPunchthrough project.
Right click on the NATPunchthrough and select "Debug->Start new Instance". This will bring up a new window with the NAT punchthrough running in it. It will have the text "My internal IP is 192.168.1.1" where the number will be different for your computer. Take note of this number. Close the window.
Next go into the "Game Engine" project in Visual Studio. Expand the list and go into the source code directory and double click on the RakNetStuff.h file.
At the top you will see the following
#define DEFAULT_NAT_PUNCHTHROUGH_FACILITATOR_IP "192.168.1.34"
Change this number to the one you just found.
Save the file and choose "Build->Build gameengine" from the menu.

Run the NAT Punchthrough server.

Before we run the cinema4d project we need to start the NAT punchthrough. This allows peer to peer clients to connect to each other. Run it like you did before by right clicking on the NATPunchthrough and selecting "Debug->Start new Instance". This will bring up a new window with the NATpunchthrough.
In the NATpunchthrough window you should press the "F" key followed by return to make it run as a facilitator. This is where all copies of cinema4d will connect.
Will will now see something like
Ready.
My GUID is 4534643643
'c'clear the screen.
'q'uit.
This means it is running. Next we run the Cinema4D project.

Now Run the Cinema4D Project

Right Click on the gameengine project and choose "Debug->Start New Instance.
Since this is the first time you have run the project you will need to tell it the executable path of Cinema4D. In the "Executable File name:" drop down select "browse" and navigate to where you installed Cinema4D, mine is installed here C:\Program Files\MAXON\CINEMA 4D R11.5\CINEMA 4D.exe. Be sure to select Cinema4D.exe and NOT Cinema4D64bit.exe. We are running a 32 bit project for our purposes.
Cinema4D will start up and once running you will see a Cube in the scene. Now press the "play" button on the animation bar at the bottom of Cinema4D. This is required to make sure our scenehook (which we will talk about later) is running.
Go back to the NATpunchthrough window and you will also see something like the following appear in the window
ID_NEW_INCOMING_CONNECTION from 192.168.1.1:436372 with guid 239847398743
This means that your cinema4d project has connected and is waiting for other players to arrive.

Run a second copy of Cinema4D

OK. Now that we have that all up and running we want to see if we can do some actual networking. To do this we will copy the project to the second computer with Cinema4D and run the project.
The most simplest way is just to copy everything. Copy the raknetcinemapart1 folder to your second computer and place it in the Cinema4D plugins folder.
Run Cinema4D on the second computer by double clicking on the Cinema4D.exe executable (making sure that both computers are actually connected to a network).
Press the play animation button at the bottom of Cinema4D again and hey presto if everything is working correctly you will now see 2 cubes in the scene. You will also notice another line appear in the NATpunchthrough window.
Select one of the cubes in the scene. Select the move tool in Cinema4D (its the one with 4 arrows on it at the top left of the screen). Try to move the cube around. If the cube doesn't move then that means its the other players cube, which you cannot currently move (more on this later). So select the other cube in the scene from the objects list. You should now be able to move this cube around in the scene.
If you now look at your other computer at the same time as you move the cube you will notice that the cube is also moving on that copy of Cinema4D as well. Magic!

About the Code

Now that you have seen what this can so lets have a look at some of the code.
Firstly the majority of the RakNet code is all borrowed from the example IrrlichtDemo project that comes with RakNet. You can find it in the RakNet\DependentExtensions\IrrlichtDemo folder. This handles all the connecting and networking code for you. With only a few small changes you can use this for any project that you wish.

RakNetMain.cpp

This file contains the main loop that calls the RakNet code. It is handled using a Cinema4D SceneHook plugin. There will be one of these scenehooks created for every document that you have in Cinema4D. So if you are using this on your work computer then make sure to DELETE the gameengine.cdl file before you go back to work. Otherwise this will run everytime you open up cinema4d.
#include "CDemo.h"
#include "c4d.h"

#define ID_RAKNET 1024553

class RakNetSceneHookData : public SceneHookData
{
private:
RakNetSceneHookData();
~RakNetSceneHookData();

public:
virtual Bool Init(GeListNode* node);
virtual Bool AddToExecution(PluginSceneHook *node, PriorityList *list);
virtual LONG InitSceneHook(PluginSceneHook *node, BaseDocument *doc, BaseThread *bt);
virtual void FreeSceneHook(PluginSceneHook *node, BaseDocument *doc);
virtual LONG Execute(PluginSceneHook *node, BaseDocument *doc, BaseThread *bt, LONG priority, LONG flags);


public:
static NodeData *Alloc(void);

CDemo m_demo;
};



RakNetSceneHookData::RakNetSceneHookData()
{
}

RakNetSceneHookData::~RakNetSceneHookData()
{
}

Bool RakNetSceneHookData::AddToExecution (PluginSceneHook *node, PriorityList *list)
{
return FALSE;
}

LONG RakNetSceneHookData::InitSceneHook (PluginSceneHook *node, BaseDocument *doc, BaseThread *bt)
{
//Can allocate data here if we want
return EXECUTION_RESULT_OK;
}

void RakNetSceneHookData::FreeSceneHook (PluginSceneHook *node, BaseDocument *doc)
{
//Then we clear the data here
}

LONG RakNetSceneHookData::Execute(PluginSceneHook *node, BaseDocument *doc, BaseThread *bt, LONG priority, LONG flags)
{
m_demo.Init(doc);
m_demo.Update();
return EXECUTION_RESULT_OK;
}


NodeData *RakNetSceneHookData::Alloc()
{
return gNew RakNetSceneHookData();
}


Bool RakNetSceneHookData::Init(GeListNode* node)
{
return TRUE;
}

Bool RegisterRakNetSceneHook()
{
   return RegisterSceneHookPlugin(ID_RAKNET, "RackNetSceneHook", PLUGINFLAG_HIDE, RakNetSceneHookData::Alloc, EXECUTION_GENERATOR, 0);
}
As you can see this file is quite empty. It has a copy of CDemo and thats about it. There is not alot going on here except to make sure a call to RakNet is happening when ever the Execute method is called by calling 2 methods on the m_demo member. Which is why we have to press the play animation button in Cinema4D to ensure this happens continuously.

CDemo.cpp

This is a slightly modified version of the file that comes with the IrrlichtDemo. The main differences here are that it has an Init method that takes a Cinema4D BaseDocument which is uses to Initialize the copy of MyRakNet, which in turn creates the players cube.
bool CDemo::Init(BaseDocument *pBaseDoc)
{
if(!m_initialized)
{
m_doc->SetLink(pBaseDoc);
m_rackNet.Init(pBaseDoc);

// RakNet startup
char dest[1024] = "TestPlayer";

// Hook RakNet stuff into this class
m_rackNet.udpProxyClient->SetResultHandler(this);
m_rackNet.playerReplica->playerName=RakNet::RakString(dest);
m_rackNet.replicaManager3->demo=this;

m_initialized = true;
}

return true;
}
The only other difference piece of code that is related to this tutorial is at the bottom of void CDemo::UpdateRakNet(void) method. Here it loops over all the GameObjects and calls UpdatePosition on it. Which will do one of 2 things. If its our object it will update the position for RakNet to send. If its another players object it will update the position of the gameobject in the current scene with the one sent from the other player.
DataStructures::DefaultIndexType idx;
for (idx=0; idx GetReplicaCount(); idx++)
{
GameObject *pGameObject = static_cast(m_rackNet.replicaManager3->GetReplicaAtIndex(idx));
if(pGameObject)
{
// Is a locally created object?
if (pGameObject->creatingSystemGUID == m_rackNet.rakPeer->GetGuidFromSystemAddress(UNASSIGNED_SYSTEM_ADDRESS))
{
pGameObject->UpdatePosition(TRUE);
}
else
{
pGameObject->UpdatePosition(FALSE);
}
}
}

RakNetStuff.cpp

The main change in this file is the creation of the player game object in the Init method. This code creates a new GameObject and tells it to manage the position of the passed in Cinema4D BaseObject, which in this case is a cube.
void MyRakNet::Init(BaseDocument *pBaseDoc)
{
  ...
// Create and register the network object that represents the player
playerReplica = new GameObject();

//Add a cube cinema4d object component to the GameObject
BaseObject *pTemp = BaseObject::Alloc(Ocube);
pBaseDoc->InsertObject(pTemp,NULL,NULL);
playerReplica->Init(pBaseDoc,pTemp);
replicaManager3->Reference(playerReplica);
...
}

GameObject.cpp

This file contains the actual GameObject, which is derived from the RakNet::Replica3 class. This handles the creation and deletion of other players objects, serializing changes for my game object to other players and also getting the new position from other player objects and updating them in my scene.
You will also notice a class called CompC4dObjectImpl. This is created internally instead of the header file so that we don't have to include the cinema4d "c4d.h" file in the main header file. This makes the code cleaner and also avoids conflicting head definitions.
The main parts in this file to look at are UpdatePosition and DeserializeConstruction.  
The UpdatePosition method will update the location of the object in Cinema4D. The position value is set when Deserialize is called. This gets called when another player moves one of their objects and the information is serialized and sent through to all other clients.
The DeserializeConstruction gets called when a new player joins the "game". This gets called only once and is where the player object is created. You will also notice that I am actually using the Object Type to determine what gets created. This allows us to change the Ocube in RakNetStuff.cpp to any kind of object we want.
#include "GameObject.h"

#include "RakNetworkFactory.h"
#include "NetworkIDManager.h"
#include "RakNetTime.h"
#include "GetTime.h"

#include "c4d.h"


class CompC4DObjectImpl
{
public:
CompC4DObjectImpl()
{
}

void UpdatePosition(bool localObject)
{
BaseObject *obj = static_cast(m_object->ForceGetLink());

if(obj->IsDirty(DIRTY_MATRIX) || !localObject)
{
Vector newPos = obj->GetPos();
if(newPos != m_position)
{
if(localObject)
{
m_position = newPos;
}
else
{
obj->SetPos(m_position);
obj->Message(MSG_UPDATE);
}
}
}
}

void Init(BaseDocument *pDoc, BaseObject *pObject)
{
m_doc->SetLink(pDoc);
m_object->SetLink(pObject);
}


public:
AutoAlloc  m_object;
AutoAlloc   m_doc;

Vector m_position;
};

DataStructures::Multilist GameObject::m_gameObjectList;

GameObject::GameObject()
: m_Impl(NULL)
{
lastUpdate=RakNet::GetTime();
m_gameObjectList.Push(this,__FILE__,__LINE__);
}

GameObject::~GameObject()
{
m_gameObjectList.RemoveAtKey(this,true,__FILE__,__LINE__);
gDelete(m_Impl);
}

bool GameObject::Init(BaseDocument *pDoc, BaseObject *pObject)
{
if(m_Impl)
gDelete(m_Impl);

m_Impl = gNew CompC4DObjectImpl();
if(m_Impl)
{
m_Impl->Init(pDoc, pObject);
return TRUE;
}
return FALSE;
}

RakNet::RM3QuerySerializationResult GameObject::QuerySerialization(RakNet::Connection_RM3 *destinationConnection)
{
return QuerySerialization_PeerToPeer(destinationConnection);
}

void GameObject::WriteAllocationID(RakNet::BitStream *allocationIdBitstream) const
{
allocationIdBitstream->Write(RakNet::RakString("GameObject"));
}

void GameObject::UpdatePosition(bool localObject)
{
m_Impl->UpdatePosition(localObject);
}    

void GameObject::SerializeConstruction(RakNet::BitStream *constructionBitstream, RakNet::Connection_RM3 *destinationConnection)
{
constructionBitstream->Write(m_Impl->m_object->ForceGetLink()->GetType());
constructionBitstream->Write(m_Impl->m_position);
}

bool GameObject::DeserializeConstruction(RakNet::BitStream *constructionBitstream, RakNet::Connection_RM3 *sourceConnection)
{
LONG objectType = Ocube;
Vector position;
constructionBitstream->Read(objectType);
constructionBitstream->Read(position);

BaseObject *pTemp = BaseObject::Alloc(objectType);
if(pTemp)
{
BaseDocument *doc = GetActiveDocument();
doc->InsertObject(pTemp,NULL,NULL);
m_Impl = gNew CompC4DObjectImpl();
m_Impl->Init(doc,pTemp);
m_Impl->m_position = position;
EventAdd(EVENT_FORCEREDRAW);
}

return true;
}

RakNet::RM3SerializationResult GameObject::Serialize(RakNet::SerializeParameters *serializeParameters)
{
serializeParameters->outputBitstream[0].Write(m_Impl->m_position);
return RakNet::RM3SR_BROADCAST_IDENTICALLY;
}

void GameObject::Deserialize(RakNet::DeserializeParameters *deserializeParameters)
{
deserializeParameters->serializationBitstream[0].Read(m_Impl->m_position);
}


void GameObject::PostDeserializeConstruction(RakNet::Connection_RM3 *sourceConnection)
{
}

void GameObject::PreDestruction(RakNet::Connection_RM3 *sourceConnection)
{
}

Where to from here?

You may be thinking... where to from here? Or perhaps your mind is already buzzing with ideas, I know mine is.

Collaboration

From here you could add the ability to allow the user to add more objects to their scene, perhaps have some kind of collaborative editing environment. Each user could join and add any objects they want and have all the data transmitted through to the other clients. You could serialize all the objects parameters, sphere radius, number of sides etc... You could send through Vertex data for changed points so if one user manipulates the geometry for your terrain in your game all other players see the changes. This is actually the basis behind some other engines, Cube2, Verse, Hero Engine.
Collaborative world building or objects creation directly in Cinema4D? Its possible.

Make a game

How about extending this and add keyboard controls. Throw in a real character and some terrain, perhaps some logic to throw spheres and cubes at each other and you have got yourself a FPGT (First Person Geometry Throwing) game.

Add Voice and Chat.

Dig deeper into RakNet and discover that it also has the ability to do VOIP (Voice over IP) so you could write your own Skype if you wanted to.
You could also very easily write a simple Instant Messaging system to talk to all your connected friends, which I might cover in another tutorial.

Conclusion

From this you have learned how to get RakNet integrated with Cinema4D, how to use a SceneHook to drive some networking updates and how to get a simple 2 player "move the cube" game working in Cinema4D.
I hope you enjoyed this little adventure into the use of RakNet in Cinema4D and that you found some of this useful or enlightening in some way. Feel free to add to experiment around with the code and have a deeper look at all the different parts of RakNet.

Comments

Popular posts from this blog

Creating a renderer using a video post plugin

Selecting a Game Engine

A C++ library for communicating with Amazon S3