クエストと対話システムは、RobloxにおけるRPGやストーリーゲームの屋台骨です。設計が杜撰だと、途中で切断したプレイヤーの進行度が消失してしまいます。
DAGによる依存関係管理、イベントバスによる疎結合設計、プレイヤーの選択に応じた会話ツリー、そしてDataStore容量を節約するビットフィールド圧縮を解説します。
1. 有向非巡回グラフ(DAG)によるクエスト網の構築
循環参照デッドロックを排除した堅牢なシナリオ設計:
- 前提条件ツリー: メインおよびサブクエストをグラフのノードとして定義し、すべての親ノードが完了した段階でのみ受注可能にします。
- 排他的な分岐ルート: 勢力選択やカルマ分岐において、一方を選ぶと対立するクエストが無効化される仕組みを構築します。
- トポロジカルソート事前検証: データベース構築時にソートを実行し、堂々巡りの循環依存関係が存在しないことを機械的に保証します。
2. 有限状態機械(FSM)によるライフサイクル管理
チートによる不正完了を遮断する厳密な状態遷移:
- 6つの標準状態:
ロック中 ➔ 受注可能 ➔ 進行中 ➔ 完了 ➔ 報酬受取済(失敗時は失敗)。 - サーバー主導の判定: クライアントは完了フラグを操作できません。プレイヤーの行動をサーバーが検証して状態を更新します。
- 楽観的UI更新: サーバーの承認パケットを受信した瞬間にHUDのアニメーションと心地よい効果音を再生します。
ServerScriptService.QuestSystem.QuestManager
-- FSMクエストマネージャーモジュール
local QuestManager = {}
QuestManager.__index = QuestManager
local QuestDatabase = require(game.ReplicatedStorage.QuestDatabase)
function QuestManager.new(player, profileData)
local self = setmetatable({}, QuestManager)
self.Player = player
self.ActiveQuests = profileData.ActiveQuests or {}
self.CompletedQuests = profileData.CompletedQuests or {}
return self
end
function QuestManager:CanStartQuest(questId)
local data = QuestDatabase[questId]
if not data then return false end
if self.CompletedQuests[questId] or self.ActiveQuests[questId] then return false end
for _, prereqId in ipairs(data.Prerequisites) do
if not self.CompletedQuests[prereqId] then return false end
end
return true
end
function QuestManager:AcceptQuest(questId)
if not self:CanStartQuest(questId) then return false end
self.ActiveQuests[questId] = {
Step = 1,
Counters = table.create(#QuestDatabase[questId].Steps[1].Objectives, 0)
}
return true
end
return QuestManager
3. イベントバス(Signal Bus)による目標トラッキング
ゲームプレイコードとクエスト検証処理の完全な分離:
- 集中型ゲームイベントバス: モンスターの死亡処理内にクエスト処理を書くのではなく、汎用的な
EntityKilledを発火させます。 - 効率的なフィルタリング: クエストマネージャーは現在進行中のタスクに関連するイベントのみを処理するため、毎フレームの無駄な監視が不要です。
- 複合目標の管理: アイテム採集と特定エネミー討伐などの複数条件をディクショナリ構造で同時に管理します。
4. 分岐型会話ツリーと選択肢ロジック
プレイヤーの選択が展開を左右するインタラクティブな対話設計:
- ノード形式の会話構造: セリフ、カメラ演出、選択肢の配列がセットになったノード群でNPC会話を構築します。
- 条件付き選択肢: クエストの進行状況や特定アイテムの所持、名声値に応じて特殊な選択肢を表示します。
- ノード連動イベント: 会話選択時にアイテム付与、カットシーン再生、ボス戦突入などのアクションを実行します。
5. ProfileServiceによるビットフィールド圧縮保存
数百個のクエストクリア実績を最小バイト数で永続化:
- ビットフィールド格納: クリア済みクエストを整数の各ビットにマッピングし、1つの32bit整数で32個のクエスト完了状態を圧縮保存します。
- アクティブクエストの最小化: 進行中のクエストIDとカウンターのみを保存し、固定メタデータは除外して容量を大幅節約します。
- スキーマ移行フック: アップデートでクエスト内容が変更された場合でも、ProfileServiceのバージョン処理で既存プレイヤーの進行度を安全に移行します。
Frequently Asked Questions
なぜクエストをDAG(有向非巡回グラフ)で設計すべきですか?
クエスト同士が相互に前提となり進行不能に陥る循環デッドロックを数学的に確実に排除できるからです。
イベントバスを使用するメリットは何ですか?
毎フレームの監視処理を排除し、行動が起きた瞬間だけ特定クエストを更新するため処理負荷を最小化できます。
突然の切断時にクエスト進行度を守るには?
ProfileServiceのセッションロック機能を利用し、メモリ上のカウンターとDataStoreを安全に同期させます。
NPC対話から安全に報酬を付与する方法は?
対話UIはクライアントで描画し、報酬受取ノード到達時にサーバー側RemoteFunctionで前提条件を再判定して付与します。