0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TypeScriptでRPGの街道をデータとして設計する――Road型を詳しく見る【第11回】

0
Last updated at Posted at 2026-08-21

4Hえんぴつ|React+TypeScriptでWeb RPGを作る

第3章「街道とイベントを設計する」
第11回:TypeScriptでRPGの街道をデータとして設計する

連載トップ・第0回はこちら

本連載では、設計・実装・テスト・改善にAIを活用しながら、React+TypeScriptによるWeb RPG開発を進めています。

1. 今回のテーマ

前回は、最初のプロトタイプで、

街道1本
+
イベント1つ

から始めた理由を紹介しました。

今回から、第3章「街道とイベントを設計する」に入ります。

最初に詳しく見るのは、街道を表すRoadです。

RPGの街道というと、

町Aと町Bをつなぐもの

と考えれば、それだけでも成立します。

しかし実際にゲームへ組み込むと、

  • どこから出発するのか
  • どこへ向かうのか
  • どのくらい危険なのか
  • どんなイベントが起こり得るのか
  • いつから利用できるのか

といった情報も必要になりました。

そこで今回は、

街道を単なる「移動先」ではなく、ゲーム進行に関わるデータとしてどう表現しているか

Road型から見ていきます。


2. 最初は「出発地と目的地」だけでもよかった

最小構成なら、街道はかなり単純に書けます。

type Road = {
  id: string;
  name: string;
  originTownId: string;
  destinationTownId: string;
};

例えば、

const road: Road = {
  id: "road_a",
  name: "北へ続く街道",
  originTownId: "town_a",
  destinationTownId: "town_b",
};

です。

これだけでも、

町A
↓
街道
↓
町B

という接続関係は表せます。

最初のプロトタイプなら、この程度から始めても問題ありません。

ただし、ゲームを広げると街道には別の役割も必要になりました。


3. 現在のRoad型

現在のデータモデルでは、Roadを次のように定義しています。

export type RoadRiskLevel = 1 | 2 | 3 | 4 | 5;

export interface Road {
  id: string;
  name: string;
  description: string;

  originTownId: string;
  destinationTownId?: string;
  destinationLocationName?: string;

  riskLevel: RoadRiskLevel;
  estimatedMinutes: number;

  eventPoolIds: string[];
  unlockCondition?: UnlockCondition;

  routeTags: string[];
}

項目を大きく分けると、

基本情報
接続先
危険度・時間
イベント
解放条件
分類情報

です。

一つずつ見ていきます。


4. id・name・description

まずは基本情報です。

id: string;
name: string;
description: string;

id

プログラム内部で街道を識別するためのIDです。

id: "road_a"

のように使います。

表示名とは分離しているため、あとから街道名を変更しても、参照関係を維持できます。

name

プレイヤーへ表示する街道名です。

description

その街道がどのような場所なのかを説明します。

例えば、

商人の荷車が行き交う道
森の中を抜ける細い街道
山間部を越える危険な道

といった違いを持たせられます。


5. 出発地はTownのIDで持つ

出発地は、

originTownId: string;

です。

TownそのものをRoadへ入れるのではなく、

Road
↓
originTownId
↓
Town

というID参照にしています。

これは第2章で扱った、

Town → Quest ID
Town → Road ID
Quest → Road ID

と同じ考え方です。

データ同士を丸ごと入れ子にせず、IDで関係を作ります。


6. 目的地は必ずしも町とは限らない

目的地には二つの項目があります。

destinationTownId?: string;
destinationLocationName?: string;

なぜ二つあるのでしょうか。

目的地が町なら、

destinationTownId: "town_b"

で表せます。

しかしRPGでは、

見張り小屋
森の奥
峠
遺跡
洞窟の入口

など、町ではない場所へ向かうこともあります。

その場合は、

destinationLocationName: "見張り小屋"

のように扱えます。

つまり、

町へ向かう街道

だけでなく、

