Skip to main content
GameDev.net gamedev.net
🔒 Locked

Self Tracking Object Design *Revised*

Started by Drew_Benton Mar 20, 2005 at 6:28 PM 46 replies 10k views
Original Post
Drew_Benton
Drew_Benton
Ok I have revised my thread some to make it easier on everyone. If you would like to read everything on this issue, I have created an article here. Otherwise I will just sum up some key points.
My question for all you you game developers is what type of system would you need employed to track objects throughout your game. If you are not sure, can you tell me your 'dream system' (realisticaly speaking in C++). I have though up of a little design that I think could prove quite useful, but I would need to test it out in a game first. However the main concept is this: 1. Have some std::map of pointers to your class/base class that you want to track. 2. To add to this list, a format of the following is used:

// First entity
if( g_List.find( "Entity 1" ) != g_List.end() )
    abort();
g_List["Entity 1"] = new Entity;
if( !g_List["Entity 1"]->Add("Entity 1") )
    abort();
All we do is check to make sure the object is not already in the list, then create new memory for the class, and finally add it to the list. All of the underlying details are handled by the class that you dervive from. However a little action on your part is needed to tie it together with the object list. 3. Finally when an object is no longer used, call the Remove function and it is removed from the list and memory is deallocated. You can call RemoveAll to remove everything. The overall goal is to be able to track any object that you want. Each object knows all of the other objects, so no central system is needed to communicate between the objects. This can be adventageous in many ways. Any impressions/questions/comments/suggestions on this? Thank you for your time and patience. [Edited by - Drew_Benton on March 23, 2005 9:50:50 PM]
bkt
bkt
I had a similar idea to when I would eventually get my MUD up and running; I wanted to make sure that I could track everything in an easy fashion. I planned on devriving all objects, mobiles, projectiles, etc from a single basic "IGameEntity" class. Each entity type would have one of itself, "IGameObject", "IGameActor", "IGameProjectile" etc.

I would go a little further (in my case) and have a list for each type of game object in case I need to do some mass searching for one reason or another. For some reason, some MUD engines, grouped together Mobiles (I call them Actors) with Characters and you needed to search the whole list (sometimes thousands of entries) to find either. The way that you've prevented would make this much easier in that retrospect.

But yes, I would say that it's a princple that I would/will probably end up using down the road. I don't plan to have this type of development until the end of the semester (when I can get some real coding on).
-John "bKT" Bellone [homepage] [[email=j.bellone@flipsidesoftware.com]email[/email]]
silverphyre673
silverphyre673
Ohh, cool idea! I've been wondering a bit about this, myself! Hmmm...

*bookmarked post*
my siteGenius is 1% inspiration and 99% perspiration
leiavoia
leiavoia
It gets messy when you have multiple inheritense, or any inheritense, really. Consider:

BasicObject* GetObject( std::string name );

or in your example:

g_list["name"]

Either way, we get a BasicObject* back. Now, what if we need to get the derived type of the Object? What is it? A CollisionObject? A Drawable? A SpecialThingWithSpecialFunctionsObject? Now you're throwing in RTTI and that's not *usually* a good design strategy IMHO. You have to know what you're asking for before you ask for it.

