Quick Start Guide

Overview

This guide walks through building a minimal Fusion 3 SDK integration from scratch. By the end you will have a working skeleton that connects to Photon Cloud, joins a room, starts Fusion, runs the frame loop, creates a networked object and synchronizes properties.

Step 1: Set Up Logging

Before creating any SDK objects, wire up logging so you can see what the SDK is doing. Implement the RealtimeCore::Common::LogOutput interface:

C++

#include "RealtimeCore/Common/LogOutput.h"
#include "RealtimeCore/Common/LogUtils.h"

class MyLogOutput : public RealtimeCore::Common::LogOutput {
public:
    void LogTrace(const RealtimeCore::Common::CharType* message) override {
        printf("[Fusion TRACE] %s\n", reinterpret_cast<const char*>(message));
    }
    void LogDebug(const RealtimeCore::Common::CharType* message) override {
        printf("[Fusion DEBUG] %s\n", reinterpret_cast<const char*>(message));
    }
    void LogInfo(const RealtimeCore::Common::CharType* message) override {
        printf("[Fusion INFO] %s\n", reinterpret_cast<const char*>(message));
    }
    void LogWarning(const RealtimeCore::Common::CharType* message) override {
        printf("[Fusion WARN] %s\n", reinterpret_cast<const char*>(message));
    }
    void LogError(const RealtimeCore::Common::CharType* message) override {
        printf("[Fusion ERROR] %s\n", reinterpret_cast<const char*>(message));
    }
};

Register it and configure log levels. LogLevel is a scoped bitmask enum; SetLogLevelsFromBitmask takes the raw uint8_t mask:

C++

static MyLogOutput* g_log_output = nullptr;

void InitLogging() {
    using RealtimeCore::Common::LogLevel;

    g_log_output = new MyLogOutput();
    RealtimeCore::Common::AddLogOutput(g_log_output);

    RealtimeCore::Common::SetLogLevelsFromBitmask(
        static_cast<uint8_t>(LogLevel::Info | LogLevel::Warning | LogLevel::Error));
}

Logging API Reference

Function Signature
AddLogOutput void AddLogOutput(LogOutput* logOutput)
RemoveLogOutput bool RemoveLogOutput(LogOutput* logOutput)
SetLogLevelsFromBitmask void SetLogLevelsFromBitmask(uint8_t logLevelMask)
LogEnable void LogEnable(LogLevel logLevel)
LogDisable void LogDisable(LogLevel logLevel)
IsLogEnabled bool IsLogEnabled(LogLevel logLevel)

All logging functions are in the RealtimeCore::Common namespace and declared in RealtimeCore/Common/LogUtils.h. LogLevel values: Trace = 1, Debug = 2, Info = 4, Warning = 8, Error = 16.

Step 2: Construct RealtimeClient and Fusion Client

The SDK uses a two-layer construction: first create a RealtimeCore::Matchmaking::RealtimeClient for transport, then pass it to FusionCore::Client for Fusion state sync.

C++

#include "Client.h"
#include "RealtimeCore/Matchmaking/RealtimeClient.h"
#include "RealtimeCore/Matchmaking/ClientConstructOptions.h"

static RealtimeCore::Matchmaking::RealtimeClient* g_realtime = nullptr;
static FusionCore::Client* g_client = nullptr;

void InitClient(const char* appId, const char* appVersion) {
    InitLogging();

    // 1. Configure the RealtimeClient
    RealtimeCore::Matchmaking::ClientConstructOptions options;
    options.AppId      = reinterpret_cast<const RealtimeCore::Common::CharType*>(appId);
    options.AppVersion = reinterpret_cast<const RealtimeCore::Common::CharType*>(appVersion);

    // 2. Create the RealtimeClient (transport layer)
    g_realtime = new RealtimeCore::Matchmaking::RealtimeClient(options);

    // 3. Create the Fusion Client (state sync layer)
    g_client = new FusionCore::Client(*g_realtime);
}

AppId and AppVersion are the only required fields — the full ClientConstructOptions list is in the Realtime API reference.

