4 / 4 入力とプレイヤーの移動
キーボード入力から移動方向を算出し、速度と経過時間を用いてプレイヤーの位置を滑らかに更新します。
斜め移動の補助処理
開始用のPlayer::Update()には、斜めに動く速さを縦横とそろえる正規化と、画面端での制限が実装済みです。この2箇所を残し、入力による方向の指定と位置の更新を追加します。正規化の計算は第05回で扱います。
入力とアクションの分離
DXライブラリのCheckHitKeyでは、キーの状態を直接調べられます。この作品では、配布済みのInputクラスがキーを調べ、「左へ移動する(Action::MoveLeft)」などの操作へまとめます。左矢印とAキーのどちらでも同じ操作になるよう、次の対応表が用意されています。
// ゲーム内の操作(Action)と物理キーの対応表
constexpr Binding Bindings[] =
{
{ Action::MoveLeft, KEY_INPUT_LEFT },
{ Action::MoveLeft, KEY_INPUT_A },
{ Action::MoveRight, KEY_INPUT_RIGHT },
{ Action::MoveRight, KEY_INPUT_D },
// ...
};操作キーの変更やキーコンフィグ機能の追加が必要になったときも、対応表を変更すれば対応できます。プレイヤーの移動処理を変更する必要はありません。
キー入力から移動方向を算出する
Player::Update()では、毎フレーム渡されるinputオブジェクトを調べ、プレイヤーが移動しようとしている方向をベクトルとして計算します。
Player.cppのPlayer::Update()内で、static_cast<void>(input);を次の例の4つのif文へ置き換えてください。既存のVector2 direction;と、その後の正規化・画面端の制限は残します。
static_cast<void>(input);は、未実装の間だけ、未使用の引数に対する警告を抑えるための行です。
void Player::Update(const Input& input, float deltaTime)
{
Vector2 direction;
// 各方向のキー入力状態を調べる
if (input.IsDown(Action::MoveLeft))
direction.x -= 1.0f;
if (input.IsDown(Action::MoveRight))
direction.x += 1.0f;
if (input.IsDown(Action::MoveUp))
direction.y -= 1.0f;
if (input.IsDown(Action::MoveDown))
direction.y += 1.0f;画面座標系では右が正、下が正であるため、左と上には-1.0fを、右と下には+1.0fを加算します。
左右や上下のキーが同時に押された場合は、互いの値が相殺されて0.0fになるため、どちらか一方の入力が不自然に優先されることを防ぎます。
斜め移動時の速度補正(ベクトルの正規化)
以下は配布済みの補助処理です。追加せず、そのまま使います。
正規化の計算を確認する
右キーと下キーを同時に押した場合、方向ベクトルは{ 1.0f, 1.0f }となります。
このベクトルの長さ(大きさ)を三平方の定理で計算すると、次のようになります。
この値をそのまま使うと、斜めに移動するときだけ縦横の約1.41倍の速さになります。意図しない高速移動を防ぐため、方向ベクトルを補正します。
ベクトルの長さが1を超える場合は、各成分を長さで割ります。向きを保ったまま長さを1.0にそろえるこの処理を、ベクトルの正規化と呼びます。
// 斜め移動時に速度が1.41倍にならないよう、長さを1.0に制限する
float length = std::sqrt(direction.x * direction.x + direction.y * direction.y);
if (length > 1.0f)
{
direction.x /= length;
direction.y /= length;
}この計算により、斜め方向と縦横方向の1フレームの移動距離をそろえられます。
速度と経過時間を用いた座標の更新
正規化処理の後にあるstatic_cast<void>(deltaTime);の行を、次の2行へ置き換えます。方向に移動速度と固定の1/60秒(deltaTime)を掛け、現在位置positionを更新します。
// 移動量 = 方向 × 移動速度 × 1フレームの経過時間
position.x += direction.x * tuning::player::MoveSpeed * deltaTime;
position.y += direction.y * tuning::player::MoveSpeed * deltaTime;速度MoveSpeedはGameTuning.hにおいて「1秒あたりに進むピクセル数(ピクセル/秒)」として定義されています。
これに1フレームの秒数であるdeltaTime(1/60秒)を掛けることで、「このフレームで進むべき正確な距離」が求まります。
例えばMoveSpeedが240ピクセル/秒の場合、1フレーム(1/60秒)あたりの移動量は次のように計算されます。
画面外への移動制限
プレイヤーが画面の外へ飛び出してしまうのを防ぐため、移動後の座標を画面内の許容範囲に収める処理を適用します。
// 画面端を越えないように座標を制限する
const Rect& bounds = tuning::player::Bounds;
position.x = std::clamp(
position.x,
-bounds.x,
tuning::ScreenWidth - bounds.x - bounds.width);
position.y = std::clamp(
position.y,
-bounds.y,
tuning::ScreenHeight - bounds.y - bounds.height);
}std::clampを使用することで、座標が画面枠から定義されたマージン(bounds)の外側へ出ないように制限しています。
キー入力によるプレイヤーの移動
パラメータを変更して挙動の違いを観察する
GameTuning.hの数値を書き換えることで、機体の移動速度をいつでも調整できます。
// プレイヤーの移動速度(ピクセル/秒)
inline constexpr float MoveSpeed = 240.0f;MoveSpeedを120.0f、次に480.0fに変え、1フレームの移動量を予想してから実行してください。確認後は240.0fに戻して次回へ進みます。
画面端の確認
この回のBoundsは28×28、表示範囲は48×58です。Boundsを画面内へ収めるため、画像の翼や下側の一部は画面外へ出る場合があります。移動を制限する範囲と、画像を描く範囲を区別して確認します。
- 矢印キーまたはWASDキーでプレイヤーが8方向へ滑らかに移動することを確認できる
- 斜め移動時に縦横方向よりも速く動いてしまわないことを確認できる
- 画面端まで移動した際に、移動制限用の
Boundsが画面内で停止することを確認できる -
GameTuning.hのMoveSpeedを変更し、移動速度の変化を確認できる
まとめと次回への引き継ぎ
第01回ではゲームループを学び、機体の表示と入力に応じた移動を実装しました。これで、自機を自由に操作できます。
第02回ではプレイヤーの弾を発射します。連射される多数の弾を安全かつ高速に管理するため、オブジェクトプールという手法を学びます。
参考実装
今回追加・変更するファイルの一例です。宣言や補助処理を確認したいときに開いてください。自分のコードへ必要な変更を反映し、既存の調整値や素材をそのまま上書きしないようにします。
Source/Shooting/Player.cpp
#include <algorithm>
#include <cmath>
#include "Shooting/ImageManager.h"
#include "Shooting/Input.h"
#include "Shooting/Tuning/GameTuning.h"
#include "Shooting/Player.h"
namespace dxgame::shooting
{
using Action = Input::Action;
void Player::Reset()
{
position = tuning::player::StartPosition;
}
void Player::Update(const Input& input, float deltaTime)
{
Vector2 direction;
if (input.IsDown(Action::MoveLeft))
direction.x -= 1.0f;
if (input.IsDown(Action::MoveRight))
direction.x += 1.0f;
if (input.IsDown(Action::MoveUp))
direction.y -= 1.0f;
if (input.IsDown(Action::MoveDown))
direction.y += 1.0f;
// 斜め入力を長さ1に収める
// 方向と長さの計算は第05回で詳しく扱う
float length = std::sqrt(direction.x * direction.x + direction.y * direction.y);
if (length > 1.0f)
{
direction.x /= length;
direction.y /= length;
}
position.x += direction.x * tuning::player::MoveSpeed * deltaTime;
position.y += direction.y * tuning::player::MoveSpeed * deltaTime;
// 指定の範囲全体が画面内に残るよう位置を制限する
const Rect& bounds = tuning::player::Bounds;
float minX = -bounds.x;
float minY = -bounds.y;
float maxX = tuning::ScreenWidth - bounds.x - bounds.width;
float maxY = tuning::ScreenHeight - bounds.y - bounds.height;
position.x = std::clamp(position.x, minX, maxX);
position.y = std::clamp(position.y, minY, maxY);
}
void Player::Draw(const ImageManager& images) const
{
images.Draw(ImageManager::Id::Player, GetDrawRect());
}
Rect Player::GetDrawRect() const
{
return tuning::player::DrawRect.MovedBy(position);
}
}Source/Shooting/Tuning/GameTuning.h
#pragma once
#include "DxGame/Rect.h"
#include "DxGame/Vector2.h"
namespace dxgame::shooting::tuning
{
// ゲーム画面の横幅 単位はピクセル
// ゲーム画面の横幅(単位:ピクセル)
inline constexpr int ScreenWidth = 640;
// ゲーム画面の縦幅 単位はピクセル
// ゲーム画面の縦幅(単位:ピクセル)
inline constexpr int ScreenHeight = 480;
// 座標、距離、Rectの各値はピクセル単位で指定する
// 時間は秒、速度は1秒あたりの移動量で指定する
// プレイヤーのpositionは中心座標を表す
// 各Rectはpositionからの相対範囲として指定する
namespace player
{
// ゲーム開始時のプレイヤー中心座標
inline constexpr Vector2 StartPosition = { 320.0f, 424.0f };
// プレイヤー中心を基準にした画像の表示範囲
inline constexpr Rect DrawRect = Rect::Centered(48.0f, 58.0f).MovedBy({ 0.0f, 3.0f });
inline constexpr Rect Bounds = Rect::Centered(28.0f, 28.0f);
// プレイヤーの速度 単位はピクセル/秒
// プレイヤーの速度(単位:ピクセル/秒)
inline constexpr float MoveSpeed = 240.0f;
}
}