Unity で Bluetooth 通信

ゲームを作るなら Unity を使うと楽という話はよく聞きます。 実際、ゲームエンジンというだけあって画面内の…

ゲームを作るなら Unity を使うと楽という話はよく聞きます。
実際、ゲームエンジンというだけあって画面内の空間に配置されたオブジェクトを容易に扱えるようになっています。
見栄えが良いものを簡単に作れるのもあってゲームでなくとも使用する場合もあるでしょう。

ただ、描画系に特化している Unity であっても、少し別のことをしようとすると途端に扱いが難しいようで
今回、描画関連は Unity で行いつつ Bluetooth 通信が必要になったので挑戦してみます。
 

やりたいこと

Android 端末と Windows 端末とを Unity で Bluetooth 通信を行う。
 

開発環境

Windows 8.1
Unity 5.3.5f1
Android Studio 2.2.1
 

実験環境

Android 5.0.2 (Lollipop)
Windows 8.1
 

備考

Windows をサーバー側、 Android をクライアント側として作成
予めペアリングされているものとする
 
 

Android の通信処理を実装

まずは、Androidの通信部分から見ていきましょう。
Unity と Android Studio の両方を使用するので少し手間がかかります。
手順は次の通りです。

  1. Unity で新しいプロジェクトを作成
  2. 空の GameObject を作成
  3. Assets に C# Script を追加
  4. Assets の C# Script を空の GameObject に割り当てる
  5. C# Script をごりごり書く
  6. Android Studio 用プロジェクトとして Export
  7. Android Studio で Import Project
  8. Bluetooth 通信処理をごりごり書く

 
下のコードは 3. で作成した C# Script です (アセット名を JavaCallScript としています)。

using UnityEngine;
using System.Collections;
using System.Runtime.InteropServices;
using System;

public class JavaCallScript : MonoBehaviour {

    private IntPtr BluetoothTestActivityClass;
    private IntPtr ptr_onClickButton;

    // Use this for initialization
    void Start () {
        AndroidJNI.AttachCurrentThread();
        IntPtr cls_Activity = AndroidJNI.FindClass("jp/co/supersoftware/unity/bluetooth/test/BluetoothTestActivity");
        Debug.Log("cls_Activity = " + cls_Activity);
        IntPtr fid_Activity = AndroidJNI.GetStaticFieldID(cls_Activity, "mBluetoothTestActivity", "Ljp/co/supersoftware/unity/bluetooth/test/BluetoothTestActivity;");
        IntPtr obj_Activity = AndroidJNI.GetStaticObjectField(cls_Activity, fid_Activity);
        BluetoothTestActivityClass = AndroidJNI.NewGlobalRef(obj_Activity);
        ptr_onClickButton = AndroidJNI.GetMethodID(cls_Activity, "onClickButton", "(I)V");
    }

    // Update is called once per frame
    void Update () {
    }

    void OnGUI () {
        bool leftTop = GUI.Button(new Rect( 48,  48, 468, 888), "ABCDEFG");
        bool rightTop = GUI.Button(new Rect(564,  48, 468, 888), "HIJKLMN");
        bool leftBottom = GUI.Button(new Rect( 48, 984, 468, 888), "OPQRSTU");
        bool rightBottom = GUI.Button(new Rect(564, 984, 468, 888), "VWXYZ");
        jvalue[] jargs = new jvalue[1];
        if (leftTop) {
            jargs[0].i = 0;
            AndroidJNI.CallVoidMethod(BluetoothTestActivityClass, ptr_onClickButton, jargs);
        }
        if (rightTop) {
            jargs[0].i = 1;
            AndroidJNI.CallVoidMethod(BluetoothTestActivityClass, ptr_onClickButton, jargs);
        }
        if (leftBottom) {
            jargs[0].i = 2;
            AndroidJNI.CallVoidMethod(BluetoothTestActivityClass, ptr_onClickButton, jargs);
        }
        if (rightBottom) {
            jargs[0].i = 3;
            AndroidJNI.CallVoidMethod(BluetoothTestActivityClass, ptr_onClickButton, jargs);
        }
    }

}

簡単に説明すると、 JNI 機能を利用して Java のメソッドを呼び出す、といった作りになります。
Start() では JNI を使う下準備をしています。
していることはリフレクションと同じで
Java 側で作成予定の BluetoothTestActivity のクラスを取得し、
それを利用してメンバ変数を取得し、これまた作成予定の onClickButton メソッドを取得するだけ。
あとは OnGUI() でテスト用のボタンを 4 つ作成し、
それが押下されたら Start() で取得した Java メソッドにボタン毎のIDを引数に入れて呼び出します。

