ドキュメント
はじめに
あるプロジェクトで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の送信
-
OSCで送信したいデータのクラス(構造体)を作成して
[OscPackable]
アトリビュートをつけてください。クラスの条件は次の通りです。- Partialなクラス(構造体)である。
- Root(クラスの入れ子ではない)である。
-
次に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++; } }
-
最後に、信号を送信する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の受信
-
OSCを受信してコールバックするクラス(構造体)に
[OscReceiver]
アトリビュートをつけてください。このクラスも必ずPartialにします。 -
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; } }
-
コールバックする関数を定義し、
[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; } } }
長々と書いてしまいましたが、要約しますと
- OscPackableなクラスを作る!送る!
- OscReceiverなクラスを作る!
- OscCallbackな関数を作る!引数はPackableなクラスに合わせる!
となります。ヨシ!書き込み、読み出し部分は一切書かなくてヨシ!
もちろん、送受信先がUnityじゃなくても動作します。
何が嬉しいのか
なんといっても送受信に関わるほとんどのコードが自動生成されるので、「何を送るか、どこで受信するか」を考えることに集中できます。
また、プロジェクトの要件・仕様は日々揺れ動くもので、「この引数を追加・削除したい!」ということは多いと思います。引数の順番や型をクラス(構造体)として扱うことで、「クラスを変更するだけ」でよいのです。これで仕様変更し放題!ヨシ!
一応、何も考えずにobject[]
として送るようにもできますが、配列の余計なアロケーションなどが走るのであまりおすすめできません。上記のような恩恵も受けられませんしね。
今後の課題
複数のPackableなクラス(構造体)を引数に取りたい。
多ければ多いほどよいわけでもないので悩ましいですが、値や機能ごとに分割するようなこともあるので一応サポートはしたいと思っています。
UnityEngineの型を送受信したい。
これ完全に忘れてました…。現状は自分で定義した型のみをサポートしているので、どうにかしてUnityEngine.Vector3
とかをサポートしたい…。たぶん、hadashiA/VYamlのリポジトリが参考になるのではと思っております。
Issueやプルリク大歓迎!
せっかくここまで作ったので、使ってフィードバック・プルリクください!お待ちしております!
参考
OSCについて
SourceGeneratorについて