Collision detection using custom XML attributes

A forum to store posts deemed exceptionally wise and useful
Post Reply
Rewpparo
Posts: 3
Joined: Fri Feb 01, 2008 9:14 am

Collision detection using custom XML attributes

Post by Rewpparo »

Hi everyone ! I've been experimenting with custom XML attributes for my game project, and I thought I'd share what I've learned so far.
Unlike vitek's code, this code parses the nodes as they are loaded, and acts only depending on user parameters. It also allows for loaded objects to respond to collisions. But thank you vitek for your code, that was very valuable information to me.

Irredit is a great tool, however the first time you load an irr file, you get disappointed because you don't have collision detection in place. In this tutorial, I'm going to show you how to automatically setup collision detection in your scene based on XML attributes that you define on each node. We're going to add two custom attributes for each node : Collider, and Collidee. I don't know if those terms are proper english, collider is the moving objets, collidee is the static objets recieving the blow.
Each of these two values can be either true of false. Our code is going to read those attributes, and setup collision detection acordingly.

At the time or writing however, Irredit can't put custom attributes in your irr files. We'll have to do it ourselves. For those who don't know, irr files are XML files, and can be read with any text editor. We're going to add a <userData> ../.. </userData> tag under our mesh in the XML tree, as such :

Code: Select all

<irr_scene>
	<attributes>
		../..
	</attributes>
	<node type="cube">
		<attributes>
			../..
		</attributes>

		<userData>
			<attributes>
				<bool name="Collider" value="false" />
				<bool name="Collidee" value="true" />
			</attributes>
		</userData>

		<materials>
			<attributes>
				../..
			</attributes>
		</materials>
	</node>
</irr_scene>
Put collider to true if your mesh is supposed to react to Collision, and Collidee to true if it's supposed to be collided with.

Now for some code. We need a way to parse those attributes in our code, and setup collision detection accordingly. This is done through an interface class called ISceneUserDataSerializer. You may have noticed that the loadScene method takes two parameters : a file or path to a file, and one of those classes. During scene loading, when the parser encounters a <userData> bloc, it passes it to the userDataSerialiser that you passed as a parameter, if you did. So we're going to make our parser class, derived from ISceneUserDataSerializer :

Code: Select all

class CollisionMaker : public irr::scene::ISceneUserDataSerializer
{
	irr::IrrlichtDevice* mDevice;
	irr::scene::IMetaTriangleSelector* mSelector;
public:
	CollisionMaker(irr::IrrlichtDevice* pDev);
	irr::scene::IMetaTriangleSelector* getSelector();
	irr::io::IAttributes* createUserData (irr::scene::ISceneNode *forSceneNode); //override from
	void OnReadUserData (irr::scene::ISceneNode *forSceneNode, irr::io::IAttributes *userData);
	void LoadScene(const c8 *filename);
};
We're going to do the following : We'll use a metaSelector to add all the colliders in the scene to a single triangle selector that will be used to create animators, and we're going to add an animator with that selector to all collidees. We'll also provide an interface to get that selector so that we can setup collisions on other objets, like the camera or units.

Let's go over all the functions in the class.

