Skip to main content
gamedev.net
🔒 Locked

Pluggable Game Objects

Started by MaulingMonkey 18 replies 2.7k views
Original Post
MaulingMonkey
MaulingMonkey
I''m trying to determine the best organization for a pluggable game object system. That is, the game object mechanics are stored in .dll (or .so on linux) files, allowing the creation of new object classes without recompiling the main program. Now, I''ll program the plugin system using LoadLibrary and whatever that other function is on Win32 (or on linux, dlopen/dlsym) to load the object factory which is stored in the DLL. Now I have to cut a few corners to make the solution work on both linux and windows, but I''d like feedback (if any) on the following solution: (uhg, been awhile since I''ve posted... lets toss a couple tags in and hope one works)
   
//-- example.godef.cc --

extern "C" {

//null terminated list of game objects

const char * GOR_Names[] = { "Spaceship" , "Asteroid" , 0 };

//object specific functions

object * GOR_CreateSpaceship( void ) { /*...*/ }
object * GOR_CreateAsteroid( void ) { /*...*/ }
void GOR_DeleteSpaceship( object * ) { /*...*/ }
void GOR_DeleteAsteroid( object * ) { /*...*/ }
void GOR_TickSpaceship( object * ) { /*...*/ }
void GOR_TickAsteroid( object * ) { /*...*/ }

}
//-- eof --

It''s slightly inelegant due to the listing of the names of each object type... is there a way to list all the symbols exported by a dll/so file? That would remove that nuisance. Other suggestions/improvements greatly appreciated.
SiCrane
SiCrane
Instead of having GOR_CreateSpaceship() and GOR_CreateAsteroid(), why not have a single GOR_Create() function that will return a different object based on a type id? As it stands, in order use a new type of object you''ll either have to recompile the executable or do some evil looking code to strip the symbols out of the dynamic library. (Which is possible in both Windows and Linux, but usually indicates bad design if you have to do it in either.)
MaulingMonkey
MaulingMonkey
quote:
Original post by SiCrane
Instead of having GOR_CreateSpaceship() and GOR_CreateAsteroid(), why not have a single GOR_Create() function that will return a different object based on a type id? As it stands, in order use a new type of object you''ll either have to recompile the executable or do some evil looking code to strip the symbols out of the dynamic library. (Which is possible in both Windows and Linux, but usually indicates bad design if you have to do it in either.)


Simple: more maintinence when creating new objects. You have to write the code to check the typeid, then pass it to the correct function or block of code, and make sure you use the correct typeids across the board.

The code wouldn''t be very evil, a simple statement (wrapped in a GameObject class inside the server) would be something like (on linux):

[code]
class GameObjectDefinition
{
private:
void *dlhandle;
std::string defname;
public:
GameObjectDefinition( std::string name , void * handle ) : defname( name ) , dlhandle( handle ) {}
GameObject * Create( void )
{
static GameObject * (*CreatFunc)(void) = ((GameObject *)(*)(void)) dlsym( dlhandle , ((std::string)"GOR_Create" + defname).c_str() );
return CreateFunc();
}
};
[/code]

with similar functions for Delete and so forth, reference checking on the library instance, and so forth, it''d be complete.

We have to use these functions anyways, since we''re loading multiple dlls, not just one, containing game objects.
Houdini
Houdini
quote:
Original post by SiCrane
Instead of having GOR_CreateSpaceship() and GOR_CreateAsteroid(), why not have a single GOR_Create() function that will return a different object based on a type id? As it stands, in order use a new type of object you''ll either have to recompile the executable or do some evil looking code to strip the symbols out of the dynamic library. (Which is possible in both Windows and Linux, but usually indicates bad design if you have to do it in either.)

Might as well take this one step further and create an object factory and have the DLL''s register new objects with this object factory. Makes it easy to dynamically add new items (as you said) yet also allows you to have multiple DLLs add seperate new items.


- Houdini
Houdini
Maega
Maega
I got C++ for Game Programmers for Christmas and it has a chapter on abstract interfaces. Chapter 10. Check it out at a book store



