凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

uOSC v2 をリリースしました

はじめに

uOSC は、Unity 向けの OSC 実装です。数ある Unity 向け OSC 実装の中でもシンプルな使い方が出来るように目指して作ったものでした。

tips.hecomi.com

github.com

EVMC4U を始めとし、思っていたよりも多くのプロジェクトにて使っていただいており嬉しい限りです。しかしながら長らく動的なアドレスやポート変更といった単純な機能も対応しないままになっていました。。そこで前回のエントリにて UPM 対応のためのメンテをして久しぶりに触ったのもあり、色々足りなかった機能の追加を行うことにしました。本エントリでは新しくなった uOSC v2 の使い方について紹介していきます。

アップデート内容

詳しくは各章で書きますが、v1 からは以下の内容が更新されています。

  • 後方互換性はキープ(v1 用に書いたコードはそのまま動きます)
  • 動的なアドレスやポートの変更に対応
  • 手動でサーバの開始・停止に対応(これまでは enabled の ON/OFF)
  • サーバやクライアントの開始・停止イベントの追加
  • メッセージを処理するリスナのインスペクタからの登録
  • カスタムエディタの追加(デバッグ用に届いたメッセージを簡単にエディタ上で確認など)
  • データ送信間隔やキューのサイズ変更などの変数を追加

インストール方法

執筆時点の最新バージョンは v2.0.3 になります。インストール方法は 3 種類ありますが、2 つめの Scoped Registries の登録がおすすめです。

.unitypackage のダウンロードと展開

従来どおり .unitypackage ファイルをダウンロードしプロジェクトに取り込む方法です。以下のリリースページから最新版をダウンロードしてください。

github.com

Package Manager で Scoped Registry の登録(推奨)

Package Manager から Advanced Project Settings に移動します。

f:id:hecomi:20211129015506p:plain

ここで Scoped Registries に次のようにレジストリを追加してください。

  • Name: hecomi
  • URL: https://registry.npmjs.com
  • Scope: com.hecomi

f:id:hecomi:20211129015644p:plain

すると、Package Manager の My Registries で uOSC が見えるようになります。

f:id:hecomi:20211129015934p:plain

Install を押下してプロジェクトに追加すると、Packages の中にプロジェクトが追加されます。この方法の利点は、以降 uOSC 側に更新が入った際にそれを検知し、Package Manager から簡単にアップデートが出来るようになることです。初期設定の手間は少しかかりますがおすすめです。

f:id:hecomi:20211129020954p:plain

各サンプルは Samples からプロジェクトに取り込むことができます。

f:id:hecomi:20211129022039p:plain

Package Manager で Git URL の登録

Scoped Registries の登録は面倒...、でも .unitypackage で直接プロジェクトに追加も散らかるので嫌だ...、という方は Package Manager で「Add package from git URL...」を選択し、以下の URL を入力すると簡単にパッケージとしてプロジェクトに追加できます。

f:id:hecomi:20211129014953p:plain

ただこの方法ではバージョン更新の検知はできません。

External Tools の設定

Package Manager で追加した場合は、External Tools でこれらのパッケージをプロジェクトに含む設定をしないと補完が効きませんのでご注意ください。

f:id:hecomi:20211129015412p:plain

基本的な使い方

データの受信

サンプルシーンは Basic サンプルの Server シーンになります。データの受信には uOscServer コンポーネントを利用します。

f:id:hecomi:20211129025025p:plain

待ち受けしたいポートを Port に入力し実行すればメッセージ待受用のスレッドが動きます。サーバに届いたメッセージを受け取るには uOscServer.onDataReceived にリスナの登録を行います。次のようなコンポーネントを作成します。

using UnityEngine;

public class ServerTest : MonoBehaviour
{
    void Start()
    {
        var server = GetComponent<uOSC.uOscServer>();
        server.onDataReceived.AddListener(OnDataReceived);
    }

    public void OnDataReceived(uOSC.Message message)
    {
        // address (e.g. /uOSC/hoge)
        var msg = message.address + ": ";

        // arguments (object array)
        foreach (var value in message.values)
        {
            if      (value is int)    msg += (int)value;
            else if (value is float)  msg += (float)value;
            else if (value is string) msg += (string)value;
            else if (value is byte[]) msg += "byte[" + ((byte[])value).Length + "]";
        }

        Debug.Log(msg);
    }
}

これでメッセージが届くたびにログが出力されます。なお、Start() でやっているように自分で GetComponent してこなくても、インスペクタからも登録できます。

f:id:hecomi:20211129025108p:plain

届いたメッセージは最大 100 件まで、Status > Messages の中から確認することができます。以下は TouchOSC からメッセージを送ってみた様子です。

f:id:hecomi:20211129024836g:plain

データの送信

サンプルシーンは Basic サンプルの Client シーンになります。データの受信には uOscClient コンポーネントを利用します。

f:id:hecomi:20211129223737p:plain

データを送信したいアドレス(IPv4 / IPv6)とポートを指定するとメッセージ送信用のスレッドが動きます。その上で次のようにメッセージを送信します。

using UnityEngine;