The constructor will store the device pointer (we'll need it), and create an empty metaselector :

Code: Select all

CollisionMaker::CollisionMaker(irr::IrrlichtDevice* pDev)
{
	mDevice = pDev;
	mSelector = mDevice->getSceneManager()->createMetaTriangleSelector();
}
getSelector will just send the poitner to the selector we made

Code: Select all

irr::scene::IMetaTriangleSelector* CollisionMaker::getSelector()
{
	return mSelector;
}
Now the createUserData function is used when you want to save the scene. We're not going to use it, so we make a dummy :

Code: Select all

irr::io::IAttributes* CollisionMaker::createUserData (irr::scene::ISceneNode *forSceneNode)
{
	//Sending 0 caus' we don't care
	return (irr::io::IAttributes*)0;
}
Now, the good part : OnReadUserData. When the XML parser finds a userData node, it will call this function. Parameters are a pointer to the node currently being loaded, and an attributes class with the userData attributes.
For this tutorial's purpose, I will make the triangle selector from mesh if the node is octree, and from bounding box if it's something else. This is to show both methods, so you can do what you want afterwards.

Code: Select all

void CollisionMaker::OnReadUserData (irr::scene::ISceneNode *forSceneNode, irr::io::IAttributes *userData)
{
	//if the attribute is not set, it's considered false. If it's false, we don't do anything.
	if(userData->getAttributeAsBool("Collidee"))
	{
		//Detecting node type
		if(forSceneNode->getType()==irr::scene::ESNT_OCT_TREE)
		{
			//Node is an octree, let's get mesh info
			irr::io::IAttributes *attributes = mDevice->getFileSystem()->createEmptyAttributes();
			forSceneNode->serializeAttributes(attributes);
			irr::scene::IAnimatedMesh* mesh = mDevice->getSceneManager()->getMesh(attributes->getAttributeAsString("Mesh").c_str());
			attributes->drop();
			//Creating selector based on mesh
			irr::scene::ITriangleSelector* thisSelector = mDevice->getSceneManager()->createOctTreeTriangleSelector(mesh->getMesh(0), forSceneNode, 128); 
			//Adding the selector to the meta selector
			mSelector->addTriangleSelector(thisSelector); 
			thisSelector->drop();
		}
		else
		{
			//Node isn't an octree, we build a triangle selector based on bounding box
			irr::scene::ITriangleSelector* thisSelector = mDevice->getSceneManager()->createTriangleSelectorFromBoundingBox(forSceneNode);
			//And we add it to the metaSelector
			mSelector->addTriangleSelector(thisSelector);
			thisSelector->drop();
		}
	}
	if(userData->getAttributeAsBool("Collider"))
	{
		//Now we animate nodes that are supposed to respond to collisions based on the metaselector
		//create an animator using the metaselector. Collision values are arbitrary. Elipsoid is made according to mesh bounding box
		irr::scene::ISceneNodeAnimatorCollisionResponse* anim = mDevice->getSceneManager()->createCollisionResponseAnimator(
			mSelector, forSceneNode, forSceneNode->getBoundingBox().MaxEdge, 
			irr::core::vector3df(0,-1,0), irr::core::vector3df(0,0,0)); 
		//Adding the animator to the node
		forSceneNode->addAnimator(anim);
		anim->drop();
	}
}
And finally, the function to load a scene with our parser :

Code: Select all

void CollisionMaker::LoadScene(const c8 *filename)
{
	mDevice->getSceneManager()->loadScene(filename, this);
}

Now as a test, we can load a level mesh (a room for example) as an octree, and add a cube some distance above the floor. Set the level's collidee value to true, the cube's collider value to true. You'll notice the cube falling down on the floor and laying flat : Collision response.

A current bug in this code is that a node can't be collider and collidee at the same time. If you do that, it will collide with itself and won't move. To solve this, a different method should be used, where we make a different metaselector for each node, with all the nodes excluding itself. But with what you've learned, I'm sure you're going to manage that easily :)

Can't wait for user data to work in IrrEdit !
Happy coding everyone !
limvot
Posts: 20
Joined: Sun Sep 09, 2007 2:41 am

How do you use this?

Post by limvot »

This is my code, and the error message. Can you show me how to use your code? Better yet, maby, can you show me your code?

(I tried to adapt the load irrscene example for this. Using version 1.4 and code blocks svn 4861

Code: Select all

/*
Since version 1.1, Irrlicht is able to save and load
the full scene graph into an .irr file, an xml based
format. There is an editor available to edit
those files, named irrEdit on http://www.ambiera.com/irredit,
which can also be used as world and particle editor.
This tutorial shows how to use .irr files.

Lets start: Create an Irrlicht device and setup the window.
*/

#include <irrlicht.h>
#include <iostream>
using namespace irr;

#pragma comment(lib, "Irrlicht.lib")

//stuff for collison in .irr

class CollisionMaker : public irr::scene::ISceneUserDataSerializer
{
   irr::IrrlichtDevice* mDevice;
   irr::scene::IMetaTriangleSelector* mSelector;
public:
   CollisionMaker(irr::IrrlichtDevice* pDev);
   irr::scene::IMetaTriangleSelector* getSelector();
   irr::io::IAttributes* createUserData (irr::scene::ISceneNode *forSceneNode); //override from
   void OnReadUserData (irr::scene::ISceneNode *forSceneNode, irr::io::IAttributes *userData);
   void LoadScene(const c8 *filename);
};

void CollisionMaker::OnReadUserData (irr::scene::ISceneNode *forSceneNode, irr::io::IAttributes *userData)
{
   //if the attribute is not set, it's considered false. If it's false, we don't do anything.
   if(userData->getAttributeAsBool("Collidee"))
   {
      //Detecting node type
      if(forSceneNode->getType()==irr::scene::ESNT_OCT_TREE)
      {
         //Node is an octree, let's get mesh info
         irr::io::IAttributes *attributes = mDevice->getFileSystem()->createEmptyAttributes();
         forSceneNode->serializeAttributes(attributes);
         irr::scene::IAnimatedMesh* mesh = mDevice->getSceneManager()->getMesh(attributes->getAttributeAsString("Mesh").c_str());
         attributes->drop();
         //Creating selector based on mesh
         irr::scene::ITriangleSelector* thisSelector = mDevice->getSceneManager()->createOctTreeTriangleSelector(mesh->getMesh(0), forSceneNode, 128);
         //Adding the selector to the meta selector
         mSelector->addTriangleSelector(thisSelector);
         thisSelector->drop();
      }
      else
      {
         //Node isn't an octree, we build a triangle selector based on bounding box
         irr::scene::ITriangleSelector* thisSelector = mDevice->getSceneManager()->createTriangleSelectorFromBoundingBox(forSceneNode);
         //And we add it to the metaSelector
         mSelector->addTriangleSelector(thisSelector);
         thisSelector->drop();
      }
   }
   if(userData->getAttributeAsBool("Collider"))
   {
      //Now we animate nodes that are supposed to respond to collisions based on the metaselector
      //create an animator using the metaselector. Collision values are arbitrary. Elipsoid is made according to mesh bounding box
      irr::scene::ISceneNodeAnimatorCollisionResponse* anim = mDevice->getSceneManager()->createCollisionResponseAnimator(
         mSelector, forSceneNode, forSceneNode->getBoundingBox().MaxEdge,
         irr::core::vector3df(0,-1,0), irr::core::vector3df(0,0,0));
      //Adding the animator to the node
      forSceneNode->addAnimator(anim);
      anim->drop();
   }
}

void CollisionMaker::LoadScene(const c8 *filename)
{
   mDevice->getSceneManager()->loadScene(filename, this);
}

int main()
{
	// ask user for driver

	video::E_DRIVER_TYPE driverType;

	printf("Please select the driver you want for this example:\n"\
		" (a) Direct3D 9.0c\n (b) Direct3D 8.1\n (c) OpenGL 1.5\n"\
		" (d) Software Renderer\n (e) Burning's Software Renderer\n"\
		" (f) NullDevice\n (otherKey) exit\n\n");

	char i;
	std::cin >> i;

	switch(i)
	{
		case 'a': driverType = video::EDT_DIRECT3D9;break;
		case 'b': driverType = video::EDT_DIRECT3D8;break;
		case 'c': driverType = video::EDT_OPENGL;   break;
		case 'd': driverType = video::EDT_SOFTWARE; break;
		case 'e': driverType = video::EDT_BURNINGSVIDEO;break;
		case 'f': driverType = video::EDT_NULL;     break;
		default: return 1;
	}

	// create device and exit if creation failed

	IrrlichtDevice* device =
		createDevice(driverType, core::dimension2d<s32>(640, 480));

	if (device == 0)
		return 1; // could not create selected driver.

	device->setWindowCaption(L"Load .irr file example");

	video::IVideoDriver* driver = device->getVideoDriver();
	scene::ISceneManager* smgr = device->getSceneManager();

	/* Now load our .irr file.
	.irr files can store the whole scene graph including animators, materials
	and particle systems. And there is also the possibility to store arbitrary
	user data for every scene node in that file. To keep this
	example simple, we are simply loading the scene here. See the documentation
	at ISceneManager::loadScene and ISceneManager::saveScene for more information.
	So to load and display a complicated huge scene, we only need a single call
	to loadScene().
	*/

	// load the scene
    smgr->CollisionMaker::LoadScene("scene/irrc.irr");

	/*
	That was it already. Now add a camera and draw the scene
	*/

	// add a user controlled camera

	smgr->addCameraSceneNodeFPS();

	// and draw everything.

	int lastFPS = -1;

	while(device->run())
	if (device->isWindowActive())
	{
		driver->beginScene(true, true, video::SColor(0,200,200,200));
		smgr->drawAll();
		driver->endScene();

		int fps = driver->getFPS();

		if (lastFPS != fps)
		{
		  core::stringw str = L"Load Irrlicht File example - Irrlicht Engine [";
		  str += driver->getName();
		  str += "] FPS:";
		  str += fps;

		  device->setWindowCaption(str.c_str());
		  lastFPS = fps;
		}

	}

	device->drop();

	return 0;
}

[/code]

in function 'init main()':
error: 'CollisionMaker' is not a base of 'irr::scene::ISceneManager'
1 error, 0 warnings etc.
Hi me noob so help me.
vitek
Bug Slayer
Posts: 3919
Joined: Mon Jan 16, 2006 10:52 am
Location: Corvallis, OR

Post by vitek »

Code: Select all

// load the scene 
smgr->CollisionMaker::LoadScene("scene/irrc.irr");
This is a very basic misunderstanding between you and the compiler. Unfortunately for you, the compiler knows what it is doing. That is likely why you didn't get an immediate response.

Break it down and look what you've written. smgr is a pointer to an ISceneManager interface. The next thing is operator->. This means that the thing on the right of operator-> must either be a member function or a member variable of class ISceneManager. CollisionMaker::LoadScene() is the name of a member function of class CollisionMaker.

So, you have a pointer to an ISceneManager on the left of operator->, but you have a member function of an unrelated class on the right. That is against the rules of the language, so you get an error message.

What you need to do is create an instance of CollisionMaker, and then call the member function LoadScene() on that instance. Your code would be similar to this...

Code: Select all

core::vector3df v;
v.set (0, 1, 0);
Travis
Rewpparo
Posts: 3
Joined: Fri Feb 01, 2008 9:14 am

Post by Rewpparo »

As Vitek says, the error is in

Code: Select all

// load the scene
    smgr->CollisionMaker::LoadScene("scene/irrc.irr");
Replace with

Code: Select all

// load the scene
CollisionMaker mk(device);
mk.LoadScene("scene/irrc.irr");
or

Code: Select all

// load the scene
CollisionMaker mk(device);
smgr->loadScene("scene/irrc.irr", &mk);
limvot
Posts: 20
Joined: Sun Sep 09, 2007 2:41 am

Still having truoble

Post by limvot »

( i am using Rewpparo's code, i have tried boath versions. Am i not including a libary or something?)
In MSVC++ 2008
------ Rebuild All started: Project: LoadIrrFile, Configuration: Debug Win32 ------
Deleting intermediate and output files for project 'LoadIrrFile', configuration 'Debug|Win32'
Compiling...
main.cpp
Compiling manifest to resources...
Microsoft (R) Windows (R) Resource Compiler Version 6.0.5724.0
Copyright (C) Microsoft Corporation. All rights reserved.
Linking...
main.obj : error LNK2019: unresolved external symbol "public: __thiscall CollisionMaker::CollisionMaker(class irr::IrrlichtDevice *)" (??0CollisionMaker@@QAE@PAVIrrlichtDevice@irr@@@Z) referenced in function _main
..\..\bin\Win32-VisualStudio\LoadIrrFile.exe : fatal error LNK1120: 1 unresolved externals
Build log was saved at "file://c:\irrlicht-1.4\examples\IrrWithCollision\Debug\BuildLog.htm"
LoadIrrFile - 2 error(s), 0 warning(s)
========== Rebuild All: 0 succeeded, 1 failed, 0 skipped ==========


IN CODE::BLOCKS and MinGW
undefienged refference to 'CollisionMaker::CollisionMaker (irr::IrrlichtDevice*)'
undefigned reference to 'vtable for CollisionMaker

2 errors 0 warnings


Sorry for the trouble
Hi me noob so help me.
vitek
Bug Slayer
Posts: 3919
Joined: Mon Jan 16, 2006 10:52 am
Location: Corvallis, OR

Post by vitek »

It looks like you didn't add the implementation of the constructor to your project. You can just copy and paste all of the code provided by Rewpparo into main.cpp.

Travis
Post Reply