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

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

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

【cocos2d】慣性スクロールの実装。背景をヌルヌル動かす

cocos2d

タッチ操作に合わせて気持ちよく背景を慣性スクロールさせてみます。
実際にはタッチ操作で自機の座標を動かし、同時に背景の座標をを逆方向に動かす事でキャラクターを画面の中心に保ったまま背景をスクロールさせています。

早速キャラクターのクラスCatクラスの実装です。画面上は仮にハチを入れていますが、いずれはネコになる予定です。

@implementation Cat
{
    CGPoint _originalTouch; // タッチ開始時のタッチ位置
    CGPoint _originalPosition; // タッチ開始時のself.position
    CGPoint _velocity; // 速度ベクトル
    CGPoint _floatPosition; //小数点以下を丸めない生の位置
    CGPoint _previousPosition; // 前回処理時のfloatPosition
}

- (id)init
{
    if (self = [super initWithSpriteFrameName:@"bee0.png"]) {
        [self runAction:repeat];
        CGSize winSize = [CCDirector sharedDirector].winSize;
        CGPoint center = ccp(winSize.width/2, winSize.height/2); //画面中央
        self.position = center;
        _floatPosition = self.position;
        [self scheduleUpdate];
    }
    return self;
}

- (void)onEnter
{
    [super onEnter];
    [[TouchLayer sharedTouch] registerRecipient:self]; 
}   

- (void)onExit
{
    [super onExit];
    [[TouchLayer sharedTouch] unregisterRecipient:self];
}

- (void)update:(ccTime)delta
{
    if (![TouchLayer sharedTouch].touching) {
        // タッチ中以外は慣性で移動
        [self inertia];
    }
}

- (void)inertia
{
    _velocity = ccpMult(_velocity, 0.9); // 慣性の大きさを設定
    _floatPosition = ccpAdd(_floatPosition, _velocity);
    self.position = [self allignPosition:_floatPosition];
    self.rotation = 90.0 + CC_RADIANS_TO_DEGREES(-ccpToAngle(_velocity));
}

- (void)touchNotificationDown:(CGPoint)location
{
    _originalTouch = location;
    _originalPosition = self.position;
    _floatPosition = self.position;
    _previousPosition = self.position;
    _velocity = CGPointZero;
}

- (void)touchNotificationMove:(CGPoint)location
{
    // タッチ中はタッチに合わせて移動
    CGPoint move = ccpSub(location, _originalTouch);
    _floatPosition = ccpAdd(_originalPosition, move);
    self.position = [self allignPosition:_floatPosition]; //小数点以下は丸める
    CGPoint deltaMove = ccpSub(_floatPosition, _previousPosition); //前回処理時からの変位
    _velocity = ccpMult(ccpAdd(_velocity, deltaMove), 0.5); //過去の平均速度ベクトル計算
    _previousPosition = _floatPosition; //現在の位置を記憶しておく
    self.rotation = 90.0 + CC_RADIANS_TO_DEGREES(-ccpToAngle(_velocity));
}

- (CGPoint)allignPosition:(CGPoint)position
{
    CGFloat x = (int)position.x;
    CGFloat y = (int)position.y;
    return ccp(x, y);
}

@end

TouchLayerというのは自作クラスでタッチ情報を集中して処理するためのクラスおよびプロトコルです。
TouchLayerについてはいずれ別の記事で詳しく書きますが、registerRecipientとして登録するとCCTouchesBegan発生時にTouchNotificationDown, CCTouchesMove発生時にTouchNotificationMoveが呼ばれるようになっています。TouchNotificationUpというメソッドもありますが、Catクラスでは実装していません。

タッチ中は指の動きに合わせてダイレクトに座標をコントロールします。

   CGPoint move = ccpSub(location, _originalTouch);
    _floatPosition = ccpAdd(_originalPosition, move);
    self.position = [self allignPosition:_floatPosition]; //小数点以下は丸める

タッチ開始時のタッチ座標とキャラクターの座標を記録しておき、指を移動したときは初期座標からの指の変位をキャラクターの座標の変位としています。

キャラクターの正確な位置はfloatPositionというインスタンス変数に入れておき、座標の計算はself.positionではなくその値を使用します。
画面表示するときに小数点以下を丸めた値をself.positionに入れます。これは、前回の記事に書いたとおりself.positionに入れる値を整数に制限してタイルマップ上にチラチラと隙間が発生するのを防ぐためです。
self.positionの値を直接丸めると計算誤差でおかしな動きをするので注意が必要です。

座標計算を同時に毎回キャラクターの速度のベクトルの平均値を計算しています。前回までの平均と今回の速度ベクトルを足して、ベクトルの長さを半分にします。

    CGPoint deltaMove = ccpSub(_floatPosition, _previousPosition); //前回処理時からの変位
    _velocity = ccpMult(ccpAdd(_velocity, deltaMove), 0.5); //過去の平均速度ベクトル計算
    _previousPosition = _floatPosition; //現在の位置を記憶しておく

タッチしている指を離したときは毎フレームupdateメソッドから呼ばれるinertiaメソッドでで移動処理を行います。
inertialメソッドでは速度ベクトルに0.9を掛け、毎フレームスピードを減衰させます。この0.9の値を0に近づけて行くと慣性が弱まり早く止まります、1に近づけると慣性が強くなりなかなか止まらなくなります。

    _velocity = ccpMult(_velocity, 0.9); // 慣性の大きさを設定
    _floatPosition = ccpAdd(_floatPosition, _velocity);
    self.position = [self allignPosition:_floatPosition];

動画です。なかなかいい感じで動いてると思いませんか?
動画は30フレームなのでそんなにいい感じに見えないかもしれませんが、実機ではヌルヌル動いています。

最終的にはあっさりしたコードになりましたが、ここにたどり着くまで試行錯誤の連続でした。

ちなみに、背景のクラスの実装はこのようになっていて、updateでCatクラスのインスタンスが常に画面の真ん中に来るように背景のpositionを毎フレーム動かしています。Catクラスの親ノードであるスプライトバッチノードは背景オブジェクトにaddChildしているので背景に同期して毎フレーム自動的に動いています。

@implementation GroundLayer
{
    __weak CCTMXTiledMap* _tileMap;
    __weak Cat* _cat;
    CGPoint _originalTouch;
    CGPoint _originalPosition;
    CGPoint _winCenter;

}

- (id)init
{
    if ((self = [super init])) {
        _tileMap = [CCTMXTiledMap tiledMapWithTMXFile:@"desert.tmx"];
        [self addChild:_tileMap z:-1];
        [self scheduleUpdate];
        CGSize winSize = [CCDirector sharedDirector].winSize;
        _winCenter = ccp(winSize.width/2, winSize.height/2);
    }
    return self;
}

- (void)update:(ccTime)delta
{
    SpriteBatch* batch = [SpriteBatch sharedSpriteBatch];
    if (_cat == nil) { // 初回はYESとなりCatインスタンスへの参照を得る
        _cat = (Cat *)[batch getChildByTag:kMyShip];
    }
    
    self.position = ccpSub(_winCenter, _cat.position);
}

@end