Step 3: Subscribe to Broadcasters

The Client exposes RealtimeCore::Common::Broadcaster<> members for all major events. Subscribe using a SubscriptionBag to manage subscription lifetimes:

C++

#include "RealtimeCore/Common/SubscriptionBag.h"

static RealtimeCore::Common::SubscriptionBag g_subscriptions;

void SetupCallbacks() {
    // Fusion is ready for state sync (fires inside a successful Start() call)
    g_subscriptions += g_client->OnFusionStart.Subscribe([]() {
        printf("Fusion started! Room is ready.\n");
    });

    // Remote object is ready (fully created and has valid data)
    g_subscriptions += g_client->OnObjectReady.Subscribe(
        [](FusionCore::ObjectRoot* obj) {
            printf("Object ready: origin=%u counter=%llu\n",
                   static_cast<unsigned>(obj->Id.Origin),
                   static_cast<unsigned long long>(obj->Id.Counter));
            // Instantiate your engine-side representation here
        }
    );

    // Object destroyed (local or remote)
    g_subscriptions += g_client->OnObjectDestroyed.Subscribe(
        [](const FusionCore::ObjectRoot* obj, FusionCore::DestroyModes mode) {
            printf("Object destroyed: mode=%d\n", static_cast<int>(mode));
            // Clean up engine-side representation
        }
    );

    // Sub-object created by remote client
    g_subscriptions += g_client->OnSubObjectCreated.Subscribe(
        [](FusionCore::ObjectChild* child) {
            FusionCore::ObjectId parent = FusionCore::ObjectChild::GetParent(child);
            printf("Sub-object created under parent: origin=%u counter=%llu\n",
                   static_cast<unsigned>(parent.Origin),
                   static_cast<unsigned long long>(parent.Counter));
        }
    );

    // Ownership changed
    g_subscriptions += g_client->OnObjectOwnerChanged.Subscribe(
        [](FusionCore::ObjectRoot* obj) {
            printf("Owner changed: new owner=%u\n",
                   static_cast<unsigned>(obj->GetOwner()));
        }
    );

    // RPC received
    g_subscriptions += g_client->OnRpc.Subscribe(
        [](FusionCore::Rpc& rpc) {
            printf("RPC received: id=%llu\n",
                   static_cast<unsigned long long>(rpc.Id));
        }
    );

    // Map table changed
    g_subscriptions += g_client->OnMapChange.Subscribe(
        [](const std::unordered_map<FusionCore::Map, FusionCore::Data>& maps,
           bool initial) {
            printf("Map change: %zu maps, initial=%d\n", maps.size(), initial);
        }
    );

    // Forced disconnect
    g_subscriptions += g_client->OnForcedDisconnect.Subscribe(
        [](RealtimeCore::Common::StringType message,
           FusionCore::ForcedDisconnectReason reason) {
            printf("Forced disconnect: reason=%d\n", static_cast<int>(reason));
        }
    );
}

All Broadcasters on Client

Broadcaster Signature When Fired
OnFusionStart void() Fusion configuration and initial map state applied (inside Start())
OnForcedDisconnect void(StringType message, ForcedDisconnectReason reason) Server forced a disconnect
OnRpc void(Rpc&) RPC received
OnRpcError void(Rpc&) RPC delivery failed
OnMapChange void(const std::unordered_map<Map, Data>&, bool initial) Map table changed
OnObjectOwnerChanged void(ObjectRoot*) Object ownership transferred
OnObjectOwnerPredictionFailed void(ObjectRoot*) A predicted ownership claim on a Dynamic object was rejected by the server
OnObjectReady void(ObjectRoot*) Remote object fully ready (remote-created objects only, and only when a subscriber is registered)
OnSubObjectCreated void(ObjectChild*) Remote sub-object created
OnObjectDestroyed void(const ObjectRoot*, DestroyModes) Object destroyed
OnSubObjectDestroyed void(ObjectChild*, DestroyModes) Sub-object destroyed
OnInterestEnter void(ObjectRoot*) Object entered our interest set
OnInterestExit void(ObjectRoot*) Object left our interest set
OnObjectForceAlive void(ObjectRoot*) Object kept alive by the authority after a local destroy
OnSubObjectForceAlive void(ObjectChild*) Sub-object kept alive by the authority after a local destroy
OnDestroyedMapActor void(ObjectId) Map actor was destroyed
OnInput void(ObjectRoot*, uint32_t sequence, float dt, Data, bool isNew) Queued input arrived on the authority (Client-Server mode)
OnPredictionReset void(ObjectRoot*) Predicted input sequence rejected; prediction reset (Client-Server mode)
OnOwnershipRequest void(ObjectRoot*, PlayerId requester) Another player requested ownership of an object we own
OnOwnershipResponse void(ObjectRoot*, bool granted) The owner replied to our ownership request

