第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などは残します。
constexpr std::size_t ChoiceCount = 3;
namespace rules
{
constexpr int StationaryDamageBonus = 6;
}6はこの例の補正量です。設定値をまとめると、判定や発射の処理を変えずに効果量を調整できます。
2. GameModelへ宣言と状態を追加する
GameModel.hのpublicで、GetPlayerの直後へ状態を取得する関数を追加します。
const Player& GetPlayer() const { return player; }
bool IsStationaryAttackActive() const
{
return stationaryAttack && player.GetHp() > 0;
}この関数は、ルールの状態を表示側へ渡す窓口です。主人公が倒れた後はfalseを返します。
privateではFireWeaponsの直前へ、状態更新と発射性能の関数を宣言します。
void UpdateRuleState(Vector2 movement);
Weapon::Stats GetAttackStats(const Weapon& weapon) const;
void FireWeapons(float seconds);メンバのpreviewの直後へ、状態を保持するboolを追加します。
bool preview = false;
bool stationaryAttack = false;第08回で追加したchoiceOffsetなど、ほかのメンバもそのまま残します。
GetAttackStatsのconst Weapon&は、武装をコピーせず読み取る参照です。戻り値のStatsは値として返すため、発射用の性能を変更しても武装定義は変わりません。
3. 開始時の状態と更新の呼び出しを接続する
GameModel.cppのStartで、previewの初期化直後へ追加します。
preview = false;
stationaryAttack = false;
player.Reset();この例では開始時を無効にし、最初のプレイ更新で入力から状態を決めます。経験値、候補の循環位置、開始武装などの既存処理は残します。
UpdatePlayingでは、player.Updateの直後に呼び出しを追加します。
player.Update(movement, seconds);
UpdateRuleState(movement);
enemies.Update(player.GetPosition(), playTime, seconds);この位置なら、その更新の入力から状態を決め、後で呼ぶFireWeaponsが同じ状態を使えます。UpgradeやPausedではUpdatePlayingへ進まないため、ルールの状態更新も停止します。
4. 条件判定と発射性能を定義する
GameModel.cppのFireWeaponsの直前へ、次の2関数を追加します。
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の中で、発射可能かを調べた直後へ追加します。
if (!weapon.IsReady())
continue;
auto stats = GetAttackStats(weapon);続いて、既存のattacks.Fireの呼び出し1行を置き換えます。
if (attacks.Fire(weapon.GetType(), stats, origin, target))後ろの処理は残します。
{
weapon.Fired();
events.shot = true;
}FireWeapons全体を配布版で置き換えず、性能の取得と引数だけを接続する変更です。独自の照準や武装の走査を追加している場合も、それを引き継げます。
発射先へ渡す武装IDは変えません。第07回で分けた攻撃方式の選択や、攻撃が保持する発射元IDも引き続き使います。
弾や衝撃波は生成時の威力を保持します。静止攻撃中に撃った弾は、発射直後に移動を始めても、その威力のまま飛びます。
発射間隔も変える場合
今回の例が変更するのはdamageです。intervalまで補正する場合は、次の発射待ち時間を設定するWeapon::Firedも接続箇所になります。
現在のFiredは武装自身のGetStats().intervalを使います。発射先へ渡すStatsだけを変えても、武装タイマーの間隔は変わりません。どの値を生成先へ渡し、どの値を武装側の待ち時間へ使うかを分けて設計できます。
6. 有効中の表示を加える
Renderer.cppのDrawHudで、既存のプレイヤーレベル表示の直後へ追加する例です。
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回では実行ファイルと素材を配布できる形へまとめます。