Skip to content

Repository files navigation

Unity OSC Control Framework

OSC Control Framework for Unity.

OCF exposes your scripts' fields, properties and methods to OSC control, by reflecting over them at runtime. It is the control layer used by GenUI, which adds a generated user interface on top — but OCF works on its own if you only need OSC.

Requirements

Requirement Notes
Unity 2022.3 or later
com.theoriz.unityosc 1.3.0 or later OCF's OSC transport. Earlier versions still work but declare Unity 2019.4.

The packages declare no UPM dependencies, so nothing installs UnityOSC for you and nothing warns you when it is too old — install it first.

Installation

Add the following line to your project's Packages/manifest.json:

{
  "dependencies": {
    "com.theoriz.unityosc": "https://github.com/Theoriz/UnityOSC.git",
    "com.theoriz.ocf": "https://github.com/Theoriz/OCF.git"
  }
}

Or in the Unity Editor, go to Window > Package Manager > + > Add package from git URL and enter:

https://github.com/Theoriz/UnityOSC.git

then

https://github.com/Theoriz/OCF.git

Exposing members

Mark the members you want to control with [OCFExposed]:

public class MyScript : MonoBehaviour
{
    [OCFExposed] public float speed = 1f;
    [OCFExposed, Range(0f, 1f)] public float amount = 0.5f;
    [OCFExposed(readOnly = true)] public string status = "idle";
    [OCFExposed] public LightMode mode = LightMode.Spot;

    public List<string> palettes = new List<string> { "warm", "cool" };
    [OCFExposed(targetList = "palettes")] public string palette = "warm";

    [OCFExposed]
    public void Randomize() { /* ... */ }
}

[OCFExposed] takes two options: readOnly, and targetList for a member chosen from a list — see Exposing a list. An enum needs neither, since its type already names its members (Exposing an enum).

Generating the Controllable

Generate it either way round:

  • From the component, three-dots menu ▸ Add Controllable. It offers to generate the script, and once compilation finishes it adds the component and wires it up for you — nothing else to do.
  • From the Project window, right-click the script ▸ Assets ▸ OCF ▸ Generate Controllable Script. This only writes the script.

Later, when you add or rename an exposed member, regenerate the mirror: right-click the component ▸ Update Controllable, or right-click the mirror script in the Project window ▸ Assets ▸ OCF ▸ Update Controllable Script.

Renaming an exposed member leaves the mirror calling one that no longer exists, so the project stops compiling until you regenerate. Update Controllable handles that case too — it takes two domain reloads and reports both steps in the Console. Values set on the Controllable component in the Inspector do not survive it.

Theoriz ▸ OCF ▸ Update All Controllables does the same for every Controllable in the project at once — useful after updating OCF itself, when a release changes what the generator emits. It tells you how many scripts it is about to rewrite before touching anything, and leaves scripts that belong to a package alone.

How it works: the two-object mirror

The generator emits a mirror class next to your script, in the same namespace:

public class MyScriptControllable : Controllable
{
    [OCFProperty]
    public float speed;

    [Range(0f, 1f)]
    [OCFProperty]
    public float amount;

    [OCFProperty(readOnly = true)]
    public string status;

    [OCFMethod]
    public void Randomize()
    {
        (controllableTargetScript as MyScript).Randomize();
    }
}

[Header], [Range] and [Tooltip] are carried over from your script and honoured by the UI. Members keep the order your script declares them in, with methods last.

The mirror re-declares each exposed member with [OCFProperty] (fields) or [OCFMethod] (methods), and Controllable binds the two by name at Awake.

Important

The names must match exactly. A mismatch fails silently — the member simply is not controllable.

Values flow both ways: incoming OSC and UI edits are written through to your script, and your script's own changes are picked up every frame.

You can also write a mirror by hand instead of generating it, which is what the extra [OCFProperty] options below need.

[OCFProperty] options

Option Type Default Effect
readOnly bool false Value is displayed but cannot be edited, and is left out of presets — nothing can write it back.
showInUI bool true Set false to control the member over OSC only, with no widget.
includeInPresets bool true Set false to leave the member out of saved presets.
targetList string Name of a List<string> whose entries this member is chosen from; renders a dropdown. See Exposing a list.
selfBound bool false The member has no counterpart on your script — the mirror carries the value itself, or forwards it somewhere of its own. See Binding a member yourself.

readOnly and targetList are also reachable from the automatic workflow — write [OCFExposed(readOnly = true)] or [OCFExposed(targetList = "myList")] and the generator forwards them. The others have no [OCFExposed] equivalent, so a member that needs one must be declared in a hand-written mirror.

Binding a member yourself

A mirror follows one script. To expose a member that lives elsewhere — on a second component, say — mark it selfBound and do both directions yourself: write the value out in OnUiValueChanged, which every edit goes through, and report a change back from PollTargetScript.

[OCFProperty(selfBound = true)] public int volume;

public override void OnUiValueChanged(string name)
{
    if (name == nameof(volume)) { mixer.volume = volume; return; }
    base.OnUiValueChanged(name);
}

protected override void PollTargetScript()
{
    base.PollTargetScript();
    if (volume != mixer.volume) { volume = mixer.volume; RaiseEventValueChanged(nameof(volume)); }
}

RaiseEventValueChanged, not RaiseScriptValueChanged: the latter re-reads the member from your target script, where a self-bound one does not exist.

