Skip to content

Scripting API Examples

Octoputs is designed to be configured in the Inspector — most workflows do not need code. This page exists for the cases where you do want to reach into the C# API: reacting to attachment events from a controller script, driving an attach/detach programmatically, or adding your own filter or movement modifier as a new picker entry.

Every example uses the current public API as of this writing. Stale names from older docs (DraggableObject, DragZone, DraggableTransfer) were renamed during the April 2026 refactor to AttachableObject, AttachmentPoint, and AttachingAgent.

What's NOT covered here

Building Octoputs setups at runtime (e.g. new GameObject + AddComponent<AttachableObject> from code). Process and command trees are serialized at edit time — wire them in the Inspector or on a prefab, then load the prefab. The runtime API on this page is for interacting with already-configured components, not constructing them.


Hooking attachment events

Both AttachableObject and AttachmentPoint expose UnityEvent hooks for each phase. The point's events carry the involved attachable as a payload; the attachable's events are parameterless because the attachment point is already known via CurrentAttachmentPoint.

using Octoputs.Core;
using UnityEngine;

public class CoinCounter : MonoBehaviour
{
    [SerializeField] private AttachmentPoint coinPouch;
    private int coinCount;

    private void OnEnable()
    {
        // AttachmentPoint events pass the AttachableObject that triggered them.
        coinPouch.OnAttached.AddListener(OnCoinDeposited);
        coinPouch.OnDetached.AddListener(OnCoinWithdrawn);
    }

    private void OnDisable()
    {
        coinPouch.OnAttached.RemoveListener(OnCoinDeposited);
        coinPouch.OnDetached.RemoveListener(OnCoinWithdrawn);
    }

    private void OnCoinDeposited(AttachableObject coin)
    {
        coinCount++;
        Debug.Log($"+1 coin ({coin.name}). Total: {coinCount}");
    }

    private void OnCoinWithdrawn(AttachableObject coin)
    {
        coinCount--;
        Debug.Log($"-1 coin ({coin.name}). Total: {coinCount}");
    }
}

Available events:

Source Event Payload Fires when
AttachableObject OnAttachStarted none Object begins attaching to a point
AttachableObject OnAttached none Attach process completes
AttachableObject OnDetachStarted none Detach begins
AttachableObject OnDetached none Detach process completes
AttachmentPoint OnAttachStarted AttachableObject An object begins attaching here
AttachmentPoint OnAttached AttachableObject An object finishes attaching here
AttachmentPoint OnDetachStarted AttachableObject An attached object begins detaching
AttachmentPoint OnDetached AttachableObject An attached object finishes detaching

The same four events sit in the AttachmentPoint Inspector, one or two per tab. Listeners wired there and listeners added from C# both fire.

Inspector
Attachment Point? ⇅ ⋮
Attaching
Attached
Detaching
Attach Conditions0+
None
Attach CancellationRevert
Attaching ProcessProcess List
On Attach Started (AttachableObject)
List is Empty
+−
Attaching tab: On Attach Started.
Inspector
Attachment Point? ⇅ ⋮
Attaching
Attached
Detaching
Detach Conditions0+
None
Allow Instant Steal
Detach CancellationRevert
Detaching ProcessProcess List
On Detach Started (AttachableObject)
List is Empty
+−
On Detached (AttachableObject)
List is Empty
+−
Detaching tab: On Detach Started and On Detached. On Attached sits in the Attached tab.

Querying current state

Both components expose a CurrentPhase enum (Idle / Attaching / Attached / Detaching) and direct accessors for the other party.

using Octoputs.Core;
using UnityEngine;

public class AttachmentReadout : MonoBehaviour
{
    [SerializeField] private AttachableObject coin;
    [SerializeField] private AttachmentPoint coinPouch;

    private void Update()
    {
        if (coin.IsAttached)
            Debug.Log($"{coin.name} is in {coin.CurrentAttachmentPoint.name} (phase: {coin.CurrentPhase})");

        if (coinPouch.IsHolding)
            Debug.Log($"{coinPouch.name} holds {coinPouch.AttachedObject.name} (phase: {coinPouch.CurrentPhase})");
    }
}

Useful properties:

Component Member Purpose
AttachableObject IsAttached True when in Attached or Detaching phase
AttachableObject CurrentAttachmentPoint The point this object is bound to (null if idle)
AttachableObject CurrentPhase Idle/Attaching/Attached/Detaching
AttachableObject CanBeAttached() / CanBeDetached() Evaluates the attach/detach conditions wired in the Inspector
AttachmentPoint IsHolding True when an object is bound here (any phase except Idle)
AttachmentPoint AttachedObject The currently-bound attachable (null if idle)
AttachmentPoint CurrentPhase Same enum, point-side
AttachmentPoint CanAttach(attachable, out reason) True if the point is free AND filters/conditions pass; reason describes the first failure
AttachmentPoint CanAcceptAttachable(attachable, out reason) Like CanAttach but ignores whether the point is currently free — useful for previewing eligibility
AttachmentPoint CanDetach() Evaluates the detach conditions wired in the Inspector

