KLab若手エンジニアの これなぁに?

KLab株式会社の若手エンジニアによる技術ブログです。phpやRubyなどの汎用LLやJavaScript/actionScript等のクライアントサイドの内容、MySQL等のデータベース、その他フレームワークまで幅広く面白い情報を発信します。

iPhone/iPad

iPhoneをアナログゲームコントローラにしてみる(2)

こんにちは、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でターゲットのビルド設定を戻して試してみてください。
サンプルをダウンロード

iPhoneをアナログゲームコントローラにしてみる(1)

こんにちは、精神年齢は若手以下かもしれないponpoko1968です。 このたび、弊社ではiPhoneとiPadを連携させて、パーティなどで多人数で楽しめるゲームを開発し、見事IVS LaunchPadの第4位に輝いたのですが、似たようなスキームで、iPhoneをiPad上のリアルタイムゲームのリモートコントローラとして用いることが出来ないか試してみました。 今回は、iPhoneの加速度センサー機能を使って、デバイスの傾きを検出し、iPad上で動作するサーバに送信するアプリを作ってみました。 仕様としては、
  • 傾きで左右の方向決定
  • 4つのボタン(タッチ)で前進、後進、Aボタン、Bボタン
  • Game Kitをつかってサーバの検索と表示
  • サーバにコントローラの状態を送信してアプリをコントロール

テンプレートの生成

今回は単一画面のシンプルなアプリなので、「View based application」で作ります。 [caption id="" align="alignnone" width="677" caption="view-based application"]
view-based application


[/caption] 今回は「AnalogRemote」というアプリ名で作成します。 ドライビングゲームなど、傾き検出を用いるアプリでは、iPhoneをホームボタンが右に来るように横にして使うのが一般的らしいので、Info.plistの「initial interface orientations」という項目を「Landscape (right home button)」に設定することで、横画面で起動させます。


また、画面の向きを認識させるため下記のメソッドをオーバライドします。

// Override to allow orientations other than the default portrait orientation.
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {

// Return YES for supported orientations
return (interfaceOrientation == UIInterfaceOrientationLandscapeRight);
}

加速度センサーによる傾きの検出

UIAccelerometerの使い方

傾きの検出には、 UIAccelerometerというクラスを用います。このクラスは、 sharedAccelerometerというクラスメソッドを用いることでシングルトンインスタンスを得ます。 UIAccelerometerはUIAccelerometerDelegateというプロトコルを通じて対象オブジェクトにデバイスの傾きを伝えるため、対象オブジェクトはUIAccelerometerDelegateプロトコルをサポートする必要があります。 今回は、唯一のビューコントローラクラスである、 AnalogRemoteViewControllerに UIAccelerometerDelegateをサポートさせます。 AnalogRemoteViewController.hを編集して、

@interface TanksControllerViewController : UIViewController<UIAccelerometerDelegate> {

...

@end
とします。 次に、AnalogRemoteViewControllerインスタンスのビューのローディングが終了したときに呼ばれるメソッドloadで、 UIAccelerometerの設定を行います。

- (void)viewDidLoad {

[super viewDidLoad];

// 0.1秒間隔で状態を通知

[[UIAccelerometer sharedAccelerometer] setUpdateInterval:0.1];

// デリゲート通知を自分(このインスタンス)に向けさせる

[[UIAccelerometer sharedAccelerometer] setDelegate:self];

...

}
デリゲート通知を設定されたクラスは、下記のメソッド

- (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration
を実装することで、setUpdateIntervalで設定された時間間隔で、加速度センサーの状態が通知されます。

デバイスの傾きを検出する

加速度センサーはX,Y,Zの3軸をもちいてデバイスの状態を検知します。


下図のように、iPhoneを横に向けた状態では、地球から作用する重力、つまり下向きの力が作用するため、X軸が約−1の値を示します。


横に向けて、さらに傾けると、下図のように、X軸とY軸の両方に力が分散されます。傾きによって、X軸とY軸にかかる力の配分が変わるため、iPhoneの傾きを検出することができます。例えると、iPhoneの重心から重しを付けた糸を垂らしているイメージを思い浮かべて頂けるとわかりやすいかと思います。糸とiPhoneの角度を調べれば、iPhoneの傾きが求まると言うわけです。


加速度センサーの状態を表示する

デバッグ用に加速度センサーの状態をリアルタイムで見るために、画面に表示するようにします。 AnalogRemoteViewControllerのメンバにx,y,z軸の数値を表示するためのUILabelを追加します。

@interface AnalogRemoteViewController : UIViewController<UIAccelerometerDelegate> {

UILabel* x;

UILabel* y;

UILabel* z;

...

}

// Interface Builder連携のためプロパティとして定義

@property(nonatomic, retain) IBOutlet  UILabel* x;

@property(nonatomic, retain) IBOutlet  UILabel* y;

@property(nonatomic, retain) IBOutlet  UILabel* z;

...

@end
プロパティx,y,zのセッター・ゲッターを定義するため、 AnalogRemoteViewController.mに、下記のように追加します。

@implementation AnalogRemoteViewController

@synthesize x,y,z;
interface builderでビューにラベルを貼り付けて、UILabelと結びつけます。 デリゲートメソッド - (void) accelerometer:didAccelerate:を実装します。

- (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration {

x.text  = [NSString stringWithFormat:@"%10.2f",acceleration.x];

y.text  = [NSString stringWithFormat:@"%10.2f",acceleration.y];

z.text  = [NSString stringWithFormat:@"%10.2f",acceleration.z];

}

X軸とY軸の逆正接を求め、角度を割り出します。


  float _angle;

  if( fabs(acceleration.x) > fabs(acceleration.z)){
    _angle = atan2(acceleration.y,-acceleration.x);

  }else {
    _angle = atan2(acceleration.y,-acceleration.z);
  }

※今回のコードでは、iPhoneがY軸を中心として一定以上地面に水平に傾いている場合には、X軸とZ軸の角度を求めるようにしています。 これで、傾きを検出することが出来ました。次回は他のボタン操作の検知と、サーバ(iPad)との通信について説明します。 サンプルをダウンロード [続く]

iPadがやって来た ヤア!ヤア!ヤア!

こんにちは!takei-hでっす! 弊社社長の真田が、アメリカのお土産としてiPadを買ってきてくれました!! ありがとうございます!>真田社長


↑iPadに群がるKLab若手メンバー 無線LANは使ってはいけないということで、とりあえずメモ帳で遊んでみたりしました。 iPhoneとは動きの滑らかさが桁違いですね!画面も大きいし!いいなーiPad!!私もほしい! みんなでわいわいiPadを触る中、iPadは俺のものだ!といわんばかりに強引にiPadを奪いとり逃げようとする暴君yoshida-kが現れた!


↑iPadをyoshida-kに奪い取られ、涙目なamo-k みんなで仲良くiPadを使いましょう! KLabではiPad/iPhoneアプリ開発もやっていますー! iPad/iPhoneに興味があるエンジニアも大募集中!→エンジニア採用

iPhone/iPadでSVG表示

こんにちは、気持ちは若手のponpoko1968です。 iPhoneアプリを作っていて、ロゴやアイコンなどの画像を表示したい事って、多いですよね。そういった場合、ラスター画像データは簡単に表示できますが、画面を拡大させたときに、画像が荒れてしまいます。 そこで、拡大しても画像が荒れない、illustratorやInkScapeなどで作成されたストロークデータを使いたいと思いました。 ところが、ストロークデータのファイル形式のなかでも標準化されている「SVG」形式のファイルを使おうと思ったのですが、意外と目的に合ったライブラリが見つからなかったりします。 iPhoneではUIWebViewというWebKitをラップしたビュー部品が用意されており、WebKitには、「SVG」というストロークデータの画像を表示する機能が備わっています。 そこで、SVGの表示にUIWebViewが使えないか試してみました。 方針としては、画面全体を表すビューを作成し、その子ビューとしてUIWebViewを配置することにします。画面全体のビューのviewDidLoadに処理を加え、ビューがロードされた時点でSVG表示用のビューを生成します。

  NSData* image = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"Logo" ofType:@"svg"]];
まず、SVGファイルのデータをNSDataに読み込みます。

  UIWebView* logoView = [[UIWebView alloc] init];
  [logoView loadData:image MIMEType:@"image/svg+xml"  textEncodingName:@"utf-8"  baseURL:nil];
UIWebViewを作成し、SVGデータを設定します。これだけでSVGデータは表示可能なのですが、このままだとUIWebViewの背景色でUIWebViewの領域が白く塗りつぶされてしまいます。 screenshot-01

  logoView.opaque = NO;
  logoView.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.0];
の2行を追加すると、ロゴ画像の背景部分が透過して表示されます。 screenshot-02


  [self.view addSubview:logoView];

ロゴのUIWebViewをサブビューにして完了です。 ※いろいろ実験したのですが、残念なことに、このビューに対してCore Animationの機能を用いて拡大縮小アニメーション操作を加えても、最初にラスタライズされた画像イメージをそのまま操作対象としてしまい、キレイに表示されません。 UIWebViewにはstringByEvaluatingJavaScriptFromString:という、外部からJavaScript文字列を適用するメソッドが用意されているので、JavaScriptでUIWebView内部の画像を操作することで拡大縮小には対応出来そうです。 JavaScriptを有効にするにはUIWebViewの内容をhtmlドキュメントとして認識させる必要があるため、SVGの読み込みの部分を下記のように変更します。

  NSString *imagePath = [[NSBundle mainBundle] resourcePath];
  imagePath = [imagePath stringByReplacingOccurrencesOfString:@"/" withString:@"//"];
  imagePath = [imagePath stringByReplacingOccurrencesOfString:@" " withString:@"%20"];
  NSString *HTMLData = @"<img src="\"Logo.svg\""  />";
  NSString *urlString = [NSString stringWithFormat:@"file:/%@//",imagePath];
  NSLog(urlString);
  [logoView loadHTMLString:HTMLData baseURL:[NSURL URLWithString: urlString]];
 KLab若手エンジニアブログのフッター