The SubscriptionBag unsubscribes all held subscriptions when destroyed. Keep it alive as long as you need the callbacks. See Pitfalls for details on subscription lifetime management.

Step 4: Connect and Join a Room

Connection uses Task<Result<T>> coroutine-based async operations on RealtimeClient. Each step must complete before the next begins. While connecting, keep calling Service() every frame to pump the network.

If your integration supports C++20 coroutines:

C++

using RealtimeCore::Matchmaking::Task;
using RealtimeCore::Matchmaking::Result;
using RealtimeCore::Matchmaking::MutableRoomView;

Task<Result<void>> ConnectAndJoin(const char* region, const char* roomName) {
    auto toU8 = [](const char* s) -> RealtimeCore::Common::StringViewType {
        return reinterpret_cast<const RealtimeCore::Common::CharType*>(s);
    };

    // 1. Connect
    RealtimeCore::Matchmaking::ConnectOptions connectOpts;
    Result<void> connectResult = co_await g_realtime->Connect(connectOpts);
    if (connectResult.IsErr()) {
        printf("Connect failed: %d\n",
               static_cast<int>(connectResult.GetErrorCode()));
        co_return connectResult;
    }

    // 2. Optionally select an explicit region
    Result<void> regionResult = co_await g_realtime->SelectRegion(toU8(region));
    if (regionResult.IsErr()) {
        printf("Region select failed\n");
        co_return regionResult;
    }

    // 3. Join or create a room
    RealtimeCore::Matchmaking::CreateRoomOptions roomOpts;
    roomOpts.MaxPlayers = 8;

    Result<MutableRoomView> roomResult = co_await g_realtime->JoinOrCreateRoom(
        toU8(roomName), roomOpts);
    if (roomResult.IsErr()) {
        printf("Join room failed: %d\n",
               static_cast<int>(roomResult.GetErrorCode()));
        co_return Result<void>::Err(roomResult.GetError());
    }

    printf("In room.\n");
    co_return Result<void>::Ok();
}

The Fusion server plugin is bound to rooms by the server's plugin configuration and injects the fusion_config and fusion_map_data room properties at room creation. You do not set CreateRoomOptions::Plugins in a standard deployment — see Connection and Matchmaking.

If coroutines are not available, store each Task and poll IsReady()Task<T> has no default constructor, so hold it in a std::optional<Task<T>>. The Complete Minimal Example below shows the full polling state machine. The connection method reference is in the Realtime API.

Step 5: Start Fusion

Joining the room does not start Fusion by itself. After the join completes, call Client::Start():

C++

if (!g_client->Start()) {
    // No room joined, or the room lacks the fusion_config / fusion_map_data
    // properties -- typically the room was created without the Fusion plugin.
    printf("Fusion Start() failed -- see error log.\n");
}

Start() reads the fusion_config and fusion_map_data room properties, applies the configuration and initial map state, fires OnFusionStart synchronously, and sends the version handshake to the server plugin. Subscribe to OnFusionStart before calling Start(). See Connection and Matchmaking for the room-property contract.

Step 6: The Frame Loop

The frame loop is the core of any Fusion integration. It must run every frame to keep the connection alive and synchronize objects.

