• Advertisement
Sign in to follow this  

my engine, mutually nested objects- bad?

This topic is 3572 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

This little game engine I'm working on has generic Monster objects ( essentially they represent characters in the game). There is also the Monster object manager, which is like a collection and factory for the monster objects. The monster object class contains certain properties that are common for all game monsters and an event handler. All game monsters have the same events. What the monster actually does during a particular event is determined by a monster behavior object. The monster behavior class is implemented as a C# interface where each method in the interface is called when a specific event is fired. The basic idea is for each type of monster in the game, you implement a monster behavior class, filling-in different code for each event. Then you pass it to the monster object manager and it produces the resulting monster object. The funny thing is that I want to expose the monster objects attributes to the behavior object, so for each event I pass a monster object reference. When the monster object calls an event it passes a reference to itself to it's behavior object, at which point the monster and its bevior object are mutually referencing each other. It seems to work fine, but I have a sense that there must be something terribly wrong with this arrangement. Call it intuition. :) Any one have a more sensible way to do this? Basically I'm just trying to write this library to internalize everything, programming-wise, except for the monster's behavior. Also when I go to get my monsters to query the properties of other monsters, I suspect it will be equally inappropriate for each monster object to contain a reference to to the monster object manager, which contains references to all of the monster objects.

Share this post


Link to post
Share on other sites
Advertisement
There's nothing inherently wrong with having a bi-directional pointer, it's a downright requirement in some situations.

In your case, separating the behaviour from the "object to be behaved" seems like an ok idea, and seems to require both objects to point to each other at some point in time.

Share this post


Link to post
Share on other sites
It's a common way of doing things. It introduces a cyclic dependency, which is not the awesomest thing in the world if you're an obsessive-compulsive OO designer, but it's not going to cause any runtime problems. One exception: If you're using ref-counted smart pointers for the two references, you'll need to make one of them weak in order to avoid a cyclic reference which would lead to a memory leak.

Share this post


Link to post
Share on other sites
Quote:

... which is not the awesomest thing in the world if you're an obsessive-compulsive OO designer


There's the problem. I'm always subconsciously thinking that everything must be strictly hierarchical or else it's not going to work.

Share this post