public class ClientTest : MonoBehaviour
{
    void Update()
    {
        var client = GetComponent<uOSC.uOscClient>();
        client.Send("/uOSC/test", 10, "hoge", 1.234f, new byte[] { 1, 2, 3 });
    }
}

送るメッセージは可変個で、intfloatstringbyte[] のいずれかに対応しています。

基本的には以上になります。

Tips

以下、より詳細な Tips になります。

開始・終了

uOscServer.autoStart が ON の場合、サーバは開始時に自動的にスタートします。もし手動でスタートさせたい場合はこれを OFF にし、手動で uOscServer.StartServer() を呼んでください。止めたい場合は uOscServer.StopServer() です。インスペクタから開始・停止したい場合はコンポーネントチェックボックスを ON / OFF してください。サーバの開始・停止は uOscServer.onServerStarted / uOscServer.onServerStopped にて検知可能です。ポートがかぶっていて開始ができない場合もあるので、その状況は uOscServer.isRunning をチェックして判断してください。

クライアントの場合はあまりユースケースがないかもしれませんが、uOscClient.StartClient() / uOscClient.StopClient() で開始・停止が可能で、開始・停止イベントは uOscClient.onClientStarted / uOscClient.onClientStopped から取得可能です。

動的なアドレスやポートの変更

uOscServer / uOscClientportaddress を変更した場合は、自動的に再接続を行います(タイミングは変更後の Update() タイミングです)。インスペクタ上から変更した場合も同じです。

OSC Bundle

OSC Bundle は複数のメッセージを一つのパケットにまとめることのできる仕組みです。uOSC ではサーバ側では内部で Bundle を展開して単一のアドレス、データ、タイムスタンプのペアとしてリスナに登録したコールバックを呼ぶようにしています。

送信側では以下のようにして Bundle を作成・送信することができます。

using UnityEngine;
using uOSC;

public class ClientBundleTest : MonoBehaviour
{
    void Update()
    {
        var client = GetComponent<uOscClient>();

        // Bundle (root)
        //   - Bundle 1 -> Now
        //     - Message 1-1
        //     - Message 1-2
        //     - Message 1-3
        //   - Bundle 2 -> 10 sec after
        //     - Message 2-1
        //     - Message 2-2
        //     - Message 2-3
        //   - Message 3 -> Immediate

        var bundle1 = new Bundle(Timestamp.Now);
        bundle1.Add(new Message("/uOSC/root/bundle1/message1", 123, "hoge", new byte[] { 1, 2, 3, 4 }));
        bundle1.Add(new Message("/uOSC/root/bundle1/message2", 1.2345f));
        bundle1.Add(new Message("/uOSC/root/bundle1/message3", "abcdefghijklmn"));

        var date2 = System.DateTime.UtcNow.AddSeconds(10);
        var timestamp2 = Timestamp.CreateFromDateTime(date2);
        var bundle2 = new Bundle(timestamp2);
        bundle2.Add(new Message("/uOSC/root/bundle2/message1", 234, "fuga", new byte[] { 2, 3, 4 }));
        bundle2.Add(new Message("/uOSC/root/bundle2/message2", 2.3456f));
        bundle2.Add(new Message("/uOSC/root/bundle2/message3", "opqrstuvwxyz"));

        var root = new Bundle(Timestamp.Immediate);
        root.Add(bundle1);
        root.Add(bundle2);
        root.Add(new Message("/uOSC/root/message3"));

        client.Send(root);

        // uOSC サーバで受け取った場合は OnDataReceived が 7 回呼ばれる
    }
}

タイムスタンプ

OSC Bundle は上述したようにタイムスタンプを持つことができます。サーバ側では次のようにして DateTime として受け取ることができます。

using UnityEngine;
using uOSC;

public class ServerTest : MonoBehaviour
{
    ...

    void OnDataReceived(Message message)
    {
        var dateTime = message.timestamp.ToLocalTime();
    }
}

データ送信用のキューサイズおよび送信間隔

uOscClient.Send() は即時送信はされずに、いったんキューに登録され、バックグラウンドスレッドで取り出しながら送信を行います。バックグラウンドスレッドはおおよそ 1ms 毎に処理されています(Unity の Update 更新間隔は 16 ms)。このキューのデフォルトの最大サイズは 100 となっており、それより大きい数のメッセージを例えば for 文の中で送ろうとすると古いメッセージから消えてしまいます。より多くのデータを送りたい場合は uOscClient.maxQueueSize を変更してください(インスペクタからも変更可能です)。

また、大きな byte[] のデータを同時に大量に送ろうとすると UDP なこともあるからかローカルであろうとロストしてしまうケースが多々あります。この際は uOscClient.dataTransmissionInterval(ミリ秒)を設定してメッセージの送信間隔を空けるようにしてください。詳細は後述します。

送信可能なデータの最大サイズ

送信可能なデータサイズは、65,536 byte から IP ヘッダ 20 byte、UDP ヘッダ 8 byte、OSC ヘッダ(可変長、アドレスの長さなどで変わる)を除いた大きさになります。これ以上大きなデータを送りたい場合は適当なプロトコルを利用したりしながらデータを分割・並び替え・復元する必要があります。私の方で簡単なサンプルである uPacketDivision というものを用意してみました。これについては後述します。