Programmatic attach and detach

Drive attaches and detaches directly from code by calling into AttachmentPoint. The point owns the bidirectional state, so calling its API also drives the bound AttachableObject.

using Octoputs.Core;
using UnityEngine;

public class PuzzleSolution : MonoBehaviour
{
    [SerializeField] private AttachableObject key;
    [SerializeField] private AttachmentPoint lockSlot;

    // Call when the player solves the puzzle — runs the full attaching state
    // process (animations, sounds, transform pipeline), then invokes onComplete.
    public void SnapKeyHome()
    {
        if (lockSlot.CanAttach(key, out var reason))
            lockSlot.Attach(key, onComplete: () => Debug.Log("Key seated."));
        else
            Debug.LogWarning($"Cannot attach key: {reason}");
    }

    // Skip the attaching state process — teleport-in, no animation.
    public void SnapKeyHomeInstant()
    {
        if (lockSlot.CanAttach(key, out _))
            lockSlot.InstantAttach(key);
    }

    // Detach the current key (returns false if nothing is bound).
    public void EjectKey()
    {
        if (!lockSlot.TryDetach())
            Debug.LogWarning("Lock slot is empty — nothing to detach.");
    }
}

The AttachableObject mirrors most of this surface so you can drive from the attachable side if that fits your controller better:

