The game I’m currently working on uses the Steamworks SDK to integrate with the Steam client and do nice things such as achievements and cloud saves.
I made my own custom engine for the game, and my initial approach for integrating the SDK was to simply link to the shared library and ship it as part of the game.
The game itself is DRM-free, and this approach allowed it to work without the Steam client installed, by making sure that if SteamAPI_Init fails (e.g., because there is no Steam client) no other Steam function is called.
When the game was accepted to be published on GOG though, I knew I had to figure out a different approach. I had no intention of juggling multiple builds of the game for each storefront (times the number of supported platforms). Luckily there’s a way to avoid this: dynamic loading of the various SDKs! When the game is started, it checks if the shared library for each SDK was shipped with the build; as soon as one of them is found it is loaded, and the engine loads the functions we want as function pointers.
Caveats
One thing to note (and a potential drawback of this approach) is that dynamic loading only works for functions exported with C linkage, as C++ function names are mangled in a platform/compiler dependent way. In the specific case of the Steamworks API this means that you must use the flat API instead of the C++ one. This is not a problem for me, since I have to use the flat API anyway for other reasons1.
It gets even more annoying with the GOG library since it only has C++ symbols, so you’re forced to first write an compile a C wrapper. Again, not a problem for me, since I would have had to do that anyway even if I just wanted to link to the library at compile time.
Implementation
Setting up dynamic loading is quite easy, especially if your engine is already using something like SDL that abstracts away the platform dependent dynamic loading. My approach is to create a class that wraps the SDK, and has the pointers to the API functions as members. When the class is initialised it attempts to dynamically load the library. It is allowed to fail, and the class can be tested to check if the library was loaded; this way, you can simply omit to bundle the shared library with the game to disable the Steam integration, instead of having to compile a different build of the game.
|
|
|
|
Why not just link to all SDKs at compile time?
Sure, you could also do that. Link against the Steam SDK, the GOG SDK, and so on. But then you need to bundle unnecessary libraries with your game, and you still have the problem of letting the engine know which SDK it should be using. By dynamically loading, you tell the engine about it by only bundling the shared library you want, and there is only one build of the game (per platform) that you have to compile.
Abstracting the SDK behaviour
Of course you still have to deal with the fact that each SDK does things in a different way. My solution for this was to make a new SDK class that encapsulates the common methods (e.g., setting an achievement), and chooses what to do conditionally based on which shared library was loaded (or does nothing if no library was found).
-
closed-source SDKs that export C++ symbols are a terrible idea, as they don’t work when using a different compiler or a different version of the same compiler used to compile the shared library. In my case I am forced to use the flat version of the Steamworks SDK because I use mingw-w64 to cross-compile the Windows version of the game, and the SDK is compiled using MSVC. The Linux and macOS versions are able to use the C++ API functions, but the whole point of this post is how to avoid having different code for different builds of the game, so you can imagine why I’m not keen on using the C API for only one platform. ↩︎