概要
SDLのイベント処理とSDL_GetKeyboardState関数から得られるキーボードの状態を利用して,入力管理用のクラスを作成する.これを用いて,前回作成したマップ上を(適切に)動くことができるようにする.
はじめに
前回
前回は,あらかじめ用意したマップデータに従って文字を画面に配置できるようになりました.
今回
ここから,入力に従って画面内のマップが上下左右に動くようにします.
冒頭にも記載した通り,SDLは
- イベント(キーを押した/離した)
- キーの状態(今押されているか)
の二種類の入力情報を提供します.これらを統合して一つのクラスで扱い,そのクラスをゲーム内で利用するのが妥当でしょう.
開発環境
Windows11 Home 25H2
Visual Studio Community 2026 ⇐ 前回までは2022でしたが引き継ぎました
使用している外部ライブラリ:
SDL3 (version: 3.2.26)
SDL3_ttf (version: 3.2.2)
入力管理クラスの概形
キー状態の表現
入力管理ですから,例えばGetKeyState(keycode)のような形でキーの状態をリクエストされ,それに応える必要があります.これを表現する方法として,列挙型を用いることにしましょう.
enum class ButtonState : short
{
NONE,
PRESSED,
RELEASED,
HELD,
REPEAT_PRESSED
};
NONEは離した状態,PRESSED/RELEASEDは押した/離したタイミング,HELDは押した状態,REPEAT_PRESSEDは長押し状態(リピート入力状態)を表します.
最終的にシンプルなローグライクを目指すという点ではPRESSEDだけ検知できれば良いという話もありますが,できるだけ汎用性は保ったまま進めたいですね.
キーボード管理クラス
class KeyboardState {
public:
KeyboardState();
void RecordState();
inline void SetRepeatState ( SDL_Scancode keyCode, bool isRepeat ) { mIsRepeatState[keyCode] = isRepeat; }
inline bool GetKeyValue ( SDL_Scancode keyCode ) const { return mCurrState[keyCode]; }
ButtonState GetKeyState(SDL_Scancode keyCode) const;
private:
const bool* mCurrState;
bool mPrevState[SDL_SCANCODE_COUNT];
bool mIsRepeatState[SDL_SCANCODE_COUNT];
};
KeyboardState::KeyboardState()
:mCurrState(SDL_GetKeyboardState(NULL))
,mPrevState()
, mIsRepeatState ( )
{
memset ( mPrevState, 0, SDL_SCANCODE_COUNT );
memset ( mIsRepeatState, 0, SDL_SCANCODE_COUNT );
}
void KeyboardState::RecordState() {
memcpy(mPrevState, mCurrState, SDL_SCANCODE_COUNT);
memset ( mIsRepeatState, 0, SDL_SCANCODE_COUNT );
}
ButtonState KeyboardState::GetKeyState(SDL_Scancode keyCode) const {
//mCurrState, mPrevState, mIsRepeatStateを用いた適切な場合分けを行う
}
このような構造になります.
mCurrStateは現在のキーボードの状態を表します.コンストラクタでSDL_GetKeyboardState(NULL)によって初期化されており,これはSDLの側で(静的に)保持している,キーボード状態を表すbool配列へのポインタです.
mPrevStateは,(適切に利用すれば)1フレーム前のキーボード状態を表します.1フレームの処理が終わった「後」にRecordStateを実行することで,次のフレームが始まったときにはmCurrStateとの差分をとればキー状態だけでなく入力のエッジも得られるはず.
ここから分かるように,実はNONE/PRESSED/RELEASED/HELDの判定だけであればキー入力のイベント情報は不要で,毎フレームキー状態を更新すれば十分に求めることができます(※イベントに従って更新すれば計算量が減るだろうという話もありますが,ウィンドウ切り替えなどによってイベントの取りこぼしが起こる可能性を考慮してこのようにしています.そのような場合が存在するのかはわかっていません).
mIsRepeatStateはリピート入力状態の判定に用います.前後2フレームの情報だけでは判定できないので,これだけはSDLからのイベント情報を用いる必要があります.
SDL3ではキー入力イベントにおいてリピート入力か否かを与えられるので(SDL2には無いようです),それをSetRepeatStateを用いてセットします.こちらもmPrevStateと同様に,1フレームの処理が終わったら全て0にします.
GetKeyValueとGetKeyStateはキーの状態を入手するためのメンバ関数で,GetKeyValueはキーが押されている/いないのみをbool値で返し,GetKeyStateはButtonStateを返します.
入力管理クラス
上記キーボード管理クラスを,もう一重外側から管理するクラスを作ります.
というのは,キーボードだけでなく,将来的にはマウスなどの別の入力デバイスも管理する可能性があるからです.
struct InputState {
KeyboardState Keyboard;
};
class InputSystem
{
public:
void FlameEndUpdate();
InputState& GetState() { return mState; }
private:
InputState mState;
};
構造体inputStateは,入力デバイスを(今はキーボードのみですが)まとめるためのものです.
InputSystemクラスはinputStateを管理するクラスですが,現時点で新しい要素はありません.メンバ関数FlameEndUpdateはKeyboardStateクラスのメンバ関数RecordStateを呼び出すのみです.
入力管理クラスの利用
入力イベント
何かイベントが起こると,SDL_AppEventが呼び出されるのでした.ここでリピート入力の処理をしましょう.
全体の構造は↓の記事に若干記述してあるので必要に応じて参照してください.
SDL_AppEventの中のイベントの種類によるswitch文を例えば以下のようにします.
switch (event->type) {
case SDL_EVENT_QUIT:
return SDL_APP_SUCCESS; /* end the program, reporting success to the OS. */
case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP:
game->mInputSystem->GetState ( ).Keyboard.SetRepeatState ( event->key.scancode, event->key.repeat );
break;
}
正確にはキーを押下したとき,かつevent->key.repeat = trueの場合のみSetRepeatStateに送ればよい(1フレームすればmIsRepeatStateは必ずfalseにセットされるから)のですが,このように記述した方が想定しない挙動をする可能性は下がるように思います.
マップを動かす
まず,忘れずに毎フレームの最後にInputSystemのFlameEndupdateを実行しましょう.
SDL_AppResult SDL_AppIterate(void* appstate)
{
Game* game = (Game*)appstate;
game->Update ( );
game->Output();
game->mInputSystem->FlameEndUpdate();
return SDL_APP_CONTINUE;
}
あとは,GameクラスのUpdate関数内で好きなように動作させるだけです.
void Game::Update()
{
InputState& state = mInputSystem->GetState ( );
ButtonState keys[9];
int up = 0, right = 0;
for ( int i = 0; i < 9; ++i ) {
keys[i] = state.Keyboard.GetKeyState ( static_cast< SDL_Scancode > ( static_cast< int > ( SDL_SCANCODE_KP_1 ) + i ) );
if ( keys[i] == ButtonState::PRESSED || keys[i] == ButtonState::REPEAT_PRESSED ) {
player_y += //いい感じの式
player_x += //いい感じの式
}
}
}
ここで,player_x, player_yはGameクラスのメンバです.まだPlayer構造体のようなものは作りません.
このように記述すれば,テンキーの1~9を押すことで前回作ったマップが動くはず.
おわりに
毎フレーム全キーの状態を記録し続けるのか…と思いながら書いていました.イベント入力が完全であること(キーを押したら必ずどこかで離す,など)を前提とすれば,毎フレームの更新は「入力イベント直後か否か」の1bitのみで,残りはイベントを基準に入力システムを構築することもできるので,いずれはそちらも検討します.
ここまで読んでいただきありがとうございます.
次回はメッセージの表示を行います.
参考文献
- Sanjay Madhav. 吉川邦夫訳. ゲームプログラミング C++. 翔泳社, 2018.
- SDL Wiki https://wiki.libsdl.org/SDL3/FrontPage