# 2Dプラットフォーマー 完成版

第13〜24回で制作する新作2Dプラットフォーマーの完成コードです。デバッグ表示で比較調整でき、受講者自身の地形画像を追加し、Release成果物として渡せる構成になっています。

## 完成した機能

- `ImageId::Background`で管理し、画面全体へ固定表示する背景画像
- CSVから読み込む可変サイズの横スクロールステージ、開始位置、行・列付きエラー診断
- `StageCell`と定義表によるCSV値、複数種類の地形画像・地形判定・トラップの管理
- 加速度、摩擦、重力、可変ジャンプ、コヨーテタイム、入力バッファ
- 表示矩形と命中矩形を分離したPlayer、Enemy、Goal
- Enemyの重力、地形衝突、壁・崖での折り返し、踏みつけ
- タイトル、プレイ中、ゲームクリア、ゲームオーバー、リトライ
- 状態に応じたBGMとSE
- 画像・音声IDと定義表の件数・重複・登録内容の検証
- カメラ追従とアニメーション
- 固定60Hzのゲームループと秒単位の速度・加速度

## F1デバッグ表示

F1で、画像を表示する範囲とゲーム判定に使う範囲を同時に確認できます。

| 色 | 表示内容 |
| --- | --- |
| 水色 | Player、Enemy、Goalの表示矩形 |
| 赤 | Playerの命中矩形 |
| 緑 | Enemyの命中矩形 |
| 黄 | Goalのクリア判定範囲 |
| 青 | 地形判定を持つタイルのグリッド |
| 紫 | 触れるとゲームオーバーになるトラップ判定 |

画像ファイルのピクセル数、ワールド上の表示サイズ、命中判定サイズは別の値です。Player、Enemy、Goalの表示・判定は`Source/Platformer/Tuning/GameTuning.h`で変更します。地形の`TileSize`は、ステージ座標と衝突計算の共通グリッド単位です。地形画像の元のピクセル数は`TileSize`と一致していなくても、描画時に指定サイズへ拡大・縮小されます。

## 地形またはトラップの種類を追加する

本編ではCSV値`10`のレンガ、`11`の苔石、`20`の棘トラップを使っています。`1`～`9`は進行・ギミック、`10`～`19`は通常地形・障害物、`20`～`29`はトラップ、`90`～`99`は敵配置の値域です。種類を追加するときは次の順に登録します。

1. `Assets/Images`へ画像を追加する。
1. `ImageManager.h`の`ImageId`と、`ImageManager.cpp`の画像パスを追加する。
1. `Stage.h`の`StageCell`へ、既存値と重複しない名前と整数値を追加する。配布済みCSVとの互換性を保つため、既存値は変更しない。
1. `Stage.cpp`の`tileDefinitions`へ、`StageCell`、`ImageId`、`isSolid`、`isHazard`、`drawRect`、`hazardRect`を1行追加する。
1. `Stage01.csv`の任意の地形値を、新しい整数値へ変更する。
1. F1表示で画像、青い地形判定、紫のトラップ判定を確認する。

描画と判定は同じ定義表を参照します。種類ごとの`if`を`Stage::Draw()`や`Stage::IsSolidTile()`へ追加しません。`drawRect`で表示位置と大きさを決め、通常地形は`isSolid`を`true`、棘などのトラップは`isHazard`を`true`にします。トラップ画像の透明部分を即死判定に含めないよう、`hazardRect`は見た目より少し内側へ設定します。装飾だけなら両方を`false`にします。

CSVの整数は読み込み時に`StageCell`へ変換し、`TryParseStageCell()`で検証します。追加した地形・トラップは定義表に登録すると有効になり、名前だけ追加して定義を忘れた値や、どこにも定義されていない値は読み込みエラーになります。エラー画面には原因となった行・列と、不正な値や不足した必須セルを表示します。

`ImageId`と画像定義、および`SoundId`と音声定義は、IDごとに1件ずつ必要です。件数の不足はコンパイル時に、IDの重複、空のパス、不正な音量は初期化時に検出します。

値域はCSVを整理するために使い、`value >= 90`のような範囲判定には使いません。描画・衝突・ゲーム進行は、列挙子の名前と定義表に登録した性質から判断します。

## 高速移動時のコリジョン

現在の`Stage::ResolveHorizontal()`と`ResolveVertical()`は、1フレーム分を移動した後の矩形が地形と重なっていれば、タイル境界まで位置を戻します。既定値では1フレームの最大移動量がタイルの32pxより小さいため、通常のプレイで問題は起きません。

速度を極端に大きくし、1フレームで地形タイル全体を飛び越えると、移動後の矩形が地形と重ならず、コリジョンを通り抜ける可能性があります。本編では処理量が移動ピクセル数に比例する1px単位の反復判定は採用せず、この制約を許容します。

高速移動にも対応する発展案としては、移動量をタイル幅以下へ分割して複数回判定する方法があります。`<algorithm>`をインクルードし、横移動なら次のような補助関数をPlayerやEnemyの移動処理から利用できます。

```cpp
bool MoveHorizontalInTileSteps(
    const Stage& stage, dxgame::Rect& rect, int moveX)
{
    int remaining = moveX;
    while (remaining != 0)
    {
        const int step = std::clamp(
            remaining, -Stage::TileSize, Stage::TileSize);
        if (stage.ResolveHorizontal(rect, step))
            return true;

        remaining -= step;
    }
    return false;
}
```

縦方向も同じ考え方で分割できます。さらに高速な弾や斜め移動まで正確に扱う場合は、移動前から移動後までの経路と地形の交差時刻を求めるswept AABBも候補になります。どちらも第24回の必須範囲には含めません。

## 最終調整の確認項目

- PlayerとEnemyの画像が命中矩形から不自然にはみ出していない。
- Goalの見た目より少し内側へ入ったときにクリアする。
- ジャンプの高さ、滞空時間、加速、停止がステージ幅に合っている。
- Enemyが壁と崖で折り返し、上から踏める。
- すべての地形画像に意図した地形判定が付いている。
- トラップの透明部分では死亡せず、紫の判定へ触れたときだけゲームオーバーになる。
- タイトルからクリアまたはゲームオーバーまで進み、リトライとタイトル復帰ができる。

## 操作

| 状態 | キー | 操作 |
| --- | --- | --- |
| タイトル | Z | ゲーム開始 |
| プレイ中 | 左右矢印 | 移動 |
| プレイ中 | Z | ジャンプ |
| 結果画面 | Z | リトライ |
| 結果画面 | T | タイトルへ戻る |
| 常時 | F1 | デバッグ表示の切り替え |
| 常時 | Esc | 終了 |

## Release成果物を作る

1. Visual Studioで`Release`・`x64`を選び、リビルドする。
1. `x64/Release`に`PlatformerGame2D.exe`と`Assets`が揃っていることを確認する。
1. 配布用フォルダーへexe、Assets、このREADMEをコピーする。
1. フォルダーをZIP化し、制作プロジェクトとは別の場所へ展開する。
1. 展開先でタイトルから結果画面まで通しプレイし、画像、音、操作、終了条件を確認する。

修正はプロジェクト直下の`Source`、`Assets`、`GameTuning.h`へ戻して行います。`x64/Release/Assets`はビルドで作り直される生成物なので、直接編集しません。