依頼先へ向かう街道

も同じRoadで扱えるようにしています。


7. destinationTownIdとdestinationLocationNameのどちらかは必要

一方で、

destinationTownId: undefined,
destinationLocationName: undefined,

では、街道がどこへ向かうのか分かりません。

そのため設計上、

destinationTownIddestinationLocationNameの少なくとも一方を持つ

という制約を設けています。

型だけでは表現しきれないルールを、

データモデル
+
データ検証

で守る考え方です。

TypeScriptで型を作ったら終わりではなく、

その型へどのような値を入れてよいか

まで決める必要があります。


8. 危険度を1~5に限定する

街道には、

riskLevel: RoadRiskLevel;

があります。

RoadRiskLevelは、

export type RoadRiskLevel =
  1 | 2 | 3 | 4 | 5;

です。

単なる、

riskLevel: number;

にすると、

riskLevel: 100;

のような値もTypeScript上は入れられます。

今回のゲームでは1~5という範囲を使うため、型でもその範囲を表しています。

これは第8回で扱ったQuestDifficultyと同じ考え方です。


9. estimatedMinutesをデータとして持つ

次は、

estimatedMinutes: number;

です。

これは、その街道に対する想定時間を表す値として持っています。

ここで重要なのは、

時間も画面へ直接書くのではなく、Roadの属性として持つ

ことです。

例えば画面側では、

<p>
  想定時間:{road.estimatedMinutes}</p>

のように表示できます。

街道ごとに値を変えても、画面側の構造は変わりません。


10. 街道はイベント候補を持つ

この章で特に重要なのが、

eventPoolIds: string[];

です。

これは、

その街道で発生候補となるイベントのID

を持つための項目です。

例えば、

eventPoolIds: [
  "event_a",
  "event_b",
  "event_c",
];

なら、

Road
├─ Event A
├─ Event B
└─ Event C

という関係を表せます。

ただし、ここで注意したいのは、

eventPoolIdsに入っているイベントを全部、毎回発生させる

という意味ではないことです。

これはあくまで候補です。


11. eventPoolIdsとeventQueueは違う

ここは今後の記事でも重要になります。

Roadが持つ、

eventPoolIds

と、プレイ中に作られる、

eventQueue

は役割が違います。

整理すると、

eventPoolIds
↓
その街道で起こり得るイベント候補

eventQueue
↓
今回のプレイで実際に処理するイベント列

です。

例えば、

Road.eventPoolIds

A
B
C
D

があっても、今回の依頼では、

eventQueue

A
C

だけになるかもしれません。

この違いは、第13回以降で詳しく扱います。


12. 街道だけではイベント列は決まらない

今回のRPGでは、イベントは街道だけから決めているわけではありません。

依頼側にも、

requiredEventIds: string[];
optionalEventIds: string[];

があります。

そのため概念的には、

Road
イベント候補
      +
Quest
必須・任意イベント
      +
現在のゲーム状態
      ↓
今回のeventQueue

という流れになります。

つまりRoadは、

イベントを直接実行するデータではなく、イベント列を組み立てる材料の一つ

です。


13. unlockConditionで「まだ使えない街道」も表す

街道には、

unlockCondition?: UnlockCondition;

もあります。

例えば、

依頼Aを完了する
↓
新しい街道が解放される

という進行を作れます。

ここでも第6回で扱った考え方が使われています。

unlockCondition
↓
解放するためのルール

と、

この街道は現在解放済み
↓
プレイヤーの状態

は別です。

Roadに入っているのは、

どうすれば使えるようになるのか

というルールです。


14. Roadそのものへ「unlocked」を入れない

例えば、

type Road = {
  id: string;
  unlocked: boolean;
};

とする方法も考えられます。

しかし複数のセーブスロットがあれば、

セーブ1
Road A:解放済み

セーブ2
Road A:未解放

という状態があり得ます。

Road Aそのものは同じです。

