Game Development Community

dev|Pro Game Development Curriculum

SimSpace - a trigger aggregator

by Orion Elenzil · 09/11/2007 (9:40 am) · 6 comments

This resource creates a new class: SimSpace, who's purpose is to act as an aggregator of triggers so that multiple triggers can be linked together to act as one.
This allows you to mark up a non-rectangular space and conveniently create behaviours for it.

To illustrate the problem,
suppose you have a U-shaped area out on some terrain, and you want to be told when players enter or leave that area. You need three triggers (say). With plain triggers, you'll have problems when the player walks quickly from one trigger to another, or stands in an area where two triggers overlap. ie, you'll get multiple onEnter() events and you'll have to figure out that some of them should be ignored.
SimSpace does this for you in a nice convenient way.

Many thanks to Bank for corrections and improvements !

USAGE
----------------------------------------------------
In the mission file, you can add simSpaces the exact same way you add simGroups -
the key point being that they can contain other objects, most notably: triggers.
Now you add a bunch of triggers to the SimSpace.
Now implement the following callbacks for your SimSpace:
function mySimSpace::onEnter(%this, %object)
{
   echo("object" SPC %object SPC "entered space" SPC %this);
}

function mySimSpace::onLeave(%this, %object)
{
   echo("object" SPC %object SPC "left space" SPC %this);
}

Note this is a bit of a different paradigm than onEnter() and onLeave() in triggers.
With triggers, onEnter() is called on the *datablock* of the trigger,
and the datablock typically contains the behavioural functionality.
That's always seemed silly to me, and i didn't do it that way.


To aggregate or not to aggregate ?
In my own application, i have certain triggers which i want to be associated with the space logically but not spacially.
For example, a trigger which opens a door when you get near the door.
I want the trigger to be logically associated with the space, because the space is where the smarts for access control live
(eg, does the player have the key required to get into this space?), but i don't want the player to have entered the space
just because they walked up to the door.
So triggers have a new field exposed to script, "aggregate", which is defaults to true, and if false then they won't do the whole SimSpace thing.



IMPLEMENTATION
----------------------------------------------------

All the source code needed is below,
but familiarity with C++ and the Torque code base is assumed.

This is based on TGE 1.3.5,
but should be fine for all known versions of TGE and TGEA.

Also two quick caveats:
first, the basic functionality seems good,
but i haven't yet tested this exhaustively.
It's going to get heavy use in my project tho,
so any serious problems will definitely bubble up.
second, this uses on a couple previous modifications i've made to stock TGE,
and i *think* i've edited it so that this should work when applied to a stock engine,
but i haven't actually checked. Please let me know any problems!



1. two new files: simSpace.h and simSpace.cc
----------------------------------------------------

put these in engine/game.

simSpace.h
#ifndef _SIMSPACE_H_
#define _SIMSPACE_H_

#include "console/simBase.h"
#include "game/gameBase.h"


class SimSpace : public SimGroup
{
// standard stuff..
private:
   typedef SimGroup Parent;
   StringTableEntry mClassName;
   StringTableEntry mSuperClassName;

public:
     SimSpace();
   ~ SimSpace();
   bool onAdd();
   void onRemove();
   DECLARE_CONOBJECT(SimSpace);
   static void initPersistFields();


// interesting stuff:
public:
   Vector<GameBase*> mObjects;
   Vector<U32      > mObjectInCounts;

   void  onObjectEnterChild(GameBase* obj);
   void  onObjectLeaveChild(GameBase* obj);

   virtual bool       isClassSimSpace    () { return true; }

};


#endif // _SIMSPACE_H_

simSpace.cc
////////////////////////////////////////////
//
// SimSpace.cc
//
// class for managing physical "spaces" such as shops, rooms, venues, etc.
// a subclass of SimGroup which aggregates [selected] child Triggers into a single meta-trigger.
//
// o. elenzil 20070829 doppelganger
// 
////////////////////////////////////////////


#include "simSpace.h"

//-----------------------------------------------------------------
// standard stuff


IMPLEMENT_CONOBJECT(SimSpace);

SimSpace::SimSpace()
{
   mClassName      = StringTable->insert("");
   mSuperClassName = StringTable->insert("");
}

SimSpace::~SimSpace()
{
}

void SimSpace::initPersistFields()
{
   addGroup("Classes");
   addField("class"     , TypeString, Offset(mClassName     , SimSpace));
   addField("superClass", TypeString, Offset(mSuperClassName, SimSpace));
   endGroup("Classes");
}


bool SimSpace::onAdd()
{
   if (!Parent::onAdd())
      return false;

   // superClassName -> SimSpace
   StringTableEntry parent = StringTable->insert("SimSpace");
   if(mSuperClassName[0]) 
   {
      if(Con::linkNamespaces(parent, mSuperClassName))
         parent = mSuperClassName;
   }
      
   // className -> superClassName
   if(mClassName[0])
   {
      if(Con::linkNamespaces(parent, mClassName))
         parent = mClassName;
   }

   // objectName -> className
   StringTableEntry objectName = getName();
   if (objectName && objectName[0])
   {
      if(Con::linkNamespaces(parent, objectName))
         parent = objectName;
   }
   
   // Store our namespace
   mNameSpace = Con::lookupNamespace(parent);

   // Call onAdd in script!
   Con::executef(this, 2, "onAdd", Con::getIntArg(getId()));
   return true;
}