ここまでできたら、パッケージ名を指定して Android Studio 用プロジェクトとしてエクスポートします。
今回はパッケージ名を jp.co.supersoftware.unity.bluetooth.test としています。
 
unity_android_export
 
Unity での操作はここまでで、今度は Android Studio を使います。
先ほどエクスポートしたプロジェクトを Import project (Eclipse ADT, Gradle, etc.) からインポートします。
そこに、先ほど追加予定だった Activity と通信用クラスを追加します。
 
BluetoothTestActivity.java

package jp.co.supersoftware.unity.bluetooth.test;

import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.bluetooth.BluetoothDevice;
import android.content.DialogInterface;
import android.os.Bundle;
import android.util.Log;

import java.util.Set;

public class BluetoothTestActivity extends UnityPlayerActivity {
    private final static int DEVICES_DIALOG = 1;
    private final static int ERROR_DIALOG = 2;

    private static BluetoothTestActivity mBluetoothTestActivity = null;

    private BluetoothTask bluetoothTask = new BluetoothTask(this);

    private ProgressDialog waitDialog;
    private String errorMessage = "";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBluetoothTestActivity = this;
    }

    @SuppressWarnings("deprecation")
    @Override
    protected void onResume() {
        super.onResume();
        // Bluetooth初期化
        bluetoothTask.init();
        // ペアリング済みデバイスの一覧を表示してユーザに選ばせる。
        showDialog(DEVICES_DIALOG);
    }

    @Override
    protected void onDestroy() {
        bluetoothTask.doClose();
        super.onDestroy();
        mBluetoothTestActivity = null;
    }

    /**
     * Unity 側から呼び出されるメソッド
     * 押下されたボタン毎に送信するメッセージを変える
     * @param position 押下されたボタンID
     */
    public void onClickButton(int position) {
        Log.d("BluetoothTestActivity", "position=" + position);
        String msg;
        switch (position) {
            case 0:
                msg = "ABCDEFG";
                break;
            case 1:
                msg = "HIJKLMN";
                break;
            case 2:
                msg = "OPQRSTU";
                break;
            case 3:
                msg = "VWXYZ";
                break;
            default:
                msg = "";
                break;
        }
        bluetoothTask.doSend(msg);
    }

    //----------------------------------------------------------------
    // 以下、ダイアログ関連
    @Override
    protected Dialog onCreateDialog(int id) {
        if (id == DEVICES_DIALOG) return createDevicesDialog();
        if (id == ERROR_DIALOG) return createErrorDialog();
        return null;
    }

    @SuppressWarnings("deprecation")
    @Override
    protected void onPrepareDialog(int id, Dialog dialog) {
        if (id == ERROR_DIALOG) {
            ((AlertDialog) dialog).setMessage(errorMessage);
        }
        super.onPrepareDialog(id, dialog);
    }

    public Dialog createDevicesDialog() {
        AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this);
        alertDialogBuilder.setTitle("Select device");

        // ペアリング済みデバイスをダイアログのリストに設定する。
        Set<BluetoothDevice> pairedDevices = bluetoothTask.getPairedDevices();
        final BluetoothDevice[] devices = pairedDevices.toArray(new BluetoothDevice[0]);
        String[] items = new String[devices.length];
        for (int i=0;i<devices.length;i++) {
            items[i] = devices[i].getName();
        }

        alertDialogBuilder.setItems(items, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.dismiss();
                // 選択されたデバイスを通知する。そのまま接続開始。
                bluetoothTask.doConnect(devices[which]);
            }
        });
        alertDialogBuilder.setCancelable(false);
        return alertDialogBuilder.create();
    }

    @SuppressWarnings("deprecation")
    public void errorDialog(String msg) {
        if (this.isFinishing()) return;
        this.errorMessage = msg;
        this.showDialog(ERROR_DIALOG);
    }

    public Dialog createErrorDialog() {
        AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this);
        alertDialogBuilder.setTitle("Error");
        alertDialogBuilder.setMessage("");
        alertDialogBuilder.setPositiveButton("Exit", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.dismiss();
                finish();
            }
        });
        return alertDialogBuilder.create();
    }

    public void showWaitDialog(String msg) {
        if (waitDialog == null) {
            waitDialog = new ProgressDialog(this);
        }
        waitDialog.setMessage(msg);
        waitDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
        waitDialog.show();
    }

    public void hideWaitDialog() {
        waitDialog.dismiss();
    }

}

 
BluetoothTask.java

