The black art of platform conversions: The (not so) straightforward port
Sponsored article: In the second part of a series dedicated to platform adaptations, Abstraction takes a deep dive into the first part of the production process, the "straightforward port"
As we stated in our previous article, the process of making a proper Abstraction-grade adaptation has several distinct steps.
Last time, we covered the assessment process, this time we'll cover what comes after that. So, let's assume that the assessment is complete. A quote has been sent out to the client and accepted. Terms and conditions have been successfully negotiated, and a contract has been signed (one that all parties are happy with). The project can now be officially started, and that kicks off with what we call a "straightforward port." Since there's a lot of moving parts to this part of the process, we will be breaking it up into two parts.
The goal and deliverables of the straightforward port are simple: make sure the game runs on the target platforms 'as is', with no time spent yet on tailoring or optimization, apart from what is necessary to prove the game is running on the new platform. For instance, we might hook up a virtual mouse cursor controlled by a gamepad to support a game originating from PC that relies on this input device.
Based on the technology used in the original title, we identify the following approaches to get to the straightforward port:
- Switching platforms in an engine that already supports the target platform
- Adding a new platform to an existing engine
- Integrating the game in an existing engine
- Reverse engineering or recreating an engine or framework
- Recreating an entire platform
Let's examine each of these by explaining some of the challenges we have encountered through real-world examples and case studies.
Please note: In the interest of providing in-depth detail and imparting as much of our knowledge and experience, we will only cover the first two approaches in this article. The remaining three approaches will be covered in our next article, which you can read on this page.
1. Switching platforms in an engine that already supports the target platform
Many modern engines suggest that porting is a thing of the past and claim to offer a turnkey solution for "exporting" your game to any platform. This is very often not the case, as illustrated in the example below, taken straight from the Abstraction Vault.
- Totally Reliable Delivery Service (TRDS) | Tomas Lori (programmer)
Before I first started working on adaptations, the process of getting a game to run in another platform sounded a bit abstract (pun intended), especially when using an engine such as Unity in which you supposedly "create once, deploy anywhere."
The first step is to install all the necessary support for the platform (or platforms) you are now targeting. This basically comes down to going to the first party dev websites (you will need special access for this) and installing the proper version of the editor support.
A recommendation here: do your best to stick to LTS (long-term support) versions of the engine if you want to have the easiest time getting all the different console plugins to play nice with each other. For TRDS, Unity 2018.4-LTS was the version that could support Playstation 4, Xbox One and Nintendo Switch, so upgrading to that as soon as possible made sure that we could identify any bugs stemming from the upgrade early on.
Once you have your support installed, you can go ahead and press the magic "Switch Platform" button, and you should be almost done, right? Well, not quite. You may find that you are already experiencing issues, even at this early point: sometimes the switching fails, and you'll need to convert to a different platform as an in-between step. You'll probably want to set up your cache server, so you don't spend centuries each time you have to switch platforms. Sometimes keeping separate checkouts of the repo, one for each platform, is very helpful in case you want to work on more than one at the same time (some of the asset conversion steps are quite lengthy and not properly multithreaded).
After you have managed to switch platforms, you'll probably be greeted with a decent amount of compile errors. Unless your game has properly setup precompiles for platform-specific parts of the code and created stubs for places where that would be missing on other platforms, you will basically have to get this fixed before anything else. Our recommendation here is to try to get your project to compile ASAP by wrapping the pieces that have issues with #if TODO pre-compiler directives.
This is useful because it will allow you to get the full build compiling and have a high-level view of the things that need to be fixed before starting to fix them. Also, often enough, you can defer fixing these issues until later since there's a decent chance not all the issues are going to be relevant straight away. For example, if your original game supports leaderboards and you just "to-do" that part of the code, there is a good chance that on runtime this won't create an issue (unless you actively go into the leaderboards). This way, you can parallelize fixing the subsystems across multiple programmers and prioritize what needs to be fixed accordingly.
For example, on TRDS we had to strip multiple bits of Steam specific calls, in systems such as Achievements, Friends, Online Authentication and more.
Once your game compiles, you should ideally attempt to run it from the editor. Fixing any editor issues early on is extremely valuable because it will allow for much faster iteration times going forward. The more issues you can fix from the editor, the less time you'll spend making builds. At this point, it will become clear which of your previous "to-do"s you'll need to fix to be able to get into the game.
Some of the more inconvenient issues to fix are created by the difference between sync versus async calls to specific platform functionality. For example, you wrote your initialization code in a completely synchronous fashion (because in your original platform, all your required calls are sync), and then suddenly, your other platform has several async callbacks for that same initialization.
The fix for this is likely project-specific and depends on how easy (or hard) it is to defer the initialization of systems that are based on one another. Unfortunately, Unity's architecture makes it very easy for people to rely on the script execution order to have systems chain together (which does not map all that nicely to an async setup), which is a very common mistake.
If you find yourself in a position where a big refactor would be needed, there's a potentially easier solution: it is often possible to move all the chained initialization logic to a separate engagement screen that can be added at the very start, which will only transition into the game properly once the initialization flow has finished successfully. This will also help significantly when certain console actions require a reset of the whole flow, because it becomes trivial to just reset all state and reload our engagement screen and do a fresh start from there. TRDS ran into this exact issue and this approach helped immensely in avoiding a full rewrite of the initialization flow.
Once you can run your game from the editor (or at least the parts that you want to test run on the console itself), you should trigger a build. This will require some configuration from your build and publishing settings (which is usually decently documented) and the addition of all necessary platform binaries.
The binaries from the platform holders are probably already integrated (when getting the initial editor support), and with the documentation, you should be able to get them working without too much difficulty. On TRDS, we also required binaries from the additional plugins used by the game, and those did take considerably longer to get access to. Up until that moment, we were unable to make console builds! So, try to get the ball rolling on acquiring access to all these as soon as you can to prevent it from blocking your progress.
After building your game, you should be able to deploy it to the console, and it will (fingers crossed) work to the same extent as it did in the editor. There are some issues that are common at this stage, the most common being magenta shaders. This is relatively easy to fix, by changing some of your structs (POSITION -> SV_POSITION, COLOR -> ST_Target, DEPTH -> SV_Depth), making sure you don't use "sample" (it's a reserved word), and removing the register keyword to explicitly bind your samplers and instead use the automatic binding.
With all that out of the way, your game should now run, and if you are using a library that correctly supports controller input on your platform (such as Rewired on TRDS), you might even be able to get into your game and start playing around.
- It crashes...
So, navigate through your UI, get to your loading screen, go in-game and... it crashes! This is another one of the most common issues we run into, especially when coming from PC. Since desktop platforms are able to virtualize memory for you on the fly and consoles have a hard limit, there's a decent chance that an un-optimized game will just go out of memory and crash as soon as you hit gameplay.
What we usually do, to at least get past the issue until you can properly address it, is significantly reduce quality settings (and potentially try out texture streaming). If your scenes are big, this might not be sufficient, and you'll need to further reduce your memory footprint with more heavy-handed changes such as breaking down your scenes into more manageable chunks. Hopefully, the new Unity Addressables module will make this more easily manageable going forward (so consider trying it out if you plan to support your game across platforms in the future).
Now that your game works on the destination platform, the next step to take is what we consider makes or breaks an adaptation: proper tailoring. We'll discuss tailoring in a future article. For now, let's look at the next straightforward port approach.
2. Adding a new platform to an existing engine
When a game is built in an engine that doesn't support the new platform, and we have the source code to that engine, we will integrate the new platform ourselves. These are typically older engines that got replaced and were never updated to support the newer platforms (for instance, Unreal Engine 3 not supporting X1, PS4, or Switch), or proprietary custom engines, made specifically for the game or developer, that currently lack the support for the new platform.
It is a huge help if the engine is already set up with multi-platform development in mind, as we will see in the next example.
- Project X | Wilco Schroo (lead programmer)
When we were bringing X (code name) to Android, we started with code and assets from the iOS version of the game. The game itself was built upon a custom cross-platform engine that has seen releases on a multitude of platforms. The fact that the engine was already designed with multiple platforms in mind made it easier to integrate a new platform, but it's not always an easy process.
One of the first things to do when starting such an adaptation is getting a high-level overview of the systems that will need to be replaced or extended with code for the new platform. There can also be implementations for other platforms in the source code that can be used to implement the new platform. For X, we were able to reuse parts of the Linux and iOS implementation because they were similar to the implementation for Android (Linux being similar in low-level implementation and iOS being similar in that it's also a mobile platform). We also chose to reuse the OpenGL implementation that was already there.
Before any code could be written, the project and build pipeline needed to be set up. The X project used CMake to generate build files for the supported platforms. CMake is a cross-platform build automation tool that uses scripts to generate build files for a specific platform. Because it's cross-platform, you would think it should be easy to add a new platform configuration. This is, however, usually not the case and highly depends on how well the target platform is supported and how well the project configuration files are written. It took us quite some time to get it working for X both due to CMake/Android issues as well as adding a new platform to the configuration files.
Compiling for Android for the first time generated a lot of compile errors, and after a certain amount of errors, the compilation would just stop. Unless the fixes were trivial, we stubbed/disabled the erroring code with a set of preprocessor definitions that we use. This allowed us to continue with the compilation quickly (move on to the next set of errors) and make the fixing of all the errors parallelizable. After the compilation errors were all gone, we did the same for linker errors and ended up with a build that wouldn't run but would at least allow us to work on and compile the changes that we had to add for Android.
By using parts of the Linux and iOS platform implementations, we saved a lot of time in the first stage of getting the game running on the Android platform. Sadly, this wasn't the case for the OpenGL implementation that we wanted to reuse. It had been stale for a while and was not using an up-to-date OpenGL version, and the renderer implementation itself was also not compatible anymore with the game. This meant that we almost had to start over with the OpenGL implementation.
When we had enough systems implemented to start running the game, we took a similar approach to what we did with the compilation. We implemented only what we needed to get the game in a stable but not complete state so that we could divide up the work. This took a while because due to the issues with the OpenGL implementation we were staring at a black screen for quite some time. After a lot of debugging, we finally got something to appear, and soon after that, we were able to see the main menu show up on the screen.
We very much hope you enjoyed the first part of our deep dive into the straightforward port step of the platform adaptation process. You can read part two on this page, where we discuss integrating in existing engines, reverse engineering, and even recreating entire platforms from scratch!
If you would like to find out any further information or just have a chat then please contact us here.
Authors: Ralph Egas (CEO of Abstraction), Erik Bastianen (CTO of Abstraction), Wilco Schroo (lead programmer), Tomas Lori (programmer), Savvas Lampoudis (senior game designer)