夏までにiPhone アプリつくってみっか!

趣味でiPhone/Androidアプリを開発し、日々勉強した事を書いています。オープンワールド系レースゲームをUnityで開発中です。

【cocos2d, Objective-C】コンポーネントベースアーキテクチャとタッチ入力を試してみる

cocos2d関連のチュートリアルサイトや書籍などで「継承(interitance)ではなく合成(composition)を使ってゲームを設計せよ」とよく書かれています。
コンポーネントベースアーキテクチャコンポーネントシステムなどと書かれている場合もあります。オブジェクト指向言語の特徴が継承ですが、これを多用してゲームを設計すると複数のクラスに必要な機能は結局ベースとなるクラスに集約的に実装されるため、ベースクラスの処理が複雑になり、コードの再利用なども難しくなります。
これに対し、コンポーネントシステムでは、1つの機能を1つのクラスに実装し、それを他のクラスから使用します。これによりコードは機能毎に分散し、他のプロジェクトで再利用するのも容易になります。
さっそくこのコンポーネントシステムを試してみましょう。ランダムに大量のキャラクターを表示するテストプログラムを修正して使用することにします。まずはHelloWorldLayerですが、大量にMySpriteを生成する部分を修正し、1個ののMySpriteオブジェクトを生成し、自分にAddChildします。また、タッチ入力でキャラクターを動かせるようにしてみます。タッチ入力を使うには、initメソッドでself.isTouchEnabled = YES;とします。つまりisTouchEnabledプロパティにYESを設定します。そして、新たに実装するのがタッチ処理関係の3個のメソッド。これらは名前のとおりタッチ開始、移動、タッチ終了のタイミングで呼ばれます。ちなみにこの方法で処理できるのはシングルタッチ(1本指でのタッチ)のみで、マルチタッチの場合は別のやり方があるようです。

#import "HelloWorldLayer.h"
#import "MySprite.h"

@implementation HelloWorldLayer {
    MySprite* _myShip;
    CGPoint _originalTouch;
    CGPoint _originalPosition;
}

+(CCScene *) scene
{
	CCScene *scene = [CCScene node];
	HelloWorldLayer *layer = [HelloWorldLayer node];
	[scene addChild: layer];
	return scene;
}

-(id) init
{
    if( (self=[super init]) ) {
        CCSpriteBatchNode* batch = [CCSpriteBatchNode batchNodeWithFile:@"Icon-72.png"];
        [self addChild:batch];
        _myShip = [[MySprite alloc] initWithImageAndAddToBatch:batch];
        [self addChild:_myShip];
        self.isTouchEnabled = YES;
    }
    return self;
}

- (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *myTouch = [touches anyObject];
    CGPoint location = [myTouch locationInView:[myTouch view]];
    location = [[CCDirector sharedDirector] convertToGL:location];
    
    _originalTouch = location;
    _originalPosition = _myShip.position;
}

- (void)ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *myTouch = [touches anyObject];
    CGPoint location = [myTouch locationInView:[myTouch view]];
    location = [[CCDirector sharedDirector] convertToGL:location];
    
    CGSize winSize = [CCDirector sharedDirector].winSize;
    
    CGPoint move = ccpSub(location, _originalTouch);
    move = ccpMult(move, 2.0f); //移動量を倍に
    
    // moveToPosition = 移動先の位置
    CGPoint moveToPosition =  ccpAdd(_originalPosition, move);
    
    // 画面からはみ出る場合は補正
    float halfSpriteWidth = _myShip.contentSize.width/2;
    float halfSpriteHeight = _myShip.contentSize.height/2;
    moveToPosition.x = MAX(MIN(moveToPosition.x, winSize.width - halfSpriteWidth),
                           halfSpriteWidth);
    moveToPosition.y = MAX(MIN(moveToPosition.y, winSize.height - halfSpriteHeight),
                           halfSpriteHeight);
    
    // 補正した位置に移動
    _myShip.position = moveToPosition;
}

- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    
}

@end

なお、ccTouchesMovedでは毎回ccTouchesBeginで始まったタッチ位置(_originalTouch)からの移動量を計算しています。そして、_myShipの移動量は指の移動量の倍にしています。キャラクターが画面からはみ出すような操作(実際にははみ出さないようになっていますが)をしても指と_myShipの位置関係がかわらないようになっているので、指の位置が大きくずれずに操作できます。ただし、指がタッチ画面から外れるとずれます。

次にMySpriteクラスです。インスタンス変数としてCCSpriteクラスの_sprite, _optionを宣言しています。この二つをコンポーネントとして合成しているイメージです。initWithImageAndAddToBatchではCCSpriteのオブジェクトを2つ生成し、前述の変数に入れています。そして、本当はHelloWorldLayerクラスの_myShip = selfにaddChildしたいところですが、それだとCCSpriteBatchNodeを使った高速化ができない(CCSpriteBatchNodeにaddChildできるのはCCSpriteだけなのでCCNodeを継承した_myShipをaddChildできない)ので仕方なくバッチノードにaddChildします。バッチノードへのポインタはinitWithImageAndAddtoBatchメソッドのパラメーターとして受け取ります。そして、_spriteと_optionの区別ができるように_optionの透明度を半分に設定します。CCSpriteのopacityプロパティは0から255の数値で指定します。0は透明、255は不透明です。そして、_optionの表示位置を_spriteの表示サイズの2倍分右にずらします。self.contentSize = _sprite.contentSizeとする事でHelloWorldLayerのccTouchesMovedで画面からはみ出さないようにする位置の補正を正しくできます。なお、これがないと半分はみだします。

#import "MySprite.h"


@implementation MySprite {
    CCSprite* _sprite;
    CCSprite* _option;
}

- (id)initWithImageAndAddToBatch:(CCSpriteBatchNode *)batch
{
    
    if ((self = [super init])) {
        _sprite = [CCSprite spriteWithFile:@"Icon-72.png"];
        [batch addChild:_sprite];
        _option = [CCSprite spriteWithFile:@"Icon-72.png"];
        [batch addChild:_option];
        _option.opacity = 255 * 0.5; //半透明にする
        CGSize screen = [CCDirector sharedDirector].winSize;
        CGPoint myPosition;
        myPosition.x = screen.width / 2;
        myPosition.y = screen.height / 2;
        self.position = myPosition;
        _sprite.position = myPosition;
        myPosition.x += _sprite.contentSize.width * 2; //オプションはちょっと右側に表示
        _option.position = myPosition;
        self.contentSize = _sprite.contentSize;
        [self scheduleUpdate];
    }
    return self;
}

- (void)update:(ccTime)delta
{
    CGPoint myPosition = self.position;
    _sprite.position = myPosition;
    myPosition.x += _sprite.contentSize.width * 2;
    _option.position = myPosition;
    
}
@end

updateメソッドではself = _myShipの位置に合わせて表示するスプライトの位置を移動します。
が、ここで疑問が生じます。なぜ手動でスプライトの位置を調整しなくてはならないのか。なぜかというとこれらのCCSpriteオブジェクトはCCSpriteBatchNodeにaddChildされているからなのですが、例えば_myShipの回転に_optionを追従させようとすると面倒そうです。sin, cos関数かccpRotateByAngle関数を使って位置の計算をする必要がありますね。
CCSpriteBatchNodeを使わないでselfにaddChildすれば多分自動的に追従してくれるはずなので今度試してみたいと思います。
最終的にはCCSpriteBatchNodeを使わないか、MySpriteクラスをCCSpriteから継承するかどちらかになると思います。