free web stats RakNet & Irrlicht - Step 1

A Primer For RakNet Using Irrlicht

By Dave Andrews (http://www.daveandrews.org)



Intro | Step 1 | Step 2 | Step 3 | Conclusion

Step 1: The Client (Without Network Code)

This will be the longest portion of the tutorial, because we have to write a skeleton Irrlicht application as well as create the functions for networking. The server will be quite a bit simpler than this client.

The client to this program is extremely simple. When completed, the client will connect to a server, and the user can then "draw" on the window created by Irrlicht using the mouse. What is drawn by the user is immediately transmitted to the server through RakNet, and when the server receives data from one client it will automatically forward that data to every other connected client.

The client will draw the screen by storing a list of 2d lines. As the user holds down the mouse button and drags the mouse, a line is drawn from the original point of the mouse to the new point of the mouse. Those points (x1, y1, x2, y2) are then added into the list, and once the client has network ability, those values are what will be transmitted to the server.

We begin with the include headers.

#include <irrlicht.h>
#include <PacketEnumerations.h>
#include <RakNetworkFactory.h>
#include <NetworkTypes.h>
#include <RakClientInterface.h>
#include <BitStream.h>

#include <windows.h> // for Sleep()
#include
<stdio.h>
#include <conio.h>
#include <string.h>
#include <stdlib.h>

We begin by including the Irrlicht include file, and the PacketEnumerations.h, RakNetworkFactory.h, NetworkTypes.h, RakClientInterface.h, and BitStream.h are all the RakNet includes we will be using. I manually include BitStream.h here, because the RakNet FAQ states that the BitStream class sometimes has problems linking, therefore it suggests to include BitStream.h in your sources and add BitStream.cpp to your project. You may need to do that in order to compile this tutorial.

RakNet works through the careful use of threads for connections. This is why I have included windows.h in my project. The main loop of the program will be a while loop that will endlessly loop, as most game main loops are. For such a simple project, I want the main thread (the one with the endless while loop) to yield execution in order to allow the RakNet threads more time to execute, through the use of the Sleep() function. For Linux programs, use the usleep() method included with unistd.h.

In a full game client, you would not want to use the Sleep() function in your main loop. But for this simple example, and also because the server is going to run on the same machine as the client, we'll use Sleep(). We'll be using Sleep(1), which will not cause the program to pause but will cause it to relinquish the processor to another process.

Now let's allow ourselves to use the Irrlicht namespaces:

using namespace irr;
using namespace core;
using namespace scene;
using namespace video;

This just gives us quick access to Irrlicht classes without the namespace mumbo-jumbo and bunches of ::'s--Kid's stuff!

The Client Connection Class

Now on to the first major class of the Client, the ClientConnection class. The purpose of this class is to both manage all the networking stuff of RakNet, but also to manage the lines to be drawn to the screen.

The ClientConnection class contains the following functions:
  1. Constructor. The constructor for ClientConnection will take two arguments: the server ip to connect to (as a string) and the server port to connect to (also as a string). It will then handle the business of initializing RakNet's networking capabilities and connecting to the server.
  2. Destructor. The destructor will disconnect from the server that the constructor connected to, and shut down RakNet's network structure.
  3. AddLineLocal. This function will take the x and y points for a line and will add a new line to the list of lines to be drawn.
  4. SendLineToServer. This function will send the points drawn by the client to the server.
  5. DrawLines. This function will receive a pointer to Irrlicht's Video Driver object and draw all the lines stored in the line list to the local client's screen.
  6. ListenForPackets. This function checks for packets coming from the server and will automatically handle them through the next function:
  7. HandlePacket. This function will check that the incoming packet is one it's interested in, in this case a packet with information on a line to add to the screen. It will then decode the packet and add the line.
The functions we will be leaving empty for this first step are Constructor, Destructor, SendLineToServer, ListenForPackets, and HandlePacket. That being said, let's dive into the code:

class ClientConnection
{

private
:
list<line2d<s32> > lineList;


public:
ClientConnection(char * serverIP, char * portString)
{

// Will be completed in step 3

}

~ClientConnection()
{

// Will be completed in step 3

}

There's the member variables, class definition, the constructor, and the destructor for the ClientConnection class. As you can see, the constructor will take parameters for the server it needs to connect to, namely the IP and the port to use. The constructor will be doing all the connecting. The destructor below it will handle disconnecting from the server.

 void AddLineLocal(s32 x1, s32 y1, s32 x2, s32 y2)
{
line2d<s32> myLine(x1, y1, x2, y2);

lineList.push_back(myLine);
}

This function merely takes 2 points, (x1,y1) and (x2, y2) and creates a 2d line from them. It then adds that line to the locally stored list of lines. The client will keep track of all lines, either those received from the server or those input from the user, and it will draw them to the screen. That's why we maintain a list of them here.

 void SendLineToServer(s32 x1, s32 y1, s32 x2, s32 y2)
{
// Will be completed in step 3
}

This function is similar to AddLineLocal, except instead it sends the supplied line (x1,y1,x2,y2) to the server. The server should then automatically forward that data to the other clients connected, but we will cover that in Step 2.

 void DrawLines(IVideoDriver * irrVideo)
{
list<line2d<s32> >::Iterator it = lineList.begin();

for(; it != lineList.end(); ++it) {
line2d<s32> currentLine = (*it);
position2d<s32> start = position2d<s32>(currentLine.start.X, currentLine.start.Y);
position2d<s32> end = position2d<s32>(currentLine.end.X, currentLine.end.Y);

irrVideo->draw2DLine(start, end);
}
}

This is some pretty cut-and-dry Irrlicht code. An iterator is declared which will step through the list of lines contained in the object. A for loop then iterates through the list and uses Irrlicht VideoDriver to draw a 2d line to the screen using the start and end points of the stored line. This is assumed to be called between the IVideoDriver->BeginScene() and IVideoDriver->EndScene() functions, which we will do when we call this function.

 void ListenForPackets()
{

// Will be completed in step 3

}

void HandlePacket(Packet * p)
{
// Will be completed in step 3
}
};

These two functions are related. The ListenForPackets function will check to see if any packets coming from the server are waiting to be handled by the client. If there is a packet waiting, then the ListenForPackets function will grab that packet and send it to HandlePacket. HandlePacket will then decode the packet, which should be information about a line, and add a line to the list of lines to draw.

The Irrlicht Event Receiver

Irrlicht uses a class called an Event Receiver to handle all the events by the system. In the case of this chalkboard program, we are interested in mouse events. If the use presses down the mouse button, we want to know where so when he moves the mouse it can draw a line from the point he or she clicked to the new point. If the user moves the mouse and has the button held down, we want it to continue drawing lines. We will implement the OnEvent() function of an Irrlicht IEventReceiver in order to do this.

The ChalkboardEventReceiver Class

We will create a class called ChalkboardEventReceiver which will intercept events from Irrlicht. This class will contain only two functions:
  1. Constructor. The constructor will need a pointer to a ClientConnection class, in order to allow the event handler to add lines to the scene and to tell the server about any lines the user has added.
  2. OnEvent. This is the standard Irrlicht event handling function, through which all pertinent events are filtered.
We're going to implement both of these functions in this step, because they have no direct networking code in them. They will however use ClientConnection's network capabilities.

Let's start with the constructor (and the member vars):
class ChalkboardEventReceiver : public IEventReceiver
{

private:

s32 x, y;
bool mouseDown;
ClientConnection * connection;

public:
ChalkboardEventReceiver(ClientConnection * c)
: mouseDown(false), connection(NULL)
{
connection = c;
}

This constructor does absolutely nothing but initialize the variables mouseDown to false and connection to NULL. It then will remember the pointer to the ClientConnection object that is sent to it. We will make sure it receives that pointer in our main() function below. Pretty straight-forward.

The OnEvent function is a bit more complicated, so I'll stretch it out into a few different code portions.

 bool OnEvent(SEvent event)
{
if(event.EventType == EET_MOUSE_INPUT_EVENT) {
if(event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN) {
x = event.MouseInput.X;
y = event.MouseInput.Y;
mouseDown = true;

return true;
}

Here is the declaration and the first portion of OnEvent. As you can see, it receives an SEvent class from Irrlicht. The SEvent is the class which holds all the information about any possible input event from Irrlicht--mouse, keyboard, GUI, anything.

In the code above we first check to see that the incoming event is one that we would be interested in: a mouse event. We then go about to business of checking to see what type of event it is. In this first portion, we check to see if it is a mouse click (just the press-down part, this does not include letting up off the mouse button.)

If the user has pressed down the mouse button, then we want to record the position where he clicked, and also flag the mouse button as being pressed.

 else if(event.MouseInput.Event == EMIE_LMOUSE_LEFT_UP) {
connection->AddLineLocal(x, y, event.MouseInput.X, event.MouseInput.Y);
connection->SendLineToServer(x, y, event.MouseInput.X, event.MouseInput.Y);
mouseDown = false;

return true;
}

Immediately following the code above we have an else which checks to see if the event received from the user letting up on the mouse button. In the case that the user has let off the button, it means he has also pressed the button at some point. So we want to draw a line from the last remembered point to the new point where the user let off. In order to do this, we call the AddLineLocal function in our ClientConnection, the function we defined above. This will take 2 points, (x1,y1) and (x2,y2) and it will create a line between them. We send that function our 2 coordinates. Since this is also a line that the client has drawn, we want to send it to the server in order to forward that line to all the other clients connected. Therefore we also call the SendLineToServer function with these 2 points.

Even though our client has no network code yet, we can still call the networking functions in the ClientConnection because they will have no effect at all.

 else if(mouseDown && event.MouseInput.Event == EMIE_MOUSE_MOVED) {
connection->AddLineLocal(x, y, event.MouseInput.X, event.MouseInput.Y);
connection->SendLineToServer(x, y, event.MouseInput.X, event.MouseInput.Y);
x = event.MouseInput.X;
y = event.MouseInput.Y;

return true;
}
}

return false;
}
};

This is the last bit of OnEvent, completing the ChalkboardEventReceiver class. The last event type we are concerned with is if the user moves the mouse. However, we are only worried about it if the mouse is moved which the mouse button is pushed down, so we first check to see if the mouse button is down.

If this even is handled and the mouse is pushed down, then we handle it just like we did if the user had let off the mouse button. Add a line locally, then send that line to the server. This one is different, however, in that it then resets the (x1,y1) point to the current mouse position. This will allow the user to hold down the mouse button and the client will draw more lines while the mouse is moving, creating the appearance that the user is just drawing points on the screen. What the user draws are not points, but a lot of little lines, mostly just 1 or 2 pixels in length.

The main() Function and the Main Loop

The main() function for the client is pretty simple. We create a ClientConnection, an event receiver, and an IrrlichtDevice. We then enter the main loop, which draws all the lines and checks for packets. After the main loop is over, meaning the user has closed the client, we destroy the IrrlichtDevice and close out.

Let's start with everything up until the main loop.

int main()
{
ClientConnection myConnection("127.0.0.1", "10000");
ChalkboardEventReceiver myReceiver(&myConnection);
IrrlichtDevice * irrDevice = createDevice(EDT_SOFTWARE, dimension2d<s32>(300,300));
IVideoDriver * irrVideo = irrDevice->getVideoDriver();

irrDevice->setEventReceiver(&myReceiver);

With this code, we create the client connection, even receiver, and the irrlicht device. As you can see, the IP address and port to connect to are hard-coded here. You don't have to do that, in fact it's fairly stupid to keep it like that, as it can only connect to localhost through port 10000! I did not include any input functions in this tutorial for simplicity, but you can just prompt the user for an IP and port and pass them into the client connection as strings.

The IrrlichtDevice creates a small window, only 300x300. I created such a small window because this code connects to localhost, meaning I'll have to have the server running, and 2 or 3 clients going on the same machine. I kept the client window small so I'd be able to see them all at once. This can be changed, also.

After setting the event receiver, we move on to the main loop.

 while(irrDevice->run()) {
Sleep(1);

irrVideo->beginScene(true, true, SColor(0,0,0,0));
myConnection.DrawLines(irrVideo);
irrVideo->endScene();

myConnection.ListenForPackets();
}

Beautiful! This does all the magic. irrDevice->run() will return false when the user tries to close the program, so we have a loop that runs until the user wants to close it. In the beginning of the loop, we Sleep in order to yield the processor to other threads which would be running on the system (in my case, the server and several other clients.)

Had I not called Sleep, this loop would consume 100% of the processor. In an environment where I'm going to be running the server and more than one client, I want them all to share the processor nicely. For most games, you would not want a sleep() in your main loop, as it could slow down framerates and all kinds of other ill effects on the performance of your game. But for this application and tutorial I decided it would be best to not have the client use all the processor.

We then continue on to drawing all the lines on the screen, which is handled by the DrawLines() function in the ClientConnection.

After that comes the fun network stuff. For Step 1, the ListenForPackets() function does nothing. But once the client is completed in Step 3, this function will handle internally all packets coming from the server and automatically update the stored lines automatically. That's just how easy RakNet is.

 irrDevice->setEventReceiver(NULL);

irrDevice->drop();

return 0;
}

And that is the end of the main function, and of Step 1. We just reset Irrlicht's event receiver to NULL, and close down the IrrlichtDevice. The ClientConnection class we created will destruct once the function is completed, which will disconnect gracefully from the server.

And we're done with Step 1! If you take the code I have given you here and compile it, you will have a working program which acts just as the final client will, allowing you to draw lines on the screen. However, it does not yet connect to a server. So now it's up to us to write one!

Back To Introduction | On To Step 2...


The content, code, and images of this page is copyright (c) Dave Andrews. Any reproduction or redistribution of this material is prohibited without consent from the author.