Skip to content

第07回 CSVによる敵の複数配置

前回: 第06回 サウンドとゲームイベント / 次回: 第08回 敵の物理挙動とライフサイクル

今回の授業内容

  • CSVへ敵の初期位置を記録する
  • 敵配置データと地形タイルを分けて読み込む
  • 固定長配列で複数の敵を管理する
  • 浮動小数点数の速度と端数保持で敵を移動する
  • 全敵を更新、描画、接触判定する
  • リトライ時にCSVの配置から全敵を復元する

今回の配布コード

第06回のゲームに、CSVの指定で敵を配置する機能を追加したコードです
タイトル画面とタイトル用BGMはまだ含めず、起動後すぐにプレイを開始します
ゲーム全体の画面遷移は第10回で追加します

配布ファイル一覧
ファイル内容
Source/Stage.h敵配置座標と最大配置数の宣言
Source/Stage.cppCSVの敵配置番号の読み込み
Source/Game.h敵の速度、端数、敵配列と敵数の宣言
Source/Game.cpp全敵の初期化、端数移動、描画、接触判定
Source/ImageManager.h敵画像の識別子を追加
Source/ImageManager.cpp敵画像の読み込み定義を追加
Assets/Maps/Stage01.csv複数の敵配置を含むステージ
Assets/Images/Enemy.png敵キャラクター画像

マップCSVに敵の配置を追加する

今回からCSVの値3を敵の初期位置として扱います

CSVの指定値値用途
0空白
1足場
2ゴール
3敵の初期位置

読み込み時のタイル座標で敵を生成します

Stage.cppcpp
else
if (value == 3)
{
    tiles[y][x] = 0;
    AddEnemySpawn(x, y);
}

敵画像を読み込む

敵キャラクター画像敵キャラクター画像サンプル

Enemy.pngAssets/Imagesに配置し、ImageManagerで読み込みます

ImageManager.hcpp
enum class ImageId
{
    // ... 既存の画像識別子
    Enemy,
    Tile,
    Goal,
    Count
};

識別子とファイルパスの対応表に敵画像を追加します

ImageManager.cppcpp
constexpr CoreImageDefinition coreDefinitions[] =
{
    { ImageId::Enemy, L"Assets/Images/Enemy.png" },
    { ImageId::Tile, L"Assets/Images/Tile.png" },
    { ImageId::Goal, L"Assets/Images/Goal.png" }
};

描画時はImageId::Enemyを使って読み込み済み画像を取得します
敵ごとに画面上の表示位置を求め、DrawExtendGraphで描画します

Game.cppcpp
void Game::DrawEnemies() const
{
    const ImageResource& image = images.Get(ImageId::Enemy);

    for (int i = 0; i < enemyCount; i++)
    {
        const Enemy& enemy = enemies[i];
        if (!enemy.isActive) continue;

        const Rect imageRect = EnemyImageRect(enemy);
        const int screenX = imageRect.x - cameraX;

        DrawExtendGraph(
            screenX, imageRect.y,
            screenX + imageRect.width,
            imageRect.y + imageRect.height,
            image.handle, TRUE);
    }
}

敵の初期位置

Stage.hcpp
struct SpawnPoint
{
    int x;
    int y;
};
Stage.hcpp
static const int maxEnemySpawns = 32;

SpawnPoint enemySpawns[maxEnemySpawns] = {};
int enemySpawnCount = 0;

CSVを読み直す際には、enemySpawnCount0に戻すのを忘れないようにしましょう

固定長配列で複数の敵を管理する

敵の位置と大きさに加えて、横速度vxと1ピクセル未満の移動量を残すremainderXも敵ごとに持たせます

Game.hcpp
struct Enemy
{
    int x;
    int y;
    int width;
    int height;
    float vx;
    float remainderX;
    int minX;
    int maxX;
    bool isActive;
};

プレイヤーと同じく、速度はfloat、画面上の座標はintで扱います
速度をfloatにすると、1.5f2.25fのような値も設定できます
整数に満たない移動量はremainderXへ残し、次のフレームへ引き継ぎます

Game.hcpp
static const int maxEnemyCount = Stage::maxEnemySpawns;

Enemy enemies[maxEnemyCount] = {};
int enemyCount = 0;

enemyCountが配置された敵の数です
CSVの敵配置数に応じて変化します

