jarinosuke blog

about software engineering, mostly about iOS

敵キャラに個性を持たせてみよう

はじめに

今回も例の記事の和訳記事になっています。
敵キャラ毎に体力や動くスピードなど、個性を持たせてゲームをもっと面白くさせて行きましょう。
元記事はこちらになります。
もし前回までのチュートリアルをこなしていない人は、それをまずやってからの方が理解が進むと思います。
#1 cocos2d for iPhoneで簡単なゲーム作成チュートリアル
#2 iPhoneゲーム内で砲台を回転させてみよう

Harder Monsers and More Levels: How To Make A Simple iPhone Game with Cocos2d Part 3

ちょっと時間が空いてしまったね。前回の記事とかで、回転する砲台だったり音のエフェクトも付ける事ができるようになったね。

だけど、回転する砲台だけじゃゲーム的にちょっと簡単すぎるよね。モンスターは一回打てば死んじゃうし、レベルが1段階しかない!まだウォーミングアップにもなってないはずだ。

このチュートリアルでは、プロジェクトにレベルに合わせてモンスターに違いを持たせて行くよ、それで色々なレベルをゲームに組み込んでいこう。

Tougher Monsters


面白くするために、二つのモンスターを作ろう:弱くて速いのと、強くて遅いのを、ね。
プレイヤーが二つを見分けられるように、それらの画像をダウンロードしてプロジェクトに加えよう。
同様にして爆発の音も加えよう。

よし、それじゃMonsterクラスを作ろう。君自身もMonsterクラスを設計するのに色々な方法があるよね。だけど今回も一番簡単な方法でやっていくことにするよ。MonsterクラスをCCSpriteクラスのサブクラスとして扱う。またMonsterクラスのサブクラスとして、一つは弱くて速いMonster、もう一つは強くて遅いMonsterを作る。

File / New Fileを選んで、Cocoa Touch Class / Objective-C classを選ぶ。NSObjectのサブクラスにしよう。で、Nextをクリックだ。名前はMonster.mにして、もちろん"Also create Monster.h"にチェックだ。

それじゃあMonster.hを以下の様にしてみて。

#import "cocos2d.h"
@interface Monster : CCSprite {
    int _curHp;
    int _minMoveDuration;
    int _maxMoveDuration;
}
@property(nonatomic, assign)int hp;
@property(nonatomic, assign)int minMoveDuration;
@property(nonatomic, assign)int maxMoveDuration;
 
@end

@interface WeakAndFastMonster : Monster {
}
+(id)monster;
@end

@interface StrongAndSlowMonster : Monster {
}
+(id)monster;
@end

単純明快だよね。ただCCSpriteからMonsterクラスを作って、いくつかMonsterの状態を知るための変数を持たせて、それをもとに二つのちょっと違うタイプのMonsterサブクラスを作っただけ。よし、それじゃあMonster.mを開いてimplementationを加えよう。

#import "Monster.h"

@implementation Monster
 
@synthesize hp = _curHp;
@synthesize minMoveDuration = _minMoveDuration;
@synthesize maxMoveDuration = _maxMoveDuration;
 
@end

@implementation WeakAndFastMonster
 
+(id)monster {
 
    WeakAndFastMonster *monster =nil;
    if((monster =[[[super alloc] initWithFile:@"Target.png"] autorelease])){
        monster.hp =1;
        monster.minMoveDuration =3;
        monster.maxMoveDuration =5;
    }
    return monster;
 
}
@end

@implementation StrongAndSlowMonster
 
+(id)monster {
 
    StrongAndSlowMonster *monster =nil;
    if((monster =[[[super alloc] initWithFile:@"Target2.png"] autorelease])){
        monster.hp =3;
        monster.minMoveDuration =6;
        monster.maxMoveDuration =12;
    }
    return monster;
 
}@end

コードの中でそれぞれのクラスを返す二つの静的なメソッドが書かれているだけだね。デフォルトのHPと動く時間間隔を設定しよう。それじゃあ、この新しいMonsterクラスをコードにつなげていこう。まず最初に新しいファイルを組み込むためのコードをHelloWorldScene.mに書き足そう。

#import "Monster.h"

そしたらaddTargetメソッドに今作った新しいクラスを生成するコードを加えて、以下のように修正しよう。

