16 KiB
Interactables System - Code Reference
Table of Contents
- Overview
- Class Hierarchy
- InteractableBase - The Template Method
- Creating Custom Interactables
- Character Movement
- Action Component System
- Events System
- Save/Load System Integration
- Integration with Puzzle System
- Advanced Patterns
Overview
Simple, centrally orchestrated interaction system for player and follower characters.
Core Concepts
- Template Method Pattern:
InteractableBasedefines the interaction flow; subclasses override specific steps - Action Component System: Modular actions respond to interaction events independently
- Async/Await Flow: Character movement and timeline playback use async patterns
- Save/Load Integration:
SaveableInteractableprovides persistence for interaction state
Class Hierarchy
ManagedBehaviour
└── InteractableBase
├── OneClickInteraction
└── SaveableInteractable
├── Pickup
└── ItemSlot
Class Descriptions
-
InteractableBase - Abstract base class that orchestrates the complete interaction flow using the Template Method pattern. Handles tap input, character movement, validation, and event dispatching for all interactables.
-
SaveableInteractable - Extends InteractableBase with save/load capabilities, integrating with the ManagedBehaviour save system. Provides abstract methods for JSON serialization and deserialization of state.
-
OneClickInteraction - Simplest concrete interactable that completes immediately when character arrives with no additional logic. All functionality comes from UnityEvents configured in the Inspector.
-
Pickup - Represents items that can be picked up by the follower, handling item combination and state tracking. Integrates with ItemManager and supports bilateral restoration with ItemSlots.
-
ItemSlot - Container that accepts specific items with validation for correct/incorrect/forbidden items. Manages item placement, swapping, and supports combination with special puzzle integration that allows swapping when locked.
InteractableBase - The Template Method
Interaction Flow
When a player taps an interactable, the following flow executes:
OnTap() → CanBeClicked() → StartInteractionFlowAsync()
↓
1. Find Characters (player, follower)
2. OnInteractionStarted() [Virtual Hook]
3. Fire interactionStarted events
4. MoveCharactersAsync()
5. OnInteractingCharacterArrived() [Virtual Hook]
6. Fire characterArrived events
7. ValidateInteraction()
8. DoInteraction() [Virtual Hook - OVERRIDE THIS]
9. OnInteractionFinished() [Virtual Hook]
10. Fire interactionComplete events
Virtual Methods to Override
1. CanBeClicked() - Pre-Interaction Validation
protected virtual bool CanBeClicked()
{
if (!isActive) return false;
// Add custom checks here
return true;
}
When to override: Add high-level validation before interaction starts (cooldowns, prerequisites, etc.)
2. OnInteractionStarted() - Setup Logic
protected virtual void OnInteractionStarted()
{
// Called after characters found, before movement
// Setup animations, sound effects, etc.
}
When to override: Perform setup that needs to happen before character movement
3. DoInteraction() - Main Logic ⭐ OVERRIDE THIS
protected override bool DoInteraction()
{
// Your interaction logic here
return true; // Return true for success, false for failure
}
When to override: Always override this - this is your main interaction logic
4. OnInteractingCharacterArrived() - Arrival Reaction
protected virtual void OnInteractingCharacterArrived()
{
// Called when character reaches interaction point
// Trigger arrival animations, sounds, etc.
}
When to override: React to character arrival with visuals/audio
5. OnInteractionFinished() - Cleanup Logic
protected virtual void OnInteractionFinished(bool success)
{
// Called after interaction completes
// Cleanup, reset state, etc.
}
When to override: Perform cleanup after interaction completes
6. CanProceedWithInteraction() - Validation
protected virtual (bool canProceed, string errorMessage) CanProceedWithInteraction()
{
// Validate if interaction can proceed
// Return error message to show to player
return (true, null);
}
When to override: Add validation that shows error messages to player
Creating Custom Interactables
Example 1: Simple Button (OneClickInteraction)
The simplest interactable just completes when the character arrives:
using Interactions;
public class OneClickInteraction : InteractableBase
{
protected override bool DoInteraction()
{
// Simply return success - no additional logic needed
return true;
}
}
Use Case: Triggers, pressure plates, simple activators
Configuration:
- Set
characterToInteractto define which character activates it - Use UnityEvents in inspector to trigger game logic
Example 2: Item Pickup
From Pickup.cs - demonstrates validation and follower interaction:
public class Pickup : SaveableInteractable
{
public PickupItemData itemData;
public bool IsPickedUp { get; internal set; }
protected override bool DoInteraction()
{
// Try combination first if follower is holding something
var heldItemObject = FollowerController?.GetHeldPickupObject();
var heldItemData = heldItemObject?.GetComponent<Pickup>()?.itemData;
var combinationResult = FollowerController.TryCombineItems(
this, out var resultItem
);
if (combinationResult == FollowerController.CombinationResult.Successful)
{
IsPickedUp = true;
FireCombinationEvent(resultItem, heldItemData);
return true;
}
// No combination - do regular pickup
FollowerController?.TryPickupItem(gameObject, itemData);
IsPickedUp = true;
OnItemPickedUp?.Invoke(itemData);
return true;
}
}
Key Patterns:
- Access
FollowerControllerdirectly (set by base class) - Return
truefor successful pickup - Use custom events (
OnItemPickedUp) for specific notifications
Example 3: Item Slot with Validation
From ItemSlot.cs - demonstrates complex validation and state management:
public class ItemSlot : SaveableInteractable
{
public PickupItemData itemData; // What item should go here
private ItemSlotState currentState = ItemSlotState.None;
protected override (bool canProceed, string errorMessage) CanProceedWithInteraction()
{
var heldItem = FollowerController?.CurrentlyHeldItemData;
// Can't interact with empty slot and no item
if (heldItem == null && currentlySlottedItemObject == null)
return (false, "This requires an item.");
// Check forbidden items
if (heldItem != null && currentlySlottedItemObject == null)
{
var config = interactionSettings?.GetSlotItemConfig(itemData);
var forbidden = config?.forbiddenItems ?? new List<PickupItemData>();
if (PickupItemData.ListContainsEquivalent(forbidden, heldItem))
return (false, "Can't place that here.");
}
return (true, null);
}
protected override bool DoInteraction()
{
var heldItemData = FollowerController.CurrentlyHeldItemData;
var heldItemObj = FollowerController.GetHeldPickupObject();
// Scenario 1: Slot empty + holding item = Slot it
if (heldItemData != null && currentlySlottedItemObject == null)
{
SlotItem(heldItemObj, heldItemData);
FollowerController.ClearHeldItem();
return IsSlottedItemCorrect(); // Returns true only if correct item
}
// Scenario 2: Slot full + holding item = Try combine or swap
if (currentlySlottedItemObject != null)
{
// Try combination...
// Or swap items...
}
return false;
}
}
Key Patterns:
CanProceedWithInteraction()shows error messages to playerDoInteraction()returns true only for correct item (affects puzzle completion)- Access settings via
GameManager.GetSettingsObject<T>()
Character Movement
Character Types
public enum CharacterToInteract
{
None, // No character movement
Trafalgar, // Player only
Pulver, // Follower only (player moves to range first)
Both // Both characters move
}
Set in Inspector on InteractableBase.
Custom Movement Targets
Add CharacterMoveToTarget component as child of your interactable:
// Automatically used if present
var moveTarget = GetComponentInChildren<CharacterMoveToTarget>();
Vector3 targetPos = moveTarget.GetTargetPosition();
See Editor Reference for details.
Action Component System
Add modular behaviors to interactables via InteractionActionBase components.
Creating an Action Component
using Interactions;
using System.Threading.Tasks;
public class MyCustomAction : InteractionActionBase
{
protected override async Task<bool> ExecuteAsync(
InteractionEventType eventType,
PlayerTouchController player,
FollowerController follower)
{
// Your action logic here
if (eventType == InteractionEventType.InteractionStarted)
{
// Play sound, spawn VFX, etc.
await Task.Delay(1000); // Simulate async work
}
return true; // Return success
}
protected override bool ShouldExecute(
InteractionEventType eventType,
PlayerTouchController player,
FollowerController follower)
{
// Add conditions for when this action should run
return base.ShouldExecute(eventType, player, follower);
}
}
Configuring in Inspector
- Respond To Events: Select which events trigger this action
- Pause Interaction Flow: If true, interaction waits for this action to complete
Built-in Action: Timeline Playback
InteractionTimelineAction plays Unity Timeline sequences in response to events:
// Automatically configured via Inspector
// See Editor Reference for details
Features:
- Character binding to timeline tracks
- Sequential timeline playback
- Loop options (loop all, loop last)
- Timeout protection
Events System
UnityEvents (Inspector-Configurable)
Available on all InteractableBase:
[Header("Interaction Events")]
public UnityEvent<PlayerTouchController, FollowerController> interactionStarted;
public UnityEvent interactionInterrupted;
public UnityEvent characterArrived;
public UnityEvent<bool> interactionComplete; // bool = success
C# Events (Code Subscribers)
Pickup example:
public event Action<PickupItemData> OnItemPickedUp;
public event Action<PickupItemData, PickupItemData, PickupItemData> OnItemsCombined;
ItemSlot example:
public event Action<PickupItemData> OnItemSlotRemoved;
public event Action<PickupItemData, PickupItemData> OnCorrectItemSlotted;
public event Action<PickupItemData, PickupItemData> OnIncorrectItemSlotted;
Subscribing to Events
void Start()
{
var pickup = GetComponent<Pickup>();
pickup.OnItemPickedUp += HandleItemPickedUp;
}
void HandleItemPickedUp(PickupItemData itemData)
{
Debug.Log($"Picked up: {itemData.itemName}");
}
void OnDestroy()
{
var pickup = GetComponent<Pickup>();
if (pickup != null)
pickup.OnItemPickedUp -= HandleItemPickedUp;
}
Save/Load System Integration
Making an Interactable Saveable
- Inherit from
SaveableInteractableinstead ofInteractableBase - Define a serializable data structure
- Override
GetSerializableState()andApplySerializableState()
Example Implementation
using Interactions;
using UnityEngine;
// 1. Define save data structure
[System.Serializable]
public class MyInteractableSaveData
{
public bool hasBeenActivated;
public int activationCount;
}
// 2. Inherit from SaveableInteractable
public class MyInteractable : SaveableInteractable
{
private bool hasBeenActivated = false;
private int activationCount = 0;
// 3. Serialize state
protected override object GetSerializableState()
{
return new MyInteractableSaveData
{
hasBeenActivated = this.hasBeenActivated,
activationCount = this.activationCount
};
}
// 4. Deserialize state
protected override void ApplySerializableState(string serializedData)
{
var data = JsonUtility.FromJson<MyInteractableSaveData>(serializedData);
if (data == null) return;
this.hasBeenActivated = data.hasBeenActivated;
this.activationCount = data.activationCount;
// IMPORTANT: Don't fire events during restoration
// Don't re-run initialization logic
}
protected override bool DoInteraction()
{
hasBeenActivated = true;
activationCount++;
return true;
}
}
Integration with Puzzle System
Interactables can be puzzle steps by adding ObjectiveStepBehaviour:
// On GameObject with Interactable component
var stepBehaviour = gameObject.AddComponent<ObjectiveStepBehaviour>();
stepBehaviour.stepData = myPuzzleStepSO;
Automatic Puzzle Integration
InteractableBase automatically checks for puzzle locks:
private (bool, string) ValidateInteractionBase()
{
var step = GetComponent<PuzzleS.ObjectiveStepBehaviour>();
if (step != null && !step.IsStepUnlocked())
{
// Special case: ItemSlots can swap even when locked
if (!(this is ItemSlot))
{
return (false, "This step is locked!");
}
}
return (true, null);
}
Result: Locked puzzle steps can't be interacted with (except ItemSlots for item swapping).
Advanced Patterns
Async Validation
For complex validation that requires async operations:
protected override (bool canProceed, string errorMessage) CanProceedWithInteraction()
{
// Synchronous validation only
// Async validation should be done in OnInteractionStarted
return (true, null);
}
protected override void OnInteractionStarted()
{
// Can perform async checks here if needed
// But interaction flow continues automatically
}
Interrupting Interactions
Interactions auto-interrupt if player cancels movement:
// Automatically handled in MoveCharactersAsync()
playerRef.OnMoveToCancelled += () => {
interactionInterrupted?.Invoke();
// Flow stops here
};
One-Time Interactions
[Header("Interaction Settings")]
public bool isOneTime = true;
// Automatically disabled after first successful interaction
// No override needed
Cooldown Systems
[Header("Interaction Settings")]
public float cooldown = 5f; // Seconds
// Automatically handled by base class
// Interaction disabled for 5 seconds after completion