変わるのは、プレイヤーの進行です。

そこで、

Road
↓
街道の定義

unlockedRoadIds
↓
現在解放されている街道

のように分けて管理します。


15. routeTagsは街道の特徴を表す

最後は、

routeTags: string[];

です。

街道へ複数の特徴を付けられるようにしています。

考え方としては、

routeTags: [
  "forest",
  "trade",
];

のような情報です。

タグを持たせると、

森に関係する街道
交易に関係する街道
危険地域の街道

のように、街道の特徴をデータとして扱えます。

現段階では、すべてのゲーム処理をタグへ依存させる必要はありません。

ただ、

街道の特徴を名前や説明文だけに埋め込まない

ための拡張ポイントになります。


16. Roadをデータにすると画面も共通化できる

街道をデータとして持つと、React側でも、

type RoadCardProps = {
  road: Road;
};

function RoadCard({
  road,
}: RoadCardProps) {
  return (
    <article>
      <h2>{road.name}</h2>
      <p>{road.description}</p>
      <p>
        危険度:{road.riskLevel}
      </p>
      <p>
        想定時間:
        {road.estimatedMinutes}</p>
    </article>
  );
}

のように共通化できます。

※上記は考え方を説明するために簡略化したコードです。

街道が増えても、

RoadAPage
RoadBPage
RoadCPage

を増やす必要はありません。

Roadデータを追加
↓
共通UIへ渡す

という構造にできます。


17. 複数の街道から選べるようになった

最初のプロトタイプでは街道は一本だけでした。

そのため、

依頼を受ける
↓
街道へ出る

で十分でした。

MVPでは街道が増え、

同じ依頼でも
複数の街道候補から選ぶ

という場面が生まれました。

ここでRoadを独立したデータとして持っていたことが効いてきます。

画面側はRoad一覧を受け取り、

街道A
危険度1

街道B
危険度3

のように比較して表示できます。

「街道を選ぶ」という操作自体を、ゲームの一部にできます。


18. 街道は「線」ではなくゲームデータになった

最初は、

町A
────
町B

という、場所同士をつなぐ線として考えていました。

しかし機能を増やすうちに、

Road
├─ 出発地
├─ 目的地
├─ 危険度
├─ 想定時間
├─ イベント候補
├─ 解放条件
└─ 特徴

を持つようになりました。

つまり街道は、

単なる画面遷移の経路ではなく、ゲームルールを持った一つのデータ

になっています。


19. 今回のポイント

今回のポイントは3つです。

  • Roadは出発地と目的地だけでなく、危険度・イベント候補・解放条件なども持つ
  • TownやGameEventを丸ごと埋め込まず、IDで関連付ける
  • eventPoolIdsは「候補」、実際に今回処理するeventQueueとは分けて考える

最初は一本だけだった街道も、データとして整理したことで、

複数街道
街道選択
イベント候補
解放条件
途中進行

へ拡張しやすくなりました。

次は、このRoadと結びつくもう一方のデータ、GameEventを詳しく見ていきます。


20. 次回

次回は、

RPGのイベントをTypeScriptの型で表現する――GameEvent設計【第12回】

です。

街道に、

eventPoolIds: string[];

を持たせても、それだけでは、

何が起こるのか
どう解決するのか
どの依頼で使えるのか
どんな条件で出るのか

までは表せません。

次回はGameEvent型を取り上げ、

「出来事」をプログラムで扱えるデータへ変える

設計を見ていきます。

 
 
この記事を最後まで読んでいただき、ありがとうございます。

♡ いいね」を押していただけるとうれしいです。
これからの記事づくりの励みになります。

もし気に入っていただけましたら、フォローもよろしくお願いします。


前の記事

第10回 TypeScriptで街道とイベントをつなぐ

次の記事

第12回 RPGのイベントをTypeScriptの型で表現する

連載トップ

第0回 連載の目的と開発ロードマップ

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?