irrKlang sound emitter and listener scene nodes

Post your questions, suggestions and experiences regarding game design, integration of external libraries here. For irrEdit, irrXML and irrKlang, see the
ambiera forums

irrKlang sound emitter and listener scene nodes

Postby vitek » Fri Nov 24, 2006 8:43 pm

Basically these scene nodes automatically update the irrKlang sound listener and sound emitter positions. All you should need to do to use them...

  1. create and initialize irrKlang engine and irrlicht device
  2. create a sound listener and set it to be the child of the camera
  3. create a sound emitter.
  4. create some sounds, bind them to the emitter with emitter->addSound()


Code: Select all
// irrlicht
#include <ISceneManager.h>
#include <ISceneNode.h>
#include <IVideoDriver.h>
#include <irrArray.h>

// irrKlang
#include <ISoundEngine.h>
#include <ISound.h>

namespace irr {
namespace scene {

class CSoundEmitterSceneNode : public ISceneNode
{
public:
   //! constructor
   CSoundEmitterSceneNode(ISceneNode* parent,
                          ISceneManager* mgr,
                          s32 id = -1);

   //! destructor
   virtual ~CSoundEmitterSceneNode();

   //! This method is called just before the rendering process of the whole scene.
   virtual void OnPreRender();

   //! does nothing.
   virtual void render();

   //! This method is called just after the rendering process of the whole scene.
   virtual void OnPostRender(u32 timeMs);

   //! Just get the bounding box
   const core::aabbox3d<f32>& getBoundingBox() const;

   //! Add a sound to this emitter
   virtual void addSound(audio::ISound* sound);

   //! Remove a sound from this emitter
   virtual void removeSound(audio::ISound* sound);

   //! Remove all sounds from this emitter
   virtual void removeAllSounds();

   //! Get the number of sounds in this emitter
   virtual u32 getSoundCount() const;

   //! Get a specific sound from this emitter
   virtual audio::ISound* getSound(u32 index);

private:
   //! non-virtual used to cleanup sound resources
   void deleteAllSounds();

private:
   core::aabbox3d<f32> Box;

   // we manage more than one sound emitter
   core::array<audio::ISound*> Sounds;
};


class CSoundListenerSceneNode : public ISceneNode
{
public:
   CSoundListenerSceneNode(ISceneNode* parent,
                           ISceneManager* mgr,
                           audio::ISoundEngine* eng,
                           u32 now,
                           s32 id = -1);

   virtual ~CSoundListenerSceneNode();

public:
   //! This method is called just before the rendering process of the whole scene.
   virtual void OnPreRender();

   //! does nothing.
   virtual void render();

   //! This method is called just after the rendering process of the whole scene.
   virtual void OnPostRender(u32 timeMs);

   //! Just get the bounding box
   const core::aabbox3d<f32>& getBoundingBox() const;

private:
   // our bounding box for debug drawing
   core::aabbox3d<f32> Box;

   // the sound engine
   audio::ISoundEngine* SoundEngine;