C++

void FrameUpdate(double deltaTime) {
    if (!g_realtime || !g_client) return;

    if (!g_client->IsRunning()) {
        // Not running Fusion yet: pump the transport and poll the connection.
        g_realtime->Service();
        PollConnection();
        return;
    }

    // 1. Begin frame: services the realtime client, processes incoming
    //    packets and fires callbacks
    g_client->UpdateFrameBegin(deltaTime);

    // 2. Read remote state from Words buffers (non-authority objects)
    SyncInbound();

    // ... game simulation ...

    // 3. Write local state to Words buffers (authority objects only)
    SyncOutbound();

    // 4. End frame: packages and sends outgoing state to other clients
    g_client->UpdateFrameEnd();
}

Critical Frame Ordering

Order Call Purpose
1 client.UpdateFrameBegin(dt) Service the connection, process inbound packets, fire callbacks
2 sync_inbound() Non-authority reads Words to engine state
3 sync_outbound() Authority writes engine state to Words buffers
4 client.UpdateFrameEnd() Queue and send outbound state/RPC packets

UpdateFrameBegin() and UpdateFrameEnd() are paired: UpdateFrameEnd() is a silent no-op unless an UpdateFrameBegin() has run first, and a second UpdateFrameBegin() without an UpdateFrameEnd() in between is also a no-op. Call them once each per frame, Begin first.

UpdateFrameBegin() services the realtime client internally. Call RealtimeClient::Service() yourself only while Fusion is not running (connecting, joining, or between sessions).

See Frame Loop for the full sequence details.

Step 7: Create an Object

Objects carry a fixed-size Words buffer (array of int32_t) that Fusion replicates. You specify the buffer size at creation time. The last 18 words are reserved for the ObjectTail.

C++

FusionCore::ObjectRoot* CreateNetworkedObject(
    size_t userWordCount,
    uint64_t typeHash,
    FusionCore::ObjectOwnerModes ownerMode)
{
    // Account for the tail area (EXTRA_TAIL_WORDS = 18)
    size_t totalWords = userWordCount + FusionCore::Object::EXTRA_TAIL_WORDS;

    FusionCore::TypeRef typeRef;
    typeRef.Hash = typeHash;
    typeRef.WordCount = static_cast<uint32_t>(totalWords);

    FusionCore::ObjectRoot* obj = g_client->CreateObject(
        totalWords,                  // total word count including tail
        typeRef,                     // type reference (hash + word count)
        nullptr,                     // header data (spawn payload)
        0,                           // header length in bytes
        FusionCore::Map{0},          // map (0 = global / not in any specific map)
        ownerMode,                   // ObjectOwnerModes enum value
        /* engineFlags */ 0,         // engine-specific bitflags
        /* requiredObjectsCount */ 0,
        FusionCore::ObjectId{}       // preconfiguredId (default = auto-allocate)
    );

    if (obj) {
        // Disable sending until spawn data is written
        obj->SetSendUpdates(false);
    }

    return obj;
}

After creation, copy your initial state into obj->Words.Ptr, then enable sending:

C++

void FinalizeObject(FusionCore::ObjectRoot* obj,
                    const int32_t* spawnData,
                    size_t userWordCount)
{
    if (!obj || !obj->Words.IsValid() || !spawnData) return;

    memcpy(obj->Words.Ptr, spawnData, userWordCount * sizeof(int32_t));

    obj->SetSendUpdates(true);
    obj->SetHasValidData();
}

ObjectOwnerModes

Mode Value Behavior
Transaction 0 Ownership via explicit request/release
PlayerAttached 1 Transferable like Transaction, but the server destroys the object when its owning player leaves
Dynamic 2 Ownership transfers dynamically with cooldown
MasterClient 3 Always owned by the master client
GameGlobal 4 Global object; only the master client can create and write it
PlayerPredicted 5 Server is authority; designated player feeds inputs (used with SimulationMode::Authority)

CreateObject Signature

C++

