GoogleのCardboardのようなスマートフォンを利用した簡易VR用のコントローラを作りました。筐体を両手で保持しながら指でゲームパッド的にボタンを押せるものです。通信はBluetooth(BLE)を使っています。
⚫️コントローラ側
ボタン(8個)の押されている状態を8bitで表して、それをBLEでスマートフォン(iPhone)に送信します。
BLEモジュールはRN4020を、CPUはLPC1114を使用しました。下記のサンプルコードはArduino的に書いたものです。
サンプルコード:
void loop() { i = 0; if(digitalRead(uArrow) == 0) i |= 0x01; if(digitalRead(dArrow) == 0) i |= 0x02; if(digitalRead(lArrow) == 0) i |= 0x04; if(digitalRead(rArrow) == 0) i |= 0x08; if(digitalRead(aBtn) == 0) i |= 0x10; if(digitalRead(bBtn) == 0) i |= 0x20; if(digitalRead(xBtn) == 0) i |= 0x40; if(digitalRead(yBtn) == 0) i |= 0x80; if(i != prevI) ble_write(i); prevI = i; }
⚫️スマホ側
スマホに表示するサンプルアプリはUnityで作ります。空間にCubeがひとつだけあってコントローラのボタンで視点(カメラ位置)を前後左右に移動させることにします。
(1) まずGoogle VR SDKをインポートします。Cubeがひとつあるプロジェクトを開いた状態で、ダウンロードしたGoogleVRForUnity.unitypackageをダブルクリックしてimportします。ProjectビューにGoogleVRがでてくるのでそのなかのPrefabs>GvrViewerMainをHierarchyビューにドラッグ&ドロップします。再生ボタンで実行すると両眼用の画面がでてくると思います。
(2) コントローラからのコマンドをカメラ位置に反映させるためにMainCameraにスクリプトを追加します。コマンドをcmdとして受け取ってその値によってカメラ位置を移動します。OnUpdateBle(string command)がBLEでコマンドを受信した時にiOS側から呼ばれるコールバック関数です。
using UnityEngine; using System; using System.Collections; public class CamController : MonoBehaviour { private int cmd; private float x_speed, z_speed; private bool IsUpdatingBle { get; set; } void Awake() { IsUpdatingBle = false; } // Use this for initialization void Start () { x_speed = 0.0f; z_speed = 0.0f; if (!IsUpdatingBle) { if (!BlePluginInterface.StartUpdatingBle (this.name)) { IsUpdatingBle = false; return; } } else { BlePluginInterface.StopUpdatingBle(); } } // Update is called once per frame void Update () { if ((cmd & 0x01) != 0) { //forward x_speed = 0.1f; } else if ((cmd & 0x02) != 0) { //back z_speed = -0.1f; } else if ((cmd & 0x04) != 0) { //left z_speed = 0.1f; } else if ((cmd & 0x08) != 0) { //right x_speed = -0.1f; } transform.Translate (x_speed, 0.0f, z_speed); } public void OnUpdateBle(string command) { char[] c = command.ToCharArray (); cmd = 16 * (c [1] - '0') + (c [2] - '0'); } }
(3) 次にコントローラとBLE通信するiOSソフト(Native Plugin)の部分です。Objective-cで記述して.mmという拡張子にします。
UnitySendMessage()のところでUnity側にコマンドを送信しています。extern "C"の部分はNative Plugin側の呼び出しインターフェイスで、Unity側のc#インターフェイスから呼ばれます。
// // BlePlugin.m // Unity-iPhone // #define SERVICE_UUID @"8d8987059e3b42a59637456f786cad8a" #define READ_NOTIFY_CHARACTERISTIC_UUID @"4b753e33faac44ed81a6dece7bb01283" #define WRITE_CHARACTERISTIC_UUID @"0add21c0de8b406391218f657b1c67a4" #import <Foundation/Foundation.h> #import <CoreBluetooth/CoreBluetooth.h> @interface BlePlugin : NSObject <CBCentralManagerDelegate, CBPeripheralDelegate> { NSString *callbackTarget; } + (BlePlugin *)sharedInstance; @property (nonatomic, strong) CBCentralManager *centralManager; @property (nonatomic, strong) CBPeripheral *peripheral; @property (nonatomic, strong) CBCharacteristic *writeCharacteristic; @property (nonatomic, strong) CBCharacteristic *readNotifyCharacteristic; @end @implementation BlePlugin static BlePlugin *sharedInstance = nil; + (id)sharedInstance { @synchronized(self) { if (sharedInstance == nil) { NSLog(@"sharedInstance"); sharedInstance = [[self alloc] init]; } } return sharedInstance; } #pragma mark - CBCentralManagerDelegate - (void)centralManagerDidUpdateState:(CBCentralManager *)central { NSLog(@"state:%ld", (long)central.state); [self.centralManager scanForPeripheralsWithServices:nil options:nil]; } - (void) centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI { NSLog(@"発見したBLEデバイス : %@", peripheral); if ([peripheral.name hasPrefix:@"RN4020"]) { self.peripheral = peripheral; [self.centralManager connectPeripheral:peripheral options:nil]; } } - (void) centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral { NSLog(@"接続成功!"); //[_scanBtn setTitleColor:[UIColor greenColor] forState:UIControlStateNormal]; //[self.centralManager stopScan]; peripheral.delegate = self; [peripheral discoverServices:nil]; } - (void) centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error { NSLog(@"接続失敗・・・"); } #pragma mark - CBPeripheralDelegate - (void) peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error { if (error) { NSLog(@"エラー:%@", error); return; } NSArray *services = peripheral.services; NSLog(@"%lu 個のサービスを発見!:%@", (unsigned long)services.count, services); for (CBService *service in services) { [peripheral discoverCharacteristics:nil forService:service]; } } - (void) peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error { if (error) { NSLog(@"エラー:%@", error); return; } NSArray *characteristics = service.characteristics; NSLog(@"%lu 個のキャラクタリスティックを発見!%@", (unsigned long)characteristics.count, characteristics); for (CBCharacteristic *characteristic in characteristics) { if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:READ_NOTIFY_CHARACTERISTIC_UUID]]) { self.readNotifyCharacteristic = characteristic; NSLog(@"READ_NOTIFY_CHARACTERISTIC_UUID を発見!"); [self.peripheral setNotifyValue:YES forCharacteristic:self.readNotifyCharacteristic]; } else if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:WRITE_CHARACTERISTIC_UUID]]) { self.writeCharacteristic = characteristic; NSLog(@"CHARA_WRITE_UUID を発見!"); } } } - (void) peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error { if (error) { NSLog(@"エラー:%@", error); return; } NSLog(@"読み出し成功! service uuid:%@, characteristic uuid:%@, value:%@", characteristic.service.UUID, characteristic.UUID, characteristic.value); if(self.readNotifyCharacteristic.isNotifying) { NSString *parameter = [NSString stringWithFormat:@"%@", characteristic.value]; UnitySendMessage([callbackTarget cStringUsingEncoding:NSUTF8StringEncoding], "OnUpdateBle", [parameter cStringUsingEncoding:NSUTF8StringEncoding]); }else{ } } - (void) peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error { if (error) { NSLog(@"エラー:%@", error); return; } NSLog(@"Notify状態更新成功! is Notifying:%d", characteristic.isNotifying); } - (BOOL)startUpdatingBle:(NSString *)newCallbackTarget { callbackTarget = newCallbackTarget; NSLog(@"startUpdatingBle"); self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil]; return YES; } - (void)stopUpdatingBle { } @end extern "C" { BOOL _startUpdatingBle(const char *callbackTarget) { BlePlugin *instance = [BlePlugin sharedInstance]; @synchronized(instance) { return [instance startUpdatingBle:[NSString stringWithUTF8String:callbackTarget]]; } } void _stopUpdatingBle() { BlePlugin *instance = [BlePlugin sharedInstance]; @synchronized(instance) { [instance stopUpdatingBle]; } } }
(4) 最後にUnity側のc#インターフェイスです。
using UnityEngine; using System.Collections; using System.Runtime.InteropServices; public class BlePluginInterface : MonoBehaviour { #if UNITY_IOS [DllImport("__Internal")] private static extern bool _startUpdatingBle(string callbackTarget); [DllImport("__Internal")] private static extern bool _stopUpdatingBle(); #endif public static bool StartUpdatingBle (string callbackTarget) { #if !UNITY_EDITOR return _startUpdatingBle(callbackTarget); #else return false; #endif } public static void StopUpdatingBle() { #if !UNITY_EDITOR _stopUpdatingBle(); #endif } }