   // doppler support
   core::vector3df PreviousPosition;
   u32 PreviousTime;
};

CSoundEmitterSceneNode::CSoundEmitterSceneNode(
      ISceneNode* parent,
      ISceneManager* mgr,
      s32 id)
   : ISceneNode(parent, mgr, id)
{
#ifdef _DEBUG
   setDebugName("CSoundEmitterSceneNode");
#endif

   setAutomaticCulling(false);
}

CSoundEmitterSceneNode::~CSoundEmitterSceneNode()
{
   deleteAllSounds();
}

void CSoundEmitterSceneNode::OnPreRender()
{
   if (IsVisible)
      SceneManager->registerNodeForRendering(this, ESNRP_SOLID);

   ISceneNode::OnPreRender();
}

void CSoundEmitterSceneNode::render()
{
   // for debug purposes only, draw a blue box for audio listeners
   if (DebugDataVisible)
   {
      video::IVideoDriver* driver = SceneManager->getVideoDriver();
      driver->setTransform(video::ETS_WORLD, AbsoluteTransformation);

      video::SMaterial m;
      m.Lighting = false;
      driver->setMaterial(m);

      driver->draw3DBox(Box, video::SColor(255, 0, 0, 255));
   }
}

void CSoundEmitterSceneNode::OnPostRender(u32 timeMs)
{
   ISceneNode::OnPostRender(timeMs);

   // would be nice if irrKlang supported velocity/doppler for sounds

   // update the sounds source positions
   const core::vector3df position = getAbsolutePosition();

   u32 s;
   for (s = 0; s < Sounds.size(); ++s)
      Sounds[s]->setPosition(position);
}

const core::aabbox3d<f32>& CSoundEmitterSceneNode::getBoundingBox() const
{
   return Box;
}

void CSoundEmitterSceneNode::addSound(audio::ISound* sound)
{
   // you can't insert the same sound multiple times
   s32 e = Sounds.binary_search(sound);
   if (e != -1)
      return;

   Sounds.push_back(sound);
   sound->grab();

   // make sure the sound is at the correct position
   const core::vector3df position = getAbsolutePosition();
   sound->setPosition(position);
}

void CSoundEmitterSceneNode::removeSound(audio::ISound* sound)
{
   s32 s = Sounds.binary_search(sound);
   if (s != -1)
   {
      Sounds.erase(s);
      sound->drop();
   }
}

void CSoundEmitterSceneNode::removeAllSounds()
{
   deleteAllSounds();
}

u32 CSoundEmitterSceneNode::getSoundCount() const
{
   return Sounds.size();
}

audio::ISound* CSoundEmitterSceneNode::getSound(u32 index)
{
   return Sounds[index];
}

void CSoundEmitterSceneNode::deleteAllSounds()
{
   u32 s;
   for (s = 0; s < Sounds.size(); ++s)
      Sounds[s]->drop();

   Sounds.clear();
}

CSoundListenerSceneNode::CSoundListenerSceneNode(ISceneNode* parent,
                                                 ISceneManager* mgr,
                                                 audio::ISoundEngine* eng,
                                                 u32 now,
                                                 s32 id)
   : ISceneNode(parent, mgr, id)
   , SoundEngine(eng)
   , PreviousTime(now)
{
#ifdef _DEBUG
   setDebugName("CSoundListenerSceneNode");
#endif

   if (SoundEngine)
      SoundEngine->grab();

   setAutomaticCulling(false);
}

CSoundListenerSceneNode::~CSoundListenerSceneNode()
{
   if (SoundEngine)
      SoundEngine->drop();
}

void CSoundListenerSceneNode::OnPreRender()
{
   // listener is always rendered!
   if (SoundEngine)
      SceneManager->registerNodeForRendering(this, ESNRP_SOLID);

   ISceneNode::OnPreRender();
}

void CSoundListenerSceneNode::render()
{
   // for debug purposes only, draw a red box for audio listener
   if (DebugDataVisible)
   {
      video::IVideoDriver* driver = SceneManager->getVideoDriver();
      driver->setTransform(video::ETS_WORLD, AbsoluteTransformation);

      video::SMaterial m;
      m.Lighting = false;
      driver->setMaterial(m);

      driver->draw3DBox(Box, video::SColor(255, 255, 0, 0));
   }
}

void CSoundListenerSceneNode::OnPostRender(u32 timeMs)
{
   // update our transformation matrix
   ISceneNode::OnPostRender(timeMs);

   // get the up vector
   core::vector3df upwards(0.f, 1.f, 0.f);
   getAbsoluteTransformation().rotateVect(upwards);

   // and forward vector
   core::vector3df forwards(0.f, 0.f, 1.f);
   getAbsoluteTransformation().rotateVect(forwards);

   // do doppler
   const f32 elapsed = (timeMs - PreviousTime) / 1000.f;
   PreviousTime = timeMs;

   // do position/velocity
   const core::vector3df position = getAbsolutePosition();
   const core::vector3df velocity = (position - PreviousPosition) / elapsed;
   PreviousPosition = position;

   // feed position, velocity and orientation information to sound engine
   SoundEngine->setListenerPosition(position, forwards, velocity, upwards);
}

const core::aabbox3d<f32>& CSoundListenerSceneNode::getBoundingBox() const
{
   return Box;
}

} // namespace scene
} // namespace irr


Here is an example of how to use it...

Code: Select all
#include <irrlicht.h>
#pragma comment(lib, "irrlicht.lib")

#include <irrklang.h>
#pragma comment(lib, "irrklang.lib")

using namespace irr;