ObjectRoot* CreateObject(
    size_t words,                                 // total word count (user + tail)
    const TypeRef& type,                          // type hash + word count
    const RealtimeCore::Common::CharType* header, // spawn data bytes (nullable)
    size_t headerLength,                          // spawn data byte count
    Map map,                                      // map this object lives in (0 = global)
    ObjectOwnerModes ownerMode,                   // ownership mode
    uint32_t engineFlags,                         // engine-specific bitflags
    int32_t requiredObjectsCount = 0,             // number of required objects
    ObjectId preconfiguredId = ObjectId()         // explicit ID, or default to auto-allocate
);

The last 18 words of the Words buffer are reserved for ObjectTail (Reserved[8], RequiredObjectsCount, InterestKey, Destroyed, RoomSendRate, prediction fields, Dummy). Never write user data to this region.

See Object Sync Patterns for the full write/read cycle.

Step 8: Sync Properties

Authority clients write properties to the Words buffer; non-authority clients read from it. Properties are serialized as int32_t words using memcpy bit-casting:

C++

void WriteFloat(FusionCore::Object* obj, int offset, float value) {
    int32_t word;
    memcpy(&word, &value, sizeof(float));
    obj->Words.Ptr[offset] = word;
}

float ReadFloat(const FusionCore::Object* obj, int offset) {
    float value;
    memcpy(&value, &obj->Words.Ptr[offset], sizeof(float));
    return value;
}

The Words buffer layout is fixed. Properties are written at fixed offsets without type markers or delimiters. Both writer and reader must agree on the layout. See Object Sync Patterns for an example type-to-word mapping.

Step 9: Shutdown

C++

void Shutdown() {
    // 1. Unsubscribe all callbacks
    g_subscriptions.UnsubscribeAll();

    // 2. Stop the Fusion client
    if (g_client) {
        g_client->Shutdown();
        delete g_client;
        g_client = nullptr;
    }

    // 3. Disconnect and destroy the RealtimeClient
    if (g_realtime) {
        // Note: If still connected, you may want to await Disconnect() first
        delete g_realtime;
        g_realtime = nullptr;
    }

    // 4. Clean up logging
    if (g_log_output) {
        RealtimeCore::Common::RemoveLogOutput(g_log_output);
        delete g_log_output;
        g_log_output = nullptr;
    }
}

Shutdown order matters:

  1. Unsubscribe callbacks first (prevents use-after-free in broadcaster callbacks).
  2. Shutdown() performs the full Fusion teardown (it calls Stop() internally).
  3. Delete the Fusion client before the RealtimeClient.
  4. Remove log outputs last.

Complete Minimal Example

C++

#include "Client.h"
#include "RealtimeCore/Matchmaking/RealtimeClient.h"
#include "RealtimeCore/Matchmaking/ClientConstructOptions.h"
#include "RealtimeCore/Matchmaking/ConnectOptions.h"
#include "RealtimeCore/Matchmaking/CreateRoomOptions.h"
#include "RealtimeCore/Common/LogOutput.h"
#include "RealtimeCore/Common/LogUtils.h"
#include "RealtimeCore/Common/SubscriptionBag.h"
#include "RealtimeCore/Common/Result.h"
#include "RealtimeCore/Common/Task.h"

#include <cstdio>
#include <cstring>
#include <optional>

// ---------------------------------------------------------------------------
// Logging
// ---------------------------------------------------------------------------

class MyLogOutput : public RealtimeCore::Common::LogOutput {
public:
    void LogTrace(const RealtimeCore::Common::CharType* msg) override {
        printf("[TRACE] %s\n", reinterpret_cast<const char*>(msg));
    }
    void LogDebug(const RealtimeCore::Common::CharType* msg) override {
        printf("[DEBUG] %s\n", reinterpret_cast<const char*>(msg));
    }
    void LogInfo(const RealtimeCore::Common::CharType* msg) override {
        printf("[INFO]  %s\n", reinterpret_cast<const char*>(msg));
    }
    void LogWarning(const RealtimeCore::Common::CharType* msg) override {
        printf("[WARN]  %s\n", reinterpret_cast<const char*>(msg));
    }
    void LogError(const RealtimeCore::Common::CharType* msg) override {
        printf("[ERROR] %s\n", reinterpret_cast<const char*>(msg));
    }
};

