Ok, I need to refine. I don't like Gang of Four singletons either; I tried them way back when on Draconus and never wanted to use them again because of order-of-initialization surprises. But I do like the syntactic sweetness of static Inst() accessing a static member, as long as we control initialization order ourselves, as Noel Llopis suggests. That's why I said "variation of a popular singleton pattern" - key word being variation, don't use it as written.
Yeah, it's true, proper forward declaration doesn't make the circular dependencies *too* bad. Though, windows.h isn't too bad either, IMO, as long as you use precompiled headers.
And yeah, using a language like C# or Java that gets rid of headers altogether is heavenly.
Globals can make it harder to test, but I've usually found you can mock out whatever globals are required pretty easily.
It sounds like in Noel's perfect world, we have to copy our game state every frame - our update function would take the old game state as its input and spit out the new game state. That would be my ideal too, come to think of it, though I've never lived up to it.
Speaking of ideals, even if you have globals, you don't want every global to be accessed from everywhere. You do want proper layering. My ideal world, here, which I've only lived up to on homebrew projects, and never managed to in shipping code, is something like Patrick Buckland describes for Magic. The game state carries no cosmetic information; you have a sort of document / view paradigm with your game state. Your low-level renderer never talks to your game state or vice-versa. You have a high-level graphics engine that looks at your game state and describes it to the renderer. In my production engines, I've always let this ideal slide and stored information about meshes, textures, and purely cosmetic animation and effect states in the game entities themselves.
But all I'm really trying to say with the last post is that code that does this:
gGraphics->DrawMesh(myMesh);
or this:
Graphics::Inst()->DrawMesh(myMesh);
is no worse than
gApp->GetGraphics()->DrawMesh(myMesh);
I suspect Noel isn't too fond of that last one either (gApp still being a global) and wonder what his alternative is.
For Gregg's thing, the ServiceLocator, I don't immediately see the advantage to adding the complexity. Not for testing, anyway. Assuming I have the source to what gGraphics points to, even if that's an untestable class that does lots of file loading and takes over the hardware, it's not too much work to change it into a GraphicsI* for which I then write a mock (I've done this on two different legacy projects, took about half a day each time) and then my tests don't look much different from Gregg's:
gGraphics = new GraphicsMock();
classThatUsesgGraphics->DoTest();
And if I'm using some kind of static Inst() function in my renderer, I move that into GraphicsI.
>It sounds like in Noel's perfect world,
>we have to copy our game state every frame -
>our update function would take the old game
>state as its input and spit out the new game
>state.
It's not a dream anymore. Multicore requires it, because one is typically working on several consecutive frames at once across the cores. A truly high performance game already requires zero globals, except a single ring buffer of frame-local data:
struct gApp
{
Frame m_frame[kFramesInFlight];
};
Posted by: Bryan McNett | July 24, 2009 at 11:41 AM
It's not about the difference between
gGraphics->DrawMesh(myMesh);
and
Graphics::Inst()->DrawMesh(myMesh);
It's about constructing objects with the interfaces they need to do their work. So whatever objects you have in your codebase that need to do graphics work, you construct them with a Graphics* (or IGraphics*) and they use that throughout their lives. That reduces coupling in your client objects, and also restricts the knowledge that your Graphics thing is in fact an awful singleton/global.
Posted by: Charles Nicholson | July 24, 2009 at 11:45 AM
Interestingly enough, if you take the following statement: "Speaking of ideals, even if you have globals, you don't want any global to be accessed from anywhere. You do want proper layering." and follow it through, you get a proliferation of pointers (which you kind of decried before), which is really what you want. If something is intrinsically messy, you don't hide it behind globals and global-enabling accessors. Well, you do if you don't want to understand what's going on.
Posted by: Mat Noguchi | July 24, 2009 at 01:41 PM
The ServiceLocator thing sounds complex but in actual use it's a joy and not complex at all.
It's not even complex to implement if you look at the code. It's rather short and elegant. I was skeptical myself before I was exposed to it.
Before we basically had gApp and one way or another almost every module would eventually need access to something in gApp. Then it was broken into services and now each module only needs access to the services it uses. Usually that's about 1/10th to 1/15th of what used to be in gApp.
It's helped not only with globals but also with dependencies so that changing one service means only modules that use that service get effected or recompiled. Before, since all the stuff was combined in gApp that meant changing almost anything ended up recompiling everything since gApp both included a ton of stuff and a ton of stuff included gApp.
Posted by: greggman | July 25, 2009 at 01:20 AM
We've mostly achieved "the ideal" - our renderer only pulls data from the game simulation, and the latter knows nothing about meshes, shaders, textures etc. The renderer itself is split in two parts, and what you'd call the "lower level" doesn't know about the game, it just submits render calls to DirectX.
An additional layer of separation comes from the fact that we use heavily Lua for all game logic and many "engine" parts; for example, our last few games were city building strategies, and the C++ code knows nothing about the economy - has no notion of money, buildings, units, upgrades etc; it also has no notion of dialogs, buttons, scroll bars etc.
And it feels less dirty when you access globals in Lua :-) Not to mention the language+runtime gives you the tools to e.g. inspect all globals used, automatically serialize some of them in savegames etc.
Posted by: Ivan-Assen Ivanov | July 27, 2009 at 10:18 AM