Question regarding indexes to entities and scripts

Started by
5 comments, last by Ezbez 17 years ago
From this thread.
Quote:Original post by ApochPiQ
Quote:Original post by Nitage In fact, I find reusing Ids a bit nasty - what happens if (say in an rts) a unit diedhalf an hour ago, but a script kept around a reference to it? Do you want to be able to tell the script that the unit in question no longer exists? Or would you prefer to hand it a reference to a totally different unit?
If this scenario ever occurs, you have serious design issues anyways, and likely some major bugs lurking under the surface. When the unit dies and becomes non-existent, every reference to it should be released. Your systems should be built in a way that makes it trivial to track all known references to a given resource or entity; as a logical consequence, it should be trivial to remove those references if an entity stops existing. If this is not a very simple process, chances are the design has some major weaknesses - most likely excessive coupling, probably some temporal coupling, and certainly a lot of unwritten and unverifiable assumptions.
This scares me because it's basically saying "Your program is going to have bugs, Ezbez!" I have a system similar to what was discussed in the linked thread. I have integer IDs for everything, and I create a new one by adding one to the last one that was made. No duplicates, no worries, eh? But now ApochPIQ tells me that it's fundamentally wrong and the very fabric of game-time is going to collapse on me one of these days because of it. Can anyone help me with this? I want a nice system, but I'm not sure how I can improve upon my current system. I guess some more details about my current system might be useful. I internally store Entities and expose them via EntityHandles to scripts. The EntityHandles don't actually ever give out an Entity, they wrap all function calls to the Entity between another layer. This layer checks that the Entity is still valid, if not, it does nothing and returns some generic nothingness to do no damage. Scripts only ever touch EntityHandles, and can store them. EntityHandles internally store indexes to the Entities.
Advertisement
When your entity dies, all references in the scripts should die too, so they should never have to manage a NULL handle. Else, you scripts might for example, leak memory quite badly (for example, accessing the entity one hour after its deletion. That's not really a good idea).

It is not recommended to silently discard NULL handle. At the very least, assert. It's like a memory violation, like accessing a dangling pointer. The exception is that you can obviously carry on the execution, during the development time. But in final builds, these should be ironed out.

If you are not confident with re-using Ids, that's fine (as long as you use a fast lookup / hash table), but consider accessing NULL handles to be a bad thing at least.

Everything is better with Metal.

I use references to pass around.

Since the code requires either serialized references or local objects, I use a single reference that stores the serialized id and smart pointer.

Then I overload the -> and * operators to make handling transparent.

Something like this:
template < class T >class ObjectRef {public:  ObjectRef( ObjectPtr ptr )    : m_ptr(ptr)    , m_guid(ptr->guid())  {}  ObjectRef( ObjectGUID guid )    : m_ptr(NULL)    , m_guid(guid)  {}  T operator ->() {  {    if (!resolve() ) throw ...    return m_ptr;  }private:  bool resolve()  {    if ( m_ptr.is_null() ) {      // do lookup to obtain instance      if ( ... not found ... )        return false;      else        m_ptr = ....    }     return true;  }  ObjectGUID m_guid;  ObjectPtr m_ptr;}


The scripts are passed the ObjectRefs.

void invoke_action( EntityRef &ref ){  ... script( ref )}


Here, I don't fuss much if object gets "destroyed" while script is holding a reference to it. Smart pointers make sure the object is still around, it's just disconnected from the event system.

And since all scriptable actions can only hold references for duration of function call, this overlap is minimal.

This isn't optimal, and I use it in networked game, so my requirements might be somewhat different, but they do the task.

When an object is destroyed, it's simply removed from global directory. Smart pointers take care of the rest.

In addition, it allows me to defer resolution of serialized references until they are needed - some might never need to be resolved.
This reminds me of a minor bug in Warcraft 2. Summoned skeletons have a timed lifespan, after which they automatically die. A player can load a skeleton onto a transport ship and then wait for it to die. The slot on the transport will then appear empty. The next unit created will appear inside of the transport instead of where it was supposed to. Even units that shouldn't be allowed on the transport, like flying units, other ships, enemy units, and buildings. The bug wasn't that serious because summoned skeletons were fairly worthless and have a long lifespan, so it didn't happen in real games.

Just an example that shows this sort of bug has actually occured in commercial games.
But how can I fix it? I'd like to be able to keep references in scripts for longer than the duration of one script. And I'm not sure how I'm supposed to go about handling the removal of all references. I could do that, but I'm not sure how that will work for scripts. For example, if Entity A gets a reference to B, then B dies. What should A do if it's handle to B disappears? Suddenly it'll be accessing a non-existant variable - the very thing I was trying to avoid when I created EntityHandles.

Edit: Though I don't really see how it takes care of the problem at hand, I really must say that I like your system Antheus. It seems like just the thing I was looking for to avoid having to do a lookup in an std::map every single function call.
Essentially you have a requirement, which is to ensure that your references stay safe to use. Either that can be by magically removing all references to something unsafe, or it can be by making references to dead things just as safe as references to living things. The first way requires that each entity knows who refers to it (eg. observer pattern, smart pointers), but gives you the benefit of knowing that you never have null references.

Either way requires that you have a system in place for handling what happens to a script with a null reference. If your language uses exceptions, defining a standard exception for this purpose makes sense. If not, you have various choices - the 'generic nothingness' of the original post is one option, though perhaps not the easiest to debug when scripts misbehave.

In other words, I think ApochPiQ is perhaps overstating the problems. It's safe to keep dangling references if your references are safe to use, and if you don't recycle ids. If either of those are false, you need to get rid of references as soon as they become invalid. That's all.
Okay, neither of those are false for me, so I'll just keep it how it is right now, but I'll throw an exception if it no longer exists and is accessed.

This topic is closed to new replies.

Advertisement