A Smart Object is an object in the world that fully contains all of the data and logic necessary to tell characters in the world how to interact with it. It has a set number of Smart Object Slots that can be occupied by characters at one time, with reservations managed by the Game Server. This has been set up to be similar to the built-in Smart Objects feature present in Unreal Engine 5, but it is not exactly the same and has been custom-tailored to fit into our game code.
This is a scene component with a transform that a character detects and gets to to interact with an object and run the custom object logic. It’s the component that will drive the logic of that one NPC. It contains these variables:
Instance variables:
SlotState
(enum). There is a
SignalSlotStateChanged
delegate that gets triggered when the state changes to hook some logic.
Deactivated
: The slot cannot be detected. Entering that state frees an interacting character
Available
: An action can be performed
Reserved
: A character is
going
to the slot
InUse
: A character is in the slot
InteractingCharacter
Static variables:
SmartObjectConditions
: Array of
SmartObjectCondition
objects that are checked to see if a character can interact with a slot
SmartObjectFunctions
: Array of
SmartObjectFunction
objects that are run when the slot is used.
AcceptanceRadius
: Distance to the slot to consider being reached
AcceptedYawDelta
: Same but with rotation
SlotType
(gameplay tags variable): To describe which type of interaction is possible.
TimeToDeactivate
/
TimeToReactivate
: If the corresponding float variables are positive, the slot will deactivate or reactivate after a given amount of time.
API:
Activate
: Sets the
State
to
Available
if in
Deactivated
, otherwise do nothing. If
TimeToDeactivate
is positive, then the slot will be set to
Deactivated
after that time.
Deactivate
: Calls
ReleaseCharacter
and sets the SlotState to
Deactivated
. If
TimeToReactivate
is positive, then the slot will be set to
Available
after that time.
Reserve(Character)
: Puts the SlotState into
Reserved
and sets the
InteractingCharacter
Use(Character)
: Puts the SlotState into
InUse
and sets the
InteractingCharacter
ForceReleaseCharacter
: Immediately free any
InteractingCharacter
RequestReleaseCharacter
: Passes the character release request to the Smart Object functions that can release the character based on their own logic, such as releasing the character after the ending section of the emote. If none of the Smart Object functions attached to the Smart Object slot contains such logic, the
InteractingCharacter
will be released using
ForceReleaseCharacter
.
CanUse(Character, OwningActor, bCanShowNotifications)
: Checks whether the given character can use the slot. Uses
SlotState
and checks the results of smart object conditions.
OwningActor
is the actor that owns the slot component. When
bCanShowNotifications
is true, Smart Object Conditions will be allowed to show failure notifications. These notifications can describe reasons for failure, like when attempting to use smart objects while mounted, etc.
DisplayPreview
: Meant to be called to display the slots in the world, will simply forward the call to smart object functions.
Some presets are available in
Game/Systems/SmartObject/Slots
to show typical use cases, like
SOS_SimpleAnim
and
SOS_SimpleEmote
. See
Using the Smart Object system
These are UObjects that will be added to a
SmartObjectSlotComponent
. To be able to be set directly on the component, they should use the
EditInlineNew
UCLASS tag, and the container variable the
Instanced
UPROPERTY tag . The base slot component class triggers the events as appropriate. The API is:
CanUse(Character, OwningActor, bCanShowNotifications)
: Forwarded from the slot component. The optional parameter
bCanShowNotifications
enables the condition to show HUD notifications, providing descriptions of failure reasons for the players.
Base set of smart object conditions:
SOC_CheckOwnedPlaceable
: Checks the parent placeable UniqueID and compares it with the interacting character.
SOC_RestrictCharacterType
:
Booleans (AcceptsThrallPlacementBrushes, AcceptsPlayers, AcceptsNPCs)
GameplayTagQuery to validate NPC gameplay tags if the interacting character is an NPC (ignore otherwise).
To get a comprehensive list, have a look at
SOS_AllCoreFunctions
in
Game/Systems/SmartObject
Just like smart object conditions, those are objects stored on the slot component which triggers the events as appropriate. The API is:
OnReserved
OnUse
OnRelease
RequestRelease
: Optional. Allows releasing of the interacting character using custom logic.
DisplayPreview(SlotTransform, Character)
: Only called on clients, allows to choose how to display a preview of the slot.
StopPreview
: Clean up anything triggered by
DisplayPreview
when we don’t need to show the preview anymore
OnSlotBeginPlay
: Called when the slot receives the BeginPlay event.
OnSlotEndPlay
: Called when the slot receives the EndPlay event. This is only called when the reason for the EndPlay event was "Destroyed."
Some smart object functions examples:
SOF_ForceTransform
: Snaps the character to a transform relative to the slot component (if the slot moves, the character also gets moved).
DisplayPreview
also shows the current pose of the character at the actual transform as a ghost humanoid.
SOF_Emote
:
Uses an
Emote
variable (enum
ECharacterEmotes
) to store which emote to play. If the emote isn't looping, deactivate the slot when the emote is done playing. Otherwise follow the looping parameters from the EmotesDataTable.
To have more emotes variations, we should update the EmotesDataTable rows to have an array of montages instead of a single entry, which should be handled by the emotes manager
DisplayPreview
also plays the looping part of the emote as a ghost humanoid.
OnRelease
sets the slot to
Deactivated
with a
TimeToReactivate
that corresponds to the duration of the exit section of the emote.
RequestReleaseCharacter
stops the emote using the Emote Controller. This allows the ending section of the emote to be played before the character is released from the Smart Object slot, such as closing the book the character is reading, for example.
SOF_OverrideAIParameters
: Provides a set of variables that can be overridden on the NPC, like the behavior tree, home location, leashing distance, tactics or engagement type. Also supports adding a DynamicEngagementBehavior.
SOF_ExitWhenAttacked
Allows the interacting character to enter combat, automatically reserving the slot. When the combat ends, the character will attempt to return to the slot and reuse it.
To get a comprehensive list, have a look at
SOS_AllCoreFunctions
in
Game/Sytems/SmartObject
This is a singleton actor that can be gotten from
AConanGameState
that is used to get access to smart object slots. Internally, smart object slots might be destroyed at runtime to save some resources on the server so going through the manager allows to construct them on the fly.
GetAvailableSmartObjectSlotsAroundLocation(Location, Radius, Height, Character, GameplayTagQuery)
returns an array of
SmartObjectSlotResult
structs representing the available slots for a given NPC (using the
CanUse
function). The structs would be (Transform, Emote, Type).
so.showqueries 1
should enable debug for this query, showing the cylinder that is used to find the slots and the found smart object slots
GetAvailableSmartObjectSlotsOnActor(Actor, Character, GameplayTagQuery)
does the same but finding the smart object slot components attached to the given actor
so.showqueries 1
should just show the found smart object slots for this
ReserveSmartObjectSlot(SmartObjectSlotResult, Character)
returns the SlotComponent that can be used to call
Use
This is for any coordination logic between the slots. It should be implemented in the actor itself.
The API:
OnSmartObjectQueried
when a character calls
GetAvailableSmartObjectSlots
. Returns a bool that specifies whether the query can go through, or if all of the smart object slot components should not be returned by the query.
OnSmartObjectReserved(Component)
when a character calls
ReserveObjectSlot
GetSpawnDataForSmartObjectSlot
retrieves the data required for spawning a smart object slot dynamically at runtime based on the slot type.
Any other bindings can be done directly to the slot delegates themselves, like binding to
SmartObjectSlotComponent::OnStateChanged
from
SetSlotState
. But you should rather bind to the actual logic that changed.
In addition to placeables, we could also have other things like resource nodes or in-game elements such as dungeon doors use the
SmartObjectInterface
and have their own logic to return slots (resource nodes could generate them on the fly).
This should be useful from the
ThrallSystemComponent
and the
BuildSystemComponent
.
They would call
GetAvailableSmartObjectSlots
from the
SmartObjectManager
and call
DisplayPreview
on the linked CDO slot component, which should show a ghost humanoid preview of what the thrall would do on the slot.
When using the
ThrallSystemComponent
, when the brush comes in range of the smart object slot, it would be snapped in place of the ghost humanoid preview.
This is the part that chooses which smart object slot to interact with
Thralls : The player will see the available slots when moving/placing the thrall and can assign them directly
Players : This should be part of the interaction logic of the parent actor. For instance, for placeables, if there is no other logic tied to interacting, it should scan for the available slot components and try to reach the closest available one. Placeables should make sure that there is only one behavior tied to interact (for instance the hidden bookshelf should only have the interaction to open it, not to read a book in front of it)
NPCs : The way NPCs choose to interact with smart object depends on which system relies on smart objects (purge behaviors, the tavern, a potential settlements system…), but it should involve querying nearby available slots. They also need to be notified when slots become available.
Assigning a thrall to a smart object slot should be considered his new home location, and a reference to the smart object should be saved in the thrall component alongside the home location variable.
This way, when a thrall is released from the slot (like for instance by being attacked by an enemy or when put in following mode), the thrall can instantly reserve the slot so no other thrall is assigned to it, and then it can naturally go back to the smart object slot when done with its other AI tasks.
There are different ways of getting to a slot:
A thrall is put inside a slot by a player
An NPC detects a slot and paths to it
A player interacts with a slot/placeable and uses force movement to get to it
All those ways are handled by systems external to the smart object system. When the smart object is detected, it is put in the
Reserved
state until the character gets to it. When the system that gets the character to the slot is happy with itself, it then calls
BeginUse
that puts the slot in the
InUse
state.
For the player flow typically (but also NPC AI), interacting with a smart object will make you enter a forced movement towards the smart object slot location. This logic is triggered with
TryForceMoveToSmartObject
. It has the parameters:
MovementRequest
that allows specifying a speed curve throughout the movement
MaxReachTime
to allow the movement to fail and snap the character to the slot
bCanCancel
to allow the player to cancel the movement through normal movement or dodge, crouch, etc…
MaxReachAttempts
to be more lenient with moving smart objects
Additionally:
The acceptance radius is the smart object slot
Radius
variable (with a minimum of 50)
RotateCharacterWhenMovingToSlot
will rotate the character to match the smart object slot rotation after having finished the first movement