573 lines
16 KiB
Markdown
573 lines
16 KiB
Markdown
# Interactables System - Code Reference
|
|
|
|
## Table of Contents
|
|
|
|
1. [Overview](#overview)
|
|
2. [Class Hierarchy](#class-hierarchy)
|
|
3. [InteractableBase - The Template Method](#interactablebase---the-template-method)
|
|
- [Interaction Flow](#interaction-flow)
|
|
- [Virtual Methods to Override](#virtual-methods-to-override)
|
|
4. [Creating Custom Interactables](#creating-custom-interactables)
|
|
- [Example 1: Simple Button (OneClickInteraction)](#example-1-simple-button-oneclickinteraction)
|
|
- [Example 2: Item Pickup](#example-2-item-pickup)
|
|
- [Example 3: Item Slot with Validation](#example-3-item-slot-with-validation)
|
|
5. [Character Movement](#character-movement)
|
|
6. [Action Component System](#action-component-system)
|
|
7. [Events System](#events-system)
|
|
8. [Save/Load System Integration](#saveload-system-integration)
|
|
9. [Integration with Puzzle System](#integration-with-puzzle-system)
|
|
10. [Advanced Patterns](#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:
|
|
|
|
```csharp
|
|
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
|
|
```csharp
|
|
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
|
|
```csharp
|
|
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**
|
|
```csharp
|
|
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
|
|
```csharp
|
|
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
|
|
```csharp
|
|
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
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
// Automatically used if present
|
|
var moveTarget = GetComponentInChildren<CharacterMoveToTarget>();
|
|
Vector3 targetPos = moveTarget.GetTargetPosition();
|
|
```
|
|
|
|
See [Editor Reference](editor_reference.md#character-movement-targets) for details.
|
|
|
|
---
|
|
|
|
## Action Component System
|
|
|
|
Add modular behaviors to interactables via `InteractionActionBase` components.
|
|
|
|
### Creating an Action Component
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
// 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`:
|
|
|
|
```csharp
|
|
[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:
|
|
```csharp
|
|
public event Action<PickupItemData> OnItemPickedUp;
|
|
public event Action<PickupItemData, PickupItemData, PickupItemData> OnItemsCombined;
|
|
```
|
|
|
|
ItemSlot example:
|
|
```csharp
|
|
public event Action<PickupItemData> OnItemSlotRemoved;
|
|
public event Action<PickupItemData, PickupItemData> OnCorrectItemSlotted;
|
|
public event Action<PickupItemData, PickupItemData> OnIncorrectItemSlotted;
|
|
```
|
|
|
|
### Subscribing to Events
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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`:
|
|
|
|
```csharp
|
|
// On GameObject with Interactable component
|
|
var stepBehaviour = gameObject.AddComponent<ObjectiveStepBehaviour>();
|
|
stepBehaviour.stepData = myPuzzleStepSO;
|
|
```
|
|
|
|
### Automatic Puzzle Integration
|
|
|
|
`InteractableBase` automatically checks for puzzle locks:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
// Automatically handled in MoveCharactersAsync()
|
|
playerRef.OnMoveToCancelled += () => {
|
|
interactionInterrupted?.Invoke();
|
|
// Flow stops here
|
|
};
|
|
```
|
|
|
|
### One-Time Interactions
|
|
|
|
```csharp
|
|
[Header("Interaction Settings")]
|
|
public bool isOneTime = true;
|
|
|
|
// Automatically disabled after first successful interaction
|
|
// No override needed
|
|
```
|
|
|
|
### Cooldown Systems
|
|
|
|
```csharp
|
|
[Header("Interaction Settings")]
|
|
public float cooldown = 5f; // Seconds
|
|
|
|
// Automatically handled by base class
|
|
// Interaction disabled for 5 seconds after completion
|
|
``` |