However, that's a problem for many of the "objects that need to know about other objects" scenarios. Whether it's a universal list or an Observer pattern or whathaveyou.
jdhardy
jdhardy
I've been thinking about this myself lately, and while I don't have time to enter my full thoughts, I will offer one comment: Globals are bad. (I'm pretty sure you already knew that. [smile])

It comes down to whether or not there is a need for islands of objects that are managed seperately. With globals (or singletons) that isn't possible.

My current thinking is toward the user registering the objects - it allows the most flexibility at the expense of a little extra work.
Drew_Benton
Drew_Benton
Quote:
Original post by leiavoia
It gets messy when you have multiple inheritense, or any inheritense, really. Consider:


I'm glad you asked! This is what I did to take care of it.
// In actiong_List["TestClass"]->Get<CTest>()->CTestAction("Test Action 2");// which follows the formatg_List[ NAME ]->Get< CLASS TYPE >()-> FUNCTION
That way, you can use it however you want, if it is not the base class. I can see how that is a little messy, but this would be the alternative:
CTest* item = (CTest*)g_List["TestClass"];item->CTestAction();
I think using the Get function is a bit more cleaner. Now I know there will be an argument of what if you use the wrong class type. Well, with great power comes great responsibility [smile]. If you take your time, you shouldn't have a problem with it really, you know?

Thanks for your reply! If possible, do you still feel the same way knowing that you can easily handle inheritance this way?
Drew_Benton
Drew_Benton
Quote:
Original post by jdhardy
I've been thinking about this myself lately, and while I don't have time to enter my full thoughts, I will offer one comment: Globals are bad. (I'm pretty sure you already knew that. [smile])


Well I knew I was going to get at least one person that would say that [smile]. Heres the thing though. Ideally you would not use this concept the way I have. This was just a put together to show what can be done and how. In real use, the g_List would be a part of some other class to which these concepts was needed.

For example: If you had a BattleCruiser in space, and it could launch a whole bunch of smaller crafts. The g_List would be of the BallteCrusier class and each of the ships would be in that list, for that class. If you made a second battle crusier, then it would have a seperate list with the ships in it. Same goes for whatever else you would like to apply it to.

Quote:
It comes down to whether or not there is a need for islands of objects that are managed seperately. With globals (or singletons) that isn't possible.

My current thinking is toward the user registering the objects - it allows the most flexibility at the expense of a little extra work.


Agreed! That is why you do not even have to use the class if you do not want to. You only add in the objects to the list by allocating the new memory and allocating. You can just declare a standalone variable and use it normally.

I will have to catch up with you later on what you specifically mean by registering objects. It might be easy to change the design to do that if it does not do it already.

Thank you too for your input, I will have some more thoughts about the design.
leiavoia
leiavoia
Quote:
I'm glad you asked! This is what I did to take care of it.

// In action
g_List["TestClass"]->Get()->CTestAction("Test Action 2");

// which follows the format
g_List[ NAME ]->Get< CLASS TYPE >()-> FUNCTION

That way, you can use it however you want, if it is not the base class. I can see how that is a little messy, but this would be the alternative:

CTest* item = (CTest*)g_List["TestClass"];
item->CTestAction();

AAAAAARRGG!!! I hate it when people put a "C" in front of everything!!!!!

sorry.

*snort*

i had to get that out.


Ok, i'm not sure how the templates solve the type problem. Could you explain more?
Drew_Benton
Drew_Benton
Quote:
Original post by leiavoia
Ok, i'm not sure how the templates solve the type problem. Could you explain more?


[lol] I never cared for it at all, but you can see what kind of state I was in when I wrote it though [wink]. I never use hungarian notation by choice, but somehow it creeps from my fingers! Anyways this is what I had done.

First the g_List is defined as:

std::map< std::string, Entity*> g_List;

So it is just a map of Entity pointers. Now, whenever I call:

List["name"] I will be accessing a Entity*. This is fine, but what if I had a dervived class of Entity that I wanted to use? Normally, I would have had to do something like this:

// Create a temp pointer to the dervived object that I want to useCTest* item;// Get the Entity* pointer and type cast it into the dervived classitem = (CTest*)g_List["TestClass"];// Call the function I need with the temp pointeritem->CTestAction("Test Action 2");
Now that is kind of messy and just fugly! Instead, by using templates, I can let the core class take care of it for me, so I can just do this:
g_List["TestClass"]->Get<CTest>()->CTestAction("Test Action 2");
As you can see, the Get function takes in a template of the data type that you are wanting to access. It does all of the conversions itself, so you do not have to. This way, you can easily use any hiearchy of dervived elements that extend from Entity. You just have to use the correct class name.

This is how the templates took care of that little problem. I was looking into being able to use void* to use for the g_List, but the -> operator is not defined for it, so I have some thinking how I can get around that part. Yes there is potential to return 0 if the object was not found, but I could add in exceptions if it were necessary or some other warning.

Does that make a little more sense? The idea is that when you work with the specific objects with the g_List[""] you will(should) now what they are so you can use the correct function. If you wanted to mass process a lot of them, you could easily set up a way to use IDs in the base class, then use some sort of switch/if conditional to call the correct code - if that was needed. I'm sure there would be a way to get that done correctly.
rick_appleton
rick_appleton
Quote:
Original post by Drew_Benton
g_List["TestClass"]->Get<CTest>()->CTestAction("Test Action 2");


Note that this kind of thing (delegating the conversion to the base class) opens the interesting opportunity to use different types of cast during different times of development. When testing stuff you might want to do a dynamic_cast to be safe and sure that you're not messing things up (print a log if it doesn't cast correctly). After enough testing, and you're sure that you're not using the cast incorrectly, you could replace it with a static_cast.
Drew_Benton
Drew_Benton
Quote:
Original post by rick_appleton
Note that this kind of thing (delegating the conversion to the base class) opens the interesting opportunity to use different types of cast during different times of development. When testing stuff you might want to do a dynamic_cast to be safe and sure that you're not messing things up (print a log if it doesn't cast correctly). After enough testing, and you're sure that you're not using the cast incorrectly, you could replace it with a static_cast.


