ProjectObjectCapabilities

April 15, 2026 ยท View on GitHub

Reusable capability components for world objects (access control, state).

Purpose

  • COMPONENTS ONLY - this plugin contains UActorComponent classes, NOT actors
  • Provides composable capabilities that any actor can attach
  • Not tied to specific actor types - generic building blocks
  • Pairs with ProjectInteraction (player side) for complete interaction flow
  • World interactions only - item identity data lives in ObjectDefinition.sections.item, storage data lives in ObjectDefinition.sections.storage, not here

Architecture: See Layer Contract for the 3-layer separation (Capabilities / Item Data / GAS Profiles).

Components vs Actors (IMPORTANT)

ProjectObjectCapabilities (this)     ProjectObject (Resources/)
------------------------------       --------------------------
COMPONENTS ONLY                      ACTORS that compose components

ULockableComponent          --->     AOpenableActor (has LockComponent)
UHealthComponent (future)   --->     ADestructibleActor (future)
UPowerableComponent (future)--->     APoweredDevice (future)

Rule: If you're creating a reusable capability (lock, health, power), put it HERE. If you're creating a placeable world object, put it in ProjectObject.

Architecture Position

Gameplay/
|-- ProjectInteraction/         <- Player side (E key, traces)
`-- ProjectObjectCapabilities/  <- Object side (lock, state)
    `-- Access/
        `-- ULockableComponent

Components

ULockableComponent

Access control component using FGameplayTag for key matching.

Properties:

PropertyTypeDescription
LockTagFGameplayTagRequired key tag (empty = unlocked)
bConsumeKeyOnUnlockboolWhether key is consumed on use
bIsUnlockedboolRuntime state (replicated)

LockTag requirement:

  • LockTag must be a registered gameplay tag.
  • Project-owned tags should be native tags from ProjectCore (ProjectGameplayTags.h/.cpp).
  • Config/DefaultGameplayTags.ini should be used for engine/plugin/imported tags that are not native declarations.
  • Invalid tag strings are now logged as errors during definition spawn/apply and the lock behaves as unlocked.

API:

// Query
bool IsLocked() const;           // Has LockTag and not yet unlocked
FGameplayTag GetLockTag() const; // Get required key tag
bool ShouldConsumeKey() const;   // Whether to consume key

// Mutation (server-only)
void Unlock();  // Mark as unlocked
void Lock();    // Reset to locked state

Delegates:

FOnAccessDenied OnAccessDenied;  // Broadcast when locked + interaction attempted
FOnUnlocked OnUnlocked;          // Broadcast when unlocked
FOnLocked OnLocked;              // Broadcast when locked

IProjectActionReceiver (authority-only, no-ops on client):

  • lock.unlock -> calls Unlock()
  • lock.lock -> calls Lock()

Interaction passthrough:

  • When already unlocked, interaction forwards motion.toggle to same-scope motion receivers (door keeps normal open/close behavior in single-selection interaction mode).

Mesh scope persistence contract:

  • Lockable mesh scope binding must persist across editor world -> PIE duplication.
  • If mesh scope is missing at runtime, mesh-scoped motion capabilities can win focus/interaction (Open) before lock checks.

Usage:

// In actor constructor
LockComponent = CreateDefaultSubobject<ULockableComponent>(TEXT("LockComponent"));

// In interaction handler
if (auto* Lock = Target->FindComponentByClass<ULockableComponent>())
{
    if (Lock->IsLocked())
    {
        if (!HasKeyFor(Lock->GetLockTag()))
            return; // No access
        Lock->Unlock();
    }
}

UActorWatcherComponent

Universal watched-actor event capability. Resolves a target actor by explicit reference and/or query filters, binds to a trigger source, then emits normalized events to listeners on the owner (via IActorWatchEventListener) and through a Blueprint multicast delegate.

Properties:

PropertyTypeDescription
WatchedActorTSoftObjectPtr<AActor>Explicit actor to watch
WatchedActorTagFNameOptional actor tag filter for actor resolution
WatchedActorNameContainsFStringOptional actor-name substring filter
TriggerEActorWatchTriggerSource trigger type (LockAccessDenied)
RequiredEventNameFNameOptional exact event name filter
RequiredEventTagFGameplayTagOptional exact gameplay-tag filter
RequiredEventTextContainsFStringOptional case-insensitive event text substring filter

Delegates:

FOnActorWatchEvent OnWatchEvent;  // Normalized event payload (name/tag/text/source/instigator)

Use case: NPC actor watches nearby locked door and reacts to denied access without hard-coupling to lockable internals in dialogue code.

class UMyNpcLogicComponent : public UActorComponent, public IActorWatchEventListener
{
    void HandleActorWatchEvent(const FActorWatchEvent& Event) override
    {
        if (Event.EventName == FName(TEXT("lock.access_denied")))
        {
            // React (start dialogue, quest, bark, etc.)
        }
    }
};

ActorWatcher is a capability ID (CapabilityComponent:ActorWatcher) and can be spawned from object definitions.

Runtime contract for door -> NPC dialogue:

  • ULockableComponent only reports deny (OnAccessDenied) and does not open UI.
  • UActorWatcherComponent only emits normalized events and notifies listeners.
  • Dialogue/UI behavior is owned by listener components (for example UProjectDialogueComponent).

Quick log checks:

  • Lock path: LogProjectObjectCapabilities ... Lockable ... Access denied
  • Watch path: LogProjectObjectCapabilities ... ActorWatcher ... Event emitted (Name=lock.access_denied, ...)
  • If both exist, capability side is healthy and debugging should continue in ProjectDialogue/ProjectDialogueUI.

ULootContainerCapabilityComponent

World interaction shell for searchable/openable storage containers.

Owns:

  • interaction label/state (Search, empty/busy response)
  • stable world-container identity
  • canonical runtime container entries exposed through IWorldContainerSessionSource
  • authority-side single-opener rule for FullOpen

Does not own:

  • authored storage spec
  • inventory-side session orchestration
  • inventory UI layout

Authoring contract:

  • capability stays in capabilities[]
  • container data lives in sections.storage
  • exact contents default to seedEntries
  • reusable randomized fill references lootProfileId

Integration route:

  • ProjectInteraction resolves focus and E
  • ProjectSinglePlay routes local input
  • ProjectInventory opens session and executes transfer rules
  • ProjectInventoryUI presents the nearby container inside the inventory screen

Future Components

Planned capability components:

  • UPowerableComponent - Requires power to function
  • UHealthComponent - Can be damaged/destroyed
  • UOwnershipComponent - Owner-only access

Dependencies

  • ProjectCore (foundation)
  • GameplayTags (for FGameplayTag)

Consumers

  • ProjectObject (Resources) - Door, window, chest actors
  • ProjectInventory (Features) - Interaction handler checks lock

References

Legacy Paths

Code marker format:

  • // LEGACY_OBJECT_PARENT_GENERALIZATION(L###): <reason>. Remove when <condition>.
Legacy IDLocationWhy It ExistsRemove Trigger
(none active)n/aCapability hierarchy resolution now uses strict interface-only mesh targetingn/a