design-skill-progression-trees について
スキルや進行システムを設計するゲーム開発者向けのガイドです。有向非巡回グラフによる解放経路、サーバー側検証、コスト設計、リスペック、データ駆動の効果管理を扱います。
主な機能
- 01前提条件を DAG でモデル化
- 02解放要求をサーバー側で検証
- 03段階的なコスト曲線の設計
- 04リスペックの仕組みを設計
- 05効果を JSONB データで保存
- 06循環や到達不能ノードを検査
活用シーン
- 01スキルやタレントの解放グラフ設計
- 02検証付きプレイヤー解放 API の実装
- 03リスペック費用と返金の計画
- 04進行グラフの構造上の問題の発見
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ fcsouza/ agent-skills --skill design-skill-progression-treesインストール後、Claude Code で/design-skill-progression-treesを入力して読み込まれたか確認します。
同梱ファイルboilerplate/skill-tree.tstemplates/skill-tree-template.md
詳細
スキルツリーと成長システム
目的
あらゆるゲームジャンルに対応する、DAG ベースの成長システムです。スキルツリー、タレントツリー、能力のアンロック、パッシブステータスのグラフ、プレステージレイヤーを扱います。データモデリング、サーバー側での検証、コストのスケーリング、振り直しの仕組みを網羅します。
使用する場面
該当するキーワード:skill tree、talent tree、ability tree、progression system、unlock system、prestige、node graph、skill points、ability unlock、passive tree、talent build、respec、skill reset
前提条件
game-design-fundamentals— コアループと報酬システムの知識postgres-game-schema— プレイヤーと通貨のデータモデルgame-economy-design— コストバランスと通貨の連携
基本原則
Chris Taylor(Supreme Commander):「優れた技術ツリーは、どの道筋も実用的なまま、選択肢が無限にあるという錯覚をプレイヤーに与えます。」 Will Wright(The Sims、Spore):「シンプルなルールが相互作用すると、創発的なシステムが生まれます。スキルツリーは決めつけるのではなく、組み合わせられるべきです。」 Jonathan Blow(Braid、The Witness):「アンロックのたびに、チェックボックスを埋めるのではなく、本当のひらめきを得たと感じられるべきです。」
- ツリーではなく DAG — 前提条件は有向非巡回グラフを形成します。循環があると検証もロールバックも、あらゆる処理が壊れます。管理者による書き込み時に非巡回性を保証してください。
- アンロックの検証はサーバー側で行う — クライアントからのアンロック要求を決して信用しないでください。書き込み前に、前提条件、通貨、クールダウンをサーバー側で検証してください。
- ロールバック用に前提条件のスナップショットを保存する — アンロック時の前提条件の状態をすべて保存してください。振り直しやパッチでグラフが変わっても、プレイヤーがアンロックした時点で何が有効だったかを監査できます。
- コストのスケーリングには指数曲線を使う — 固定コストでは、エンドゲームのノードがプレイヤーの収入に対して簡単に安くなりすぎます。
cost = baseCost * tier^exponent(通常、exponent は 1.5~2.0)を使用してください。 - プレイヤーに優しい振り直しを用意する — 必ずリセット手段を提供してください。クールダウン付きで無料にするか、ソフト通貨で支払えるようにします。プレイヤーを不適切な選択に永久に縛りつけてはいけません。
- 効果はコードではなくデータで管理する — 効果を JSONB(
{ "stat": "attack", "modifier": "+10%" })として保存し、サーバー側で適用してください。これにより、デプロイなしでホットリロード、A/B テスト、バランス調整パッチを実施できます。 - 段階的に表示する — すべてのノードを一度に表示しないでください。前提条件を満たしたノードを順次表示します。プレイヤーの負担を軽減し、新たな発見の機会を生み出します(Jonathan Blow)。
手順
1. ノードのカテゴリを定義する
すべてのノードを分類します。アクティブスキル(プレイヤーが発動する能力)、パッシブステータス(常時有効なボーナス)、アンロック(コンテンツやメカニクスを解放するもの)、プレステージ(エンドゲーム後のリセットボーナス)です。
2. DAG をモデル化する
JSONB に保存する隣接リストを使います。各ノードは、ノード ID の prerequisites 配列を持ちます。管理者による書き込みのたびに非巡回性を検証してください。
3. Drizzle スキーマを作成する
skill_nodes(ツリー定義)テーブルと player_skills(プレイヤーの状態)テーブルを作成します。完全な実装についてはボイラープレートを参照してください。
4. アンロック API を実装する
エンドポイント:POST /skills/unlock/:nodeId。処理の流れ:前提条件を満たしているか確認し、利用可能な通貨を確認してから、アトミックトランザクション(通貨の差し引き+player_skills への挿入+スナップショット保存)を実行します。
5. アンロック済みノードをキャッシュする
プレイヤーセッションごとに、アンロック済みノード ID を Redis に保存します。アンロックまたは振り直し時に無効化してください。ツリーの参照は頻繁に発生するため、リクエストごとにデータベースを参照する方法では遅すぎます。
6. 振り直しエンドポイントを追加する
エンドポイント:POST /skills/respec。処理の流れ:振り直し料金を差し引く(またはクールダウンを確認する)、player_skills のすべての行を削除する、プレイヤーのウォレットにスキルポイントを返金する、Redis キャッシュを無効化する、の順に処理します。
7. ツリーの健全性チェックを追加する
管理者専用のユーティリティを用意します。循環の検出(前提条件グラフを DFS で走査)、孤立ノードの検出(削除済みノードを参照する前提条件を持つノード)、到達不能なノードの検出を行います。
Drizzle スキーマ
// skill_nodes — defines the tree structure (admin-managed)
export const skillNodes = pgTable("skill_nodes", {
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
name: text("name").notNull(),
description: text("description"),
category: text("category").notNull(), // 'active' | 'passive' | 'unlock' | 'prestige'
tier: integer("tier").notNull().default(1),
prerequisites: jsonb("prerequisites").$type<string[]>().default([]),
cost: integer("cost").notNull().default(1),
effects: jsonb("effects").$type<Record<string, unknown>[]>().default([]),
isVisible: boolean("is_visible").notNull().default(true),
metadata: jsonb("metadata").$type<Record<string, unknown>>().default({}),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at").notNull().defaultNow(),
});
// player_skills — tracks which nodes each player has unlocked
export const playerSkills = pgTable("player_skills", {
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
playerId: text("player_id").notNull().references(() => players.id, { onDelete: "cascade" }),
nodeId: text("node_id").notNull().references(() => skillNodes.id),
unlockedAt: timestamp("unlocked_at").notNull().defaultNow(),
snapshot: jsonb("snapshot").$type<Record<string, unknown>>().default({}),
});
API パターン
| エンドポイント | メソッド | 説明 |
|---|---|---|
/skills/tree | GET | 各ノードのプレイヤーのアンロック状況を含む完全なツリー |
/skills/unlock/:nodeId | POST | ノードをアンロックします。前提条件を検証し、コストを差し引き、アトミックに書き込みます |
/skills/respec | POST | 全体をリセットします。手数料を差し引き、player_skills を削除し、ポイントを返還します |
相互参照
game-economy-design— コスト調整、スキルポイントの通貨シンクpostgres-game-schema— プレイヤーと通貨のテーブルスキーマredis-game-patterns— セッションごとにアンロック済みノードのセットをキャッシュgame-design-fundamentals— コアループへの統合と進行ペース
落とし穴とアンチパターン
- 「クライアント側のツリー状態」 — アンロック状態をクライアントに保存すること。簡単に不正操作され、確実に同期ずれが発生します
- 「振り直しの仕組みがない」 — 選択を誤ったプレイヤーは離脱します。必ずリセット手段を用意してください
- 「コスト曲線が一定」 — すべてのノードのコストが同じだと、収入の増加に伴い、エンドゲームのノードが簡単に安くなりすぎます
- 「循環検出がない」 — 前提条件が一つでも循環すると、検証で無限ループが発生します。書き込み時に DAG を強制してください
- 「効果のハードコード」 — 効果を JSONB データではなくコードに埋め込むと、バランス調整のたびにデプロイが必要になります
- 「すべてのノードを表示する」 — ツリー全体を最初から表示すると、新規プレイヤーを圧倒し、発見の楽しみを損ないます
デザイナーの哲学
Chris Taylor(Supreme Commander、Total Annihilation):テックツリーは、プレイヤーに強力な何かを築き上げているという感覚を与えるべきです。どの分岐も、新たな戦略の可能性を切り開くように感じられる必要があります。プレイヤーが二つの分岐を見て、一方が明らかに優れているなら、そのツリーは失敗です。
Will Wright(The Sims、SimCity、Spore):最高の進行システムとは、単純なルールの相互作用から、創発的なビルドが生まれるものです。独自の能力を50個設計するのではなく、50通りに組み合わせられる、組み合わせ可能な能力を10個設計してください。
Jonathan Blow(Braid、The Witness):スキルのアンロックは、進捗バーを埋めることではなく、新しいことを理解したと感じられるものであるべきです。アンロック後にプレイヤーが賢くなった、または能力が高まったと感じないなら、そのノードは無駄です。
参考資料
- 「テックツリーと進行システムの設計」 — GDC 2016、Chris Taylor
- 「ゲームデザインの芸術」 — Jesse Schell、スキルと運に関する章
- 「マルチプレイヤーゲームにおける能力システムのバランス調整」 — GDC 2019
- 「シンプルなシステムから生まれる創発的なゲームプレイ」 — Will Wright、GDC Classic
- 「進行デザインにおけるプレイヤーの主体性」 — GDC 2020