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 (map + id) Level geometry, doors, switches
Global Instance CreateGlobalInstanceObject Deterministic (singleton per map+id) 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 + ExtraTailWords = 18)
    const TypeRef&      type,        // { Hash, WordCount } for type identification
    const CharType*     header,      // Opaque spawn payload (scene path, config, etc.)
    size_t              headerLength,
    Map                 map,         // Map this object lives in (0 = global)
    ObjectOwnerModes    ownerMode,   // Ownership behavior
    uint32_t            engineFlags = 0,           // Engine-specific bitflags
    int32_t             requiredObjectsCount = 0,  // Dependencies before ready
    ObjectId            preconfiguredId = ObjectId{}
);

ObjectId Assignment

C++

ObjectId Client::GetNewObjectId(Map map);

Every object receives an ObjectId composed of three fields:

C++

struct ObjectId {
    PlayerId Origin;    // Player who created the object  (uint16_t)
    Map      Map;       // Which map the object lives in (uint16_t)
    uint32_t Counter;   // Monotonically increasing counter per (origin, map)
};

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 a remote client's object becomes fully synchronized, 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.

An object becomes "ready" when its first state packet has been applied (GetHasReceivedState() returns true) and all of its required objects (specified by requiredObjectsCount) are also resolved.

Destruction

The owner destroys a dynamic object by calling:

C++

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

Set engineObjectAlreadyDestroyed to true if the engine-side entity has already been freed.
The SDK broadcasts the destruction to all clients:

C++

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

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] true if network data exists
    size_t              words,
    const TypeRef&      type,
    const CharType*     header,
    size_t              headerLength,
    Map                 map,               // Map this object lives in
    uint32_t            id,                // Deterministic object ID within map
    ObjectOwnerModes    ownerMode,
    uint32_t            engineFlags,
    int32_t             requiredObjectsCount
);

The alreadyPopulated Pattern

The critical difference from CreateObject is the alreadyPopulated output parameter:

  • false -- This client is the first to create the object (typically the master client). Engine defaults should be serialized into the Words buffer.
  • true -- Another client already populated the object. The 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 a deterministic id parameter instead of auto-assigned IDs.
All clients loading the same map must produce the same ID for the same object.
A common approach is hashing the object's name or path:

C++

uint32_t id = static_cast<uint32_t>(FusionCore::CRC64(rootNodeName, len));

Global Instance Objects (CreateGlobalInstanceObject)

A variant for singleton objects keyed by (map, id) 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 CharType*     header,
    size_t              headerLength,
    Map                 map,
    uint32_t            id,
    ObjectOwnerModes    ownerMode,
    uint32_t            engineFlags,
    int32_t             requiredObjectsCount = 0
);

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.

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 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
bool Client::AddSubObject(ObjectRoot* ParentObject, ObjectChild* SubObject);

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++

bool Client::HasSubObjects(const Object* Root);
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::ExtraTailWords;  // ExtraTailWords = 18

The ExtraTailWords 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 before the OnObjectReady callback fires.
Required object IDs are stored in the ObjectTail at the end of the Words buffer, just before the standard tail fields.

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::ExtraTailWords;
// 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