Object Creation

Overview

Fusion provides three distinct paths for creating networked objects, plus a two-step process for sub-objects. All paths produce objects backed by a Words buffer and replicated by the SDK, but they differ in ID assignment, lifetime and creation flow.

Path Method ID Assignment Use Case
Dynamic CreateObject Auto-incremented per client (per map) Player avatars, projectiles, pickups
Map-placed CreateMapObject Deterministic (origin + map + hash) Level geometry, doors, switches
Global Instance CreateGlobalInstanceObject Deterministic (singleton per map + hash) Game managers, scoreboards
Sub-object CreateSubObject + AddSubObject Explicit Inventory items, attachments

Dynamic Objects (CreateObject)

Dynamic objects are the most common path. The creating client provides a type descriptor and header blob; the SDK assigns a unique ObjectId and replicates the object to all interested clients.

C++

ObjectRoot* Client::CreateObject(
    size_t              words,       // Word count (user data + EXTRA_TAIL_WORDS = 18)
    const TypeRef&      type,        // { Hash, WordCount } for type identification
    const RealtimeCore::Common::CharType* header,  // Opaque spawn payload
    size_t              headerLength,
    Map                 map,         // Map this object lives in (0 = global)
    ObjectOwnerModes    ownerMode,   // Ownership behavior
    uint32_t            engineFlags,               // Engine-specific bitflags
    int32_t             requiredObjectsCount = 0,  // Dependencies before ready
    ObjectId            preconfiguredId = ObjectId()
);

The owner mode is clamped by SanitizeOwnerMode before use. When the calling client is not the master client, MasterClient, GameGlobal and PlayerPredicted modes are silently downgraded to Transaction (an error is written to the log, but the call still succeeds and returns the object).

ObjectId Assignment

C++

ObjectId Client::GetNewObjectId(Map map);

Every object receives an ObjectId composed of three fields:

C++

struct ObjectId {
    static constexpr size_t WORD_SIZE = 4;   // 16 bytes = 4 words

    PlayerId Origin;    // Player who created the object (uint16_t)
    Map      Map;       // Which map the object lives in (uint16_t)
    uint64_t Counter;   // Counter (dynamic objects) or 64-bit hash (map objects)
};

ObjectId is 16 bytes (Counter sits at offset 8 after padding) and spans WORD_SIZE = 4 words when stored in a Words buffer. The SDK auto-assigns IDs via GetNewObjectId(map) (or you can pass a preconfiguredId to CreateObject). The Origin field enables conflict-free creation across clients without coordination.

Header (Engine Blob)

The header parameter is an opaque byte blob stored alongside the object as Object::EngineBlob. Remote clients receive it in the OnObjectReady callback and use it to determine which scene to instantiate. Typical contents include a scene resource path or a type hash. The SDK does not interpret the header -- it passes it through as-is.

The older public Object::Header field has been replaced by the EngineBlob / EngineFlags / EngineHash triple. engineFlags is a free uint32_t for the engine binding to stash per-object metadata (formerly the role of the removed ObjectSpecialFlags enum).

Remote Object Ready

When an object created by another client becomes fully synchronized locally, the SDK fires:

C++

Broadcaster<void(ObjectRoot*)> OnObjectReady;

The callback receives the fully initialized ObjectRoot with EngineBlob, Type and the owner state populated. ObjectRoot::IsReady() returns true from this point onward. The integration layer reads the engine blob, instantiates the appropriate scene and begins synchronization.

OnObjectReady fires only for objects created by remote clients — it never fires for objects this client created itself (any Create* call). The readiness check runs after every applied state packet and requires each of the object's required objects (specified by requiredObjectsCount) to exist locally with valid data. The check is also skipped entirely while no OnObjectReady subscriber is registered — subscribe before objects can arrive, otherwise IsReady() stays false and the callback is never delivered retroactively.

Destruction

The owner destroys a dynamic object by calling:

C++

bool Client::DestroyObjectLocal(ObjectRoot* obj, bool engineObjectAlreadyDestroyed);

