254 lines
9.5 KiB
Markdown
254 lines
9.5 KiB
Markdown
# Apple Hills Interaction System
|
||
|
||
A concise, code-first guide to creating and extending interactions using `Interactable` and modular action/requirement components. Designed to match the style of the other updated docs (TOC, inline code, case studies).
|
||
|
||
## Table of Contents
|
||
- [What This Solves](#what-this-solves)
|
||
- [Architecture at a Glance](#architecture-at-a-glance)
|
||
- [Quick Start (Code-First)](#quick-start-code-first)
|
||
- [Subscribe to Interaction Events](#subscribe-to-interaction-events)
|
||
- [Create a Custom Action](#create-a-custom-action)
|
||
- [Trigger Programmatically](#trigger-programmatically)
|
||
- [Core Components](#core-components)
|
||
- [`Interactable`](#interactable)
|
||
- [`CharacterMoveToTarget`](#charactermovetotarget)
|
||
- [`InteractionActionBase` and concrete actions](#interactionactionbase-and-concrete-actions)
|
||
- [`InteractionRequirementBase`](#interactionrequirementbase)
|
||
- [Interaction Event Flow](#interaction-event-flow)
|
||
- [Case Studies](#case-studies)
|
||
- [Open a Door on Arrival](#open-a-door-on-arrival)
|
||
- [Pick Up an Item then Play Timeline](#pick-up-an-item-then-play-timeline)
|
||
- [Kick Off Dialogue When Player Arrives](#kick-off-dialogue-when-player-arrives)
|
||
- [Troubleshooting / FAQ](#troubleshooting--faq)
|
||
- [Paths & Namespaces](#paths--namespaces)
|
||
- [Change Log](#change-log)
|
||
|
||
## What This Solves
|
||
- Standardized interaction lifecycle with reliable events (`InteractionStarted`, `PlayerArrived`, `InteractingCharacterArrived`, `InteractionComplete`, `InteractionInterrupted`).
|
||
- Composable behavior via components derived from `InteractionActionBase` and `InteractionRequirementBase`.
|
||
- Clean separation of input, locomotion-to-target, cinematic timelines, and game logic.
|
||
|
||
## Architecture at a Glance
|
||
- Driver: `Interactable` — owns lifecycle, input hook, character selection via `CharacterToInteract`, one‑shot/cooldown, and event dispatch.
|
||
- Targets: `CharacterMoveToTarget` — editor-authored world points for `Trafalgar`/`Pulver` to path to before executing actions.
|
||
- Actions: `InteractionActionBase` (abstract) — modular responses to specific `InteractionEventType` values; can pause the flow with async tasks.
|
||
- Requirements: `InteractionRequirementBase` (abstract) — gatekeepers for availability; multiple can be attached.
|
||
- Cinematics: `InteractionTimelineAction` — plays one or more `PlayableAsset` timelines per event; optional character auto-binding.
|
||
|
||
## Quick Start (Code-First)
|
||
|
||
### Subscribe to Interaction Events
|
||
```csharp
|
||
using Interactions;
|
||
using UnityEngine;
|
||
|
||
public class InteractDebugHooks : MonoBehaviour
|
||
{
|
||
[SerializeField] private Interactable interactable;
|
||
|
||
private void OnEnable()
|
||
{
|
||
interactable.interactionStarted.AddListener(OnStarted);
|
||
interactable.characterArrived.AddListener(OnCharacterArrived);
|
||
interactable.interactionInterrupted.AddListener(OnInterrupted);
|
||
interactable.interactionComplete.AddListener(OnComplete);
|
||
}
|
||
|
||
private void OnDisable()
|
||
{
|
||
interactable.interactionStarted.RemoveListener(OnStarted);
|
||
interactable.characterArrived.RemoveListener(OnCharacterArrived);
|
||
interactable.interactionInterrupted.RemoveListener(OnInterrupted);
|
||
interactable.interactionComplete.RemoveListener(OnComplete);
|
||
}
|
||
|
||
private void OnStarted(Input.PlayerTouchController player, FollowerController follower)
|
||
=> Debug.Log("Interaction started");
|
||
|
||
private void OnCharacterArrived() => Debug.Log("Character arrived");
|
||
private void OnInterrupted() => Debug.Log("Interaction interrupted");
|
||
private void OnComplete(bool success) => Debug.Log($"Interaction complete: {success}");
|
||
}
|
||
```
|
||
|
||
### Create a Custom Action
|
||
```csharp
|
||
using System.Threading.Tasks;
|
||
using Interactions;
|
||
using Input;
|
||
using UnityEngine;
|
||
|
||
public class PlaySfxOnArrivalAction : InteractionActionBase
|
||
{
|
||
[SerializeField] private AudioSource sfx;
|
||
|
||
private void Reset()
|
||
{
|
||
// React to the arrival event; don't block the flow
|
||
respondToEvents = new() { InteractionEventType.InteractingCharacterArrived };
|
||
pauseInteractionFlow = false;
|
||
}
|
||
|
||
protected override bool ShouldExecute(InteractionEventType evt, PlayerTouchController player, FollowerController follower)
|
||
{
|
||
return sfx != null;
|
||
}
|
||
|
||
protected override async Task<bool> ExecuteAsync(InteractionEventType evt, PlayerTouchController player, FollowerController follower)
|
||
{
|
||
sfx.Play();
|
||
// non-blocking action returns immediately when pauseInteractionFlow == false
|
||
return true;
|
||
}
|
||
}
|
||
```
|
||
Attach this component under the same hierarchy as an `Interactable`. Registration is automatic via `OnEnable()`/`OnDisable()` in `InteractionActionBase`.
|
||
|
||
### Trigger Programmatically
|
||
Normally input goes through `ITouchInputConsumer.OnTap(...)`. For testing, you can call the public tap handler:
|
||
```csharp
|
||
using UnityEngine;
|
||
using Interactions;
|
||
|
||
public class TestTrigger : MonoBehaviour
|
||
{
|
||
[SerializeField] private Interactable interactable;
|
||
|
||
[ContextMenu("Trigger Interact (dev)")]
|
||
private void Trigger()
|
||
{
|
||
interactable.OnTap(interactable.transform.position);
|
||
}
|
||
}
|
||
```
|
||
|
||
## Core Components
|
||
|
||
### `Interactable`
|
||
- Handles input, cooldowns (`cooldown`), one‑shot (`isOneTime`), and which character participates (`characterToInteract`).
|
||
- Exposes events: `interactionStarted`, `characterArrived`, `interactionInterrupted`, `interactionComplete`.
|
||
- Discovers and dispatches to child `InteractionActionBase` components; awaits those that request to pause.
|
||
|
||

|
||
|
||
### `CharacterMoveToTarget`
|
||
Defines the world positions characters should reach before actions evaluate.
|
||
- Can target `Trafalgar`, `Pulver`, or `Both` via configuration.
|
||
- Supports offsets and editor gizmos; multiple instances allowed.
|
||
|
||

|
||
|
||
### `InteractionActionBase` and concrete actions
|
||
- Filter by `InteractionEventType` using `respondToEvents`.
|
||
- Control flow with `pauseInteractionFlow` and async `ExecuteAsync(...)`.
|
||
- Built‑in example: `InteractionTimelineAction` for cinematics.
|
||
|
||

|
||
|
||
### `InteractionRequirementBase`
|
||
- Attach one or more to gate the interaction based on items, puzzles, proximity, etc.
|
||
|
||
## Interaction Event Flow
|
||
1. `InteractionStarted`
|
||
2. `PlayerArrived`
|
||
3. `InteractingCharacterArrived`
|
||
4. `InteractionComplete` (bool success)
|
||
5. `InteractionInterrupted`
|
||
|
||
Actions receive these events in order and may run concurrently; those with `pauseInteractionFlow` true are awaited.
|
||
|
||
## Case Studies
|
||
|
||
### Open a Door on Arrival
|
||
```csharp
|
||
using System.Threading.Tasks;
|
||
using Interactions;
|
||
using Input;
|
||
using UnityEngine;
|
||
|
||
public class DoorOpenOnArrival : InteractionActionBase
|
||
{
|
||
[SerializeField] private Animator animator; // expects a bool parameter "Open"
|
||
|
||
private void Reset()
|
||
{
|
||
respondToEvents = new() { InteractionEventType.InteractingCharacterArrived };
|
||
pauseInteractionFlow = false;
|
||
}
|
||
|
||
protected override async Task<bool> ExecuteAsync(InteractionEventType evt, PlayerTouchController p, FollowerController f)
|
||
{
|
||
animator.SetBool("Open", true);
|
||
return true;
|
||
}
|
||
}
|
||
```
|
||
|
||
### Pick Up an Item then Play Timeline
|
||
Attach two actions: your `PickupItemAction` that pauses until the item is collected, and an `InteractionTimelineAction` mapped to `InteractionEventType.InteractionComplete` to celebrate.
|
||
|
||
### Kick Off Dialogue When Player Arrives
|
||
```csharp
|
||
using System.Threading.Tasks;
|
||
using Dialogue;
|
||
using Input;
|
||
using Interactions;
|
||
using UnityEngine;
|
||
|
||
public class StartDialogueOnArrival : InteractionActionBase
|
||
{
|
||
[SerializeField] private DialogueComponent dialogue;
|
||
|
||
private void Reset()
|
||
{
|
||
respondToEvents = new() { InteractionEventType.PlayerArrived };
|
||
pauseInteractionFlow = false;
|
||
}
|
||
|
||
protected override async Task<bool> ExecuteAsync(InteractionEventType evt, PlayerTouchController p, FollowerController f)
|
||
{
|
||
dialogue.StartDialogue();
|
||
return true;
|
||
}
|
||
}
|
||
```
|
||
|
||
## Troubleshooting / FAQ
|
||
- Interaction doesn’t fire:
|
||
- Confirm `Interactable` is active and not in cooldown or already completed (`isOneTime`).
|
||
- Ensure `CharacterMoveToTarget` exists for the selected `CharacterToInteract`.
|
||
- Actions not running:
|
||
- Verify `respondToEvents` includes the lifecycle moment you expect.
|
||
- Check that the component sits under the same hierarchy so it registers with the `Interactable`.
|
||
- Timeline never finishes:
|
||
- Make sure `InteractionTimelineAction` has valid `PlayableAsset` entries and binding flags.
|
||
- Double triggers:
|
||
- Guard reentry in your actions or check `_interactionInProgress` usage in `Interactable` by following logs.
|
||
|
||
## Paths & Namespaces
|
||
- Scripts: `Assets/Scripts/Interactions/`
|
||
- `Interactable.cs`
|
||
- `InteractionActionBase.cs`
|
||
- `InteractionTimelineAction.cs`
|
||
- `InteractionEventType.cs`
|
||
- `InteractionRequirementBase.cs`
|
||
- Editor tooling: `Assets/Editor/InteractableEditor.cs`
|
||
- Primary namespace: `Interactions`
|
||
|
||
## Additional Editor Visuals
|
||
- Timeline mapping configuration UI:
|
||
|
||

|
||
|
||
- Unity Timeline editor when authoring cinematics for interactions:
|
||
|
||

|
||
|
||
- Example target placement in Scene view:
|
||
|
||

|
||
|
||
## Change Log
|
||
- v1.1: Added Table of Contents, code-first snippets, case studies, standardized inline code references, preserved existing editor images, and added troubleshooting/paths.
|
||
- v1.0: Original overview and setup guide.
|