OO design problem - ownership and context

Started by
6 comments, last by stonemetal 15 years, 10 months ago
Consider a RPG/roguelike game in which we have a number of levels (let's assume I want to simulate all of them at once), level consists of a grid of squares, a square can contain a monster, and a monster owns a number of items. When I model this as Level, Square, Monster and Item classes, I find that Square's methods may want to know which Level it's on, a Monster may want to refer to its Square and Level, and Item has to know its owner, etc. How to model this relationship? 1. The obvious solution that comes to mind (and what looks most OO-ish to me) is to make a reference to the owner in each class - that is, Square has a Level pointer member, etc. This looks nice, but creates a LOT of unnecessary pointers - we already know that Monster A is on Square B which is on Level C, because Level has an array of Squares, which has a pointer to the Monster. 2. I can make all the methods require the context as arguments, and just pass the pointers around when calling them - that is, Monster methods receive Level and Square pointers. This avoids creation of tons of unnecessary pointers, but is awfully verbose and error-prone. 3. I can also make these classes temporary wrapper classes - for example, I store all the Square info in a simple struct, then create and pass around a temporary Square object (with a pointer to square info, pointer to level, and all the methods) when necessary. This also looks a bit ugly, though. 4. ???? Do you have any suggestions? This looks like a frequent OO design problem...
Advertisement
Generally, a monster shouldn't care what square it's it. The method that might belongs in square and acts on a monster (or is a free function/functor that acts on a square and a monster).

Though "parent" pointers aren't that terrible in practice.
Quote:Original post by humpolec
1. The obvious solution that comes to mind (and what looks most OO-ish to me) is to make a reference to the owner in each class - that is, Square has a Level pointer member, etc. This looks nice, but creates a LOT of unnecessary pointers - we already know that Monster A is on Square B which is on Level C, because Level has an array of Squares, which has a pointer to the Monster.

They pointers are not unnecessary if you need them to model your classes.
In any case, are we talking about enough squares to actually make this become a problem? A 1000x1000 grid would require a million extra pointers. At 4 bytes each, that's 4MB. Hardly a big problem on today's computers, is it? Of course, if you have 100 levels of this size, and you need to keep them all in memory at the same time, it starts getting more expensive. So work out approximately how many pointers we'd be talking about, and you can easily determine whether it'd be a problem.

Quote:
2. I can make all the methods require the context as arguments, and just pass the pointers around when calling them - that is, Monster methods receive Level and Square pointers. This avoids creation of tons of unnecessary pointers, but is awfully verbose and error-prone.

Error-prone? How so? When you work with a monster, you tell it which level/square it should act on. That seems pretty fool-proof to me. In general, it's not a bad idea to make these things explicit. If a monster depends on a square, you tell it which square to use.
Quote:Generally, a monster shouldn't care what square it's it. The method that might belongs in square and acts on a monster (or is a free function/functor that acts on a square and a monster).

I think actions such as walking or dropping items (and monster actions in general) should be handled by a monster.

Quote:Original post by Spoonbender
In any case, are we talking about enough squares to actually make this become a problem? A 1000x1000 grid would require a million extra pointers. At 4 bytes each, that's 4MB. Hardly a big problem on today's computers, is it? Of course, if you have 100 levels of this size, and you need to keep them all in memory at the same time, it starts getting more expensive. So work out approximately how many pointers we'd be talking about, and you can easily determine whether it'd be a problem.

No, I don't think the data size would be a problem. It just seems a waste. Looks like this would be the simplest way, though...

Quote:Error-prone? How so? When you work with a monster, you tell it which level/square it should act on. That seems pretty fool-proof to me.

It looks dangerous to me because it'd be possible to pass a wrong square object to the monster, which can happen for example in a function handling interaction between monsters, or movement.

Whenever you "find" that class X "may want" to know about class Y, there is a good chance that it's not actually the case. You may surprise yourself :) Try to think about the flow of message passing between the objects, and simplify it.

A Square is very unlikely to care about its Level except to find adjacent Squares. If a bit of functionality needs to think about several Squares, try to give it to the Level, letting it find the relevant Squares and delegate to them as needed. For example, you could have Level::illuminate(int x, int y, int radius), which illuminates squares for a given distance around a certain middle point: it would call Square::blocksLight() as part of figuring out which Squares actually get illuminated, and Square::illuminate on the ones that do.

Square::illuminate(), in turn, calls Monster::reactToLight() on any contained Monsters, if the Square is in fact lightable. This is in contrast to the Monster trying to figure out which Square it's in and asking that Square if it's lit. (I really can't think of a sane game design that requires a Monster to know what Level it's on, except perhaps as a level *number*, i.e. depth in the dungeon.)

Similarly, when an Item is used, just pass the owner to Item::use() (i.e. option 2). You'll find it's not as verbose and error-prone as you think, once you start using it for the situations where it's really applicable.

In particular,

Quote:It looks dangerous to me because it'd be possible to pass a wrong square object to the monster


In practice, this turns out not to be an issue very often because the code is typically already in the context of the Square in question: thus, it simply passes 'this'.



Also, assuming C++, please use references where you can, and pointers only where you really need to.
Quote:Original post by humpolec
Quote:Generally, a monster shouldn't care what square it's it. The method that might belongs in square and acts on a monster (or is a free function/functor that acts on a square and a monster).

I think actions such as walking or dropping items (and monster actions in general) should be handled by a monster.


Yet you also think that a monster shouldn't need to know what square it's in. (and via general data normalization, it shouldn't)

But the basic idea is that are not monster actions, they're actions between a monster and its environment.
Quote:Original post by Zahlman(I really can't think of a sane game design that requires a Monster to know what Level it's on, except perhaps as a level *number*, i.e. depth in the dungeon.)

Using your example, a monster may want to lit a lamp, or cast an area-effect spell that illuminates area around the monster, which in turn would trigger Level::illuminate().

Of course, all monster actions could be handled by Level, but I still don't understand how that should be better, if the monster abilities, skills, inventory, AI strategy etc. are stored in the monster object.

Quote:Also, assuming C++, please use references where you can, and pointers only where you really need to.

Actually I want to try out D (which encourages references everywhere, much like Java), but I don't think that makes a big difference in design. :)
I would try and minimize the number of up references you had. You could up reference from monster to square, then pass all environment effects to the square you are on and let it handle how that effects the world. Or you could up reference to the world and tell it you did such and such while standing on square so and so, but that seems the wrong way to me. I agree with zalman passing in the square the mob is on when you call update seems like the better path than holding it internally, no synchronization issues to worry about.


The other path to pursue that is a little more complicated but may solve other entity to entity communication issues you may run in to later would be some sort of event system.

This topic is closed to new replies.

Advertisement