Appearance
第07回 CSVによる敵の複数配置
前回: 第06回 サウンドとゲームイベント / 次回: 第08回 敵の物理挙動とライフサイクル
今回の授業内容
- CSVへ敵の初期位置を記録する
- 敵配置データと地形タイルを分けて読み込む
- 固定長配列で複数の敵を管理する
- 浮動小数点数の速度と端数保持で敵を移動する
- 全敵を更新、描画、接触判定する
- リトライ時にCSVの配置から全敵を復元する
今回の配布コード
第06回のゲームに、CSVの指定で敵を配置する機能を追加したコードです
タイトル画面とタイトル用BGMはまだ含めず、起動後すぐにプレイを開始します
ゲーム全体の画面遷移は第10回で追加します
配布ファイル一覧
| ファイル | 内容 |
|---|---|
| Source/Stage.h | 敵配置座標と最大配置数の宣言 |
| Source/Stage.cpp | CSVの敵配置番号の読み込み |
| 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.pngをAssets/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を読み直す際には、enemySpawnCountを0に戻すのを忘れないようにしましょう
固定長配列で複数の敵を管理する
敵の位置と大きさに加えて、横速度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.5fや2.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から敵を生成する
Enemy.pngをAssets/Imagesへ配置し、ImageManagerで読み込む- CSVの値
3を敵配置として読み取る - 敵配置をワールド座標へ変換する
- 地形タイルには
0を保存する - 敵配置数をデバッグ表示で確認する
実習2: 複数の敵を動かす
Enemyを固定長配列へ変更するResetEnemies()で全敵を初期化するvxとremainderXを使って全敵を移動する- 更新、描画、接触判定をループへ変更する
- 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.pngをImageManagerで読み込み、敵の描画に使用しました- 地形データと敵配置データを分けました
- 固定長配列と使用中の要素数で複数の敵を管理しました
- プレイヤーと同じく、浮動小数点数の速度と端数保持で敵を移動しました
- 同じ処理を全敵へ繰り返し適用しました
- リトライ時にCSVの配置から全敵を復元しました