読者です 読者をやめる 読者になる 読者になる

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

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

【Objective-C】プロパティを使ってみる(nonatomic, readonly, weakどうすりゃいいの?)

Objective-C

前回のMyEnemyのテストプログラムにより、CCNodeのサブクラスに対してCCFadeInなどのアクションを設定するとクラッシュ事が判明しました。現時点ではMySprite, MyBulletでは動きに関するアクションしか設定していないため、たまたま動作していますが、今後に備えキャラクター本体のオブジェクトではなく、スプライトに対してアクションを設定したいと思います。
今後MySpriteオブジェクト本体のpositionなどは使用しませんので、updateメソッド内でスプライトの位置を本体と合わせる必要がなくなります。その代わり、HelloWorldLayerからスプライトのpositionを設定する事になります。MySpriteが持つ_spriteインスタンス変数をプロパティとして外から見えるようにしてあげればいいのですが、いろいろなオプション設定があり、何を設定するのが正しいのかわかりません。
まず、nonatomicオプションですが、これを設定するとアクセス時のオーバーヘッドが軽減されるとのことです。ただし、並列プログラミング時にはトラブルが発生する可能性があります。今回は並列プログラミングなどの高度な事はするつもりはありませんが、今後の事を考えnonatomicオプションは指定しません。
次にreadonlyオプションですが、これを設定するとsetアクセサが生成されません。従って値を書き換える事ができなくなります。値を書き換えるということはプロパティを通してアクセスするオブジェクトを入れ替えるということになります。そういう使い方はしないので、安全のためreadonlyを指定したいと思います。readonlyを指定したプロパティのプロパティを書き換える事は問題ありません。つまり、

_myShip.sprite = anotherObject;

は禁止されますが、

_myShip.sprite.position = ccp(100, 200);

は禁止されません。

一番悩ましいのがstrong(規定値)にするかweakにするかです。プロパティにweakオプションを設定する場合は対応するインスタンス変数にも__weak指定をする必要があります。(ちなみに__weakをつける位置は型名の前でも後でもどちらでも良いようです。)cocos2dのノード階層により、cocos2dのCCSpriteなどのオブジェクト(アクションはのぞく)は親ノードにaddChildする事で保持されます。従って、インスタンス変数にはオブジェクトを保持する必要はなく、weakで参照だけすればリテインサイクルは発生しません。従ってオブジェクトを入れるインスタンス変数は全て__weak指定にすれば良さそうですが、次のような使い方はできません。

_test = [[CCSprite alloc] initWithFile@"test-sprite.png"];
[self addChild:_test];

"Assigning retained object to weak variable; object will be released after assignment"
「weak変数にいれてもオブジェクトはすぐにリリースされてしまいますよ」と警告されてしまいました。つまり、この後でaddChildしようとしてもすでにオブジェクトは解放されてしまっているのです。警告を無視してプログラムを実行すると見事にクラッシュします。オブジェクトが解放され、_testにはnilが入っているためです。

