一つのサーバーに対いて、複数のクライアントが接続して、ゲームを進行させる場合、サーバーでは、複数のクライアント情報が必要になる。
そして、一つのクライアントでは相手と通信する機能が必要となる。
以上より、一つのクライアントの通信を管理するクラスとして、TcpCommunicationを作りました。この中に通信用のTcpClientオブジェクトを内包しています。
そして、サーバ側の管理クラスとして、TcpAppServerを作りました。
このクラスではTCP接続を管理するTcpListenerを持ち、
接続許可で得られるTcpClientオブジェクトを内包するTcpCommunicationを生成し、
その接続相手の集合を、List<TcpCommunication> clientlistで管理しています。
それぞれのクラスは、TcpCommunication.csとTcpAppServer.csのファイルに定義しています。
このページでは、
この使い方とその簡単な作品例(ネット共有のホワイト・ボード)を
紹介しています。
(このソース群を含めたUnityパッケージもダウンロードできます。)
上記の例は、サーバー用の作品とクライアント用作品が別々になっている2つの作品例です。
別途に、サーバーまたはクライアントを起動時に選択して始める、サーバーとクライアント機能をまとめた1つの作品例を
こちらのページで紹介しています。
(これは、ドラックによるカメラの回転を、全てのクライアントで行えて、その画面を共有する作品です。)
TcpAppServer.onTcpServerStart += (server) => {// サーバ起動時の処理 // tcpCommunication のstatic メンバtarget_IP, tcp_portが示すサーバーに接続 tcpCommunication.ConnectTo(); };個別のクライアントの作品で、TcpAppServerの接続ボタンを利用して接続の場合は、Startメソッドなどで次のように実行させます。
TcpCommunication.tcpClientButton += () => {// TcpAppServerの接続用GUIボタンで実行 tcpCommunication.ConnectTo(); };この接続先は、TcpCommunication.target_IP,TcpCommunication.tcp_port です。
tcpCommunication.onConnect += ()=>{ panel.SetActive(false);// サーバに接続したら背景のパネルを消去 TcpAppServer.onGUI_flag = false;//onGUI用操作画面の表示を抑止(デバックの場合はtrueに設定) TcpAppServer.onGUI_flagClient = false;//onGUIクライアントの接続部表示のみ消す TcpAppServer.onGUI_flagServer = false;//onGUIサーバ起動部表示のみ消す };
TcpAppServer.onReceive += (TcpCommunication com, BinPacket msg) => { // commのクライアントからのの受信処理(この例では、msgの文字列を受信) };
BinPacket binPacket = null; for (;;) { // 受信情報の取り出し BinPacket recPacket = QueueBuffer.GetBuff(tcpCommunication.queueBuffer); if (recPacket == null) break; // recPacketの受信データを使った何かしらの処理・・・ }
変数 | List<TcpCommunication> clientlist = new List<TcpCommunication>() |
接続を許可したクライアントのリスト。これで、このリスト要素(接続中の相手)を指定して、BinPacketを送信できます。 |
変数 | Dictionary<string, TcpCommunication> clientmap = new Dictionary<string, TcpCommunication>(); |
接続を許可したクライアントを"IPアドレス:ポート番号"のキーで記憶するマップ |
変数 | TcpListener tcpListener = null; | サーバー用オブジェクト(接続待機) |
変数 | bool onGUI_flag = true; | このOnGUIの接続用GUIを利用しない場合にfalse |
static 変数 | string onGUI_message = ""; | 接続時の進行を表示する文字列で、onGUI_flagをfalseにすると、表示されなくなる。 自前のプロフラムのデバックなどで、自由に設定使用できるが、進行の表示で勝手に上書されるため、 TcpAppServer.onGUI_message +="希望の文字列";のように連結処理で表示させるよい。 |
static 変数 | bool onGUI_flagClient = true; | クライアント接続用のボタンなどの表示制御 |
static 変数 | bool onGUI_flagServer = true; | サーバ接続用のボタンなどの表示制御 |
static 変数 | float onGUI_startY = 20; | OnGUI部品を並べ始める縦の基準座標 他に、onGUI_width = 400;OnGUI()で使う表示幅変更、onGUI_stepYのOnGUI部品を並べ始める縦の基準Y座標、 onGUI_height、onGUI_TextFieldFontSize、など多数あります。 |
メソッド | StartServer() | onGUIのサーバ起動用ボタン操作で呼び出されるので、それを利用する場合は呼び出す必要がない。 自前で呼び出すと、(ipAdrs, TcpCommunication.tcp_port)のバインドで、クライアント接続待ちスレッドが起動する。 |
static メソッド |
TcpAppServer.onTcpServerStart += (TcpAppServer server) =>{ } |
サーバ起動イベントの処理:StartServer()のクライアント接続待ちスレッド内で、クライアントが接続して許可した時に実行させる記述を書く。
例えば、TcpAppServer.onGUI_flagServer = false;などonGUI用操作表示部の制御などに使える。 また、サーバークライアント兼用作品の場合は、自身のクライアントの接続処理(tcpCommunication.ConnectTo()など)を書く |
static メソッド |
TcpAppServer.onAcceptTcp += (TcpCommunication com) =>{ } |
クライアントが接続すると、自動的に接続を許可して、そのcomのクライアントの受信スレッドが始まるが、そに接続を許可で呼び出される。 (作品によって使わなくてもよい。接続直後にクライアントにデータを送るプロトコルにする場合などは、ここに、例えば次のように書く) com.SendMessage(msg); msgは文字列またはBinPacketのバイナリを指定できます。 |
static メソッド |
TcpAppServer.onReceive += (TcpCommunication com, BinPacket messaeg) =>{ } |
サーバーの受信処理。クライアントの受信スレッドが自動的に動作して、この中でcomのクライアントの受信すると、このデリゲートが呼び出される。
利用者は、このmessaeg受信パケットに対応プログラムを、ここに書くことになる。例えば、comに送信する場合は、次のようになる。 com.SendMessage(msg); msgは文字列またはBinPacketのバイナリを指定できます。 |
メソッド | ResetTcpServer() | サーバーを停止。これを実行して停止した後に再びサーバー起動する場合は、StartServer()を実行するとよい。 |
static 変数 | string target_IP = "" | 接続先IPアドレスで、OnGUIの接続用GUIを使うとオペレータの入力文字列が自動的にセットされる。 また何も指定しない場合、自身のローカルアドレスがTcpAppServerで自動的に設定される。 |
static 変数 | int tcp_port = 3000; | 接続先プログラムのポート番号で、一般的にStart()などで設定し直して使う。TcpAppServerのサーババインド用で利用される。 OnGUIの接続用GUIで、オペレータによる変更が可能ですが、その初期表示にも使われ、入力文字列が自動的にセットされる。 |
変数 | TcpClient tcpClient; | 接続ソケット情報(これを利用して、下記のConnectTo()を実行します。) |
メソッド | async Task ConnectTo() |
呼び出すと上記(target_IP, tcp_port)の接続先サーバーにTCP接続要求を行うメソッド。 この実行後に、接続待機が非同期で行われる。一般に下記onGUIの接続用ボタン操作で呼び出す。 |
static 変数 |
TcpCommunication.tcpClientButton += () => { }; |
ユーザーの接続ボタンの処理。TcpCommunicationオブジェクトのConnectTo();などの接続処理をここで行います。 |
static 変数 |
TcpCommunication.onConnect += () => { }; |
サーバーの接続許可で実行するクライアント側のイベント処理。ここでアプリのメイン機能などをスタートするとよい。
この呼び出し直後に受信スレッドが起動して、受信したデータはBinPacketのパケットセットされて、QueueBufferのキューに蓄えられます。 受信処理プログラムは、このキューを監視して、適宜に取り出して処理しなければなりません。 |
static 変数 | string clientId = "ShipA"; | クライアント識別IDで、OnGUIの接続用GUIの初期表示にも使われ、入力文字列が自動的にセットされる。 利用者はStart()などで設定し直して使う。使い方は自由で、使わなくても良い。クライアントを名前で管理したい場合にあると、便利かも?ということで用意した。 |
変数 | object messageObject=null | 上記clientIdはstaticなので、クライアント側のアプリで利用で自由に使える変数となるが、これはサーバ側のclientlist要素で自由に設定することを期待した変数 |
static メソッド | void OnGUI_Sub() | この呼び出しを、MonoBehaviour継承クラスで行えば、OnGUIで、onGUI_messageに設定される状態通知(エラーなど)が、表示できます。
表示する位置やサイス情報は、onGUI_〜の変数である程度の表示位置やサイス指定が可能です。 例えば、QueueBufferで使うキューが一杯になった場合は「"QueueBuffer Full!"」が設定されて表示します。 利用者がデバックなどで、TcpCommunication.onGUI_message+=で表示文字列を設定しても構いません。 (TcpAppServer.onGUI_flagをfalseに設定することで、非表示にできまさす。) |
変数 | bool isRecRunning = false; | 受信スレッドが動いている間だけtrue |
メソッド | void SendMessage(string msg) | 接続相手への文字列送信メソッド。msgは内部でUTF-8として処理される。 |
メソッド | void SendMessage(byte [] msg) | 接続相手へのバイナリ配列の送信メソッド |
メソッド | ResetTcpClient() | 一度、エラーなどで接続を切って、再びConnectTo()を行う場合に使うリセット |
static 変数 | int topSize = 4; | このパケットの基本は、任意バイト長を管理するパケットで、先頭にバイト数を管理する領域があり、その領域サイスを指定する。
これが2であればUInt16(65535byteまで)、4であればUInt32 (2147483647byteまで)の送信サイスとする。 2byteに変更する場合で、設定してから使います。(この記憶は、ビックエンディアンで行われる) |
BinPacket(byte[] packet) | 引数のバイト列を参照して記憶するコンストラクタ | |
static メソッド | byte[] makePacketBinary(byte[] data) | 送信用のバイト列取得メソッドで、dataの前にtopSizeバイトのサイズデータを埋め込んだbyte列を生成して返す。 |
static メソッド | byte[] getData(byte[] packet) | 上記のmakePacketBinaryで送られた、バイナリから先頭のサイズを除いてバイト列を取り出して返す。 (受信用メソッドです。BinPacketインスタンスのpacketを引数で指定し、内容のbyte列を取り出して返す。) |
static メソッド | byte[] makePacketBinary(String data) | 送信用のバイト列取得メソッドで、dataの前にtopSizeバイトのサイズデータを埋め込んだbyte列を生成して返す。 |
static メソッド | byte[] getString(byte[] packet) | 上記のmakePacketBinaryで送ったstringを引数の受信バイナリからを取り出して返す。 (受信用メソッドです。BinPacketインスタンスのpacketを引数で指定し、内容のstringを取り出す。) |
メソッド | override string ToString() | 受信用メソッドです。 BinPacketインスタンスのpacketを引数で指定し、内容のstringを取り出す。 |
変数 | int number = 0; | バッファに記憶される個数 |
QueueBuffer(int size) | コンストラクタ 引数の個数の要素を持つリング構造のキューバッファを生成します。 | |
static メソッド | bool Is_Full(QueueBuffer buff) | バッファが一杯で、記憶できなくなったらtrueになります。 (TcpCommunicationの受信スレッドで使っており、エラーメッセージを出しています。) |
static メソッド | void SetBuff(QueueBuffer queueBuffer, BinPacket packet) | キューにpacketを入れる。 packetをqueueBuffer追加する。(上記Is_Full()がfalseの場合だけ実行する使い方をしなければならない) |
static メソッド | BinPacket GetBuff(QueueBuffer queueBuffer) | キューからの取り出して返す。キューが空の場合はnullを返す。 上記のSetBuffと排他制御をしており、それぞれを別スレッド使っても問題が起きないように工夫している。 |
using UnityEngine; using System.Net.Sockets; using System.Threading; using System.Text;//Encoding using System.IO;//StreamWriter,StreamReader using System; using System.Threading.Tasks; using System.Buffers.Binary; public class TcpCommunication { public delegate void TcpClientStartButtonDlegate();// TcpAppServerのOnGUIクライアント起動ボタンで実行するメソッド private static void dummyTcpClientStartButton() { Debug.Log($"dummy message Connect to {target_IP}:{tcp_port}"); } public static TcpClientStartButtonDlegate tcpClientButton = dummyTcpClientStartButton; public delegate void OnConnectDlegate();// サーバに接続できた時に実行するメソッド private static void dummyOnConnect() { Debug.Log($"Connected {target_IP}:{tcp_port}!"); } public OnConnectDlegate onConnect = dummyOnConnect; // 接続時の変数 public static string target_IP = "";//接続先IP public static int tcp_port = 3000; //接続先プログラムのポート番号 public static string clientId = "ShipA";// クライアント識別ID public string netRemoteString = ""; // 送信先の先頭文字列(IPアドレス:ポート番号) public string netLocalString = ""; // 自身の先頭文字列(IPアドレス:ポート番号) public QueueBuffer queueBuffer = new QueueBuffer(1024);//メッセージ受信で使うキュー Thread mythread;//using System.Threading;が必要 (クライアントの受信スレッド) bool isConnectWaiting = false;// TCP接続の待機中だけTrue public static SynchronizationContext context; // 別スレッドからUnitのGUIやGameObjectを操作する時にこれを介す。 Thread myRecthread;////受信処理スレッド public bool isRecRunning = false;// 受信スレッドが動いている間だけ true になる。 public TcpClient tcpClient; //接続ソケット情報 public Stream stream;// バイト列の送受信用 //public static Encoding encoding = new UTF8Encoding(false); public object messageObject = null;//このクラスを利用するアプリケーション側で、自由に利用して構わない変数 public static string onGUI_message = "";// 状態通知(エラーなど) public static float onGUI_startY = 400;// OnGUI部品を並べ始める縦の基準座標 public static float onGUI_height = 300;// OnGUI部品高さのパラメタ public static float onGUI_startX = 10;// OnGUI部品を並べ始める横の基準座標 public static float onGUI_width = 700;// OnGUI部品幅のパラメタ public static Color onGUI_color = Color.black;// OnGUI部品幅のパラメタ public static int onGUI_LabelFontSize = 26;// OnGUI部品幅のパラメタ public TcpCommunication()// 接続用と、サーバ側の両方で使う { if (TcpCommunication.context == null && SynchronizationContext.Current != null) { // 未設定で、有効な状態の場合だけ設定 TcpCommunication.context = SynchronizationContext.Current; } if (TcpCommunication.context == null) Debug.Log("TcpCommunication.contextが未設定"); } // Tcpサーバ側で使うコンストラクタ public TcpCommunication(TcpClient tcpClient) : base() { this.tcpClient = tcpClient; this.netRemoteString = tcpClient.Client.RemoteEndPoint.ToString();// GetHeaderString(tcpClient); this.netLocalString = tcpClient.Client.LocalEndPoint.ToString(); this.stream = tcpClient.GetStream(); //Debug.Log("TcpCommunication サーバ用コンストラクタ実行"); this.myRecthread = new Thread(new ThreadStart(MyRecThread));//受信処理スレッド this.myRecthread.Start(); } public async Task ConnectTo()// staticの(string target_IP, int tcp_port)で接続するクライアント側用 { if (isRecRunning)// 受信スレット動作中 { return; } if (isConnectWaiting)// 接続を待つ { return;// 接続待機中であれば、何もしない。 } ResetTcpClient(); isConnectWaiting = true;// 接続中 try { if (tcp_port > 65535) throw new Exception("ポート番号が大きい過ぎでエラー"); // 接続処理 await Task.Run(() => { this.tcpClient = new TcpClient(target_IP, tcp_port); //サーバに接続する。 }); this.netRemoteString = tcpClient.Client.RemoteEndPoint.ToString();//GetHeaderString(this.tcpClient); this.netLocalString = tcpClient.Client.LocalEndPoint.ToString(); context.Post((state) =>// GUI操作 { onConnect();// 接続した時のdelegateメソッド }, null); mythread = new Thread(new ThreadStart(MyRecThread));//受信スレッドの生成 mythread.Start();//受信スレッドのスタート } catch (Exception err) { onGUI_message += err.Message + "\r\n"; } isConnectWaiting = false;// 接続終了 } public void ResetTcpClient() { if (stream != null) { stream.Dispose(); stream = null; } isRecRunning = false; if (this.tcpClient != null) { this.tcpClient.Dispose(); this.tcpClient = null; } } // サーバー、クライン等検討の受信スレッド(受信したBinPacketをQueueBufferに記憶) private void MyRecThread() { isRecRunning = true; this.stream = tcpClient.GetStream(); string partner = $"{tcpClient.Client.RemoteEndPoint}"; // Debug.Log("MyRecThread.実行開始"); while (isRecRunning) { try { if (QueueBuffer.Is_Full(this.queueBuffer)) { System.Threading.Thread.Sleep(1);//1m秒待ち onGUI_message = $"QueueBuffer Full!"; continue; } BinPacket packet = BinPacket.getBinPacket(this.stream); QueueBuffer.SetBuff(queueBuffer, packet); TcpCommunication.onGUI_message = $"MyRecThread {packet.packet.Length}byte 受信"; // Debug.Log($"MyRecThread {packet.packet.Length}byte(先頭{BinPacket.topSize}byteはサイズ) 受信"); } catch (Exception err) { isRecRunning = false; onGUI_message = $"受信スレッド MyRecThreadを、不具合により終了!{err}"; break; } } isRecRunning = false; } public void SendMessage(string msg)// 接続相手への送信メソッド { if (this.stream == null) return; byte[] packet = BinPacket.makePacketBinary(msg);// 送信データ取得 this.stream.Write(packet, 0, packet.Length);//バイナリ送信 TcpCommunication.onGUI_message = $"SendMessage: {packet.Length}byteを \"{msg}\"の文字列を送信"; // Debug.Log($"SendMessage: {packet.Length}byte(先頭{BinPacket.topSize}byteはサイズ)で \"{msg}\"を文字列を送信"); } public void SendMessage(byte [] msg)// 接続相手への送信メソッド { if (this.stream == null) return; byte[] packet = BinPacket.makePacketBinary(msg);// 送信データ取得 this.stream.Write(packet, 0, packet.Length);//バイナリ送信 TcpCommunication.onGUI_message = $"SendMessage: {packet.Length}byteをbyte配列を送信"; // Debug.Log($"SendMessage: {packet.Length}byte(先頭{BinPacket.topSize}byteはサイズ)をbyte配列を送信"); } public static void OnGUI_Sub()// TcpCommunication.onGUI_messageの文字列を表示する。(OnGUIで呼び出して利用する) { if (TcpAppServer.onGUI_flag == false) return; GUIStyle lableStyle = GUI.skin.GetStyle("label"); // ---Labelスタイル設定 lableStyle.fontSize = onGUI_LabelFontSize; GUI.color = onGUI_color;//文字色設定 GUI.Label(new Rect(onGUI_startX, onGUI_startY, onGUI_width, onGUI_height), $"{ TcpCommunication.onGUI_message }"); } }// TcpCommunication public class BinPacket // TcpCommunicationの送受信で使うパケットの出し入れ用staticメソッド群などを定義 { public static int topSize = 4;// 先頭に、2であればUInt16、4であればUInt32 の送信サイスとする。 internal byte[] packet;//QueueBufferに管理させる(先頭にサイズを埋め込んだuInt16の2byteあり) private BinPacket() { } public BinPacket(byte[] packet) { this.packet = packet; } // 送信用のバイト列取得用 internal static byte[] makePacketBinary(byte[] data)// dataの前にサイズデータを埋め込んだbyte列を生成して返す。 { // 先頭2または4byteにビックエンディアンビットサイズ(最大byte:65535または2147483647)があるバイト列 byte[] packet = new byte[data.Length + topSize]; if (topSize == 2 && packet.Length < 65535) BinaryPrimitives.WriteUInt16BigEndian(packet, (ushort)data.Length); else if (topSize == 4 && packet.Length < 2147483647) BinaryPrimitives.WriteUInt32BigEndian(packet, (uint)data.Length); else throw new Exception("makePacketBinary: bytes size exceeded."); Buffer.BlockCopy(data, 0, packet, topSize, data.Length); return packet; } // 送信時のバイト列取得用 public static byte[] makePacketBinary(String data)// buffer先頭にサイズデータを埋め込んだbyte列を取得 { byte[] sbuff = System.Text.Encoding.UTF8.GetBytes(data); // 先頭2または4byteにビックエンディアンビットサイズ(最大byte:65535または2147483648)があるバイト列 byte[] packet = new byte[sbuff.Length + topSize]; if (topSize == 2 && packet.Length < 65535) BinaryPrimitives.WriteUInt16BigEndian(packet, (ushort)sbuff.Length); else if (topSize == 4 && packet.Length < 2147483647) BinaryPrimitives.WriteUInt32BigEndian(packet, (uint)sbuff.Length); else throw new Exception("makePacketBinary: string size exceeded."); Buffer.BlockCopy(sbuff, 0, packet, topSize, sbuff.Length); return packet; } // 受信用 BinPacketインスタンスのpacketを引数で指定し、内容のbyte列を取り出す。 public static byte[] getData(byte[] packet) { // TcpCommunicationオブジェクトのSendMessage(byte [] msgg)で送ったパケットからの取り出し UInt32 count = 0; if (topSize == 2) count = BinaryPrimitives.ReadUInt16BigEndian(packet);//後ろに並ぶbyte数を取得 else if (topSize == 4) count = BinaryPrimitives.ReadUInt32BigEndian(packet);//後ろに並ぶbyte数を取得 byte[] data = new byte[count]; Buffer.BlockCopy(packet, topSize, data, 0, (int)count);// 2147483648以下 return data; } // 受信用 BinPacketインスタンスのpacketを引数で指定し、内容のstringを取り出す。 internal static string getString(byte[] packet) { // TcpCommunicationオブジェクトのSendMessage(string msg)で送ったパケットからの取り出し UInt32 count = 0; if (topSize == 2) count = BinaryPrimitives.ReadUInt16BigEndian(packet);//後ろに並ぶbyte数を取得 else if (topSize == 4) count = BinaryPrimitives.ReadUInt32BigEndian(packet);//後ろに並ぶbyte数を取得 byte[] sbuff = new byte[count]; Buffer.BlockCopy(packet, topSize, sbuff, 0, (int)count);// 2147483648以下 try { string s = System.Text.Encoding.UTF8.GetString(sbuff, 0, (int)count);// stringのobjectを復元 return s; } catch (Exception e) { return $"{count}byteのBinPacket {e.ToString()}"; } } public override string ToString() { return getString( this.packet ); } // 受信streamからBinPacketを生成して返す internal static BinPacket getBinPacket(Stream stream) { // 受信バイトを取得 byte[] aCount = new byte[topSize]; int n = aCount.Length; int idx = 0; while(idx < n) { idx += stream.Read(aCount, idx, n - idx); } UInt32 count = 0; if (topSize == 2) count = BinaryPrimitives.ReadUInt16BigEndian(aCount);//後ろに並ぶbyte数を取得 else if (topSize == 4) count = BinaryPrimitives.ReadUInt32BigEndian(aCount);//後ろに並ぶbyte数を取得 byte[] packet = new byte[topSize + count]; Buffer.BlockCopy(aCount, 0, packet, 0, topSize); n = packet.Length; while (idx < n) { idx += stream.Read(packet, idx, n - idx); } BinPacket binPacket = new BinPacket(); binPacket.packet = packet; return binPacket; } public static void Test()// テストコード { BinPacket binPacket = new BinPacket(); binPacket.packet = BinPacket.makePacketBinary("abcあいう");// パケットに入れる。 Debug.Log(BinPacket.getString(binPacket.packet));// 上記が表示されればOK byte[] data = { 0, 1, 2, 3 }; binPacket.packet = BinPacket.makePacketBinary(data);// パケットに入れる。 byte [] data2 = BinPacket.getData(binPacket.packet); for(int i=0; i < data2.Length; i++) { Debug.Log(data2[i] == data[i]);// すべてtrueならOK } } }// end BinPacket class // QueueBufferを生成した後、これを引数するstaticメソッド群で操作するためのクラス public class QueueBuffer { private object lockObj = new object(); bool mutex = false; // trueにした関数だけが、ringBufferを出し入れできる。 BinPacket [] ringBuffer = null; int in_idx = -1; int outIdx = -1; public int number = 0;//上記ringBufferバッファに記憶される個数 public QueueBuffer(int size) { ringBuffer = new BinPacket[size]; } public static bool Is_Full(QueueBuffer buff) { return buff.number == buff.ringBuffer.Length; } // packetをqueueBuffer追加する。(上記Is_Full()がfalseの場合だけ実行する使い方をしなければならない) public static void SetBuff(QueueBuffer queueBuffer, BinPacket packet)// キューにpacketを入れる { while (true) { lock (queueBuffer.lockObj)// ロック { if (queueBuffer.mutex == false) { queueBuffer.mutex = true; break; } } System.Threading.Thread.Sleep(1);//1m秒待ち } queueBuffer.number++; if (queueBuffer.in_idx + 1 >= queueBuffer.ringBuffer.Length) { queueBuffer.in_idx = -1; } queueBuffer.ringBuffer[++queueBuffer.in_idx] = packet; queueBuffer.mutex = false; } // queueBufferが空(buff.numberが0)であれば、nullを返す。空でなければバッファから抜き出して返す。 public static BinPacket GetBuff(QueueBuffer queueBuffer)// キューからの取り出し { if (queueBuffer.number == 0) return null; lock (queueBuffer.lockObj)// ロック { if (queueBuffer.mutex) return null; queueBuffer.mutex = true; } queueBuffer.number--; if (queueBuffer.outIdx + 1 >= queueBuffer.ringBuffer.Length) { queueBuffer.outIdx = -1; } BinPacket binPacket = queueBuffer.ringBuffer[++queueBuffer.outIdx]; queueBuffer.mutex = false; return binPacket; } }
using UnityEngine; using System.Collections.Generic; using System.Net; using System.Threading; using System.Net.Sockets; using System; // Canvas か、専用に作ったGameObjectにアタッチして使うのがよいでしょう。 public class TcpAppServer : MonoBehaviour { public static TcpAppServer instance; // 自身のインスタンスを記憶 public delegate void OnTcpServerStartDlegate(TcpAppServer server);// サーバ起動で実行するメソッド private static void dummyTcpServerStart(TcpAppServer server) { Debug.Log($"onTcpServerStart"); } public static OnTcpServerStartDlegate onTcpServerStart = dummyTcpServerStart; public delegate void OnAcceptTcpDelegate(TcpCommunication com);// 接続許可で実行するメソッド private static void dummyAcceptTcp(TcpCommunication com) { Debug.Log($"onAcceptTcp:[{com.netRemoteString}]"); } public static OnAcceptTcpDelegate onAcceptTcp = dummyAcceptTcp; public delegate void OnReceiveDelegate(TcpCommunication com, BinPacket messaeg);// 受信で実行するメソッド private static void dummyOnReceive(TcpCommunication com, BinPacket msg) { Debug.Log($"[{com.netRemoteString}]:{msg}"); } public static OnReceiveDelegate onReceive = dummyOnReceive; static string host = System.Net.Dns.GetHostName(); //使用しているマシンのhost名を取得 public static IPAddress ipAdrs = null; // サーバIPアドレス public TcpListener tcpListener = null; //サーバー用オブジェクト(接続待機) // 接続相手のTcpCommunicationの集合を管理 public List<TcpCommunication> clientlist = new List<TcpCommunication>(); public Dictionary<string, TcpCommunication> clientmap = new Dictionary<string, TcpCommunication>(); Thread startServerThread;//using System.Threading;が必要 (クライアントの接続待ち) bool isStartServer = false; SynchronizationContext context; // 別スレッドからUnitのGUIやGameObjectを操作する時にこれを介す。 IPHostEntry ipHostEntry = null; //このホストの情報取得 IPAddress[] adrList = null; //IPアドレスのリストを取得用 public static bool onGUI_flag = true;// このOnGUIの接続用GUIを利用しない場合にfalse public static bool onGUI_flagClient = true;// クライアント接続用のボタン表示 public static bool onGUI_flagServer = true;// サーバー起動用のボタン表示 public static float onGUI_startY = 20;// OnGUI部品を並べ始める縦の基準座標 public static float onGUI_stepY = 40;// OnGUI部品を並べる縦の間隔のパラメタ public static float onGUI_height = 60;// OnGUI部品高さのパラメタ public static float onGUI_startX = 10;// OnGUI部品を並べ始める横の基準座標 public static float onGUI_width = 700;// OnGUI部品幅のパラメタ public static int onGUI_LabelFontSize = 26;// OnGUI部品文字サイスのパラメタ public static int onGUI_ButtonFontSize = 36;// OnGUI部品文字サイスのパラメタ public static int onGUI_TextFieldFontSize = 36;// OnGUI部品文字サイスのパラメタ public static string onGUI_message = "";// onGUI 用のメッセージ表示 void Start() { instance = this;//自身のインスタンス this.context = SynchronizationContext.Current; TcpCommunication.context = SynchronizationContext.Current; // このホストがIPアドレス情報 this.ipHostEntry = Dns.GetHostEntry(host); //このホストの情報取得 this.adrList = ipHostEntry.AddressList; //IPアドレスのリストを取得 if (this.adrList == null || adrList.Length == 0) { onGUI_message = "ネットワーク環境が使えません。"; return; } int defalt_net_idx = 0; for (int idx = 0; idx < adrList.Length; idx++)// サーバで使えるIPアドレスボタン列挙 { string ip = adrList[idx].ToString(); if (ip.IndexOf('.') != -1) defalt_net_idx = idx; // 列挙最後IPv4 // Debug.Log($"{ip}");//IPアドレスのボタン } if (TcpCommunication.target_IP == "")// 未設定であれば列挙最後IPv4をデフォルトにする。 { TcpCommunication.target_IP = this.adrList[defalt_net_idx].ToString(); ; } } // 起動時の操作やデバック用のユーザインターフェイス(onGUI_flag、onGUI_flagClient、onGUI_flagServerで利用範囲指定) private void OnGUI() { if (onGUI_flag == false) return; float yPos = onGUI_startY; GUI.backgroundColor = Color.cyan;// new Color(0.9f,0.9f,0.9f); GUIStyle lableStyle = GUI.skin.GetStyle("label"); // ---Labelスタイル設定 lableStyle.fontSize = onGUI_LabelFontSize; GUIStyle buttonStyle = GUI.skin.GetStyle("button"); // ---Buttonスタイル設定 buttonStyle.fontSize = onGUI_ButtonFontSize; GUI.skin.textField.fontSize = onGUI_TextFieldFontSize; // ---TextFieldスタイル設定 if (onGUI_flagClient) { GUI.color = Color.black;//文字色設定 GUI.Label(new Rect(onGUI_startX, yPos, onGUI_width / 2, onGUI_height), "クライアント名"); GUI.color = Color.yellow;//文字色設定 TcpCommunication.clientId = GUI.TextField( new Rect(onGUI_width / 2, yPos, onGUI_width / 2, onGUI_height), TcpCommunication.clientId, 20, GUI.skin.textField); yPos += onGUI_stepY * 2; GUI.color = Color.black;//文字色設定 GUI.Label(new Rect(onGUI_startX, yPos, onGUI_width, onGUI_height), "以下が接続先サーバのIPアドレス(編集可能)"); yPos += onGUI_stepY; GUI.color = Color.yellow;//文字色設定 TcpCommunication.target_IP = GUI.TextField( new Rect(onGUI_startX, yPos, onGUI_width, onGUI_height), TcpCommunication.target_IP, 50, GUI.skin.textField); yPos += onGUI_stepY * 2; } if (onGUI_flagClient || onGUI_flagServer) { GUI.color = Color.black;//文字色設定 GUI.Label(new Rect(onGUI_startX, yPos, onGUI_width / 2, onGUI_height), "TCP接続のポート番号"); GUI.color = Color.yellow;//文字色設定 string str_tcp_port = TcpCommunication.tcp_port + ""; str_tcp_port = GUI.TextField( new Rect(onGUI_width / 2, yPos, onGUI_width / 2, onGUI_height), str_tcp_port, 6, GUI.skin.textField); try { TcpCommunication.tcp_port = int.Parse(str_tcp_port); } catch (Exception) { } yPos += onGUI_stepY * 1.5f; } if (onGUI_flagClient) { GUI.color = Color.black;//文字色設定 GUI.Label(new Rect(onGUI_startX, yPos, onGUI_width, onGUI_height), "上記サーバのクライアントとして始める場合は、"); yPos += onGUI_stepY; GUI.Label(new Rect(onGUI_startX, yPos, onGUI_width, onGUI_height), "次の接続ボタンをクリックしてください。"); yPos += onGUI_stepY; GUI.color = Color.yellow;//文字色設定 if (GUI.Button(new Rect(onGUI_startX, yPos, onGUI_width, onGUI_height), "サーバに接続する"))//IPアドレスのボタン { TcpCommunication.tcpClientButton(); } yPos += onGUI_stepY * 3; } if (onGUI_flagServer) { GUI.color = Color.black;//文字色設定 GUI.Label(new Rect(onGUI_startX, yPos, onGUI_width, onGUI_height), "サーバーとして始める場合は、以下のIPアドレスボタンから"); yPos += onGUI_stepY; GUI.Label(new Rect(onGUI_startX, yPos, onGUI_width, onGUI_height), "使用するアドレスを選んで、クリックしてください。"); yPos += onGUI_stepY; GUI.color = Color.yellow;//文字色設定 for (int idx = 0; idx < adrList.Length; idx++, yPos += onGUI_stepY * 1.8f)// サーバで使えるIPアドレスボタン列挙 { if (GUI.Button(new Rect(onGUI_startX, yPos, onGUI_width, onGUI_height), adrList[idx].ToString()))//IPアドレスのボタン { ipAdrs = adrList[idx];// サーバに使うIPアドレスを記憶 TcpCommunication.target_IP = adrList[idx].ToString(); StartServer();//サーバーの起動 } } } GUI.color = Color.black;//文字色設定 //this.msgError = "起動しました。"; if (onGUI_message!="") GUI.Label(new Rect(onGUI_startX, yPos, onGUI_width, onGUI_height), $"Message:{ onGUI_message }"); yPos += onGUI_stepY*2; if (this.clientlist.Count > 0) { GUI.Label(new Rect(onGUI_startX, yPos, onGUI_width, onGUI_height), $"接続数:{ this.clientlist.Count }"); yPos += onGUI_stepY; TcpCommunication com = this.clientlist[this.clientlist.Count - 1]; GUI.Label(new Rect(onGUI_startX, yPos, onGUI_width, onGUI_height), $"最後に接続した相手{com.netRemoteString} "); } } // サーバ再起動の準備 public void ResetTcpServer() { for( int n=0; n < this.clientlist.Count; n++) { this.clientlist[n].ResetTcpClient();// 各クライアントオブジェクトをリセット } if (this.isStartServer) { tcpListener.Stop(); } this.isStartServer = false; tcpListener = null; } // TCPサーバ起動関連処理(スレッド起動) public void StartServer() { try { tcpListener = new TcpListener(ipAdrs, TcpCommunication.tcp_port); //サーバー用オブジェクト生成 tcpListener.Start(); this.isStartServer = true; //LISTENING 状態 } catch (Exception e) { onGUI_message = $"サーバ起動失敗:{ipAdrs}:{TcpCommunication.tcp_port}, {e.Message}"; return; } this.startServerThread = new Thread(new ThreadStart(StartServerThread));//クライアント待機スレッド this.startServerThread.Start(); onTcpServerStart(this); // サーバー起動時のdelgateメソッド } // サーバ用待ち受けスレッド private void StartServerThread() { isStartServer = true; TcpClient tcpClient = null; onGUI_message = $"サーバ{ipAdrs}:{TcpCommunication.tcp_port}待ち受けスレッド起動!"; try { while (isStartServer) { tcpClient = tcpListener.AcceptTcpClient(); //ライアントからの接続要求を受け入れる(接続を待っている) onGUI_message = "ライアントからの接続要求を受け入れ!"; // サーバ側の受信スレッドも起動 TcpCommunication tcpCommunication = new TcpCommunication(tcpClient); this.clientlist.Add(tcpCommunication); clientmap.Add(tcpCommunication.netRemoteString, tcpCommunication); context.Post((state) =>// ここで、GUI用の処理の追加コードを可能にする { TcpAppServer.onAcceptTcp(tcpCommunication);// delegateメソッド起動 }, null); } } catch (Exception e) { onGUI_message = $"Error: StartServerThread: { e.Message }"; isStartServer = false; return; } // Debug.Log("StartServerThread End"); } void FixedUpdate() { // 全てのクライアントからの受信処理を実行 for(int i=0; i < this.clientlist.Count; i++) { TcpCommunication com = this.clientlist[i]; BinPacket packet = QueueBuffer.GetBuff(com.queueBuffer);//キューから取り出す if (packet != null) { onReceive(com, packet);// デリゲート受信処理 } } } /// オブジェクトが破棄時に実行 private void OnDestroy() { // Debug.Log("OnDestroy"); ResetTcpServer(); } }
using System;
using System.IO;
using Unity.Collections;
using UnityEngine;
using UnityEngine.UI;
public class WhiteBorad : MonoBehaviour
{
const int width = 360;// RawImageに設定するTexture2Dのサイズ
const int height = 360;
const string filePath = "white_board.bin";// textureの画像を保存するファイル名
public static int brushSize = 3;// 上記色のこのサイズのい正方形をブラシとして使う
RawImage rawImage;// Canvasいっぱいに配置したRawImageで、ここが描画対象
void Start()
{
Time.fixedDeltaTime = 0.02f;//またはApplication.targetFrameRate = 50;
GameObject rawImageObj = GameObject.Find("RawImage");
this.rawImage = rawImageObj.GetComponent<RawImage>();
Color32 color = new Color32(255, 255, 255, 255);
this.rawImage.texture = createTexture(width, height, color);// color色のTexture2Dを生成;
// filePathのファイルが存在すれば、そのファイル内容で初期表示
FileInfo fileInfo = new FileInfo(filePath);
if (true && fileInfo.Exists)
{
byte[] buff = new byte[fileInfo.Length];
using (FileStream stream = File.Open(filePath, FileMode.Open))
{
int idx = 0;
while (idx < buff.Length)
{
idx += stream.Read(buff, idx, buff.Length - idx);// ファイルから読み込む
}
}
Texture2D texture = (Texture2D)this.rawImage.texture;
Color32[] pixels = texture.GetPixels32();
BytesToColor32(buff, pixels);// ファイルから取得したバイト列でテクスチャピクセル設定
texture.SetPixels32(pixels);
texture.Apply();
}
}
void FixedUpdate()
{
if (Input.GetMouseButton(1))// 右ボタン操作で消去
{
this.rawImage.texture = createTexture(width, height, new Color32(255, 255, 255, 255) );
}
if (Input.GetMouseButton(0))// 左ボタン操作
{
Vector3 pos = Input.mousePosition;
Vector3 viewPos = Camera.main.ScreenToViewportPoint(pos);
int x = (int)(viewPos.x * width);//マウス位置からピクセル位置を取得
int y = (int)(viewPos.y * height);
Debug.Log($"viewPos={viewPos}x ={x:000}y={y:000}");
Color32 color = new Color32(255, 0, 0, 255);
draw(color, x, y);// brushSizeで描画処理// 描画 赤
}
}
public void OnApplicationQuit()// プログラム終了時に、rawImage.textureの画像をファイル化
{
Texture2D texture = (Texture2D)this.rawImage.texture;
byte[] buff = getBytesByPixels(texture.GetPixels32());// textureの画像をバイナリ化
using (FileStream stream = File.Open(filePath, FileMode.Create))
{
stream.Write(buff, 0, buff.Length);// ファイルに書き込み
}
}
// this.rawImage.textureの画像のピクセル群で、(x,y)位置を左下としたbrushSizeの正方形をcolor色に設定
private void draw(Color32 color, int x, int y)
{
int idx = y * height + x;// クリック位置からTexture2D内のピクセルを指定色に変更する。
Texture2D texture = (Texture2D)this.rawImage.texture;
Color32[] pixels = texture.GetPixels32();
try
{
for (int y2 = 0; y2 < brushSize; y2++)
{
for (int x2 = 0; x2 < brushSize; x2++)
{
pixels[idx + x2 + y2 * width] = color;
}
}
}
catch (Exception _) { }
texture.SetPixels32(pixels);
texture.Apply();
}
// color色で、width × heightのサイズのTexture2Dを生成する。
public static Texture2D createTexture(int width, int height, Color32 color)
{
Texture2D texture = new Texture2D(width, height, TextureFormat.RGBA32, false);
NativeArray<Color32> pixels = texture.GetRawTextureData<Color32>();
Debug.Log($"{pixels.Length} ( {texture.height}, {texture.width})");
for (int i = 0; i < pixels.Length; i++)
{
pixels[i] = color;
}
texture.Apply();
return texture;
}
// byte配列で、Texture2Dから取得したpixels配列を設定する(バイナリから画像を設定)
public static void BytesToColor32(byte[] bytes, Color32[] pixels)
{
bool chk = bytes.Length == pixels.Length * 4;
if (chk == false) throw new Exception("BytesToColor32: {bytes.Length]byteをpixelsに設定出来ません。");
for (int idx_bytes = 0, idx = 0; idx < pixels.Length; idx++)
{
byte r = bytes[idx_bytes++];
byte g = bytes[idx_bytes++];
byte b = bytes[idx_bytes++];
byte a = bytes[idx_bytes++];
pixels[idx] = new Color32(r, g, b, a);
}
}
// Texture2Dから取得したpixels配列からbyte配列からを生成して返す。(画像のバイナリ化)
public static byte[] getBytesByPixels(Color32[] pixels)
{
byte[] bytes = new byte[pixels.Length * 4];
for (int i = 0, idx_bytes = 0; i < pixels.Length; i++)
{
bytes[idx_bytes++] = pixels[i].r;
bytes[idx_bytes++] = pixels[i].g;
bytes[idx_bytes++] = pixels[i].b;
bytes[idx_bytes++] = pixels[i].a;
}
return bytes;
}
}
サーバー起動画面 | クライアント起動画面 |
---|---|
![]() | ![]() |
サーバーで IPアドレスボタンを押した後の画面 |
クライアントで 接続ボタンを操作した直後のサーバ側画面 |
クライアントで 接続ボタンを押して、通信が始まった時の画面 |
---|---|---|
![]() |
![]() |
![]() |
TCPサーバー側実行可能ファイルは、クライアントから送られる"RED,100,20"のような文字列を受信して、
それを、自身のRawImageに描画する。
そのRawImageのイメージをFixedUpdate()で、接続しているすべてのクライアントに配信する。
また、マウス右ボタンで画面消去が出来る。
終了時に使って画像をバイナリファイルで保存し、起動時にそれを復元して表示する。
TCPクライアント側実行可能ファイルは、サーバーに接続すると、
FixedUpdate()でサーバからの受信チェックを行って、バイナリ受信があれば、それで自身のRawImageに描画する。
また、クライアントから送られる"RED,100,20"のような文字列を受信して、それを、自身のRawImageに描画する。
またFixedUpdate()では、マウス左ボタンを押したタイミングで、クライアント名に指定したを色と座標を文字列でサーバに送信する。
以下が動作イメージです。「Build And Run」のメニュー操作でクライアントを2つ起動し、
Unityエディタではサーバとして動かし、左クライアントで接続後、「へのへの」を描いた情報を
サーバと共有した後、右のクライアントを接続しようとしている動作例です。
(この動作ムービーを紹介したXへのリンク)
このムービは、パッケージをインポートした後、EX_WhiteBoradClientSceneを開いて、「Build And Run」を2回実行し、
その後にシーンをEX_WhiteBoradSeverSceneで開き直してサーバをスタートさせています。
using System; using System.Collections.Generic; using System.IO; using Unity.Collections; using UnityEngine; using UnityEngine.UI; public class WhiteBoradServer : MonoBehaviour { float serverSendNextTiming; //サーバー側で、Time.timeが、この時間以上になったら情報を発信する。 const int width = 360;// RawImageに設定するTexture2Dのサイズ const int height = 360; const string filePath = "white_board.bin"; Dictionary<string, Color32> colorMap = new Dictionary<string, Color32>(){ {"R", new Color32(255, 0, 0, 255)}, {"G", new Color32(0, 255, 0, 255)}, {"B", new Color32(0, 0, 255, 255)}, {"W", new Color32(255, 255, 255, 255)} }; public static int brushSize = 3;// 上記色のこのサイズのい矩形をブラシとして使う RawImage rawImage;// Canvasいっぱいに配置したRawImageで、ここが描画対象 void Start() { Time.fixedDeltaTime = 0.02f;//Application.targetFrameRate = 50;に相当する固定フレーム間隔指定 TcpAppServer.onGUI_width = 300;//OnGUI()で使う表示幅変更 TcpAppServer.onGUI_TextFieldFontSize = 24;//OnGUI()で使う表示文字サイス変更 TcpAppServer.onGUI_LabelFontSize = 14; TcpAppServer.onGUI_ButtonFontSize = 16; TcpAppServer.onGUI_flagClient = false;// サーバ専用のonGUI操作画面を使う(クライアントが不要) TcpCommunication.tcp_port = 51234;// サーバで使うポート番号(実行後に変更可能) TcpAppServer.onTcpServerStart += (server) => {// サーバ起動時の処理 TcpAppServer.onGUI_flagServer = false;// サーバ用のOnGUI表示を消す }; TcpAppServer.onAcceptTcp += (client) => {// 接続してきたクライアントへのACCSEPT時の処理 //TcpAppServer.onGUI_flag = false;// trueで接続時のデバック情報表示制御 }; TcpAppServer.onReceive += (TcpCommunication com, BinPacket msg) => { // クライアントからのの受信処理(この例では、msgの文字列を受信) string[] datas = msg.ToString().ToUpper().Split(",");// 受信例 "RED,100,20"で、色とxとyの座標 if (datas.Length != 3) { TcpAppServer.onGUI_message = $"onReceiveの受信文字列エラー:{msg.ToString()}"; return; } Color32 color = new Color32(0, 0, 0, 255);// 描画 黒 string colorkey = datas[0].Length > 0 ? datas[0].Substring(0,1) : "X"; if (colorMap.ContainsKey(colorkey)) color = colorMap[colorkey];//受信先頭文字から色を取得 int x = int.Parse(datas[1]); int y = int.Parse(datas[2]); draw(color, x, y); }; GameObject rawImageObj = GameObject.Find("RawImage"); this.rawImage = rawImageObj.GetComponent<RawImage>();// 描画対象のRawImageをインスタンス変数にセット Color32 color = new Color32(255, 255, 255, 255); this.rawImage.texture = createTexture(width, height, color);// color色のTexture2Dを生成; // filePathのファイルが存在すれば、そのファイル内容で初期表示 FileInfo fileInfo = new FileInfo(filePath); if (false && fileInfo.Exists) { byte[] buff = new byte[fileInfo.Length];// ファイル内容を記憶するバッファ using (FileStream stream = File.Open(filePath, FileMode.Open)) { int idx = 0; while (idx < buff.Length) { idx += stream.Read(buff, idx, buff.Length - idx);// ファイルから読み込む } } Texture2D texture = (Texture2D)this.rawImage.texture; Color32[] pixels = texture.GetPixels32(); BytesToColor32(buff, pixels);// texture内のPixels32配列にbuffをセット texture.SetPixels32(pixels);// 画面更新 texture.Apply(); } } void FixedUpdate() { if (Input.GetMouseButton(1))// 右ボタン操作で消去 { this.rawImage.texture = createTexture(width, height, new Color32(255, 255, 255, 255)); } bool sendTiming = Time.time >= this.serverSendNextTiming; if (sendTiming == false) return;// クライアントへのイメージ送信タイミング制御 this.serverSendNextTiming = Time.time + 0.01f; // 10ミリ秒ごとに送信(次の送信タイミング) // サーバーの画面の送信情報を取得 Texture2D texture = (Texture2D)this.rawImage.texture; Color32[] pixels = texture.GetPixels32(); byte[] sendBuffer = getBytesByPixels(pixels); // 接続してきたクライアントすべてに送信 for (int i = 0; i < TcpAppServer.instance.clientlist.Count; i++) { TcpCommunication com = TcpAppServer.instance.clientlist[i]; if (com == null || com.isRecRunning == false) continue; com.SendMessage(sendBuffer); // クライアントに送信 } } // this.rawImage.textureに、color色で( x, y )位置にbrushSize幅の正方形を描画 private void draw(Color32 color, int x, int y) { int idx = y * height + x;// クリック位置からTexture2D内のピクセルを指定色に変更する。 Texture2D texture = (Texture2D)this.rawImage.texture; Color32[] pixels = texture.GetPixels32(); try { for (int y2 = 0; y2 < brushSize; y2++) { for (int x2 = 0; x2 < brushSize; x2++) { pixels[idx + x2 + y2 * width] = color; } } } catch (Exception _) { } texture.SetPixels32(pixels); texture.Apply(); } public void OnApplicationQuit()// プログラム終了時に、SerializeStrVector3オブジェクトをファイル化 { Texture2D texture = (Texture2D)this.rawImage.texture; byte[] buff = getBytesByPixels(texture.GetPixels32()); using (FileStream stream = File.Open(filePath, FileMode.Create)) { stream.Write(buff, 0, buff.Length);// ファイルに書き込み } } // // color色で、width × heightのサイズのTexture2Dを生成する。 public static Texture2D createTexture(int width, int height, Color32 color) { Texture2D texture = new Texture2D(width, height, TextureFormat.RGBA32, false); NativeArray<Color32> pixels = texture.GetRawTextureData<Color32>(); Debug.Log($"{pixels.Length} ( {texture.height}, {texture.width})"); for (int i = 0; i < pixels.Length; i++) { pixels[i] = color; } texture.Apply(); return texture; } // byte配列で、Texture2Dから取得したpixels配列を設定する(バイナリから画像を設定) public static void BytesToColor32(byte[] bytes, Color32[] pixels) { bool chk = bytes.Length == pixels.Length * 4; if (chk == false) throw new Exception("BytesToColor32: {bytes.Length]byteをpixelsに設定出来ません。"); for (int idx_bytes = 0, idx = 0; idx < pixels.Length; idx++) { byte r = bytes[idx_bytes++]; byte g = bytes[idx_bytes++]; byte b = bytes[idx_bytes++]; byte a = bytes[idx_bytes++]; pixels[idx] = new Color32(r, g, b, a); } } // Texture2Dから取得したpixels配列からbyte配列からを生成して返す。(画像のバイナリ化) public static byte[] getBytesByPixels(Color32[] pixels) { byte[] bytes = new byte[pixels.Length * 4]; for (int i = 0, idx_bytes = 0; i < pixels.Length; i++) { bytes[idx_bytes++] = pixels[i].r; bytes[idx_bytes++] = pixels[i].g; bytes[idx_bytes++] = pixels[i].b; bytes[idx_bytes++] = pixels[i].a; } return bytes; } }
using System; using Unity.Collections; using UnityEngine; using UnityEngine.UI; public class WhiteBoradClient : MonoBehaviour { TcpCommunication tcpCommunication = new TcpCommunication(); // クライアント用 const int width = 360;// RawImageに設定するTexture2Dのサイズ const int height = 360; public static int brushSize = 3;// 上記色のこのサイズの矩形をブラシとして使う RawImage rawImage;// Canvasいっぱいに配置したRawImageで、ここが描画対象 void Start() { Time.fixedDeltaTime = 0.02f;//Application.targetFrameRate = 50;に相当する固定フレーム間隔指定 TcpAppServer.onGUI_width = 300;//OnGUI()で使う表示幅変更 TcpAppServer.onGUI_TextFieldFontSize = 24;//OnGUI()で使う表示文字サイス変更 TcpAppServer.onGUI_LabelFontSize = 14; TcpAppServer.onGUI_ButtonFontSize = 16; TcpAppServer.onGUI_flagServer = false;//onGUIサーバ起動部表示のみ消す TcpCommunication.onGUI_LabelFontSize = 16; TcpCommunication.clientId = "Red"; // 自由に使える情報で、今回は色指定に使った。 TcpCommunication.target_IP = "192.168.0.110"; // デフォルト接続先IPアドレス(実行後に変更可能) TcpCommunication.tcp_port = 51234;// サーバで使うポート番号(実行後に変更可能) TcpCommunication.tcpClientButton += () => {// TcpAppServerの接続用GUIボタンで実行 TcpAppServer.onGUI_flagClient = false;//onGUIクライアントの接続部表示のみ消す tcpCommunication.ConnectTo();// TcpCommunication.target_IP,TcpCommunication.tcp_portに接続 }; tcpCommunication.onConnect += () => { TcpAppServer.onGUI_flag = true;// デバックをしないならflaseにすると良い。 TcpCommunication.onGUI_message += "接続した!"; // デバック用 }; GameObject rawImageObj = GameObject.Find("RawImage"); this.rawImage = rawImageObj.GetComponent<RawImage>(); Color32 color = new Color32(255, 255, 255, 255); this.rawImage.texture = createTexture(width, height, color);// color色のTexture2Dを生成; } void FixedUpdate() { if (Input.GetMouseButton(0))// 左ボタン操作 { // 色と、クリック情報の送信文字列を生成して、送信 Vector3 pos = Input.mousePosition; Vector3 viewPos = Camera.main.ScreenToViewportPoint(pos); int x = (int)(viewPos.x * width); int y = (int)(viewPos.y * height); Debug.Log($"viewPos={viewPos}x ={x:000}y={y:000}"); string sendMsg = $"{TcpCommunication.clientId},{x},{y}"; this.tcpCommunication.SendMessage(sendMsg); // サーバに送信 } // クライアント側の受信処理(受信した最後のパケットを取り出す。途中のパケットが無視できる場合) BinPacket binPacket = null; for (;;) { // 受信情報の取り出し BinPacket tempPacket = QueueBuffer.GetBuff(tcpCommunication.queueBuffer); if (tempPacket == null) break; binPacket = tempPacket; } if (binPacket == null) return; // 受信パケット無し // 受信パケットで、rawImage.textureを更新 byte[] bytes = BinPacket.getData(binPacket.packet);//受信パケットから、内容のバイナリを取り出だす。 Texture2D texture = (Texture2D)this.rawImage.texture; Color32[] pixels = texture.GetPixels32(); BytesToColor32(bytes,pixels); texture.SetPixels32(pixels);// 受信データで画面を更新 texture.Apply(); } private void OnGUI() { TcpCommunication.OnGUI_Sub();// TcpCommunication.messgae の表示(デバック用) } public static Texture2D createTexture(int width, int height, Color32 color) { Texture2D texture = new Texture2D(width, height, TextureFormat.RGBA32, false); NativeArray<Color32> pixels = texture.GetRawTextureData<Color32>(); Debug.Log($"{pixels.Length} ( {texture.height}, {texture.width})"); for (int i = 0; i < pixels.Length; i++) { pixels[i] = color; } texture.Apply(); return texture; } // byte配列からpixels配列を設定 public static void BytesToColor32(byte[] bytes, Color32[] pixels) { bool chk = bytes.Length == pixels.Length * 4; if (chk == false) throw new Exception("BytesToColor32: {bytes.Length]byteをpixelsに設定出来ません。"); for (int idx_bytes = 0, idx = 0; idx < pixels.Length; idx++) { byte r = bytes[idx_bytes++]; byte g = bytes[idx_bytes++]; byte b = bytes[idx_bytes++]; byte a = bytes[idx_bytes++]; pixels[idx] = new Color32(r, g, b, a); } } }