Reserved names

A generated Controllable inherits from Controllable, so an [OCFExposed] member that reuses one of Controllable's member names will shadow the real one and break it. Every member Controllable declares carries a controllable prefix, lower case on fields and events, capitalised on methods:

  • Controllable state: controllableId, controllableDebug, controllableFolder, controllableTargetDirectory, controllableSourceScene, controllableUsePresets, controllableCurrentPreset, controllablePresetList, controllableTargetScript
  • Preset methods: ControllableSave, ControllableSaveAs, ControllableLoad, ControllableShow, ControllableLoadWithName
  • Events: controllableUiValueChanged, controllableValueChanged, controllableScriptValueChanged
  • From Unity: name, tag, transform, gameObject, enabled

Controllable is a MonoBehaviour, so every public member of MonoBehaviour is reserved too (Invoke, StartCoroutine, GetComponent, …) — name is the one that bites in practice.

You do not have to check by hand: the generator refuses to emit a colliding member and logs an error naming it. Just rename your member.

OSC control

Every exposed member gets an address:

/OCF/{id}/{property}    set a value
/OCF/{id}/{method}      invoke a method

{id} defaults to the target script's type name, and can be overridden with the controllableId field on the Controllable. Messages that do not match a registered controllable are ignored.

Methods with parameters are reachable over OSC (their arguments map to the message arguments) but get no UI widget.

To consume your own OSC messages — anything not addressed to /OCF/ — subscribe to the receiver directly:

using UnityOSC;

OSCMaster.Receivers["myReceiver"].messageReceived += (OSCMessage m) => Debug.Log(m.Address);

Presets

Controllable can save and restore the state of its [OCFProperty] members to a file. Members marked readOnly or includeInPresets = false are left out. The generated panel exposes Save, Save As, Load and Show buttons plus a preset dropdown, and the same methods are reachable over OSC.

The ControllableMaster panel carries the global buttons instead: Save All, Save As All, Load All and Open Presets Folder. The last one reveals the presets root in your file browser, and is also a button on the ControllableMaster component in the Inspector so you can reach the folder without entering Play mode.

/OCF/ControllableMaster/ControllableOpenPresetsFolder

Note

Show reveals a single preset file and does nothing while no preset is selected. Open Presets Folder always opens the folder.

Selecting a preset loads it. Setting controllableCurrentPreset — from the dropdown or over OSC (/OCF/{id}/controllableCurrentPreset "myPreset.pst") — loads that preset immediately. Load reloads the current preset, and Load All does it for every controllable.

To load a specific file, use the ControllableLoadWithName method, giving it the case-sensitive file name:

/OCF/{id}/ControllableLoadWithName "myPreset.pst"

The last-used preset is remembered across runs and reloaded on enable. The selection is stored beside the .pst files as _lastUsedPreset.txt.

Where presets are stored

Each Controllable gets its own folder under a shared root:

<root>/<folder or scene name>/<controllable id>/myPreset.pst

folder is the Controllable's own field; when it is empty the scene name is used instead.

The root is picked once per run, first match winning:

# Source Set where
1 -presetsPath "<absolute path>" Command line, e.g. MyApp.exe -presetsPath "D:/Shows/Venue A/Presets"
2 customPresetDirectory Inspector, on ControllableMaster
3 <application folder>/Presets/ Default
3 <Documents>/<product name>/Presets/ Default, when useDocumentsDirectory is ticked on ControllableMaster

Paths must be absolute. A relative path, or a folder that cannot be created or written to, is reported with one error in the Console and the default is used instead.

Note

On Android neither override applies: presets always live under Application.persistentDataPath, the only writable location.

Exposing a list

To pick a value from a list of strings, keep the List<string> on your own script and point a string member at it by name:

public class MyScript : MonoBehaviour
{
    public List<string> options = new List<string> { "red", "green", "blue" };

    [OCFExposed(targetList = "options")]
    public string selected = "red";
}

Generate the Controllable as usual. The dropdown writes the selected entry into selected, and the list is read live, so entries added at runtime appear the next time the dropdown refreshes.

The list may also live on the mirror, which is what a hand-written one does — targetList is looked up on the mirror first and on your script second:

public class MyScriptControllable : Controllable
{
    public List<string> options = new List<string> { "red", "green", "blue" };

    [OCFProperty(targetList = "options")]
    public string selected;
}

Exposing an enum

Declare the field with its real enum type and mark it [OCFExposed] — nothing else is needed:

public enum LightMode { None = 0, Spot = 5, Wash = 12 }

[OCFExposed] public LightMode mode = LightMode.Spot;

The generated mirror declares the same enum type and the panel renders a dropdown of its members.

Over OSC the member can be set either way:

/OCF/MyScript/mode "Wash"   ← member name, case-insensitive
/OCF/MyScript/mode 12       ← the member's declared value

A value naming no member logs a warning listing the valid names and leaves the member alone. Presets store the member name.

Note

A [Flags] enum has no widget: one dropdown cannot represent a combination of members. It is controllable over OSC — by combined value, or by the comma-separated form "Red, Blue" — and saved in presets.

About

Automatic OSC Control Framework for Unity

Topics

Resources

Stars

9 stars

Watchers

5 watching

Forks

Releases

Packages

Contributors

Languages