0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

UnityでOSC通信 | ExtremeOsc

Last updated at Posted at 2025-02-23

ドキュメント

はじめに

あるプロジェクトでUnityを使ってOSC通信をすることになり、すでにいくつか公開されているOSCのライブラリを使ってました。

しかし、アドレスの種類や引数の数が増えていくにしたがって、ソースコードが長くなってしまったり、ひたすらボイラープレート的なコードを書かなければならなかったりと大変な思いをしました…。

例えば、こんな感じのOSCがあるとしたら

/test/address ,iiifffiiffif

こんな感じのコードをひたすら書きます。(仮想のライブラリです)

OscServer server;

public void Initialize()
{
    server = new OscServer(12345);
    server.AddCallback("/test/address", OnMessageTestAddress);
    // アドレスが増えたら追加していく
}

private void OnMessageTestAddress(string address, OscArguments arguments)
{
    int a = arguments.GetInt(0);
    int b = arguments.GetInt(1);
    int c = arguments.GetInt(2);
    // これをひたすら続ける
}

OSCの仕様上、上記のようになってしまうのは仕方のないことです。一方で、C#を使っているのだから値をまとめたクラス/構造体で扱えないものか…と思ったので、自分でつくってみました!

ExtremeOsc

ExtremeOscは、Unity向けのOSC (Open Sound Control)のC#実装です。SourceGeneratorによって、これまで書かざるを得なかった大量のコードを自動生成します。これによって、次のような恩恵が受けられます。

  • クラスまたは構造体に対して、自動的にOSC信号へ変換するメソッドを追加
  • クラスの関数にOSCのアドレスを指定すると、自動的にOSC信号に対してコールバックを実行

サポートしている型

OpenSoundControl Specification 1.0にある基本的な型はサポートしています!

Tag C# Type
i int
h long
f float
s string
S Symbol
b byte[]
d double
c char
r UnityEngine.Color32
t TimeTag (DateTime)
T bool (true)
F bool (false)
N Nil
I Infinitum
m MIDI as int

使い方

インストール方法

丁寧にテストをしたつもりですが、まだ自信がないのでプレビュー版としています。ReleasesページからUnity Packageをダウンロードしてインポートするか、リポジトリをクローンしてください。

OSCの送信

  1. OSCで送信したいデータのクラス(構造体)を作成して[OscPackable]アトリビュートをつけてください。クラスの条件は次の通りです。

    • Partialなクラス(構造体)である。
    • Root(クラスの入れ子ではない)である。
  2. 次にOSCで送信したいプロパティまたはフィールドに[OscElementAt(int)]アトリビュートをつけてください。必ず連番にします。

    // 自動生成されるコードのために、必ずpartialにします。
    [OscPackable]
    public partial class ExampleData
    {
        // OscElementAtの引数は必ず連番にします。
        [OscElementAt(0)]
        public int IntValue { get; set; }
        [OscElementAt(1)]
        public float FloatValue { get; set; }
        [OscElementAt(2)]
        public string StringValue { get; set; }
    
        public ExampleData()
        {
            IntValue = 0;
            FloatValue = 0.0f;
            StringValue = string.Empty;
        }
    }
    

    上記のようなコードを書くと、次のようなコードが自動的に生成されます。

    partial class ExampleData : IOscPackable
    {
        // OSCのタグ -> ,ifs
        public static readonly byte[] TagTypes = new byte[] { 44, 105, 102, 115 };
    
        // 送信するbyte[]に書き込む
        public void Pack (byte[] buffer, ref int offset)
        {
            int offsetTagTypes = offset + 1;
            OscWriter.WriteString(buffer, TagTypes, ref offset);
            OscWriter.WriteInt32(buffer, IntValue, ref offset);
            offsetTagTypes++;
            OscWriter.WriteFloat(buffer, FloatValue, ref offset);
            offsetTagTypes++;
            OscWriter.WriteStringUtf8(buffer, StringValue, ref offset);
            offsetTagTypes++;
        }
    
        // 受信したbyte[]から読み出す
        public void Unpack (byte[] buffer, ref int offset)
        {
            int offsetTagTypes = offset + 1;
            OscReader.ReadString(buffer, ref offset);
            this.IntValue = OscReader.ReadInt32(buffer, ref offset);
            offsetTagTypes++;
            this.FloatValue = OscReader.ReadFloat(buffer, ref offset);
            offsetTagTypes++;
            this.StringValue = OscReader.ReadString(buffer, ref offset);
            offsetTagTypes++;
        }
    }
    
  3. 最後に、信号を送信するOscClientでインスタンスを送信します。いくつか例を示します。

    public class ExampleClient : MonoBehaviour
    {
        private OscClient client = null;
    
        private void Awake()
        {
            client = new OscClient("127.0.0.1", 5555);
        }
    
        private void Update()
        {
            if(Input.GetKeyDown(KeyCode.Space))
            {
                var data = new ExampleData
                {
                    IntValue = Random.Range(0, 100),
                    FloatValue = Random.Range(0.0f, 1.0f),
                    StringValue = "Hello, World!"
                };
    
                // Packableなクラスの送信
                client.Send("/example", data);
            }
    
            if (Input.GetKeyDown(KeyCode.Return))
            {
                var data = new ExampleData
                {
                    IntValue = Random.Range(0, 100),
                    FloatValue = Random.Range(0.0f, 1.0f),
                    StringValue = "Hello, World!"
                };
    
                // 後述 : Packableなクラスと同じ順番で並んでいる引数に対して
                client.Send("/example/arguments", data);
            }
    
            if(Input.GetKeyDown(KeyCode.LeftShift))
            {
                // 引数なし
                client.Send("/example/noargument");
            }
    
            if(Input.GetKeyDown(KeyCode.RightShift))
            {
                var data = new object[]
                {
                    Random.Range(0, 100),
                    Random.Range(0.0f, 1.0f),
                    "Hello, World!"
                };
    
                // 型の順番が合っていればobject[]もOK。
                // ただし、無駄なアロケーションあり
                client.Send("/example/arguments", data);
            }
        }
    
        private void OnDestroy()
        {
            client?.Dispose();
            client = null;
        }
    }
    

