コンテンツにスキップ

ケーススタディ 01: プレイヤー入力

素朴な実装

// C++20(疑似エンジンAPI)
void Player::Update(float dt) {
    if (Input::GetKey(Key::A)) x_ -= speed_ * dt;
    if (Input::GetKey(Key::D)) x_ += speed_ * dt;
    if (Input::GetKeyDown(Key::Space)) Jump();
    if (Input::GetKeyDown(Key::J)) Attack();
}

この段階では正しい。 ジャム・プロトタイプならこれで出荷までいけます。

機能追加で破綻する過程

  1. パッド対応: 全if文に || Input::GetButton(...) が増殖
  2. キーコンフィグ: ハードコードされたKey::Aを差し替える場所がない
  3. UI操作との衝突: メニュー中もジャンプしてしまう → if (!menuOpen) が全行に
  4. リプレイ/チュートリアル自動操作: 「本物のキーボード」以外から操作する経路がない
  5. 長押し・同時押し・先行入力: 判定ロジックが各所に重複し、操作感の調整が全ファイル横断作業に

問題の根: 「物理入力の読み取り」「意図への変換」「意図の実行」が1か所に癒着している(関心の分離)。

案A: 意図の構造体+入力ソース抽象(コンポジション・定石)

Command Bufferと入力処理で詳述した形。

struct FrameInput { float moveX = 0; bool jump = false; bool attack = false; };

class IInputSource {                     // 差し替え可能な供給源(DIP)
public:
    virtual ~IInputSource() = default;
    virtual FrameInput Poll() = 0;
};
class KeyboardPadSource : public IInputSource { /* バインド表を見て変換 */ };
class ReplaySource : public IInputSource { /* 記録から供給 */ };

void Player::Update(const FrameInput& in, float dt) {   // Playerは「意図」だけ見る
    x_ += in.moveX * speed_ * dt;
    if (in.jump) Jump();
}
  • 利点: リバインド=バインド表のデータ変更。リプレイ・AI操作・テストが差し替えで可能。UI衝突は「UIが入力を消費したら空のFrameInputを渡す」で一元処理(Chain of Responsibility的な優先度)
  • 欠点: 間接層1枚。ボタン→意図の対応表管理が必要

案B: Commandオブジェクト方式

CommandのInputMapper(キー→ICommandの表)。

  • 利点: 操作の追加が新Command1つ。Undo・マクロ・履歴に自然に拡張
  • 欠点: アクションゲームの「毎フレームの連続値(移動軸)」はCommandの粒度に合いにくい(移動だけ別扱いになりがち)。離散的な操作(ターン制・エディタ)向き

案C: 継承による案(比較のため)

PlayerKeyboardPlayer / PadPlayer / ReplayPlayer に派生させる——悪手。操作デバイスはプレイヤーの is-a ではなく(LSP的にも歪む)、デバイス×キャラ種の組み合わせ爆発を起こします(継承vsコンポジションの反面教材)。

案D: パターンを使わない簡潔案

素朴な実装+関数1枚だけ挟む:

FrameInput ReadInput() {    // 読み取りをこの1関数に集約(構造体は導入する)
    FrameInput in;
    in.moveX = (Input::GetKey(Key::D) ? 1.f : 0.f) - (Input::GetKey(Key::A) ? 1.f : 0.f);
    in.jump = Input::GetKeyDown(Key::Space);
    return in;
}

インターフェースなし・クラスなし。それでも「読み取りの一元化」と「Playerが意図を受け取る」構造は手に入る——小規模の最適解はここです。

規模別の判断

規模 推奨 理由
小(ジャム・プロト) 素朴な実装 or 案D リバインドもリプレイも要件にない
中(リバインド・パッド対応あり) 案D → 案A(要件が来た時点で昇格) 案Dからの移行は安い(FrameInputが既にある)
大(リプレイ・ネット・自動テスト) 案A(+離散操作は案B併用) 差し替え可能性が本領を発揮
Unity / UE 新Input System / Enhanced Input エンジンが案A+Bを製品化済み。自作より学習が先

この題材の教訓

  • 「入力の抽象化」の価値は要件(リバインド・リプレイ・多デバイス)に比例する。要件なしの抽象化はYAGNI違反
  • ただしFrameInput(意図の構造体)だけは早期導入しても損しない——後の全案への移行路になる、安い保険

前: カテゴリ目次 | 次: 武器切り替え