#8 Framework 4

To be honest, it took me a while to remember what I meant by “interaction”, when it came to writing it. I really should decide on something better to call it. Oh well.

So, I’m not talking about player interactions here, nor about inter-entity interactions. The issue here is that, in the ideal design, we break all of the functionality down into small reusable chunks. I’m not sure if I need to go into a design discussion here. Basically, if we have one behaviour which deals with everything that the player could possibly do, first of all, it becomes an unmanageable beast, with a file probably exceeding several thousand lines without comments. To make it more obvious what things are doing, you want to be quite free with your comments, putting them everywhere and documenting the code as much as possible, but this leads to yet more lines of code. Soon you end up with a 10,000+ line monster file which takes you 2 hours to find the bit you want, by which point you’ve forgotten what specifically you wanted to do. There’s also the fact that if, for example, you add health to a player, and then to an enemy, you practically have to duplicate code to get it working in the different places.

What about the messaging system, I hear you say. We can easily get behaviours to listen to other behaviours, right? Then we can have a message flow between them and interact in that way. This is entirely true, but at the moment there’s no way to nicely get a certain behaviour from an entity. If we check the header for MObject3d.h we see the following:

MObject3d.h
// behaviors
void updateBehaviors(void);
void deleteBehavior(unsigned int id);
void invertBehavior(unsigned int idA, unsigned int idB);
void changeBehavior(unsigned int id, MBehavior * behavior);
inline void addBehavior(MBehavior * behavior){ m_behaviors.push_back(behavior); }
inline unsigned int getBehaviorsNumber(void){ return m_behaviors.size(); }
inline MBehavior * getBehavior(unsigned int id){ return m_behaviors[id]; }

So, we have a way to get a behaviour, but it’s just accessing the behaviour from an std::vector by index. We _could_ try to insist that behaviours are in a certain order, but that’s quite fragile, and manually looping through the behaviours each time will bulk out the code a lot.

Let’s try and tidy it up to begin with, then when things get more complex, we’ll hopefully have a nice system in place. As the majority of the code for this will be just the loop mentioned before, I will do this tutorial a bit differently, we’ll focus on the designing the system, then I’ll just show what I wrote, rather than explain what I’m doing as I’m going along. I’ve also put the code onto a different page, so this post doesn’t get unmanagably long.

Let’s think about design now. We can’t realistically modify MBehavior to do what we want. Well, we can, Maratis is zlib and we can do what we want with it. That’s not really what we want to do though. Much as we could, at each stage, fiddle with Maratis, it disrupts workflow a bit. We also can’t guarantee that our design matches with the design of the engine owner. Also, if we move the functionality into the engine ourselves, we are left with having to rebuild the whole of Maratis every time there’s a change. If it’s only 1 person working on the project, that’s not too bad, but constantly redistributing binaries to artists gets frustrating.

So we’ll do this in the project. That’s fine. What, exactly do we want to do? Well, our messaging system should take care of a lot of the common interactions, but we also need to be able to register things to listen to each other at the very least, so we need to be able to get sibling behaviours. Ideally, we’d want to expose something like GetBehaviourByType on the object, but that’s not too easy as we can’t very easily extend MObject3D without potentially breaking things. It’s much safer to do this work on top of MBehavior. So we’ll add a layer between MBehavior and our actual behaviours which can take care of things like this.

Now, the issue is how we will know what behaviour to get back. The first thing we could try is to do something like loop through all of the behaviours in the parent object and then do something like dynamic_cast to check if it’s the type we’re looking for, but that’s quite nasty and also pretty inefficient. If the object had a map of the behaviours, mapped to some form of ID, we could look it up easily. But there isn’t a reason why we can’t still do this. We just have to keep the behaviour information external to the object. It’s a little more hassle, but should keep things fairly neat and tidy.

The next thing to decide is what information this behaviour database. If we start at the object level, we will need to keep a map of all it’s behaviours, with some form of ID as the key. We’ll worry about the ID in a bit. But that’s just for one object, because this is an external database, we’ll also have to have a higher level collection so we can basically filter the behaviours by object too. So we’ll wrap the map of behaviours into another map, using the MObject3d as the key. There shouldn’t need to be any more data stored inside the database at all at this stage.

BehaviourDB.h

BehaviourDB.cpp

The Behaviour itself should be fairly straightforward too. On creation, it should register itself with the database, on destruction it should remove itself. The only other functionality is to be able to call GetBehaviour.

That leads us finally to the ID. Now, this could be absolutely anything. Technically we could make it a std::string and it would work perfectly, but comparing std::string isn’t as fast as, say, comparing a single 32 bit integer. So, if we could guarantee that we have a unique number for each behaviour, we could use that. This is where hash functions come in. They take a piece of data, in our case it will be a string, and perform some magic on it, and get a number out of it. There is always the possibility of hash clashes, where two different pieces of data will produce the same hash, but decent hashing functions will minimise any chances pretty well.

Util.h

Util.cpp

Rather than write some arbitrary behaviour to prove a point, the easiest thing is just to put the check into TutorialBehaviour. I have to stress that there is NO reason at all why you should do this for any other behaviour. It’s just to check that the wrapper and database are working correctly. So we’ll set up the TutorialBehaviour to inherit from Behaviour rather than MBehavior, and then, only do it’s normal functionality if it can successfully retrieve itself back from the database.

TutorialBehaviour.h

TutorialBehaviour.cpp

If you do all of this, and the “game” still runs exactly as it was before you did any coding, you’ve done it right! Once again, we’ve succeeded in doing almost nothing and spending time doing it. On the plus side, once we have multiple behaviours which need to be able to interact with each other, we can easily do GetBehaviour in order to attach messages or even just poke directly.

As usual, if there are any comments, jump on the Maratis forums and send me a message, I’ll reply as soon as possiblye

<-Previous Next->

Discuss

Leave a Reply

Your email address will not be published. Required fields are marked *

Diary of a Slacker