Pixel Monster(ピクセルモンスター)開発記 part.2 – Bluetooth

●対戦機能を実現するために、2つのデバイス間でモンスター情報を交換する必要があります。
そのために、Bluetooth通信を利用します。

・GameKitフレームワークを用いて、Bluetoothでpeer-to-peer通信を行う。
・独自クラスをNSData型に変換するために、NSCodingプロトコルを実装する。

まず、Bluetoothを使うにはGameKitフレームワークを追加します。GameKitフレームワークについては、Apple公式のドキュメントがありますので、そちらをどうぞ。Game Kitプログラミングガイド

Blutoothでデータの送受信を行うには主にGKPeerPickerControlerとGKSessionオブジェクトを使います。

#import 
#import 
 
@interface BTViewController : UIViewController {
    GKPeerPickerController* _picker;
    GKSession* _session;
}

Bluetoothでデータの送受信をするために実装することを簡単にまとめると次の2つになります。
1. GKPeerPickerControllerでピアの検出、および接続を行う
– GKPeerPickerControllerのデリゲートメソッドを実装
2. GKSession でデータの送受信を行う
– GKSessionのデリゲートメソッドを実装

GKSessionのデリゲートを使用して独自のユーザインターフェイスを実装することもできますが、
Game Kitは検出および接続処理の標準的なユーザインターフェイスを提供していて、それがGKPeerPickerControllerです。

GKPeerPickerControllerでピアの検出、および接続を行う


・GKPeerPickerControllerをインスタンス化し、デリゲートを設定
・connectionTypesMaskを設定

connectionTypesMaskには許可するネットワークタイプを指定します。Bluetooth接続のGKPeerPickerConnectionTypeNearbyとインターネット接続のGKPeerPickerConnectionTypeOnlineがあり、デフォルトはBluetooth接続のみで、インターネット接続のみを指定することはできません。インターネット接続を指定する場合はBluetooth接続と共に指定する必要があります。

	_picker = [[GKPeerPickerController alloc] init];
	_picker.delegate = self;
	_picker.connectionTypesMask = GKPeerPickerConnectionTypeNearby;
	[_picker show];

また、インターネット接続を指定する場合は、次のデリゲートメソッドを実装する必要がありますが、今回はBluetooth接続のみ許可するので、実装する必要はないです。

- (void)peerPickerController:(GKPeerPickerController *)picker didSelectConnectionType:(GKPeerPickerConnectionType)type {
    if (type == GKPeerPickerConnectionTypeOnline) { 
        picker.delegate = nil; 
        [picker dismiss]; 
        [picker autorelease];
        // ここに独自のインターネットユーザインターフェイスを実装する
    }
}

●Bluetoothを用いた対戦

次に、ピアが見つかった時のデリゲートメソッドを実装します。

- (GKSession *)peerPickerController:(GKPeerPickerController *)picker sessionForConnectionType:(GKPeerPickerConnectionType)type
{
    UIDevice* device = [UIDevice currentDevice];
    GKSession* session = [[GKSession alloc] initWithSessionID:@"FiSProjectSessionID" displayName:device.name sessionMode:GKSessionModePeer];
    [session autorelease];
    return session;
}
UIDevice* device = [UIDevice currentDevice];

UIDeviceクラスはデバイスの様々な情報を取得、設定を行えます。

今回はピアの選択画面で表示される名前にデバイス名を設定するために使います。

displayName:device.name

とすることで同じセッションIDを持つデバイス名が表示することができます。

データの交換をする相手のデバイスを選択し、セッションが確立したときのデリゲートメソッドとキャンセル時のデリゲートメソッドを実装します。

- (void)peerPickerController:(GKPeerPickerController *)picker didConnectPeer:(NSString *)peerID toSession:(GKSession *)session
{
	self.session = session;
	session.delegate = self;
	[session setDataReceiveHandler:self withContext:nil];
	picker.delegate = nil;
	[picker dismiss];
	[picker autorelease];
}
- (void)peerPickerControllerDidCancel:(GKPeerPickerController *)picker
{
    // キャンセルしたときの処理	
	picker.delegate = nil;
	[picker autorelease];
        [self dismissModalViewControllerAnimated:NO];
}

2. GKSession でデータの送受信を行う

まず、セッションの状態が変化したときに呼び出されるデリゲートメソッドを実装します。

- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state
{	
    switch (state)
    {
	case GKPeerStateConnected:
            // 送信用のクリーチャーデータ生成
            _myCreature = [[BattleCreature alloc] initCreature:_creature];
            [self sendCreature :_myCreature];
	    break;
	case GKPeerStateDisconnected:
            if(_myCreature != nil)
            {
                [_myCreature release];
                _myCreature = nil;
            }
	    self.session = nil;	
            break;	
	default:
	    break;
    }
}

// モンスターデータを送信するメソッド
- (void)sendCreature:(BattleCreature*)creature
{
    NSData* data = [NSKeyedArchiver archivedDataWithRootObject:creature];
    NSError* error = nil;
    [session_ sendData:data
               toPeers:[NSArray arrayWithObject:self.peerID]
          withDataMode:GKSendDataReliable
                 error:&error];
}

データの送信は sendDataToAllPeers:withDataMode:error:メソッドを呼び出すことですべての接続済みピアへ、 または sendData:toPeers:withDataMode:error:メソッドを呼び出すことで一部のピアへデータを送信できます。

送信するデータはNSDataオブジェクトにカプセル化されるので、送信するデータはNSData型に変換する必要があり、

[NSKeyedArchiver archivedDataWithRootObject:creature]

でBattleCreatureクラスのオブジェクトをNSData型に変換していますが、
任意のクラスをNSData型に変換するためには、NSCodingプロトコルを実装する必要があります。任意のクラスをNSData型に変換したい場合は、プロトコルにNSCodingを追加し、encodeWithCoder:とinitWithCoder:を実装します。
そのためにBattleCreatureクラスに次のようにNSCodingプロトコルを実装します。

@interface BattleCreature : NSObject {
    int         _power;
    NSString*   _userName;
    NSString*   _name;
}
-(id)initWithCoder:(NSCoder *)decoder{
    self = [super init];
    
    if(self){
        _power = [decoder decodeIntForKey:@"_power"];
        _userName = [[decoder decodeObjectForKey:@"_userName"] retain];
        _name = [[decoder decodeObjectForKey:@"_name"] retain];
    }
    
    return self;
}
- (void)encodeWithCoder:(NSCoder *)coder{
    [coder encodeInt:_power forKey:@"_power"];
    [coder encodeObject:_userName forKey:@"_userName"];
    [coder encodeObject:_name forKey:@"_name"];
}

これでBattleCreatureクラスのオブジェクトをNSData型に変換が可能になりました。

データの受信は次のデリゲートメソッドを実装することで行います。
NSData型でデータを読み込み、それをBattleCreatureクラスのオブジェクトに変換することで、相手のモンスター情報を取得できます。

- (void) receiveData:(NSData *)data fromPeer:(NSString *)peer
           inSession: (GKSession *)session context:(void *)context 
{
    BattleCreature *enemy = [NSKeyedUnarchiver unarchiveObjectWithData:data];
}

これで、モンスター情報を交換できました〜。そしてバトル!