int main()
{
  IrrlichtDevice *device =
    createDevice(video::EDT_OPENGL, core::dimension2d<s32>(640, 480), 16);
  if (!device)
    return 0;

  audio::ISoundEngine* engine = audio::createIrrKlangDevice();
  if (!engine)
    return 0;

  device->setWindowCaption(L"Irrlicht/IrrKlang demo");

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

  // note!!!
  //
  // modified version of example.irr has a node with name 'sphere'
  //
  smgr->loadScene("../../media/example.irr");

  scene::ISceneNode* camera = smgr->addCameraSceneNodeFPS();

  scene::CSoundListenerSceneNode* listener =
    new scene::CSoundListenerSceneNode(camera, smgr, engine, device->getTimer()->getRealTime());
  listener->setDebugDataVisible(true);

  scene::ISceneNode* node = smgr->getSceneNodeFromName("sphere");
  if (node)
  {
    scene::CSoundEmitterSceneNode* emitter =
      new scene::CSoundEmitterSceneNode(node, smgr);
    emitter->setDebugDataVisible(true);

    audio::ISound* sound =
      engine->play3D("../../media/irrlichttheme.ogg", node->getAbsolutePosition(), true, false, true);
    sound->setMinDistance(10.f);
    sound->setMaxDistance(100.f);

    emitter->addSound(sound);
  }

  while(device->run())
  {
    if (driver->beginScene(true, true, video::SColor(255,100,101,140)))
    {
      smgr->drawAll();

      driver->endScene();
    }
  }

  device->drop();

  return 0;
}


Travis
Last edited by vitek on Sat Dec 23, 2006 11:00 pm, edited 2 times in total.
User avatar
vitek
Bug Slayer
 
Posts: 3919
Joined: Mon Jan 16, 2006 10:52 am
Location: Corvallis, OR

Postby Anteater » Sun Dec 03, 2006 10:10 pm

Cool. What's the license?
User avatar
Anteater
 
Posts: 266
Joined: Thu Jun 01, 2006 4:02 pm
Location: Earth

Postby vitek » Sun Dec 03, 2006 10:35 pm

For you $50. Everyone else can do as they wish. Kidding... irrKlang has its own license, and the above code is free for the taking.
User avatar
vitek
Bug Slayer
 
Posts: 3919
Joined: Mon Jan 16, 2006 10:52 am
Location: Corvallis, OR

Postby hybrid » Sun Dec 03, 2006 11:53 pm

Oh yeah, that's definitely a brilliant addition. Now the only thing that's missing is a Linux/OSX port of irrKlang. I guess it's now time to add the 'contributions' directory for extensions which are not usable without external files. But just as the true type scene node it's a pretty useful thing and we can more easily keep those things in sync if they are tagged as officially supported.
hybrid
Admin
 
Posts: 14143
Joined: Wed Apr 19, 2006 9:20 pm
Location: Oldenburg(Oldb), Germany

Postby sio2 » Sat Jan 13, 2007 11:31 am

This is also a nice example of Irrlicht internals and how to use Irrlicht interfaces to extend functionality (without having to edit the source and recompile the dll).

I got it working pretty easily, with a few minor niggles, and it seems to work really well with irrKlang 0.4.

I lumped all the code into one "main.cpp" but if I were using it in a project I'd separate the class into its own cpp file and header file. A minor point and easy enough to do.

I'm using SVN rev. 406 and the debug settings have changed from a bool type to an enumerated type. Once again, simple enough to modify.

I had no sound to begin with and was scratching my head until I realised I'd named my sphere scene node "Sphere" instead of "sphere". :roll:

A strange issue: the sphere node in example.irr is unnamed so I opened it in irrEdit 0.6, gave the sphere a name (so the example code could find it by name) and saved the scene as example_sphere.irr. When I ran my app it was as though all scene nodes had debug data enabled (wireframe/billboards). Opening example.irr in Notepad, manually adding the name and saving to example_sphere.irr showed the scene just fine in my app, though. :?
sio2
Competition winner
 
Posts: 1003
Joined: Thu Sep 21, 2006 5:33 pm
Location: UK

Postby Midnight » Wed Feb 28, 2007 10:21 pm

sounds like an irredit bug.

haven't tried this yet but knowing vitek this is an awsome addition.

thanks again dude.
User avatar
Midnight
 
