こんにちは、ponpoko1968です。 前回の続きです。

ボタンを追加する

前回はコントローラの左右の動きを加速度センサーを用いて検知する部分を説明しました。 今回は「前進」「後進」「A」「B」ボタンを追加します。比較的ありがちな部分なのでさらっと説明します。 まず、AnalogRemoteViewControllerクラスにボタンを追加します。

@interface AnalogRemoteViewController : UIViewController {
...
  UIButton* buttonForward;
  UIButton* buttonBackward;
  UIButton* buttonA;
  UIButton* buttonB;
  NSArray* buttongs;
  unsigned char   buttonState;
}
...
@property(nonatomic, retain) IBOutlet  UIButton* buttonUp;
@property(nonatomic, retain) IBOutlet  UIButton* buttonDown;
@property(nonatomic, retain) IBOutlet  UIButton* buttonA;
@property(nonatomic, retain) IBOutlet  UIButton* buttonB;
ViewDidLoadメソッドが呼ばれるタイミングで、作ったボタンをNSArrayに格納し、コントローラの状態更新時に使います。

 buttons =  [NSArray arrayWithObjects:buttonForward,buttonBackward,buttonA,buttonB,nil];
  [buttons retain];
buttonStateはビット単位で各ボタンの押下状態を保持します。NSArrayへの格納順に、下位ビットから、「前進」「後進」「A」「B」の順に格納します。 このような処理を行う理由は、リアルタイムに、なおかつ極力正確に、リモートマシンへコントローラの状態を送信するためです。後ほど詳述します。 下図のように、InterfaceBuilderでビューにボタンを貼り付けて、 4つ全てのボタンの下記イベントを、
  • Touch Down  -- ボタンが押された
  • Touch Up Inside -- ボタンが離された(ボタン領域の内側で)
  • Touch Up Outside -- ボタンが離された(ボタン領域の外側で)
- (IBAction)respondToButton:(id)sender forEvent:(UIEvent*)event メソッドに反応させるようにします。 またこのとき、AnalogRemoteViewControllerクラスに定義した各ボタンオブジェクトと、Interface Builder上のGUIボタンを結びつけることを忘れないようにしてください。

コントローラの状態を更新する

ボタンにタッチされた際、離された際の動作を記述します。

- (IBAction)respondToButton:(id)sender forEvent:(UIEvent*)event {
  NSUInteger buttonId;
  // イベントの送信元オブジェクトを同定
  if( (buttonId = [buttons indexOfObject:sender]) != NSNotFound ){
  // イベントの種類を判別(押下か、離されたのか)
    UITouch* touch = [[event touchesForView:buttons[ buttonId ]] anyObject];

  // ボタン状態のビットフィールドを更新
    if( touch.phase == UITouchPhaseBegan ) { // 押下
      buttonState |= 1 << buttonId;
    }else if( touch.phase == UITouchPhaseEnded ) { // 離された
      buttonState &= ~(1 << buttonId);
    }
  }
}

サーバに接続する

ここからGame Kitを用いた通信の説明です。Game Kitは名前から連想されるようなゲームを作るためのライブラリではなく、無線LANもしくはBluetoothをもちいてiPhone/iPad同士で簡単にP2P通信が出来るようにするフレームワークです。 くわしくは、 Game Kitプログラミングガイドを参照して下さい。 今回は、GKPeerPickerという便利クラスをつかってみます。このクラスは通信相手を探し出してGUI表示し、相手との接続までをサポートする便利クラスです。 AnalogRemoteViewControllerの宣言部で、GKPeerPickerControllerDelegate,GKSessionDelegateプロトコルを追加します。また、通信セッション関連の2つのメンバを追加します。
@interface AnalogRemoteViewController : UIViewController {
...
  
  //  Game Kit関連
  GKPeerPickerController* picker;
  GKSession       *gameSession;
  NSString        *gamePeerId;
}

...
@property(nonatomic, retain) GKSession   *gameSession;
@property(nonatomic, copy)   NSString    *gamePeerId;
プロパティの宣言部で、gamePeerIdは代入時の動作として、「copy」を指定していることに注意してください。これは、通信セッションが切れてしまった場合に、gameSessionはリリースされてしまいますが、接続先を表すgamePeerId文字列をクラス側でコピーして保持しておくことで再接続を試みることに使う事を意図しています。 さらに、viewDidLoadメソッドの呼び出しに、

  picker = [[GKPeerPickerController alloc] init];
  picker.delegate = self;
  picker.connectionTypesMask = GKPeerPickerConnectionTypeNearby;
  [picker show];