void SimSpace::onRemove()
{
   // Call onRemove in script!
   Con::executef(this, 2, "onRemove", Con::getIntArg(getId()));
   Parent::onRemove();
}

//-----------------------------------------------------------------
// interesting stuff

void SimSpace::onObjectEnterChild(GameBase* obj)
{
   S32 found = -1;
   for (U32 i = 0; i < mObjects.size() && found == -1; i++)
   {
      if (mObjects[ i ] == obj)
         found = i;
   }

   if (found == -1)
   {
      // pass it on ! note we onLeave ourselves before onLeaving our parent.
      SimSpace* parentSpace = getSpace();
      if (parentSpace != NULL)
         parentSpace->onObjectEnterChild(obj);

      mObjects       .push_back(obj);
      mObjectInCounts.push_back(1  );
      Con::executef(this      , 2, "onEnter", Con::getIntArg(obj->getId()));
   }
   else
   {
      mObjectInCounts[found] += 1;
   }
}

void SimSpace::onObjectLeaveChild(GameBase* obj)
{
   S32 found = -1;
   for (U32 i = 0; i < mObjects.size() && found == -1; i++)
   {
      if (mObjects[ i ] == obj)
         found = i;
   }

   if (found == -1)
   {
      Con::logf(ConsoleLogEntry::Error, ConsoleLogEntry::General, "SimSpace::onObjectLeaveChild() - %s is not inside space %s !", obj->getDebugString(), this->getDebugString());
      return;
   }

   mObjectInCounts[found] -= 1;
   if (mObjectInCounts[found] <= 0)
   {
      mObjects       .erase(found);
      mObjectInCounts.erase(found);
      Con::executef(this      , 2, "onLeave", Con::getIntArg(obj->getId()));

      // pass it on ! note we onLeave ourselves before onLeaving our parent.
      SimSpace* parentSpace = getSpace();
      if (parentSpace != NULL)
         parentSpace->onObjectLeaveChild(obj);
   }
}


2. changes to simBase.
----------------------------------------------------
these provide a performant way to find the nearest parent simSpace of any object, and also simplify the world editor.
for more info, check out isClassSimSet.
William Todd Scott suggested that this resource might be a better way to do this,
but i haven't yet grokked that resource.

simBase.h
up near the top, add this line:
class SimSpace;

underneath this line: SimGroup* getGroup() const { return mGroup; }
add this line:

SimSpace* getSpace();

down at the bottom of the simObject class declaration, above initPersistFields(), add:
public:
   virtual bool       isClassSimGroup () { return false; }
   virtual bool       isClassSimSpace () { return false; }