[edited by - Maega on December 26, 2003 7:38:20 PM]
MaulingMonkey
MaulingMonkey
quote:
Original post by Anonymous Poster
i''m kinda hoping that someone will prove me wrong here but..
i don''t think it''s possible to import data from a dynamicly loaded dll in win32, so the GOR_Names thing won''t work.


Here, I''ll prove you wrong... I know I''ve seen a dlopen/sym wrapper that works on both win32 and linux... *dives into msdn*
Here we go.. definition of LoadLibrary:

HMODULE LoadLibrary( LPCTSTR lpFileName );

Definition of GetProcAddress (1st quick description):
The GetProcAddress function the address of an exported function or variable from the specified dynamic-link library (DLL).

Syntax:
FARPROC GetProcAddress( HMODULE hModule , LPCSTR lpProcName );

An example can be found here: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dllproc/base/using_run_time_dynamic_linking.asp

------------

Hondi: ? Not sure I quite follow... you suggesting that I have a single class that would look something like this?:


class cGOManager
{
public:
void LoadGOPlugin( std::string filename );
object * Create( std::string object );
void Destroy( object * );
};


with ''object'' here being a wrapper around the DLL class ''object'' that also keeps track of type id to make destructor lookup simple?
-----------------
Maega: dude, progamming books sell for the same price as babies!! Well, at least from the perspective of a broke colledge student . I''ve allready got a bookshelf which is about to collapse underneath the weight of programming books.

....


say that isn''t a small purple book is it? *runs upstairs to check if it is*
jeffzi
jeffzi
at the very beginning you mention wanting to list the exported functions from a .dll... this is possible using link.exe that ships with MS VC++.

if you do ''link.exe -dump -exports [binary name]'' it will show you all exported functions.

so, yes it''s possible. i''ve seen an example on how to do this before, but i don''t remember where. if i can find it i''ll post that as well.
MaulingMonkey
MaulingMonkey
quote:
Original post by jeffzi
at the very beginning you mention wanting to list the exported functions from a .dll... this is possible using link.exe that ships with MS VC++.

if you do ''link.exe -dump -exports [binary name]'' it will show you all exported functions.

so, yes it''s possible. i''ve seen an example on how to do this before, but i don''t remember where. if i can find it i''ll post that as well.


hmm... and linux has "nm [binary name]" (and I''m sure there''s a flag for exports only).

All I gotta do is find sourcecode examples now... although I suppose I could parse a pipe from these programs.

Anyhoo, thanks for the feedback .
superpig
superpig
quote:
Original post by MaulingMonkey
Maega: dude, progamming books sell for the same price as babies!! Well, at least from the perspective of a broke colledge student . I''ve allready got a bookshelf which is about to collapse underneath the weight of programming books.


Got no public libraries in your area? It''s funny how many people overlook those.

As for the whole approach... given that every game object will supply certain functions (''Create,'' ''Delete,'' and ''Tick,'' from your example) I''d recommend using classes for this. The ''interface'' class approach:


class IGameObject
{
virtual void Tick()=0;
virtual ~IGameObject() {}
};

/* in your DLL */
class CSpaceship : public IGameObject
{
CSpaceship();
void Tick() { /* ... code ... */ }
~CSpaceship(){}

static IGameObject *FactoryFunction()
{ return new CSpaceship(); }
}

void __declspec(dllexport) RegisterCreationFunctions(std::map<std::string, IGameObject *(*)()> &objectMap)
{
objectMap["spaceship"] = CSpaceship::FactoryFunction;
objectMap["asteroid"] = CAsteroid::FactoryFunction;
}

Etcetera. The game itself passes that map into the assorted DLLs (by GetProcAddress''ing the RegisterCreationFunctions function), and the DLL is then responsible for adding it''s own classes to the map. This is not too far removed from the approach Quake/HL takes to game object (a.k.a. ''entities'') - a given entity type has a ''classname'' (info_player_start, monster_headcrab, etc) which the mapfile uses to specify types of objects. Here, your classname is what''s used to key the map (''spaceship'', ''asteroid,'' etc) and then you can actually *create* those objects by looking up the factory functions in the map - IGameObject *newObject = (objectMap[className])();.

