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.
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:
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.
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.
- Attached tab: a Transform Command as the Attached Process.
- Its operation, the Direct transform pipeline.
- 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¶
- Build a complete drag-and-drop scene without code: Octoputs Quick Start · Your First Drag and Drop
- Reactive logic in pure Inspector wiring: Jungle Reactor tutorial
- All built-in modifiers, targets, and appliers: Transform Operations
- Every public type in detail: API Reference