// ---------------------------------------------------------------------------
// Globals
// ---------------------------------------------------------------------------

using RealtimeCore::Matchmaking::Task;
using RealtimeCore::Matchmaking::Result;
using RealtimeCore::Matchmaking::MutableRoomView;

static MyLogOutput* g_log = nullptr;
static RealtimeCore::Matchmaking::RealtimeClient* g_realtime = nullptr;
static FusionCore::Client* g_client = nullptr;
static RealtimeCore::Common::SubscriptionBag g_subs;
static FusionCore::ObjectRoot* g_myObject = nullptr;

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

static auto toU8(const char* s) {
    return reinterpret_cast<const RealtimeCore::Common::CharType*>(s);
}

static int32_t floatWord(float v) {
    int32_t w;
    memcpy(&w, &v, sizeof(float));
    return w;
}

static float wordFloat(int32_t w) {
    float v;
    memcpy(&v, &w, sizeof(float));
    return v;
}

// ---------------------------------------------------------------------------
// Initialization
// ---------------------------------------------------------------------------

void Init(const char* appId, const char* appVersion) {
    using RealtimeCore::Common::LogLevel;

    // Logging
    g_log = new MyLogOutput();
    RealtimeCore::Common::AddLogOutput(g_log);
    RealtimeCore::Common::SetLogLevelsFromBitmask(
        static_cast<uint8_t>(LogLevel::Info | LogLevel::Warning | LogLevel::Error));

    // RealtimeClient
    RealtimeCore::Matchmaking::ClientConstructOptions opts;
    opts.AppId      = toU8(appId);
    opts.AppVersion = toU8(appVersion);
    g_realtime = new RealtimeCore::Matchmaking::RealtimeClient(opts);

    // Fusion Client
    g_client = new FusionCore::Client(*g_realtime);

    // Callbacks -- subscribe BEFORE Start() so OnFusionStart is observed
    g_subs += g_client->OnFusionStart.Subscribe([]() {
        printf("Fusion started!\n");

        // Create a test object: 3 user words (float x, float y, float z)
        size_t totalWords = 3 + FusionCore::Object::EXTRA_TAIL_WORDS;
        FusionCore::TypeRef type{0x12345678, static_cast<uint32_t>(totalWords)};

        g_myObject = g_client->CreateObject(
            totalWords, type, nullptr, 0,
            FusionCore::Map{0},
            FusionCore::ObjectOwnerModes::Transaction,
            /* engineFlags */ 0);

        if (g_myObject) {
            g_myObject->SetSendUpdates(true);
            g_myObject->SetHasValidData();
        }
    });

    g_subs += g_client->OnObjectReady.Subscribe(
        [](FusionCore::ObjectRoot* obj) {
            printf("Remote object ready: origin=%u counter=%llu\n",
                   static_cast<unsigned>(obj->Id.Origin),
                   static_cast<unsigned long long>(obj->Id.Counter));
            obj->Engine = nullptr; // Store your engine pointer here
        }
    );

    g_subs += g_client->OnObjectDestroyed.Subscribe(
        [](const FusionCore::ObjectRoot* obj, FusionCore::DestroyModes mode) {
            printf("Object destroyed: mode=%d\n", static_cast<int>(mode));
        }
    );

    g_subs += g_realtime->OnDisconnected.Subscribe(
        [](RealtimeCore::Matchmaking::DisconnectCause cause) {
            printf("Disconnected: cause=%d\n", static_cast<int>(cause));
        }
    );
}

// ---------------------------------------------------------------------------
// Connection (polling pattern)
// ---------------------------------------------------------------------------

enum class Phase { Idle, Connecting, Region, Joining, Running };
static Phase g_phase = Phase::Idle;

