So, what to do? Or is this a case where a global may really be the best solution? Note that it mentions as one of the biggest, if not the biggest problem with globals is:
What to do indeed, especially since they neglect to mention that issues like concurrency won't magically go away just because the data is wrapped in 20 layers of object orientation. It doesn't matter if it is a simple array (with global access functions) or buried in an object that everybody has a reference to. Two threads accessing it at the same time will be a problem.
Even the non-locality example basically skips how to change your design. The problem isn't so much that the data is global, but that everybody is supposed to get to it somehow. In fact, even when you have a non-global object and just pass around references to it, how is that improving the basic problem of too many parts of the code touching it? It may even just hide the actual problem behind 10 layers of indirection, making it absurdly hard to debug.
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.
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.
Without any error checking etc.
// prototype.h
class Tile;
class TilePrototype
{
static const TilePrototype& get(TileId id);
void (*onActivated)(Tile& tile);
}
// prototype.cpp
vector<TilePrototype> prototypes;
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...).
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.