For now, I'm sticking to SDL 2.0, but I want to be able to change toolkits later on without having to rip the guts out of my code. I also want to be able to react to changes in the SDL 2.0 spec -- it's still in development, remember -- without having to rip the guts out of my code. So I'm abstracting all the toolkit-specific stuff that I can behind a framework of abstract interfaces that I can implement on a toolkit-specific basis.
By way of a for instance, let's consider timing code. I'll need to be able to figure out how long I've spent on any given frame if I want to have any hope of doing proper animation, but at the same time high-resolution timing is a tough thing to get right if I'm doing it purely from scratch at the operating system level, and any two toolkits I might choose are likely to do it differently. Besides which, I only really need a single high-resolution timer that can keep track of its state from frame to frame -- or at least that's what I think about my requirements now -- and I don't need nearly as much of an interface as, say, SDL provides.
So I can define an abstract Timer class pretty concisely: Timer.h (BitBucket)
class Timer { public: virtual ~Timer() { } virtual void sleep(const int msec) = 0; virtual void startFrame() = 0; virtual double frameTime() = 0;
Basically we have a virtual do-nothing destructor and an interface of pure virtual functions that a later class gets to implement. (Since I'm passing the int to sleep() by value I probably don't need to list it as const, but I'm still coming to grips with modern const-correctness procedure. Look for stuff like this to change in the future.) Note that there's no public constructor: I want Timer to be an interface to a single high-performance timing system, which means I don't want client code to be able to create Timers with impunity. That's all going to be controlled strictly by the app framework, on which more below.
Here's the rest of the class:
protected: Timer() { } private: Timer(const Timer& rhs); Timer& operator=(const Timer& rhs); };
All this does is make sure Timers -- and, more importantly, their derived classes -- can't be passed by value or copied around. Pure reference semantics for these guys; I don't want to pass my Timer by value inadvertently and get a copy that doesn't properly update the global tick count, or get different parts of the animation code out of sync.
Meanwhile, I also have an AppFramework class that hands out Timer objects: AppFramework.h (BitBucket)
class AppFramework { public: AppFramework() { } virtual ~AppFramework() { } virtual GlContextPtr glContext() = 0; virtual InputPtr input() = 0; virtual TimerPtr timer() = 0; private: // As usual, no-copy semantics AppFramework(const AppFramework& rhs); AppFramework& operator=(const AppFramework& rhs); };
The AppFramework is the gatekeeper for all the various toolkit-specific interface systems I need to get this project off the ground. Toolkits get initialized -- SDL_Init gets called, for example -- in the framework's ctor, and appropriate shutdown code executes in its dtor. The framework mediates allocation of and access to OpenGL windows, input devices, and so on. In the program code (tungsten.cc -- BitBucket), it looks like this:
int main(int argc, char *argv[]) { AppFrameworkPtr framework(new SdlAppFramework); GlContextPtr context = framework->glContext(); TimerPtr timer = framework->timer(); InputPtr input = framework->input();
This gives me access to the interfaces I need while pushing almost every reference to SDL into derived classes (SdlAppFramework, SdlGlContext, SdlInput, and SdlTimer respectively). Those derived framework classes list SdlAppFramework as a friend class, giving it access to their (protected) ctors while preventing anyone else from inadvertently creating one.
Could I do this all with a singleton class for each framework component, rather than mess around with protected ctors and friend classes? Well, sure. There are a few things I like about this approach, though, that make me prefer it for now:
- Most importantly, explicitly controlling the allocation of timer objects lets me be certain that they only get created after the underlying toolkit has been initialized. With timers this probably isn't such a big deal, but for GlContext -- which gives me an application window and a link to the graphics driver -- I really need to make sure initialization happens in the right order. If only AppFramework can give me a GlContext, and AppFramework's child classes do toolkit initialization right in the constructor, then this order-of-operations problem gets resolved essentially for free.
- By centralizing the creation of framework objects in an AppFramework class, I can make sure that everything I create is consistent with the underlying toolkit's assumptions internally to that AppFramework. (Maybe timers can only be created after a window is opened in some toolkit I pick up down the road -- I can keep that all inside the AppFramework's implementation rather than ask the client to worry too much about it.)
- It's not unreasonable to think that I might want to have multiple timers floating around later on, and this lets me push that decision into the SdlAppFramework implementation, rather than the Timer itself. Current clients of Timer don't have to care.
- It's not obvious to me that implementing virtualized singletons is any less messy than the way I'm doing things now.
Next up for consideration is the GlContext class -- and maybe a better way to post code listings than <pre> tags.
No comments:
Post a Comment