ここで、"GKPeerPickerConnectionTypeNearby"と指定しているのは、Bluetoothによる接続を意味します。 この記事の執筆時点ではGKPeerPickerはBluetooth接続のみサポートしているようなので、無線LANを用いて複数のiPhone/iPadが参加するような比較的大がかりな通信を行いたい場合には、GKSessionDelegateでサポートされているメソッドを実装して、通信相手の選択などのGUIを作成する必要があります。 上記のコードのうち、 [picker show]の実行で下図のような画面が出ます。 次にGKPeerPickerからのデリゲートメソッドに対応するコードを記述します。 まず、これからはじめる通信セッションの識別情報を聞いてきます。

- (GKSession *)peerPickerController:(GKPeerPickerController *)picker sessionForConnectionType:(GKPeerPickerConnectionType)type { 
  GKSession *session = [[GKSession alloc] initWithSessionID:@"KLabRemoteSample" displayName:nil sessionMode: GKSessionModePeer]; 
  return [session autorelease];
}
initWithSessionID:”@"KLabRemoteSample"の部分で、セッションを識別する文字列を指定します。GameKitはここで指定した文字列と同じ文字列をつかって作成された通信セッションで待ち受けているサーバを探します。 上記コードはGameKitのサンプルどおりですが、最後の行で、[session autorelease]としています。コメントにもあるように、GKPicker側がsessionを保持するため、ユーザ側のクラスでは保持する必要がないことを意味します。この後、Game Kit通信セッションの確立後のデリゲートメソッドの引数としてユーザ側に渡されるため、このタイミングでsessionへの参照を保持する必要はあまりないでしょう。 サーバとの接続が確立すると、下記のメソッドが呼ばれます。


- (void)peerPickerController:(GKPeerPickerController *)_picker didConnectPeer:(NSString *)peerID toSession:(GKSession *)session { 
  self.gamePeerId = peerID;
  
  self.gameSession = session; 
  self.gameSession.delegate = self; 
  [self.gameSession setDataReceiveHandler:self withContext:NULL];
  
  // GKPeerPickerのダイアログを消去
  [picker dismiss];
  picker.delegate = nil;
  [picker autorelease];
}

データを送信する

コントローラの状態が変更されるタイミング、すなわちボタンの押下とリリース、加速度センサーの更新時にデータを送信するメソッドを実装します。Game Kitで送信するデータはNSDataクラスに格納してフレームワークに渡します。


- (void) sendControllerStatus {
  if( gamePeerId ){
    NSUInteger state[2];

    memcpy(state,&angleValue,sizeof(angleValue));
    state[0] = htonl(state[0]);
    state[1] = htonl( buttonState );
   
    NSData* data = [NSData dataWithBytes:state length:sizeof(state) ];
    NSError *error;
    NSArray* peers = [NSArray arrayWithObject:gamePeerId];
    [gameSession sendData:data toPeers:peers withDataMode: GKSendDataUnreliable error:&error];
ここで、withDataMode: GKSendDataUnreliableと指定しています。直訳すると「信頼性のない送信」という意味ですが、Game Kitから出力されるログの内容やマニュアルから推測すると、Game KitはBluetoothネットワーク上に構築されたIPネットワークを使用しているようで、結局の所UDPによる送信を行っているようです。UDPを使用した場合、その仕組み上、サーバ側がパケットを確実に受け取ることや、正しい順序でサーバにパケットの到着することが保証されません。そのかわり、通信にかかるコストが低いため、リアルタイム性は高まります。今回はコントローラの状態(傾き・ボタンの押下の有無)をほぼ定期的に、全て送信する事で、これらのメリット・デメリットに対応することにします。 次回はiPad上で動作するコントローラの状態を受信するサーバを作ってみます。
注)この記事の執筆中、筆者が開発に用いているiPhoneとxcodeをiOS4対応にバージョンアップしてしまいました。サンプルをiOS3.1.2で実行される方は、xcodeでターゲットのビルド設定を戻して試してみてください。
サンプルをダウンロード