Layer Contract: Object Data Architecture

April 9, 2026 ยท View on GitHub

One definition system. Capabilities for world interactions. Sections for other systems.

Core Definitions

  • ObjectDefinition is the only definition type.
  • Capabilities = world interactions only.
  • Sections = data for other systems.
  • Item = ObjectDefinition with sections.item.
  • World pickup item = Item + Pickup capability.

Core Principle

ObjectDefinition (JSON SOT)
  |
  +-- meshes[]           = visual representation
  |
  +-- capabilities[]     = world interactions only
  |     - Pickup         (PickupTime, InitialQuantity)
  |     - Hinged         (OpenAngle, Speed, TargetMesh)
  |     - Lockable       (LockTag, DefaultLocked)
  |     - Sliding        (OpenOffset, Speed, TargetMesh)
  |
  +-- sections{}         = data for other systems (optional)
        - item           (identity + rules + behavior refs)
        - storage        (container data for world storage and loot places)
        - animation      (locomotionProfile, traversalProfile)
        - customization  (mutableSource)
        - view           (defaultMode, cameraParent, relativeOffset)
        - quest          (future)

Skeletal Assembly Extensions

Optional fields on FObjectMeshEntry for skeletal assembly:

FieldTypeDefaultPurpose
kindFNameauto-detectComponent type: SkeletalMesh, StaticMesh, CustomizableSkeletalMesh
roleFNamenoneSemantic role: DriverBody, WorldBody, LocalBody, Head, BodyCustomization, etc.
visibilityFNamedefaultHidden, OwnerOnly, SkipOwner

Validation: if any mesh has role/visibility but no SkeletalAssembly capability, validation fails.

Spawn behavior with SkeletalAssembly capability:

  1. Create mesh components (using kind, role, visibility)
  2. Attach SkeletalAssembly coordinator first (regardless of JSON order)
  3. Defer other skeletal capabilities until assembly reaches Ready
  4. Actor-scoped capabilities (not targeting assembly-managed meshes) activate immediately

Visibility-driven mesh setup (only when visibility is explicitly set):

  • AlwaysTickPoseAndRefreshBones on skeletal meshes
  • FirstPersonPrimitiveType=FirstPerson for OwnerOnly meshes and Head role
  • NoCollision profile (visual-only layers)

Empty-mesh creation: entries with Kind/Role but no asset create empty skeletal mesh components. Used for meshes populated at runtime (Mutable-driven, leader-pose copies).

What Makes Something an "Item"?

An item is an ObjectDefinition that has sections.item.

A world pickup item is an item that also has Pickup capability (can be picked up from world).

Items without Pickup capability: Vendor rewards, quest rewards, crafting outputs - they exist in inventory but may never exist as world actors.

A door is an ObjectDefinition that:

  • Has Hinged + Lockable capabilities
  • No item section

There is no separate ItemDefinition asset type.


Capabilities (World Interactions Only)

Location: ProjectObjectCapabilities module

Rule: A capability answers: "What can the player DO to this object in the world?"

CapabilityPropertiesPurpose
PickupPickupTime, InitialQuantityCan be picked up
LootContainerOneTimeUse, interaction mode labelCan be searched/opened as world storage
HingedOpenAngle, Speed, TargetMeshRotates on hinge
LockableLockTag, DefaultLockedCan be locked/unlocked
SlidingOpenOffset, Speed, TargetMeshSlides open/closed

Hard contract: Capability properties MUST be world-interaction fields.

