2010年7月10日土曜日

iPhoneゲーム内で砲台を回転させてみよう

前回のエントリ、英語が苦手なかたは、cocos2dを用いてiPhone上で至極シンプルなゲーム作成チュートリアル(本ブログ和訳版)は予想以上の反響でビックリしたよ!
それで何人かのナイスガイはもっと色々とシリーズ化してくれって言ってきたんだ。

特に、どうやって砲台を発射する方向にどうやって回転させたら良いか分かる様なチュートリアルを教えてくれってのが多かったかな。確かにそれは様々なゲームで共通して必要な要素だと思う、特におれの大好きなジャンルのタワーディフェンスなんかでもそうだよね!

だからこのチュートリアルでは、それをどうやるかをカバーしつつ、くわえてどうやって単純なゲームの中で砲台を回転させるかも説明するよ。ジェイソンとロバートにはこのチュートリアルを教えてくれたことを感謝するよ!



Getting Set Up

もし君が前回のチュートリアルを終えているなら、そのプロジェクトを引き続き使ってもらって構わないよ。そうじゃない場合は、ここからダウンロードして、今から始めよう。
次に、新しいプレイヤーの画像砲弾の画像をダウンロードして、それらをプロジェクトに加えよう。で、Player.pngProjectile.pngを消しちゃおう。でもって、それらのspriteを読み込むためのコードを以下のように修正しよう。

/* In the init method */
CCSprite *player =[CCSprite spriteWithFile:@"Player2.png"];
/* In the ccTouchesEnded method */
CCSprite *projectile =[CCSprite spriteWithFile:@"Projectile2.png"];


今回は、両附とも特に幅も高さもspriteに指定はしてないよね。cocos2dが代わりに勝手にやってくれてる。コンパイルして実行してみよう、そうしたら全てがちゃんと表示されて砲台が弾を撃ってるのが分かるかな?あまり自然に見えないのは、砲台が撃つ方向を向いていないからだよね、だから直しちゃおう!

Rotating To Shoot
といいたいところなんだけどその前に、プレイヤーSpriteを回転させるために、何かしらのそれに対する参照が必要になってくるよね。HelloWorldScene.hを開いてHelloWorldレイヤークラスのメンバ変数の以下の部分を修正しよう。

CCSprite *_player;

そうしたら次に以下のようにlayerplayerオブジェクトを加えるために、initメソッド内のコードを加えよう。

_player =[[CCSprite spriteWithFile:@"Player2.png"] retain];
_player.position = ccp(_player.contentSize.width/2, winSize.height/2);
[self addChild:_player];

最後にメモリを掃除するためのコードをdeallocメソッド内に書き足そう。

[_player release];
_player =nil;

オーケー、それじゃこれでplayerオブジェクトから参照することができそうだね。
回転させてみよう!回転させるために、まず始めにそれを回転させるための角度を計算するのが必要になってくるね。計算するために、高校数学の三角法を思い出そう。
SOH, CAH, TOAって暗記用語覚えてるかい?それらが思い出すのに役に立つはずだぜ、たとえばタンジェント(Tangent)は求めたい角度の真っ正面(Opposite)を隣(Adjacent)ので割る。って具合だ。写真を貼るから確認してくれ。

上記の通り、回転させたい角度はyxで割った値のアークタンジェントを求めれば良いってことになるよね。しかし、心に留めとかなきゃいけないことが2つある。一つ目、arctangent(offY / offX)を計算する時に、その結果はラジアンで返されるってことだ。cocos2dは普通の角度を扱うのに対してね。だけど幸運な事に、cocos2dでは簡単に変換してくれる関数があるから心配は無用さ。二つ目、おれたちは普通角度っていったら正の角度しか考慮しないよな、cocos2dの回転は正負両方あってしかも正の値は時計回り(反時計周りじゃないぞ)なんだ。まぁ絵をみてくれ。

だからちゃんとした方向にするために、得た結果に-1をかける必要があるんだ。たとえば、上記の画像で回転した角度に-1をかけたら-20°になったとする、そういう場合は反時計周りに20°進むってことになるよね。よしこれで説明は終わり、コードを書いていこう!以下のコードをccTouchesEndedの中に書いてくれ。

/* Determine angle to face */ float angleRadians = atanf((float)offRealY /(float)offRealX);
float angleDegrees = CC_RADIANS_TO_DEGREES(angleRadians);
float cocosAngle =-1* angleDegrees;
_player.rotation = cocosAngle;

コンパイル&ラン!砲台が弾を撃つ方向に向いてくれるはずだぜ!

Rotate Then Shoot
これで大分良くなったんだけど、砲台が弾を撃つ方向に急に回転しちゃっててクールじゃないし、もうちょっと滑らかに動いた方が良いよね。そこを直そう、だけどちょっとしたことで済むから心配しないでね。まずはじめに、HelloWorldScene.hを開いて、以下のコードをメンバ変数に加えよう。

CCSprite *_nextProjectile;

そうしたらccTouchesEndedを修正したり、finishShootという名の新しいメソッドを以下のように追加しよう!

-(void)ccTouchesEnded:(NSSet*)touches withEvent:(UIEvent *)event {
    if(_nextProjectile !=nil)return;
    /* Choose one of the touches to work with */
    UITouch *touch =[touches anyObject];
    CGPoint location =[touch locationInView:[touch view]];
    location =[[CCDirector sharedDirector] convertToGL:location];
    /* Set up initial location of projectile */
    CGSize winSize =[[CCDirector sharedDirector] winSize];
    _nextProjectile =[[CCSprite spriteWithFile:@"Projectile2.png"] retain];
    _nextProjectile.position = ccp(20, winSize.height/2);
    /* Determine offset of location to projectile */
    int offX = location.x - _nextProjectile.position.x;
    int offY = location.y - _nextProjectile.position.y;
    /* Bail out if we are shooting down or backwards */
    if(offX <=0)return;
    /* Play a sound! */
    [[SimpleAudioEngine sharedEngine] playEffect:@"pew-pew-lei.caf"];
    /* Determine where we wish to shoot the projectile to */
    int realX = winSize.width +(_nextProjectile.contentSize.width/2);
    float ratio =(float) offY /(float) offX;
    int realY =(realX * ratio)+ _nextProjectile.position.y;
    CGPoint realDest = ccp(realX, realY);
    /* Determine the length of how far we're shooting */
    int offRealX = realX - _nextProjectile.position.x;
    int offRealY = realY - _nextProjectile.position.y;
    float length = sqrtf((offRealX*offRealX)+(offRealY*offRealY));
    float velocity =480/1; // 480pixels/1sec
    float realMoveDuration = length/velocity;
    /* Determine angle to face */
    float angleRadians = atanf((float)offRealY /(float)offRealX);
    float angleDegrees = CC_RADIANS_TO_DEGREES(angleRadians);
    float cocosAngle =-1* angleDegrees;
    float rotateSpeed =0.5/ M_PI; // Would take 0.5 seconds to rotate 0.5 radians, or half a circle
    float rotateDuration =fabs(angleRadians * rotateSpeed);    
    [_player runAction:[CCSequence actions:
      [CCRotateTo actionWithDuration:rotateDuration angle:cocosAngle],
      [CCCallFunc actionWithTarget:self selector:@selector(finishShoot)],
      nil]];
    /* Move projectile to actual endpoint */
    [_nextProjectile runAction:[CCSequence actions:
      [CCMoveTo actionWithDuration:realMoveDuration position:realDest],
      [CCCallFuncN actionWithTarget:self selector:@selector(spriteMoveFinished:)],
      nil]];
    /* Add to projectiles array */
    _nextProjectile.tag =2;
}-(void)finishShoot {
    /* Ok to add now - we've finished rotation! */
    [self addChild:_nextProjectile];
    [_projectiles addObject:_nextProjectile];
    /* Release */
    [_nextProjectile release];
    _nextProjectile =nil;
}

たくさんコードがあって良く分からなくなったかもしれないね、だけど大した変更は加えてないはずだよ。ちょっとしたリファクタリングだけさ。以下に変更点を書いておこう。
-関数の始めのほうで、もしnextProjectileに値があったら返ってもらうようにしたよ。だってそれはまだshootingの処理にいることになるからね。
-sceneに加えるまえに、projectileっていう名前のローカルオブジェクトを使う。このバージョンではnextProjectileというメンバ変数をつくっている、けれどそれはまだ追加しないよ。
-砲台を回転させる時のスピードを決めるために0.5秒間の回転角度をもとに定義したよ。円一周は2piだってことを覚えといてね。
-だから正確な角度がどれくらいなのかを計算するために、そのスピードで動いてるラジアン度をかけているよ。
-そうしたら砲台を正しい角度に回転させるアクションシーケンスをスタートだ。そこではじめて弾がsceneに追加される。

よーし、じゃあ発射してみよう!コンパイル&ラン、砲台がスムーズに動いてるよね!

What's Next ? 
これで終わり。今回のコードはここに置いてあるよ。次のチュートリアルシリーズharder monsters and more levels(なるべく早く邦訳したい)にしよう!

cocos2d for iPhoneで簡単なゲーム作成チュートリアル


はじめに
ここ1,2ヶ月、cocos2dというフレームワークでゲームの開発をしています。
ただあまり日本語のドキュメントが整っておらず、英語のwikiやウェブサイトを色々巡回していました。
なかなかversionなどの関係でクラス名が変わっていて、つまらない所でハマったりなど、無駄な時間がかかったりしてしまいました。
そんななかとても良い入門記事を見つけ、内容的に飛び抜けて分かりやすく素晴らしい記事だったので、つたない英語で翻訳させてもらいました。

ボリューム的にはかなり多いのですが、「ゲーム制作をしてみたい」、という気持ちだけあればこなせるように設計されているチュートリアルなので興味がある方は是非最後まで読んで欲しいです。




How To Make A Simple iPhone Game With Cocos2D Tutorial

忍者ゴーイングピューピュー!



cocos2dはあなたがiPhoneでゲームを作る際に、時間の浪費を最小限にしてくれるパワフルなライブラリ群だよ。
スプライト、クールな画像効果、アニメーション、物理エンジン、サウンドエンジン、もっともっと!

おれはcocos2dを学び始めたばかりで、そこにはcocos2dをこれからはじめるのに適した、たくさんの実用的なチュートリアルがあると思ってたんだけど、おれは自分が望んでいた「超シンプル、だけどアニメーションだったり衝突だったり音楽だったりを応用的なものは使わずにできるサンプル」を見つけられなかった。
おれは自分自身で簡単なゲームを最終的に作り、そしてその経験をもとに、これからの新参者にむけてチュートリアルを作ろうと考えた。
このチュートリアルはcocos2dを使ってiPhoneの簡単なゲームを創る手順を、始めから終わりまで、付き添って教えてくれると思うよ。チュートリアルに沿いながら、もしくはいっきにこの記事の終わりにあるサンプルプロジェクトに取りかかっても良い。どっちでも良い。どちらにしろ、そこには忍者がいると思う。


Downloading and Installing cocos2d

cocos2d the Cocos2d Google Code page からダウンロードできるはず。このブログの投稿時点では、最新バージョンは0.99.0-final このバージョンがこのチュートリアルで用いるやつだよ。

コードを持ってきたら、おそらくあなたは便利なプロジェクトのテンプレートが欲しくなるよね。Terminalを開いてダウンロードしたcocos2dをディレクトリにいき、以下のコマンドを打ち込んでくれ。

./install_template.sh

覚えておいて欲しいんだけど、もしXCodeが標準的ではないディレクトリにインストールされているなら、オプションとしてさきほどのインストールスクリプトを持たせることができる。(あなたのマシーンに異なるバージョンのSDKを持たせていたりするなら必要だからね)


Hello cocos2d!

じゃあたった今インストールしたcocos2dのテンプレートを使って、簡単なHelloWorldプロジェクトを作ってみよう。
XCodeを立ち上げて、「cocos2d-0.99.0 Application template」を選択して新しくcocos2dプロジェクトを作ろう。
そしてプロジェクトの名前は「Cocos2DSimpleGame」にしてください。

じゃあまずはビルドしてみよう、オールグリーンなはずだ。こんな画面が出るはず。

cocos2dは「scenes」というコンセプトで構成されてる。ゲームにとっての、レベルだったりスクリーンといった概念に近いと思う。色んなsceneを持つことになるだろう。たとえば、メニュー画面だったり、他にもゲームのメインのアクション画面だったり、最後にはゲームオーバー画面を持つべきだね。それらのsceneの中には、たくさんのlayersPhotoshopのそれと同じようなもの)を持ち、そのlayersspritelabelmenuなどその他たくさんのnodeを含むことになります。そしてそれらnodeは他のnodeを同じように含むことも可能だよ。(spriteがその中に子spriteを持つなんてことも可能)

サンプルプロジェクトをみたならば、そこには「HelloWorldScene」というsceneが一つしか無いことに気が付くでよね。おれたちはこれからそこに主なゲームプレイを加えていくよ。そのファイルを開けてみると、すぐにinitメソッドの中に「Hello World」というlabelsceneに加えている事が分かるはず。
ではこれから、それを取り出してその代わりにspriteを加えてみよう。

Adding A Sprite

spriteを加える前に、それに使うための画像が必要だよね。まぁ別に自分で作ってくれても良いんだけど、俺の愛しい奥さんが創ったこの画像を作ってくれても良いんだよ。 a Player image, a Projectile image, a Target Image.
画像はゲットできたかな?できたらそれらをドラッグしてXCodeのなかの「Resources」フォルダに落とそう。「Copy items into destination group's folder(if needed)」にチェックするのも忘れないように。
これで俺たちは画像を手に入れたことになる、次にプレイヤーを配置したいところをおれたちは指し示さないといけないんだ。
覚えておいて欲しいんだけど、cocos2dでは左下の座標が(0, 0)で、xyの値はそれぞれ右に、上にあがることで上昇する。今回のプロジェクトはランドスケープだから最終的に右上の座標は(480, 320)になるよな。
もう一つ覚えておいて欲しいのは、デフォルトではオブジェクトの位置をセットするときに、そのポジションてのは俺たちがこれから加えようとしてるspriteの中心の座標とリンクしてるって事なんだ。だからもし、おれたちがプレイヤーのspriteを左端の真ん中ににそろえて置こうとした場合以下のようにしなくちゃならない。
xのポジションは [player sprite's width] / 2
yのポジションは [window height] / 2
って具合だ。下に画像を置いとくから、それを参考にすると良いかもね。


じゃあそれをコードにしてみよう。Classesフォルダを開いてHelloWorldScene.mをクリックし、initメソッドを以下のと置き換えちゃおう!

-(id) init
{
  if((self=[super init])){
    CGSize winSize =[[CCDirector sharedDirector] winSize];
    CCSprite *player =[CCSprite spriteWithFile:@"Player.png" 
      rect:CGRectMake(0, 0, 27, 40)];
    player.position = ccp(player.contentSize.width/2, winSize.height/2);
    [self addChild:player];                
  }
  return self;
}


コンパイルして動かしてみよう、spriteは元気そうに表示されてるはずだ。だけどbackgroundがデフォルトの黒のままだよね。この画像だと白の方が良さそうだ。簡単な方法の一つとして、backgroundlayerCCColoredLayerクラスを使って変えるってのができる。じゃあまた書いてみよう。HelloWorldScene.hを開いてHelloWorldインターフェースの定義を以下のようにしてみよう。

@interface HelloWorld : CCColorLayer

そしたらHelloWorldScene.mをクリックして、ちょっとだけコードを追加する。こんな感じでね。

if((self=[super initWithColor:ccc4(255,255,255,255)])){

もう一回コンパイルして動かしてみよう、今度は白い背景のうえに君のspriteがいるはずだ。
おっと、おれたちの忍者がActionしたそうにしてるぜ!?



Moving Targets

今度はもういくつかおれたちの忍者と戦う相手としていくつかscenetargetを追加してみよう。
もうちょっと面白くするために、動くtargetが欲しいよね、そうじゃないとさっきと全然変わらないじゃん!
だから右の方にtargetをつくって、彼らに左に動くように伝えるためのactionをセットアップしよう。

以下のメソッドを今すぐinitメソッドの前に加えてくれ。

-(void)addTarget {
  CCSprite *target =[CCSprite spriteWithFile:@"Target.png" 
    rect:CGRectMake(0, 0, 27, 40)]; 
  /* Determine where to spawn the target along the Y axis */
  CGSize winSize =[[CCDirector sharedDirector] winSize];
  int minY = target.contentSize.height/2;
  int maxY = winSize.height - target.contentSize.height/2;
  int rangeY = maxY - minY;
  int actualY =(arc4random()% rangeY)+ minY;
  /* Create the target slightly off-screen along the right edge, and along a random position along the Y axis as calculated above*/
  target.position = ccp(winSize.width +(target.contentSize.width/2), actualY);
  [self addChild:target];
  /* Determine speed of the target */
  int minDuration =2.0;
  int maxDuration =4.0;
  int rangeDuration = maxDuration - minDuration;
  int actualDuration =(arc4random()% rangeDuration)+ minDuration;
  /* Create the actions */
  id actionMove =[CCMoveTo actionWithDuration:actualDuration 
    position:ccp(-target.contentSize.width/2, actualY)];
  id actionMoveDone =[CCCallFuncN actionWithTarget:self 
    selector:@selector(spriteMoveFinished:)];
  [target runAction:[CCSequence actions:actionMove, actionMoveDone, nil]];
}

できるだけ分かるやすくするために冗長な表現もコメントアウトとして残した。
最初の部分では結構前に話した「どこにオブジェクトを置いて~」ってのを同じように計算してる。
ここでの新しい要素はactionの追加だろうな。cocos2dはたくさんの手軽でエクストリームなactionが組み込まれてて、それをspriteのアニメーションに使ったりできる。そのアニメーションってのも色々あって、移動だったりジャンプだったりフェードだったりアニメーションだったり、いっぱいある。ここでは三つのアクションをtargetに対して使う事にする。

CCMoveTo
これはオブジェクトを直接off-screenから左に動かすときに使う。動く間隔を指定する事もできて、今回はランダムに24秒かかるようにしてる。
CCCallFuncN
これは何かのアクションが終わった時に呼ばれるコールバック関数。今回はspriteMoveFinishedって名前にしてるけど、まだ書き終わってない。あとでやる。
CCSequence
これはいくつかのアクションを繋げるときに使う。今回はCCMoveToを最初に使ってそれが終わった後にCCCallFuncNを呼び出すようにしてる。

では次にCCCallFuncNで呼び出してるコールバック関数を加えよう。これをaddTargetの前に足して欲しい。
|





-(void)spriteMoveFinished:(id)sender {
  CCSprite *sprite =(CCSprite *)sender;
  [self removeChild:sprite cleanup:YES];
}

この関数の目的はspritescreen外になったらsceneから取り除くためのものだ。これは画面外に出てったsprite達がどんどん増えてメモリリークしてしまうのを防ぐ上でめちゃくちゃ重要。他にももっと良い方法があって、spriteの再利用配列をつくったりするともっと良いんだけど、おれは初心者のチュートリアルには簡単な道しるべに成る方を選ぶよ。

じゃあ最後にもうひとつ。今の状態だとターゲットを創るたびにメソッドを呼ばなきゃならないよね。
これを面白くするために、ずーっと継続的に動かしておこう。これを実現するためにcocos2dではコールバック関数を継続的にスケジューリングして呼ぼう。以下のコードをinitメソッド内でreturnする前に加えよう。

[self schedule:@selector(gameLogic:) interval:1.0];

そして次にそのコールバック関数を簡単に定義しちゃおう

-(void)gameLogic:(ccTime)dt {
  [self addTarget];
}

これで終わりだ!コンパイルして実行してみよう、きみにも楽しそうにスクリーンを横切るtargetが見えるかい?

Shooting Projectiles
ここで、忍者はいくつかアクションをし始めたよね。じゃあ次はシューティングを加えよう!シューティングを加える方法はたくさんあると思うんだけど、このゲームではユーザーがスクリーンをタップしたら弾丸がプレイヤーからタップした方向へ発射する、というものにしよう。
初心者レベルに合わせるためにCCMoveToアクションを使いたい、けれどちょっとしか数学を使わなきゃならなそうだ。なんでかっていうと、CCMoveToは目的地が必要になってくるんだけど、それをタッチポイントにしちゃ駄目だよね、だってタッチポイントはあくまでプレイヤーから発射される弾丸の方向を決めるためだけのものだから。おれたちがしたいのは弾丸がタッチポイントを通り抜けて画面外まで出るまで動き続けることなんだ。

以下にその写真をのせるよ。


みて分かる通り、タッチポイントとプレイヤーx, y座標を使って小さな三角形があるよね。それを使って同じ比率で大きな三角形を創るんだ、何を基準にするかっていうと画面外にあたるところの座標だね。

よし、コードを書いてみよう。まず最初にlayerのタッチを有効にしないといけない。以下のコードをinitメソッドの中に加えよう。

self.isTouchEnabled =YES;

これでlayerのタッチが有効になった、タッチイベントのコールバック関数は知ってるよね。ってことで、タッチが完了したときに呼び出されるccTouchesEndedメソッドを加えよう。

-(void)ccTouchesEnded:(NSSet*)touches withEvent:(UIEvent *)event {
  /* Choose one of the touches to work with */
  UITouch *touch =[touches anyObject];
  CGPoint location =[touch locationInView:[touch view]];
  location =[[CCDirector sharedDirector] convertToGL:location];
  /* Set up initial location of projectile */
  CGSize winSize =[[CCDirector sharedDirector] winSize];
  CCSprite *projectile =[CCSprite spriteWithFile:@"Projectile.png" 
    rect:CGRectMake(0, 0, 20, 20)];
  projectile.position = ccp(20, winSize.height/2);
  /* Determine offset of location to projectile */
  int offX = location.x - projectile.position.x;
  int offY = location.y - projectile.position.y;
  /* Bail out if we are shooting down or backwards */
  if(offX <=0)return;
  /* Ok to add now - we've double checked position */
  [self addChild:projectile];
  /* Determine where we wish to shoot the projectile to */
  int realX = winSize.width +(projectile.contentSize.width/2);
  float ratio =(float) offY /(float) offX;
  int realY =(realX * ratio)+ projectile.position.y;
  CGPoint realDest = ccp(realX, realY);
  /* Determine the length of how far we're shooting */
  int offRealX = realX - projectile.position.x;
  int offRealY = realY - projectile.position.y;
  float length = sqrtf((offRealX*offRealX)+(offRealY*offRealY));
  float velocity =480/1; /* 480pixels/1sec */
  float realMoveDuration = length/velocity;
  /* Move projectile to actual endpoint */
  [projectile runAction:[CCSequence actions:
    [CCMoveTo actionWithDuration:realMoveDuration position:realDest],
    [CCCallFuncN actionWithTarget:self selector:@selector(spriteMoveFinished:)],
    nil]];
}

最初の部分では作用してるタッチの中で一つ選んでるね、んで現在のviewで座標をとってきてる、それでその座標をconvertToGL関数を使ってゲーム内のレイアウト用の座標に変換してる。このゲームはランドスケープモードなので、この処理は特に重要だね。
次に弾丸spriteを読み込んで、普段通りポジションを初期化してる。そしたら「その弾丸がどこまで動いていくのか」を決めなきゃならない、さきほど述べた簡単なアルゴリズムをプレイヤーとタッチポイント間のベクトルに当てはめて求めよう。
ちなみにそのアルゴリズムは理想的なものじゃない、弾丸のx座標が画面外に出るまで動かせてしまっていて、たとえy座標がさきに画面外に出たとしても動き続けることになっている!他にも色々な効果的なやりかたはいくらでもある、たとえば画面外に出るまでの最短距離を計算したりね。だけど初心者向けのチュートリアルなんだからこれで良いんだよ。

最後にしなきゃいけないのは、動きの間隔を決める事だ。弾丸は定期的な比率で方向に関わらず撃たれるべきで、だからもう一回ちょっとした数学を考えなきゃならない。どのくらいの距離動いているかはピタゴラスの定理を使えば求められるよね。さっきの絵を思い出して欲しいんだけど、三角形の斜辺は他の垂直に交わっている2辺の和に等しいっていう法則がある。
もし距離があるなら、間隔をあけるために、速さに応じて分割しなければならない。理由は、「速さ=距離÷時間」別の言い方をすれば「時間=距離÷速さ」だからだ。
残りはさきほどtargetに対して行ったアクションの設定だ。コンパイル&ラン、さすればきみの忍者は襲いくる群れを一網打尽にできるだろう。


Collision Detection
これでどこにでも手裏剣を投げられるようになったね、けどおれたちの忍者が本当にしたいことって、相手を倒すことだよね。だから次は、手裏剣がtargetに当たった時に衝突を検知するためのコードを書こう。
実現する方法はcocos2dにはたくさんある、box2dchipmunkなどの物理エンジンライブラリを使ったりね。だけどシンプルにいきたいので、自分たちで衝突検知を実装しよう。
それを行うために、targetと手裏剣のトラッキングを行い続けることが必要になってくる。
以下を、HelloWorldSceneクラスの宣言のところに付け加えよう。

NSMutableArray*_targets;
NSMutableArray*_projectiles;

で、initメソッドのなかでこれらの配列を初期化しよう。

_targets =[[NSMutableArray alloc] init];
_projectiles =[[NSMutableArray alloc] init];


で、deallocメソッドの中でメモリを掃除するためのコードも書こう。

[_targets release];
_targets =nil;
[_projectiles release];
_projectiles =nil;


さぁ、新しいtargettarget配列に追加し、あとで使うタグをつけるためにaddTargetメソッドを修正しよう。

target.tag =1;
[_targets addObject:target];

そしてccTouchesEndedメソッドも同様にして手裏剣を配列に入れてタグをつけるコードを足そう。

projectile.tag =2;
[_projectiles addObject:projectile];

最後にspriteMoveFinishedメソッドに妥当なタグがついているspriteを除去するためのコードを追加しよう。

if(sprite.tag ==1){// target
  [_targets removeObject:sprite];
}elseif(sprite.tag ==2){// projectile
  [_projectiles removeObject:sprite];
}

コンパイル&ラン、全てがちゃんと動いてるはずだ。この時点では目に見えて変わってる点は無い。けれど既に衝突検知をするための準備は着々と進んでいる。

じゃあ以下のコードをHelloWorldSceneに追加しよう。

-(void)update:(ccTime)dt {
  NSMutableArray*projectilesToDelete =[[NSMutableArray alloc] init];
  for(CCSprite *projectile in _projectiles){
    CGRect projectileRect = CGRectMake(
      projectile.position.x -(projectile.contentSize.width/2), 
      projectile.position.y -(projectile.contentSize.height/2), 
      projectile.contentSize.width, 
      projectile.contentSize.height);
    NSMutableArray*targetsToDelete =[[NSMutableArray alloc] init];
    for(CCSprite *target in _targets){
      CGRect targetRect = CGRectMake(
        target.position.x -(target.contentSize.width/2), 
        target.position.y -(target.contentSize.height/2), 
        target.contentSize.width, 
        target.contentSize.height);
      if(CGRectIntersectsRect(projectileRect, targetRect)){
        [targetsToDelete addObject:target];                                
      }                                          
    }
    for(CCSprite *target in targetsToDelete){
      [_targets removeObject:target];
      [self removeChild:target cleanup:YES];                                                                     
    }
    if(targetsToDelete.count > 0){
      [projectilesToDelete addObject:projectile];
    }
    [targetsToDelete release];
  }
  for(CCSprite *projectile in projectilesToDelete){
    [_projectiles removeObject:projectile];
    [self removeChild:projectile cleanup:YES];
  }
  [projectilesToDelete release];
}

上記のコードはとっても分かりやすいね。弾丸とtargetをイテレートしてそれらの当たり判定と同じ長方形を作って、CGRectIntersectsRectを使って重なっているかを確かめている。もし何かみつかれば、それをsceneと配列から除く。toDeleteオブジェクトに追加しないと配列から除去されないから注意してね。何度も言うようだけど、これより最適な方法はいくらでもある、だけどシンプルな解決方法を使ってみんなに分かってもらいたいからこうしてるんだ。分かってくれ。
あといっこだけやることがあるんだ、このメソッドをできるだけ頻繁に実行するようスケジュールしなくちゃ。以下のコードをinitメソッド内に足そう。

[self schedule:@selector(update:)];

動かせば、弾丸がtargetとぶつかるとそれらが消えてなくなったでしょ!?

Finishing Touches

もうこれでかなり良い感じで動作する(エクストリームシンプルだけどね)のゲームができたね。だからいくつか音楽のエフェクトを入れて、簡単なゲームロジックも入れてみよう。

もし、おれの blog series on audio programming for the iPhone を読んでくれてるなら、いかにcocos2d開発者がゲーム内で音を鳴らすのが簡単か気付くはずだ。
まずBGMとシューティングサウンドをResourcesフォルダにドラッグして入れよう。フリー音源の cool background music I made awesome pew-pew sound effect から持ってきても良いし、もしくは自分のをつくっても良い。
そしたらHelloWorldScene.mに以下のコードを書こう。

#import "SimpleAudioEngine.h"

initメソッド内で、以下のようにしてBGMを鳴らしてみよう。

[[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"background-music-aac.caf"];

んでもって、ccTouchesEndedメソッド内でも以下のようにしてサウンドエフェクトを行おう。

[[SimpleAudioEngine sharedEngine] playEffect:@"pew-pew-lei.caf"];

さぁ、それじゃあ新しく"You Win""You Lose"を表示してくれるsceneを作ってみよう。Classesフォルダをクリックして「File/New File」、で「Objective-C class」を選択、サブクラスにNSObjectを選ぼう。Nextをクリックしてファイル名をGameOverSceneとして、「Also create GameOverScene.h」をチェック。
そしたらGameOverScene.hを以下のコードで置き換えよう。

#import "cocos2d.h"
@interface GameOverLayer : CCColorLayer {
  CCLabel *_label;
}@property(nonatomic, retain) CCLabel *label;
@end@interface GameOverScene : CCScene {
  GameOverLayer *_layer;
}@property(nonatomic, retain) GameOverLayer *layer;
@end

同様にしてGameOverScene.mも以下のコードと置き換える。

#import "GameOverScene.h"
#import "HelloWorldScene.h"
@implementation GameOverScene
@synthesize layer = _layer;
-(id)init {
  if((self =[super init])){
    self.layer =[GameOverLayer node];
    [self addChild:_layer];
  }
  return self;
}-(void)dealloc {
  [_layer release];
  _layer =nil;
  [super dealloc];
}@end@implementation GameOverLayer
@synthesize label = _label;
-(id) init
{
  if((self=[super initWithColor:ccc4(255,255,255,255)])){
    CGSize winSize =[[CCDirector sharedDirector] winSize];
    self.label =[CCLabel labelWithString:@"" fontName:@"Arial" fontSize:32];
    _label.color = ccc3(0,0,0);
    _label.position = ccp(winSize.width/2, winSize.height/2);
    [self addChild:_label];
    [self runAction:[CCSequence actions:
      [CCDelayTime actionWithDuration:3],
      [CCCallFunc actionWithTarget:self selector:@selector(gameOverDone)],
      nil]];
  }      
  return self;
}-(void)gameOverDone {
  [[CCDirector sharedDirector] replaceScene:[HelloWorld scene]];
}-(void)dealloc {
  [_label release];
  _label =nil;
  [super dealloc];
}@end

そろそろ疑問に思うかもしれないけれど、scenelayerっていう二つの異なるオブジェクトがあるよね。それらの違いを明確にしないといけない。sceneはいくつでもlayerを含むことができる、けれどこの例だと一つしか無いね。layerは画面の真ん中にlabelを置いてるだけだ、そして3秒間でHelloWorldSceneに戻るトランジションが設定されてる。

最後に、いくつかエクストリームなゲームロジックを追加しよう。一つ目、プレイヤーが敵を倒した時の手裏剣の個数をカウントしよう。メンバ変数としてHelloWorldScene.hのなかのHelloWorldクラスに以下を追加する。

int _projectilesDestroyed;


HelloWorldScene.m内ではGameOverSceneクラスをimportするために、

#import "GameOverScene.h"

updateメソッド内のtargetsToDeleteループあたりでカウント計測と勝利条件のチェックを追加しよう。

_projectilesDestroyed++;
if(_projectilesDestroyed > 30){
  GameOverScene *gameOverScene =[GameOverScene node];
  [gameOverScene.layer.label setString:@"You Win!"];
  [[CCDirector sharedDirector] replaceScene:gameOverScene];
}

最後に、もし一個だけでもtargetを逃した場合はあなたの負けにしよう。spriteMoveFinisedメソッドを以下のコードを、tag==1のケース内に足す事で修正しよう。

GameOverScene *gameOverScene =[GameOverScene node];
[gameOverScene.layer.label setString:@"You Lose :["];
[[CCDirector sharedDirector] replaceScene:gameOverScene];

ビルドして実行してみよう、これで勝ち負けが分かり然るべき時にゲームオーバーが出るでしょう。

Gimme The Code!
以上!
simple Cocos2D iPhone Game
ここにソース置いとくから使いたい時に使って。

Where To Go From Here?

このプロジェクトは、もっとcocos2dの新しいものを使って拡張していく際に、とても良い土台になると思う。
もしかしたら、勝つためにあと何匹敵を倒さなければいけないかを表示する棒グラフを表示させようとするかもしれないし(drawPrimitiveTestというサンプルプロジェクトをみてみよう)、もっとクールなデスアニメーションを付けようとするかもしれない(ActionTest, EffectsTest, EffectsAdvancedTest)、もっと音楽を付けようとするかもしれない、もしかしたらもっと楽しめるゲームロジックを+かもしれない。
もう青天井さ!!!
もしこのチュートリアルシリーズに興味を持ってくれたなら、How To Add A Rotating Turretも読んでくれると嬉しいな!
また、もっとcocos2dについて勉強したいと思ったら、how to create buttons in cocos2dintro to box2d, もしくはhow to create a simple breakout gameなどを読んでね。