What's the best way to implement a level map like this?

Started by
16 comments, last by mike3 12 years, 1 month ago
Not sure if this will was mentioned but possibly a map of function pointers using the enum's as the keys.

map<TileType, (*func)(args...)> tileFunctions;

Then you can have 1 update or other function with private functions for each type of tile.

(*tileFunctions[this->tileType])(args to send);

The only downside here is that you'd have to make it so each of the tile update functions has the same arguments and return values.
Advertisement

Avoiding globals is a matter of designing (and limiting) your dependencies, not of candy wrapping some data and still try to use it from everywhere.

It's a bit like "thou shalt not goto" resulting in people not writing better code, but replacing goto with even less readable and more idiotic constructs to simulate a goto. If you ask "how to not make it global, but I still need it _everywhere_" then that's your problem right there.


So how would one limit it here, when in this case one needs it whenever a tile needs to be changed, which can happen in several places, such as destroying walls or other types of tiles, or creating new walls, any of which may happen in the course of the game. It would seem there would be no way to limit that without also removing game functionality, so am I right in classifying those as "necessary dependencies" and not worrying about them? In other words, we have to use this in multiple places, no way around it? It's just the natural way this is? Though, now that I look at this, the "global" data seems fairly well-packaged. Looking at the snippet below, it would look like nobody needs to know of the existence of TilePrototype (or almost nobody -- main() may need to call some kind of initializer that sets up the array of prototypes but that's it), just the tile IDs to change the tile to different types (or not, we could even bury that functionality in Tile, such that the first time a Tile is constructed, the prototype list is prepared, thereby removing even that little bit of visibility). All that stuff is buried inside Tile, and in implementing Tile is probably the only place we ever touch that list directly. But still, couldn't that be considered "using it from everywhere", since it's used wherever Tile is, or is that just being waaaaay too paranoid and picky? But I'm wondering, because in your snippet, right there, there's a global!!!! And you mention how "candy wrapping" doesn't solve the problem!

Another problem here is, what if one uses oooone little teeny weeny global here, but then before you know it it's joined by a dozen others, and a big, sticky, stringy, gooey dependency mess from hell with all the associated bug-inducing fun that entails?


You mean have member functions of TilePrototype that take MapCells as argument? But doesn't that set up kind of a circular dependency between the two? Isn't that bad? Or what? What did you have in mind for the structure of the actual classes/data structures/etc. involved ("outline" code like I posted would be best)?


It's not always a matter of "but text book X said this is bad", but of "what's the lesser of two evils". If you can come up with a good way to do what needs to be done and which doesn't add 10 times the complexity for the sake of avoiding a circular dependency, then of course go for it.[/quote]

Thanks for the answer.



Without any error checking etc.

// prototype.h
class Tile;
class TilePrototype
{
static const TilePrototype& get(TileId id);
void (*onActivated)(Tile& tile);
}

// prototype.cpp
vector prototypes; (<-------- GLOBAL!!!!! -- mike3)

const TilePrototype& TilePrototype::get(TileId id) { return prototypes[id]; }

// tile.h
class Tile
{
TileId tileId;
TileData data;
};


If you go with virtual functions, the vector needs to store TilePrototype*, if you go with scripting, the function pointer should instead be whatever script handle your favorite method is using (might be strings or wrappers or...).


Looks pretty much like what I thought, then. But usually, we would make the elements in class Tile private, no, and provide member functions to access them, right? So then we'd also need an onActivated() function in Tile that calls the one in TilePrototype with *this as argument? And onXXXX() functions for each corresponding one in TilePrototype that we wish to invoke from outside? And what about dependency problems from that global?


And I just noticed a new problem. Consider the "onMove" event. There may be certain kinds of things that should occur on a move to very specific tiles, e.g. one particular tile on a map, not one kind of tile.


I don't know how large you intend your maps to become. Personally I'd try not to bloat my map elements with lots of functions just because maybe some day I might like to do such and such. If this particular reaction to a very specific tile is not related to the tile or tile type itself, you have found your first example of why games use triggers as "invisible" game objects. For this special stuff, you don't have the tile perform some super special reaction, but you create a single trigger and "place" it on that tile.

You really don't want to end up with every tile having members like
MySpecialOnMove, MySpecialOnItIsFriday, MySpecialOnLightLowerThan10, just because on level 24 you have this one tile that when stepped on at the wrong time in darkness will close a door.

You can of course go ahead and "unify" those concepts by removing the above function pointers/virtual functions and instead simply place certain triggers on certain tiles by default. But keep in mind that moving too much stuff from prototype to tile instance can quickly increase your memory usage.
[/quote]

And so that means that there needs to be some function that calls the trigger onMove, the tile onMove, etc. (all of these!) when something moves into that tile, right?
Most of the logic you ask for I wouldn't place in either tile or prototype. Send events to the map and let it be responsible to handle them. They are also your major tool in fighting depedencies. Some class enemy doesn't have to know what a tile is, just because it moves on them. It will not go and call oldTile->onLeave(me) and newTile->onEnter(me), but it will send event MoveEvent(me, fromPos, toPos).

This event will end up hitting the map which will grab the two prototypes and call their functions, passing the relevant tiles as parameters. If you are worried about too many dependencies, don't have everybody call functions on everybody else. Yes, this event system will be some form of "global", because it's the module everybody will use to communicate with everybody else.