_test = [[CCSprite spriteWithFile@"test-sprite.png"];
[self addChild:_test];

なら大丈夫です。しかし、フォーラムなどの書き込みを読むと、必ずしも保証されるとは限らないとのこと。コンパイラの実装や最適化によってはもっと早いタイミングで解放される場合もあるとか。
あちこちから同じオブジェクトをリテインする事でリテインサイクルが発生したりメモリリークしたりする可能性は高まりますが、突然原因不明のクラッシュに悩まされるよりは良さそうなので、weak指定はしません。
(2013.4.13追記:一旦ローカル変数にオブジェクトを入れる事で問題が解決しました。詳しくはこちら。)

よって、MySpriteクラスのヘッダファイルはこのようになりました。

#import <Foundation/Foundation.h>
#import "cocos2d.h"

@interface MySprite : CCNode {
    
}

-(id)initWithImage;

@property (readonly) CCSprite* sprite;

@end

実装ファイルはこうです。最近のXCodeではインスタンス変数を宣言しなくても自動的にプロパティ名の前に"_"をつけたインスタンス変数が生成され、クラス内部でアクセスする事ができますが、わかりやすさのために明示的に宣言しています。

#import "MySprite.h"
#import "MyBullet.h"
#import "MyBatch.h"


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

- (id)initWithImage
{
    
    if ((self = [super init])) {
        _sprite = [CCSprite spriteWithSpriteFrameName:@"Icon-72.png"];
        [[MyBatch sharedMyBatch] addChild:_sprite];
        _option = [CCSprite spriteWithSpriteFrameName:@"Icon-72.png"];
        [[MyBatch sharedMyBatch] addChild:_option];
        
        _option.opacity = 255 * 0.5; //半透明にする
        CGSize screen = [CCDirector sharedDirector].winSize;
        CGPoint myPosition = ccp(screen.width / 2, screen.height / 2);
        _sprite.position = myPosition;
        myPosition.x += _sprite.contentSize.width * 2; //オプションはちょっと右側に表示
        _option.position = myPosition;
        [self scheduleUpdate];
        
        CCRotateBy* rotateby = [CCRotateBy actionWithDuration:2 angle:360];
        CCScaleBy* scaleUp = [CCScaleBy actionWithDuration:1 scale:2.0];
        CCScaleBy* scaleDown = [CCScaleBy actionWithDuration:1 scale:1.0/2.0];
        CCSequence* sequence = [CCSequence actions:rotateby, scaleUp, scaleDown, nil];
        CCRepeatForever* repeat = [CCRepeatForever actionWithAction:sequence];
        [_sprite runAction:repeat];
        
        [self schedule:@selector(shoot) interval:0.1];
    }
    return self;
}

- (void)update:(ccTime)delta
{
    _option.position = ccpRotateByAngle(ccpAdd(_sprite.position, ccp(_sprite.contentSize.width * 2 * _sprite.scale, 0)),
                                        _sprite.position, CC_DEGREES_TO_RADIANS(-_sprite.rotation));
    _option.rotation = _sprite.rotation;
    _option.scale = _sprite.scale;
}

- (void)shoot
{
    CGPoint vector = ccpSub(_option.position, _sprite.position);
    CGFloat speed = 200;
    
    CCSpriteFrame* frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"Icon-Small.png"];
    
    MyBullet* bullet = [MyBullet bulletWithPosition:_option.position vector:vector speed:speed spriteFrame:frame];
    [self.parent addChild:bullet];
    
}

@end

MyBulletも同様に修正しますが、外部からスプライトを操作する事はないのでプロパティは用意しません。updateメソッドでスプライトのposition, rotation, scaleをオブジェクト本体と合わせる処理が不要になったのですっきりしました。

#import "MyBullet.h"
#import "MyBatch.h"


@implementation MyBullet {
    CCSprite* _sprite;
    
}

- (id)initWithPosition:(CGPoint)position vector:(CGPoint)vector speed:(CGFloat)speed spriteFrame:(CCSpriteFrame *)spriteFrame
{
    if ((self = [super init])) {
        _sprite = [CCSprite spriteWithSpriteFrame:spriteFrame];
        _sprite.position = position;
        [[MyBatch sharedMyBatch] addChild:_sprite];
        
        CGPoint unitVector = ccpNormalize(vector);      // 単位ベクトルを求める
        CGPoint zeroDegree = ccp(1, 0);                 // 0度のベクトル
        float bulletAngle = ccpAngleSigned(unitVector, zeroDegree); 
        _sprite.rotation = 90.0 + CC_RADIANS_TO_DEGREES(bulletAngle); // 頭を進行方向に向ける
        CGPoint velocity = ccpMult(unitVector, speed);  // 単位ベクトルに速度を掛ける
        
        CCMoveBy* moveBy = [CCMoveBy actionWithDuration:1.0 position:velocity];
        CCRepeatForever* repeatForever = [CCRepeatForever actionWithAction:moveBy];
        [_sprite runAction:repeatForever];
        [self scheduleUpdate];
    }
    return self;
}

+ (id)bulletWithPosition:(CGPoint)position vector:(CGPoint)vector speed:(CGFloat)speed spriteFrame:(CCSpriteFrame *)spriteFrame
{
    return [[self alloc] initWithPosition:position vector:vector speed:speed spriteFrame:spriteFrame];
}

- (void)update:(ccTime)delta
{
    // 画面外に出た時の処理
    CGSize screenSize = [CCDirector sharedDirector].winSize;
    CGRect screenRect = CGRectMake(0, 0, screenSize.width, screenSize.height);
    
    if (CGRectIntersectsRect(_sprite.boundingBox, screenRect) == NO) {     // 画面の中にいない?
        [_sprite removeFromParentAndCleanup:YES];
        [self removeFromParentAndCleanup:YES];                          // 親ノードから外れてオブジェクト解放
    }
}


@end

いよいよ次回、敵を撃破します。つまり、あたり判定の実装です。