Game Integration

C# Side

Please refer to the C# documentation.

C++ Side

Bifrost uses nlohmann JSON as its default serializer for the C++ server and as such, these instructions assume JSON as the serializer for the initial setup.

Setup Instructions:

There are a few ways to integrate Bifrost integrated in the game project:

  • Via static libraries and header files available in a share (provided by your Test/Tech lead)

  • By adding the Bifrost source directly to your game and building it locally

  • Via the UnrealToolkit (Unreal 4 or 5 games)

To integrate Bifrost as source and build it as part of the project, contact the SQ Tech team.

1. Linking bifrost

Skip this step if integrating Bifrost as source or as part of the UnrealToolkit. Get a drop of all Bifrost’s headers, static libs and all its required dependencies as mentioned on the installation page.

  • Add the path to the Bifrost include headers into the include directories property of the game project

  • Add the path to the Bifrost libraries into the library directories property

  • Link against the following libraries

    • Infrastructure.lib

    • Serializer.JSON.lib

    • Transport.lib

  • Add the name of any required platform specific libraries

    • Windows:

      • wsock32.lib

      • ws2_32.lib

    • Xbox (GameCore):

      • xgameruntime.lib

It is common for title developers to compile out Bifrost configuration in shipping configuration. How that is accomplished is dependent on the title. A couple of methods include:

  • A compiler preprocessor definition that is defined in the configurations that Bifrost should be enabled.

  • Build tools avoids adding Bifrost when generating the solution for shipping configuration.

2. Implement game specific error handling

Add additional error codes specific to the game project by creating a ReturnCode enum class with the underlying type int16_t. Then create a class inheriting the templated GenericResult to return custom error codes in hooks.

Create any return codes specific to the game project:

#include <stdint.h>

enum class GameReturnCode : int16_t
{
    /** The operation was a success. */
    Success,

    /** An error occurred trying to get the player. */
    PlayerError,
};

Create a GameResult header class:

#include <Bifrost/GenericResult.hpp>
#include "GameReturnCode.h"

class GameResult : public Microsoft::XboxStudios::Bifrost::GenericResult<GameReturnCode>
{
public:
    GameResult(GameReturnCode returnCode);
};

An optional BoundedString can be added with the GameResult to include an error message with the return code.

Implement the GameResult class:

#include <Bifrost/ErrorHandling/BResultCategory.h>
#include "GameResult.h"

using namespace Microsoft::XboxStudios::Bifrost;

GameResult::GameResult(GameReturnCode returnCode) : GenericResult<GameReturnCode>(BResultCategory::Game, returnCode) {}

3. Create messages

The following messages support JSON serialization using nlohmann library.

Sample messages:

Assume the following is in a TestGameHooks.h file.

#include "Serializer/json.hpp"

struct PlayerLocation {
    int X;
    int Y;
    int Z;

    NLOHMANN_DEFINE_TYPE_INTRUSIVE(PlayerLocation, X, Y, Z)
}

struct PlayerLocationRequest {
    int PlayerIndex;

    NLOHMANN_DEFINE_TYPE_INTRUSIVE(PlayerLocationRequest, PlayerIndex)
}

struct PlayerLocationResponse {
    std::string PlayerName;
    PlayerLocation Location;

    NLOHMANN_DEFINE_TYPE_INTRUSIVE(PlayerLocationResponse, PlayerName, Location)
}

Note that the above macro will serialize property keys as named in the structure. The key names are case sensitive and must match exactly what the client expects. To customize the property key names or property value, implement to_json(json& j, const PlayerLocation& p) and from_json(const json& j, PlayerLocation& p) methods instead of the macro. More details can be found here.

4. Creating game hooks

Bifrost hooks may have any of the following method signatures:

// A hook that expects a request and has a response.
BResult(const TRequest&, std::unique_ptr<TResponse>&);

// A hook that expects no request and has a response.
BResult(std::unique_ptr<TResponse>&);

// A hook that expects a request and has no response.
BResult(const TRequest&);

// A hook that expects no request and has no response.
BResult();

