Skip to content

第10回 独自ルールの実装と比較改善 ​

作りたい遊びを処理へつなぐ ​

これまでに作った主人公、敵、武装、成長、ウェーブへ、自分のルールを組み合わせます。移動中と静止中で攻撃が変わる、残りHPによって性能が変わる、特定の場所で効果を得るなど、どんな行動を楽しむゲームにするかを考えます。

この回では「移動入力をしていない間は攻撃の威力が上がる」という例で、条件判定、状態の保持、発射性能、表示の接続を示します。このルールを作品へ採用する必要はありません。自分の条件や効果を実装するときの構造として利用できます。

制作は、手元の改造を引き継いで進めます。第07回の武装と攻撃方式、第08回の取得条件、第09回のウェーブも、そのまま使います。

条件・状態・効果・表示を分ける ​

独自ルールでは、次のつながりを考えると編集箇所を見つけやすくなります。

考えること静止攻撃の例主な編集場所
何を条件にするか移動入力がないGameModelの更新処理
何を保持するか威力補正が有効かGameModelのメンバ
何へ効果を与えるか発射時の威力発射へ渡すStats
どう伝えるか有効中の文字表示Renderer
音を使うならいつ鳴らすか有効になった瞬間などFrameEventsとGameの音声処理

ゲームの条件はGameModel側で決め、Rendererは状態を読み取って表示します。表示処理の中で威力を変える形にはしません。

効果音も、ゲームの状態変化を通知してから音声側で再生できます。条件判定が直接DxLibの音声関数を呼ばない形にすると、音の差し替えとルールの変更を分けられます。

「静止」の意味を決める ​

静止には、複数の判定方法があります。

判定方法画面端へ移動入力を続けた場合被弾表示中
移動入力がない静止としない入力がなければ静止にできる
実際の座標が変わらない静止とする座標変化で判断する
Player::Motion::Idleである移動が完全に止まれば対象になり得るHurtは対象にならない

第05回で読んだPlayer::Motionは表示状態です。Idleだけを使うと、被弾中のHurtを除くことにもなります。

ここでは移動入力を基準にします。画面端で動けなくても入力があれば補正せず、被弾表示中でも入力がなければ補正する例です。自分のルールでは、どの状態を対象にするか選べます。

1. 調整値を定義する ​

GameTuning.hのtuning名前空間で、ChoiceCountの直後へ独自ルールの設定を追加する例です。すでに追加したMaxOwnedWeaponsなどは残します。

cpp
 constexpr std::size_t ChoiceCount = 3;

namespace rules
{
    constexpr int StationaryDamageBonus = 6;
}

6はこの例の補正量です。設定値をまとめると、判定や発射の処理を変えずに効果量を調整できます。

2. GameModelへ宣言と状態を追加する ​

GameModel.hのpublicで、GetPlayerの直後へ状態を取得する関数を追加します。

cpp
 const Player& GetPlayer() const { return player; }

bool IsStationaryAttackActive() const
{
    return stationaryAttack && player.GetHp() > 0;
}

この関数は、ルールの状態を表示側へ渡す窓口です。主人公が倒れた後はfalseを返します。

privateではFireWeaponsの直前へ、状態更新と発射性能の関数を宣言します。

cpp
void UpdateRuleState(Vector2 movement);
Weapon::Stats GetAttackStats(const Weapon& weapon) const;
 void FireWeapons(float seconds);

メンバのpreviewの直後へ、状態を保持するboolを追加します。

cpp
 bool preview = false;
bool stationaryAttack = false;

第08回で追加したchoiceOffsetなど、ほかのメンバもそのまま残します。

GetAttackStatsのconst Weapon&は、武装をコピーせず読み取る参照です。戻り値のStatsは値として返すため、発射用の性能を変更しても武装定義は変わりません。

3. 開始時の状態と更新の呼び出しを接続する ​

GameModel.cppのStartで、previewの初期化直後へ追加します。

cpp
 preview = false;
stationaryAttack = false;
 player.Reset();

この例では開始時を無効にし、最初のプレイ更新で入力から状態を決めます。経験値、候補の循環位置、開始武装などの既存処理は残します。

UpdatePlayingでは、player.Updateの直後に呼び出しを追加します。

cpp
 player.Update(movement, seconds);
UpdateRuleState(movement);
 enemies.Update(player.GetPosition(), playTime, seconds);

この位置なら、その更新の入力から状態を決め、後で呼ぶFireWeaponsが同じ状態を使えます。UpgradeやPausedではUpdatePlayingへ進まないため、ルールの状態更新も停止します。

4. 条件判定と発射性能を定義する ​

GameModel.cppのFireWeaponsの直前へ、次の2関数を追加します。

cpp
void GameModel::UpdateRuleState(Vector2 movement)
{
    stationaryAttack = LengthSquared(movement) < 0.0001f;
}

Weapon::Stats GameModel::GetAttackStats(const Weapon& weapon) const
{
    auto stats = weapon.GetStats();

    if (IsStationaryAttackActive())
        stats.damage += tuning::rules::StationaryDamageBonus;

    return stats;
}

 void GameModel::FireWeapons(float seconds)

配布版の移動入力は、Game::ReadFrameInputでxとyへまとめています。両成分が0なら、この例では静止攻撃を有効にします。左右などの反対入力が打ち消し合った場合も、受け取るベクトルは0になります。

