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;
    [OCFExposed(includeInPresets = false)] public int seed = 0;

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

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

[OCFExposed] takes readOnly, includeInPresets and targetList, all described under [OCFProperty] options below — the generator forwards them to the mirror. An enum needs none of them, since its type already names its members (Exposing an enum). None of them apply to a method, which has no value.

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, for a renderer to honour. 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: values set on the mirror — over OSC, from a preset, or by a renderer — 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 selfBound below needs.

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, so a renderer can offer its members to choose from.

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 is not offered for editing: a single choice among the members cannot represent a combination of them. It is controllable over OSC — by combined value, or by the comma-separated form "Red, Blue" — and saved in presets.

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 list is read live, so entries added at runtime are picked up.

targetList is a constraint: a value that is not one of the entries is refused with a warning, whether it arrives over OSC, from a preset or from a renderer. Your own script can still assign the member directly — a value stored that way is reported as it is, not silently swapped for an entry.

Use it for a set only known at runtime. When the set is fixed, an enum is the better fit: it is typed, it needs no list beside it, and it is checked the same way (Exposing an enum).

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;
}

If the entries come from outside the process — files on disk, a device that may have gone away — override RefreshTargetList(string listName) on the mirror to re-read them. A renderer calls it just before it shows the entries, so what the user picks from is current. That is how the preset list keeps up with .pst files added or deleted while the app runs.

[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.
includeInPresets bool true Set false to leave the member out of saved presets.
targetList string Name of a List<string> this member's value must be one of. 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.

All of these except selfBound are reachable from the automatic workflow: write the same option on [OCFExposed] and the generator forwards it. selfBound cannot be, since it means the member has no counterpart on your script — there is nowhere to put the attribute, so it belongs to 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 OnExternalValueChanged, which every edit goes through, and report a change back from PollTargetScript.

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

public override void OnExternalValueChanged(string name)
{
    if (name == nameof(volume)) { mixer.volume = volume; return; }
    base.OnExternalValueChanged(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: controllableExternalValueChanged, 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

Methods with parameters are reachable over OSC (their arguments map to the message arguments).

Controllable Id

{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.

Ids must be unique. Two Controllables sharing one id would share an OSC address and a preset folder, so the second to register is refused with an error: it keeps mirroring its target script, but is left unregistered — no OSC address, no presets, and nothing to show — until you give it another id. The Controllable inspector flags the clash before you press Play.

A class can also claim an id for itself, so nothing else can take it:

[OCFReservedId("MyPanel")]
public class MyPanelControllable : Controllable { }

An instance of that class (or of a subclass) takes the id automatically when controllableId is left empty; any other Controllable asking for it is refused.

The input port

ControllableMaster listens on port 6001. If another application already holds it, it retries on the next port up:

Field Default Effect
Incremental Connect on If the input port is busy, increment it and retry. Uncheck to fail on 6001 instead.
Max Connect Attempts 60 How many ports to try before giving up with a warning in the Console.

The port actually opened is printed in the Console at start, and advertised over Zeroconf/Bonjour, so send your OSC there rather than assuming 6001. The read-only Status fields below show whether a port is open and the machine's IP address.

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. Each Controllable exposes ControllableSave, ControllableSaveAs, ControllableLoad and ControllableShow as [OCFMethod]s, plus the controllablePresetList and controllableCurrentPreset members, so presets are driveable over OSC and offered by any renderer.

ControllableMaster carries the global equivalents instead: ControllableSaveAll, ControllableSaveAsAll, ControllableLoadAll and ControllableOpenPresetsFolder. 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

ControllableShow reveals a single preset file and does nothing while no preset is selected. ControllableOpenPresetsFolder always opens the folder.

Selecting a preset loads it. Setting controllableCurrentPreset, over OSC (/OCF/{id}/controllableCurrentPreset "myPreset.pst") or from anywhere else, loads that preset immediately. ControllableLoad reloads the current preset, and ControllableLoadAll 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.

Preset files can be added, renamed or deleted while the app runs: the list is re-read whenever a renderer is about to show it, and loading a preset whose file has gone warns and refreshes the list rather than failing.

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.

About

Automatic OSC Control Framework for Unity

Topics

Resources

Stars

9 stars

Watchers

5 watching

Forks

Releases

Packages

Contributors

Languages