Posts: 1772
Joined: Fri Jul 02, 2004 2:37 pm
Location: Wonderland

Update it for Irrlicht 1.3 pleeeeaaaaaase

Postby eviral » Thu Mar 29, 2007 6:52 pm

Hello Vitek,

Nice wrapper !

Could you please update it for Irrlicht 1.3 ?

OnPreRender doesn't exist anymore for example...

Thanks a lot !

Eviral
eviral
 
Posts: 91
Joined: Mon Oct 25, 2004 10:25 am

Postby vitek » Thu Mar 29, 2007 7:08 pm

Lots of people still use Irrlicht 1.2 or earlier. You can easily fix this yourself using search and replace. OnPreRender becomes OnRegisterSceneNode and OnPostRender becomes OnAnimate.
User avatar
vitek
Bug Slayer
 
Posts: 3919
Joined: Mon Jan 16, 2006 10:52 am
Location: Corvallis, OR

VERSION FOR IRRLICHT 1.3 IS HERE

Postby eviral » Thu Mar 29, 2007 7:43 pm

Post updated...

HERE IS THE FINAL VERSION 100% WORKING WITH IRRLICHT 1.3 :

// irrlicht
#include <ISceneManager.h>
#include <ISceneNode.h>
#include <IVideoDriver.h>
#include <irrArray.h>

// irrKlang
#include <ISoundEngine.h>
#include <ISound.h>

