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 inObjectDefinition.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:
| Property | Type | Description |
|---|---|---|
LockTag | FGameplayTag | Required key tag (empty = unlocked) |
bConsumeKeyOnUnlock | bool | Whether key is consumed on use |
bIsUnlocked | bool | Runtime state (replicated) |
LockTag requirement:
LockTagmust be a registered gameplay tag.- Project-owned tags should be native tags from
ProjectCore(ProjectGameplayTags.h/.cpp). Config/DefaultGameplayTags.inishould 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.toggleto 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:
| Property | Type | Description |
|---|---|---|
WatchedActor | TSoftObjectPtr<AActor> | Explicit actor to watch |
WatchedActorTag | FName | Optional actor tag filter for actor resolution |
WatchedActorNameContains | FString | Optional actor-name substring filter |
Trigger | EActorWatchTrigger | Source trigger type (LockAccessDenied) |
RequiredEventName | FName | Optional exact event name filter |
RequiredEventTag | FGameplayTag | Optional exact gameplay-tag filter |
RequiredEventTextContains | FString | Optional 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:
ULockableComponentonly reports deny (OnAccessDenied) and does not open UI.UActorWatcherComponentonly 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 functionUHealthComponent- Can be damaged/destroyedUOwnershipComponent- Owner-only access
Dependencies
- ProjectCore (foundation)
- GameplayTags (for FGameplayTag)
Consumers
- ProjectObject (Resources) - Door, window, chest actors
- ProjectInventory (Features) - Interaction handler checks lock
References
- ProjectMotionSystem - USpringRotatorComponent (motion)
- ProjectObject - Uses these components
- ProjectInteraction - Player-side interaction
Legacy Paths
Code marker format:
// LEGACY_OBJECT_PARENT_GENERALIZATION(L###): <reason>. Remove when <condition>.
| Legacy ID | Location | Why It Exists | Remove Trigger |
|---|---|---|---|
| (none active) | n/a | Capability hierarchy resolution now uses strict interface-only mesh targeting | n/a |