Unity C#でMulticastを送受信したい

こんにちは、Luncoです。  

この記事は [カバー株式会社 Advent Calendar 2024](https://qiita.com/advent-calendar/2024/cover) 20日目の記事です。

## はじめに
UnityでMulticast通信をしたいタイミングってありませんか？  
複数マシンでの時間的変化のある大量な情報の同期などで使いたくなるタイミングはあるんじゃないでしょうか？ありますよね？？？？？？  

本記事ではUnity C#でMulticast送受信する際の方法について紹介します。  

## Multicast（マルチキャスト）とは
UDPを使用したデータ通信で、Unicast通信の1対1通信のとは違い、1対複数でデータを送信することができる通信方式です。  
複数の対象に全く同じデータを送信したい際に、Unicast通信だと送信対象の数だけ送出しなければならないところを、Multicastでは1回の送出で送ることができます。  

## MulticastとBroadcastの違いについて
ここまで読んでそれBroadcastでもよくないか...？と思った方もいらっしゃるかもしれません。  
同一ネットワーク上の複数のPCに同じデータを送れるという点ではMulticastでもBroadcastでも問題ありません。  
しかしMulticastはBroadcastとは違い、グループに参加という概念があり、グループに参加していない同一ネットワーク上のPCではデータを受け取りません。(実際にはWireShark等で確認するとパケット自体は到達しているようです。)  
これにより、同一ネットワーク上のPCやPCのネットワークインターフェースの負荷も考慮したい場合、Multicastを採用するといったことが選択肢になりえると思います。

## UnityでMulticastを使った送受信を実装してみる
今回は送信側も受信側も、複数のネットワークに接続していることを想定して、ネットワークインターフェースを指定する形で実装しました。

### 環境
Windows 11  
Unity 2022.3.18f1  

### 送信側

```C#
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using UnityEngine;

public class MulticastSender : MonoBehaviour
{
    [SerializeField] private string _multicastAddress = "239.255.0.1";
    [SerializeField] private string _interfaceAddress = "192.168.0.2";
    [SerializeField] private int _port = 8001;

    private IPEndPoint _multicastEndpoint;
    private UdpClient _udpClient;

    void Start()
    {
        IPAddress interfaceAddress = IPAddress.Parse(_interfaceAddress);
        IPAddress multicastAddress = IPAddress.Parse(_multicastAddress);
        IPEndPoint localEndPoint = new IPEndPoint(interfaceAddress, _port);

        // 送出するInterfaceを指定してUdpClientを作成する
        _udpClient = new UdpClient(localEndPoint);

        // 送出先を設定する
        _multicastEndpoint = new IPEndPoint(multicastAddress, _port);
    }

    byte[] GetMessageByte(string message)
    {
        return Encoding.UTF8.GetBytes(message);
    }

    void Update()
    {
        // 送信するテキストをbyte[]へ変換
        var message = $"Hello Multicast!! {DateTime.Now.ToString("hh:mm:ss.fff")}";
        var messageByte = GetMessageByte(message);
        // 送出
        _udpClient.Send(messageByte, messageByte.Length, _multicastEndpoint);
    }
}
```



### 受信側

```C#
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using UnityEngine;

public class MulticastReceiver : MonoBehaviour, IDisposable
{
    [SerializeField] private string _multicastAddress = "239.255.0.1";
    [SerializeField] private string _interfaceAddress = "192.168.0.2";
    [SerializeField] private int _port = 8001;

    private UdpClient _udpClient;
    private IPEndPoint _localEndpoint;

    private Thread _thread;

    private bool _isDisposed;
    
    void Start()
    {
        IPAddress multicastAddress = IPAddress.Parse(_multicastAddress);
        IPAddress interfaceAddress = IPAddress.Parse(_interfaceAddress);

        _udpClient = new UdpClient();
        // ポートを指定
        _localEndpoint = new IPEndPoint(IPAddress.Any, _port);

        // SocketにBindする
        _udpClient.Client.Bind(_localEndpoint);
        // インターフェースを指定してマルチキャストグループに参加
        _udpClient.JoinMulticastGroup(multicastAddress, interfaceAddress);

        // 受信開始
        _thread = new Thread(new ThreadStart(ReceiveThread));
        _thread.Start();
    }

    private void ReceiveThread()
    {
        byte[] data;
        while (true)
        {
            if (_isDisposed) break;
            data = _udpClient.Receive(ref _localEndpoint);
            string receiveText = Encoding.UTF8.GetString(data);
            Debug.Log(receiveText);
        }
    }

    public void Dispose()
    {
        _isDisposed = true;
    }
}

```

## 実行
各スクリプトを送信側のUnity、受信側のUnity上でGameObjectにアタッチして実行します。  

### 送信側
![2024-12-10_19h50_13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148928/7257e71e-e0cb-f5d3-6914-245b0fb92a2d.png)

### 受信側
![2024-12-10_19h50_25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148928/ac99d390-d112-1a24-e566-738b9cfb7862.png)

### 実行結果
![2024-12-10_20h20_37.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148928/4b392e34-c6a2-8b39-4001-241ad3f99f73.gif)

受信することができました！！  

## まとめ
無事Unity C#でMulticastを送信、受信することができました。  
Unityを複数台のマシンで動作させて状態を同期するなんてことはそうそうないかもしれませんが、万が一発生したときは参考にしていただければと思います。  
余談ですが、Unity C#のUdpClientのReceive関数はGC Allocが発生します。  
パフォーマンスを追求したい場合はUdpClientの拡張メソッドなどを作成して回避することをお勧めします。  

最後まで読んでいただきありがとうございました。  
明日の [カバー株式会社 Advent Calendar 2024](https://qiita.com/advent-calendar/2024/cover) は [@AkitsuguHirano](https://qiita.com/AkitsuguHirano) さんが投稿します。  
よろしければぜひご覧ください。