package jp.co.supersoftware.unity.bluetooth.test;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.os.AsyncTask;
import android.util.Log;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Set;
import java.util.UUID;

public class BluetoothTask {
    private static final String TAG = "BluetoothTask";

    /**
     * Bluetooth 接続時の UUID (SPP プロファイル)
     */
    private static final UUID APP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");

    private BluetoothTestActivity activity;
    private BluetoothAdapter bluetoothAdapter;
    private BluetoothDevice bluetoothDevice = null;
    private BluetoothSocket bluetoothSocket;
    private InputStream btIn;
    private OutputStream btOut;

    public BluetoothTask(BluetoothTestActivity activity) {
        this.activity = activity;
    }

    /**
     * Bluetoothの初期化。
     */
    public void init() {
        // BTアダプタ取得。取れなければBT未実装デバイス。
        bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        if (bluetoothAdapter == null) {
            activity.errorDialog("This device is not implement Bluetooth.");
            return;
        }
        // BTが設定で有効になっているかチェック。
        if (!bluetoothAdapter.isEnabled()) {
            activity.errorDialog("This device is disabled Bluetooth.");
            return;
        }
    }
    /**
     * @return ペアリング済みのデバイス一覧を返す。デバイス選択ダイアログ用。
     */
    public Set<BluetoothDevice> getPairedDevices() {
        return bluetoothAdapter.getBondedDevices();
    }

    /**
     * 非同期で指定されたデバイスの接続を開始する。
     * - 選択ダイアログから選択されたデバイスを設定される。
     * @param device 選択デバイス
     */
    public void doConnect(BluetoothDevice device) {
        bluetoothDevice = device;
        try {
            bluetoothSocket = bluetoothDevice.createRfcommSocketToServiceRecord(APP_UUID);
            new ConnectTask().execute();
        } catch (IOException e) {
            Log.e(TAG,e.toString(),e);
            activity.errorDialog(e.toString());
        }
    }

    /**
     * 非同期でBluetoothの接続を閉じる。
     */
    public void doClose() {
        new CloseTask().execute();
    }

    /**
     * 非同期でメッセージの送受信を行う。
     * @param msg 送信メッセージ.
     */
    public void doSend(String msg) {
        new SendTask().execute(msg);
    }

    /**
     * Bluetoothと接続を開始する非同期タスク。
     * - 時間がかかる場合があるのでProcessDialogを表示する。
     * - 双方向のストリームを開くところまで。
     */
    private class ConnectTask extends AsyncTask<Void, Void, Object> {
        @Override
        protected void onPreExecute() {
            activity.showWaitDialog("Connect Bluetooth Device.");
        }

        @Override
        protected Object doInBackground(Void... params) {
            try {
                bluetoothSocket.connect();
                btIn = bluetoothSocket.getInputStream();
                btOut = bluetoothSocket.getOutputStream();
            } catch (Throwable t) {
                doClose();
                return t;
            }
            return null;
        }

        @Override
        protected void onPostExecute(Object result) {
            if (result instanceof Throwable) {
                Log.e(TAG,result.toString(),(Throwable)result);
                activity.errorDialog(result.toString());
            } else {
                activity.hideWaitDialog();
            }
        }
    }

    /**
     * Bluetoothと接続を終了する非同期タスク。
     */
    private class CloseTask extends AsyncTask<Void, Void, Object> {
        @Override
        protected Object doInBackground(Void... params) {
            try {
                try{btOut.close();}catch(Throwable t){/*ignore*/}
                try{btIn.close();}catch(Throwable t){/*ignore*/}
                bluetoothSocket.close();
            } catch (Throwable t) {
                return t;
            }
            return null;
        }

        @Override
        protected void onPostExecute(Object result) {
            if (result instanceof Throwable) {
                Log.e(TAG,result.toString(),(Throwable)result);
                activity.errorDialog(result.toString());
            }
        }
    }

    /**
     * サーバにメッセージの送信を行う非同期タスク。
     */
    private class SendTask extends AsyncTask<String, Void, Object> {
        @Override
        protected Object doInBackground(String... params) {
            try {
                btOut.write(params[0].getBytes());
                btOut.flush();
                return "";
            } catch (Throwable t) {
                doClose();
                return t;
            }
        }