Richard "Superpig" Fine
- saving pigs from untimely fates, and when he''s not doing that, runs The Binary Refinery.
Enginuity1 | Enginuity2 | Enginuity3 | Enginuity4 | Enginuity5
ry. .ibu cy. .y''ybu. .abu ry. dy. "sy. .ubu py. .ebu ry. py. .ibu gy." fy. .ibu ny. .ebu
"Don''t document your code; code your documentation." -me
"I''m not a complete idiot; parts of me are missing." -}}i{{
Richard "Superpig" Fine - saving pigs from untimely fates - Microsoft DirectX MVP 2006/2007/2008/2009
"Shaders are not meant to do everything. Of course you can try to use it for everything, but it's like playing football using cabbage." - MickeyMouse
MaulingMonkey
MaulingMonkey
There used to be a public library literally a block away. It''s getting demolished right now (to be replaced with a bigger/better one) but I''ve checked there before... 1 or 2 books related to MFC, the rest being about html... oh yeah and ''windows for dummies'' and that sort of thing. There also might have been a single book about java. Maybe.

I like your (superpig''s) way except for the forced vtable lookup... with at least a few accessor functions that assume object type, since each object type will be on different stacks, iterating through a list of such objects will be sped up. Albiet microscopicly, but still sped up.

Now all I got to do is track down what the problem is within my linker... for some reason it refuses to resolve opengl functions even though I''m linking the library...
Houdini
Houdini
Better to hide all that in a generic object factory class, like so:


typedef ObjectFactory<IGameObject, std::string> GameObjectFactory;

void __declspec(dllexport) RegisterClasses(GameObjectFactory &object_factory)
{
object_factory.Register<CSpaceship>("spaceship");
object_factory.Register<CAsteroid>("asteroid");
}

This way you don't need to worry about editing those game object classes to add those static create functions, and you get the added bonus of being able to iterate through registered classes, check to see if a class has been registered, etc.


- Houdini

[edited by - Houdini on December 27, 2003 3:15:16 PM]
Houdini
Houdini
Houdini
quote:
Original post by aboeing
might aswell do the whole pluggable factories thing then:
Industrial Strength Pluggable Factories

Yes, but the implemention given in that article is not very good. It requires creating a maker class for every single class you want the pluggable factory to be able to create. Using an object factory removes all that extra code.

Creating a true pluggable object factory class is is easy as deriving from the object factory class and creating a new Create function that accepts an istream& parameter instead of an ID.

quote:
Original post by aboeing
or just cut all the crap and do this:
SCF

Yes but a true class registration is superior to that method in many ways.


- Houdini
Houdini
MaulingMonkey
MaulingMonkey
quote:
Original post by Houdini
Better to hide all that in a generic object factory class, like so:


typedef ObjectFactory<IGameObject, std::string> GameObjectFactory;

void __declspec(dllexport) RegisterClasses(GameObjectFactory &object_factory)
{
object_factory.Register<CSpaceship>("spaceship");
object_factory.Register<CAsteroid>("asteroid");
}


This way you don't need to worry about editing those game object classes to add those static create functions, and you get the added bonus of being able to iterate through registered classes, check to see if a class has been registered, etc.


- Houdini

[edited by - Houdini on December 27, 2003 3:15:16 PM]


Hmm... this I'm liking... It's a lot like what I had layed out, only taking away the messy C functions with their necessary prefix to prevent function name conflicts, and places them into the relativly sane organization of the class structure.

For those who are interested, here's what my current arcitecture layout is:

I   Main Program ("Object Server" or "Glue")
II Object Definition DLLs
A DLL code side
1 Registration function
2 Internal class definitions
B Glue code side (per object type)
1 Single storage for each object type (*1)
2 Accessor create/destroy/apply/etc. wrapper functions
III Process Implementation DLLs (*2)
A DLL code side
1 Registraion function
2 Processor functions
B Glue code side (per processor)
1 Registry of object types
2 Accessor functions


*1 Poor example: Create and Destroy create and add or delete and remove from a std::vector. Better version would create a local array which could be iterated through with locality.

*2 DLLs designed to provide a processing function (aka render a model, move an object according to physics, etc)

--------------------

Ok, now just for the sake of mentioning it, I feel like explaining what I'm doing with all this .

The Object DLLs provide new object types, which register themselves with the main program, and also register what kind of object they are. The main program dosn't really care about what kind of object they are, but uses this to match the object types up with Processors, stored in Processor DLLs.

Each object type wrapper also keeps track of all the instances of the said object... either keeping pointers to them or creating them on a local stack type structure.

Each Processor DLL keeps track of all the different object types that it can be/is to be applied to. Since the object type wrapper keeps track of all the object instances, it's possible to apply a processing function to all of these objects.

A simple example layout would be a game:

Object DLL provides:
"Marine Unit" object, which can:
Be rendered
Be influenced by physics
Be updated by time (to move)
"Headquarters Building" object, which can:
Be rendered
Be updated by time (to produce marines)
Button object, which can:
Be rendered
Processor DLL provides:
Rendering Processor (renders objects)
Physics Processor (makes objects fall)
Timer Processor (makes time flow)


On loading, first the processor DLL would be loaded. Then the Object DLL would be loaded, registering their types with the processors. Finally, the game would be started since it could now create Marines. It'd be possible to add new processors and object types while the game was running. Our final memory map would look something like this:

std::vector< processor * > Processors
0 -> Renderer_Processor
1 -> Physics_Processor
2 -> Timer_Processor

processor Renderer_Processor
std::vector< objtype * > implementors
0 -> Marine_Unit_Type
1 -> Headquarters_Building_Type
2 -> Button_UI_Type
void (Process *)( object * ) -> (loaded dll function)

processor Physics_Processor
std::vector< objtype * > implementors
0 -> Marine_Unit_Type
void (Process *)( object * ) -> (loaded dll function)

processor Timer_Processor
std::vector< objtype * > implementors
0 -> Marine_Unit_Type
1 -> Headquarters_Building_Type
void (Process *)( object * ) -> (loaded dll function)

objtype Marine_Unit_Type
std::vector< object * > objects;
0 -> Marine_Unit "Drunken SOB"
1 -> Marine_Unit "Uncle Chester"
2 -> Marine_Unit "Timmy"
...

objtype Headquarters_Building_Type
std::vector< object * > objects;
0 -> Headquarters_Building "Allies Base"
1 -> Headquarters_Building "Evil Monkey Camp"
...

objtype Button_UI_Type
std::vector< object * > objects;
0 -> Button_UI "Win Game"
1 -> Button_UI "Lose Game"
2 -> Button_UI "Quit"


Whenever, for example, the renderer processor is activated (aka when the program is ready to render the next frame) it iterates through all the 'implementors', and for each it iterates through it's 'objects' list, and calls the loaded function upon it.

Anyhoo, just some brain candy for the interested until I actually implement it and bug the gamedev.net guys to let me make an article about it .

[edited by - MaulingMonkey on December 28, 2003 6:06:44 AM]

[edited by - MaulingMonkey on December 28, 2003 6:10:26 AM]
aboeing
aboeing
quote:
Original post by Houdini
Yes, but the implemention given in that article is not very good. It requires creating a maker class for every single class you want the pluggable factory to be able to create. Using an object factory removes all that extra code.

Agreed, but the article does a good job of explaining the pluggable factory concept. Combine that with some policies and you end up with a very nice framework (replace different versions of things, different api''s etc easily). Given that all the ''extra'' code is the same for every object, its very easy to write a macro which will expand to do all the stuff you want. [another reason for prefering this design is for serialization, I belive theres a gamedev article on specificaly this topic] Since no code has actually been written yet, its not like every class will need to be modified. Plus you dont need to maintain a list of objects like the object factory you propsed.

quote:
Original post by Houdini
quote:
Original post by aboeing
or just cut all the crap and do this:
SCF

Yes but a true class registration is superior to that method in many ways.

I just like that link because it explains the concept nicely and discuss how one can make something that will be COM compatible -- always a bonus.
Houdini
Houdini
quote:
Original post by aboeing
Agreed, but the article does a good job of explaining the pluggable factory concept. Combine that with some policies and you end up with a very nice framework (replace different versions of things, different api''s etc easily).

Very true. I didn''t mean to sound like I don''t like the article, as that article is a fantastic introduction to pluggable factories.

quote:
Original post by aboeing
Given that all the ''extra'' code is the same for every object, its very easy to write a macro which will expand to do all the stuff you want. [another reason for prefering this design is for serialization, I belive theres a gamedev article on specificaly this topic] Since no code has actually been written yet, its not like every class will need to be modified. Plus you dont need to maintain a list of objects like the object factory you propsed.

Using macros was my first attempt at making an easy to use pluggable factory implementation. However, personally I''ve found that the object factory with a small tweak to accept an istream is cleaner than using macros.

BTW, I just wanted to point out (which you may have already known) that the object factory doesn''t maintain a list of objects of classes that are registered, but rather a list of functors that will create those registered classes.

quote:
Original post by aboeing
I just like that link because it explains the concept nicely and discuss how one can make something that will be COM compatible -- always a bonus.

True, I just find that implementation very limiting. But it is easy to understand.


- Houdini
Houdini
aboeing
aboeing
quote:
Original post by Houdini
BTW, I just wanted to point out (which you may have already known) that the object factory doesn''t maintain a list of objects of classes that are registered, but rather a list of functors that will create those registered classes.


Yes, the other bonus of your method is that it is far easier to put objects into DLL''s.

I guess it comes down to a matter of personal preference. I prefer the macros to maintaining a list.

Might be a good idea to use a pluggable factory style thing for "small" objects (like spaceship, asteroid etc...) and an object factory as has been suggest for "big" objects (like "game engine" and "physics engine" -- things that are likely to be in a dll, and arn''t likely to be modified too often)

Here are some more component systems to choose from:
XPLC
MaulingMonkey
MaulingMonkey
I've almost gotten my little test system fully running. The only currently known bug is that the program segfaults when trying to destroy an object, and I have traced this to an STL call... specifically, the line:

ObjectRegistry.erase( Object );

edit: doh, finally figured it out... my Object class wasn't setting Object->ObjType which it uses for destruction.

However, making this adjustment makes the program crash sooner! commenting out this little, simple line, reverts it to the old behavior. Code snippet:

CObject * CObjectType::Create( void )
{
CObject * Object = CreateFunc();
Object->ObjType = this; //<-- problem spot

ObjectRegistry.insert( Object );
return Object;
}


Commenting out the problem spot results in segfaulting when someone attempts to destroy the object. Including it makes it crash earlier... in my DLL functor nonetheless.

The functor is insanely simple:

void pxoFunctor_TextOut( ITextOut * Object )
{
Object->TextOut( cout ); //<-- crashes here
}


It is operating on a class that inherits from two classes... would this be a problem? The function is an overloaded vitual... the program crashes either trying to call, or calling, TextOut.

The Object being pointed to is of this type:

class HelloObject : public CObject , ITextOut
{
public:
virtual void TextOut( std::ostream & out )
{
out << "Hello World!" << endl;
}
};


The pointer is valid... I tossed cout statements in a constructor (printing address), destructor, and in the functor (also printing address, which lines up with the constructor one).

If anyone can shed some light on my poor brain cells, lemme know. I'll upload the full sorce to sorceforge soon, even if buggy, to get some feedback on how to fix (if still buggy).

[edited by - MaulingMonkey on December 29, 2003 7:41:25 AM]
aboeing
aboeing
you havent given us much code, but the only thing I can think of is that your trying to cast your CObject to a ITextout without going to a HelloObject

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.