Ahhh! Thanks a bunch. Right now in the Get() code, all it does is a templated type cast and returns that. I will look into those different cast things so I am not doing a hard and dangerous cast. Thanks for that suggestion! I will look at that now.
silvermace
silvermace
hi, good idea, few concerns:

1. strings to access everything, slow?
2. what happens if you get a failure or g_List["Something"] fails?
3. why not use globals if you're going to all the trouble to put all objects inside a list with no namespaces for its items?
4. how will you insure that objects in use are not being removed?
5. how will you handle collisions?

cheers
-danu
Drew_Benton
Drew_Benton
Quote:
Original post by silvermace
hi, good idea, few concerns:

1. strings to access everything, slow?
2. what happens if you get a failure or g_List["Something"] fails?
3. why not use globals if you're going to all the trouble to put all objects inside a list with no namespaces for its items?
4. how will you insure that objects in use are not being removed?
5. how will you handle collisions?

cheers
-danu


1. I use a std::map for access, so I believe it has a O( log(n) ) runtime. That or it's O( nlog(n) ) I can't remember which, so it's not that slow. However, I can easily switch to using a hash_map and get O(1) access time. I currently use hash_maps in my audio library to which I used to use maps, so I will make the switch. Thanks for pointing that out.

2. Right now that is left to the end user. Right now I have something like this in the code:
g_List["Entity 5"]->Remove();g_List["Entity 5"]->ShowAll();

To which g_List["Entity 5"] does not exist in the list. If proper care is not taken, you could perhaps crash it thorugh the Get() function, but since this should be encapsulated by a class, if( g_List.find("Entity 5") != g_List.end() ) should be used before you call the function. That is just good STL practice with pointers and such, make sure the element exists before you access it. Otherwise, STL will create a new variable and it would be valid to a certain degree (be able to use static functions and members), but anything other than that will warrant a crash.

Currently in the Get() function, I have swapped in a dynamic_cast so I can make sure the correct type is being used. I have not yet had time to work with that, but ideally, either it will either throw an exception or just halt execution. This is more debug specific because when you go to release, you will have to had made sure that you program works correctly [wink] WhatI mean is that when you debug your code, you want to stop, but when you are ready for release, you will not have all the checks and such in.

3. Because this example is really trivial in it's demonstration. It shows how you could use it quite erratically with the use of the global variable. I am working on a few better demos that are more accurate for it's use. The main thing is that by using this, even in the way I did for the example, it should reduce what you need to do to track all the objects. I will try and make something of comparison to show the difference between this and *another* way.

4. Good question. Right now I leave all removing of objects to the user. This is show by the use of:
// If it's not in the listif( g_List.find( "Entity 3" ) != g_List.end() )     abort();// Allocate new memoryg_List["Entity 3"] = new Entity;// Add it in!if( !g_List["Entity 3"]->Add("Entity 3") )     abort();

So basically, if you do not call RemoveAll() at the end, you will have leaks. That or you don't call 'Remove' the objects when they are no longer needed. The ender user gets to decide when objects need to be removed. If they wanted a more automated system, they would either make one or if it was really wanted, the design could be changed some to incorporate garbage collection. I would rather let the end user control when objects are removed.

5. Good question! Right now I have all of that error checking to make sure the same name is not used, but that will be a problem down the line. At runtime I will either make it so it halts and tells the user of a collision or I will make a system to which you can have multiple objects with the same name, to which you can operate on ALL the objects with the same name. This is more realistic because each object can be of the same type name and such, just different properties. I will expand it for that usage.