The engineObjectAlreadyDestroyed parameter is currently ignored — pass any value. The call succeeds only if the local client can modify the object (CanModify); it returns false otherwise. On success, it marks the root and all of its sub-objects destroyed, fires OnSubObjectDestroyed for each child and OnObjectDestroyed for the root locally with DestroyModes::Local, and the destruction replicates to all clients:

C++

Broadcaster<void(const ObjectRoot*, DestroyModes)> OnObjectDestroyed;

Remote clients receive the callback with DestroyModes::Remote.

DestroyModes

Mode Value Description
Local 0 Owner explicitly destroyed
Remote 1 Destruction replicated from owner
MapChange 2 Destroyed due to a map removal or transition
Shutdown 3 Client shutting down
RejectedNotOwner 4 Server rejected: not the owner
ForceDestroy 5 Server forced destruction

Map-placed Objects (CreateMapObject)

Map-placed objects represent entities that exist in the loaded scene itself -- not spawned dynamically but part of the level. Every client loading the same map creates the same map-placed objects locally; the SDK reconciles them using deterministic IDs.

C++

ObjectRoot* Client::CreateMapObject(
    bool&               alreadyPopulated,  // [out] see below — zero-initialize!
    size_t              words,
    const TypeRef&      type,
    const RealtimeCore::Common::CharType* header,
    size_t              headerLength,
    Map                 map,               // Map this object lives in
    uint16_t            origin,            // Origin part of the deterministic id
    uint64_t            hash,              // 64-bit hash part of the deterministic id
    ObjectOwnerModes    ownerMode,
    uint32_t            engineFlags,
    int32_t             requiredObjectsCount
);

The resulting ObjectId is {origin, map, hash}origin and hash replace the single id parameter of older SDKs. The owner mode is clamped by SanitizeOwnerMode before it is stored.

The alreadyPopulated Pattern

The critical difference from CreateObject is the alreadyPopulated output parameter. The SDK only ever writes true to it — when a root with the ObjectId{origin, map, hash} already exists locally, the existing object is returned and alreadyPopulated is set. When the object does not exist yet, the parameter is left untouched, so the caller must initialize it to false before the call:

  • false -- No object with this ID existed locally; a fresh one was created. Engine defaults should be serialized into the Words buffer.
  • true -- An object with this ID already existed (typically received from the network before registration). Its Words buffer contains network data that should be deserialized into the engine state.

This bidirectional flow eliminates the need for separate "spawn data" exchange for map-placed objects.

Deterministic IDs

Map-placed objects use the deterministic (origin, map, hash) triple instead of auto-assigned IDs. All clients loading the same map must produce the same values for the same object. A common approach is a fixed origin plus a hash of the object's name or path:

C++

uint64_t hash = FusionCore::Crc64(rootNodeName, len);

Global Instance Objects (CreateGlobalInstanceObject)

A variant for singleton objects keyed by (map, hash) that persist for the lifetime of their map (or for the whole session if map = 0):

C++

ObjectRoot* Client::CreateGlobalInstanceObject(
    bool&               alreadyPopulated,
    size_t              words,
    const TypeRef&      type,
    const RealtimeCore::Common::CharType* header,
    size_t              headerLength,
    Map                 map,
    uint64_t            hash,
    ObjectOwnerModes    ownerMode,
    uint32_t            engineFlags,
    int32_t             requiredObjectsCount = 0
);

The resulting ObjectId is {0, map, hash} — the origin is always 0, so the object is a singleton for its (map, hash) pair. Global instance objects in Map = 0 survive across MapChange / MapAdd / MapRemove calls and maintain their state across map transitions. They follow the same alreadyPopulated pattern as map-placed objects, including the must-zero-initialize rule.

Sub-Objects (CreateSubObject + AddSubObject)

Sub-objects are child objects attached to an existing root object. They share the root's authority but have their own ObjectId, Words buffer and synchronization state.

Two-Step Creation

C++

