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
Related
- Objects -- Object hierarchy, Words buffer layout, ObjectTail
- Ownership -- ObjectOwnerModes and authority transfer
- Serialization -- Words buffer and type-to-word mapping
- Overview
- Dynamic Objects (CreateObject)
- Map-placed Objects (CreateMapObject)
- Global Instance Objects (CreateGlobalInstanceObject)
- Sub-Objects (CreateSubObject + AddSubObject)
- Two-Step Creation
- Creation Flow
- Remote Sub-Object Callback
- Sub-Object Destruction
- Querying Sub-Objects
- Word Count Calculation
- TypeRef Convention
- Activation Pattern
- Required Objects
- Related