5.1さらうどん

@giginetの技術ブログ。ゲーム開発、iOS開発、その他いろいろ

GameKitを用いて複数のiOSデバイス間でP2P通信

GlobalGameJameに向けて、多数のデバイスの通信について弄っておりました!

Bluetoothを用いて、近くのiOSバイスで通信を行って対戦ゲームを開発するのは、GameKitと呼ばれるフレームワークを利用することでわりと簡単に実装することができます。

その中でも、GKSessionを用いて、2台の端末を通信させるには資料が多く、実装も楽だったのですが、複数台通信については、日本語で読める資料がほぼなかったのでまとめてみました。

2台のデバイス間でP2P通信を行う

これは非常に楽で、GKPeerPickerControllerを利用すると、至れり尽くせりで勝手に接続してくれます。

GKPeerPickerController Class Reference


以下の解説が非常に良くまとまっているので、こちらを読めば大体実装できるかと。

複数間のデバイスP2P通信を行う

さて、こちらが本題。前述のGKPickerViewは2台までの通信しかサポートしていないので、複数台通信を行うには自前で実装してあげる必要があります。

また、こちらの方法で実装を行うと、全てのデバイス間で相互に通信させず、あるデバイスをホストとして扱い、その他のデバイスをクライアントとして接続させることができるので、こちらの方針で解説いたします。

基本的な流れ

基本的な流れとしては

  1. GKSessionを有効化し、通信可能な状態にする
  2. 他のPeerが利用可能になったことを検知し、接続申請を出す
  3. 申請を出された側は、接続申請を受けたことを検知し、接続許可を出す
  4. コネクションが張られるのでデータを送受信する

といった、よくある感じの流れです。

1. GKSessionを有効化し、通信可能な状態にする
    self.session = [[GKSession alloc] initWithSessionID:@"GKSessionTest" displayName:nil sessionMode:GKSessionModeServer];
    self.session.delegate = self; // Delegateを指定する
    [self.session setDataReceiveHandler:self withContext:nil]; // 受信データを受け取るオブジェクトを指定します
    self.session.available = YES; // 通信を待ち受ける状態にする

SessionIDは特定の通信を表す識別子です。同じSessionIDを持ったGKSession同士でしか通信が成されません。

displayNameは相手デバイスに表示される名称です。nilにすると勝手にデバイス名が入るようなのでnilでOKかと。

sessionModeは通信の種類で、以下のようになっています

GKSessionModeServer Clientのみを通信相手として検知します
GKSessionModeClient Serverのみを通信相手として検知します
GKSessionModePeer 両方を通信相手として検知します

今回は、ホストとクライアントで分かれて通信を通信を管理したいので、ホスト側をServer, クライアント側をClientにすると良いと思います。

setDataReceiveHandler
は、他のPeerからデータを受け取ったとき、reciveDataが呼ばれるオブジェクトを指定します。詳しくは後述。

2. 他のPeerが利用可能になったことを検知し、接続申請を出す

他の接続先デバイスをPeerと呼び、PeerIDと呼ばれる一意な文字列で管理されます。

Peerの接続状態が変更されると、GKSessionDelegateのdidChangeStateが呼ばれます。

- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state {
  switch (state) {
    case GKPeerStateAvailable:
    {
    // 他のPeerが利用可能になったとき
    [session connectToPeer:peerID withTimeout:5.0f]; // peerIDに対して接続を要求する
    break;
    }
    case GKPeerStateUnavailable:
    {
    // 他のPeerが利用できなくなったとき
    break;
    }
    case GKPeerStateConnecting:
    {
    // 他のPeerに接続申請中のとき
    break;
    }
    case GKPeerStateConnected:
    {
    // 他のPeerに正常に接続したとき
    break;
    }
  }
}

接続状態が変わったとき、対象のPeerのPeerIDがpeerID、新しい状態がstateに渡ってくるので、利用可能になった瞬間に、相手に接続要求を出しています。

3. 申請を出された側は接続申請を受けたことを検知し、接続許可を出す

これは簡単で、相手から通信の要請があったとき、GKSessionDelegateのdidReceiveConnectionRequestFromPeerが呼ばれるので、これで受け取ったpeerIDに対してAcceptを行ってください。

- (void)session:(GKSession *)session didReceiveConnectionRequestFromPeer:(NSString *)peerID {
  [session acceptConnectionFromPeer:peerID error:nil]; // peerIDからの接続要求を受ける
}
4. コネクションが張られるのでデータを送受信する

前の手順までを全てこなすと、双方のStateがGKPeerStateConnectedとなり、接続状態となります。

一度コネクションが張られてしまえば、あとはNSDataをやりとりできるので、(通信容量の許す限り)なんでもできます。

送信したいPeerIDをNSArrayに格納して以下のように送ればOKです。

送る側

    NSString *message = @"Hi, Bob!"; // 送信するテキスト
    NSData* data = [message dataUsingEncoding:NSUTF8StringEncoding]; // 送るデータを作成
    NSError *err = nil;
    [self.session sendData:data toPeers:[NSArray arrayWithObject:peerID] withDataMode:GKSendDataReliable error:&err]; // peerIDに送信する


第3引数のGKSendDataModeは

GKSendDataReliable 確実に届くまで何度も送信する
GKSendDataUnReliable 1度のみ送信を行う。正常に送信されたかは保証できない

といった感じのようです。大抵はReliableにしておけば大丈夫だと思います。


受け取る側は、他のPeerからデータを受け取るとreceiveDataと言うメソッドがコールバックで呼ばれます。

なぜか、このコールバックはProtocolで定義されていないみたいなので、自前で実装することになります。

先ほどsetDataReceiveHandlerでselfを指定しているので、selfのreceiveDataが呼び出されます。

受け取る側

- (void) receiveData:(NSData *)data fromPeer:(NSString *)peer inSession: (GKSession *)session context:(void *)context {
    NSString *received = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; // 受け取ったデータをUTF8でデコード
    NSLog(@"%@", received); // 出力する
}
参考

今、解説した一連の処理が、こちらで実装されていて、サンプルとしては大変わかりやすかったので是非ともご覧ください

shrtlist/GKSessionP2P · GitHub

公式のサンプルではGKTankというサンプルがオススメ

GKTank

まとめ

超簡単にBluetoothを使った通信が実装できますね!GameKit凄い!