// Task<> has no default constructor -- hold polled tasks in std::optional.
static std::optional<Task<Result<void>>> g_connectTask;
static std::optional<Task<Result<void>>> g_regionTask;
static std::optional<Task<Result<MutableRoomView>>> g_joinTask;

void StartConnect() {
    RealtimeCore::Matchmaking::ConnectOptions connectOpts;
    g_connectTask.emplace(g_realtime->Connect(connectOpts));
    g_phase = Phase::Connecting;
}

void PollConnection() {
    switch (g_phase) {
    case Phase::Connecting:
        if (g_connectTask && g_connectTask->IsReady()) {
            auto result = g_connectTask->Get();
            g_connectTask.reset();
            if (result.IsErr()) { g_phase = Phase::Idle; return; }
            g_regionTask.emplace(g_realtime->SelectRegion(u8"us"));
            g_phase = Phase::Region;
        }
        break;
    case Phase::Region:
        if (g_regionTask && g_regionTask->IsReady()) {
            auto result = g_regionTask->Get();
            g_regionTask.reset();
            if (result.IsErr()) { g_phase = Phase::Idle; return; }
            RealtimeCore::Matchmaking::CreateRoomOptions roomOpts;
            roomOpts.MaxPlayers = 8;
            g_joinTask.emplace(g_realtime->JoinOrCreateRoom(u8"test_room", roomOpts));
            g_phase = Phase::Joining;
        }
        break;
    case Phase::Joining:
        if (g_joinTask && g_joinTask->IsReady()) {
            auto result = g_joinTask->Get();
            g_joinTask.reset();
            if (result.IsErr()) { g_phase = Phase::Idle; return; }

            // In the room -- now start Fusion. OnFusionStart fires inside
            // Start() when the room carries the Fusion plugin's properties.
            if (!g_client->Start()) {
                printf("Fusion Start() failed -- room lacks the Fusion plugin?\n");
                g_phase = Phase::Idle;
                return;
            }
            g_phase = Phase::Running;
        }
        break;
    default:
        break;
    }
}

// ---------------------------------------------------------------------------
// Frame Loop
// ---------------------------------------------------------------------------

void Tick(double dt) {
    if (!g_realtime || !g_client) return;

    if (g_phase != Phase::Running || !g_client->IsRunning()) {
        // Not running Fusion yet: pump the transport and poll the connection.
        g_realtime->Service();
        PollConnection();
        return;
    }

    // Frame begin: services the connection, receives inbound
    g_client->UpdateFrameBegin(dt);

    // --- sync_inbound: read remote state ---
    for (auto& [id, obj] : g_client->AllRootObjects()) {
        if (!g_client->IsOwner(obj)) {
            float x = wordFloat(obj->Words.Ptr[0]);
            float y = wordFloat(obj->Words.Ptr[1]);
            float z = wordFloat(obj->Words.Ptr[2]);
            // Apply x, y, z to your engine representation via obj->Engine
            (void)x; (void)y; (void)z;
        }
    }

    // ... game simulation ...

    // --- sync_outbound: write authority state ---
    if (g_myObject && g_client->IsOwner(g_myObject)) {
        float x = 1.0f, y = 2.0f, z = 3.0f;
        g_myObject->Words.Ptr[0] = floatWord(x);
        g_myObject->Words.Ptr[1] = floatWord(y);
        g_myObject->Words.Ptr[2] = floatWord(z);
    }

    // Frame end: send outbound
    g_client->UpdateFrameEnd();
}

// ---------------------------------------------------------------------------
// Cleanup
// ---------------------------------------------------------------------------

void Cleanup() {
    g_subs.UnsubscribeAll();

    if (g_client) {
        g_client->Shutdown();
        delete g_client;
        g_client = nullptr;
    }
    if (g_realtime) {
        delete g_realtime;
        g_realtime = nullptr;
    }
    if (g_log) {
        RealtimeCore::Common::RemoveLogOutput(g_log);
        delete g_log;
        g_log = nullptr;
    }
}

Next Steps

Back to top