Where

  • BResult represents the result of the hook. Note: The GameResult explained in step 2 inherits from BResult.

  • TRequest/TResponse represents any type that implements to_json(json& j, const T& t) and from_json(const json& j, T& t) methods. These methods are automatically generated if using the NLOHMANN_DEFINE_TYPE_INTRUSIVE macro shown in step 3.

Create GameHooks header class:

#include "TestGameHooks.h"
#include "GameResult.h"
#include "HookTableBuilderJSON.h"

using BResult = Microsoft::XboxStudios::Bifrost::BResult;
using HookTableBuilderJSON = Microsoft::XboxStudios::Bifrost::Serializer::HookTableBuilderJSON;

class GameHooks
{
public:
    static BResult GetPlayerLocation(const PlayerLocationRequest& request, std::unique_ptr<PlayerLocationResponse>& response);
    // If hooks are static only, consider using Microsoft::XboxStudios::Bifrost::Serializer::StaticHookTableBuilderJSON instead.
    void RegisterHooks(HookTableBuilderJSON& builder);
};

Note that the hook class contains a RegisterHooks method that will register hooks in a hook table builder that will be used to construct the server later.

Implement GameHooks:

#include "GameHooks.h"

void GameHooks::RegisterHooks(HookTableBuilderJSON& builder)
{
    builder.AddHook<PlayerLocationRequest, PlayerLocationResponse>("GetPlayerLocation", GetPlayerLocation);
}

BResult GameHooks::GetPlayerLocation(const PlayerLocationRequest& request, std::unique_ptr<PlayerLocationResponse>& response)
{
    // Use the player index in the request to call a game API to get the player position. Example:
    Vector3f playerPosition = GameState::GetPlayerPosition(request.playerIndex());

    response = std::make_unique<PlayerLocationResponse>();
    response->X = playerPosition.x;
    response->Y = playerPosition.y;
    response->Z = playerPosition.z;
    response->PlayerName = GameState::GetPlayerName(request.playerIndex());

    return GameResult(GameReturnCode::Success);
}

Note that if using non-static hooks, you must create a lambda that captures this pointer to register a hook. For example:

builder.AddHook<PlayerLocationRequest, PlayerLocationResponse>("GetPlayerLocation",
    [this](const PlayerLocationRequest& request, std::unique_ptr<PlayerLocationResponse>& response) { return this->GetPlayerLocation(request, response); });

One may prefer creating macros to auto generate the above code block.

5. Create and use the Bifrost server

Next, define what transport and serializer protocol to use with the ServerWithReferences.

The following is an example of a server that uses the windows transport protocol and JSON serializer protocol:

#include "Bifrost/Server.hpp"
#include "JSONSerializer.h"
#include "HookTableBuilderJSON.h"
#include "Win_TcpTransport.h
#include "GameHooks.h"

// Select the JSON serializer
using Serializer = Microsoft::XboxStudios::Bifrost::Serializer::JSONSerializer;

// Select the transport protocol based on platform
using Transport = Microsoft::XboxStudios::Bifrost::Transport::Win_TcpTransport;

// The hook table builder to use to register JSON hooks.
using HookTableBuilderJSON = Microsoft::XboxStudios::Bifrost::Serializer::HookTableBuilderJSON;

// Define the serializer and transport protocol the Bifrost server will use
using BifrostServer = Microsoft::XboxStudios::Bifrost::ServerWithReferences<Serializer, Transport>;

Register hooks with the hook table builder and construct a server:

HookTableBuilderJSON builder;
GameHooks hooks;
hooks.RegisterHooks(builder);
BifrostServer server(
    builder.Build(),
    Transport(4601), // Select port number here. Default is 4600.
    1000000); // Configure maximum chunk size here. Default is 512 KB.

Start the server in a startup/initialization function:

server.Start();

Process incoming requests somewhere in the game loop:

server.ProcessRequests();

When requests are received, they will be placed into a queue and processed on demand. Call ProcessRequests() somewhere in the game loop to process the queue.

On game shutdown/exit, make sure to stop the server:

server.Stop();

Compile and run the game title. To learn how to invoke the game hook, proceed to the tool integration page.