Bulkhead Interactive showcases how to port a PC/console game to Nintendo Switch
The upgrade brought along some significant rendering changes that made the end presentation look noticeably different due to the introduction of the new filmic tonemapper, among other things. This shift required color-grading work, some tweaks to specularity, and a fresh pass on the reflection setup in levels to bring the look of the game more in line with the artists’ original vision.
Immersion is important for a story-driven game, so we want players to spend as little time as possible in loading screens. To that end, Unreal Engine provides a couple of solutions to stream levels, targeting a wide variety of game architectures from general level streaming to the more specialized world composition. For a mostly linear game like The Turing Test, general level streaming is a solid solution that allowed us to amortize the cost of loading chunks of the game world in and out, while maintaining low memory overhead and getting rid of as many blocking loads as feasible.
The downside, however, is that the garbage collector has to work overtime, causing spikes on the game thread during gameplay. On a relatively constrained portable system like the Nintendo Switch, this caused us some considerable woes during level streaming. New actors and components have to be registered, initialized, and collated when they’re introduced into the world, while the garbage collector is dealing with entities leaving the active playset. This was compounded by complex actors with dozens of components used throughout the game which, in general, just didn’t come up as an issue on other platforms.
We experimented with the possibility of overlooking the hitches and allowing players free roam during transitions, however, that led to unstable physics interactions in the form of vital actors (objects needed to progress through the level) being ejected from the scene after colliding with them during the starved world ticks. In the end, we settled on introducing a small delay where the player would be rooted in place but allowed to look around, while the next level was streamed in, as a sort of meet-half-way solution.
If there is one great piece of advice we can share for similar cases, it is to look into the available options for level streaming (Project Settings > Streaming). These can limit the amount of time per frame spent on actor and component initialization and teardown, and other similarly valuable variables.
All previous releases of The Turing Test were only available in English, but EFIGS localization is a base requirement to pass certification on Nintendo Switch. This proved to be a challenge since The Turing Test features a lot of written information baked into environment textures that act as crucial storytelling elements.
The solution to this was to dynamically create widgets and render them to textures. This was done only during loading screens or when the player manually changes the game language. This system saved us the overhead of having multiple textures for each instance of text in the world and required no additional work during iterations on the translations.
To ensure a smooth experience for the player, we had to reduce the impact of the rendering and game threads on the framerate of the game. For the most part, we were GPU-bottlenecked once gameplay code was optimized across the board. To diagnose our main focus areas, we utilized the various STAT commands available via the command line. Once we identified an issue, we drilled deeper with either the built-in profilers—including the recently introduced Unreal Insights—or the platform-provided graphics debugger to analyze where the cost was coming from and drew up solutions.
We translated more expensive Blueprint operations into C++ and avoided unnecessary actor ticks where possible. Replacing a lot of skeletal meshes with static ones and removing collisions from moving objects (like fans) ensured the game thread costs would be more manageable. We encountered a few problem areas where transparency flagged up as an issue. Using the shader complexity view mode helped us identify where overdraw was occurring. We then modified materials, meshes, and particle systems to ensure the framerate would be consistent.
A potentially polarizing choice we made was to limit the game’s framerate to 45 fps as we found it was a reasonable target we could maintain throughout scenes of various logical and graphical intensity. It’s not quite the coveted 60 frames per second, but still a step above 30. We saw little gain in slightly modulating some graphical options up or down for either of the standard frame rates.
Alternatively, we could have leaned on modern rendering features provided by Unreal Engine, such as dynamic resolution, but we settled for a more traditional approach after some deliberation.
Among the most intensive real-time rendering features we identified on the Nintendo Switch were ambient occlusion and screen space reflections. We deemed them vital for the visual fidelity of the environment, so with care, we tuned their quality to what we found reasonable to meet our targets. Much of the graphical fine-tuning was done on the fly during development thanks to the brilliant support for that in the engine. As we slowly converged on the final values, we utilized device profiles leading up to the end of the project.
In the end, the tools and features provided by Unreal Engine allowed our game to quickly take form on the Nintendo Switch and gave us time to optimize our assets and code, providing players with a smoother, tailored experience.