down at the bottom of the simGroup class declaration, above bool processArguments(..., add:
virtual bool       isClassSimGroup() { return true; }



simBase.cc
up at the top:
#include "game/simSpace.h"

just below ConsoleMethod(SimObject, getGroup, S32, 2, 2, "obj.getGroup()") add:
ConsoleMethod(SimObject, getSpace, S32, 2, 2, "obj.getSpace()")
{
   argc; argv;
   SimSpace *space = object->getSpace();
   if(!space)
      return -1;
   return space->getId();
}


just below SimObject* SimObject::findObject(const char* ) add:
SimSpace* SimObject::getSpace()
{
   if (mGroup == NULL)
      return NULL;

   if (mGroup->isClassSimSpace())
      return static_cast <SimSpace*> (mGroup);
   else
      return mGroup->getSpace();
}

and finally down at the bottom, add:
#define ConsoleMethodIsClass(function) ConsoleMethod( SimObject, function  , bool, 2, 2, ""){return object->function();}
ConsoleMethodIsClass(isClassSimGroup )
ConsoleMethodIsClass(isClassSimSpace )
.. the previous creates a consoleMethod on all simObjects called isClassSimSpace(), which returns true if the object is a simSpace or any class derived from a simSpace.
i found this pretty useful for many other classes like simGroup, AIPlayer, datablock, etc, but do what thou wilt, as they say.
ditto isClassSimGroup(), which is used in step 4 below:




3. changes to triggers
----------------------------------------------------
trigger.h
in the class declaration of Trigger, after the line U32 mCurrTick add:
bool              mAggregate;



trigger.cc
up at the top:
#include "game/simSpace.h"

in Trigger::Trigger(), after mCurrTick = 0; add:

mAggregate = true;



after addField("polyhedron", TypeTriggerPolyhedron.... add:
addField("aggregate" , TypeBool             , Offset(mAggregate        , Trigger));



in void Trigger::potentialEnterObject(GameBase* enter), after deleteNotify(enter); and before Con::executef(mDataBlock,..., add:
if (mAggregate)
      {
         // deal with spaces.
         SimSpace* parentSpace = getSpace();
         if (parentSpace != NULL)
         {
            parentSpace->onObjectEnterChild(enter);
         }
      }

in Trigger::processTick(), *after* Con::executef(mDataBlock, 3, "onLeaveTrigger"... add:
if (mAggregate)
            {
               // deal with spaces
               SimSpace* parentSpace = getSpace();
               if (parentSpace != NULL)
               {
                  parentSpace->onObjectLeaveChild(remove);
               }
            }




4. patch up World Editor to know about our new class
----------------------------------------------------

guiTreeViewCtrl.cc
right after else if (!dStrcmp(iconString, "SimGroup")) icon = SimGroup1; add:
else if (!dStrcmp(iconString, "SimSpace"))
      icon = SimGroup1;



EditorGui.cs
change this:
%System_Item[0] = "SimGroup";
to this:
%System_Item[0] = "SimGroup";
   %System_Item[1] = "SimSpace";


objectBuilderGui.gui
after the function ObjectBuilderGui::buildSimGroup() add:
function ObjectBuilderGui::buildSimSpace(%this)
{
   %this.className = "SimSpace";
   %this.process();
}


5. optional - usage example
----------------------------------------------------
implement the following somewhere in server-side script:
function SimSpace::onEnter(%this, %object)
{
   echo("object" SPC %object SPC "entered space" SPC %this);
}

function SimSpace::onLeave(%this, %object)
{
   echo("object" SPC %object SPC "left space" SPC %this);
}




.. and that's it.

#1
08/31/2007 (10:26 am)
Hey, this is great. Just right what I need :)
Just to clarify something:

1. the changes that you say are in "simObject.cc" are actually in "simBase.cc" (as there is no simObject.cc file at all).

2. the part:
Quote:in Trigger::processTick(), *after* Con::executef(mDataBlock... add:
should be:
Quote:in Trigger::processTick(), *after* Con::executef(mDataBlock, 3, "onLeaveTrigger"... add:
instead, as there are two Con::executef() in processTick (we need the first, the "onLeaveTrigger" one).

That for those who are new with Torque, so they don't stuck at this :)

Thanks again!

Edit: the parts:
Quote:
in RecurseSelectObjectsInGroup change this:
if(%object.getclassname() $= "SimGroup")
to this:
if(%object.isClassSimGroup())
and
Quote:in ExpandSelectedAndSelectInEditorTree()
change this:
if(%object.getclassname() $= "SimGroup")
to this:
if(%object.isClassSimGroup())
are not applicable to any torque version I have (1.3>152). so, this step can be ignored (? you have different editors?).

Additionally you need to add somewhere at the end of objectBuilderGui.gui file the following:
function ObjectBuilderGui::buildSimSpace(%this)
{
   %this.className = "SimSpace";
   %this.process();
}
#2
08/31/2007 (10:46 am)
updated first comment with more instructions of script implementation.

Edit: I found that inheriting SimSpace from ScriptGroup (instead of SimGroup) gives you hell lots of more flexibility :) You just need to separate the scriptObject.cc into .h and .cc and include the scriptObject.h into SimSpace.h
#3
08/31/2007 (2:00 pm)
awesome, thanks for the corrections, bank. added in. - i'm not sure what the RecurseSelectedObjects business is. Looks like something custom we added in. Looks like it's for selecting all the objects in a SimGroup. I'd done buildSimSpace() but forgot to include it in the resource. thanks!

ScriptGroup - interesting. i'd assumed that i'd be able to do the whole ClassName thing with SimGroups, but apparently not the case ? I'll probably go that route. Thanks!
#4
08/31/2007 (4:24 pm)
Hey Bank - i integrated your suggestion about using ScriptGroup,
except that i couldn't quite get that to do what i wanted,
so i just copied the relevant functionality. - That seems to be what a bunch of other classes do.

Also, you might be interested in a small new feature added in, which is a boolean "aggregate" flag on triggers.
See new comments near the top starting with "to aggregate or not to aggregate".

Again, thanks for looking at this.
#5
08/31/2007 (6:21 pm)
This "aggregate" thingie looks suitable for my project too, nice one.

Regarding the scriptGroup, your workaround is fine as far as it works and suits your needs :)

In my project I have moved (some time ago) all declarations from scriptObject.cc into new .h file (as I have derived some classes from it), so for me it was simply telling:
...
[b]#include "console/scriptObject.h"[/b]
...
class SimSpace : public [b]ScriptGroup[/b]
{
// standard stuff..
private:
   typedef [b]ScriptGroup[/b] Parent;
...
And I have it fully functional with all the namespace-related stuff working fine. It's easier for me as scriptObject.h already here at my side :)

If someone needed: here are separated scriptObject.* files: for TGE13 and for TGE14x/TGE15x (just drop it into "engine/console/" folder, replacing current scriptObject.cc file. You can think about adding scriptObject.h file into solution project.

Edit: Oops. uploaded wrong archives. Now it's fixed.
#6
09/01/2007 (8:52 pm)
huh, nice.