よくよく考えて見れば、操作用のボタンは普通にUIButtonで追加すればいいだけのことでした。

左右のどちらかのボタンを押すと移動、同時に押すとジャンプします。この操作方法は「Shift」というiPhoneゲームのまねです。

ipmaori-20

maori-iphone_src1102b.zip(要XCode4)

要点に絞って簡単に説明しておきます。Box2DAppDelegate.mmのapplicationDidFinishLaunchingメソッド内でglView上に2つのボタンを追加します。

- (void)applicationDidFinishLaunching:(UIApplication *)application {
    [application setStatusBarHidden:true];

	[glView removeFromSuperview];	//NOUSE
	glView.animationInterval = 1.0 / 60.0;

	……(中略)……

    //ボタンを配置する
    btn_left = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    btn_left.frame = CGRectMake(0,180,120,120);
    [btn_left setOpaque: YES];
    [btn_left setAlpha:0.25f];
    [btn_left addTarget:self action:@selector(buttonDown:) forControlEvents:UIControlEventTouchDown];
    [btn_left addTarget:self action:@selector(buttonUp:) forControlEvents:UIControlEventTouchUpInside];
    [glView addSubview:btn_left];
    btn_right = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    btn_right.frame = CGRectMake(360,180,120,120);
    [btn_right setOpaque: YES];
    [btn_right setAlpha:0.25f];
    [btn_right addTarget:self action:@selector(buttonDown:) forControlEvents:UIControlEventTouchDown];
    [btn_right addTarget:self action:@selector(buttonUp:) forControlEvents:UIControlEventTouchUpInside];
    [glView addSubview:btn_right];
}

ボタンが押されたときと放されたときに、buttonDown、buttonUpというメソッドを呼び出します。その中ではDXInput.cppで定義しているSetJoypadInputStateという関数を呼び出し、ボタンの状態を記録します。

-(void)buttonDown:(UIButton*)button{
    //NSLog(@"butonDown\n");
    if(button == btn_left) SetJoypadInputState(PAD_INPUT_LEFT, true);
    if(button == btn_right) SetJoypadInputState(PAD_INPUT_RIGHT, true);
}
-(void)buttonUp:(UIButton *)button{
    //NSLog(@"buttonUp\n");
    if(button == btn_left) SetJoypadInputState(PAD_INPUT_LEFT, false);
    if(button == btn_right) SetJoypadInputState(PAD_INPUT_RIGHT, false);
}

DXInput.cppではボタンから受け取った入力情報を、ゲーム部分に渡すための関数を定義しています。

//ゲームから使用する関数
int GetJoypadInputState(){
	//左ボタンと右ボタン両方が押されているときBボタンが押されたと見なす
	if((g_joypadinputstate & PAD_INPUT_LEFT) && (g_joypadinputstate & PAD_INPUT_RIGHT)){
		g_joypadinputstate |= PAD_INPUT_B;
	} else {
		g_joypadinputstate &= ~PAD_INPUT_B;
	}
	if(g_joypadinputstate) printf("Get Key %d\n", g_joypadinputstate);
	return g_joypadinputstate;
}

//Viewから使用する関数
//ボタンのステートを設定a
void SetJoypadInputState(int buttonID, bool pressed){
	if(pressed){
		g_joypadinputstate |= buttonID;
		printf("SetState Key %d\n", buttonID);
	} else {
		g_joypadinputstate &= ~buttonID;
		printf("SetState Release Key %d\n", buttonID);
	}
}

このようにC++とObjective-Cを混ぜて使うときの注意点なのですが、拡張子がcppのソースコードにObjective-C用のヘッダファイルをインクルードするとエラーになります。その場合は拡張子をmmに変え、Objective-C++のソースコードとしてください。C/C++の文法だけで完結している場合や、C++用のヘッダファイルをmmのソースコードにインクルードする場合は大丈夫です。

ゲームの本体部分は、MyGameクラスとMaoriBrosクラスに分かれています。MyGameクラスはアクションゲームで共通して使う部分の定義、MaoriBrosクラスはゲーム固有の処理の定義を書くためのもので、iPhoneTest.h/mmで定義されているTestクラス→MyGameクラス→MaoriBrosクラスという継承関係になっています。

これらのクラスのソースコードファイルの拡張子は「mm」になっていますが、中身は99%C++です。一応、キャラクターを動かす部分のコードを見せておくと、こんな感じになっています。

void MaoriBros::MoveHero(){
	//キーチェック
	int key = GetJoypadInputState( );
	//printf("Main Key Check %d\n", key);
	//主人公の位置と移動ベクトル

	b2Vec2 pos = g_stage_m.hero.body->GetPosition();
	b2Vec2 vec = g_stage_m.hero.body->GetLinearVelocity();
	float angle = g_stage_m.hero.body->GetAngle();
	//左右移動
	if(key & PAD_INPUT_LEFT) {
		if(g_stage_m.isontheground == true){
			vec.x = -PHSX(HEROSPEED);
			g_stage_m.hero.body->SetLinearVelocity(vec);
		}
	}
	if(key & PAD_INPUT_RIGHT) {
		if(g_stage_m.isontheground == true){
			vec.x = PHSX(HEROSPEED);
			g_stage_m.hero.body->SetLinearVelocity(vec);
		}
	}
	//ジャンプ
	if(IsBKeyTrigger(key) == true){
		if(g_stage_m.isontheground == true){
			b2Vec2 impvec(0, -JUMPIMPACTY);
			g_stage_m.hero.body->ApplyLinearImpulse(impvec, pos);
			g_stage_m.isontheground = false;
		}
	}
	//転倒対策
	if(abs(angle) > b2_pi/24){
		g_stage_m.hero.body->SetAngularVelocity(-angle*3);
	}
	//ゲームオーバーチェック
	//if(IsOutScreen(pos.x, pos.y) == true) GoGameOver();
	if(g_stage_m.hero.damage > 0.0f) GoGameOver();
}

なるべく元のWindows向けC言語のソースコードを活かす方向で移植しているので、見ての通りほとんどC++です。

マップデータのテキストファイルを読み込部分だけはObjective-Cを使う必要があり、そこだけハイブリッドになっています(こちらに掲載されていたiOSでテキストファイルを読み込む方法の解説をほとんどそのまま利用しました)。NSStringクラスのstringWithContentsOfFileというクラスメソッドで読み込むのですね。文字列のクラスにファイル読み込みのメソッドがついているというのは、ちょっと珍しい気もします。

//マップの読み込み
int MaoriBros::LoadMapData(NSString *TextFileName){
	NSInteger line_index;
	NSString* file_data;	//読み込んだテキストファイルを記憶する
	NSArray* data_lines;	//1行ずつテキストを読み込む
	NSString* data_line;	//1行分の文字列

	NSError* is_error = nil;

	NSString* TextFilePath;

    TextFilePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:TextFileName];
	// TextFilePath で指定されたテキストファイルを UTF8 形式で開きます。
	file_data = [NSString stringWithContentsOfFile:TextFilePath encoding:NSUTF8StringEncoding error:&is_error];

	if (is_error != nil){
		// ファイルが開けなかった場合のエラー処理です。
		NSLog(@"%@", [is_error localizedDescription]);
		return -1;
	} else {
		// 改行ごとにファイルの内容を分割しています。
		data_lines = [file_data componentsSeparatedByString:@"\n"];

		// 存在する行の数だけ、繰り返し処理を行います。
		line_index = 0;
		while(line_index < data_lines.count-1)
		{
			const char *buf;	//テキスト読み込みバッファ
			//マップサイズと主人公初期位置読み込み
			//1行分のテキストを取得
			data_line = [data_lines objectAtIndex:line_index];
			buf = [data_line UTF8String];
			line_index++;
			float w, h, sx, sy;
			sscanf(buf, "%f, %f, %f, %f", &w, &h, &sx, &sy);
			g_stage.mapsize_w = w;
			g_stage.mapsize_h = h;

	……(後略)……

ゲームとしては細かいところで色々と動きがおかしいのですが(右上方向にしかジャンプできないとか、出現するはずの敵が出現しないとか、自動スクロールしないので手でドラッグするしかないとか)、それを直そうとするとゲーム部分(具体的にいえばMyGameクラスとMaoriBrosクラス)のソースコードをゼロから見直さなければダメそうです。Box2Dの最新版をiPhoneで動かすという目的は一応達成できているので、ここで一段落とさせていただきます。