namespace irr {
namespace scene {

class CSoundEmitterSceneNode : public ISceneNode
{
public:
//! constructor
CSoundEmitterSceneNode(ISceneNode* parent,
ISceneManager* mgr,
s32 id = -1);

//! destructor
virtual ~CSoundEmitterSceneNode();

//! This method is called just before the rendering process of the whole scene.
virtual void OnRegisterSceneNode();

//! does nothing.
virtual void render();

//! This method is called just after the rendering process of the whole scene.
virtual void OnAnimate(u32 timeMs);

//! Just get the bounding box
const core::aabbox3d<f32>& getBoundingBox() const;

//! Add a sound to this emitter
virtual void addSound(audio::ISound* sound);

//! Remove a sound from this emitter
virtual void removeSound(audio::ISound* sound);

//! Remove all sounds from this emitter
virtual void removeAllSounds();

//! Get the number of sounds in this emitter
virtual u32 getSoundCount() const;

//! Get a specific sound from this emitter
virtual audio::ISound* getSound(u32 index);

private:
//! non-virtual used to cleanup sound resources
void deleteAllSounds();

private:
core::aabbox3d<f32> Box;

// we manage more than one sound emitter
core::array<audio::ISound*> Sounds;
};


class CSoundListenerSceneNode : public ISceneNode
{
public:
CSoundListenerSceneNode(ISceneNode* parent,
ISceneManager* mgr,
audio::ISoundEngine* eng,
u32 now,
s32 id = -1);

virtual ~CSoundListenerSceneNode();

public:
//! This method is called just before the rendering process of the whole scene.
virtual void OnRegisterSceneNode();

//! does nothing.
virtual void render();

//! This method is called just after the rendering process of the whole scene.
virtual void OnAnimate(u32 timeMs);

//! Just get the bounding box
const core::aabbox3d<f32>& getBoundingBox() const;

private:
// our bounding box for debug drawing
core::aabbox3d<f32> Box;

// the sound engine
audio::ISoundEngine* SoundEngine;

// doppler support
core::vector3df PreviousPosition;
u32 PreviousTime;
};

CSoundEmitterSceneNode::CSoundEmitterSceneNode(
ISceneNode* parent,
ISceneManager* mgr,
s32 id)
: ISceneNode(parent, mgr, id)
{
#ifdef _DEBUG
setDebugName("CSoundEmitterSceneNode");
#endif

setAutomaticCulling(irr::scene::E_CULLING_TYPE::EAC_OFF);
}

CSoundEmitterSceneNode::~CSoundEmitterSceneNode()
{
deleteAllSounds();
}

void CSoundEmitterSceneNode::OnRegisterSceneNode()
{
if (IsVisible)
SceneManager->registerNodeForRendering(this, ESNRP_SOLID);

//ISceneNode::OnPreRender();
ISceneNode::OnRegisterSceneNode();

}

void CSoundEmitterSceneNode::render()
{
// for debug purposes only, draw a blue box for audio listeners
if (DebugDataVisible)
{
video::IVideoDriver* driver = SceneManager->getVideoDriver();
driver->setTransform(video::ETS_WORLD, AbsoluteTransformation);

video::SMaterial m;
m.Lighting = false;
driver->setMaterial(m);

driver->draw3DBox(Box, video::SColor(255, 0, 0, 255));
}
}

void CSoundEmitterSceneNode::OnAnimate(u32 timeMs)
{
//ISceneNode::OnPostRender(timeMs);
ISceneNode::OnAnimate(timeMs);

// would be nice if irrKlang supported velocity/doppler for sounds

// update the sounds source positions
const core::vector3df position = getAbsolutePosition();

u32 s;
for (s = 0; s < Sounds.size(); ++s)
Sounds[s]->setPosition(position);
}

const core::aabbox3d<f32>& CSoundEmitterSceneNode::getBoundingBox() const
{
return Box;
}

void CSoundEmitterSceneNode::addSound(audio::ISound* sound)
{
// you can't insert the same sound multiple times
s32 e = Sounds.binary_search(sound);
if (e != -1)
return;

Sounds.push_back(sound);
sound->grab();

// make sure the sound is at the correct position
const core::vector3df position = getAbsolutePosition();
sound->setPosition(position);
}

void CSoundEmitterSceneNode::removeSound(audio::ISound* sound)
{
s32 s = Sounds.binary_search(sound);
if (s != -1)
{
Sounds.erase(s);
sound->drop();
}
}

void CSoundEmitterSceneNode::removeAllSounds()
{
deleteAllSounds();
}

u32 CSoundEmitterSceneNode::getSoundCount() const
{
return Sounds.size();
}

audio::ISound* CSoundEmitterSceneNode::getSound(u32 index)
{
return Sounds[index];
}

void CSoundEmitterSceneNode::deleteAllSounds()
{
u32 s;
for (s = 0; s < Sounds.size(); ++s)
Sounds[s]->drop();

Sounds.clear();
}

CSoundListenerSceneNode::CSoundListenerSceneNode(ISceneNode* parent,
ISceneManager* mgr,
audio::ISoundEngine* eng,
u32 now,
s32 id)
: ISceneNode(parent, mgr, id)
, SoundEngine(eng)
, PreviousTime(now)
{
#ifdef _DEBUG
setDebugName("CSoundListenerSceneNode");
#endif

if (SoundEngine)
SoundEngine->grab();

setAutomaticCulling(irr::scene::E_CULLING_TYPE::EAC_OFF);
}

CSoundListenerSceneNode::~CSoundListenerSceneNode()
{
if (SoundEngine)
SoundEngine->drop();
}

void CSoundListenerSceneNode::OnRegisterSceneNode()
{
// listener is always rendered!
if (SoundEngine)
SceneManager->registerNodeForRendering(this, ESNRP_SOLID);

//ISceneNode::OnPreRender();
ISceneNode::OnRegisterSceneNode();
}

void CSoundListenerSceneNode::render()
{
// for debug purposes only, draw a red box for audio listener
if (DebugDataVisible)
{
video::IVideoDriver* driver = SceneManager->getVideoDriver();
driver->setTransform(video::ETS_WORLD, AbsoluteTransformation);

video::SMaterial m;
m.Lighting = false;
driver->setMaterial(m);

driver->draw3DBox(Box, video::SColor(255, 255, 0, 0));
}
}

void CSoundListenerSceneNode::OnAnimate(u32 timeMs)
{
// update our transformation matrix
//ISceneNode::OnPostRender(timeMs);
ISceneNode::OnAnimate(timeMs);

// get the up vector
core::vector3df upwards(0.f, 1.f, 0.f);
getAbsoluteTransformation().rotateVect(upwards);

// and forward vector
core::vector3df forwards(0.f, 0.f, 1.f);
getAbsoluteTransformation().rotateVect(forwards);

// do doppler
const f32 elapsed = (timeMs - PreviousTime) / 1000.f;
PreviousTime = timeMs;

// do position/velocity
const core::vector3df position = getAbsolutePosition();
const core::vector3df velocity = (position - PreviousPosition) / elapsed;
PreviousPosition = position;

// feed position, velocity and orientation information to sound engine
SoundEngine->setListenerPosition(position, forwards, velocity, upwards);
}

const core::aabbox3d<f32>& CSoundListenerSceneNode::getBoundingBox() const
{
return Box;
}

} // namespace scene
} // namespace irr
Last edited by eviral on Sat Mar 31, 2007 5:58 pm, edited 1 time in total.
eviral
 
