Engine Binding
Overview
This guide covers the patterns for connecting Fusion SDK objects to engine-side representations: the Object::Engine pointer, bidirectional registry design, type hash conventions, spawnable type registration and the spawner/factory pattern.
These patterns are engine-agnostic.
The examples use generic C++ constructs that can be adapted to any game engine.
Object::Engine Pointer
Every Object (both ObjectRoot and ObjectChild) has a void* Engine field.
C++
class Object {
public:
void* Engine{nullptr}; // Your engine-side pointer
// ...
};
This is the SDK's designated place to store a back-reference to your engine representation (node, actor, entity, etc.).
The SDK never reads or writes Engine -- it is entirely for your use.
C++
// After creating a Fusion object, store the engine pointer
SharedMode::ObjectRoot* fusionObj = client->CreateObject(/*...*/);
fusionObj->Engine = myEngineNode;
// Later, retrieve it in callbacks
subs += client->OnObjectOwnerChanged.Subscribe(
[](SharedMode::ObjectRoot* obj) {
auto* node = static_cast<MyEngineNode*>(obj->Engine);
if (node) {
node->OnAuthorityChanged();
}
}
);
Lifetime rule: You must null out Engine when the engine-side object is destroyed and check for null before dereferencing.
The Fusion object may outlive the engine representation (e.g., during scene transitions or async destruction).
Bidirectional Registry Pattern
The Engine pointer gives you Fusion-to-engine lookup.
But you also need engine-to-Fusion lookup: given an engine object, find its Fusion counterpart (e.g., for despawning or RPC sending).
PackedObjectId Helper
Fusion's ObjectId has two uint32_t components: Origin (PlayerId) and Counter.
The ObjectId struct has a built-in conversion to uint64_t.
C++
// ObjectId has an implicit conversion operator to uint64_t:
// operator uint64_t() const;
// And a constructor from uint64_t:
// explicit ObjectId(const uint64_t& packed);
// Pack:
SharedMode::ObjectId id(origin, counter);
uint64_t packed = static_cast<uint64_t>(id);
// Unpack:
SharedMode::ObjectId unpacked(packed);
// unpacked.Origin == origin
// unpacked.Counter == counter
The packing layout is: low 32 bits = Origin, high 32 bits = Counter.
Registry Design
C++
class ObjectRegistry {
// Engine ID -> packed Fusion ObjectId
std::unordered_map<uint64_t, uint64_t> engineToFusion;
// Packed Fusion ObjectId -> Engine ID
std::unordered_map<uint64_t, uint64_t> fusionToEngine;
public:
void Register(uint64_t engineId, SharedMode::ObjectId fusionId) {
uint64_t packed = static_cast<uint64_t>(fusionId);
engineToFusion[engineId] = packed;
fusionToEngine[packed] = engineId;
}
void UnregisterByEngineId(uint64_t engineId) {
auto it = engineToFusion.find(engineId);
if (it != engineToFusion.end()) {
fusionToEngine.erase(it->second);
engineToFusion.erase(it);
}
}
void UnregisterByFusionId(SharedMode::ObjectId fusionId) {
uint64_t packed = static_cast<uint64_t>(fusionId);
auto it = fusionToEngine.find(packed);
if (it != fusionToEngine.end()) {
engineToFusion.erase(it->second);
fusionToEngine.erase(it);
}
}
bool HasEngineId(uint64_t engineId) const {
return engineToFusion.count(engineId) > 0;
}
bool HasFusionId(SharedMode::ObjectId fusionId) const {
return fusionToEngine.count(static_cast<uint64_t>(fusionId)) > 0;
}
uint64_t GetEngineId(SharedMode::ObjectId fusionId) const {
uint64_t packed = static_cast<uint64_t>(fusionId);
auto it = fusionToEngine.find(packed);
return (it != fusionToEngine.end()) ? it->second : 0;
}
SharedMode::ObjectId GetFusionId(uint64_t engineId) const {
auto it = engineToFusion.find(engineId);
if (it != engineToFusion.end()) {
return SharedMode::ObjectId(it->second);
}
return SharedMode::ObjectId(0, 0);
}
void Clear() {
engineToFusion.clear();
fusionToEngine.clear();
}
};
Registration Points
Register and unregister objects at these lifecycle events.
| Event | Action |
|---|---|
After CreateObject() + engine instantiation |
Register(engineId, obj->Id) |
In OnObjectReady callback |
Register(engineId, obj->Id) |
After CreateSceneObject() |
Register(engineId, obj->Id) |
After CreateSubObject() + AddSubObject() |
Register(engineId, child->Id) |
In OnSubObjectCreated callback |
Register(engineId, child->Id) |
In OnObjectDestroyed callback |
UnregisterByFusionId(obj->Id) |
In OnSubObjectDestroyed callback |
UnregisterByFusionId(child->Id) |
| When engine object is freed | UnregisterByEngineId(engineId) |
Usage: RPC Routing
The registry enables routing incoming RPCs to the correct engine object.
C++
subs += client->OnRpc.Subscribe([&](SharedMode::Rpc& rpc) {
if (rpc.TargetObject.IsSome()) {
// Object-targeted RPC
uint64_t engineId = registry.GetEngineId(rpc.TargetObject);
auto* node = GetEngineObject(engineId);
if (node) {
DispatchRpc(node, rpc);
}
} else {
// Broadcast RPC (TargetObject is {0, 0})
DispatchBroadcastRpc(rpc);
}
});
Usage: Sub-Object Parent Lookup
When a sub-object arrives, find its parent's engine representation.
C++
subs += client->OnSubObjectCreated.Subscribe([&](SharedMode::ObjectChild* child) {
uint64_t parentEngineId = registry.GetEngineId(child->Parent);
auto* parentNode = GetEngineObject(parentEngineId);
if (parentNode) {
HandleSubObjectCreated(child, parentNode);
}
});
Type Hash Convention
When a remote client creates an object, the local client needs to know which scene or prefab to instantiate.
This is solved by matching TypeRef::Hash values.
A type hash is a uint64_t that uniquely identifies a scene or prefab type.
The convention is to hash the resource path.
C++
// Using the SDK's built-in CRC64:
uint64_t typeHash = SharedMode::CRC64(path.c_str(), path.length());
// Or using engine-specific hashing:
// uint64_t typeHash = scenePath.hash();
The type hash is set at object creation time via TypeRef::Hash and is available on remote objects via Object::Type.Hash.
C++
struct TypeRef {
uint64_t Hash; // Type identifier (scene path hash)
uint32_t WordCount; // Total Words buffer size (including tail)
};
Spawnable Type Registry
Each spawner maintains a list of spawnable scenes or prefabs.
C++
struct SpawnableType {
std::string scenePath;
uint64_t typeHash;
void* cachedResource = nullptr; // Lazy-loaded scene resource
};
class SpawnableTypeRegistry {
std::vector<SpawnableType> types;
public:
void RegisterScene(const std::string& path) {
SpawnableType type;
type.scenePath = path;
type.typeHash = SharedMode::CRC64(path.c_str(), path.length());
types.push_back(type);
}
SpawnableType* FindByTypeHash(uint64_t hash) {
for (auto& type : types) {
if (type.typeHash == hash) {
return &type;
}
}
return nullptr;
}
bool HasTypeHash(uint64_t hash) const {
for (const auto& type : types) {
if (type.typeHash == hash) return true;
}
return false;
}
};
Spawner / Factory Pattern
A spawner is the engine-side component that manages the full lifecycle of networked objects: registration, local spawning, remote instantiation and despawning.
Spawner Responsibilities
| Responsibility | When |
|---|---|
| Register spawnable scenes | At initialization |
| Local spawn | When authority wants to create an object |
| Remote spawn | When OnObjectReady fires with a matching type hash |
| Local despawn | When authority wants to remove an object |
| Remote despawn | When OnObjectDestroyed fires |
Local Spawn Flow
The local spawn sequence proceeds through these steps.
- Load scene resource, instantiate off-tree
- Apply initial data (position, properties, etc.)
- Compute word count from replication config
- Serialize spawn-time properties to header byte buffer
- Call
CreateObject(totalWords, typeRef, header, ..., ownerMode) SetSendUpdates(false)on the new objectmemcpyserialized data to Words buffer- Store
obj->Engine = instance - Register in bidirectional registry
- Add instance to the scene tree
SetSendUpdates(true),SetHasValidData(true)
C++
void* SpawnLocal(SharedMode::Client* client,
ObjectRegistry& registry,
SpawnableTypeRegistry& typeRegistry,
const std::string& scenePath,
SharedMode::ObjectOwnerModes ownerMode)
{
// 1-2. Instantiate and configure
void* instance = InstantiateScene(scenePath);
ApplyInitialData(instance);
// 3. Compute word count
int userWords = ComputeWordCount(instance);
size_t totalWords = userWords + SharedMode::Object::ExtraTailWords;
// 4. Serialize spawn data
std::vector<uint8_t> spawnData = SerializeSpawnData(instance);
// 5. Create Fusion object
uint64_t typeHash = SharedMode::CRC64(scenePath.c_str(), scenePath.length());
SharedMode::TypeRef typeRef{typeHash, static_cast<uint32_t>(totalWords)};
SharedMode::ObjectRoot* obj = client->CreateObject(
totalWords, typeRef,
reinterpret_cast<const PhotonCommon::CharType*>(spawnData.data()),
spawnData.size(),
0, // scene index
ownerMode
);
if (!obj) return nullptr;
// 6. Disable sending until ready
obj->SetSendUpdates(false);
// 7. Copy spawn data to Words buffer
int usable = static_cast<int>(obj->Words.Length)
- SharedMode::Object::ExtraTailWords;
int toCopy = (userWords < usable) ? userWords : usable;
SerializePropertiesToWords(instance, obj->Words.Ptr, toCopy);
// 8. Store engine pointer
obj->Engine = instance;
// 9. Register
uint64_t engineId = GetEngineObjectId(instance);
registry.Register(engineId, obj->Id);
// 10. Add to scene tree
AddToSceneTree(instance);
// 11. Enable sending
obj->SetSendUpdates(true);
obj->SetHasValidData(true);
return instance;
}
Remote Spawn Flow
The remote spawn sequence proceeds through these steps.
OnObjectReadycallback fires withObjectRoot*- Match
Type.Hashto a registered spawnable scene - Load and instantiate the scene
- Deserialize spawn data from
obj->Header - Deserialize initial Words to engine properties
- Store
obj->Engine = instance - Register in bidirectional registry
- Add instance to scene tree
SetHasValidData(true)
C++
void HandleRemoteSpawn(SharedMode::Client* client,
SharedMode::ObjectRoot* obj,
ObjectRegistry& registry,
SpawnableTypeRegistry& typeRegistry)
{
// 2. Find matching type
SpawnableType* type = typeRegistry.FindByTypeHash(obj->Type.Hash);
if (!type) {
printf("Unknown type hash: %llu\n",
static_cast<unsigned long long>(obj->Type.Hash));
return;
}
// 3. Instantiate
void* instance = InstantiateScene(type->scenePath);
// 4. Deserialize header (spawn data)
if (obj->Header.Valid()) {
DeserializeSpawnData(instance, obj->Header.Ptr, obj->Header.Length);
}
// 5. Apply initial Words to engine
if (obj->Words.IsValid()) {
int usable = static_cast<int>(obj->Words.Length)
- SharedMode::Object::ExtraTailWords;
DeserializeWordsToProperties(instance, obj->Words.Ptr, usable);
}
// 6-7. Bind and register
obj->Engine = instance;
uint64_t engineId = GetEngineObjectId(instance);
registry.Register(engineId, obj->Id);
// 8. Add to scene
AddToSceneTree(instance);
// 9. Mark ready
obj->SetHasValidData(true);
}
Despawn
C++
void Despawn(SharedMode::Client* client,
ObjectRegistry& registry,
void* engineInstance)
{
uint64_t engineId = GetEngineObjectId(engineInstance);
SharedMode::ObjectId fusionId = registry.GetFusionId(engineId);
if (fusionId.IsNone()) return;
SharedMode::ObjectRoot* obj = client->FindObjectRoot(fusionId);
if (obj) {
// Second param: false = engine object NOT already destroyed.
// The OnObjectDestroyed callback will handle engine cleanup.
client->DestroyObjectLocal(obj, false);
}
}
The DestroyObjectLocal signature is:
C++
bool DestroyObjectLocal(ObjectRoot* obj, bool engineObjectAlreadyDestroyed);
The engineObjectAlreadyDestroyed parameter tells the SDK whether the engine-side object is already gone (e.g., the user destroyed it before calling despawn).
If true, your OnObjectDestroyed handler should skip freeing the engine object.
DestroyModes Enum
The OnObjectDestroyed callback includes a DestroyModes enum indicating why the object was destroyed.
C++
enum class DestroyModes {
Local = 0, // DestroyObjectLocal() was called by this client
Remote = 1, // Remote authority destroyed the object
SceneChange = 2, // Scene changed, object cleaned up
Shutdown = 3, // Client shutting down
RejectedNotOwner = 4, // Creation rejected: not the owner
ForceDestroy = 5 // Server force-destroyed the object
};
Use this to decide whether to free the engine object.
C++
subs += client->OnObjectDestroyed.Subscribe(
[&](const SharedMode::ObjectRoot* obj, SharedMode::DestroyModes mode) {
// Unregister from our registry
registry.UnregisterByFusionId(obj->Id);
// Free the engine object (unless already destroyed)
if (obj->Engine && mode != SharedMode::DestroyModes::Shutdown) {
DestroyEngineObject(obj->Engine);
}
}
);
Multiple Spawner Support
An integration can support multiple spawners, each managing different object types.
- Each spawner registers its own set of spawnable scenes
- On remote creation, broadcast to all spawners -- only the one with a matching type hash handles it
- Spawners self-register with the client
C++
class SpawnerManager {
std::unordered_set<Spawner*> spawners;
public:
void RegisterSpawner(Spawner* s) { spawners.insert(s); }
void UnregisterSpawner(Spawner* s) { spawners.erase(s); }
bool HandleRemoteObjectReady(SharedMode::Client* client,
SharedMode::ObjectRoot* obj)
{
for (auto* spawner : spawners) {
if (spawner->CanHandleType(obj->Type.Hash)) {
spawner->SpawnRemote(client, obj);
return true;
}
}
return false; // No spawner claimed this object
}
void HandleRemoteObjectDestroyed(const SharedMode::ObjectRoot* obj,
SharedMode::DestroyModes mode)
{
for (auto* spawner : spawners) {
spawner->TryHandleRemoteDestruction(obj, mode);
}
}
};
static SpawnerManager g_spawnerManager;
Wire it to the callbacks.
C++
subs += client->OnObjectReady.Subscribe(
[](SharedMode::ObjectRoot* obj) {
if (!g_spawnerManager.HandleRemoteObjectReady(g_client, obj)) {
printf("No spawner for type hash %llu\n",
static_cast<unsigned long long>(obj->Type.Hash));
}
}
);
subs += client->OnObjectDestroyed.Subscribe(
[](const SharedMode::ObjectRoot* obj, SharedMode::DestroyModes mode) {
g_spawnerManager.HandleRemoteObjectDestroyed(obj, mode);
}
);
Synchronizer Registration Pattern
Synchronizers (components that handle per-frame Words buffer sync) also register with the client.
C++
class SyncManager {
std::unordered_set<Synchronizer*> synchronizers;
public:
void Register(Synchronizer* s) { synchronizers.insert(s); }
void Unregister(Synchronizer* s) { synchronizers.erase(s); }
void SyncOutboundAll(SharedMode::Client* client) {
for (auto* sync : synchronizers) {
if (sync->HasAuthority(client)) {
sync->SyncOutbound();
}
}
}
void SyncInboundAll(SharedMode::Client* client) {
for (auto* sync : synchronizers) {
if (!sync->HasAuthority(client)) {
sync->SyncInbound();
}
}
}
};
The frame loop calls these at the appropriate points.
C++
void FrameUpdate(double dt) {
g_realtime->Service();
if (!g_client->IsRunning()) return;
g_syncManager.SyncOutboundAll(g_client); // Before End
g_client->UpdateFrameEnd();
g_client->UpdateFrameBegin(dt);
g_syncManager.SyncInboundAll(g_client); // After Begin
}
Integration Entry Point
The plugin's entry point registers all classes and creates the singleton client.
C++
// Pseudocode for a plugin entry point
void InitializePlugin() {
// 1. Register project settings (app ID, log level, etc.)
RegisterProjectSettings();
// 2. Register custom classes with the engine
RegisterClass<ReplicationConfig>();
RegisterClass<Spawner>();
RegisterClass<Synchronizer>();
RegisterClass<FusionClient>();
// 3. Create singleton
auto* singleton = new FusionClient();
RegisterSingleton("FusionClient", singleton);
}
void UninitializePlugin() {
UnregisterSingleton("FusionClient");
delete g_fusionClient;
}
Architecture Overview
| Layer | Component | Role |
|---|---|---|
| Engine Layer | Spawner(s) | Factory — creates and destroys engine instances for networked objects |
| Synchronizer(s) | Sync loop — reads/writes the Words buffer each frame | |
| Object Registry | Bidirectional map between engine IDs and Fusion ObjectIds | |
| Singleton | FusionClient | Owns spawner manager, sync manager, object registry, frame loop and subscription bag |
| void* Engine boundary | ||
| Fusion SDK | SharedMode::Client |
AllObjects, CreateObject, CreateSubObject, UpdateFrameBegin/End, RPC system, Broadcasters |
| Transport | PhotonMatchmaking::RealtimeClient |
Connect, SelectRegion, JoinOrCreateRoom, Service(), Task<Result<T>> async |
Related
- Object Sync Patterns -- Words buffer mechanics
- Sub-Objects -- Child object creation and lifecycle
- Pitfalls -- Critical gotchas
- Overview
- Object::Engine Pointer
- Bidirectional Registry Pattern
- PackedObjectId Helper
- Registry Design
- Registration Points
- Usage: RPC Routing
- Usage: Sub-Object Parent Lookup
- Type Hash Convention
- Spawnable Type Registry
- Spawner / Factory Pattern
- Multiple Spawner Support
- Synchronizer Registration Pattern
- Integration Entry Point
- Architecture Overview
- Related