best website stats

Please Donate!

I am a college student, working diligently on this project in his free time. If DustyEngine has helped you in any way, please donate just $1!

Advertisements

Tutorial 2: Expanding a Built-In Task

This tutorial is meant to show you how to build upon a task that has already been defined by the engine. This tutorial assumes you have a basic understanding of the Irrlicht 3d engine. Enough understanding to create a window, read in a mesh, and add mesh nodes and light nodes to the scene. The irrlicht code will be briefly explained but not in detail.

I won't lie to you, this is extremely looooong tutorial :) I tend to draw out my explanations. There really isn't that much code to this, and it's all very simple if you understand basic Irrlicht uses. Dusty Engine greatly simplifies code, and I believe this tutorial shows that.

Sure, the effect that this tutorial achieves could be done without Dusty Engine. But this is a simple example. Dusty Engine's power lies in its scalability, it could handle the tasks in this program and a thousand other custom tasks without problem.

In this tutorial, we are going to modify Dusty Engine's built-in MoveTask. Note that in order to use the engine, you do not have to modify an existing task. You can create new ones that do just about anything. But in this case we will be working with one that already exists.

To see the outcome of the program we are going to write, download Demo 2 above. We are going to create a scene that contains a bunch of little spheres that bounce around the screen. Those spheres are lit by 5 different lights, and each time the program runs the lights are in different random positions and the spheres have different random movement directions. The lights also change to random colors each time the program is executed.

Alright, all that said, lets get into some code. We'll begin with the code of the new task we are creating. I have called this task "BounceMoveTask" because it builds upon the DustyEngine::MoveTask task. The BounceMoveTask adds in a rect which the node position from the MoveTask is tested against. The node is made to "bounce" when it reaches the boundaries of that rect.

I have put this task into a header file "bouncingmovetask.h".

#ifndef __BOUNCINGMOVE_TASK_H__
#define __BOUNCINGMOVE_TASK_H__

#include <dustyengine.h>
#include <irrlicht.h>

The code above begins by using defines to make sure this file is only included once. It then includes the dustyengine include file and irrlicht include file. Straightforward stuff.

class BouncingMoveTask : public DustyEngine::MoveTask
{
public:
BouncingMoveTask() : MoveTask()
{
}

~BouncingMoveTask()
{
}

Above is the definition of the BouncingMoveTask task and the constructor and destructor. They do nothing except that the constructor makes sure the MoveTask constructor is called.

	void SetWidth(irr::f32 w)
{
objectWidth = w;
}

void SetBoundingBox(irr::core::rect<irr::f32> box)
{
boundingBox = box;
}

The bouncing movement task has 2 values of its own that it needs: the width of the object associated with it (the node that MoveTask handles) and a bounding rectangle. Each time the object moves, its position and width are tested against the bounding rectangle, and if the object has hit the edge of the rectangle, it will bounce off.

It was also possible to use the bounding rect that is received from the node through Irrlicht's node's function to get its bounding rect, but this technique is much simpler for this demo.

	void Reset()
{
MoveTask::Reset();
boundingBox = irr::core::rect<irr::f32>(0.0f, 0.0f, 0.0f, 0.0f);
objectWidth = 0.0f;
}

The Reset() function did not have to be implemented for this task, because I am not using a TaskManager in the main program, and the task really never has to be reset because it runs until the user closes the program. But I still implemented it to show the purpose of the Reset() function. This function returns the task to the state it was at the time it was instanciated. Namely, it resets the width and the bounding box to default values. Also, if you are inheriting from another task, make sure to call the parent class's Reset() function as well.

	void OnUpdate()
{
if(node != NULL) {
MoveTask::OnUpdate();

irr::core::vector3df position = node->getPosition();

irr::f32 offSet = objectWidth / 2;

if(position.X < boundingBox.LowerRightCorner.X + offSet ||
position.X > boundingBox.LowerRightCorner.Y - offSet) {
moveVector.X = -moveVector.X;
}

if(position.Y < boundingBox.UpperLeftCorner.X + offSet ||
position.Y > boundingBox.UpperLeftCorner.Y - offSet) {
moveVector.Y = -moveVector.Y;
}
}
}

Ahh, now here's the bread and butter of the task. The OnUpdate() function. OnUpdate() is what is called by the TaskTree when the task needs to be executed.

This task's OnUpdate will first move the node that's associated with it, by calling the MoveTask's OnUpdate function. It then moves on to check the position the node is currently at against the edges of the bounding box. OffSet is the width that has been associated with this node divided by 2. This makes sure that the very edge of the node is what gets tested against the edge. If a collision is detected on the X axis, then the moveVector (from MoveTask) has its X direction negated so it'll move in the opposite direction. The same is then done for the Y direction.

This has the desired effect of "bouncing" the object against the bounding rect.

private:
irr::core::rect<irr::f32> boundingBox;
irr::f32 objectWidth;
};

#endif