Posts: 91
Joined: Mon Oct 25, 2004 10:25 am

Postby vitek » Thu Mar 29, 2007 9:06 pm

You need to replace _all_ instances of the strings as mentioned above. You should replace OnPreRender with OnRegisterSceneNode and you should replace OnPostRender with OnAnimate. The old names [OnPreRender and OnPostRender] will be completely removed if you did it right, and you won't have to add a fake timeMs in there anywhere.

Travis
User avatar
vitek
Bug Slayer
 
Posts: 3919
Joined: Mon Jan 16, 2006 10:52 am
Location: Corvallis, OR

Ok but what about timeMs ???

Postby eviral » Fri Mar 30, 2007 7:03 pm

Ok, i've replace all instances of onPreRender and onPostRender as you told me but about the fake TimeMs I don't know how to dot because OnRegisterSceneNode() doesn't have any parameter.

timeMs is now a parameter of onAnimate.

Do i need to move the part of code with TimeMs into OnAnimate instead of OnRegisterSceneNode ?


Please help, i really need your class working with IrrLicht 1.3.

Thanks

Eviral



void CSoundListenerSceneNode::OnRegisterSceneNode()
{
// update our transformation matrix
//ISceneNode::OnPostRender(timeMs);
ISceneNode::OnRegisterSceneNode();

// get the up vector
core::vector3df upwards(0.f, 1.f, 0.f);
getAbsoluteTransformation().rotateVect(upwards);

// and forward vector
core::vector3df forwards(0.f, 0.f, 1.f);
getAbsoluteTransformation().rotateVect(forwards);

irr::u32 timeMs = 5; // TEMP HARD CODED TIME

// do doppler
const f32 elapsed = (timeMs - PreviousTime) / 1000.f;
PreviousTime = timeMs;

// do position/velocity
const core::vector3df position = getAbsolutePosition();
const core::vector3df velocity = (position - PreviousPosition) / elapsed;
PreviousPosition = position;

// feed position, velocity and orientation information to sound engine
SoundEngine->setListenerPosition(position, forwards, velocity, upwards);
}
eviral
 
Posts: 91
Joined: Mon Oct 25, 2004 10:25 am

Postby vitek » Fri Mar 30, 2007 11:39 pm

I'm losing my patience. I said to replace OnPreRender() with OnRegisterSceneNode(). Neither of them take a timeMs parameter, so there is no problem. Any problem that you are running into you've created yourself. You are replacing OnPostRender() with OnRegisterSceneNode() and that is just plain wrong. If the function takes a timeMs parameter, it should be renamed to OnAnimate(). If it doesn't, then it should be renamed to OnRegisterSceneNode().

I suggest you go back to the original code at the top of this thread. Copy and paste the original code above into your source code editor. Do the search and replace as I've suggested three times. It will work. Once you get it working, please come back here and delete the bad code that you have repeatedly pasted so that others don't have to wade through the confusion.

Travis
User avatar
vitek
Bug Slayer
 
Posts: 3919
Joined: Mon Jan 16, 2006 10:52 am
Location: Corvallis, OR

Sorry Sorry Sorry

Postby eviral » Sat Mar 31, 2007 5:57 pm

ok, ok, sorry...

It works well now...

Thanks a lot
eviral
 
Posts: 91
Joined: Mon Oct 25, 2004 10:25 am

Postby MasterGod » Mon Mar 10, 2008 12:34 am

vitek, your scene nodes are a great addition to my engine. They work perfectly and they are exactly what I needed.
Thanks a lot.
Thanks eviral too for converting to newer edition of Irrlicht.
Image
Dev State: Abandoned (For now..)
Requirements Analysis Doc: ~87%
UML: ~0.5%
User avatar
MasterGod
 
Posts: 2061
Joined: Fri May 25, 2007 8:06 pm
Location: Israel


Return to Game Programming

Who is online

Users browsing this forum: No registered users and 1 guest