        @Override
        protected void onPostExecute(Object result) {
            if (result instanceof Exception) {
                Log.e(TAG,result.toString(),(Throwable)result);
                activity.errorDialog(result.toString());
            }
        }
    }

}

 
マニフェストファイルに使用するパーミッションと Activity を忘れずに追加。
AndroidManifest.xml

    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

    <application
        中略...
        <activity
            android:name="jp.co.supersoftware.unity.bluetooth.test.UnityPlayerActivity"
            android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale"
            android:label="@string/app_name"
            android:launchMode="singleTask"
            android:screenOrientation="fullSensor">
            <!--<intent-filter>-->
            <!--<action android:name="android.intent.action.MAIN" />-->

            <!--<category android:name="android.intent.category.LAUNCHER" />-->
            <!--<category android:name="android.intent.category.LEANBACK_LAUNCHER" />-->
            <!--</intent-filter>-->
            <meta-data
                android:name="unityplayer.UnityActivity"
                android:value="true" />
        </activity>
        <activity android:name=".BluetoothTestActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

Android アプリはこれで完成。
 
 

Windows の通信処理を実装

Windows 側は Android ほど複雑ではないので基本的に Unity だけで完結します。
手順は次の通りです。

  1. Windows の設定
  2. Unity で新しいプロジェクトを作成
  3. 使用する API を .NET 2.0 にする
  4. 空の GameObject を作成
  5. Assets に C# Script を追加
  6. Assets の C# Script を空の GameObject に割り当てる
  7. C# Script をごりごり書く

 
Windows の場合少し特殊で、Bluetooth 通信という処理があるわけではなく
指定されたポートに対してシリアル通信をすることでそれを実現しています。
なので Bluetooth デバイスをポートに割り当てる作業が必要になります。
 
bluetooth
 
これで準備が完了したので Unity を操作します。

シリアル通信をする場合、 PlayerSettings で Api Compatibility Level を
.NET 2.0 Subset ではなく .NET 2.0 にする必要があるのでしておきます。

そして、 5. で作成した C# Script を編集します。

using UnityEngine;
using UnityEngine.UI;
using System;
using System.Collections;
using System.IO.Ports;
using System.Threading;

public class ConnectionScript : MonoBehaviour {

    private SerialPort serialPort = null;
    String portName = "COM3";   // 接続ポート
    int baudRate =  115200;             
    int readTimeOut = 100;                  
    int bufferSize = 32;
     
    bool programActive = true; 
    Thread thread;

    // 初期表示文字列
    private string message = "Windows Test";

    // Use this for initialization
    void Start () {
        try {
            serialPort = new SerialPort();
            serialPort.PortName = portName;
            serialPort.BaudRate = baudRate;
            serialPort.ReadTimeout = readTimeOut;   
            serialPort.Open();
        } catch (Exception e) {
            Debug.Log (e.Message);
        }
 
        // Execute a thread to manage the incoming BT data
        thread = new Thread(new ThreadStart(ProcessData));
        thread.Start();
    }

    // Update is called once per frame
    void Update () {
    }

    void OnGUI () {
        GUI.Button(new Rect(100, 100, 400, 400), message);
    }

    void ProcessData(){
        Byte[] buffer = new Byte[bufferSize];
        int bytesRead = 0;
        Debug.Log ("Thread started");
         
        while (programActive) {
            try {
                // 受信データ読み込み
                bytesRead = serialPort.Read (buffer, 0, bufferSize);
  
                if (bytesRead > 0) {
                    // バイトデータを文字列に変換
                    string text = System.Text.Encoding.UTF8.GetString(buffer);
                    message = text;
                    Array.Clear(buffer, 0, bufferSize);
                }
            } catch (TimeoutException) {
                // Do nothing, the loop will be reset
            }
        }
        Debug.Log ("Thread stopped");
    }

    public void OnDisable(){
        programActive = false;   
 
        if (serialPort != null && serialPort.IsOpen) 
            serialPort.Close ();
    }

}

今回接続先を COM3 としていますが、環境によって変わるので注意が必要です。
あとは Windows 用にビルドすれば Windows アプリは完成です。
 
 

動作確認

サーバーとして動作するWindows アプリを立ち上げます。
bluetooth_test_1
 
クライアントの Android アプリを実行します。
bluetooth_test_2
 
接続する機器を選択してメイン画面を表示します。
bluetooth_test_3
 
試しに右上のボタンをタップしてみましょう。
bluetooth_test_4
 
ボタンの文字が切り替わり、ちゃんと通信できているのが分かります。
 
 
Unity で描画以外のことをモリモリやりたい場合はプラットフォームごとに別で処理を作る必要があります。
Windows の場合 Bluetooth 通信は Unity だけでどうにかなりましたが
必要ならば DLL を作成してそれを呼び出す形を取らないといけなくなります。
Android も今回のように機能の部分は Android 側に投げることになりそうです。