4Hえんぴつ|React+TypeScriptでWeb RPGを作る
第4章「戦闘・交渉・調査と状態管理」
第17回:戦闘・交渉・調査を別々に作らない――ResolutionTypeで共通化する
本連載では、設計・実装・テスト・改善にAIを活用しながら、React+TypeScriptによるWeb RPG開発を進めています。
1. 今回のテーマ
第3章では、街道で複数のイベントを発生させ、
街道へ入る
↓
イベントが起きる
↓
解決する
↓
次のイベントへ進む
という流れを作りました。
では、その「解決する」はどう作るのでしょうか。
今回のRPGでは、イベントに対して主に、
戦闘
交渉
調査
の3種類から行動を選びます。
最初に考えやすいのは、
戦闘システム
交渉システム
調査システム
をそれぞれ別々に作る方法です。
しかし現在の設計では、そうしていません。
3つを、
「イベントを解決する方法」
として共通化しています。
2. 3種類をそのまま別実装するとどうなるか
例えば、イベント画面で次のように分岐させることもできます。
以下は考え方を説明するため、実装を簡略化しています。
if (choice === "combat") {
resolveCombat();
}
if (choice === "negotiation") {
resolveNegotiation();
}
if (choice === "investigation") {
resolveInvestigation();
}
最初は分かりやすいです。
しかし、それぞれの処理の中に、
能力値を取得する
難易度を取得する
成否を判定する
結果を表示する
HPや評判を更新する
イベントを解決済みにする
といった処理を書き始めると、似たコードが増えていきます。
3. 違うのは「何を使って解決するか」
戦闘・交渉・調査は、ゲーム上の意味は違います。
例えば、
戦闘
→ 力で危険を突破する
交渉
→ 相手と話して解決する
調査
→ 情報や痕跡から答えを探す
という違いがあります。
ただし、プログラム上の流れを見ると、
方法を選ぶ
↓
対応する能力値を見る
↓
難易度と比較する
↓
結果を決める
↓
ゲーム状態を更新する
という部分は共通しています。
そこで、
違う部分だけをデータにして、処理の流れは共通化する
ことにしました。
4. ResolutionTypeを作る
現在は、解決方法を次の型で表しています。
export type ResolutionType =
| "combat"
| "negotiation"
| "investigation";
これによって、
const resolutionType: ResolutionType =
"combat";
のように扱えます。
文字列ではありますが、任意の文字列ではありません。
例えば、
const resolutionType: ResolutionType =
"magic";
と書けば、現在のResolutionTypeには存在しないためTypeScriptが検出できます。
5. 文字列を直接使うより意味を限定できる
もし、
type ResolutionType = string;
としてしまうと、
combat
Combat
battle
fight
negotiation
talk
のような表記揺れが入り込めます。
一方、
type ResolutionType =
| "combat"
| "negotiation"
| "investigation";
なら、使える値を型として限定できます。
これは小さな違いですが、ゲーム内の参照箇所が増えるほど効いてきます。
6. AbilityKeyにも同じ考え方を使う
現在のMVPでは、解決方法と主能力を1対1で対応させています。
export type AbilityKey = ResolutionType;
つまり、
combat
↓
戦闘能力
negotiation
↓
交渉能力
investigation
↓
調査能力
という対応です。
能力値も、
export interface AbilitySet {
combat: number;
negotiation: number;
investigation: number;
}
として持ちます。
これによって、選んだ解決方法から対応する能力値を扱いやすくなります。
7. ResolutionTypeから能力値を選べる
例えば、プレイヤーの能力値が、
const abilities = {
combat: 4,
negotiation: 2,
investigation: 3,
};
だったとします。
以下は考え方を説明するため、実装を簡略化しています。
function getAbilityValue(
abilities: AbilitySet,
type: ResolutionType
) {
return abilities[type];
}
すると、
getAbilityValue(abilities, "combat");
// 4
getAbilityValue(abilities, "investigation");
// 3
のように、同じ処理で取得できます。
戦闘用、交渉用、調査用に別々の関数を作る必要がありません。
8. GameEventにも処理を直接書かない
第12回では、GameEventが、
resolutionIds: string[];
を持つことを紹介しました。
イベントそのものには、
戦闘ならこの処理
交渉ならこの処理
調査ならこの処理
とは書きません。
代わりに、
GameEvent
↓
resolutionIds
↓
ResolutionDefinition
と参照します。
ResolutionDefinitionには、解決方法の種類や難易度、結果への参照などを持たせています。
正式設計では、1イベントにつき戦闘・交渉・調査を各1件持たせる構成です。
9. 「戦闘イベント」「交渉イベント」に分けない
この設計では、
戦闘イベント
交渉イベント
調査イベント
という分類にはしません。
一つの出来事に対して、
戦って解決する
話して解決する
調べて解決する
という複数の方法を用意します。
例えば、
街道を塞いでいる相手がいる
という一つの出来事でも、
戦闘
→ 力ずくで突破する
交渉
→ 話し合って通してもらう
調査
→ 別の通路や原因を探す
という違いを作れます。
イベントと解決方法を分けることで、同じイベントを別々の体験として扱えます。
10. 共通化すると結果処理にもつながる
解決方法をResolutionTypeとして共通化すると、その後の流れも共通化できます。
概念的には、
ResolutionTypeを選ぶ
↓
能力値を取得
↓
ResolutionDefinitionを取得
↓
難易度を見る
↓
判定
↓
成功・部分成功・失敗
↓
ResultDefinition
↓
GameStateを更新
となります。
戦闘だけ特別なルートを通るのではありません。
交渉も調査も、同じ「解決」の流れに乗せられます。
11. それでも戦闘らしさは失われない
共通化すると、
戦闘も交渉も全部同じになってしまわないか
という疑問もあります。
ここで共通化しているのは、
内部の判定フロー
です。
表示や演出まで同じにする必要はありません。
例えば、
戦闘
→ 剣や敵を意識した文章・演出
交渉
→ 会話や相手の反応を中心に表示
調査
→ 痕跡や発見内容を中心に表示
とできます。
つまり、
ゲームルール
↓
共通化
見せ方
↓
必要に応じて変える
という分け方です。
12. 共通化しすぎないことも重要
一方で、
共通化できるものは全部一つにする
わけでもありません。
将来、戦闘だけに、
ターン
装備
敵HP
状態異常
などが必要になれば、戦闘専用の仕組みを追加する可能性はあります。
現在のMVPで必要なのは、
戦闘
交渉
調査
↓
能力値と難易度によって結果を決める
という範囲です。
その範囲で共通化しています。
必要になる前から複雑な戦闘システムを作らない
という、プロトタイプから続けている方針でもあります。
13. 型を作ることが、設計を決めることになる
ResolutionTypeは短い型です。
export type ResolutionType =
| "combat"
| "negotiation"
| "investigation";
コードだけを見ると数行しかありません。
しかし、この型によって、
このRPGでは
「戦闘」「交渉」「調査」を
同じ階層の解決方法として扱う
という設計が明確になります。
TypeScriptの型は、単なるエラー防止だけでなく、
ゲームのルールをコード上で表現するもの
としても使えます。
14. 今回のポイント
今回のポイントは3つです。
- 戦闘・交渉・調査を別々のシステムとして作らず、
ResolutionTypeで共通の「解決方法」として表現する -
AbilityKeyもResolutionTypeに対応させ、選択した方法から能力値を扱えるようにする - 共通化するのは判定の仕組みであり、文章や演出まで同じにする必要はない
最初は、
戦闘
交渉
調査
という三つの別機能に見えました。
しかし処理を整理すると、
方法を選ぶ
↓
能力を見る
↓
難易度と判定する
↓
結果を反映する
という共通構造が見えてきました。
次回は、この中の、
「能力を見る」
部分を詳しく扱います。
15. 次回
次回は、
戦闘・交渉・調査に能力値を対応させる――AbilitySetの設計【第18回】
です。
現在のプレイヤー能力は、
export interface AbilitySet {
combat: number;
negotiation: number;
investigation: number;
}
という形で持っています。
次回は、
なぜ能力値をこの3種類にしたのか
ResolutionTypeとどう対応するのか
職業やスキルとどう分けるのか
を整理しながら、
「プレイヤーが何を得意としているか」をデータとしてどう持つか
を見ていきます。
この記事を最後まで読んでいただき、ありがとうございます。
「♡ いいね」を押していただけるとうれしいです。これからの記事づくりの励みになります。
もし気に入っていただけましたら、フォローもよろしくお願いします。
前の記事
次の記事
第18回 戦闘・交渉・調査に能力値を対応させる――AbilitySetの設計