Files
AppleHillsProduction/docs/interactables/code_reference.md
2025-11-11 15:55:38 +01:00

16 KiB

Interactables System - Code Reference

Table of Contents

  1. Overview
  2. Class Hierarchy
  3. InteractableBase - The Template Method
  4. Creating Custom Interactables
  5. Character Movement
  6. Action Component System
  7. Events System
  8. Save/Load System Integration
  9. Integration with Puzzle System
  10. Advanced Patterns

Overview

Simple, centrally orchestrated interaction system for player and follower characters.

Core Concepts

  • Template Method Pattern: InteractableBase defines 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: SaveableInteractable provides 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 characterToInteract to 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 FollowerController directly (set by base class)
  • Return true for 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 player
  • DoInteraction() 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

Action Component Setup

  • 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

  1. Inherit from SaveableInteractable instead of InteractableBase
  2. Define a serializable data structure
  3. Override GetSerializableState() and ApplySerializableState()

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