GetAttackStatsでは、現在の武装性能をローカル変数へコピーします。そのコピーへ補正して返すため、Weapon.cppの性能表へ加算が蓄積することはありません。

この例は全武装のdamageへ補正します。特定の武装だけを対象にするならweapon.GetType、方式ごとに効果を変えるなら、その武装定義のattackを条件に使えます。

5. 発射へ補正後のStatsを渡す ​

FireWeaponsの中で、発射可能かを調べた直後へ追加します。

cpp
 if (!weapon.IsReady())
     continue;

auto stats = GetAttackStats(weapon);

続いて、既存のattacks.Fireの呼び出し1行を置き換えます。

cpp
if (attacks.Fire(weapon.GetType(), stats, origin, target))

後ろの処理は残します。

cpp
{
    weapon.Fired();
    events.shot = true;
}

FireWeapons全体を配布版で置き換えず、性能の取得と引数だけを接続する変更です。独自の照準や武装の走査を追加している場合も、それを引き継げます。

発射先へ渡す武装IDは変えません。第07回で分けた攻撃方式の選択や、攻撃が保持する発射元IDも引き続き使います。

弾や衝撃波は生成時の威力を保持します。静止攻撃中に撃った弾は、発射直後に移動を始めても、その威力のまま飛びます。

発射間隔も変える場合 ​

今回の例が変更するのはdamageです。intervalまで補正する場合は、次の発射待ち時間を設定するWeapon::Firedも接続箇所になります。

現在のFiredは武装自身のGetStats().intervalを使います。発射先へ渡すStatsだけを変えても、武装タイマーの間隔は変わりません。どの値を生成先へ渡し、どの値を武装側の待ち時間へ使うかを分けて設計できます。

6. 有効中の表示を加える ​

Renderer.cppのDrawHudで、既存のプレイヤーレベル表示の直後へ追加する例です。

cpp
 Text(config::ScreenWidth - 150, 30, L"LV " + std::to_wstring(game.GetLevel()), White, 1);

if (game.IsStationaryAttackActive())
    Text(28, 90, L"静止攻撃:威力上昇", Green);

状態はGameModelから取得します。描画時に移動キーを調べ直さないため、攻撃と表示で同じ条件を使えます。位置や文言、画像による演出は、作品の画面に合わせて変更できます。

この表示はルールの有効状態です。武装の基礎性能を示す強化カードとは別の情報になります。最終的な発射威力も見せたい場合は、どの値を表示するかを区別して接続できます。

音を加えるなら、条件が成立している毎更新ではなく、成立した瞬間や補正攻撃が発射された瞬間など、鳴らしたい出来事を決めます。FrameEventsへ通知を追加し、Game::HandleEventsでSoundManagerへ渡す方法を使えます。

数値が変わることを確認する ​

保存してビルドし、移動入力の有無と表示を確かめます。画面端で入力を続ける場合、入力を離した場合、被弾表示中も、設定した条件と一致するか見られます。

Visual StudioでGetAttackStatsのreturn行へブレークポイントを置くと、stats.damageを確認できます。配布版のBolt Lv1が威力14なら、補正6の例では有効中に20です。手元で威力を変えている場合は、その値へ6を加えます。

敵を倒すまでの命中数でも差が出る場合があります。無傷のHP18の敵に、ほかの攻撃なしで当てるなら、威力14は2命中、20は1命中です。通常プレイには別武装や事前ダメージもあるため、敵の見た目や時刻だけでは同じ条件だと判断できません。

条件が成立すること、発射へ補正値が渡ること、実際の命中でHPが変わることを分けて追うと、意図と違う箇所を探せます。

遊びの変化を比べて改善する ​

コードが設定どおりに動くことと、そのルールで遊ぶのが面白いことは別です。静止攻撃の例なら、威力が増えても、立ち止まることで接触が増える場合があります。敵の配置や取得武装、回収したい経験値によって結果は変わります。

効果量を6から3へ変える、成立条件を実移動にする、一定時間止まってから発動するなど、作品で気になった点から調整できます。比較する時間や回数は、確かめたい場面に合わせて決められます。

数値差を調べるときは、武装レベルや敵HPをそろえると関係を読みやすくなります。プレイヤーの判断がどう変わるかを見たいときは、そのルールを使って自由に遊ぶことが役立ちます。比較条件をそろえる方法と、実際の遊びの中で確かめる方法を使い分けられます。

複数の要素を変えた作品では、攻撃だけでなく、移動、敵の出現、成長、演出のつながりも見ます。メモや数値は、自分が判断したいことを確かめるために使えます。

自分のルールへ応用する ​

HPが一定以下なら効果を得る場合は、GetHpとGetMaxHpが条件の入口です。敵を倒した回数や、ある場所に滞在した時間を使うなら、GameModelへその状態を持たせ、該当する更新や出来事で変更できます。

継続時間を持つ効果なら、開始・終了・一時停止・リセットの扱いもルールの一部です。表示と音は同じ状態や通知を参照し、プレイヤーが変化を読み取れるようにできます。

既存の基盤を使いながら、必要なルールや演出を組み合わせてください。変更した内容は作品へ引き継ぎ、第11回では実行ファイルと素材を配布できる形へまとめます。