I wrote in an earlier post about the potential for Rubygame to have multiple engines in the future. (I called them “backends”, but I think “engine” is a better term.) Rubygame could have one engine that uses OpenGL and OpenAL, one engine for Android devices, etc., to make it more portable. I don’t know if this is something I’ll actually pursue, but it’s an interesting concept, so I felt like writing about it in more detail.
When I say engine, I mean the underlying software libraries that do the low-level work of creating the display window, pushing pixels, loading image and sound files, and so on. Rubygame has always used SDL 1.2 (and SDL_gfx, SDL_image, etc.) as its engine, but SDL has some shortcomings that have bothered me over the years, and I’d be happy to ditch it. One of the biggest thorns in my side with SDL has been how much of a chore it is to acquire and use it on Mac and Windows. Back when Rubygame was a compiled extension (before the transition to FFI in Rubygame 2.6), Mac and Windows users couldn’t install Rubygame at all unless we provided a pre-compiled binary, or unless they installed a C compiler and the SDL development packages. Even then, Mac users required a special version of the Ruby interpreter, called RSDL, to use Rubygame (or any other Ruby library that used SDL).
Both of those issues should be solved now, but downloading and installing the SDL libraries themselves is still a barrier to using Rubygame. It would be much nicer if Rubygame could work right out of the box, without the user needing to install any extra libraries.
Accomplishing that would require leveraging libraries that are likely to be already installed, such as libraries provided by the operating system. But because many libraries provided by one OS aren’t available on other OSes (e.g. DirectX is not available on Mac or Linux), Rubygame would need to support multiple engines, and be able to detect at runtime which engines are currently available.
(There are some libraries, like OpenGL, that are cross-platform yet often provided by the OS. But, I don’t think it’s possible to recreate the full functionality of Rubygame using only OS-provided cross-platform libraries. For example, I don’t think there are any such libraries for loading a variety of popular image and sound formats.)
Supporting multiple engines would be a lot of work, and to be honest I’m not sure it would be worth it. A more practical approach might be to find some suitable cross-platform libraries and focus on minimizing the hassle of installing them. But, a modular design supporting multiple engines is an interesting challenge, at least to daydream about.
The first hurdle in a multi-engine design would be separating out the parts of Rubygame that are specific to the current engine, SDL. Off the top of my head, that would be the Screen, various events, Surface, Sound, Music, and TTF classes; in terms of functionality, it’s the display window; input and window events; image loading, manipulation, and display; audio loading and playback; and vector font loading and rendering. The rest of Rubygame is Ruby code built on top of those features, and could be shared across engines.
The biggest pitfall to avoid would be limiting Rubygame to merely the basic functionality that all the engines provide. For example, if one of the engines doesn’t support fullscreen mode, it would be misguided to cripple other engines that do support fullscreen mode, in an attempt to provide a consistent API. That strategy would make Rubygame the “lowest common denominator”, inheriting every weakness of each engine, but none of their strengths. It would be less useful than the current wrapper around SDL, yet it would take more effort to create and support. Obviously, that’d be no good.
Yet, there is a need to provide a consistent API. A game developer doesn’t want to have to worry about differences between implementations. If you have to do that, there’s not much benefit in using a single library (Rubygame), rather than working with each separate engine directly. Ideally, a developer should not have to know which engine is being used; if they use the API properly, as documented, their game should just work.
So, Rubygame would need to provide a consistent API that works across all engines, yet it would also need to accomodate differences in implementation and functionality in each engine. How can these two conflicting requirements be reconciled?
Well, this is a problem web developers/designers grapple with every day. It is the source of much complaining about Internet Explorer 6, rendering differences between browsers, and so forth. Two common strategies for dealing with this problem are progressive enhancement and graceful degradation. Both strategies address how to create content that works across all “engines” (browser implementations), yet can still take advantage of features when they are available.
Progressive enhancement prescribes starting with the baseline content that all engines support, then allowing the experience to be enhanced depending on what additional features are available in each engine. Graceful degradation prescribes starting with all the bells and whistles, then allowing for the experience to degrade towards a baseline depending on what features are missing from each engine.
It’s worthwhile to note that both strategies recognize that there is a baseline set of required features that all engines must provide, and beyond that are optional extra features that any given engine may or may not provide. What’s more, the engine must provide a way to detect whether an optional feature is implemented, and/or automatically ignore instructions for optional features which are not supported.
These concepts are applicable to Rubygame as well. Actually, Rubygame already does it already, to a limited extent. SDL is a required library, which Rubygame cannot function at all without; that sets the baseline. The other libraries (SDL_gfx, SDL_image, SDL_mixer, and SDL_ttf) are optional, and Rubygame can still function without them, albeit with fewer features. And, a game developer can test for the presence or absence of these features by checking whether the related classes or methods are defined. For example, the various Surface#draw_* methods only exist if SDL_gfx was loaded; even more specifically, the Surface#draw_curve method only exists if the loaded version of SDL_gfx implements that feature.
In theory, that would be sufficient to enable a game developer to use either the progressive enhancement or graceful degradation strategies to adjust the program’s behavior depending on what features are available. At least, it’s enough to gracefully inform the player that some feature that the program depends on is not currently available. But, Rubygame would need a much smarter system if it were to support multiple engines in a practical way.
For one thing, checking for the existence of individual classes or methods is just not going to cut it. Consider the class method Surface.load, which loads an image file of any supported type. Surface.load only exists if the SDL_image library was loaded, so a program can assume that if that method exists, it can load at least some images. But how can a program check that it supports a specific file type, say PNG? The docs say that it supports PNG on some systems, but there’s no way to check that this system supports PNG, except to try to load a PNG file and check whether it succeeded or failed. That’s no good at all.
It would be much better if there were a central list indicating which features are available: which image types can be loaded, which shapes can be drawn, whether fullscreen is available, and so forth. This simplify the process of checking whether a feature is available, as well as laying the foundation for a multi-engine design.
In a multi-engine design, each engine would provide a list of features that it provides. Or, possibly, two lists: an “ideal” list which can be inspected without actually loading the entire engine, listing all the features it could provide on an ideal system; and a “reality” list, available after the engine is loaded, listing all the features it actually does provide on the current system.
In theory, a program could indicate which features it depends on, and which features it can optionally use, and Rubygame could intelligently choose the best available engine (or combination of engines) to satisfy those criteria. A program could indicate whether it has a preference for a certain engines (to act as a tie breaker if multiple engines can provide the same features), or even that it utterly depends on certain engines (and should raise an error if the engines are not available).
And, if Rubygame is automatically and dynamically inspecting and loading the engines, there’s no reason why it would have to be limited to engines that ship with Rubygame. People could create and distribute custom engines as gems or packaged with their game, tell Rubygame where to look (via a PATH-like environment variable), and Rubygame would detect and consider the custom engines just as it does the official engines.
Actually implementing support for multiple engines could be an interesting metaprogramming exercise. A naïve way to do it would be for each engine to directly modify the main Rubygame modules and classes. But, that would leave a mess if there was a problem loading one of the engines, and it wouldn’t let Rubygame load an engine and check its “reality” features list before applying it.
A better approach would be for each engine to define a mixin module for each Rubygame class it wanted to modify. To activate the engine, the Rubygame classes would simply be extended with those mixin modules. That would allow the engine to be loaded and checked before activating it, which is good.
One potential shortcoming of both those methods is that there’s no clean or simple way to deactivate an engine or switch engines once one has been activated. It’s too bad Ruby doesn’t provide a way to un-extend/un-include a mixin module, or the second method would be pretty much perfect. A more complex method could involve dynamically rebinding methods to transplant them from the engine’s modules/classes to the Rubygame classes, then unbinding them to deactivate the engine. But this would be complicated to achieve, and I’m not sure deactivating/switching engines would be that important, so it may not be worth the trouble.
Anyway, as I said, I’m not sure this is something I would pursue. It’s an interesting idea, but it would be a lot of work, and there are probably more fruitful ideas to pursue. I may just leave it as a thought experiment.