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

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

【cocos2d】自動弾避けアルゴリズム

暑い!夏になってしまう!
そうです。夏までにこのゲームを完成させなくてはならないので急がなくてはなりません。
今回は、自機であるミツバチが敵の弾や仲間のハチを避けつつも集団で行動するアルゴリズムについて書きたいと思います。
自動弾避け機能がわかりやすいように仲間を増やす以外の操作をいっさいせずに動画を撮りました。

軽快に弾を避けて動き回る様子がわかると思います。

- (void)steering
{
    CCArray* group = [SpriteBatch sharedSpriteBatch].children;
    ObjType type = kMyShip | kEnemy | kEnemyBullet;
    float range;
    if ([TouchLayer sharedTouch].touching){
        range = 20.0;
    } else {
        range = 40.0;
    }
    NSMutableArray* checkResult = [CollisionRader checkRader:self Type:type checkGroup:group range:range];
    CGPoint avoidVector = CGPointZero;
    
    // レーダー範囲内にいるオブジェクトを避けるベクトルを算出
    for (CollisionSprite* element in checkResult) {
        CGPoint comingVector = ccpSub(self.position, element.position);
        comingVector = ccpNormalize(comingVector); // cominVectorがZeroでない事は保証されている
        avoidVector = ccpAdd(avoidVector, comingVector);
        // ゼロベクトルの場合はNormalizeしない(できない)
        if (ccpLengthSQ(avoidVector) == 0.0) {
            continue;
        }
        avoidVector = ccpNormalize(avoidVector);
    }
    avoidVector = ccpMult(avoidVector, 30.0);
    
    CGPoint touchLocation = ccpAdd([TouchLayer sharedTouch].location, TOUCH_OFFSET); //指に隠れないようオフセット
    CGPoint touchVector = ccpSub(touchLocation, self.position);
    
    //タッチ中あるいはレーダーレンジの2倍以上離れたときあるいは10%の確率でタッチ方向に動かす
    if ([TouchLayer sharedTouch].touching ||
        (ccpLength(touchVector) > range * 2.0)
        || MYRAND(0, 1) < 0.1) {
        avoidVector = ccpAdd(avoidVector, touchVector);
    }
    
    float distance = ccpLength(avoidVector);
    float speed = 0;
    float brakeDistance = 50.0;
    if (distance < brakeDistance) {
        speed = _maxSpeed * distance / brakeDistance;
    } else {
        speed = _maxSpeed;
    }
    if (speed == 0) {
        return; // speed = 0なら距離がが0で移動しないので計算しない。計算すると値がnanになりバグる。
    }
    avoidVector = ccpMult(ccpNormalize(avoidVector), speed);
    
    NSAssert((_maxAccel > 0.0) && ( _maxSpeed > 0.0), @"_maxAccel and _maxSpeed should be greater than 0");
    
    CGPoint avoidAccel = ccpSub(avoidVector, _velocity);
    
    // 加速度にリミッターを掛ける
    if (ccpLength(avoidAccel) > _maxAccel) {
        avoidAccel = ccpNormalize(avoidAccel);
        avoidAccel = ccpMult(avoidAccel, _maxAccel);
    }
    
    _velocity = ccpAdd(_velocity, avoidAccel);
    if (ccpLength(_velocity) > _maxSpeed) { //リミッター
        _velocity = ccpNormalize(_velocity);
        _velocity = ccpMult(_velocity, _maxSpeed);
    }
    self.position = ccpAdd(self.position, _velocity);
}

途中に出てくるcheckRaderメソッドの実装はつぎのとおりです。

+ (NSMutableArray *)checkRader:(CollisionSprite *)sender Type:(ObjType)type checkGroup:(CCArray *)group range:(float)range
{
    NSMutableArray* caught = [NSMutableArray array];
    for (CollisionSprite* child in group) {
        if (child == sender) continue; //自分の場合はスキップ
        if ((child.tag & type) == 0) continue; //判定対象でなければスキップ
        long distance = ccpDistance(sender.position, child.position);
        if (distance > 0 && distance < range) { //距離0の場合はccpNormalizeができないので無視
            [caught addObject:child];
        }
    }
    if ([caught count] == 0) {
        return nil;
    } else {
        return caught;
    }
}


各ハチが自分の周囲の状況をチェックし、仲間や、敵、敵の弾がある範囲にいる場合はそれらを避ける方向に方向転換するという処理になっています。
なお、チェックする距離をタッチ中は短くする事で、人間が操作しているときはあまり余計な動きはしないようになっています。
また、タッチ位置から離れすぎたときはタッチ方向に何となく向かうようにして、集団がバラバラにならないようにしています。従いまして完璧に自由に弾を避けられるわけではないのでそこそこ弾を喰らいます。あまりに完璧だともはやゲームではなくなってしまいますので。

各ハチが総当たりで他のオブジェクトとの間合いをチェックしますので、ハチの数が増えると結構な処理量になります。
従いまして、ハチの数がやたらと増えすぎないようハチの数を見て難易度調整をする予定です。