And those private variables and the endif are the entirity of the BounceMoveTask task definition. That is how easy it is to create a task in DustyEngine. You can add new functions to customize their use, and then simply implement the abstract functions (we didn't have to do OnPause/Unpause/creation/destruction for this task because they were implemented in MoveTask.)

Now to move on to the main.cpp file.

#include <dustyengine.h>
#include <irrlicht.h>
#include "bouncingmovetask.h"

using namespace irr;
using namespace core;
using namespace scene;
using namespace video;
using namespace io;
using namespace gui;
using namespace DustyEngine;

This is just the basic includes and namespaces. The namespace used in Dusty Engine is DustyEngine. Pretty simple :)

vector3df RandomVector(RandGen * randGen, float minx, float maxx, 
float miny, float maxy, float minz, float maxz)
{
return vector3df(randGen->RandFloat(minx, maxx),
randGen->RandFloat(miny, maxy),
randGen->RandFloat(minz, maxz));
}

This is a helper function which simply uses Dusty Engine's RandGen class to generate a random Irrlicht 3d vector (a vector3df) RandGen is not some special super random number generator, it simply wraps rand() into an easy-to-use class of functions.

SColor RandomColor(RandGen * randGen, int minA, int maxA, 
int minR, int maxR, int minG, int maxG, int minB, int maxB)
{
return SColor(randGen->RandInt(minA, maxA),
randGen->RandInt(minR, maxR),
randGen->RandInt(minG, maxG),
randGen->RandInt(minB, maxB));
}

This is another heper function for this program. This function will return a random color using Dusty Engine's RandGen class. The color has random values within a specified range.

void SetupCamera(IrrlichtDevice * irrDevice)
{
ICameraSceneNode * camera = irrDevice->getSceneManager()->addCameraSceneNode();
matrix4 orthoMatrix;
orthoMatrix.buildProjectionMatrixOrthoLH(16.0f, 16.0f, 2.0f, -2.0f);
camera->setProjectionMatrix(orthoMatrix);
}

Now we are getting into the meat and potatoes of this program. This is the function which will create a camera for the scene. The only confusing part of this code is that it creates an orthagonal viewing matrix for the camera. This means that the objects on the scene will not be subject to depth. An object 1000 meters away appears the same as an object 1 meter away.

Also, an orthagonal view constrains the world into a certain range. Using this code, the world in the scene will have the following ranges: x = [-8, 8] and y = [-8, 8]. This is because the total width is 16 and the total height is 16, with the point (0, 0) in the center of the screen.

You should read the tutorial on orthagonal projection using Irrlicht on Irrlicht's tutorial page, by Saigumi. Click here to view the tutorial page.

void AddLights(irr::u32 numLights, RandGen * randGen, IrrlichtDevice * irrDevice)
{
for(irr::u32 i = 0; i < numLights; i++)
irrDevice->getSceneManager()->addLightSceneNode(0, RandomVector(randGen, -8, 8, -8, 8, 0, 0),
RandomColor(randGen, 0, 255, 0, 255, 0, 255, 0, 255), 1.0f);
}

Here is a function which will add a certain number of lights to the scene. Those lights have a random position (within the range of the world defined in the orthagonal matrix above.) They also have a random color. Since the random number generator is seeded with the time in the main function a little below, the colors and locations of the lights will be new each time the program is run.

void CreateSphere(DustyDriver * driver, IrrlichtDevice * irrDevice, IAnimatedMesh * mesh)
{
IAnimatedMeshSceneNode * node;
BouncingMoveTask * bounceMoveTask;

ISceneManager * irrSceneMan = irrDevice->getSceneManager();

node = irrSceneMan->addAnimatedMeshSceneNode(mesh);
node->setScale(vector3df(0.5, 0.5, 0.5));

bounceMoveTask = new BouncingMoveTask();
bounceMoveTask->SetBoundingBox(rect<f32>(-8, 8, -8, 8));
bounceMoveTask->SetNode(node);
bounceMoveTask->SetVector(RandomVector(driver->GetRandGen(), -0.5, 0.5, -0.5, 0.5, 0.0, 0.0));
bounceMoveTask->SetWidth(1.5);

driver->GetTaskTree()->AddTask(bounceMoveTask, 0, 50);
}

This is the most important function in the program for Dusty Engine. This is the program which creates the little balls that float around the screen, and creates the task that controls them. For each ball, there is one task. That task will move the ball around the screen, and "bounce" it when it reaches the bounding box.

The first thing this function does is create a node on the scene, using the irrlicht device passed to it, created from the mesh that is passed to it. It then scales that node down, just for display purposes. The scale could be anything, I just tweaked it down to what I felt looked the best.

Next, a bouncing move task is created. After the task is added to the tree, it is forgotten about. The task tree cleans up after itself. When the tree is destroyed, it goes through all the tasks it has and drop()s them. You don't have to worry about manually dropping all your tasks. However, if you're wanting to use the task later one, you need to keep a pointer to it, and grab() it. This way, if the task is ever dropped by the task tree, it won't be deleted out of memory until you're ready to delete it.