Link to post
Share on other sites
For events you almost cannot avoid it - you need to know when either producer or consumer goes away (especially if it's deleted). In which case, unless you imply some really rigid framework around it, each needs reference to other. At least if you're using ref-counted or manually managed pointers.

The only way around it is using garbage collection (not ref-counted type), but that can be impractical in C++.

Bigger concern in case of events may be memory impact. The number of references retained can quickly grow, trashing your heap, and causing lots of overhead with (de)registration.

But that's just a side-effect of event-driven design.

Share this post


Link to post
Share on other sites
Man, lately it seems that every time a good question comes up, I'm fresh from reading something distinctly relevant. Perhaps its a testament to the quality and quantity of material I've been reading, perhaps its that, to borrow from an old adage, "When all you've got is a hammer, everything starts to look like a nail" or in this case, "When you've just read about a hammer, everything starts to look like a nail."


In any event, I think what I'm about to say will put things into a more pleasant light for the OO-obsessed...

Quote:
Interface Principle from Exceptional C++, Item 32
For a class X, all functions, including free functions, that both

  • "Mention" X

  • Are "Supplied with" X


are logically part of X, because they form part of the interface of X.


It follows in Item 33 that if you apply this same principle to a member function belonging to a class A, which takes as a parameter another class B, it follows that the function is a part of B and also, because it takes an implicit A* as a parameter, that B depends on A. Since A also depends on B, the classes are interdependent.

This interdependence is a known and accepted part of the C++ language, and is supported by Koenig lookup, which, in short, expands the scope of name resolution to include the scope in which its parameterized types exist if no potential matches are found in closer scopes (local scope, class scope).

What this argument supports is that, even though Monster and Behavior are distinct classes, Behavior is, in fact, part of the interface of Monster (albeit, perhaps a private one). It's simply encapsulated in a manner which makes a different, but equivalent, implementation easy to substitute.

This "part of" relationship is recognized in C++, though still not as strong as the relationship between a class and its member functions (which benefit from automatic access to Monster's internals). In fact, if you were to define Behavior as a friend of Monster, then a function in Behavior taking a Monster parameter and an implicit Behavior parameter is functionally equivalent to an equivalent function in Monster taking a Behavior parameter and an implicit Monster parameter where Monster has been made a friend of Behavior. By taking this example one step further and making Behavior an inner class of Monster, you can see that it is indeed true that Behavior is a part of Monster. This exposes the fact that, due to the nature in which you have chosen to implement this relationship (namely, in a way that supports run-time binding of behaviors) you have chosen to make Behavior external to Monster itself only as an implementation detail (In fact, you could keep run-time binding with an inner class implementation if it were acceptable to have the entire Behavior hierarchy within Monster, but I'd argue that you've made the better decision by separating responsibilities into separate bodies, if not interfaces.)

It also follows that Behavior could also be made a true inner class if it were bound at compile time, such as if Behavior were supplied as a template parameter to an instantiation of Monster.

In short, there is nothing wrong with interdependence if the interdependent bodies form a single interface.

Share this post


Link to post
Share on other sites
Your solution seems very sensible to me. A Monster needs to know what its Behaviour is, and a Behaviour needs to know what Monster it is dealing with at this moment.

Share this post


Link to post
Share on other sites
Why not pass in the attributes that are relevant to the action instead of the entire class? Another way to resolve it is to create a monster attributes class. Then compose the monster class of an attributes class and a behavior class. A third way to resolve the issue is to move the attributes to the behavior class. Either of the second two improves separation of concerns.

Share this post


Link to post
Share on other sites
Quote:

Why not pass in the attributes that are relevant to the action instead of the entire class?


I could (I might be able to) put all of the accessible attributes in a class by themselves. Then I'd have a monster object which contained the attributes object and the behavior object. So the monster object would do the event logic and pass the attributes object to the behavior object. That way the the behavior object would have a reference to the attributes and it would not be necessary for the attributes to reference the behavior.

That might placate my OCD object oriented tendencies.

Share this post


Link to post
Share on other sites
Although the relationship between the the monster object and the monster object manager will still be circular so I appreciate the advise on how to deal with that situation.

Share this post


Link to post
Share on other sites
What relationship does Monster have with the Monster Manager? What methods does Monster call?

I suppose that a monster tells its manager to remove it from its list, yes?


There are a couple approaches one could take for this problem --

First, each monster could have an "active" tag which is set to active when the monster is alive or not when it isn't. When the manager goes through it's list, it only processes the active monsters, and it places inactive monsters on a free list so that they can be re-used as a different monster later (here's where your run-time behavior binding comes in handy) -- actually, you could just remove inactive monsters from the list and, assuming no other references to them are kicking around, they should get garbage-collected soon enough.

Another approach is to implement a messaging and events system, which is designed to loosen the coupling between classes. Essentially, instead of calling Manager.RemoveMonster(me), you place a message in a queue for the manager to process eg PostMessage(MONSTER_MANAGER, REMOVE, me) -- So the Monster knows that something exists which manages it, but it doesn't know any of the specifics of the Manager itself, which is what encapsulation is all about.

Unlike your original "problem" this one actually is an interdependence you should strive to get rid of. I would also recommend that you try to separate the Factory functionality from the Manager functionality, and that you be very careful about what all the Manager functionality includes. Its very easy to just throw a bunch of stuff into a "manager" class, but if it takes on more than one responsibility you are violating correct OO design. A good place to start is to consider whether your manager does anything more than acting as a collection of monsters -- if it does, separate additional functionality into another class and rename it to MonsterCollection, if it is only a collection, just rename it MonsterCollection.

Share this post


Link to post
Share on other sites
Actually I just want the monster to have access to the monster object manager so that it can reference other instances.

Example- you could define for a monster's time slice event:


void time_slice_event (monster_object self, monster_object_manager MM )
{
if (MM["monster leader"].exists())
{
if (self.location.distance(MM["monster leader"].location)>30)
self.location = self.location.stepTowards(MM["monster leader"].location);
}
}




The situation you describe, Ravyne, I would like to avoid so I'll probably do like you were saying in your last paragraph. i think it would be too weird for an object to be able to delete itself through the monster object manager.

Share this post


Link to post
Share on other sites
Why don't you just pass the other instances to the time_slice_event function?
In case of a leader you could add that one as an attribute. That way different monsters can have different leaders. Or better: Introduce a monster group and make one monster the leader and the others its followers. Thus you just change the leader of the group without having to iterate over every monster.

Just as a side note: in your example you call MM["monster leader"] 3 times, in order to get the same object. That would require you to look it up 3 times, which is likely to be somewhat slow (string comparisons, string-to-int conversions, lookup routines, etc.). If you really need to query MM directly, just get the monster instance you want and store a reference or pointer for reuse during the execution of the function.

Share this post


Link to post
Share on other sites
Quote:

Why don't you just pass the other instances to the time_slice_event function?
In case of a leader you could add that one as an attribute. That way different monsters can have different leaders.

That would be good but the function call (the parameters) is going to have to be the same for all game objects. This is because there is an encapsulated routine that calls the event handling methods. So each type of monster could have a different leader but that is up to the code body of the function.

Quote:

Or better: Introduce a monster group and make one monster the leader and the others its followers. Thus you just change the leader of the group without having to iterate over every monster.

Maybe, as you say, I should implement some other layer of the architecture that handles the interdependency between objects?

Quote:

Just as a side note: in your example you call MM["monster leader"] 3 times, in order to get the same object. That would require you to look it up 3 times, which is likely to be somewhat slow (string comparisons, string-to-int conversions, lookup routines, etc.). If you really need to query MM directly, just get the monster instance you want and store a reference or pointer for reuse during the execution of the function.


That's true. I was just using this as an illustrative example of how I intend to use MM rather than real code. I agree your way would be more optimal.

Share this post


Link to post
Share on other sites
I've been absent minded! The system I exemplified would work for data inherent to all monsters but not user-defined that is specific to a certain class of monsters (unless each monster had some kind of keymap array for that). Maybe a message system would be more appropriate. I vaguely recall reading about such systems for game entities, .

So, for the example before- the leader monster would call a method to send a message to every monster of a particular class.


void time_slice_event (monster_object self)
{
// Do whatever to move around

MyMessage M = new MyMessage("follow_me")
M.addparameter(self.location);
M.send_to_all(M, "minion");
}



and then each entity would have a receive_message event. the minion would
do this


void receive_message (monster_object self, MyMessage Message_received)
{
if ((Message_received.message=="follow_me") && (Message_received.senderclass == "monster leader"))
{
if (self.location.distance((Vector3D)Message_received.parameters[0])>30)
self.location = self.location.stepToward (Vector3D) Message_received.parameters[0];
}

}



So I like that a little better even though it requires more code and, once again, optimization.

Share this post


Link to post
Share on other sites
In your last post you moved the movement code outside of the event where the rest of the movement is placed (from the time_slice_event to receive_message). This might complicate tracing errors.

A better approach might be to send a 'follow_me' message once and passing the sender as a pointer. This would trigger a 'watch leader' state within the receiver of the message which then monitors the leader (passed as the sender) every time_slice_event and updating its position accordingly. Add a 'stop_following_me' message to reset the receiver to a state with its own movement AI.

Share this post


Link to post
Share on other sites
This is kind of a hybrid of the two approaches. Direct referencing + messaging.

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement