Skip to content

SimpleNetworkは、VRChatの複雑なネットワークをシンプルにするNetworkingラッパーなUdonSharpモジュールです。

Notifications You must be signed in to change notification settings

tutinoco/SimpleNetwork

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

30 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SimpleNetwork

SimpleNetworkMonster SimpleNetworkは、VRChatのNetworkingがしんどい方のためのNetworkingラッパーなUdonSharpモジュールです。 SendCustomNetworkEventメソッドで、引数を扱えない問題を解消する目的で作成されました。

SimpleNetworkは、SimpleNetworkUdonBehaviorの進化版です。

特徴

  • SendCustomNetworkEventで不可能な引数の送信ができ、メッセージングプログラミングを可能にします。
  • 煩わしい所有権から解放され、権限の無いオブジェクトからも低レイテンシーで高速な通信が行えます。
  • オブジェクトにグループ名を設定して、複数のオブジェクトにイベントをイテレーション送信できます。
  • 大量にイベントを発行しても、自動的に全てのイベントを1回の送信にまとめるため、安定して動作します。
  • AllOwnerのほかに、Masterやプレイヤーを直接設定して、イベント受信者を限定することができます。
  • Masterにイベント送信を依頼するなど、他のプレイヤーにイベントの送信を依頼することができます。
  • 最後に実行したイベントやイベント履歴をサーバに保存し、後から参加したプレイヤーに送信することができます。
  • SendCustomNetworkEventDelayedFramesに相当する、ネットワークイベントの遅延送信が可能です。
  • シーンに存在しない動的に生成したオブジェクトに対しても通信が可能です。

使い方

SimpleNetwork.prefabをシーンに配置し、適当なクラスを作成してSimpleNetworkBehaviourを継承すると、SendEvent()メソッドで、全てのプレイヤー(自分自身を含む)のオブジェクトに値を送信することができます。 イベントを受信するには、サブクラスでReceiveEventメソッドをオーバーライドします。第一引数にはイベント名が、第二引数には値が届きます。

using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;
using tutinoco; // 追加して

public class Test : SimpleNetworkBehaviour // 継承して
{
    void Start()
    {
        SimpleNetworkInit(); // 初期化して
        SendEvent("Talk", "こんにちは!"); // イベントを送ると
    }

    public override void ReceiveEvent(string name, string value)
    {
        if( name == "Talk" ) {
            Debug.Log(value); // こんにちは!が全ユーザに届きます。
        }
    }
}

上記のコードでは、オブジェクトが自らのイベントを送信していますが、特に嬉しいのは、値を送信できるようになったことで、命令されたら動くアクションだけをまとめたクラスを簡単に作成できるようになることです。 以下は、命令を待って動くモンスタークラスの例です。

using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;
using tutinoco;

public class Monster : SimpleNetworkBehaviour
{
    [SerializeField] private Rigidbody rigidbody;
    [SerializeField] private Text fukidashi;

    void Start()
    {
        SimpleNetworkInit();
    }

    public override void ReceiveEvent(string name) // 第二引数valueを省略しても
    {
        // 【上にジャンプ】
        if( name == "jump" ) {
            float power = GetFloat(); // floatで受け取れます。
            rigidbody.AddForce(transform.up*power, ForceMode.Impulse);
        }

        // 【座標にワープ】
        if( name == "warp" ) {
            Vector3 pos = GetVector3(); // Vector3で受け取れます。
            gameObject.transform.position = pos;
        }

        // 【ふきだしに文字表示】
        if( name == "talk" ) {
            fukidashi.text = GetString(); // stringで受け取れます。
        }
    }
}

このモンスタークラスを使って作られたモンスターを制御するには、適当な箇所に以下のコードを記述します。

// ジャンプ力5.0で飛ぶ!
monster.SendEvent("jump", 5.0f);

// x:1 y:2 z:3 の座標にワープ!
monster.SendEvent("warp", new Vector3(1.0f, 2.0f, 3.0f));

// ふきだしに文字を表示!
monster.SendEvent("talk", "僕を捕まえられるかな?");

様々な型の送受信

bool char byte sbyte short ushort int uint long ulong float double Vector2 Vector3 Vector4 Quaternion string VRCUrl Color Color32に加え Object[] SimpleNetworkBehaviourの送受信に対応しています。

現在、Object[]以外の配列の送受信は非対応です。

public override void ReceiveEvent(string name)
{
    // SendEvent("hoge", true);
    bool boolValue = GetBool();

    // SendEvent("hoge", 5);
    int intValue = GetInt();

    // SendEvent("hoge", 3.14f);
    float floatValue = GetFloat();

    // SendEvent("hoge", "こんにちは");
    string stringValue = GetString();

    // SendEvent("hoge", new Vector3(1.0f, 2.0f, 3.0f));
    Vector3 vector3Value = GetVector3();
}

また、値の無いイベントの送信も可能です。

monster.SendEvent("Init");
monster.SendEvent("Init", none);

// monster.SendEvent("Init", null); // 型指定の無いnullは非対応です ><

イベントの送信

イベントのローカル実行

// 自分のみイベントを実行
ExecEvent("Jump", 5.0f);

イベントの遅延送信

// 約2秒の120フレーム後にイベント送信
ExecEvent("Jump", 5.0f, 120); // 自分のみ
SendEvent("Jump", 5.0f, 120); // 全員

送信先の指定

// 全員にイベントを送信(デフォルト)
SendEvent(SendTo.All, "Jump", 5.0f);

// オブジェクト所有者にイベントを送信
SendEvent(SendTo.Owner, "Jump", 5.0f);

// インスタンスマスターにイベントを送信
SendEvent(SendTo.Master, "Jump", 5.0f);

// 自分自身にイベントを送信
SendEvent(SendTo.Self, "Jump", 5.0f); // ExecEventに同じ
SendEvent(SendTo.Me, "Jump", 5.0f); // RequestEvent()ではSelfとMeは挙動が異なる

// オブジェクト所有者以外にイベントを送信
SendEvent(SendTo.NotOwner, "Jump", 5.0f);

// インスタンスマスター以外にイベントを送信
SendEvent(SendTo.NotMaster, "Jump", 5.0f);

// 自分以外にイベントを送信
SendEvent(SendTo.NotSelf, "Jump", 5.0f);

// 指定のプレイヤーにイベントを送信
SendEvent(player, "Jump", 5.0f);

もちろん以下のように送信先を指定した上でイベントの遅延送信を行うこともできます。

// インスタンスマスターに2秒遅延してイベントを送信
SendEvent(SendTo.Master, "Jump", 5.0f, 120);

イベント送信の依頼

RequestEvent()メソッドを使うことで、イベントの送信を他のプレイヤーに依頼することができます。

// インスタンスマスターから全員にイベントを送信するよう依頼
RequestEvent(RequestTo.Master, "Jump", 5.0f);

// インスタンスマスターからオブジェクトオーナーにイベントを送信するよう依頼
RequestEvent(RequestTo.Master, SendTo.Owner, "Jump", 5.0f);

// プレイヤー1からプレイヤー2にイベントを送信するよう依頼
VRCPlayerApi player1 = VRCPlayerApi.GetPlayerById(1);
VRCPlayerApi player2 = VRCPlayerApi.GetPlayerById(2);
RequestEvent(player1, player2, "Jump", 5.0f);

// オブジェクトオーナー側で60フレーム待機してから自分にイベントを送信するよう依頼
RequestEvent(RequestTo.Owner, SendTo.Me, "Jump", 5.0f, 60);

複数の値を送信する

複数の値を送るには、Pack()メソッドを利用して値をまとめます。

Vector3 position = Vector3(1.0f, 2.0f, 3.0f);
Quaternion rotation = Quaternion.identity;

monster.SendEvent("warp2", Pack(position, rotation));

Mainクラスの役割を果たしているクラスでもSimpleNetworkBehaviourを継承することをお勧めしますが、 万が一呼び出し元がSimpleNetworkBehaviourのサブクラスでない等、Packメソッドを利用できない場合には、以下のようにObject[]を利用します。

object[] values = new object[] {position, rotation}; // object[]に格納して
monster.SendEvent("warp2", values); // 送信する。

複数の値を受信するには、以下のように値の引数番号を指定します。

public override void ReceiveEvent(string name)
{
    Vector3 position = GetVector3(0);       // 0番目の値をVector3で取得
    Quaternion rotation = GetQuaternion(1); // 1番目の値をQuaternionで取得

    // 引数番号を省略することもできます。
    Vector3 position2 = GetVector3();       // 最初に見つかったVector3を取得
    Quaternion rotation2 = GetQuaternion(); // 最初に見つかったQuaternionを取得
}

なお、対応していない型の値をPack()に含めるとはできません。

様々な使い方

パラメータの順序

ExecEvent(名前,, グループ指定, 遅延);
SendEvent(送信先, 名前,, グループ指定, 遅延, 参加同期);
RequestEvent(依頼先, 送信先, 名前,, グループ指定, 遅延, 参加同期);
CreateEvent(依頼先, 送信先, 名前,, グループ指定, 遅延, 参加同期);

各イベント送信メソッドのパラメータの設定方法は上記の通りで、名前以外は省略することができます。 設定可能な型は以下の通りです。

  • 依頼先:RequestTo型、または依頼先プレイヤーをVRCPlayerApi型で指定します。
  • 送信先:SendTo型、または送信先プレイヤーをVRCPlayerApi型で指定します。
  • 名前:string型で実行するイベント名を指定します。
  • 値:送信する値を設定します。対応している型は、色々な型の送受信を参照してください。
  • グループ指定:string型でグループ名を指定してイベントを受信するオブジェクトを選択します。
  • 遅延:int型でフレーム数を設定し、イベントの送信を遅らせます。 [](* 参加同期:JoinSync型で、ワールドに参加したユーザに同期するイベントを設定します。)

2つの受信方法

SimpleNetworkではイベントを受信する方法に「ダイレクトレシーブ」と「インディレクトレシーブ」が存在します。

ダイレクトレシーブを使うとGetFloat()などの値取得用のメソッドを利用せず、直接値を受け取ることができます。 以下のように様々なReceiveEvent()メソッドをオーバーライドすることで、型に応じて受信用メソッドを分けることができます。

// float型のデータを受信
public override void ReceiveEvent(string name, float value) { ... }

// Vector3型のデータを受信
public override void ReceiveEvent(string name, Vector3 value) { ... }

// Packでまとめられた複数の値を受信
public override void ReceiveEvent(string name, Object[] values) { ... }

おすすめの受信方法は、すべてのイベントと値を柔軟に受信できる、インディレクトレシーブです。

// 全てのイベントを受信
public override void ReceiveEvent(string name) { ... } // 第二引数を省略する

値の受信方法はGetFloat()などの受信メソッドを利用することですが、該当する型のデータが届かなかった場合にエラーが発生するため注意が必要です。 また、GetValues()メソッドを利用することで受信したすべての値を受け取ることもできます。

メタデータの受信

送信された値以外にも、付随するデータを取得することができます。

public override void ReceiveEvent(string name)
{
    // 送信元オブジェクトを取得します
    SimpleNetworkBehaviour behavior = GetSource();

    // 送信元プレイヤーIDを取得します
    int player = GetSender();

    // このイベントを受信したプレイヤーIDの一覧を取得します
    int[] players = GetRecipients();

    // このイベントのグループターゲット文字列を取得します
    string target = GetTarget();

    // グループでイテレートした際のインデックスを取得します
    int index = GetIndex();

    // このイベントがどのくらい遅延して送られたかを取得します
    int delay = GetDelay();

    // 受信したすべての値を受け取ります。
    Object[] values = GetValues();

    // 指定した型が受信した値の何番目にあるか取得。無ければ-1が返ります。
    int argIndex = IndexOf(typeof(int));
}

制御可能なオブジェクトの複製

通常、Instantiateを使って複製したオブジェクトはUdonでは制御できませんが SimpleNetworkでは、制御可能なオブジェクトを複製する機能が備わっています。

オブジェクトを複製するにはDuplicate()メソッドを実行します。

// monsterをx:0 y:0 z:0に複製します
Duplicate(monster, new Vector3(0,0,0) /*,Quaternion.identity*/); // Quaternionは省略可能

複製されたオブジェクトを取得するには、OnDuplicateComplete()メソッドをオーバーライドします。

public override void OnDuplicateComplete( SimpleNetworkBehaviour behaviour )
{
    Monster newMonster = (Monster)behaviour;
    newMonster.SendEvent("Jump", 5.0f); // 複製したオブジェクトにイベントを送信
}

グループ名を設定する

SimpleNetworkInit()メソッドの第一引数にグループ名を設定すると、イベントのグループ指定送信を行った際にマッチしたグループにイベントを送信することができます。

例えば、下記のようにMonsterクラスのStart()メソッドでSimpleNetworkBehaviourを初期化する際に"Monster"というグループ名を設定します。

Monster.cs

using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;
using tutinoco;

public class Monster : SimpleNetworkBehaviour
{
    private VRCPlayerApi target;

    void Start()
    {
        SimpleNetworkInit("Monster"); // グループ名を設定して初期化
    }

    public override void ReceiveEvent(string name)
    {
        // 【モンスターの初期化】
        if( name == "Init" ) {
            float distance = GetFloat();
            float x = Random.Range(-0.5*distance, 0.5*distance);
            float z = Random.Range(-0.5*distance, -0.5*distance);
            Vector3 pos =  new Vector3(x, 0f, z);
            gameObject.transform.position = pos;
        }

        // 【プレイヤーを見る】
        if( name == "See" ) {
            int playerId = GetInt();
            VRCPlayerApi target = VRCPlayerApi.GetPlayerById(playerId);
            gameObject.transform.LookAt(target.transform);
        }
    }

}

全てのMonsterに同じイベントを送信...つまり、全てのMonsterに一斉に見つめられるようにするにはMainクラスを以下のようにします。

Main.cs

using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;
using tutinoco;

public class Main : SimpleNetworkBehaviour // Mainクラスでも継承する
{
    // [SerializeField] private Monsters[] monsters; // ←これはもういらない
    private VRCPlayerApi target;

    void Start()
    {
        SimpleNetworkInit();

        // Monsterを10m四方にランダム配置して初期化する
        if( IsMaster() ) SendEvent("Init", 10.0f "Monster"); // ← for文を利用せず全てのMonsterを初期化
    }

    void Update()
    {
        // 全てのモンスターがtargetを常に見る
        if( IsMaster() && target != null ) SendEvent("See", target, "Monster");
    }

    public override void OnPlayerJoined(VRCPlayerApi player)
    {
        target = player.playerId;
    }

}

冒頭ではモンスターにイベントを送信する際、monster.SendEvent("Jump", 5.0f);といった形でイベントを送信していました。 しかし、グループ名を利用することで、メインクラスは、モンスターを所持したり、複数のモンスターをコレクションする必要は無くなり、 グループ名を指定するだけで、全てのMonsterにイベントを送信することができるようになりました。

また、GetIndex()メソッドを利用することで、イテレーションのインデックスを取得することができるため、 処理された順番によって異なる処理を施すこともできます。

public override void ReceiveEvent(string name)
{
    // 【モンスターの初期化】
    if( name == "Init" ) {

        // 複数のMonsterを一列に並べて配置
        float x = (float)GetIndex();
        Vector3 pos = new Vector3(x, 0f, 0f); // インデックスをXに代入
        gameObject.transform.position = pos;
    }

    ...
}

また、GetBehaviours()メソッドを利用することで、イテレーションによく使う対象オブジェクトの数を確認することもできます。

int monsterLength = GetBehaviours("Monster").Length; // モンスター数を確認

// ワイルドカードを使用することもできます。
SimpleNetworkBehaviour[] enemys = GetBehaviours("Mon*"); // Monから始まるグループ名のオブジェクトを全て取得

ワイルドカード

ワイルドカードは、イベントのグループ指定送信を行うときにも利用することもできます。 以下のように、グループ名にマッチしたオブジェクトにイベントを送信することができます。

// MonsterだけでなくMoisterなどにもイベントを送信する
SendEvent("Initialize", none, "Mo?ster");

// MonsterだけでなくRocksterやPopsterにもイベントを送信する
SendEvent("Initialize", none, "*ster");

// MonsterだけでなくMondeyやMonochromeなどにもイベントを送信する
SendEvent("Initialize", none, "Mon*");

また、OR|やAND&を使った選択も可能です。

// MonsterとAlienとInvaderにイベントを送信する
SendEvent("Initialize", none, "Monster|Alien|Invader");

// Mから始まりabcを含むグループ名のオブジェクトにイベントを送信する
SendEvent("Initialize", none, "M*&*abc*");

// Mから始まりabcを含むグループ名または
// Aから始まりdefを含むグループ名のオブジェクトにイベントを送信する
SendEvent("Initialize", none, "M*&*abc*|A*&*def*");

グループ名の変更

初期化時に設定したグループ名は、後から変更することもできます。

SetGroupName("Monster-TypeA-001");

SetGroupNameによるグループ名の変更は、ローカルのみで実行されるため、安易に利用すると他ユーザのオブジェクトがイベントを受け取れなくなる可能性あるため利用には注意が必要です。

イベントの作成

イベントを送信せず、前もってイベントオブジェクトを作成しておくことができます。

// インスタンスマスターに2秒遅延して実行させるイベントを作成
object[] evObj = CreateEvent(RequestTo.Master, SendTo.Self, "Jump", 7.0f, "Monster", 120);

// イベントオブジェクトを使ってイベントを送信
RequestEvent(evObj);

// こうするとRequestToやSendToは無視して実行されます
SendEvent(evObj); // RequestToを無視して送信
ExecEvent(evObj); // RequestToとSendToを無視して実行