// Step 1: Create the child
ObjectChild* Client::CreateSubObject(
    ObjectId            parent,        // Parent root's ObjectId
    size_t              words,
    const TypeRef&      type,
    const RealtimeCore::Common::CharType* header,
    size_t              headerLength,
    uint64_t            engineHash,    // Identity hash on the parent (was 32-bit `targetObjectHash`)
    ObjectId            id,            // Explicit child ID
    uint32_t            engineFlags    // Engine-specific bitflags (was `ObjectSpecialFlags`)
);

// Step 2: Attach to parent
static bool Client::AddSubObject(ObjectRoot* parentObject, ObjectChild* subObject);

If an ObjectChild with the given id already exists, CreateSubObject returns the existing child. AddSubObject returns false if the child is already attached to the parent. A locally created child that has never been sent can be rolled back with Client::RevertSubObjectCreation(child).

Creation Flow

C++

// 1. Create the child
ObjectChild* child = client->CreateSubObject(
    parentId, words, type,
    header, headerLength,
    engineHash, childId, /* engineFlags */ 0
);

// 2. Write initial state into child Words
child->SetSendUpdates(false);
write_initial_state(child);

// 3. Attach to parent
client->AddSubObject(parentRoot, child);

// 4. Activate
child->SetSendUpdates(true);
child->SetHasValidData();

AddSubObject must be called after the child's Words buffer is populated. Once added, the SDK replicates the child to remote clients.

Remote Sub-Object Callback

C++

Broadcaster<void(ObjectChild*)> OnSubObjectCreated;

Remote clients receive the ObjectChild with its parent ObjectId (via ObjectChild::GetParent(child)) and its EngineHash (the value passed as engineHash at creation). The integration layer matches EngineHash to determine the child's type.

Sub-Object Destruction

C++

bool Client::DestroySubObjectLocal(ObjectChild* obj);

Broadcaster<void(ObjectChild*, DestroyModes)> OnSubObjectDestroyed;

Querying Sub-Objects

C++

static bool Client::HasSubObjects(const Object* root);
static const std::vector<ObjectId>& Client::GetSubObject(const Object* root);
Object* Client::FindSubObjectWithHash(ObjectRoot* root, uint32_t subObjectHash);

Word Count Calculation

The words parameter in all creation methods must include both user data words and the SDK-reserved tail:

C++

size_t total_words = user_property_words + Object::EXTRA_TAIL_WORDS;  // EXTRA_TAIL_WORDS = 18

The EXTRA_TAIL_WORDS account for the ObjectTail structure (Reserved[8], RequiredObjectsCount, InterestKey, Destroyed, RoomSendRate, prediction fields, Dummy).

TypeRef Convention

C++

struct TypeRef {
    uint64_t Hash;       // Crc64 or engine-specific hash
    uint32_t WordCount;  // Total words including tail
};

The Hash must match between creator and consumer for remote instantiation to succeed. The WordCount should equal the words parameter passed to the creation method.

Activation Pattern

After creating an object and writing initial state:

C++

// Pause sending during initial population
obj->SetSendUpdates(false);

// Write all initial properties via memcpy into Words buffer
populate_words(obj);

// Resume sending and mark data as valid
obj->SetSendUpdates(true);
obj->SetHasValidData();

This prevents the SDK from sending a partially-populated object. The SetSendUpdates(false) call ensures no outgoing packet includes this object until all properties are written.

Required Objects

The requiredObjectsCount parameter specifies how many other objects must be present (with valid data) before the OnObjectReady callback fires on remote clients. Required object IDs are stored in the Words buffer just before the ObjectTail; each slot spans ObjectId::WORD_SIZE (4) words, which the SDK allocates in addition to the words you pass.

C++

// Query required objects
std::span<ObjectId> ids = root->RequiredObjects();
size_t count = ids.size();
bool isReq = root->IsRequired(someId);

The total word count must account for required objects:

C++

size_t total = user_words + Object::EXTRA_TAIL_WORDS;
// requiredObjectsCount is passed separately, not added to word count
  • Objects -- Object hierarchy, Words buffer layout, ObjectTail
  • Ownership -- ObjectOwnerModes and authority transfer
  • Serialization -- Words buffer and type-to-word mapping
Back to top