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?

RPGの町を画面ではなくデータとして持つ【第5回】

0
Last updated at Posted at 2026-08-15

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

第2章「RPGをデータとして設計する」
第5回:RPGの町を画面ではなくデータとして持つ

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

1. 今回のテーマ

第1章では、

  • Webアプリから始めた理由
  • 最初から全部作らない考え方
  • 町・依頼・街道・イベントのつながり
  • ゲームデータと状態と画面の違い

を見てきました。

今回から、第2章

「RPGをデータとして設計する」

へ入ります。

最初に扱うのは「」です。

町が一つだけなら、

リーネの町の画面

のように、その町専用の画面を作る方法でも動かせます。

しかし、町を増やしたくなったとき、

町Aの画面
町Bの画面
町Cの画面

と増やしていく方法でよいのでしょうか。

今回のWeb RPGでは、そうせず、

町そのものをデータとして持ち、Reactの画面はそのデータを表示する

という形にしています。

今回は、その考え方を整理します。


2. 最初の町が一つだけなら、専用画面でも作れる

例えば、最初のプロトタイプに町が一つしかないとします。

その場合、Reactで次のように書いても表示できます。

function TownPage() {
  return (
    <main>
      <h1>はじまりの町</h1>
      <p>街道の入口にある小さな町です。</p>
      <button>依頼を見る</button>
      <button>街道へ出る</button>
    </main>
  );
}

これでも、町画面としては成立します。

町が一つしかない間は、むしろ分かりやすいかもしれません。

問題は、町を増やしたときです。


3. 町が増えるたびに画面を増やす?

例えば、町が三つになったとします。

単純に考えると、

StartTownPage
ForestTownPage
PortTownPage

のように、町ごとのReactコンポーネントを作る方法があります。

しかし、実際の町画面には共通部分が多くあります。

例えば、

  • 町の名前
  • 町の説明
  • 受注できる依頼
  • 接続している街道
  • 移動先
  • セーブ
  • 冒険の状況

などです。

違うのは主に、

表示するデータ

です。

それなのに町ごとに画面を作ると、

町A用の画面
町B用の画面
町C用の画面

に、似たコードが繰り返されることになります。

そこで発想を逆にしました。

町ごとに画面を作る

のではなく、

共通の町画面
+
町ごとのデータ

にします。


4. Townというデータを作る

現在のWeb MVPでは、町をTownという型で表しています。

実際の実装では、次のような形です。

export interface Town {
  id: string;
  name: string;
  shortName: string;
  description: string;
  availableQuestIds: string[];
  connectedRoadIds: string[];
  unlockCondition?: UnlockCondition;
  isStartingTown: boolean;
  endingAvailable?: boolean;
}

一つずつ見てみます。

id

id: string;

町を一意に識別するためのIDです。

画面に表示する名前とは別に持ちます。

例えば、

town_start

のような値です。


name

name: string;

プレイヤーへ表示する正式な町の名前です。


shortName

shortName: string;

画面の狭い場所などで使う短い名称です。

スマートフォンの縦画面では、表示できる幅が限られるため、正式名称とは別に短い表示名を持てるようにしています。


description

description: string;

町の説明です。

町画面に表示する文章も、Reactコンポーネントへ直接書くのではなく、町データ側に持たせます。


availableQuestIds

availableQuestIds: string[];

その町に関係する依頼のIDです。

町の中へ依頼データそのものを入れるのではなく、

Town
↓
QuestのID

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


connectedRoadIds

connectedRoadIds: string[];

その町から接続する街道のIDです。

こちらも街道そのものを埋め込まず、

Town
↓
RoadのID

として関連付けています。


unlockCondition

unlockCondition?: UnlockCondition;

その町を利用できるようになる条件です。

最初から行ける町もあれば、

特定の依頼を完了する
↓
次の町が解放される

という町もあります。

?が付いているので、解放条件を持たない町も表現できます。


isStartingTown

isStartingTown: boolean;

ゲーム開始地点となる町かどうかです。


endingAvailable

endingAvailable?: boolean;

エンディングへ進める町かどうかなど、ゲーム進行上の役割を表すために使います。


5. なぜ名前ではなくIDでつなぐのか

例えば、町から依頼を参照するときに、

availableQuestNames: [
  "戻らない荷車",
];

のように、表示名で関連付ける方法も考えられます。

しかし、名前は後から変える可能性があります。

例えば、

戻らない荷車
↓
消えた荷車

とタイトルを変更したら、参照している場所も直さなければならなくなります。

そこで、

availableQuestIds: [
  "quest_missing_wagon",
];

のように、

変わりにくいIDで関連付けます。

表示名とシステム上の識別子を分けるわけです。

ID
↓
プログラムが識別する

name
↓
プレイヤーへ表示する

という役割分担です。


6. 町データとReactの画面を分ける

町をデータとして持てば、React側は町の内容を知る必要がありません。

説明用に単純化すると、次のようにできます。

type TownPageProps = {
  town: Town;
};

function TownPage({ town }: TownPageProps) {
  return (
    <main>
      <h1>{town.name}</h1>

      <p>{town.description}</p>

      <p>
        依頼:
        {town.availableQuestIds.length}</p>

      <p>
        接続街道:
        {town.connectedRoadIds.length}</p>
    </main>
  );
}

ここには、

町Aならこの文章
町Bならこの文章

という処理はありません。

渡されたTownを表示しているだけです。

例えば、

<TownPage town={currentTown} />

とすれば、currentTownが変わるだけで同じ画面を使えます。


7. 町が増えてもReactコンポーネントは増えない