参加同期

SimpleNetworkでは、最後に実行したイベントやイベント履歴をサーバに保存して、後から参加したプレイヤーに送信することができます。 サーバに値を保存するには次のようにします。

// 後から参加したプレイヤーにもSetPositionイベントを送信するよう設定
object[] evObj = SendEvent("SetPosition", pos);
SetJoinEvent(evObj);

// 後から参加したプレイヤーにDrawLineイベント履歴を連続的に送信するよう設定
object[] evObj = SendEvent("DrawLine", pos);
LoggedJoinEvent(evObj);

// CreateEvent()メソッドを使うことで
// イベントを送信せずに参加同期イベント登録のみ行うことも可能です
object[] evObj = CreateEvent("SetPosition", pos);
SetJoinEvent(evObj);

グループと併用することも可能です

// 全てのドミノをtype1でリセットしたことを後から参加したプレイヤーに送信する形でリセットする
int type = 1;
SetJoinEvent(SendEvent("Reset", type, "Domino"));

サーバに保存されている値を削除するにはClearJoinEvent()メソッドを実行します

ClearJoinEvent(); // このオブジェクトに保存された全てのイベント履歴を削除
ClearJoinEvent("Monster|Alien"); // MonsterとAlienに保存された全てのイベント履歴を削除
ClearJoinEvent(monster); // 指定したオブジェクトに保存された全てのイベント履歴を削除

複雑なイベント送信

依頼先指定、送信先指定、グループ指定、遅延、参加同期など、すべての機能を使うと以下のような挙動になります。

// int型の1とfloat型の2.0fが設定されたHogeHogeイベントをマスターから送信するように依頼。
// マスターはイベントの送信を120フレーム遅延して実行し
// オーナー権限を持つ、Mから始まりabcを含むグループ名のオブジェクトにイベントを届けた上で
// サーバにイベントを保存し、後から参加したプレイヤーにも届ける。
RequestEvent(RequestTo.Master, SendTo.Owner, "HogeHoge", Pack(1, 2.0f), "M*&*abc*", 120, JoinSync.Latest);

デバッグ

SimpleNetworkが送受信したデータの内容を確認するにはデバッグモードを有効にします。

// Debug.Logに送信されたコマンド等が送られる
SimpleNetwork.DebugMode(true);

仕組み

structure

80人までの同時接続が可能なSimpleNetworkProxy(以下、プロキシ)がSimpleNetwork.prefabに用意されており、プロキシには各型のUdonSynced同期変数の配列が用意されています。

プレイヤーがワールドに参加すると、利用可能なプロキシが自動的に割り当てられるため、全てのプレイヤーは専用プロキシを所有します。

SimpleNetworkは、シーンに存在する全てのSimpleNetworkBehaviorから発行されたイベントを収集しており、SimpleNetworkBehaviorを継承したオブジェクトでSendEvent()メソッドが実行されると、イベント情報がSimpleNetworkに送信され、収集した複数のイベントの名前や値などの情報は1フレーム毎にひとつ同期にまとめられます。

同期する値は、専用プロキシの各型の同期変数に投げられ、これにより全てのプレイヤーに値が同期され、同期された値はSimpleNetworkがリアルタイムに受信します。

プロキシは、OnDeserialization()によってコードの受信を検知しており、SimpleNetworkは、受け取った値をイベントオブジェクトにまとめて配列を作成し、各イベントをReceiveEvent()に送信します。このとき、イベントのターゲットに含まれないプレイヤーは、受信したイベントを無視するよう設定されています。

実行が許されたイベントは、適切なオブジェクトのReceiveEvent()メソッドが呼ばれ、SimpleNetworkBehaviorに値を保存、GetInt()などのメソッドにより値にアクセスすことで、引数付きのネットワークイベントの発行が実現されています。

また、プロキシのUdonBehaviourSyncModeManualに設定されており、同期変数は高速に同期されるので、SimpleNetworkSendEvent()は、SendCustomNetworkEvent()よりも高速に送受信できます。

注意事項

  • イベント名に__DUPLICATE____SETGROUPNAME__を利用することはできません。
  • グループ名に? * | &を利用することはできません。利用した場合は自動的に取り除かれます。
  • SimpleNetworkInitを利用したグループ名を初期化時設定できる機能はローカルで設定されるため、動的なグループ名の設定は予期せぬ動作につながる可能性があります。

About

SimpleNetworkは、VRChatの複雑なネットワークをシンプルにするNetworkingラッパーなUdonSharpモジュールです。

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages