cocos2dを用いてiPhone上で至極シンプルなゲーム作成チュートリアル
はじめに
ここ1,2ヶ月、cocos2dというフレームワークでゲームの開発をしています。
ただあまり日本語のドキュメントが整っておらず、英語のwikiやウェブサイトを色々巡回していました。
なかなかversionなどの関係でクラス名が変わっていて、つまらない所でハマったりなど、無駄な時間がかかったりしてしまいました。
そんななかとても良い入門記事を見つけ、内容的に飛び抜けて分かりやすく素晴らしい記事だったので、つたない英語で翻訳させてもらいました。
ボリューム的にはかなり多いのですが、「ゲーム制作をしてみたい」、という気持ちだけあればこなせるように設計されているチュートリアルなので興味がある方は是非最後まで読んで欲しいです。
以下 How To Make A Simple iPhone Game With Cocos2D Tutorialの邦訳記事です。
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の中には、たくさんのlayers(Photoshopのそれと同じようなもの)を持ち、そのlayersはspriteやlabel、menuなどその他たくさんのnodeを含むことになります。そしてそれらnodeは他のnodeを同じように含むことも可能だよ。(spriteがその中に子spriteを持つなんてことも可能)
サンプルプロジェクトをみたならば、そこには「HelloWorldScene」というsceneが一つしか無いことに気が付くでよね。おれたちはこれからそこに主なゲームプレイを加えていくよ。そのファイルを開けてみると、すぐにinitメソッドの中に「Hello World」というlabelをsceneに加えている事が分かるはず。
ではこれから、それを取り出してその代わりに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)で、xとyの値はそれぞれ右に、上にあがることで上昇する。今回のプロジェクトはランドスケープだから最終的に右上の座標は(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がデフォルトの黒のままだよね。この画像だと白の方が良さそうだ。簡単な方法の一つとして、backgroundのlayerをCCColoredLayerクラスを使って変えるってのができる。じゃあまた書いてみよう。HelloWorldScene.hを開いてHelloWorldインターフェースの定義を以下のようにしてみよう。
@interface HelloWorld : CCColorLayer
そしたらHelloWorldScene.mをクリックして、ちょっとだけコードを追加する。こんな感じでね。
if((self=[super initWithColor:ccc4(255,255,255,255)])){
もう一回コンパイルして動かしてみよう、今度は白い背景のうえに君のspriteがいるはずだ。
おっと、おれたちの忍者がActionしたそうにしてるぜ!?
Moving Targets
今度はもういくつかおれたちの忍者と戦う相手としていくつかsceneにtargetを追加してみよう。
もうちょっと面白くするために、動く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から左に動かすときに使う。動く間隔を指定する事もできて、今回はランダムに2〜4秒かかるようにしてる。
・CCCallFuncN
これは何かのアクションが終わった時に呼ばれるコールバック関数。今回はspriteMoveFinishedって名前にしてるけど、まだ書き終わってない。あとでやる。
・CCSequence
これはいくつかのアクションを繋げるときに使う。今回はCCMoveToを最初に使ってそれが終わった後にCCCallFuncNを呼び出すようにしてる。
では次にCCCallFuncNで呼び出してるコールバック関数を加えよう。これをaddTargetの前に足して欲しい。
-(void)spriteMoveFinished:(id)sender { CCSprite *sprite =(CCSprite *)sender; [self removeChild:sprite cleanup:YES]; }
この関数の目的はspriteがscreen外になったらsceneから取り除くためのものだ。これは画面外に出てったsprite達がどんどん増えてメモリリークしてしまうのを防ぐ上でめちゃくちゃ重要。他にももっと良い方法があって、spriteの再利用配列をつくったりするともっと良いんだけど、おれは初心者のチュートリアルには簡単な道しるべに成る方を選ぶよ。
じゃあ最後にもうひとつ。今の状態だとターゲットを創るたびにメソッドを呼ばなきゃならないよね。
これを面白くするために、ずーっと継続的に動かしておこう。これを実現するためにcocos2dではコールバック関数を継続的にスケジューリングして呼ぼう。以下のコードをinitメソッド内でreturnする前に加えよう。
[self schedule:@selector(gameLogic:) interval:1.0];
そして次にそのコールバック関数を簡単に定義しちゃおう
-(void)gameLogic:(ccTime)dt { [self addTarget]; }
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にはたくさんある、box2dやchipmunkなどの物理エンジンライブラリを使ったりね。だけどシンプルにいきたいので、自分たちで衝突検知を実装しよう。
それを行うために、targetと手裏剣のトラッキングを行い続けることが必要になってくる。
以下を、HelloWorldSceneクラスの宣言のところに付け加えよう。
NSMutableArray*_targets; NSMutableArray*_projectiles;
で、initメソッドのなかでこれらの配列を初期化しよう。
_targets =[[NSMutableArray alloc] init]; _projectiles =[[NSMutableArray alloc] init];
で、deallocメソッドの中でメモリを掃除するためのコードも書こう。
[_targets release]; _targets =nil; [_projectiles release]; _projectiles =nil;
さぁ、新しいtargetをtarget配列に追加し、あとで使うタグをつけるために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
そろそろ疑問に思うかもしれないけれど、sceneとlayerっていう二つの異なるオブジェクトがあるよね。それらの違いを明確にしないといけない。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 cocos2d, intro to box2d, もしくは how to create a simple breakout gameなどを読んでね。