データが受信できない?

ファイアウォールの設定を確認してください。Unity やビルドした exe で許可をし忘れていることはよくあります。

コード例

intstring といったデータを送るのは簡単だと思うので、byte[] を送る参考例を紹介したいと思います。

テクスチャの送信①

まずはテクスチャを PNGエンコードして送信する方法です。PNGエンコードすることでそのままでは 65,000 byte を超えてしまうようなデータも圧縮されてパケットに収まるようになります(大きすぎると超えますが)。

送信側
using UnityEngine;
using uOSC;

public class ClientBlobTest : MonoBehaviour
{
    [SerializeField]
    Texture2D texture;

    byte[] byteTexture_;

    void Start()
    {
        byteTexture_ = texture.EncodeToPNG();
    }

    void Update()
    {
        var client = GetComponent<uOscClient>();
        client.Send("/uOSC/blob", byteTexture_);
    }
}
受信側
using UnityEngine;
using uOSC;

public class ServerBlobTest : MonoBehaviour
{
    Texture2D texture_;

    void Start()
    {
        var server = GetComponent<uOscServer>();
        server.onDataReceived.AddListener(OnDataReceived);

        texture_ = new Texture2D(256, 256, TextureFormat.ARGB32, true);

        var renderer = GetComponent<Renderer>();
        renderer.material.mainTexture = texture_;
    }

    void OnDataReceived(Message message)
    {
        if (message.address == "/uOSC/blob")
        {
            var byteTexture = (byte[])message.values[0];
            texture_.LoadImage(byteTexture);
        }
    }
}

テクスチャの送信②

より大きなデータを送りたい場合はパケット分割をします。簡単な Windows 向けのプラグインとして uPacketDivision というものを作ってみました。

github.com

これは以下の記事のプラグインを整理して分割したものです。

tips.hecomi.com

送信側
using UnityEngine;
using System.Runtime.InteropServices;
using uPacketDivision;
using uOSC;

public class Sender : MonoBehaviour
{
    [SerializeField]
    uint packetSize = 65000;

    [SerializeField]
    uOscClient client;

    [SerializeField]
    int sendFrameInterval = 10;

    Divider _divider = new Divider();
    int _width = 0;
    int _height = 0;

    int count = 0;

    void Update()
    {
        if (count-- > 0) return;

        Divide();
        Send();

        count = sendFrameInterval;
    }

    void Divide()
    {
        var renderer = GetComponent<Renderer>();
        if (!renderer) return;

        var texture = renderer.sharedMaterial.mainTexture as Texture2D;
        if (!texture) return;

        _width = texture.width;
        _height = texture.height;

        var pixels = texture.GetPixels32();
        _divider.maxPacketSize = packetSize;
        _divider.Divide(pixels);
    }

    void Send()
    {
        if (!client) return;

        client.Send("/Size", _width, _height);

        for (uint i = 0; i < _divider.GetChunkCount(); ++i)
        {
            client.Send("/Data", _divider.GetChunk(i));
        }
    }
}
受信側
using UnityEngine;

public class Receiver : MonoBehaviour
{
    [SerializeField]
    uint timeout = 100;

    Assembler _assembler = new Assembler();
    Texture2D _texture;

    void Update()
    {
        _assembler.timeout = timeout;
    }

    public void OnDataReceived(uOSC.Message message)
    {
        if (message.address == "/Size")
        {
            var w = (int)message.values[0];
            var h = (int)message.values[1];
            OnSize(w, h);
        }
        else if (message.address == "/Data")
        {
            var data = (byte[])message.values[0];
            OnData(data);
            CheckEvent();
        }
    }

    void OnSize(int w, int h)
    {
        if (_texture && _texture.width == w && _texture.height == h) return;
        _texture = new Texture2D(w, h);
    }

    void OnData(byte[] data)
    {
        _assembler.Add(data);
    }

    void CheckEvent()
    {
        switch (_assembler.GetEventType())
        {
            case EventType.FrameCompleted:
            {
                OnDataAssembled(_assembler.GetAssembledData<Color32>());
                break;
            }
            case EventType.PacketLoss:
            {
                var type = _assembler.GetLossType();
                Debug.LogWarning("Loss: " + type);
                break;
            }
            default:
            {
                break;
            }
        }
    }

    void OnDataAssembled(Color32[] pixels)
    {
        if (!_texture) return;

        if (pixels.Length != _texture.width * _texture.height) return;

        _texture.SetPixels32(pixels);
        _texture.Apply();
        GetComponent<Renderer>().material.mainTexture = _texture;
    }
}

なお、Tips の章でも書きましたが、dataTransmissionInterval は 2ms 程度に指定しておく必要があります。

こんな感じでより大きいデータを適当に分割・復元して送ることが出来るようになります。ただ UDP なのでパケットロスはしますので、使い場所は選ぶと思います。

おわりに

今回は対応しませんでしたが、v3 は object 利用によるボックス化によるオーバーヘッド回避の対応をしたいと思います。他にも要望などありましたら Twitter などでお気軽に話しかけてください。