C++スネークゲームにおける衝突判定と成長処理の実装

スネークゲームの根幹となる衝突判定と、蛇が成長する仕組みについて解説する。ここでは、蛇が壁や自身に衝突した場合の判定と、食べ物を摂取した際の体節追加処理を実装していく。

衝突判定を実装するにあたり、蛇の頭部が次に進む座標をあらかじめ計算し、その座標に存在するオブジェクトの種類を調べる手法をとる。この次座標の計算処理を再利用可能にするため、Serpentクラスに新たにcalcFuturePositionメソッドを追加し、既存の移動処理から呼び出す形にリファクタリングを行った。

Vec2 Serpent::calcFuturePosition()
{
    Vec2 futurePos = headData.currentPos;

    switch(headData.heading)
    {
        case DIR_NORTH:
            futurePos.posX -= 1;
            break;
        case DIR_SOUTH:    
            futurePos.posX += 1;
            break;
        case DIR_WEST:    
            futurePos.posY -= 1;
            break;
        case DIR_EAST:    
            futurePos.posY += 1;
            break;
        default:break;
    }
    return futurePos;
}

SerpentSegment* Serpent::advance()
{
    SerpentSegment* discardedTail = nullptr;
    if(headData.bodyLength > 1)
    {
        appendSegment(headData.currentPos, headData.symbol);
        discardedTail = removeTailSegment();
    }
    else
    {
        discardedTail = new SerpentSegment;
        discardedTail->currentPos = headData.currentPos;
    }
    headData.currentPos = calcFuturePosition();
    return discardedTail;
}

次に、ゲームエンジン側での衝突判定を実装する。取得した次座標のマップデータを参照し、障害物や蛇自身の体であればゲームオーバー、食べ物であれば体節追加のフラグを返すようにする。

int GameEngine::evaluateImpact()
{
    Vec2 targetPos = activeSerpent.calcFuturePosition();
    TileData tileInfo;
    int tileCategory = grid.fetchTileState(targetPos.posX, targetPos.posY, tileInfo);

    switch(tileCategory)
    {
        case TILE_BARRIER:
        case TILE_SERPENT:
            return IMPACT_FATAL;
        case TILE_FOOD:
            return IMPACT_GROW;
        case TILE_VACANT:
        default:break;
    }

    return IMPACT_NONE;
}

蛇を成長させるには、マップ上の空きマスにランダムに食べ物を出現させる処理が必要である。壁や蛇の体と座標が重ならなくなるまでランダムな座標を生成し続け、条件を満たす空き地にアイテムを配置する。

void GameEngine::spawnFood()
{
    if(foodPos.posX * foodPos.posY != 0)
    {
        return;
    }
    TileData tileInfo;
    while(true)
    {
        while(foodPos.posX == 0 || foodPos.posX == grid.getWidth() - 1)
        {
            foodPos.posX = rand() % grid.getWidth();
        }
        while(foodPos.posY == 0 || foodPos.posY == grid.getHeight() - 1)
        {
            foodPos.posY = rand() % grid.getHeight();
        }
        
        int tileCategory = grid.fetchTileState(foodPos.posX, foodPos.posY, tileInfo);
        if (tileCategory == TILE_VACANT)
        {
            tileInfo.symbol = 'F';
            tileInfo.category = TILE_FOOD;
            grid.updateTileState(foodPos.posX, foodPos.posY, tileInfo);
            break;
        }
        else
        {
            foodPos.posX = 0;
            foodPos.posY = 0;
        }
    }
}

最後に、メインループの移動処理にこれらの判定を組み込む。衝突結果に応じて処理を分岐させ、食べ物を取得した場合は蛇にセグメントを追加して食べ物を再生成する。通常移動の場合は末尾のセグメントを削除し、新たな頭部の位置をマップ情報に反映させることで蛇の移動と成長を表現する。

int GameEngine::advanceSerpent()
{
    int impactResult = evaluateImpact();

    switch(impactResult)
    {
        case IMPACT_GROW:
            activeSerpent.growBody(activeSerpent.fetchHead()->symbol);
            foodPos.posX = 0;
            foodPos.posY = 0;
            break;
        case IMPACT_FATAL:
            return IMPACT_FATAL;
        case IMPACT_NONE:
        default:break;
    }

    SerpentSegment* oldTail = activeSerpent.advance();

    grid.clearTile(oldTail->currentPos.posX, oldTail->currentPos.posY);
    delete oldTail;
    
    TileData headTile;
    headTile.category = TILE_SERPENT;
    headTile.symbol = activeSerpent.fetchHead()->symbol;
    grid.updateTileState(activeSerpent.fetchHead()->currentPos.posX,
                         activeSerpent.fetchHead()->currentPos.posY,
                         headTile);

    return IMPACT_NONE;
}

タグ: C++ ゲーム開発 スネークゲーム アルゴリズム

7月30日 08:57 投稿