4Hえんぴつ|React+TypeScriptでWeb RPGを作る
第4章「戦闘・交渉・調査と状態管理」
第18回:戦闘・交渉・調査に能力値を対応させる――AbilitySetの考え方
本連載では、設計・実装・テスト・改善にAIを活用しながら、React+TypeScriptによるWeb RPG開発を進めています。
1. 今回のテーマ
前回は、
export type ResolutionType =
| "combat"
| "negotiation"
| "investigation";
として、
戦闘
交渉
調査
を別々のシステムではなく、
「イベントを解決する方法」
として共通化しました。
では、プレイヤーがその方法を選んだとき、
何を基準に得意・不得意を判定するのか
が次の問題になります。
今回のRPGでは、そのために、
export interface AbilitySet {
combat: number;
negotiation: number;
investigation: number;
}
というAbilitySetを使っています。
今回は、
「プレイヤーが何を得意としているか」を、TypeScriptのデータとしてどう表現するか
を見ていきます。
2. 解決方法だけでは判定できない
例えば、イベントで、
戦闘
交渉
調査
の3つを選べるとします。
プレイヤーが「戦闘」を選んだとき、
戦闘を選んだ
↓
成功
と固定してしまうと、能力値の意味がありません。
一方、
戦闘を選んだ
↓
プレイヤーの戦闘能力を見る
↓
イベント側の難易度と比較する
↓
結果を決める
とすれば、
同じ選択でも、キャラクターによって結果が変わる
ようになります。
3. AbilitySetを作る
現在の設計では、プレイヤーの主能力を次のように持っています。
export interface AbilitySet {
combat: number;
negotiation: number;
investigation: number;
}
例えば、
const abilities: AbilitySet = {
combat: 4,
negotiation: 2,
investigation: 3,
};
なら、
戦闘:4
交渉:2
調査:3
というキャラクターです。
この例なら、
戦闘が得意で、交渉はやや苦手
という特徴をデータとして表現できます。
4. ResolutionTypeと同じキーにする
ここでポイントになるのが、
combat
negotiation
investigation
というキーです。
前回作ったResolutionTypeも、
export type ResolutionType =
| "combat"
| "negotiation"
| "investigation";
でした。
つまり、
ResolutionType
combat
negotiation
investigation
AbilitySet
combat
negotiation
investigation
で対応しています。
現在のMVPでは、この対応を1対1にしています。
5. AbilityKeyもResolutionTypeに合わせる
現在の設計では、
export type AbilityKey = ResolutionType;
としています。
これによって、
戦闘を選ぶ
↓
combat能力を使う
交渉を選ぶ
↓
negotiation能力を使う
調査を選ぶ
↓
investigation能力を使う
という対応を型で表現できます。
別の対応表を用意しなくても、
解決方法の種類そのものを能力値のキーとして使える
わけです。
6. 選んだ方法から能力値を取得する
以下は考え方を説明するため、実装を簡略化しています。
function getAbilityValue(
abilities: AbilitySet,
type: ResolutionType
): number {
return abilities[type];
}
例えば、
const abilities: AbilitySet = {
combat: 4,
negotiation: 2,
investigation: 3,
};
getAbilityValue(abilities, "combat");
// 4
getAbilityValue(abilities, "negotiation");
// 2
となります。
この形なら、
getCombatAbility();
getNegotiationAbility();
getInvestigationAbility();
のように、3種類の関数を別々に作る必要がありません。
7. なぜ能力値も3種類にしたのか
RPGの能力値には、もっと多くの項目を持たせることもできます。
例えば、
腕力
敏捷
知力
魅力
精神力
器用さ
運
などです。
それらから、
戦闘判定
交渉判定
調査判定
を計算する設計も可能です。
ただし今回のWeb RPGは、最初から複雑なキャラクタービルドを作ることが目的ではありませんでした。
まず必要だったのは、
戦闘が得意
交渉が得意
調査が得意
という違いを表現できることです。
そのためMVPでは、
解決方法と同じ3能力に絞っています。
8. 能力値を増やしすぎない
例えば最初から、
interface AbilitySet {
strength: number;
agility: number;
intelligence: number;
charisma: number;
perception: number;
luck: number;
}
として、
combat =
strength × 0.6
+ agility × 0.4
negotiation =
charisma × 0.7
+ intelligence × 0.3
のような計算もできます。
ただし、そうすると、
能力値設計
計算式
バランス調整
UI表示
職業補正
スキル補正
など、決めることが一気に増えます。
MVP段階では、
プレイヤーの選択によって結果が変わる仕組みを確認すること
を優先しました。
9. 小さい型でもキャラクターの違いを作れる
能力値が3つだけでも、組み合わせによって個性は作れます。
例えば、
const warrior = {
combat: 5,
negotiation: 1,
investigation: 2,
};
なら戦闘型です。
const negotiator = {
combat: 1,
negotiation: 5,
investigation: 3,
};
なら交渉型。
const investigator = {
combat: 2,
negotiation: 2,
investigation: 5,
};
なら調査型です。
つまり、能力値の数を増やさなくても、
どの方法を選びやすいキャラクターなのか
は表現できます。
10. PlayerはAbilitySetを持つ
現在の設計では、プレイヤーデータに能力値を持たせています。
export interface Player {
id: string;
name: string;
jobId: string;
abilities: AbilitySet;
skillIds: string[];
}
ここで、
abilities: AbilitySet;
として持つことで、
Player
↓
abilities
↓
combat / negotiation / investigation
という形で参照できます。
11. 職業と能力値は同じものではない
Playerには、
jobId: string;
もあります。
ここで、
職業
=
能力値
にはしていません。
例えば、
戦士
↓
combat = 5
と完全に固定する方法もあります。
しかし、職業は、
初期能力
スキル
推奨される解決方法
などをまとめた、
キャラクター構成の元になるデータ
として扱えます。
プレイヤー自身が持つ現在の能力値とは分けています。
12. JobにはbaseAbilitiesを持たせる
現在の職業モデルは、概念的に次のような構造です。
export interface Job {
id: string;
name: string;
description: string;
baseAbilities: AbilitySet;
skillIds: string[];
recommendedResolution: ResolutionType;
}
例えば、
Job
↓
初期能力値
↓
Player作成時のabilities
という流れを作れます。
この分離によって、将来、
成長
装備
イベント
スキル
などでプレイヤー能力が変わっても、職業マスターそのものを書き換えずに済みます。
13. スキルも能力値とは分ける
Playerには、
skillIds: string[];
もあります。
また、スキル側には、
export interface Skill {
id: string;
name: string;
description: string;
resolutionType: ResolutionType;
modifier: number;
}
という情報を持たせられます。
例えば、
基礎戦闘能力
4
+
戦闘系スキル補正
1
=
判定時の値
5
という使い方ができます。
つまり、
AbilitySet
↓
基本的な得意・不得意
Skill
↓
追加の補正
と役割を分けています。
14. 能力値そのものと判定値は同じとは限らない
ここは重要です。
例えば、
abilities.combat
が4だったとしても、
最終的な判定に必ず4だけを使うとは限りません。
将来的には、
基礎能力
+
スキル補正
+
イベント補正
+
フラグ補正
のように計算できます。
そのため、
AbilitySet
=
判定の最終結果
ではなく、
判定に使う基本値
として考える方が拡張しやすくなります。
15. 能力値の意味をUIにも利用できる
能力値を明確なデータとして持つと、画面にも利用できます。
例えば、
戦闘 ★★★★☆
交渉 ★★☆☆☆
調査 ★★★☆☆
のように表示できます。
さらにイベント画面でも、
戦闘 得意
交渉 やや苦手
調査 普通
といった補助表示へつなげることもできます。
ただし、
表示用の文字列そのものをAbilitySetへ保存する必要はありません。
数値から画面側で組み立てられます。
16. 「得意だから必ず成功」にはしない
能力値が高いからといって、
combat = 5
↓
戦闘なら必ず成功
にすると、選択が単純になります。
今回の設計では、
プレイヤー能力
だけではなく、
イベントごとの難易度
も組み合わせて結果を決めます。
つまり、
能力値が高い
↓
有利
能力値が低い
↓
不利
ではありますが、
能力値だけで結果を固定するわけではありません。
17. 同じキャラクターでもイベントによって結果は変わる
例えば、
戦闘能力:4
のプレイヤーがいたとします。
簡単な相手なら、
戦闘能力 4
vs
難易度 2
で有利です。
しかし強い相手なら、
戦闘能力 4
vs
難易度 5
となります。
つまり、
プレイヤー側
AbilitySet
+
イベント側
difficulty
によって判定が変わります。
次回は、このイベント側の難易度を扱います。
18. ResolutionDefinitionへつながる
第17回で少し紹介したResolutionDefinitionには、
type: ResolutionType;
ability: AbilityKey;
difficulty: number;
があります。
これによって、
このイベントの
この解決方法では
どの能力を使うか
どれくらい難しいか
をデータとして定義できます。
つまり、
Player
↓
AbilitySet
ResolutionDefinition
↓
ability / difficulty
を組み合わせて判定する構造です。
19. AbilityKeyを分けている理由
現在は、
export type AbilityKey = ResolutionType;
なので、
combat → combat
negotiation → negotiation
investigation → investigation
の1対1です。
それなら、
abilityを持たず、typeだけ見ればよいのでは?
とも考えられます。
MVPだけを考えれば、それでも動かせます。
ただ、abilityという役割を明示しておけば、将来、
investigation型の行動だが
別の能力を使う
といった拡張を検討するときにも境界が分かりやすくなります。
現在は同じ値でも、
「解決方法」と「判定に使う能力」という意味は別
として設計しています。
20. 型を合わせると処理を共通化しやすい
ResolutionTypeとAbilitySetのキーを合わせたことで、
const abilityValue =
player.abilities[resolution.type];
のような処理ができます。
以下は考え方を説明するため、実装を簡略化しています。
function getResolutionAbility(
player: Player,
resolutionType: ResolutionType
): number {
return player.abilities[resolutionType];
}
この形なら、
combatならcombatを読む
negotiationならnegotiationを読む
investigationならinvestigationを読む
というifやswitchを何度も書かずに済みます。
21. 型の設計が条件分岐を減らす
例えば、キーがバラバラなら、
switch (resolutionType) {
case "combat":
return player.abilities.attack;
case "negotiation":
return player.abilities.talk;
case "investigation":
return player.abilities.search;
}
のような変換が必要になります。
一方、
ResolutionType
と
AbilitySetのキー
を揃えておけば、
player.abilities[resolutionType]
で済みます。
これは小さなコード差ですが、
データの意味を揃えることで処理を単純にする
例でもあります。
22. MVPでは1〜5程度の小さな値域にする
現在の能力値は、MVPでは大きな数値を必要としていません。
例えば、
1
2
3
4
5
程度でも、
苦手
やや苦手
標準
得意
かなり得意
という差を表現できます。
数値の桁を大きくすると、
42と45はどれくらい違うのか
83は強すぎるのか
など、バランス設計が難しくなります。
まずは小さな値域で、
選択による違いがゲームとして感じられるか
を確認します。
23. AbilitySetは小さいが中心的なデータ
AbilitySet自体は、
export interface AbilitySet {
combat: number;
negotiation: number;
investigation: number;
}
という小さな型です。
しかし、この3値は、
プレイヤー
職業
スキル
解決方法
難易度
判定
へつながります。
つまり、
キャラクターとイベント判定をつなぐ中心的なデータ
になっています。
24. 今回のポイント
今回のポイントは3つです。
-
AbilitySetで、戦闘・交渉・調査の得意・不得意を数値として持つ -
ResolutionTypeと能力値のキーを揃え、選んだ解決方法から能力値を直接参照できるようにする - 職業・スキル・判定結果とは分離し、
AbilitySetは基本能力として扱う
第17回では、
どう解決するか
↓
ResolutionType
を作りました。
今回は、
その方法がどれくらい得意か
↓
AbilitySet
を加えました。
次に必要なのは、
そのイベントが
どれくらい難しいのか
です。
25. 次回
次回は、
イベントごとに難易度を持たせる――ResolutionDefinitionで判定条件を分ける【第19回】
です。
同じ戦闘でも、
弱い相手との戦闘
と、
危険な相手との戦闘
が同じ難しさでは不自然です。
また同じイベントでも、
戦闘なら難しい
交渉なら普通
調査なら簡単
という違いも作れます。
そこで、
ResolutionDefinition
に、
解決方法
使用能力
難易度
条件
結果への参照
を持たせます。
次回は、
GameEventから判定条件を切り離した理由
を中心に整理します。
この記事を最後まで読んでいただき、ありがとうございます。
「♡ いいね」を押していただけるとうれしいです。これからの記事づくりの励みになります。
もし気に入っていただけましたら、フォローもよろしくお願いします。
前の記事
次の記事
第19回 判定条件をイベントごとに変える――ResolutionDefinitionで難易度をデータ化する