Image courtesy of Joy Way

STRIDE developer Joy Way shares their VR cross-platform multiplayer practices

Artem Tarasov, Multiplayer Lead for Joy Way
Hello, I’m Artem Tarasov, Multiplayer Lead for Joy Way.

Joy Way is a Cypriot VR game development and publishing company. Having started about six years ago, we’ve worked on multiple projects for PC and standalone VR across SteamVR, Meta Quest, PlayStation VR, Pico, and others.

The development of STRIDE, our action parkour VR game, was a challenge for the team, especially its multiplayer modes, which feature cross-platform play. We had to deal with several platforms, various input devices, physics-based locomotion, correct hit registration, and other tasks.

This blog will cover our solutions to some of the challenges we encountered while developing multiplayer game modes for STRIDE.
 

You can use this blog as a guide for leveraging Unreal Engine to develop a cross-platform game and connecting it with custom backend services. This blog will be useful for Unreal programmers that work with both C++ and Blueprints.

Writing platform-specific code

The first challenge every developer faces when it comes to developing a cross-platform game is the necessity of writing platform-specific code.

The first and most common case is using platform-specific plugins and modules. In Unreal, you can Whitelist and Blacklist platforms for each individual module:
And for plugins, you can do the same with BlacklistPlatforms, SupportedTargetPlatforms, and WhitelistPlatforms (choose one or multiple parameters).
After you whitelisted or blacklisted platforms in order to use modules as C++ dependencies, you will need to include the module name in PublicDependencyModuleNames and/or PrivateDependencyModuleNames. And from that point on, things become less trivial.

First, you need to exclude modules that are not supported on some platforms. The most straightforward way to achieve this is to use if/else statements in your modules .Build.cs file.

For example:
Secondly, you need to use platform-specific compilation flags. For example:
You can read more on C++ preprocessor directives here.
A multiplayer map in STRIDE

Handling multiple platforms running on one OS

When developing STRIDE, we faced the problem that some of the target VR platforms (Pico and Meta Quest) were both running on Android. Because of this, several problems arose: from that point, we could no longer use Unreal Platform’s configs to set apart Pico and Quest intersecting config values. By default, in code, you cannot determine on which platform you are. Here are our solutions.

The config values problem can be solved using an -ini arguments injection into the Unreal Build Tool. You can implement this in your CI/CD build scripts and pass -ini configurations into the Unreal Automation Tool to override configs. Pass in this format:
You can read more about config injections here. Also, there is a way to apply injected config values on a packed game by modifying the Unreal Automation Tool. Read more here.

From now, you can pass a config value to UAT to determine which platform you are on.

The next step is to add platform definitions for the Pico and Quest devices. Note the difference between GlobalDefinitions from Target Rules and PublicDefinitions from Module Rules. By default, UAT produces two builds: one for the editor (to run Commandlets during build) and one for the target platform. GlobalDefinitions are definitions for the whole target. This means that if you put a Pico/Quest platform definition in GlobalDefinitions, then the editor build (required to run Commandlets) will also have the unwanted platform definition. Considering the above, you want to place a platform definition in PublicDefinitions from Module Rules. Here is how you can do it:
Now you can use familiar preprocessor directives to determine what platform you are on in C++ code and use BlueprintFunctionLibrary to expose platform checks to Blueprints.

Connecting the game with custom backend services

This part of the blog will cover our approach to the integration of backend services. Our backend consists of multiple HTTP APIs and a WebSocket API. The implementation of HTTP requests and WebSocket events is fairly simple, so I want to focus on our approach to chaining asynchronous calls.
Сross-platform hub location in STRIDE
Firstly, we started implementing API calls using lambda function callbacks for responses. However, very soon, we had a large bunch of nested calls, and the code became quite difficult to maintain. So we decided to make each request a separate UBlueprintAsyncActionBase.

It is pretty straightforward to use UBlueprintAsyncActionBase automatically generated nodes in Blueprints. Here is the programming guide.
 
Get Stride Net User Data

However, we still have an important problem to solve: where do we call these nodes? In some cases, you can make calls in-game entities, and this will be a good placement. But what about calls on the level of GameInstance? Our solution is to use an extended UObject for the «Worker» entity.

Workers are UObject derived classes for controlling the life cycle of which we use GameInstanceSubsystems. You can read more about programming subsystems here. It is not necessary to use GameInstanceSubsystems. Moreover, probably, using LocalPlayerSubsystems would be a better solution.

Workers make maintenance of call chains very straightforward.
Now I will demystify the extension of UObject and share a few unobvious tips.

Primarily, there is only one thing you want to extend. GetWorld to call global functions with WorldContext. Here is a code example of Worker GetWorld override.
Pay attention to the first check on CDO. This will make sense further.

Here is the code example with the creation of Worker.
Summarizing the above. You should inherit from UObject, override GetWorld, then inherit from your C++ Worker, and finally, instance the correct object in your Worker’s life cycle core. Here is how to get a class from Blueprints to instance a correct object.
Now about CDO. As you see, we assign the TSubclassOf variable on construction of GameInstanceSubsystem. And there, you might have an issue unless you don't check for CDO. We faced editor crashes and major asset system troubles without a CDO check, so you might too.

Separating each request and exposing request chains to Blueprint graphs helped us to get rid of “spaghetti code” and made backend-related code much easier to maintain, which resulted in faster iterations and fewer bugs.
If you are interested in learning more about our games and getting a behind the scenes look of VR game development, follow Joy Way on Twitter and join our Discord server.

    Get Unreal Engine today!

    Get the world’s most open and advanced creation tool.
    With every feature and full source code access included, Unreal Engine comes fully loaded out of the box.