Unityアプリで簡単ソケット通信

アプリボット ゲームプログラマの220Rnです。

ゲームを開発していて、例えばステージデータなどを頻繁に更新し、

アプリに反映させてテストプレイをしたい!などと思ったことはありませんか。

ありますよね。そのようなとき、サーバを立てるまでもないものの、

アプリとPCで通信してデータのやり取りをすることで作業の効率化を図りたいと思いました。

そこで、ソケット通信で自作ツールとUnityアプリを接続して

簡単なデータ通信を実装した方法について紹介します。

Unityアプリでソケット通信を始める

ソケット通信については調べたらたくさん情報が出てきますが、

Unityで実装している例はそこまで多くなかったのでimportすれば

すぐに使えるように作りました。

(セキュリティなどは無視しているので使用に関しては開発環境までに留めておくのが望ましいです)

検証環境

Unity 2017.2.0p3

クライアント:telnetでのテスト

コードサンプル

短いので全部貼ります。万が一使用する場合は自己責任でお願いします。

SocketServer.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using UnityEngine;

/*
 * SocketServer.cs
 * ソケット通信(サーバ)
 * Unityアプリ内にサーバを立ててメッセージの送受信を行う
 */
namespace Script.SocketServer
{
    public class SocketServer : MonoBehaviour {
        private TcpListener _listener;
        private readonly List<TcpClient> _clients = new List<TcpClient>();

        // ソケット接続準備、待機
        protected void Listen(string host, int port){
            Debug.Log("ipAddress:"+host+" port:"+port);
            var ip = IPAddress.Parse(host);
            _listener = new TcpListener(ip, port);
            _listener.Start();
            _listener.BeginAcceptSocket(DoAcceptTcpClientCallback, _listener);
        }

        // クライアントからの接続処理
        private void DoAcceptTcpClientCallback(IAsyncResult ar) {
            var listener = (TcpListener)ar.AsyncState;
            var client = listener.EndAcceptTcpClient(ar);
            _clients.Add(client);
            Debug.Log("Connect: "+client.Client.RemoteEndPoint);

            // 接続が確立したら次の人を受け付ける
            listener.BeginAcceptSocket(DoAcceptTcpClientCallback, listener);

            // 今接続した人とのネットワークストリームを取得
            var stream = client.GetStream();
            var reader = new StreamReader(stream,Encoding.UTF8);

            // 接続が切れるまで送受信を繰り返す
            while (client.Connected) {
                while (!reader.EndOfStream){
                    // 一行分の文字列を受け取る
                    var str = reader.ReadLine ();
                    OnMessage(str);
                }

                // クライアントの接続が切れたら
                if (client.Client.Poll(1000, SelectMode.SelectRead) && (client.Client.Available == 0)) {
                    Debug.Log("Disconnect: "+client.Client.RemoteEndPoint);
                    client.Close();
                    _clients.Remove(client);
                    break;
                }
            }
        }


        // メッセージ受信
        protected virtual void OnMessage(string msg){
            Debug.Log(msg);
        }

        // クライアントにメッセージ送信
        protected void SendMessageToClient(string msg){
            if (_clients.Count == 0){
                return;
            }
            var body = Encoding.UTF8.GetBytes(msg);

            // 全員に同じメッセージを送る
            foreach(var client in _clients){
                try{
                    var stream = client.GetStream();
                    stream.Write(body, 0, body.Length);
                }catch {
                    _clients.Remove(client);
                }
            }
        }

        // 終了処理
        protected virtual void OnApplicationQuit() {
            if (_listener == null){
                return;
            }

            if (_clients.Count != 0){
                foreach(var client in _clients){
                    client.Close();
                }
            }
            _listener.Stop();
        }
    }
}

SocketServerを継承して、送受信する部分を実装していきます。

ServerTest.cs
using UnityEngine;

/*
 * TestServer.cs
 * SocketServerを継承、開くポートを指定して、送受信したメッセージを具体的に処理する
 */
namespace Script.SocketServer
{
    public class ServerTest : SocketServer {
#pragma warning disable 0649
        // ポート指定(他で使用していないもの、使用されていたら手元の環境によって変更)
        [SerializeField]private int _port;
#pragma warning restore 0649

        private void Start(){
            // 接続中のIPアドレスを取得
            var ipAddress = Network.player.ipAddress;
            // 指定したポートを開く
            Listen(ipAddress, _port);

            // システムに接続情報をセット(表示用)
            MyViewer.Instance.SetIpAddressPort (ipAddress + ":" + _port);
        }

        // クライアントからメッセージ受信
        protected override void OnMessage(string msg){
            base.OnMessage(msg);

            // -------------------------------------------------------------
            // あとは送られてきたメッセージによって何かしたいことを書く
            // -------------------------------------------------------------

            // 今回は受信した整数値を表示用システムにセットする
            int num;
            // 整数値以外は何もしない
            if (int.TryParse (msg, out num)) {
                // 値をセットする
                MyViewer.Instance.SetNum (num);
                // クライアントに受領メッセージを返す
                SendMessageToClient ("Accept:"+ num+"\n");
            } else {
                // クライアントにエラーメッセージを返す
                SendMessageToClient ("Error\n");
            }
        }
    }
}

そしてサンプル用に作った、クライアントから受信した整数値から 日本酒の温度帯の呼び方

画面に表示するだけの素敵なコードを載せます。

(余談ですが、著者は部活動が盛んな弊社の日本酒映画部 の部長を務めております)

