Overview

World objects in Loop Adventure express their capabilities through interfaces. Each interface adds a distinct interaction type, and a single object can implement several at once.

InterfaceWhat it enablesTriggered by
IInteractableCustom interaction — open chests, collect items, trigger doorsInstruction_Interact or automatic collection on turn end
ICarryObjectPlayer can pick up and drop the objectInstruction_Carry and Instruction_Drop
IPushablePlayer can push the object one tile in a directionInstruction_Push
IObstacleObject blocks player movement and push validationChecked by IPushable.Validate() and movement instructions
💡
Prerequisite Your class must extend ExecutionContextElement to participate in the execution lifecycle. The engine clones scene objects at runtime — without this base class, the object won't be included in execution.

1 — Implement IInteractable

Use IInteractable when you want the player to trigger a one-shot action: collect an item, open a container, activate a mechanism. If automatic is true, the interaction fires at the end of any turn the player occupies the same tile — no instruction needed.

  1. Create a new script that extends ExecutionContextElement and implements IInteractable.
  2. Set automatic: return true for pickups that collect on touch; return false for chests or mechanisms that require an explicit Interact instruction.
  3. Implement Validate(): return true if the interaction is currently legal (e.g. chest has an item). Return false to silently block — add a NotificationCenter.Notify() call before returning to give the player feedback.
  4. Implement Interact(): put your behaviour here — add inventory items, play animations, destroy the object, fire events.
  5. Declare OnInteract with a [field: SerializeField] attribute so it is wirable in the Inspector.
using UnityEngine;
using UnityEngine.Events;

namespace LoopAdventure
{
    public class InteractableObject_Sign : ExecutionContextElement, IInteractable
    {
        [SerializeField] string message;
        [field: SerializeField] public UnityEvent OnInteract { get; set; }

        // false = requires Interact instruction; true = collected automatically
        public bool automatic => false;

        public bool Validate() => !string.IsNullOrEmpty(message);

        public void Interact()
        {
            NotificationCenter.Notify(NotificationCenter.MessageType.Message, message);
            OnInteract?.Invoke();
        }
    }
}

2 — Add ICarryObject Support

Add ICarryObject to let the player pick up the object with Instruction_Carry and drop it with Instruction_Drop. The engine parents the object to the player during carry and clears the carry state on drop.

  1. Add ICarryObject to the class declaration.
  2. Implement Take(): disable the collider so the carried object doesn't interfere with movement; play a pickup sound.
  3. Implement Drop(): re-enable the collider and restore the object's normal state.
  4. The engine handles parenting and unparenting automatically — you do not need to call transform.SetParent().
public class InteractableObject_Pot : ExecutionContextElement, ICarryObject
{
    [SerializeField] AudioSource audioSource;
    [SerializeField] AudioClip pickupSound, dropSound;

    // ICarryObject requires Transform — MonoBehaviour provides it automatically
    Transform ICarryObject.transform => transform;

    public void Take()
    {
        GetComponent<Collider2D>().enabled = false;
        audioSource.PlayOneShot(pickupSound);
    }

    public void Drop()
    {
        GetComponent<Collider2D>().enabled = true;
        audioSource.PlayOneShot(dropSound);
    }
}

3 — Add IPushable Support

Add IPushable to let the player move the object one tile with Instruction_Push. The interface includes a default Validate() implementation that checks for IObstacle components in the target cell — you usually don't need to override it.

  1. Add IPushable to the class declaration.
  2. Implement Push(context, direction): move the object to the adjacent tile. Use MapManager.cellSize for the offset and match the movement style of the built-in objects (lerp or instant).
  3. Implement Stop(): stop looping sounds or animations when the push completes.
  4. If you also implement IObstacle, the default Validate() will detect your own collider as a blocker — make sure the object's collider is on a layer that doesn't trigger its own IObstacle check, or override Validate().
Transform IPushable.transform => transform;

public void Push(ExecutionContext context, Direction direction)
{
    Vector2 target = transform.position.ToVector2()
                    + direction.AsVector2() * MapManager.cellSize;
    transform.position = new Vector3(target.x, target.y, transform.position.z);
    audioSource.PlayOneShot(pushSound);
}

public void Stop() { } // stop looping sounds here if any

4 — Add IObstacle Support

IObstacle tells the engine that this object physically blocks movement. IPushable.Validate() uses it to decide whether a push is legal. Player movement instructions also check for it.

  1. Add IObstacle to the class declaration.
  2. Implement Validate(): return true while the object should block (e.g. a closed door); return false when it should not (e.g. after a door opens). For static solid objects, always return true.
// Solid object — always blocks
public bool Validate() => true;

// Togglable obstacle — door or gate
bool isOpen;
public bool Validate() => !isOpen;

5 — Combining Interfaces

Interfaces are designed to be combined freely. The built-in objects demonstrate the most common combinations:

ClassIInteractableICarryObjectIPushableIObstacle
InteractableObject_Pickup✓ (automatic)
InteractableObject_Chest✓ (manual)
InteractableObject_Carry
InteractableObject_PushableObject
InteractableObject_Door✓ (togglable)

6 — Prefab Setup

  1. Create an empty GameObject. Name it to match your class (e.g. Interactable_Sign).
  2. Add a BoxCollider2D — size it to one grid cell (1 × 1 units). This is what the engine detects with Physics2D.OverlapBox.
  3. Add your script component. Configure all Inspector fields.
  4. Add a SpriteRenderer and an Animator if the object has visual feedback.
  5. Add an AudioSource if the object plays sounds.
  6. Save as a prefab under Assets/Prefabs/Interactables/.
  7. Place the prefab in your level scene aligned to the tile grid.
💡
Grid alignment Objects must be snapped to the tile grid. Place them at integer world positions that match MapManager.cellSize (default 1.0). Use the scene snapping tool or set position manually in the Inspector.