Method What it does
point.Attach(attachable, onComplete = null) Runs the attaching process; calls back when the phase reaches Attached
point.InstantAttach(attachable) Same effect, skips the attaching state process
point.Detach(onComplete = null) Runs the detaching process; calls back when phase returns to Idle
point.InstantDetach() Force-completes any in-progress attach/detach to Idle immediately
point.TryDetach() Detaches only if CanDetach() passes — returns true if a detach started
point.CancelAttach() / point.CancelDetach() Pauses or reverts the in-progress phase (driven by the point's CancellationStrategy setting)
attachable.StartAttach(point, onComplete = null) Attachable-side initiator — usually invoked indirectly via point.Attach(...)
attachable.StartDetach(onComplete = null) Attachable-side detach; useful when you don't have a reference to the holding point
attachable.InstantDetach() Force-completes any active attach/detach

Driving an AttachingAgent from code

AttachingAgent does not expose public Grab() / Drop() methods — it listens to its Grab Event and Drop Event fields (Jungle IEvent references). To trigger an agent from code, fire the same event it listens to: hold a reference to that IEvent (or its concrete type — EventAsset, etc.) and invoke its raise method. From the Inspector's perspective the agent doesn't care whether the event was raised by player input or by your script.


Writing a custom attachable filter

Filters in Octoputs are leaf predicates — small [Serializable] types that get registered with the polymorphic picker and show up alongside the built-in checks. The contract for an attachable-side filter is IAttachableObjectCheck, a single-method interface:

public interface IAttachableObjectCheck
{
    bool IsValid(AttachableObject attachable);
}

Mark the implementation [Serializable] (so it survives Unity's SerializeReference) and tag it with [JungleClassInfo] so the picker shows it under a sensible heading.

using System;
using Jungle.Attributes;
using Octoputs.Core;
using UnityEngine;

[Serializable]
[JungleClassInfo("Min Mass",
    "Passes when the attachable's Rigidbody mass is above a threshold.",
    null, "Attachment/Filtering")]
[TypeHelp("Accepts only attachables whose Rigidbody is at least this heavy. " +
          "Use to keep light objects out of a heavy-only slot.")]
public class MinMassCheck : IAttachableObjectCheck
{
    [SerializeField]
    [Tooltip("Reject objects with a Rigidbody mass below this value (kg).")]
    private float minMass = 1f;

    public bool IsValid(AttachableObject attachable)
    {
        if (!attachable) return false;
        var body = attachable.GetComponent<Rigidbody>();
        if (!body) return false;
        return body.mass >= minMass;
    }
}

Compile the project — the check appears in the popup picker on any AttachmentPoint filter list under Attachment / Filtering / Min Mass.

Filtering
Attachable Filters0+
None
Search
Filtering
Eligibility
Filter
Held / Free
Min Mass
On Attached Object
Same Tag As Point
Your check sits next to the built-in ones under Attachment → Filtering.

The point-side counterpart is IAttachmentPointCheck (with bool IsValid(AttachmentPoint point)) and is wired the same way — it appears on the attachable's pointFilter list and on the agent's dropFilters list.

Cross-domain filtering

To filter by a generic GameObject property (tag, layer, custom component), use the existing GameObjectFilterAttachableCheck adapter — it wraps any Jungle.Primitives.IGameObjectFilter. You don't need a custom IAttachableObjectCheck for those cases. See Tag-Based Filtering.


Writing a custom transform modifier

Modifiers sit in the middle stage of an AttachmentPoint's transform pipeline — between the target (where the object wants to go) and the applier (how the transform actually moves). Each one transforms a TransformData (Position + Rotation + Scale).

Extend TransformModifierBase and override Modify(targetValue, currentValue). The base class already exposes a BlendFactor SmartFloat that the pipeline applies for you — return the full effect from Modify; the pipeline lerps to it.

using System;
using Jungle.Attributes;
using Octoputs.Transforming;
using UnityEngine;

[Serializable]
[JungleClassInfo("Pulse Scale",
    "Sinusoidally scales the target while attached.",
    null, "Effects")]
[TypeHelp("Multiplies the target scale by 1 + amplitude * sin(2π * frequency * t). " +
          "Used to make held objects breathe or pulse while in a slot.")]
public class PulseScaleModifier : TransformModifierBase
{
    [SerializeField] [Tooltip("Peak scale offset (0.1 = ±10%).")]
    private float amplitude = 0.1f;

    [SerializeField] [Tooltip("Cycles per second.")]
    private float frequency = 1f;

    private float startTime;

    public override void Initialize()
    {
        startTime = Time.time;
    }

    public override TransformData Modify(TransformData targetValue, TransformData currentValue)
    {
        float phase = (Time.time - startTime) * frequency * Mathf.PI * 2f;
        float k = 1f + amplitude * Mathf.Sin(phase);
        return new TransformData(targetValue.Position, targetValue.Rotation, targetValue.Scale * k);
    }
}

Drop the modifier into any AttachmentPoint's attaching / attached / detaching process. The pipeline runs its Initialize on phase entry and dispatches Modify each frame. The same modifier works on every shape of target — direct, raycast hit, blended, curve — because it operates purely on the TransformData stream.

Inspector
Attachment Point? ⇅ ⋮
Attaching
Attached
Detaching
Attached Process1Transform Command
Operations1+
Operation 12Set Transform over Time (Direct)
Destination
Modifiers
Modifiers1+
Modifier 13Pulse Scale
Blend Factor1
Amplitude0.1
Frequency1
Options
On Attached (AttachableObject)
List is Empty
+−
Pulse Scale running on whatever the point holds.
  1. Attached tab: a Transform Command as the Attached Process.
  2. Its operation, the Direct transform pipeline.
  3. Your modifier in the pipeline's Modifiers list, with its own fields below Blend Factor.

Vector3-only modifiers

For position-only modifiers (no rotation/scale), extend Vector3ValueModifier and override Vector3 Modify(Vector3 input). Same picker, narrower contract. See the built-in Vector3AddModifier, Vector3ScaleModifier, Vector3NegateModifier, Vector3NormalizeModifier and Vector3UniformScaleModifier in Jungle.Spatial for templates.


Subscribing to cycles for Reactor wiring

Every Octoputs lifecycle host (AttachableObject, AttachmentPoint, AttachingAgent) implements ICycleProvider<T> and ITargetedCycleProvider<T> from Jungle.Core. These are the seams the Reactor uses for its cycle reactions (GameObjectCycleReaction for the per-target ones, BoolCycleReaction for the plain begin/end) — but you can call them directly from any component when a Reactor isn't enough.

using Octoputs.Core;
using UnityEngine;

public class LockSlotIndicator : MonoBehaviour
{
    [SerializeField] private AttachmentPoint slot;
    [SerializeField] private Renderer indicatorRenderer;

    private void OnEnable()
    {
        // AttachmentCycle.Holding fires begin() on Attached, end() on DetachStarted.
        slot.SubscribeCycle(AttachmentCycle.Holding, OnHoldBegin, OnHoldEnd);
    }

    private void OnDisable()
    {
        slot.UnsubscribeCycle(AttachmentCycle.Holding, OnHoldBegin, OnHoldEnd);
    }

    private void OnHoldBegin() => indicatorRenderer.material.color = Color.green;
    private void OnHoldEnd()   => indicatorRenderer.material.color = Color.white;
}

Cycle kinds:

Provider Kinds
AttachableObject (AttachableCycle) Attaching, Attached, Detaching, Occupancy
AttachmentPoint (AttachmentCycle) Attaching, Holding, Detaching, Occupancy
AttachingAgent (AgentCycle) Grabbing, Holding, Dropping, etc. — see the AgentCycle enum

Occupancy fires begin on the first non-idle phase and end on return to idle — the simplest "is something happening here?" hook.

The targeted variants (SubscribeTargetedCycle) pass the partner GameObject (the attachable on the point side, the point on the attachable side) into the callbacks so a single subscriber can route by target.


Next steps