You know, it occurred to me that some folk may find it interesting to see how we manage to port the Game Maker runner to other platforms, so I thought... Let's write about it. I then thought, well... it's not specific to the runner, Russell and I have been doing this for almost 20 years. I'll use the runner as the example, but the method holds true no matter what you do.
So the first thing you have to get into the habit of doing is abstraction. This doesn't mean layer upon layer of code and calls, but a very thin level that can remove any real platform dependence. Now, since I'm a graphics guy at heart, I'll talk about abstracting graphics code, but the theory is the same be it networking, audio or a basic file system. So here goes...
When we got the C++ runner it was all tangled up with windows specific code, from DirectX to MFC (Windows Foundation Classes), so the first thing we had to do was remove all the specifics, well... as much as we could. We spent months removing MFC and started using a simple WIN32 interface as this is much simpler and gave us control over the main loop which was vital to actually running on other platforms. We also started to add a thin layer between Game Maker, and DirectX. This meant that instead of calling D3D directly, it now called one of our functions, and we called D3D. This was done throughout the code, from creating textures and surfaces, to rendering lines and triangles.
The first thing we did was takeover the screen/canvas creation. This means we're now creating the device and have access to all the normal D3D functions inside our little world, and as far as Game Maker knows, it's simply asked to OpenWindow().
Next, we want to get something drawn, so we change all the rendering calls to a call to our triangle rendering instead so we now have access to all the vertices and can draw flat/coloured triangles where all the sprites would be. Most of these changes are pretty straight forward, and are a simple search/replace. Change DrawPrimitive() into a DrawArray(). Once that's done, we can start to change ALL the primitive types from being D3D specific, into our own custom values. So rather than using D3DPT_TRIANGLELIST, we now use our own ePrimType_TRILIST. So far so good.
Now there's lots of drawing code inside Game Maker, fonts, sprites, tiles and backgrounds and so on, and we really don't want to have to rewrite these every time. However, now that we have something that will render triangles in an abstracted way, we can rewrite them once, into OUR format. So the font rendering will now make vertices into our buffers, and use an ePrimType_TRILIST to draw them. Hay-Presto! The font code no longer needs D3D to render things. But what about the textures it uses?
Well, we created a new CreateTexture() call as well, which returns a simple void* which then allowed the internals of texture management to do whatever it liked. It also has the advantage that the texture system can now resize/resample textures if it liked, as Game Maker will never know; very handy when memory is tight! After all, Game Maker doesn't really care what the call is, as long as it can set the texture, so this works well.
So now we have the ability to render primitives, and create/change textures, so what else? Well there are lots of render states to do, so we'll need to abstract them as well. This means things like the D3D culling renderstate (i.e. D3DCULL_CW) now changes to eCull_CounterClockwise and so on, until every state, every D3D call has been abstracted away into a new interface. But why bother? Well, for a start it means that we don't have to include the D3D headers on other platforms! D3D obviously has windows specific includes internally and that would end up being a nightmare, but my abstracting things a little, the other platforms have become much simpler to port to.
And, although it seems odd... D3D is in effect our first port. Game Maker now runs using YoYo's interface and API, which then has a set of classes which translate things into D3D calls (which has been upgraded to DX9 BTW). Theres lots of other functions which we abstracted, matrix operations, viewports, grabbing screens, surfaces and render targets and the like, but at the end of it, we have a clean, non-D3D interface. And we can now port it easily.
Now, this method is true of any platform specific API you care to mention. We also changed the Audio system on windows to XAudio2 (the latest DirectX audio system), and we did this by simply having calls to Load a sample and return a void* which Game Maker can then use as a handle to the sound,midi or MP3.
Now... the real trick here is to get most of the code to be platform independent. Although things like fonts draw to the screen, they don't have to know anything about the underlying API. All it wants to do is set a texture, some blend modes, and then give you some vertex data. The interface then handles the rest.
Once you have a set of files that ARE platform dependent, you can then port these to any system you like. The real win for this, is that any new feature you add to Game Maker, will usually appear on all the platforms at once. Only the most obscure thing like grabbing screen rects without using the CPU (the PSP needs this) will have to be hand crafted. But other things like optimising font rendering will be the same on all platforms, and only require to be written once.
So, for the record.... we now have a Win32 DirectX 9 render, and PSP render, and an iOS OpenGL ES render. Making a Mac OpenGL render would now be trivial as the OpenGL ES is actually a subset of it, so we can actually ADD features!
As you can see, making a true platform independent bit of code is fairly simple, as long as you think ahead and don't try to be too clever. It also means that all systems benefit from any core changes, and porting to another system can sometimes be achieved rapidly if required.
Now, porting to something like iOS is interesting because your supposed to use Objective C, but in reality... anything that can call C++, can use the whole runner. We use Objective C to create the display, flip the screen and get the touch input, but everything else is done in the normal way using the C++ runner. Any system that allows C++ code, can be done like that making it a trivial port (i.e. simple to do, but takes a little time). If a platform doesn't allow calls to C++ (like XNA), then this means you have to write directly in the provided language, and that then becomes a monster task to rewrite the whole engine. It also means any improvements to the C++ code must then be replicated to the new system, which isn't very nice.
The Game Maker C++ Runner is now very portable thanks to the abstraction it's received, and we also know it's very hardware independent as it runs on Intel, MIPS and ARM cpus. Different CPUs can also give some headaches as byte order can flip, and many systems don't let you access INTs on non-INT boundaries and so on. We've now been through all of that, and now have a very portable engine which is looking good for the future.
So there you go... This is how we ported the runner over, and how other platforms suddenly seem to spring up. All that's really missing from it are some of the features we removed initially to get it all working, and they will come back in time, and one day, the C++ runner will be THE way to run Game Maker games.
No comments:
Post a Comment