Violations:

  • DisplayName in Pickup (that's for UI, not world)
  • Weight in Pickup (that's for inventory rules, not world)
  • Effects in Pickup (that's for GAS, not world)
  • GridSize/MaxWeight/AllowedTags in LootContainer properties (those belong in sections.storage)

Sections (Data for Other Systems)

Location: TMap<FName, FInstancedStruct> Sections in ObjectDefinition

Rule: A section answers: "What data do other systems need about this object?"

Item Section

Purpose: Data for inventory, UI, and GAS systems. Contains identity, rules, and behavior refs.

FieldTypePurpose
DisplayNameFTextIdentity - UI display
DescriptionFTextIdentity - tooltip
IconTSoftObjectPtrIdentity - inventory icon
WeightfloatRules - encumbrance
VolumefloatRules - container capacity
MaxStackint32Rules - stacking
GridSizeFIntPointRules - grid inventory
TagsFGameplayTagContainerIdentity - categorization
bCanBeDroppedboolRules
bCanBeTradedboolRules
bIsQuestItemboolRules
MagnitudesTMap<FGameplayTag, float>Consumable - SetByCaller values
bConsumeOnUseboolConsumable - remove on use
GrantedAbilitiesTArrayEquipment - C++ ability classes
GrantedEffectsTArrayEquipment - GameplayEffect assets
EquipSlotTagFGameplayTagEquipment - slot tag
EquipAbilitySetFSoftObjectPathEquipment - generated AbilitySet (filled by generator)

Hard contract: Section fields MUST NOT be world-interaction fields.

The object doesn't "know" inventory tech. It just contains optional data. Only inventory/GAS code interprets the item section.

Storage Section

Purpose: Data for world storage, nearby loot containers, and persistent container state.

FieldTypePurpose
GridSizeFIntPointRules - container footprint in cells
MaxWeightfloatRules - per-container weight limit
MaxVolumefloatRules - per-container volume limit
MaxCellsint32Rules - optional enabled-cell limit
AllowedTagsFGameplayTagContainerRules - item filter for specialized storage
bAllowRotationboolRules - placement rotation support
bPersistentboolPersistence policy
ContainerSlotIdFNameStable authored slot/key within object
SeedEntriesTArrayExact authored contents (default)
LootProfileIdFPrimaryAssetIdOptional shared randomized fill profile

Hard contract: Storage section fields MUST NOT be world-interaction fields.

The object still does not "know" inventory runtime internals. It only exposes optional storage data. World-capability and inventory code interpret the section through stable contracts.


GAS Boundary: Compile-Time vs Runtime

COMPILE-TIME (no GAS dependency)
================================
ProjectCore (tags, base types)
    |
    v
ProjectObject (ObjectDefinition, sections)
  - stores FSoftObjectPath, TMap<FGameplayTag, float>
  - NO #include of GameplayEffect.h
    |
    v
ProjectObjectCapabilities (Pickup, Hinged, Lockable)
  - world interactions only
  - NO GAS references

================================
RUNTIME (GAS dependency)
================================
ProjectInventory / ProjectGAS
  - reads item section
  - loads FSoftObjectPath --> UGameplayEffect*
  - applies effects with SetByCaller magnitudes

One Editor Panel

Kill separate Items/Objects panels. One "Definitions" browser with AssetRegistry tag filters:

FilterAssetRegistry Tag
All Objects(no filter)
PickupsALIS.Cap.Pickup = "true"
World storageALIS.Cap.LootContainer = "true" or ALIS.Section.Storage = "true"
ItemsALIS.Section.Item = "true" OR ALIS.ItemTag.*
DoorsALIS.Cap.Hinged OR ALIS.Cap.Lockable
ConsumablesALIS.ItemTag.Item.Type.Consumable
EquipmentALIS.ItemTag.Item.Type.Equipment

No asset loading for filtering. All filters read AssetRegistry tags only.


JSON Structure

Canonical form (matches runtime TMap<FName, FInstancedStruct> Sections):

{
  "id": "WaterBottle",
  "meshes": [
    {
      "id": "body",
      "asset": "/ProjectObject/HumanMade/Consumables/Drink/WaterBottle/SM_WaterBottle"
    }
  ],

  "capabilities": [
    {
      "type": "Pickup",
      "scope": ["actor"],
      "properties": {
        "InitialQuantity": "1",
        "PickupTime": "0.5"
      }
    }
  ],

  "sections": {
    "item": {
      "displayName": "Water Bottle",
      "description": "A bottle of water.",
      "icon": "/Game/UI/Icons/Items/T_WaterBottle",
      "weight": 0.5,
      "volume": 0.1,
      "maxStack": 5,
      "gridSize": "1,1",
      "tags": "Item.Type.Consumable,Item.Survival.Hydration",
      "canBeDropped": true,
      "canBeTraded": true,
      "isQuestItem": false,

      "magnitudes": {
        "SetByCaller.Hydration": -30.0
      },
      "consumeOnUse": true
    },
    "storage": {
      "gridSize": "3,3",
      "maxWeight": 12.0,
      "maxVolume": 18.0,
      "maxCells": 9,
      "allowRotation": true,
      "persistent": false,
      "containerSlotId": "Primary",
      "seedEntries": [],
      "lootProfileId": "LootProfileDefinition:Scavenge_SmallConsumables"
    }
  }
}

Schema vs authoring:

  • object.schema.json validates canonical form only (sections.item)
  • Generator accepts sugar (top-level "item": {...}) and maps it to sections.item

GAS Integration

Consumables (magnitudes)

JSON specifies SetByCaller magnitudes. Runtime applies via GE_GenericInstant:

"magnitudes": { "SetByCaller.Hydration": -30.0 },
"consumeOnUse": true

Runtime: UProjectGASLibrary::ApplyMagnitudes(ASC, Magnitudes)

Equipment (abilities/effects)

JSON specifies C++ ability classes and effect assets. Generator emits AbilitySet:

"grantedAbilities": ["/Script/ProjectCombat.GA_MeleeAttack"],
"grantedEffects": ["/Script/ProjectGAS.GE_MeleeStats"],
"equipSlotTag": "Item.EquipmentSlot.MainHand"

Runtime: AbilitySet->GiveToAbilitySystem(ASC, &Handles)


Inventory Flow

Inventory stores instances (lightweight):

struct FInventoryEntry
{
    FPrimaryAssetId ObjectId;  // "ObjectDefinition:WaterBottle"
    int32 Quantity;
    // Instance-specific data (durability, ammo, etc.)
};

Definition data (DisplayName, Weight, Effects) lives in ObjectDefinition.Sections["Item"].

Pickup flow:

  1. Player interacts with object that has Pickup capability
  2. Pickup capability broadcasts OnPickupAttempted
  3. Inventory system creates FInventoryEntry with ObjectId + Quantity
  4. World actor destroyed

Drop flow:

  1. Inventory system calls ProjectObjectSpawn::SpawnFromDefinition(ObjectId)
  2. World actor spawned with Pickup capability
  3. FInventoryEntry removed

Deferred: Profile Layer

Status: Not implemented. Deferred until reuse appears.

When to implement: When N items share exact same behavior (effects + magnitudes). Currently each item has unique magnitudes (35 analyzed, 0 sharing).


See Also