//CCSprite *target = [CCSprite spriteWithFile:@"Target.png" rect:CGRectMake(0, 0, 27, 40)]; 
Monster *target =nil;
if((arc4random()%2)==0){
    target =[WeakAndFastMonster monster];
}else{
    target =[StrongAndSlowMonster monster];
}

よしこれで50%できた。そしたら前に作った移動の時間間隔に関する変数を今のMonsterクラスの変数に変えよう。

int minDuration = target.minMoveDuration; //2.0;
int maxDuration = target.maxMoveDuration; //4.0;

じゃあいくつかupdateMethodに変更を加えて行こう。

BOOL monsterHit = FALSE;

次に、CGRectIntersectsRectのなかで、オブジェクトをすぐtargetsToDeleteに加えるかわりに以下のコードを書き足そう。

//[targetsToDelete addObject:target];
monsterHit = TRUE;
Monster *monster =(Monster *)target;
monster.hp--;
if(monster.hp <=0){
    [targetsToDelete addObject:target];
}break;

要するに、モンスターをすぐに倒す代わりにHPを引いて、もし0になったら消滅させるんだ。また弾がモンスターに当たったと同時にループを抜けるようにしてるのは、一発の弾が当たるのは一体だけだからだ。最後にprojectilesToDeleteに対して以下のコードを修正しよう。

if(monsterHit){
    [projectilesToDelete addObject:projectile];
    [[SimpleAudioEngine sharedEngine] playEffect:@"explosion.caf"];
}

コンパイル&ラン、そうすると2種類のモンスターが画面上を走り抜けるのが分かるよね。砲台が忙しくなるね!Multiple Levelsレベルを段階的に調整する要素を加えるためには、まずはリファクタリングが必要になってくる。それ自体は本当に単純で簡単なんだけどたくさん有り過ぎるんだ、それを全てやるとなるととても長い記事になってそれこそ退屈になってしまう。それをやらない代わりに、それをやる上でいくつかハイレベルなヒントとなることを順番に書いておくね。

  • レベルクラスを作ろう。

現状では、HelloWorldSceneはモンスター生成だったり、それの頻度などの"レベル"に関しては完全にハードコーディングされてるよね。じゃあまず始めに、いくつかの情報をlevelクラスに持たせてHelloWorldSceneの中で同じ論理で再利用できるようにしてみよう。

  • シーンを再利用しよう。

現状では、sceneが移り変わるたびにsceneクラスをのインスタンスを作っているよね。この欠点は注意深いメモリ管理ができていない点で、initメソッド内で何かを読み込む時に遅れを生じさせることになってしまう。

  • AppDelegateをswitchboardとして継承しよう。

現状では、ひとつもレベルの様なグローバルな状態は無いし、それぞれのシーン入れ替わりに必要な時にだけ、単純にハードコーティングされてるだけだよね。
レベルの様なグローバルな状態をAppDelegateにポインタとして持たせるように修正して、それをsceneのどこからでもアクセスできる様な中心的な場所にすれば良いんだ。
またAppDelegate内にsceneの依存関係などを考慮して切り替えを行う様なメソッドを持たせるんだ。それらがリファクタリングの主なポイントだ。
自分で考えてみて、全て記述したこのプロジェクトと見比べてみてくれ。ただしこれは一つの方法であって決して答えではないと思ってくれ、もし君がクールな方法でシーンやオブジェクトを最適化することができたら教えてくれ!
とにかく、コードをダウンロードして色々やってみてくれ。回転する砲台、見た目が変わる敵、段階的なレベル、勝ち/負けのシーン、そして忘れちゃいけないのが音楽のエフェクト、どうだ、素晴らしく良いスタートを切れたはずだ!

That's A Wrap!

もういちど言うけど、まだ納得がいっていないようなら、ここにサンプルを置いておくから取ってみてくれ。
これが恐らくおれがシリーズとして出すものとしては、最後のチュートリアルになるはずだ。cocos2dについての記事は書き続けていくし、もし君たちから面白いアイディアについて指摘されたら追加して行くけど、これからはもう少し掘り下げた特殊な記事だったり、多少これより複雑なものについて扱っていくよ。
このシリーズ楽しんでくれたかな?、君たちのCocos2dのゲームが成功することを祈ってるよ!