MyViewer.cs
using UnityEngine;
using UnityEngine.UI;

/*
 * MyViewer.cs
 * 受信したメッセージを元に情報の管理・UIへの表示などをする
 * 通信用の非同期スレッドから直接Unityのメインスレッド呼ぶとエラーになるので一枚噛ませている
 */
namespace Script
{
    public class MyViewer : MonoBehaviour {
        // 受信した値など集約用のシステム
        public static MyViewer Instance;

        private int _num = -9999;  // 安易な初期値
        private string _ipPort = "none"; // 接続先情報保持用

#pragma warning disable 0649
        // 画面表示用
        [SerializeField]private Text _ipportField;
        [SerializeField]private Text _textField;
#pragma warning restore 0649

        private void Awake(){
            Instance = this;
        }

        private void Update () {
            // 接続先表示
            _ipportField.text = _ipPort;

            // 初期値なら更新しない
            if(_num == -9999){
                return;
            }

            // 例)数字が送られてきたらその温度帯の燗酒の温度表現を表示する
            string str;
            if (_num > -5 && _num < 0) {
                str = "雪どけ";
            } else if (_num >= 0 && _num < 7) {
                str = "雪冷え";
            } else if (_num >= 7 && _num < 12) {
                str = "花冷え";
            } else if (_num >= 12 && _num < 17) {
                str = "涼冷え";
            } else if (_num >= 17 && _num < 30) {
                str = "冷や";
            } else if (_num >= 30 && _num < 35) {
                str = "日向燗";
            } else if (_num >= 35 && _num < 38) {
                str = "人肌燗";
            } else if (_num >= 38 && _num < 42) {
                str = "ぬる燗";
            } else if (_num >= 42 && _num < 48) {
                str = "上燗";
            } else if (_num >= 48 && _num < 53) {
                str = "熱燗";
            } else if (_num >= 53 && _num < 80) {
                str = "飛び切り燗";
            } else if (_num >= 80 && _num < 90) {
                // 玉川酒造のフィリップ・ハーパー杜氏が好きな温度で正式なものではない
                // 著者もハーパー氏に倣ってここまで上げて飲んでみるなどして楽しんでいる
                str = "ハーパー燗"; 
            } else {
                str = "オススメしない";
            }

            _textField.text = _num + "℃は...\n"+str;

        }

        // 受信した数値セット
        public void SetNum(int n){
            _num = n;
        }

        // 接続情報セット
        public void SetIpAddressPort(string ipport){
            _ipPort = ipport;
        }
    }
}

テスト用のシーンと上記コードのパッケージも貼っておきます。

こちらのunitypackageをダブルクリックで開けばインポートされます。

SocketSample

これらを準備してServerTestシーンを開きます。以下のような構成になっていれば大丈夫です。

8161cf07-7de3-7568-7be9-195a615d0073.png

それでは実行していきましょう!当然ですが、ネットワークには接続してくださいね。

b28bd4f0-4c42-5bad-2c71-982efbfefe0f.png

無事に起動できたら同一PCでも同一ネットワーク上に接続しているPCでもいいので、

ターミナルなどからtelnetを使用してテストします。画面上部に表示されたIP/Portを以下のように入力します。

(接続環境によって異なりますので注意)

telnet 10.13.3.63 11888

Enterを押して無事に接続されると以下のようになります。

Trying 10.13.9.30...

Connected to xxx-xxx.

Escape character is '^]'.

Unity上でもログでConnect: ~と出力されました。

切断する際は上記ログにも書いてありますが、Macでは

1. Control+]

2. 「quit」と入力して Enter

となります。

decda674-a9b7-9f01-edea-b110b508cb60.png

さて、ここまで来たらあとは整数値を入力して送信しましょう!

45

21b22c97-0033-fb68-f451-79a388f5de09.png

55

ef8d3fbf-4166-ccd1-41bb-dba125b0b388.png

サンプルアプリでは整数値以外はエラーとして返すようにしており、

画面の更新はしないようにしてます。

また、サンプルアプリではクライアントが複数の場合にも対応してます。

1台が接続完了した状態で他の端末から別途同時に接続でき、レスポンス(Accept: 数値)を

全クライアントに向けて送信します。サンプルでは、最後にサーバに送られた整数値に基づく

温度帯のメッセージをサーバとなっているアプリが表示してます。

もちろん、実機でも利用可能です。実機との通信が本記事の目的でもあります。

以下、実機テストの様子です。

a1d4a23c-524a-5c85-df96-b92baab46699.jpg

クライアントプログラムの実装

TCPClientを用いたサンプルは複数存在しているため、本記事では割愛します。

まとめ

本記事ではUnity内にTCPClient/TCPListenerを用いてソケットサーバを立てて

クライアントから送られてくるメッセージを処理する簡単なサンプルアプリを実装しました。

受信したメッセージの解釈をそれぞれの状況に合わせて変更すれば、

簡単に通信を実装できることを目標にしました。

例えば「update」という文字列が送られて来たら何らかの更新処理を走らせたり、

json形式のデータをそのまま受け取って保存するように変更しても使えます。

そのシーンに合わせたプロトコルを自由に設計してください。

新規ゲーム開発プロジェクトでは、冒頭で述べたようなステージデータを

リアルタイムにアプリへ反映させる開発環境の構築に上記通信を用いています。

※日本酒温度帯の閾値は著者がざっくりと定義したもので厳密なものではありませんのでご留意ください。

(本来の定義としては、「涼冷えは15℃前後」などとなってます)