この構造にすると、町を増やしたい場合は、

Reactコンポーネントを作る

のではなく、

Townデータを追加する

ことになります。

イメージとしては、

const towns: Town[] = [
  {
    id: "town_a",
    name: "町A",
    shortName: "町A",
    description: "町Aの説明",
    availableQuestIds: ["quest_a"],
    connectedRoadIds: ["road_a"],
    isStartingTown: true,
  },
  {
    id: "town_b",
    name: "町B",
    shortName: "町B",
    description: "町Bの説明",
    availableQuestIds: ["quest_b"],
    connectedRoadIds: ["road_b"],
    isStartingTown: false,
  },
];

となります。

※このデータは考え方を説明するための例で、実際のゲームデータではありません。

町が、

1つ
↓
3つ
↓
10個

と増えても、

基本となる町画面は共通のままです。


8. 「画面の違い」と「データの違い」を分ける

もちろん、すべての町を完全に同じ見た目にする必要はありません。

例えば、

宿屋がある
特別な施設がある
エンディングへ進める

など、町による違いはあります。

ただし、その違いも可能な範囲で、

町ごとに別画面を作るのではなく、データや状態から判断する

ようにします。

例えば、

{town.endingAvailable && (
  <button>
    エンディングへ進む
  </button>
)}

なら、

endingAvailable === true

の町だけボタンを表示できます。

同じ町画面のまま、データによって表示を変えられます。


9. Townには「今の状態」を入れすぎない

ここで一つ重要な点があります。

Townには、

町そのものを定義する情報

を入れます。

例えば、

名前
説明
接続している街道
関係する依頼

です。

一方、

この町は現在解放されている
プレイヤーが今この町にいる
この町で依頼を何件完了した

といった情報は、プレイによって変化します。

第4回で扱った、

ゲームを定義するデータ

と、

現在のゲーム状態

の違いです。

例えば、

Town
↓
この町は何なのか

GameState
↓
この町が今どういう状態なのか

と分けて考えます。

町データに何でも入れてしまうと、

変わらない設定
+
プレイ中に変わる状態

が混ざってしまいます。

この分離は、後でセーブ機能を作るときにも重要になります。


10. 「解放条件」と「解放済み」は違う

少し似ていて分かりにくいのが、この二つです。

解放条件

と、

現在解放済みか

は同じではありません。

例えば町データ側に、

unlockCondition

があるとします。

これは、

どうすればこの町へ行けるようになるのか

というルールです。

一方、

プレイヤーがその条件をすでに満たしたか

は、現在のゲーム状態です。

整理すると、

Town.unlockCondition
↓
町が解放される条件

GameState
↓
実際に解放済みかどうか

となります。

このように、

ルールと結果を分ける

ことも、データ設計では重要でした。


11. 最初はここまで必要ではなかった

最初のプロトタイプでは、町は一つだけでした。

その時点では、

unlockCondition
shortName
endingAvailable

のような項目をすべて必要としていたわけではありません。

しかしMVPで、

  • 町が増える
  • 町同士を街道でつなぐ
  • 依頼の完了で町を解放する
  • エンディングへ進める場所を作る

と拡張すると、必要な情報が見えてきました。

つまり現在のTown型は、

最初から完成形を予測して作ったものではありません。

小さく作る
↓
実際に動かす
↓
町を増やす
↓
足りない情報が分かる
↓
型を広げる

という流れで育っています。

この連載で繰り返し扱いたいのも、この部分です。


12. Townをデータにしたことで何が変わったか

町をReactの画面ではなくデータとして持つことで、

大きく三つのことが整理しやすくなりました。

町を増やしやすい

新しい町を追加するとき、基本的には町データを追加します。

町ごとの専用コンポーネントを毎回作る必要がありません。

他のデータとつなぎやすい

IDを使って、

Town
├─ Quest
└─ Road

と関連付けられます。

セーブデータと分けやすい

町そのものの設定と、

どこまで解放したか
現在どの町にいるか

というプレイ状態を分離できます。

この構造は、その後の依頼・街道・イベントでも基本的に同じ考え方を使っています。


13. 今回のポイント

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

  • 町ごとにReactコンポーネントを作るのではなく、Townデータを共通画面へ渡す
  • 表示名ではなくIDで依頼や街道と関連付ける
  • 町そのものの定義と、プレイ中に変わる町の状態を分ける

町が一つしかないうちは、こうした分離は少し大げさに見えるかもしれません。

しかし、

1つを動かす
↓
複数へ増やす

という段階で、

画面ではなくデータとして持っていたこと

が効いてきました。


14. 次回

次回は、

TownとTownStateを分ける理由【第6回】

です。

今回は、

町そのもの

と、

町の現在状態

は別のものだというところまで触れました。

次回は、この違いをもう少し掘り下げます。

例えば、

町の名前
町の説明
接続街道

と、

解放済みか
現在地か
利用可能な状態か

を、なぜ同じデータへ入れない方がよいのか。

マスターデータと状態データの分離

という視点から整理します。

 
 
この記事を最後まで読んでいただき、ありがとうございます。
♡ いいね」を押していただけるとうれしいです。これからの記事づくりの励みになります。
もし気に入っていただけましたら、フォローもよろしくお願いします。


前の記事

第4回 ReactでRPGを作ると、画面とゲーム状態はどう分ける?

次の記事

第6回 TownとTownStateを分ける理由

連載トップ

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


関連するnote記事

町をゲーム世界の入口としてどう設計したかという制作側の話は、noteでも書いています。

一つの町を、世界への入口にする。 #13

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?