9.5 KiB
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
- Architecture at a Glance
- Quick Start (Code-First)
- Core Components
- Interaction Event Flow
- Case Studies
- Troubleshooting / FAQ
- Paths & Namespaces
- Change Log
What This Solves
- Standardized interaction lifecycle with reliable events (
InteractionStarted,PlayerArrived,InteractingCharacterArrived,InteractionComplete,InteractionInterrupted). - Composable behavior via components derived from
InteractionActionBaseandInteractionRequirementBase. - Clean separation of input, locomotion-to-target, cinematic timelines, and game logic.
Architecture at a Glance
- Driver:
Interactable— owns lifecycle, input hook, character selection viaCharacterToInteract, one‑shot/cooldown, and event dispatch. - Targets:
CharacterMoveToTarget— editor-authored world points forTrafalgar/Pulverto path to before executing actions. - Actions:
InteractionActionBase(abstract) — modular responses to specificInteractionEventTypevalues; can pause the flow with async tasks. - Requirements:
InteractionRequirementBase(abstract) — gatekeepers for availability; multiple can be attached. - Cinematics:
InteractionTimelineAction— plays one or morePlayableAssettimelines per event; optional character auto-binding.
Quick Start (Code-First)
Subscribe to Interaction Events
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
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:
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
InteractionActionBasecomponents; awaits those that request to pause.
CharacterMoveToTarget
Defines the world positions characters should reach before actions evaluate.
- Can target
Trafalgar,Pulver, orBothvia configuration. - Supports offsets and editor gizmos; multiple instances allowed.
InteractionActionBase and concrete actions
- Filter by
InteractionEventTypeusingrespondToEvents. - Control flow with
pauseInteractionFlowand asyncExecuteAsync(...). - Built‑in example:
InteractionTimelineActionfor cinematics.
InteractionRequirementBase
- Attach one or more to gate the interaction based on items, puzzles, proximity, etc.
Interaction Event Flow
InteractionStartedPlayerArrivedInteractingCharacterArrivedInteractionComplete(bool success)InteractionInterrupted
Actions receive these events in order and may run concurrently; those with pauseInteractionFlow true are awaited.
Case Studies
Open a Door on Arrival
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
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
Interactableis active and not in cooldown or already completed (isOneTime). - Ensure
CharacterMoveToTargetexists for the selectedCharacterToInteract.
- Confirm
- Actions not running:
- Verify
respondToEventsincludes the lifecycle moment you expect. - Check that the component sits under the same hierarchy so it registers with the
Interactable.
- Verify
- Timeline never finishes:
- Make sure
InteractionTimelineActionhas validPlayableAssetentries and binding flags.
- Make sure
- Double triggers:
- Guard reentry in your actions or check
_interactionInProgressusage inInteractableby following logs.
- Guard reentry in your actions or check
Paths & Namespaces
- Scripts:
Assets/Scripts/Interactions/Interactable.csInteractionActionBase.csInteractionTimelineAction.csInteractionEventType.csInteractionRequirementBase.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.