Thank you so much for your time and response. You have brought up a lot of great points that I will incorporate into a demo to show some more. I think I will start working on a simple 3D OpenGL demo to which shows what is possible and stuff, the console mode is just not cutting it! Thanks again for your time, feel free to ask about anything I've said or if you think I am off with a concept or explanation [wink].

- Drew Benton
Couvillion
Couvillion
Original post by Drew_Benton
Quote:
Original post by silvermace
...
4. Good question. Right now I leave all removing of objects to the user. This is show by the use of:
// If it's not in the listif( g_List.find( "Entity 3" ) != g_List.end() )     abort();// Allocate new memoryg_List["Entity 3"] = new Entity;// Add it in!if( !g_List["Entity 3"]->Add("Entity 3") )     abort();

So basically, if you do not call RemoveAll() at the end, you will have leaks. That or you don't call 'Remove' the objects when they are no longer needed. The ender user gets to decide when objects need to be removed. If they wanted a more automated system, they would either make one or if it was really wanted, the design could be changed some to incorporate garbage collection. I would rather let the end user control when objects are removed.



...
- Drew Benton


Why not just put a call to RemoveAll() in the destructor of whatever class g_List is? Clean-up will be done when the global goes out of scope at the end of execution.

class globalList : public map<string, Entity *>{public:  ~globalList()  {    RemoveAll();//Probably better to move functionality of RemoveAll here.  }};


Use an instance of globalList, rather than a map.

I know a lot of people don't like to to inherit from STL classes. I'll let the gurus explain why this is a bad idea.

You can always write a wrapper:

class globalList {  map<string, Entity *> m_list;//The new global list.public:  //Allow access via [].  Entity *operator[](string const &key)  {     return m_list[key];  }  //Add other accessor methods to taste.  ~globalList()  {    RemoveAll();//Probably better to move functionality of RemoveAll here.  }};
~~C--O -
leiavoia
leiavoia
The templated Get() makes sense as long as you're absolutely sure you got the right type (and usually it's not an issue, i admit). An alternative you might consider, which is faster but has less flexibility, is maintaining seperate lists for different types.

Another thing you've already mentioned is possibly switching to an integer ID instead of strings. It's much harder to remember which object is #348 though, i admit. I've worked with systems like that and found them rather uncomfortable.
Telastyn
Telastyn
Odd, looking back, my current project kind of impliments the same sort of thing, albeit in a far less generalized way.

Essentially, I have 3 levels where objects are tracked.

First, is a hash table, based on a UID [because even if maps are log(n), the n will be much larger for a string than an ID]. This is commonly used for picking arbitrary objects from a reference. Objects are stored in the hash table using my own variant sort of class.

Second, most objects have a list of instances. The global list players for instance, will commonly hold the list of players. This is commonly used for iteration over the list, or for finding an instance of something without having to iterate through every single game object...

Third, any class that will often have to access another will have a direct pointer within the class. A unit will have a pointer to the tile it's in [and vice versa], since anything acting on the unit will often need to know info about the tile [and vice versa]. Iterating through each unit and then each tile per unit would be terrible, hence the direct link.

All of the linking/de-linking is done in a rather brute method in each object's 'create/move/destroy' sort of functions.

In my dream system, there wouldn't be the need for pointers, or for even the 2nd lists perhaps. Something maybe like Perl's "context" would be ideal, something that handles all of those explicit relations in a more implicit [and invisible] manner.
Drew_Benton
Drew_Benton
Phew! Lot's of catching up to do now. I will reply to this one then catch up with the rest after my Math Test (~1hr).

Quote:
Original post by Couvillion
Why not just put a call to RemoveAll() in the destructor of whatever class g_List is? Clean-up will be done when the global goes out of scope at the end of execution.


Oh yes. That is the best and most perferred way to have things done. That way it will ensure that everything is properly removed. I am working on an example that shows that in action. Right now, with my poor example, it is necessay to call remove yourself, but ideally, it is all handle by your class you make. Sub-objects of that calls will simply remove themselves, while the base object will remove all.

Quote:

Use an instance of globalList, rather than a map.


Very intresting! That will work well for a global scope, to which my program is written right now, so I will definitly give that a try. I am still learning the basics of STL concepts, so I will look into not inheriting from STL classes. Your example right there is what I am doing currently in a better demo. Each base class will have it's own m_list variable so all of it's subsidies could work with it as well. Great point!

Thanks for your comments and suggestions as well! I will be compiling some sort of doucument with all the questions/comments/replys with it to make sure I can find a 'best' solution to this that people would perfer to use.
Drew_Benton
Drew_Benton
Quote:
Original post by leiavoia
The templated Get() makes sense as long as you're absolutely sure you got the right type (and usually it's not an issue, i admit). An alternative you might consider, which is faster but has less flexibility, is maintaining seperate lists for different types.


Well the main reason I did the templated list was so that you could have some Base class and as many Derived classes from it with out having a need to make a list for each type. You just make one list to the Base class then using inheritance and polymorphism, you can get the correct class. I added the dynamic_cast that will throw an exception if you try and convert the wrong object, so that is there for the safety.

Quote:
Another thing you've already mentioned is possibly switching to an integer ID instead of strings. It's much harder to remember which object is #348 though, i admit. I've worked with systems like that and found them rather uncomfortable.


Oh no, let me re-explain. I will always use the string for access. This way is most convient and possible to be hashed for constant run time. The ID's I suggested would be what I am implementing into each class and dervived class. That way each object cab go through the other objects and 'know' what they are. The ID's are just the type that is assigned in the constructors of the classes we make.

Thank you for your continued advice [smile]! I will make sure I re-read everything again when I get back and make sure I am addressing any issues in the code.

- Drew
Drew_Benton
Drew_Benton
Quote:
Original post by Telastyn
Odd, looking back, my current project kind of impliments the same sort of thing, albeit in a far less generalized way.

Very intresting!

Quote:

First, is a hash table, based on a UID [because even if maps are log(n), the n will be much larger for a string than an ID]. This is commonly used for picking arbitrary objects from a reference. Objects are stored in the hash table using my own variant sort of class.

Agreed! With strings in the general form, the night will be a little bit higher, but the map uses the < operator to determine what is what, so it's not that bad. Also since strings can be hashed, you can achieve the same speed if you use the hash_map compared to a slower map. With integers, a map would be better than a hash_map.

Quote:
Second, most objects have a list of instances. The global list players for instance, will commonly hold the list of players. This is commonly used for iteration over the list, or for finding an instance of something without having to iterate through every single game object...

I will have to think about this and see how it could apply to this design. I know that using the iterators are very benefitial and can speed things up a bit if you have them stored to get an object rather than looking for it each time.

Quote:

Third, any class that will often have to access another will have a direct pointer within the class. A unit will have a pointer to the tile it's in [and vice versa], since anything acting on the unit will often need to know info about the tile [and vice versa]. Iterating through each unit and then each tile per unit would be terrible, hence the direct link.
Using this concept, I would need to look into being able to make some 'global' class that contained pointers to all the other classes' lists. That way, if objects are not of the same base type, they can still communicate with each other. That is something I definitly need to factor in to the design because right now, the way it works that would not be possible.

Quote:

All of the linking/de-linking is done in a rather brute method in each object's 'create/move/destroy' sort of functions.

In my dream system, there wouldn't be the need for pointers, or for even the 2nd lists perhaps. Something maybe like Perl's "context" would be ideal, something that handles all of those explicit relations in a more implicit [and invisible] manner.

I am unfamilar with Perl so I will look into the context it uses. I agre though that it would be nice not to need pointers, but hopefully I can make a design that does all of the behind the scense work in an invisible manner so the programmer can just use it easily without any worries [smile].

Once again, thank your for your time and input on this issue! I definitly have a big to do list now of things to address and consider.

- Drew
nilkn
nilkn
Quote:
Original post by leiavoia
It gets messy when you have multiple inheritense, or any inheritense, really. Consider:

BasicObject* GetObject( std::string name );

or in your example:

g_list["name"]

Either way, we get a BasicObject* back. Now, what if we need to get the derived type of the Object? What is it? A CollisionObject? A Drawable? A SpecialThingWithSpecialFunctionsObject? Now you're throwing in RTTI and that's not *usually* a good design strategy IMHO. You have to know what you're asking for before you ask for it.

However, that's a problem for many of the "objects that need to know about other objects" scenarios. Whether it's a universal list or an Observer pattern or whathaveyou.


IIRC, this could be solved with some tricky use of a pluggable factory. I would explain, but it's rather complicated, so I'll just leave it at this for now.

Topic Locked

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

Sign in to reply to this topic.