Game.cppcpp
void Game::ResetEnemies()
{
    enemyCount = stage.GetEnemySpawnCount();

    for (int i = 0; i < enemyCount; i++)
    {
        const SpawnPoint spawn = stage.GetEnemySpawn(i);
        Enemy& enemy = enemies[i];

        enemy.x = spawn.x + 2;
        enemy.y = spawn.y + Stage::tileSize - 28;
        enemy.width = 28;
        enemy.height = 28;
        enemy.vx = (i % 2 == 0) ? 2.0f : -2.0f;
        enemy.remainderX = 0.0f;
        enemy.minX = enemy.x - 64;
        enemy.maxX = enemy.x + 64;
        enemy.isActive = true;
    }
}

敵を更新する

すべての敵に更新、描画、接触判定を行います
横移動では、速度をremainderXへ足し、整数部分だけを座標へ反映します

Game.cppcpp
void Game::UpdateEnemies()
{
    for (int i = 0; i < enemyCount; i++)
    {
        Enemy& enemy = enemies[i];
        if (!enemy.isActive) continue;

        enemy.remainderX += enemy.vx;
        const int pixelMove =
            static_cast<int>(enemy.remainderX);
        enemy.remainderX -= pixelMove;
        enemy.x += pixelMove;

        if (enemy.x < enemy.minX)
        {
            enemy.x = enemy.minX;
            enemy.vx = -enemy.vx;
            enemy.remainderX = 0.0f;
        }
        else if (enemy.x > enemy.maxX)
        {
            enemy.x = enemy.maxX;
            enemy.vx = -enemy.vx;
            enemy.remainderX = 0.0f;
        }
    }
}

処理中の敵の情報はEnemy& enemyで参照します
巡回範囲の端で向きを変えるときは、直前の向きで残った端数も0.0fへ戻します

Game.cppcpp
for (int i = 0; i < enemyCount; i++)
{
    const Enemy& enemy = enemies[i];

    if (enemy.isActive &&
        IsHitRect(PlayerRect(), EnemyRect(enemy)))
    {
        gameState = GameOver;
        sounds.StopBgm();
        sounds.PlaySe(SoundId::GameOver);
        return;
    }
}

実習

実習1: CSVから敵を生成する

  1. Enemy.pngAssets/Imagesへ配置し、ImageManagerで読み込む
  2. CSVの値3を敵配置として読み取る
  3. 敵配置をワールド座標へ変換する
  4. 地形タイルには0を保存する
  5. 敵配置数をデバッグ表示で確認する

実習2: 複数の敵を動かす

  1. Enemyを固定長配列へ変更する
  2. ResetEnemies()で全敵を初期化する
  3. vxremainderXを使って全敵を移動する
  4. 更新、描画、接触判定をループへ変更する
  5. CSVへ敵を追加し、コードを変更せず出現数が増えることを確認する

今回の完成条件

  • CSVの値3から複数の敵を配置できる
  • Enemy.pngを読み込み、すべての敵を画像で表示できる
  • 敵配置セルが地形の当たり判定にならない
  • 全敵が個別の位置、浮動小数点数の速度、移動端数を持つ
  • どの敵と接触してもゲームオーバーになる
  • リトライ時に全敵がCSVの初期位置へ戻る
  • 最大数を超えた敵を配列へ書き込まない

よくあるトラブル

敵の表示が配置したタイルの高さに合わない

CSV座標はタイル左上です
敵の足元をタイル下端へ合わせるため、敵の高さを引いてください

1体目しか動かない

更新、描画、接触判定のすべてがenemyCount回のループになっているか確認します

1.5fにすると移動が不規則に見える

enemy.remainderXへ速度を加え、整数部分を引いた残りを次のフレームへ保存しているか確認します
enemy.x += static_cast<int>(enemy.vx)だけでは、毎フレーム0.5が失われます

リトライのたびに敵が増える

CSVを読み直す場合はenemySpawnCountを、敵を復元する場合はenemyCountを初期化してから追加します

まとめ

  • CSVへ敵の初期位置を記録しました
  • Enemy.pngImageManagerで読み込み、敵の描画に使用しました
  • 地形データと敵配置データを分けました
  • 固定長配列と使用中の要素数で複数の敵を管理しました
  • プレイヤーと同じく、浮動小数点数の速度と端数保持で敵を移動しました
  • 同じ処理を全敵へ繰り返し適用しました
  • リトライ時にCSVの配置から全敵を復元しました