OSCの受信

  1. OSCを受信してコールバックするクラス(構造体)に[OscReceiver]アトリビュートをつけてください。このクラスも必ずPartialにします。

  2. OSCを受信するためのOscServerも作成しましょう。コールバックを実行するクラスのインスタンスを登録しましょう。

    // 1. 必ずPartial
    [OscReceiver]
    public partial class ExampleServer : MonoBehaviour
    {
        private OscServer server = null;
    
        private void Awake()
        {
            server = new OscServer(5555);
            // 2. このクラスのインスタンスでコールバックしたいのでRegister
            server.Register(this);
            server.Open();
        }
    
        private void OnDestroy()
        {
            server.Unregister(this);
            server?.Dispose();
            server = null;
        }
    }
    
  3. コールバックする関数を定義し、[OscCallback(address)]アトリビュートをつけてください。必ず最初の引数はstring addressにしてください。

    // 2.のコードの続き
    [OscReceiver]
    public partial class ExampleServer : MonoBehaviour
    {
        [OscCallback("/example/noargument")]
        private void OnExampleNoArgument(string address)
        {
            // 引数なし
        }
        
        [OscCallback("/example")]
        private void OnExample(string address, ExampleData data)
        {
            // Packableなクラス
        }
        
        [OscCallback("/example/arguments")]
        private void OnExampleArguments(string address, int intValue, float floatValue, string stringValue)
        {
            // 値ごとに引数に分けてもOK
        }
    
        [OscCallback("/example/argument/ref")]
        private void OnExampleArgumentRef(string address, ref ExampleData data)
        {
            // ref, inをつけると作成済みの値を使いまわします。
        }
    }
    

    上記のようなコードを書くと、次のようなコードが自動生成されます。アドレスを判別してUnpackする処理を出力しただけです。

    public void ReceiveOscPacket (byte[] buffer)
    {
        int offset = 0;
        string address = OscReader.ReadString(buffer, ref offset);
        int offsetTagTypes = offset + 1;
        switch (address)
        {
            case "/example/noargument":
            {
                OnExampleNoArgument(address);
                break;
            }
            case "/example":
            {
                var __value = new ExtremeOsc.Example.ExampleData();
                __value.Unpack(buffer, ref offset);
                OnExample(address, __value);
                break;
            }
            case "/example/arguments":
            {
                OscReader.ReadString(buffer, ref offset);
                int intValue = OscReader.ReadInt32(buffer, ref offset);
                offsetTagTypes++;
                float floatValue = OscReader.ReadFloat(buffer, ref offset);
                offsetTagTypes++;
                string stringValue = OscReader.ReadString(buffer, ref offset);
                offsetTagTypes++;
                OnExampleArguments(address, intValue, floatValue, stringValue);
                break;
            }
        }
    }
    

長々と書いてしまいましたが、要約しますと

  1. OscPackableなクラスを作る!送る!
  2. OscReceiverなクラスを作る!
  3. OscCallbackな関数を作る!引数はPackableなクラスに合わせる!

となります。ヨシ!書き込み、読み出し部分は一切書かなくてヨシ!

もちろん、送受信先がUnityじゃなくても動作します。

スクリーンショット 2025-02-23 182446.png

何が嬉しいのか

なんといっても送受信に関わるほとんどのコードが自動生成されるので、「何を送るか、どこで受信するか」を考えることに集中できます。

また、プロジェクトの要件・仕様は日々揺れ動くもので、「この引数を追加・削除したい!」ということは多いと思います。引数の順番や型をクラス(構造体)として扱うことで、「クラスを変更するだけ」でよいのです。これで仕様変更し放題!ヨシ!

一応、何も考えずにobject[]として送るようにもできますが、配列の余計なアロケーションなどが走るのであまりおすすめできません。上記のような恩恵も受けられませんしね。

今後の課題

複数のPackableなクラス(構造体)を引数に取りたい。

多ければ多いほどよいわけでもないので悩ましいですが、値や機能ごとに分割するようなこともあるので一応サポートはしたいと思っています。

UnityEngineの型を送受信したい。

これ完全に忘れてました…。現状は自分で定義した型のみをサポートしているので、どうにかしてUnityEngine.Vector3とかをサポートしたい…。たぶん、hadashiA/VYamlのリポジトリが参考になるのではと思っております。

Issueやプルリク大歓迎!

せっかくここまで作ったので、使ってフィードバック・プルリクください!お待ちしております!

参考

OSCについて

SourceGeneratorについて

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?