C#で楽しいゲーム作り講座　P2P通信編

対戦ゲームを作るならば、通信対戦は必須でしょう。本記事ではインターネット通信のやり方をやっていきましょう。

:::note alert
通信の安全性については私は全然知らないので注意してくださいね！
:::

ちなみにUnityなら簡単にできるよ！みたいな記事はありましたがなぜかC#単体ではそんな記事はないという感じだったので書かせていただきます。

:::note alert
今回はシグナリングサーバーを使わない方法を取ります。この方法だと通信環境やパソコンの環境によっては通信が確立できない可能性があります。
:::

[参考](https://microsoft.github.io/MixedReality-WebRTC/)

#下ごしらえ

Microsoft.MixedReality.WebRTCをnugetからインストールする。

どうやって通信を行うのか考えておく。私の場合は画面の描画情報をクライアントに渡し、サーバーはクライアントからマウスやキーの入力情報を受け取るとしました。

#どう通信をするねん
クライアント用とサーバー用のクラスを作り、cast()によって情報を送りgetn()で受信した値を取得させます。


```C#:tuusinnman

    class serverman3
    {
        int cou = 0;
        public void resetcount() { cou = 0; jyu.Clear(); }//↓でカウントが多いと接続してない判定になるようにしてるのでこの通信を使いまわすために必要
        //コネクテッド情報
        public bool connected { get { if (pc == null) return false; return cou <= 600 && pc.IsConnected; } }
        tuusinform form;
        public void closeform() { form.Close(); }
        PeerConnection pc;

         List<inputin> jyu=new List<inputin>();
        int maxbuf = 3;

        public inputin getn() //バッファーからデータを取り出す関数
        {
            if (jyu.Count>0)
            {
                cou = 0;
                var a=jyu[0];
                jyu.RemoveAt(0);//バッファーから削除する。
                return a;
            }
            cou++;//受け取れてないカウンターを一つ進める。60溜まったら通信が断絶されたとか判断する。
            return null;
        }

        public serverman3()
        {
            pc = new PeerConnection();
            pcs.Add(pc);//アプリ終了後でディスポーズするためにおいておく。そうしないとプロセスが残り続ける
            var llis = new List<IceServer>();
            foreach (var aaa in bvc.stuns) //STUNサーバーのURLが入ってるのがbvc.stuns
            {
                llis.Add(new IceServer { Urls = {aaa } });
                break;
            }
            var config = new PeerConnectionConfiguration
            {
                IceServers = llis, SdpSemantic = SdpSemantic.UnifiedPlan,
               IceTransportType = IceTransportType.All,
               BundlePolicy = BundlePolicy.Balanced
            };//この辺はテンプレでいいと思う
            var aa=pc.InitializeAsync(config);//ピアコネを初期化

            aa.Wait();

            //この辺はただ知らせるだけだからいらないかも
            pc.Connected += () => {
                Console.WriteLine("PeerConnection: connected.");
            };
            pc.RenegotiationNeeded += () => {
                Console.WriteLine("PeerConnection: detected.");
            };

            pc.IceStateChanged += (IceConnectionState newState) => {
                Console.WriteLine($"ICE state: {newState}");
            };
            //これは絶対に必要。データをやり取りするためのチャンネルが追加されたらそのチャネルのイベントにSDatarecievedを追加
            pc.DataChannelAdded +=(DataChannel chan)=> 
            {
                chan.MessageReceived += SDatarecieved;
            };
            
            var a = pc.AddDataChannelAsync("D1", true,true);//これでデータチャネルを足す
            
            a.Wait();
    
           form= new tuusinform(pc,true);//このフォームは通信情報（IPアドレスとか）を交換するためのもの。普通はサーバーで交換する
            form.Show();
        }
        private void SDatarecieved(byte[] e)//データチャネルにメッセージ(でーた)が来た時に発動するイベント
        {
         //受信したデータは3つ程度のバッファーに溜めておく
         //こっちは軽いので圧縮(解凍)は必要ない？
            try
            {
                var a = inputin.andbyte(e);//入力inputinをbyte[]に変換する関数
                if (a != null)
                {
                   jyu.Add(a);
                   if (jyu.Count > maxbuf) jyu.RemoveAt(0);//最大を超過したらけす。
                }
            }
            catch (Exception eee) { Console.WriteLine("oohnryokai! " + eee.ToString()); }
        }
      
        public void cast(hyojiman h)//画面表示情報をクライアントに飛ばす。
        {
            if (pc.DataChannels.Count < 2)//通信ができていない間は不用意に飛ばさないようにする。
            {
                return;
            }
            try
            {
                var b = fileman.hyoandbyte(h);//表示情報をbyte[]に変換する
                b = asyukku(b);//圧縮する(しないと通信制限に引っかかっちゃうよ！)
              

                {
                    foreach (var d in pc.DataChannels)//データを送る
                    {
                        d.SendMessage(b);
                        break;
                    }
                }
                
            }
            catch (Exception eee) { Console.WriteLine("saurope! " + eee.ToString()); }//一応おいてるエラーメッセージ
        }
        public void dispose()
        {
            closeform();
            pcs.Remove(pc);
            pc.Close();
            pc.Dispose();
        }
      
        static public byte[] asyukku(byte[] src)//byteを圧縮する
        {
            using (var ms = new MemoryStream())
            {
                using (var ds = new DeflateStream(ms, CompressionMode.Compress, true/*msは*/))
                {
                    ds.Write(src, 0, src.Length);
                }

                ms.Position = 0;
                byte[] comp = new byte[ms.Length];
                ms.Read(comp, 0, comp.Length);
               
                return comp;
            }
        }
        static public byte[] kaitou(byte[] src)//byte[]を解凍する
        {
            using (var ms = new MemoryStream(src))
            using (var ds = new DeflateStream(ms, CompressionMode.Decompress))
            {
                using (var dest = new MemoryStream())
                {
                    ds.CopyTo(dest);

                    dest.Position = 0;
                    byte[] decomp = new byte[dest.Length];
                    dest.Read(decomp, 0, decomp.Length);
                    return decomp;
                }
            }
        }

    }
    class clientman3
    {
        List<hyojiman> jyu = new List<hyojiman>();//バッファー
        int maxbuf = 3;
        public hyojiman getn()//バッファーからデータを取り出す関数
        {
      
            if (jyu.Count>0)
            {
                count = 0;
                var a = jyu[0];
                jyu.RemoveAt(0);
                return a;
            }
            count++;
            return null;
        }

        PeerConnection pc;
        //コネクテッド判定
        public bool connected { get { if ( pc== null) return false; return count<=600&&pc.IsConnected; } }

        public void resetcount() { count = 0;jyu.Clear(); }//再接続に必要
        int count = 0;
        tuusinform form;//通信情報(IPアドレス)とかを交換するための奴
        public void closeform() { form.Close(); }
       
        public clientman3()
        {
            pc = new PeerConnection();
            serverman3.pcs.Add(pc);//アプリ終了後でディスポーズするためにおいておく。そうしないとプロセスが残り続ける

            var llis = new List<IceServer>();
            foreach (var aaa in bvc.stuns)//bvc.stunsにはSTUNサーバーのURLがいくつか入ってるよ。
            {
                llis.Add(new IceServer { Urls = { aaa } });
                break;
            }
      
            var config = new PeerConnectionConfiguration
            {
                IceServers =llis,
                SdpSemantic = SdpSemantic.UnifiedPlan,
                IceTransportType = IceTransportType.All,
                BundlePolicy = BundlePolicy.Balanced

            };
            
            var aa = pc.InitializeAsync(config);
            
            aa.Wait();
            pc.Connected += () =>
            {
                Console.WriteLine("PeerConnection: connected.");
            };
            pc.IceStateChanged += (IceConnectionState newState) =>
            {
                Console.WriteLine($"ICE state: {newState}");
            };

            //必須よ
            pc.DataChannelAdded += (DataChannel chan) =>
            {
                chan.MessageReceived += CDatarecieved;
            };
          var a= pc.AddDataChannelAsync("D1", true, true);

            a.Wait();
          
            Console.WriteLine("aaaa7");
            form=new tuusinform(pc, false);
            form.Show();
        }
     
        private void CDatarecieved(byte[]e)//データを受け取ったときの対応
        {
            try
            {
                var get = e;

                get = serverman3.kaitou(get);//データを解凍する
                var a = fileman.hyoandbyte(get);//解凍した奴を画面表示情報にする
                if (a != null)
                {
                    jyu.Add(a);
                    if (jyu.Count > maxbuf) jyu.RemoveAt(0);
                }
            }
            catch (Exception aea) { Console.WriteLine(aea); }
        }
  
        public void cast(inputin i)//データを送信する奴
        {
            if (pc.DataChannels.Count < 2)
            {
                return;
            }
            try
            {
                var b = inputin.andbyte(i);//軽いので圧縮は必要ない？
                {
                    foreach (var d in pc.DataChannels)
                    {
                        d.SendMessage(b);
                        break;
                    }
                }
            }
            catch
            {
             
            }
        }
        public void dispose()
        {
            closeform();
            serverman3.pcs.Remove(pc);
            pc.Close();
            pc.Dispose();
        }
        private void Disconnected(object sender, EventArgs e)
        {
            
        }
  
    }

```

ちなみにクラスをbyte[]に変換する方法はこちら

```C#:henkan.cs

 static public byte[] andbyte(inputin hyo)
        {
            try
            {
                if (hyo != null)
                {
                    BinaryFormatter binaryFormatter = new BinaryFormatter();//御なじみバイフォマ
                    using (MemoryStream ms = new MemoryStream())//仮想のメモリ？ファイル？ストリーム？を作ってそこに書き込む
                    {
                        if (ms != null && binaryFormatter != null)
                        {
                            binaryFormatter.Serialize(ms, hyo);

                            return ms.ToArray();
                        }
                    }
                }
            }
            catch (Exception eee) { Console.WriteLine("tytytyttyt" + eee.ToString()); }
            return null;
        }
        static public inputin andbyte(byte[] hyo)
        {
            try
            {
                if (hyo != null)
                {
                    BinaryFormatter binaryFormatter = new BinaryFormatter();//はたまたバイフォマ
                    using (MemoryStream ms = new MemoryStream(hyo))//byteをいれてメモストを作る
                    {
                        if (ms != null && binaryFormatter != null)
                        {
                            var a= binaryFormatter.Deserialize(ms);//デシリアライズ！
                           if(a!=null) return (inputin)a;
                        }
                    }
                }
            }
            catch (Exception eee) { Console.WriteLine("tumibukakierror" + eee.ToString()); }
            return null;
            
        }

```
#通信情報の交換


そしてtuusinformで直接IPアドレスを交換し合います(本当は同じ動作をサーバーに接続して自動で行ってマッチメイクとかするけどサーバーのことわからないからしょうがなし。)

通信を開始するにはSdpMessage,Icecandidateなんちゃらが必要で、これをPeerConecctionにごにょごにょします。

```C#:tusinform.cs
/*
*クライアントは事前(tuusinformのコンストラクタ)にPeerConecction.CreateOffer();をする。そうするとSdpMessageを取得できる。
*textBox3.Text=SdpMessage.Content +"(^v^)" +IceCandidate.SdpMid +"(^v^)"+IceCandidate.SdpMlineInde+"(^v^)"+IceCandidate.Content;
*/
var ttt=(textBox3.Text).Split(new string[] { "(^v^)" },StringSplitOptions.RemoveEmptyEntries);
//区切り文字から分割する。

SdpMessage mess = new SdpMessage();//SdpMessageをつくる
mess.Content = ttt[0];//Contentを代入
if (sv) mess.Type = SdpMessageType.Offer;//サーバーであればタイプをオファー、違ければアンサーにする
else mess.Type = SdpMessageType.Answer;
try
{
 var b = PeerConnection.SetRemoteDescriptionAsync(mess);//ピアコネにSdpMessageをセット
 b.Wait();
 if (sv)
 {
  this.Text = "相手に左のメッセージを渡す";
 }
 else
 {
  this.Text = "後は待てばいいと思うが……";
 }
}
catch (Exception ee) { Console.WriteLine("^^:" + ee.ToString());this.Text = "失敗。ごめん"; }
if (mess.Type == SdpMessageType.Offer)
{
 PeerConnection.CreateAnswer();//サーバーならアンサーを作成。これでサーバー側のSdpMessageも構築
}
try //共通でIceCandidateをセット
{
 var cand = new IceCandidate
 {
  SdpMid = ttt[1],SdpMlineIndex = Convert.ToInt32(ttt[2]),Content = ttt[3]
 };
 PeerConnection.AddIceCandidate(cand);
}
catch { }
```

でSdpMssageや、IceCandidateを取得するには以下の手順が必要です。呼び出されるイベントを通してでしか取得できなかったはずです。

```C#:tuusinform.cs
     public tuusinform(PeerConnection ppp,bool server )
        {
			sv = server;
			PeerConnection = ppp;
			InitializeComponent();
			
			PeerConnection.LocalSdpReadytoSend += sdpready;//CreateOffer()もしくはCreateAnswerで呼び出されるイベント
			PeerConnection.IceCandidateReadytoSend += icecandget;//タイミングはよくわからないけどとにかく呼び出されるイベント

			PeerConnection.DataChannelAdded += DataChannelAdded;//ただの御報せ様なので必要ない
			PeerConnection.DataChannelRemoved += DataChannelRemoved;//ただの御報せ様なので必要ない
			if (!sv)
			{
				PeerConnection.CreateOffer();
				this.Text = "通信相手にこの文字列をコピーして渡す";
			}
			else 
			{
				this.Text = "通信相手から文字列を受け取って右に入力する";
			}
		}
private void sdpready(SdpMessage s) 
		{
		 tex1 = s.Content + "(^v^)";//区切り文字と共に保管する
		}
private void icecandget(IceCandidate s) 
		{
			if (tex2 == "")
			{
				tex2 = s.SdpMid + "(^v^)"+ s.SdpMlineIndex + "(^v^)"+ s.Content;//区切り文字と共に保管する
			}
		}

```

今回はなにかしらのチャットアプリでこのSdpMessageをやり取りするので文章に改行が入っているとおかしくなりますし、そのままIPアドレスを送るのはなんか癪ですよね。そこでStringをbyte[]に変換してされに暗号化みたいなことをするといい感じです。最終的にはこういう感じにするといいんじゃないでしょうか。
![通信相手にこの文字列をコピーして渡す 2021_11_21 10_58_43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2295239/f4df8255-822c-97ed-85ce-ebef7fc0c599.png)
一応黒塗りにさせていただきました。この左側の文字を相手にDiscordか何かで渡したりすればよいわけですね。