🥳

【書きかけ】Unityで押さえるべきネットワーク技術:TCP、UDP、WebRTC、RPC、WebSocketの実装と活用ポイント

2024/12/11に公開

はじめに

記事の目的

本記事では、各通信プロトコルを紹介し、それらをUntiyで実装する際の実装例を掲載します。
また備忘録も兼ねて、それらの通信プロトコルの活用事例についても紹介していこうと思います。

対象読者

Unityエンジニア(C#が読み書きできる)向けの記事です。

本記事の範囲

本記事では、通信プロトコルに注力し、ネットワークモデルについては説明しません。

ネットワーク技術の概要

TCP: 信頼性確保型の通信プロトコル

TCP(Transmission Control Protocol)は、インターネットにおいて最も広く使用されている信頼性のある接続指向型プロトコルです。通信を行う前にクライアントとサーバー間で「コネクション(接続)」を確立し、その後、データをパケット単位で送受信します。TCPは受信側がパケットの到着を確認する仕組み(ACK)や、パケット損失時の再送、送信レートの調整を行う「輻輳制御」など、信頼性と公平性を担保するメカニズムを備えています。

UDP: 軽量でリアルタイム性重視の通信プロトコル

UDP(User Datagram Protocol)は、TCPとは対照的なコネクションレス型のプロトコルで、データは「データグラム」として即座に送信されます。受信側がパケット紛失や順序を保証しないため、アプリケーション側が必要であれば独自で対処する必要があります。

WebRTC: ブラウザ間P2P通信とマルチメディアストリーミング

WebRTC(Web Real-Time Communication)は、ブラウザ間、あるいはブラウザとサーバー間で音声・映像・任意データをリアルタイムにやり取りできる技術仕様です。P2P通信を想定しており、デフォルトでUDPベースでのリアルタイム性を重視した伝送を行います。さらにNAT越えや暗号化などにも対応しています。近年では、音声通話、ビデオチャット、スクリーンシェアリング、オンラインコラボレーションツールなどで多用されています。

WebSocket: 双方向通信を可能にするウェブ標準

WebSocketは、TCP上で双方向かつ低オーバーヘッドな全二重通信を実現するプロトコルです。通常、HTTP(S)で初期ハンドシェイクを行い、その後は専用のWebSocketフレームによる軽量な通信へ移行します。ブラウザとサーバー間、あるいは他のクライアント間をサーバーが中継して、リアルタイムなメッセージ交換が可能となります。

RPC: リモート手続き呼び出しによる抽象化

RPC(Remote Procedure Call)は、別のプロセス(または別のマシン)上にある関数(メソッド)を、あたかもローカル関数のように呼び出せる仕組みです。RPCそのものは特定のトランスポートに依存しませんが、実装の多くはTCPやHTTPなど既存プロトコル上で手続き呼び出しを抽象化します。最近では、gRPCなどが人気で、HTTP/2+Protobufを組み合わせた高速・軽量なRPCを提供します。

各ネットワーク技術の比較

紹介した5つのネットワーク技術を速度、データ量、信頼性で比較した表を以下に掲載します。
また、それらのネットワーク技術の用途を挙げてみます。

技術 速度 データ量 信頼性 主な用途・特徴例
TCP 3 4 HTTPやFTPなどの汎用データ転送、Webサービスなど
UDP 5 3 オンラインゲーム、VoIP、リアルタイムメディアストリーミングに最適
WebRTC 4 4 ブラウザ間ビデオチャット、P2Pファイル共有、メディアストリーム
RPC 3 2 中程度 マイクロサービス間通信、ゲームサーバーコマンド呼び出し
WebSocket 3 4 リアルタイムチャット、通知、ゲームイベント同期、双方向Web通信

Unity C#での実装方法

TCPの実装例

TCPの実装例
TcpConnectionManager.cs
    
    using System;
    using System.Net.Sockets;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;

    public class TcpConnectionManager
    {
        private TcpClient _client;
        private NetworkStream _stream;
        private CancellationTokenSource _cts;

        public bool IsConnected => _client != null && _client.Connected;

        // 接続処理: 成功時はtrueを返す
        public async Task<bool> ConnectAsync(string ip, int port)
        {
            try
            {
                _cts = new CancellationTokenSource();
                _client = new TcpClient();
                await _client.ConnectAsync(ip, port);
                _stream = _client.GetStream();
                return true;
            }
            catch (Exception)
            {
                Disconnect();
                return false;
            }
        }

        // メッセージ送信(非同期)
        public async Task<bool> SendAsync(string message)
        {
            if (!IsConnected || _stream == null) return false;

            try
            {
                byte[] data = Encoding.UTF8.GetBytes(message);
                await _stream.WriteAsync(data, 0, data.Length, _cts.Token);
                return true;
            }
            catch (Exception)
            {
                Disconnect();
                return false;
            }
        }

        // メッセージ受信ループを開始(必要ならUI Managerから呼び出す)
        public async Task ReceiveLoopAsync(Action<string> onMessageReceived)
        {
            if (!IsConnected || _stream == null) return;

            byte[] buffer = new byte[1024];
            while (!_cts.Token.IsCancellationRequested && IsConnected)
            {
                int bytesRead = 0;
                try
                {
                    bytesRead = await _stream.ReadAsync(buffer, 0, buffer.Length, _cts.Token);
                }
                catch
                {
                    // 例外発生時は接続切断処理
                    Disconnect();
                    break;
                }

                if (bytesRead == 0)
                {
                    // サーバー側が切断した
                    Disconnect();
                    break;
                }

                string receivedMessage = Encoding.UTF8.GetString(buffer, 0, bytesRead);
                onMessageReceived?.Invoke(receivedMessage);
            }
        }

        // 切断処理
        public void Disconnect()
        {
            try
            {
                _cts?.Cancel();
                _stream?.Close();
                _client?.Close();
            }
            catch { }
            finally
            {
                _stream = null;
                _client = null;
                _cts = null;
            }
        }
    }

UIManager.cs
    
using UnityEngine;
using UnityEngine.UI;
using System.Threading.Tasks;

public class UIManager : MonoBehaviour
{
    [SerializeField] private InputField IPInputField;
    [SerializeField] private InputField PortInputField;
    [SerializeField] private Text StatusText;

    [SerializeField] private Button ConnectButton;
    [SerializeField] private Button DisconnectButton;

    [SerializeField] private InputField MessageInputField;
    [SerializeField] private Button SendButton;

    private TcpConnectionManager _tcpManager;
    private Task _receiveTask;

    private void Awake()
    {
        _tcpManager = new TcpConnectionManager();

        ConnectButton.onClick.AddListener(OnConnectButtonPressed);
        DisconnectButton.onClick.AddListener(OnDisconnectButtonPressed);
        SendButton.onClick.AddListener(OnSendButtonPressed);

        UpdateStatus("待機中...");
    }

    private async void OnConnectButtonPressed()
    {
        if (_tcpManager.IsConnected)
        {
            UpdateStatus("既に接続済みです。");
            return;
        }

        string ip = IPInputField.text;
        if (!int.TryParse(PortInputField.text, out int port))
        {
            UpdateStatus("ポート番号が不正です。");
            return;
        }

        UpdateStatus("接続中...");
        bool connected = await _tcpManager.ConnectAsync(ip, port);
        if (connected)
        {
            UpdateStatus($"接続成功: {ip}:{port}");
            StartReceiveLoop();
        }
        else
        {
            UpdateStatus("接続失敗");
        }
    }

    private void OnDisconnectButtonPressed()
    {
        if (!_tcpManager.IsConnected)
        {
            UpdateStatus("未接続です。");
            return;
        }

        _tcpManager.Disconnect();
        UpdateStatus("切断しました。");
    }

    private async void OnSendButtonPressed()
    {
        if (!_tcpManager.IsConnected)
        {
            UpdateStatus("送信失敗: 接続していません。");
            return;
        }

        string msg = MessageInputField.text;
        if (string.IsNullOrEmpty(msg))
        {
            UpdateStatus("メッセージが空です。");
            return;
        }

        bool sent = await _tcpManager.SendAsync(msg);
        if (sent)
        {
            UpdateStatus("送信成功: " + msg);
        }
        else
        {
            UpdateStatus("送信失敗");
        }
    }

    private async void StartReceiveLoop()
    {
        // メッセージ受信ループ開始
        _receiveTask = _tcpManager.ReceiveLoopAsync(OnMessageReceived);
        await _receiveTask;
    }

    private void OnMessageReceived(string message)
    {
        // UI操作はメインスレッドで行う必要があるため、MainThreadに戻す。
        // Unityでは、UnityMainThreadDispatcherやTaskScheduler.FromCurrentSynchronizationContext()を使う方法もありますが、
        // ここでは簡易的にInvokeしてUIスレッドへ戻します。(Unity 2023以降ならUnitySynchronizationContextなども利用可)
        UnityMainThreadInvoke(() =>
        {
            UpdateStatus("受信: " + message);
        });
    }

    private void UpdateStatus(string text)
    {
        StatusText.text = text;
    }

    // メインスレッドで実行するための簡易的実装例
    private void UnityMainThreadInvoke(System.Action action)
    {
        if (this == null) return; 
        // MonoBehaviourがないと使えないが、一般的にはGameObjectにこのスクリプトがアタッチされている。

        if (action == null) return;

        // UnityではUpdate等でQueue処理するか、コルーチンで行うことが多い
        // ここでは単純化のため、次のフレームで実行する仕組みをコルーチンで用意
        StartCoroutine(RunNextFrame(action));
    }

    private System.Collections.IEnumerator RunNextFrame(System.Action action)
    {
        yield return null; // 次のフレームまで待機
        action();
    }
}

UDPの実装例

UDP実装例
UdpServerManager.cs

using UnityEngine;
using UnityEngine.UI;
using System.Threading.Tasks;
using System.Threading;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System;

public class ServerManager : MonoBehaviour
{
    [SerializeField] private InputField IPInputField;
    [SerializeField] private InputField PortInputField;
    [SerializeField] private Text StatusText;
    [SerializeField] private Button StartButton;
    [SerializeField] private Button StopButton;

    private TcpServer _server;
    private CancellationTokenSource _cts;

    private void Awake()
    {
        _server = new TcpServer();
        StartButton.onClick.AddListener(OnStartButtonPressed);
        StopButton.onClick.AddListener(OnStopButtonPressed);

        UpdateStatus("待機中...");
    }

    private async void OnStartButtonPressed()
    {
        if (_server.IsRunning)
        {
            UpdateStatus("サーバーは既に稼働中です。");
            return;
        }

        string ip = IPInputField.text;
        if (!int.TryParse(PortInputField.text, out int port))
        {
            UpdateStatus("ポート番号が不正です。");
            return;
        }

        UpdateStatus($"サーバー起動中... {ip}:{port}");
        _cts = new CancellationTokenSource();

        bool result = await _server.StartServerAsync(ip, port, OnServerLog, _cts.Token);

        if (result)
        {
            UpdateStatus($"サーバー起動成功 {ip}:{port}");
        }
        else
        {
            UpdateStatus("サーバー起動失敗");
        }
    }

    private void OnStopButtonPressed()
    {
        if (!_server.IsRunning)
        {
            UpdateStatus("サーバーは稼働していません。");
            return;
        }

        _cts.Cancel();
        _server.StopServer();
        UpdateStatus("サーバー停止");
    }

    private void OnServerLog(string message)
    {
        Debug.Log(message);
        // 非同期コールバック内でUI操作するため、メインスレッドへ戻す
        UnityMainThreadInvoke(() =>
        {
            UpdateStatus(message);
        });
    }

    private void UpdateStatus(string text)
    {
        if (StatusText != null)
        {
            StatusText.text = text;
        }
    }

    // メインスレッドで処理を行うためのユーティリティ
    private void UnityMainThreadInvoke(Action action)
    {
        if (this == null) return;
        if (action == null) return;
        StartCoroutine(RunNextFrame(action));
    }

    private System.Collections.IEnumerator RunNextFrame(Action action)
    {
        yield return null;
        action();
    }
}

UdpClientManager.cs
    
using UnityEngine;
using UnityEngine.UI;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

public class UdpClientManager : MonoBehaviour
{
    [SerializeField] private InputField IpInputField;
    [SerializeField] private InputField PortInputField;
    [SerializeField] private InputField MessageInputField;
    [SerializeField] private Text StatusText;
    [SerializeField] private Button SendButton;

    private void Awake()
    {
        SendButton.onClick.AddListener(OnSendButtonPressed);
        UpdateStatus("クライアント待機中...");
    }

    private async void OnSendButtonPressed()
    {
        string ip = IpInputField.text;
        if (!int.TryParse(PortInputField.text, out int port))
        {
            UpdateStatus("ポート番号が不正です。");
            return;
        }

        string message = MessageInputField.text;
        if (string.IsNullOrEmpty(message))
        {
            UpdateStatus("メッセージが空です。");
            return;
        }

        bool success = await SendUdpMessage(ip, port, message);
        if (success)
        {
            UpdateStatus($"送信成功: {message}");
        }
        else
        {
            UpdateStatus("送信失敗");
        }
    }

    private async Task<bool> SendUdpMessage(string ip, int port, string message)
    {
        UdpClient udpClient = new UdpClient();
        byte[] data = Encoding.UTF8.GetBytes(message);

        try
        {
            await udpClient.SendAsync(data, data.Length, ip, port);
            udpClient.Close();
            return true;
        }
        catch (System.Exception e)
        {
            UpdateStatus("エラー: " + e.Message);
            return false;
        }
    }

    private void UpdateStatus(string text)
    {
        Debug.Log(text);
        if (StatusText != null)
        {
            StatusText.text = text;
        }
    }
}

WebRTCとUnity: UnityWebRTCパッケージの利用

以下のパッケージをPackageManager経由でインポートする

com.unity.webrtc

WebRTC実装例
ReceiverSide.cs

using UnityEngine;
using UnityEngine.UI;
using Unity.WebRTC;
using System.Collections;

public class ReceiverSide : MonoBehaviour
{
    [SerializeField] private Button SetOfferButton;
    [SerializeField] private Button CreateAnswerButton;
    [SerializeField] private InputField RemoteSdpInputField;
    [SerializeField] private Text StatusText;

    private RTCPeerConnection _peerConnection;
    private RTCDataChannel _dataChannel;

    private void Awake()
    {
        // WebRTC.Initialize()は不要
        CreatePeerConnection();

        SetOfferButton.onClick.AddListener(() => StartCoroutine(SetOfferCoroutine()));
        CreateAnswerButton.onClick.AddListener(() => StartCoroutine(CreateAnswerCoroutine()));

        UpdateStatus("Waiting for Offer...");
    }

    private void CreatePeerConnection()
    {
        var config = GetConfiguration();
        _peerConnection = new RTCPeerConnection(ref config);

        _peerConnection.OnIceCandidate = candidate => {
            Debug.Log("Receiver ICE candidate: " + candidate.Candidate);
        };

        _peerConnection.OnDataChannel = channel => {
            _dataChannel = channel;
            _dataChannel.OnOpen = () => {
                UpdateStatus("DataChannel opened on Receiver.");
            };
            _dataChannel.OnMessage = bytes => {
                var msg = System.Text.Encoding.UTF8.GetString(bytes);
                UpdateStatus("Received: " + msg);
            };
        };
    }

    private RTCConfiguration GetConfiguration()
    {
        return new RTCConfiguration
        {
            iceServers = new[]
            {
                new RTCIceServer { urls = new string[] { "stun:stun.l.google.com:19302" } }
            }
        };
    }

    private IEnumerator SetOfferCoroutine()
    {
        var sdp = RemoteSdpInputField.text;
        var desc = new RTCSessionDescription
        {
            sdp = sdp,
            type = RTCSdpType.Offer
        };

        var setRemoteOp = _peerConnection.SetRemoteDescription(ref desc);
        yield return setRemoteOp;

        if (setRemoteOp.IsError)
        {
            UpdateStatus("Failed to set remote description: " + setRemoteOp.Error.message);
        }
        else
        {
            UpdateStatus("Offer set. Now create an Answer.");
        }
    }

    private IEnumerator CreateAnswerCoroutine()
    {
        var answerOp = _peerConnection.CreateAnswer();
        yield return answerOp;
        var answer = answerOp.Desc;

        var setLocalOp = _peerConnection.SetLocalDescription(ref answer);
        yield return setLocalOp;

        if (setLocalOp.IsError)
        {
            UpdateStatus("Failed to set local description: " + setLocalOp.Error.message);
        }
        else
        {
            Debug.Log("==== SEND THIS ANSWER TO SENDER ====");
            Debug.Log(answer.sdp);
            Debug.Log("====================================");

            UpdateStatus("Answer created and shown in console. Provide this to Sender.");
        }
    }

    private void UpdateStatus(string message)
    {
        Debug.Log(message);
        if (StatusText != null)
        {
            StatusText.text = message;
        }
    }

    private void OnDestroy()
    {
        _dataChannel?.Dispose();
        _peerConnection?.Close();
        _peerConnection?.Dispose();
        // WebRTC.Shutdown()は不要
    }
}

SenderSide.cs

using UnityEngine;
using UnityEngine.UI;
using Unity.WebRTC;
using System.Collections;

public class SenderSide : MonoBehaviour
{
    [SerializeField] private Button CreateOfferButton;
    [SerializeField] private Button SendMessageButton;
    [SerializeField] private Button SetRemoteDescriptionButton;
    [SerializeField] private InputField RemoteSdpInputField;
    [SerializeField] private InputField MessageInputField;
    [SerializeField] private Text StatusText;

    private RTCPeerConnection _peerConnection;
    private RTCDataChannel _dataChannel;

    private void Awake()
    {
        // WebRTC.Initialize()は最新バージョンでは不要

        CreatePeerConnection();
        
        CreateOfferButton.onClick.AddListener(() => StartCoroutine(CreateOfferCoroutine()));
        SetRemoteDescriptionButton.onClick.AddListener(() => StartCoroutine(SetRemoteDescriptionCoroutine()));
        SendMessageButton.onClick.AddListener(OnSendMessageButtonClicked);

        UpdateStatus("Waiting...");
    }

    private void CreatePeerConnection()
    {
        var config = GetConfiguration();
        _peerConnection = new RTCPeerConnection(ref config);
        _peerConnection.OnIceCandidate = candidate => {
            Debug.Log("Sender ICE candidate: " + candidate.Candidate);
        };

        // DataChannel作成
        var options = new RTCDataChannelInit { ordered = true };
        _dataChannel = _peerConnection.CreateDataChannel("chat", options);
        _dataChannel.OnOpen = () => {
            UpdateStatus("DataChannel Opened. Ready to send message.");
        };
        _dataChannel.OnClose = () => {
            UpdateStatus("DataChannel Closed.");
        };
    }

    private RTCConfiguration GetConfiguration()
    {
        return new RTCConfiguration
        {
            iceServers = new[]
            {
                new RTCIceServer { urls = new string[] { "stun:stun.l.google.com:19302" } }
            }
        };
    }

    private IEnumerator CreateOfferCoroutine()
    {
        var offerOp = _peerConnection.CreateOffer();
        yield return offerOp;
        var offer = offerOp.Desc;

        var setLocalOp = _peerConnection.SetLocalDescription(ref offer);
        yield return setLocalOp;

        Debug.Log("==== SEND THIS OFFER TO RECEIVER ====");
        Debug.Log(offer.sdp);
        Debug.Log("====================================");

        UpdateStatus("Offer created. Waiting for Answer...");
    }

    private IEnumerator SetRemoteDescriptionCoroutine()
    {
        var sdp = RemoteSdpInputField.text;
        var desc = new RTCSessionDescription
        {
            sdp = sdp,
            type = RTCSdpType.Answer
        };

        var setRemoteOp = _peerConnection.SetRemoteDescription(ref desc);
        yield return setRemoteOp;

        if (setRemoteOp.IsError)
        {
            UpdateStatus("Failed to set remote description: " + setRemoteOp.Error.message);
        }
        else
        {
            UpdateStatus("Remote Description(Answer) set. Connection should establish soon.");
        }
    }

    private void OnSendMessageButtonClicked()
    {
        if (_dataChannel != null && _dataChannel.ReadyState == RTCDataChannelState.Open)
        {
            var msg = MessageInputField.text;
            _dataChannel.Send(msg);
            UpdateStatus("Sent: " + msg);
        }
        else
        {
            UpdateStatus("DataChannel not open!");
        }
    }

    private void UpdateStatus(string message)
    {
        Debug.Log(message);
        if (StatusText != null)
        {
            StatusText.text = message;
        }
    }

    private void OnDestroy()
    {
        _dataChannel?.Dispose();
        _peerConnection?.Close();
        _peerConnection?.Dispose();
        // WebRTC.Shutdown()は不要になったため削除
    }
}



RPCの実装例: Unity Networkingの使用

以下のパッケージをPackageManager経由でインポートする。

com.unity.netcode.gameobjects

RPC実装例
NetcodeExample.cs

using UnityEngine;
using UnityEngine.UI;
using Unity.Netcode;

public class NetcodeExample : NetworkBehaviour
{
    [Header("UI References")]
    [SerializeField] private Button StartHostButton;
    [SerializeField] private Button StartClientButton;
    [SerializeField] private Button SendButton;
    [SerializeField] private InputField MessageInputField;
    [SerializeField] private Text StatusText;

    private void Awake()
    {
        // ボタンイベント設定
        StartHostButton.onClick.AddListener(() => {
            NetworkManager.Singleton.StartHost();
            UpdateStatus("Host started.");
        });

        StartClientButton.onClick.AddListener(() => {
            NetworkManager.Singleton.StartClient();
            UpdateStatus("Client started.");
        });

        SendButton.onClick.AddListener(OnSendButtonPressed);
        
        UpdateStatus("Waiting for start...");
    }

    private void OnSendButtonPressed()
    {
        // クライアントのみメッセージを送れるようにする
        if (!IsClient || IsServer) 
        {
            UpdateStatus("Only client can send messages.");
            return;
        }

        string msg = MessageInputField.text;
        if (string.IsNullOrEmpty(msg))
        {
            UpdateStatus("Message is empty.");
            return;
        }

        // クライアント側でServerRpcを呼ぶ
        SendMessageToServerRpc(msg);
        UpdateStatus("Sent to Server: " + msg);
    }

    // ServerRpc: クライアントが呼ぶとサーバーで実行される
    [ServerRpc]
    private void SendMessageToServerRpc(string message, ServerRpcParams rpcParams = default)
    {
        Debug.Log("Server received: " + message);
        // 全クライアントへブロードキャスト
        BroadcastMessageClientRpc(message);
    }

    // ClientRpc: サーバーから全クライアントへ通知
    [ClientRpc]
    private void BroadcastMessageClientRpc(string message, ClientRpcParams rpcParams = default)
    {
        UpdateStatus("Broadcast: " + message);
    }

    private void UpdateStatus(string text)
    {
        Debug.Log(text);
        if (StatusText != null)
        {
            StatusText.text = text;
        }
    }
}

WebSocketの実装例: C#用WebSocketライブラリを用いたリアルタイム通信

以下のNugetパッケージをインポートする(Assets/Plugin以下にwebsocket-sharp.dllがあればOK)

https://www.nuget.org/packages/WebSocketSharp

WebSocket実装例
EchoBehavior.cs

using System;
using WebSocketSharp;
using WebSocketSharp.Server;

public class EchoBehavior : WebSocketBehavior
{
    protected override void OnMessage(MessageEventArgs e)
    {
        Console.WriteLine("Received from client: " + e.Data);
        // クライアントからのメッセージをそのまま返す(エコー)
        Send("Echo: " + e.Data);
    }

    protected override void OnOpen()
    {
        Console.WriteLine("Client connected.");
    }

    protected override void OnClose(CloseEventArgs e)
    {
        Console.WriteLine("Client disconnected.");
    }
}

class Program
{
    static void Main(string[] args)
    {
        // ws://localhost:8080/echo にアクセスするとEchoBehaviorが呼ばれる
        var wssv = new WebSocketServer("ws://0.0.0.0:8080");
        wssv.AddWebSocketService<EchoBehavior>("/echo");
        wssv.Start();

        Console.WriteLine("WebSocket Server started on ws://localhost:8080/echo");
        Console.WriteLine("Press ENTER to stop server...");
        Console.ReadLine();
        wssv.Stop();
    }
}


EchoBehavior.cs

using UnityEngine;
using UnityEngine.UI;
using WebSocketSharp;

public class WebSocketClient : MonoBehaviour
{
    [SerializeField] private InputField IpInputField;      // "ws://localhost:8080/echo"など
    [SerializeField] private InputField MessageInputField; // 送信メッセージ入力
    [SerializeField] private Text StatusText;
    [SerializeField] private Button ConnectButton;
    [SerializeField] private Button SendButton;

    private WebSocket _ws;

    private void Awake()
    {
        ConnectButton.onClick.AddListener(OnConnectButtonPressed);
        SendButton.onClick.AddListener(OnSendButtonPressed);
        UpdateStatus("Waiting...");
    }

    private void OnConnectButtonPressed()
    {
        if (_ws != null && _ws.IsAlive)
        {
            UpdateStatus("Already connected.");
            return;
        }

        string url = IpInputField.text;
        if (string.IsNullOrEmpty(url))
        {
            UpdateStatus("URL is empty.");
            return;
        }

        _ws = new WebSocket(url);

        _ws.OnOpen += (sender, e) =>
        {
            UnityMainThreadInvoke(() =>
            {
                UpdateStatus("Connected to " + url);
            });
        };

        _ws.OnMessage += (sender, e) =>
        {
            UnityMainThreadInvoke(() =>
            {
                UpdateStatus("Received: " + e.Data);
            });
        };

        _ws.OnClose += (sender, e) =>
        {
            UnityMainThreadInvoke(() =>
            {
                UpdateStatus("Connection closed.");
            });
        };

        _ws.OnError += (sender, e) =>
        {
            UnityMainThreadInvoke(() =>
            {
                UpdateStatus("Error: " + e.Message);
            });
        };

        _ws.ConnectAsync();
        UpdateStatus("Connecting...");
    }

    private void OnSendButtonPressed()
    {
        if (_ws == null || !_ws.IsAlive)
        {
            UpdateStatus("Not connected.");
            return;
        }

        string msg = MessageInputField.text;
        if (string.IsNullOrEmpty(msg))
        {
            UpdateStatus("Message is empty.");
            return;
        }

        _ws.Send(msg);
        UpdateStatus("Sent: " + msg);
    }

    private void UpdateStatus(string text)
    {
        Debug.Log(text);
        if (StatusText != null)
        {
            StatusText.text = text;
        }
    }

    private void OnDestroy()
    {
        if (_ws != null)
        {
            _ws.Close();
            _ws = null;
        }
    }

    // メインスレッド戻し用
    private void UnityMainThreadInvoke(System.Action action)
    {
        if (this == null) return;
        if (action == null) return;
        StartCoroutine(RunNextFrame(action));
    }

    private System.Collections.IEnumerator RunNextFrame(System.Action action)
    {
        yield return null;
        action();
    }
}


プログラムが動かないので書きかけ

技術選定の考え方

  • プロジェクト要件に合わせた選択(信頼性 vs レイテンシ、軽量性 vs 機能性)
  • ゲームジャンルやアプリ用途に合わせる(リアルタイムアクション vs ターンベース、ストリーミング必要性等)

活用事例と実運用上のヒント

  • ゲーム内チャット・マッチメイキングでのWebSocket
  • マルチプレイヤー同期でのUDP+補正ロジック
  • 映像・音声伝送が必要なVR/ARコンテンツでのWebRTC
  • シンプルなリモートコマンド実行でのRPC
  1. 参考文献・関連リンク
    • Unity公式ドキュメント
    • 各プロトコル仕様・RFC
    • 使用ライブラリの公式リポジトリ・ガイドリンク

Discussion