After the bouncing move task is created, the program first assignes the bounding box to it. This bounding box matches the dimensions that were supplied earlier when we set up the camera and the orthagonal matrix. This is so the balls will bounce through the whole screen, without going off the screen.

Then the new task is told what node to use, which would be the node that had just been created. The SetNode() function came from MoveTask, so we did not have to define it when we defined the BouncingMoveTask because it was inherited.

A random vector is then given to the task to move on. Once again, this was handled in the MoveTask.

The SetWidth() function follows. To simplify this example, I did not test the bounding box of the node against the bounding box given to the task. I just gave each task a "width" with which to test against the bounding box. I tweaked this a few times to get a number I felt was good, and for the sphere mesh I used I decided 1.5 was the best number.

The most important step follows. The task is then added to the task tree, under the root node, with a priority of 50. This means that, system willing, this task will be executed every 50 milliseconds.

void AddSpheres(irr::u32 numSpheres, DustyDriver * dustyDriver, IrrlichtDevice * irrDevice, IAnimatedMesh * sphereMesh)
{
for(irr::u32 i = 0; i < numSpheres; i++) {
CreateSphere(dustyDriver, irrDevice, sphereMesh);
}
}

AddSpheres just simplifies the code a bit, because we're going to be adding multiple spheres to the scene. It just adds a certain number of spheres to the scene. I could have done all this in the main(), but I decided to do this to keep the main as short and simple as possible.

int main()
{
IrrlichtDevice * irrDevice = createDevice(EDT_OPENGL, dimension2d<s32>(800, 600), 16, false, false, false, 0);
DustyDriver * dustyDriver = new DustyDriver(irrDevice);
IVideoDriver * irrVideoDriver = irrDevice->getVideoDriver();
ISceneManager * irrSceneManager = irrDevice->getSceneManager();
RandGen * randGen = dustyDriver->GetRandGen();
TaskTree * taskTree = dustyDriver->GetTaskTree();

SetupCamera(irrDevice);

randGen->SeedTime();

AddLights(5, randGen, irrDevice);

IAnimatedMesh * mesh = irrSceneManager->getMesh("sphere.obj");

AddSpheres(20, dustyDriver, irrDevice, irrSceneManager->getMesh("sphere.obj"));

while(irrDevice->run() && taskTree->IsRunning()) {
stringw myString = "FPS: ";
myString += irrVideoDriver->getFPS();
myString += " Tasks: ";
myString += int(taskTree->NumTasks());
myString += " Triangles: ";
myString += int(irrVideoDriver->getPrimitiveCountDrawn());

irrDevice->setWindowCaption(myString.c_str());
irrVideoDriver->beginScene(true, true, SColor(0,0,0,0));
irrSceneManager->drawAll();
irrVideoDriver->endScene();

taskTree->DoUpdates();
}

dustyDriver->drop();
irrDevice->drop();

return 0;
}

And there's the most important function, the main(). The main function begins by creating an irrlicht device, and creating a dusty driver object using that IrrlichtDevice. DustyDriver is a very important object. When the driver is instanciated, it creates a TaskTree, a TimeServer, and a RandGen object. Those are the main three objects in Dusty Engine.

* The TaskTree handles execution and pausing/unpausing/execution of tasks.
* The TimeServer handles creation of TimeServerEntry objects, which are used to manage time in DustyEngine.
* The RandGen wraps around rand() to create an easy-to-use random number generator.

After the DustyDriver is created, it gets several pointers to important irrlicht and dusty objects, then moves on. The code creates 5 lights using the AddLights function above. It then loads in the sphere.obj mesh. That mesh is sent to the AddSpheres function to create 20 spheres using that mesh.

Then the main Irrilcht loop is begun. Notice that there is another condition in the Irrlicht loop (there is both irrDevice->run() and taskTree->isRunning().) This is because the task tree can be told to stop execution, and the program may want to exit when the task tree is stopped. This allows any task in the program to stop execution of the program. It does not HAVE to do so, in fact this program does not make use of that. But it is an important option, and there for flexibility.

After that a string is created to be the caption of the program showing the FPS and other important info.

The scene is then drawn using the typical Irrilcht beginScene()....drawAll()....endScene() method.

What follows that is extremely important. The task tree is told to execute all nodes that need to be executed. If that line were not executed, the spheres would just sit there and never move. This is when the BouncingMoveTask of each node is actually executed, if enough time has passed.

After the loop, the DustyDriver is dropped, the IrrDevice is dropped, and the program is exited.

That is a very simple program showing the use of Dusty Engine. This has been an extremely long tutorial, but was a joy to write. I hope I have answered any questions, and shown someone the power of this engine. I truly belive this is a very powerful tool for programmers and I plan to use it in all my Irrlicht projects.

-Dave Andrews