Appearance
第18回 重力、ジャンプ、着地
4 / 4 着地した瞬間を通知する
このステップの目的
接地状態の変化から着地イベントを作り、ジャンプと着地を確認します。
状態と出来事を分ける
接地状態は床に立つ間続きます。着地イベントは空中から接地へ変わった一回だけです。第20回では、この出来事に音を接続します。
Player.hでPlayerEventsを定義し、Updateはその値を返します。UpdateInputとUpdatePhysicsにはPlayerEvents&を渡します。参照を使うことで、二つの関数が同じイベント変数へ結果を書き込めます。
Player.h(名前空間内、Playerクラスの前)cpp
struct PlayerEvents
{
bool jumped = false;
bool landed = false;
};Player.cppcpp
PlayerEvents Player::Update(const Input& input, const Stage& stage,
float deltaTime)
{
PlayerEvents events;
UpdateInput(input, deltaTime, events);
UpdatePhysics(stage, deltaTime, events);
return events;
}
void Player::UpdatePhysics(const Stage& stage, float deltaTime,
PlayerEvents& events)
{
const bool wasOnGround = onGround_;
const float gravity = velocity_.y < 0.0f
? tuning::player::RiseGravity
: tuning::player::FallGravity;
velocity_.y += gravity * deltaTime;
velocity_.y = std::min(velocity_.y,
tuning::player::MaxFallSpeed);
MoveHorizontal(deltaTime);
MoveVertical(stage, deltaTime);
dxgame::Rect groundProbe = GetHitbox();
groundProbe.y += 1.0f;
onGround_ = stage.IsSolidAtRect(groundProbe);
if (onGround_ && velocity_.y >= 0.0f)
{
velocity_.y = 0.0f;
remainderY_ = 0.0f;
}
if (!wasOnGround && onGround_)
events.landed = true;
}wasOnGroundは物理更新前の接地状態です。!wasOnGround && onGround_で着地への変化を調べます。MoveVerticalの移動量が0でも足元を調べる処理を残します。
画面と変数で確認する
空中から落下して床に着地し、Zでジャンプします。F1のデバッグ表示で接地状態を確認してください。さらにUpdatePhysicsのevents.landed = trueへブレークポイントを置き、着地時に一回だけ止まることを確認します。静止中に繰り返し止まる場合は、足元の確認とwasOnGroundを記録する位置を調べます。
次回は横移動にも地形判定を加え、天井への衝突を扱います。
チェックリスト
- 床で静止しても接地状態が保たれる。
- 足場から落ちると接地状態が解除される。
- 着地イベントが着地の瞬間だけ成立する。
- 空中でキーを連打しても再ジャンプしない。
参考実装
今回追加・変更するファイルの一例です。宣言や補助処理を確認したいときに開いてください。自分のコードへ必要な変更を反映し、既存の調整値や素材をそのまま上書きしないようにします。
Source/Platformer/Game.cpp
Source/Platformer/Game.cppcpp
#include <Windows.h>
#include <string>
#include "DxLib.h"
#include "Config/ApplicationConfig.h"
#include "Platformer/Game.h"
#include "Platformer/Tuning/GameTuning.h"
namespace dxgame::platformer
{
bool Game::Initialize()
{
if (!images_.LoadAll())
return false;
constexpr wchar_t StagePath[] = L"Assets/Maps/Stage01.csv";
if (!stage_.Initialize(StagePath))
{
const std::wstring message = std::wstring(StagePath) + L"\n" +
stage_.GetLastLoadError();
MessageBoxW(nullptr, message.c_str(), L"Map Load Error", MB_OK);
return false;
}
ResetStage();
return true;
}
void Game::MainLoop()
{
// 小数分の待ち時間を次フレームへ持ち越す
double nextFrame = GetNowCount();
while (ProcessMessage() == 0)
{
input_.Update();
if (input_.IsHeld(Key::Escape))
break;
Update(dxgame::config::DeltaTime);
ClearDrawScreen();
Draw();
ScreenFlip();
nextFrame += dxgame::config::FrameMilliseconds;
const int waitMilliseconds =
static_cast<int>(nextFrame - GetNowCount());
if (waitMilliseconds > 0)
WaitTimer(waitMilliseconds);
else
nextFrame = GetNowCount();
}
}
void Game::Update(float deltaTime)
{
UpdateDebugToggle();
player_.Update(input_, stage_, deltaTime);
ResetAfterFall();
camera_.Update(player_.GetPosition(), stage_.GetWorldWidth());
}
void Game::Draw() const
{
DrawBackground();
stage_.Draw(camera_.GetX(), images_);
player_.Draw(camera_, images_);
DrawHud();
DrawDebug();
}
void Game::DrawBackground() const
{
images_.Draw(ImageId::Background, { 0.0f, 0.0f,
static_cast<float>(dxgame::config::ScreenWidth),
static_cast<float>(dxgame::config::ScreenHeight) });
}
void Game::DrawHud() const
{
const int textColor = GetColor(255, 255, 255);
DrawString(12, 10, L"LEFT/RIGHT: MOVE", textColor);
DrawString(12, 30, L"Z: JUMP", textColor);
DrawString(12, 50, L"F1: DEBUG ESC: QUIT", textColor);
}
void Game::DrawDebug() const
{
if (!debugVisible_)
return;
const dxgame::Rect playerDrawRect =
camera_.WorldToScreen(player_.GetDrawRect());
DrawBox(static_cast<int>(playerDrawRect.x),
static_cast<int>(playerDrawRect.y),
static_cast<int>(playerDrawRect.x + playerDrawRect.width),
static_cast<int>(playerDrawRect.y + playerDrawRect.height),
GetColor(80, 240, 255), FALSE);
const dxgame::Rect playerRect =
camera_.WorldToScreen(player_.GetHitbox());
DrawBox(static_cast<int>(playerRect.x), static_cast<int>(playerRect.y),
static_cast<int>(playerRect.x + playerRect.width),
static_cast<int>(playerRect.y + playerRect.height),
GetColor(255, 80, 80), FALSE);
const dxgame::Rect goalDrawRect =
camera_.WorldToScreen(stage_.GetGoalDrawRect());
DrawBox(static_cast<int>(goalDrawRect.x),
static_cast<int>(goalDrawRect.y),
static_cast<int>(goalDrawRect.x + goalDrawRect.width),
static_cast<int>(goalDrawRect.y + goalDrawRect.height),
GetColor(80, 240, 255), FALSE);
const dxgame::Rect goalRect =
camera_.WorldToScreen(stage_.GetGoalTrigger());
DrawBox(static_cast<int>(goalRect.x), static_cast<int>(goalRect.y),
static_cast<int>(goalRect.x + goalRect.width),
static_cast<int>(goalRect.y + goalRect.height),
GetColor(255, 220, 40), FALSE);
DrawBox(8, 76, 300, 178, GetColor(0, 0, 0), TRUE);
const int textColor = GetColor(255, 255, 255);
const dxgame::Vector2& position = player_.GetPosition();
const dxgame::Vector2& velocity = player_.GetVelocity();
DrawFormatString(14, 84, textColor,
L"pos: %.1f, %.1f", position.x, position.y);
DrawFormatString(14, 104, textColor,
L"velocity: %.2f, %.2f", velocity.x, velocity.y);
DrawFormatString(14, 124, textColor,
L"ground: %d camera: %d", player_.IsOnGround() ? 1 : 0,
camera_.GetX());
DrawFormatString(14, 144, textColor, L"csv: %d",
stage_.IsLoadedFromCsv() ? 1 : 0);
DrawString(14, 164,
L"cyan: draw red: player yellow: goal", textColor);
}
void Game::UpdateDebugToggle()
{
if (input_.IsPressed(Key::Debug))
debugVisible_ = !debugVisible_;
}
void Game::ResetAfterFall()
{
if (player_.GetPosition().y <=
stage_.GetWorldHeight() + tuning::stage::FallResetMargin)
{
return;
}
ResetStage();
}
void Game::ResetStage()
{
player_.Reset(stage_.GetPlayerSpawn());
camera_.Update(player_.GetPosition(), stage_.GetWorldWidth());
}
}Source/Platformer/Game.h
Source/Platformer/Game.hcpp
#pragma once
#include "Platformer/Camera.h"
#include "Platformer/ImageManager.h"
#include "Platformer/Input.h"
#include "Platformer/Player.h"
#include "Platformer/Stage.h"
namespace dxgame::platformer
{
// 入力、プレイヤー、ステージ、描画順を調整する
class Game
{
public:
// 必須画像、音声、CSVを読み込む
bool Initialize();
// ウィンドウ終了まで更新と描画を繰り返す
void MainLoop();
private:
void Update(float deltaTime);
void Draw() const;
void DrawBackground() const;
void DrawHud() const;
void DrawDebug() const;
void UpdateDebugToggle();
void ResetAfterFall();
void ResetStage();
Input input_;
ImageManager images_;
Stage stage_;
Camera camera_;
Player player_;
bool debugVisible_ = false;
};
}Source/Platformer/Player.cpp
Source/Platformer/Player.cppcpp
#include <algorithm>
#include <cmath>
#include "Platformer/Camera.h"
#include "Platformer/ImageManager.h"
#include "Platformer/Input.h"
#include "Platformer/Player.h"
#include "Platformer/Stage.h"
#include "Platformer/Tuning/GameTuning.h"
namespace dxgame::platformer
{
void Player::Reset(const dxgame::Vector2& position)
{
position_ = position;
velocity_ = {};
remainderX_ = 0.0f;
remainderY_ = 0.0f;
onGround_ = false;
}
PlayerEvents Player::Update(const Input& input, const Stage& stage,
float deltaTime)
{
PlayerEvents events;
UpdateInput(input, deltaTime, events);
UpdatePhysics(stage, deltaTime, events);
return events;
}
void Player::Draw(const Camera& camera, const ImageManager& images) const
{
const dxgame::Rect screenRect = camera.WorldToScreen(GetDrawRect());
images.Draw(ImageId::Player, screenRect);
}
dxgame::Rect Player::GetHitbox() const
{
return tuning::player::Hitbox.MovedBy(position_);
}
dxgame::Rect Player::GetDrawRect() const
{
return tuning::player::DrawRect.MovedBy(position_);
}
const dxgame::Vector2& Player::GetPosition() const
{
return position_;
}
const dxgame::Vector2& Player::GetVelocity() const
{
return velocity_;
}
bool Player::IsOnGround() const
{
return onGround_;
}
void Player::UpdateInput(const Input& input, float deltaTime,
PlayerEvents& events)
{
const bool moveLeft = input.IsHeld(Key::Left);
const bool moveRight = input.IsHeld(Key::Right);
if (moveLeft && !moveRight)
{
velocity_.x -= tuning::player::Acceleration * deltaTime;
}
else if (moveRight && !moveLeft)
{
velocity_.x += tuning::player::Acceleration * deltaTime;
}
else if (velocity_.x > 0.0f)
{
velocity_.x = std::max(0.0f,
velocity_.x - tuning::player::Friction * deltaTime);
}
else if (velocity_.x < 0.0f)
{
velocity_.x = std::min(0.0f,
velocity_.x + tuning::player::Friction * deltaTime);
}
velocity_.x = std::clamp(velocity_.x,
-tuning::player::MaxSpeed, tuning::player::MaxSpeed);
if (onGround_ && input.IsPressed(Key::Jump))
{
velocity_.y = tuning::player::JumpPower;
remainderY_ = 0.0f;
onGround_ = false;
events.jumped = true;
}
}
void Player::UpdatePhysics(const Stage& stage, float deltaTime,
PlayerEvents& events)
{
const bool wasOnGround = onGround_;
const float gravity = velocity_.y < 0.0f
? tuning::player::RiseGravity
: tuning::player::FallGravity;
velocity_.y += gravity * deltaTime;
velocity_.y = std::min(velocity_.y,
tuning::player::MaxFallSpeed);
MoveHorizontal(deltaTime);
MoveVertical(stage, deltaTime);
dxgame::Rect groundProbe = GetHitbox();
groundProbe.y += 1.0f;
onGround_ = stage.IsSolidAtRect(groundProbe);
if (onGround_ && velocity_.y >= 0.0f)
{
velocity_.y = 0.0f;
remainderY_ = 0.0f;
}
if (!wasOnGround && onGround_)
events.landed = true;
}
void Player::MoveHorizontal(float deltaTime)
{
remainderX_ += velocity_.x * deltaTime;
const int pixelMove = static_cast<int>(remainderX_);
remainderX_ -= static_cast<float>(pixelMove);
position_.x += static_cast<float>(pixelMove);
}
void Player::MoveVertical(const Stage& stage, float deltaTime)
{
remainderY_ += velocity_.y * deltaTime;
const int pixelMove = static_cast<int>(remainderY_);
remainderY_ -= static_cast<float>(pixelMove);
if (pixelMove == 0)
return;
dxgame::Rect movedRect = GetHitbox();
if (stage.ResolveVertical(movedRect, pixelMove))
{
velocity_.y = 0.0f;
remainderY_ = 0.0f;
}
position_.y = movedRect.GetCenter().y;
}
}Source/Platformer/Player.h
Source/Platformer/Player.hcpp
#pragma once
#include "DxGame/Rect.h"
#include "DxGame/Vector2.h"
namespace dxgame::platformer
{
class Camera;
class ImageManager;
class Input;
class Stage;
// Player内で成立した出来事を、音や演出へ接続するためにGameへ返す
struct PlayerEvents
{
bool jumped = false;
bool landed = false;
};
// プレイヤーの位置、速度、接地状態を管理する
class Player
{
public:
// プレイ開始時の状態へ戻す
void Reset(const dxgame::Vector2& position);
// 入力、重力、地形衝突、アニメーションを1フレーム更新する
PlayerEvents Update(const Input& input, const Stage& stage,
float deltaTime);
// カメラを考慮してプレイヤーを描画する
void Draw(const Camera& camera, const ImageManager& images) const;
dxgame::Rect GetHitbox() const;
dxgame::Rect GetDrawRect() const;
const dxgame::Vector2& GetPosition() const;
const dxgame::Vector2& GetVelocity() const;
bool IsOnGround() const;
private:
void UpdateInput(const Input& input, float deltaTime,
PlayerEvents& events);
void UpdatePhysics(const Stage& stage, float deltaTime,
PlayerEvents& events);
void MoveHorizontal(float deltaTime);
void MoveVertical(const Stage& stage, float deltaTime);
dxgame::Vector2 position_ = {};
dxgame::Vector2 velocity_ = {};
float remainderX_ = 0.0f;
float remainderY_ = 0.0f;
bool onGround_ = false;
};
}Source/Platformer/Stage.cpp
Source/Platformer/Stage.cppcpp
#include <algorithm>
#include <cmath>
#include <fstream>
#include <sstream>
#include <stdexcept>
#include <string>
#include <utility>
#include "Config/ApplicationConfig.h"
#include "Platformer/Stage.h"
namespace dxgame::platformer
{
namespace
{
struct TileDefinition
{
StageCell cell;
ImageId imageId;
bool isSolid;
};
constexpr TileDefinition tileDefinitions[] =
{
{ StageCell::Brick, ImageId::Tile, true },
{ StageCell::Moss, ImageId::TileMoss, true }
};
const TileDefinition* FindTileDefinition(StageCell cell)
{
for (const TileDefinition& definition : tileDefinitions)
{
if (definition.cell == cell)
return &definition;
}
return nullptr;
}
bool TryParseStageCell(int value, StageCell& cell)
{
cell = static_cast<StageCell>(value);
if (cell == StageCell::Empty ||
cell == StageCell::Goal ||
cell == StageCell::PlayerSpawn)
{
return true;
}
return FindTileDefinition(cell) != nullptr;
}
}
bool Stage::Initialize(const std::filesystem::path& csvPath)
{
loadedFromCsv_ = LoadFromCsv(csvPath);
return loadedFromCsv_;
}
bool Stage::IsSolidAtRect(const dxgame::Rect& rect) const
{
if (rect.width <= 0.0f || rect.height <= 0.0f)
return false;
const int leftTile = PixelToTile(rect.x);
const int rightTile = PixelToTile(rect.x + rect.width - 0.001f);
const int topTile = PixelToTile(rect.y);
const int bottomTile = PixelToTile(rect.y + rect.height - 0.001f);
for (int y = topTile; y <= bottomTile; y++)
{
for (int x = leftTile; x <= rightTile; x++)
{
if (IsSolidTile(x, y))
return true;
}
}
return false;
}
bool Stage::ResolveVertical(dxgame::Rect& rect, int moveY) const
{
if (moveY == 0)
return false;
// 移動後の矩形だけを調べるため、1フレームでタイルを越える
// 極端な速度ではコリジョンを通り抜ける可能性がある。
// 極端な速度ではコリジョンを通り抜ける可能性がある
rect.y += static_cast<float>(moveY);
const int leftTile = PixelToTile(rect.x);
const int rightTile = PixelToTile(rect.x + rect.width - 0.001f);
const int topTile = PixelToTile(rect.y);
const int bottomTile = PixelToTile(rect.y + rect.height - 0.001f);
bool hit = false;
float resolvedY = rect.y;
for (int y = topTile; y <= bottomTile; y++)
{
for (int x = leftTile; x <= rightTile; x++)
{
if (!IsSolidTile(x, y))
continue;
const float candidate = (moveY > 0)
? static_cast<float>(y * TileSize) - rect.height
: static_cast<float>((y + 1) * TileSize);
if (!hit ||
(moveY > 0 && candidate < resolvedY) ||
(moveY < 0 && candidate > resolvedY))
{
resolvedY = candidate;
}
hit = true;
}
}
rect.y = resolvedY;
return hit;
}
void Stage::Draw(int cameraX, const ImageManager& images) const
{
const int firstTileX = std::max(0, cameraX / TileSize);
const int lastTileX = std::min(mapWidth_ - 1,
(cameraX + dxgame::config::ScreenWidth) / TileSize + 1);
for (int y = 0; y < mapHeight_; y++)
{
for (int x = firstTileX; x <= lastTileX; x++)
{
const StageCell cell = cells_[y][x];
if (cell == StageCell::Empty)
continue;
DrawTile(x * TileSize - cameraX, y * TileSize, cell, images);
}
}
const dxgame::Rect goalDrawRect = GetGoalDrawRect();
images.Draw(ImageId::Goal, {
goalDrawRect.x - static_cast<float>(cameraX), goalDrawRect.y,
goalDrawRect.width, goalDrawRect.height
});
}
dxgame::Rect Stage::GetGoalDrawRect() const
{
return tuning::goal::DrawRect.MovedBy(goalPosition_);
}
dxgame::Rect Stage::GetGoalTrigger() const
{
return tuning::goal::Trigger.MovedBy(goalPosition_);
}
const dxgame::Vector2& Stage::GetPlayerSpawn() const
{
return playerSpawnPosition_;
}
int Stage::GetWorldWidth() const
{
return mapWidth_ * TileSize;
}
int Stage::GetWorldHeight() const
{
return mapHeight_ * TileSize;
}
int Stage::GetMapWidth() const
{
return mapWidth_;
}
int Stage::GetMapHeight() const
{
return mapHeight_;
}
bool Stage::IsLoadedFromCsv() const
{
return loadedFromCsv_;
}
const std::wstring& Stage::GetLastLoadError() const
{
return lastLoadError_;
}
bool Stage::LoadFromCsv(const std::filesystem::path& csvPath)
{
Clear();
std::ifstream file(csvPath);
if (!file.is_open())
return FailLoad(L"ファイルを開けません。");
bool foundGoal = false;
bool foundPlayerSpawn = false;
int expectedWidth = -1;
std::string line;
int y = 0;
while (std::getline(file, line))
{
const int lineNumber = y + 1;
if (line.empty())
return FailLoad(L"空行は使用できません。", lineNumber, 1);
if (line.back() == ',')
{
const int columnNumber =
static_cast<int>(std::count(
line.begin(), line.end(), ',')) + 1;
return FailLoad(L"空のセルは使用できません。",
lineNumber, columnNumber);
}
std::stringstream lineStream(line);
std::string cell;
int x = 0;
std::vector<StageCell> row;
while (std::getline(lineStream, cell, ','))
{
const int columnNumber = x + 1;
int value = 0;
std::size_t parsedLength = 0;
try
{
value = std::stoi(cell, &parsedLength);
}
catch (const std::invalid_argument&)
{
return FailLoad(L"整数を入力してください。",
lineNumber, columnNumber);
}
catch (const std::out_of_range&)
{
return FailLoad(L"整数の範囲を超えています。",
lineNumber, columnNumber);
}
if (parsedLength != cell.size())
{
return FailLoad(L"整数以外の文字が含まれています。",
lineNumber, columnNumber);
}
StageCell csvCell = StageCell::Empty;
if (!TryParseStageCell(value, csvCell))
{
return FailLoad(
L"未定義のStageCell値です: " +
std::to_wstring(value),
lineNumber, columnNumber);
}
StageCell mapCell = StageCell::Empty;
if (csvCell == StageCell::Goal)
{
if (foundGoal)
{
return FailLoad(L"Goalは1つだけ配置してください。",
lineNumber, columnNumber);
}
goalPosition_ = {
x * static_cast<float>(TileSize) + TileSize * 0.5f,
y * static_cast<float>(TileSize) + TileSize * 0.5f
};
foundGoal = true;
}
else if (csvCell == StageCell::PlayerSpawn)
{
if (foundPlayerSpawn)
{
return FailLoad(
L"PlayerSpawnは1つだけ配置してください。",
lineNumber, columnNumber);
}
playerSpawnPosition_ = {
x * static_cast<float>(TileSize) + TileSize * 0.5f,
y * static_cast<float>(TileSize) + TileSize * 0.5f
};
foundPlayerSpawn = true;
}
else
{
mapCell = csvCell;
}
row.push_back(mapCell);
x++;
}
if (row.empty())
return FailLoad(L"セルがありません。", lineNumber, 1);
if (expectedWidth < 0)
{
expectedWidth = static_cast<int>(row.size());
}
else if (static_cast<int>(row.size()) != expectedWidth)
{
const int actualWidth = static_cast<int>(row.size());
const int errorColumn = actualWidth < expectedWidth
? actualWidth + 1
: expectedWidth + 1;
return FailLoad(
L"列数が一致しません。必要: " +
std::to_wstring(expectedWidth) + L"、実際: " +
std::to_wstring(actualWidth),
lineNumber, errorColumn);
}
cells_.push_back(std::move(row));
y++;
}
if (y == 0)
return FailLoad(L"CSVに行がありません。");
if (!foundGoal)
return FailLoad(L"Goalがありません。");
if (!foundPlayerSpawn)
return FailLoad(L"PlayerSpawnがありません。");
mapWidth_ = expectedWidth;
mapHeight_ = y;
return true;
}
bool Stage::FailLoad(const std::wstring& reason,
int lineNumber, int columnNumber)
{
std::wstring message;
if (lineNumber > 0)
{
message = L"行" + std::to_wstring(lineNumber);
if (columnNumber > 0)
message += L"、列" + std::to_wstring(columnNumber);
message += L": ";
}
message += reason;
Clear();
lastLoadError_ = std::move(message);
return false;
}
void Stage::Clear()
{
cells_.clear();
goalPosition_ = {};
playerSpawnPosition_ = {};
mapWidth_ = 0;
mapHeight_ = 0;
loadedFromCsv_ = false;
lastLoadError_.clear();
}
void Stage::DrawTile(int screenX, int screenY, StageCell cell,
const ImageManager& images) const
{
const TileDefinition* definition = FindTileDefinition(cell);
if (definition == nullptr)
return;
images.Draw(definition->imageId, {
static_cast<float>(screenX), static_cast<float>(screenY),
static_cast<float>(TileSize), static_cast<float>(TileSize)
});
}
int Stage::PixelToTile(float pixel)
{
return static_cast<int>(std::floor(pixel / TileSize));
}
bool Stage::IsSolidTile(int tileX, int tileY) const
{
if (tileX < 0 || tileX >= mapWidth_ || tileY < 0)
return true;
if (tileY >= mapHeight_)
return false;
const TileDefinition* definition =
FindTileDefinition(cells_[tileY][tileX]);
return definition != nullptr && definition->isSolid;
}
}Source/Platformer/Stage.h
Source/Platformer/Stage.hcpp
#pragma once
#include <filesystem>
#include <string>
#include <vector>
#include "DxGame/Rect.h"
#include "DxGame/Vector2.h"
#include "Platformer/ImageManager.h"
#include "Platformer/Tuning/GameTuning.h"
namespace dxgame::platformer
{
// ステージCSVの各整数値と意味を1か所で定義する
// 数値は外部ファイル形式との約束なので、追加後も既存値を変更しない
// 外部ファイル形式の定義値のため、追加後も既存値を変更しない
enum class StageCell : int
{
Empty = 0,
// 1~9: 進行・ギミック
Goal = 1,
PlayerSpawn = 2,
// 10~19: 通常地形・障害物
Brick = 10,
Moss = 11,
// 20~29: トラップ、90~99: 敵配置のために空けておく
};
// CSVマップ、地形判定、ゴールを管理する
class Stage
{
public:
static constexpr int TileSize = tuning::stage::TileSize;
// 実行フォルダーを基準にCSVを読み込み、失敗理由を保持する
// 実行フォルダを基準にCSVを読み込み、失敗理由を保持する
bool Initialize(const std::filesystem::path& csvPath);
// 矩形が地形と重なっているかを返す
bool IsSolidAtRect(const dxgame::Rect& rect) const;
// 縦方向の移動後に地形境界へ位置を補正する
bool ResolveVertical(dxgame::Rect& rect, int moveY) const;
// カメラ内に見えるタイルとゴールを描画する
void Draw(int cameraX, const ImageManager& images) const;
dxgame::Rect GetGoalDrawRect() const;
dxgame::Rect GetGoalTrigger() const;
const dxgame::Vector2& GetPlayerSpawn() const;
int GetWorldWidth() const;
int GetWorldHeight() const;
int GetMapWidth() const;
int GetMapHeight() const;
bool IsLoadedFromCsv() const;
const std::wstring& GetLastLoadError() const;
private:
bool LoadFromCsv(const std::filesystem::path& csvPath);
bool FailLoad(const std::wstring& reason,
int lineNumber = 0, int columnNumber = 0);
void Clear();
void DrawTile(int screenX, int screenY, StageCell cell,
const ImageManager& images) const;
static int PixelToTile(float pixel);
bool IsSolidTile(int tileX, int tileY) const;
std::vector<std::vector<StageCell>> cells_;
dxgame::Vector2 goalPosition_ = {};
dxgame::Vector2 playerSpawnPosition_ = {};
int mapWidth_ = 0;
int mapHeight_ = 0;
bool loadedFromCsv_ = false;
std::wstring lastLoadError_;
};
}Source/Platformer/Tuning/GameTuning.h
Source/Platformer/Tuning/GameTuning.hcpp
#pragma once
#include "Config/ApplicationConfig.h"
#include "DxGame/Rect.h"
namespace dxgame::platformer::tuning
{
// 距離はピクセル、時間は秒、速度はピクセル/秒、
// 加速度と重力はピクセル/秒²を基本単位とする。
// 加速度と重力はピクセル/秒²を基本単位とする
namespace player
{
// 中心座標を基準にした表示範囲と地形判定範囲
// 画像本来の大きさとは独立して調整する
inline constexpr Rect DrawRect =
Rect::Centered(32.0f, 48.0f).MovedBy({ 0.0f, -8.0f });
inline constexpr Rect Hitbox = Rect::Centered(24.0f, 30.0f);
// 左右入力中の加速度、最大速度、無入力時の減速度
// 速度の単位はピクセル/秒、加減速度はピクセル/秒²
inline constexpr float Acceleration = 1260.0f;
inline constexpr float MaxSpeed = 270.0f;
inline constexpr float Friction = 900.0f;
// 上昇中と落下中の重力、および最大落下速度
// 重力の単位はピクセル/秒²、速度はピクセル/秒
inline constexpr float RiseGravity = 1620.0f;
inline constexpr float FallGravity = 2700.0f;
inline constexpr float MaxFallSpeed = 720.0f;
// ジャンプ開始時の上向き速度 単位はピクセル/秒
// ジャンプ開始時の上向き速度(単位:ピクセル/秒)
// 画面上方向はY座標が減るため負の値にする
inline constexpr float JumpPower = -660.0f;
}
namespace stage
{
// タイル画像の元サイズとは独立した、ワールド上の1マスの大きさ
inline constexpr int TileSize = 32;
// マップ下端からこの距離を越えたら落下とみなす
inline constexpr float FallResetMargin = 128.0f;
}
namespace camera
{
// 追従対象を画面内のどのX座標へ置くか
inline constexpr float TargetScreenX =
dxgame::config::ScreenWidth * 0.5f;
}
namespace enemy
{
// 敵の中心座標を基準にした表示範囲と命中範囲
inline constexpr Rect DrawRect = Rect::Centered(32.0f, 32.0f);
inline constexpr Rect Hitbox = Rect::Centered(28.0f, 28.0f);
}
namespace goal
{
// ゴール画像の表示範囲と、クリア成立に使う範囲
inline constexpr Rect DrawRect = Rect::Centered(32.0f, 32.0f);
inline constexpr Rect Trigger = Rect::Centered(20.0f, 28.0f);
}
}