Dependencies aren't created by what that article calls "hidden globals", but by design. Draw a bunch of boxes representing your class and modules and draw lines where you believe one must use the other. Notice how this design usually has nothing to do with implementation details of what is "global in disguise".

Another way (which can get very tedious very quick) is to forward everything over several layers, but when you end up with Game::movement calling World::movement calling Map::movement calling Tile::movement you will appreciate just sending an event and having everybody who cares (and registered for it) to handle it automatically. Downside is of course again in debugging. When your map ignores movement events, it's less straight forward to find out why.

Though seriously, for your first few games, focus on getting them done, not making them text book examples of squeaky clean programming. Trying to be holier than the pope (as we like to say over here) will keep you from making progress which often kills motivation and the project with it.
f@dzhttp://festini.device-zero.de

Most of the logic you ask for I wouldn't place in either tile or prototype. Send events to the map and let it be responsible to handle them. They are also your major tool in fighting depedencies. Some class enemy doesn't have to know what a tile is, just because it moves on them. It will not go and call oldTile->onLeave(me) and newTile->onEnter(me), but it will send event MoveEvent(me, fromPos, toPos).

This event will end up hitting the map which will grab the two prototypes and call their functions, passing the relevant tiles as parameters. If you are worried about too many dependencies, don't have everybody call functions on everybody else. Yes, this event system will be some form of "global", because it's the module everybody will use to communicate with everybody else.

Dependencies aren't created by what that article calls "hidden globals", but by design. Draw a bunch of boxes representing your class and modules and draw lines where you believe one must use the other. Notice how this design usually has nothing to do with implementation details of what is "global in disguise".

Another way (which can get very tedious very quick) is to forward everything over several layers, but when you end up with Game::movement calling World::movement calling Map::movement calling Tile::movement you will appreciate just sending an event and having everybody who cares (and registered for it) to handle it automatically. Downside is of course again in debugging. When your map ignores movement events, it's less straight forward to find out why.

Though seriously, for your first few games, focus on getting them done, not making them text book examples of squeaky clean programming. Trying to be holier than the pope (as we like to say over here) will keep you from making progress which often kills motivation and the project with it.


So then you're saying that using the small global in the example to store the list of tile prototypes is not bad in this case, then, right?
And I'm debating now whether to use function pointers or instead to use virtuals w/TilePrototype then specialized into, e.g. WallTilePrototype, etc. Or even whether to use function objects instead of function pointers (I use them in some other places where something can be customized to various behaviors by passing it a function).

Most of the logic you ask for I wouldn't place in either tile or prototype. Send events to the map and let it be responsible to handle them. They are also your major tool in fighting depedencies. Some class enemy doesn't have to know what a tile is, just because it moves on them. It will not go and call oldTile->onLeave(me) and newTile->onEnter(me), but it will send event MoveEvent(me, fromPos, toPos).

This event will end up hitting the map which will grab the two prototypes and call their functions, passing the relevant tiles as parameters. If you are worried about too many dependencies, don't have everybody call functions on everybody else. Yes, this event system will be some form of "global", because it's the module everybody will use to communicate with everybody else.


So I take it that "MoveEvent" is something sent to the game's event queue, and that it is the current map is implicit? But does this also mean that a global is okay in this circumstance?

I don't know how large you intend your maps to become. Personally I'd try not to bloat my map elements with lots of functions just because maybe some day I might like to do such and such. If this particular reaction to a very specific tile is not related to the tile or tile type itself, you have found your first example of why games use triggers as "invisible" game objects. For this special stuff, you don't have the tile perform some super special reaction, but you create a single trigger and "place" it on that tile.


Doesn't that trigger need to be stored on the tile, so that when we move on that tile we can activate the trigger? But then why not just include a placeholder for a function?

So I take it that "MoveEvent" is something sent to the game's event queue, and that it is the current map is implicit? But does this also mean that a global is okay in this circumstance?


Depending on which of the many ways you pick, something should have registered to receive move events. I would definitely avoid any kind of windows like event handling with one huge switch/case. That function should not care about what a move event is, what a map is and even less what to do with it. Especially since you will want to add event types and preferably the only two places that should be aware of them are the sender and receiver.
f@dzhttp://festini.device-zero.de

[quote name='mike3' timestamp='1331028239' post='4919717']
So I take it that "MoveEvent" is something sent to the game's event queue, and that it is the current map is implicit? But does this also mean that a global is okay in this circumstance?


Depending on which of the many ways you pick, something should have registered to receive move events. I would definitely avoid any kind of windows like event handling with one huge switch/case. That function should not care about what a move event is, what a map is and even less what to do with it. Especially since you will want to add event types and preferably the only two places that should be aware of them are the sender and receiver.
[/quote]

So what knows what the map is, then, in this case, to handle the move?
Now I've got another question: how do we store entities (triggers, monsters, even the player!) on the map, which are associated with map tiles? E.g. a monster standing on a tile. Should this be stored as part of MapCell, etc. above? What's the best way to do this? What kind of accessors should the map have to access that stuff in a "safe" manner?

This topic is closed to new replies.

Advertisement