← Back to Portal Hub

로블록스 퀘스트 및 대화 아키텍처: 유한 상태 머신(FSM), 노드 기반 분기 및 DataStore 저장

By DopaBrain 게임 시스템 아키텍처 팀 • 2026-09-30 • Technical Guide

스토리 중심의 로블록스 게임, 오픈월드 RPG, 시뮬레이터 장르에서 동적인 NPC 상호작용과 다단계 퀘스트 목표는 유저 잔존율(리텐션)을 결정하는 핵심 요소입니다. 개별 NPC ProximityPrompt 내부에 퀘스트 스크립트를 하드코딩하거나 여러 개의 BoolValue로 진행도를 관리하는 방식은 레이스 컨디션, 퀘스트 먹통 버그, 보상 복사 취약점을 유발합니다.

본 시스템 아키텍처 가이드에서는 상용급 서버 권한 퀘스트 및 대화 엔진을 설계합니다. 다단계 퀘스트 수명 주기를 제어하는 계층형 유한 상태 머신(HFSM), 조건부 요구사항(레벨, 인벤토리 아이템, 이전 선택지)을 지원하는 노드 기반 대화 파서, 원자적 DataStore 직렬화 파이프라인을 다룹니다.

1. 퀘스트 수명 주기 아키텍처: 계층형 유한 상태 머신(HFSM)

퀘스트는 단순한 완료/미완료 플래그가 아니며, 서버의 엄격한 검증 하에 정해진 상태를 전이하는 결정론적 생명주기를 갖습니다:

2. 상용급 서버 퀘스트 매니저 및 상태 머신 구현 코드

아래는 선행조건 검증 및 안전한 목표 카운트 증감을 지원하는 서버 측 Luau 퀘스트 상태 머신 모듈입니다:

QuestStateMachine.luau (서버 퀘스트 엔진)
--!strict
local HttpService = game:GetService("HttpService")
local Players = game:GetService("Players")

local QuestManager = {}
QuestManager.__index = QuestManager

export type QuestState = "Locked" | "Available" | "Active" | "Completable" | "Completed" | "Failed"

export type Objective = {
    Id: string,
    Type: "Kill" | "Gather" | "Interact",
    TargetId: string,
    Current: number,
    Required: number
}

export type QuestData = {
    QuestId: string,
    State: QuestState,
    Objectives: { [string]: Objective },
    StartTime: number
}

local playerQuests: { [Player]: { [string]: QuestData } } = {}
local questDefinitions = {
    ["bandit_camp_1"] = {
        RequiredLevel = 5,
        Objectives = {
            ["kill_bandits"] = { Type = "Kill", TargetId = "Bandit", Required = 5 },
            ["find_chest"] = { Type = "Interact", TargetId = "CampChest", Required = 1 }
        }
    }
}

function QuestManager.StartQuest(player: Player, questId: string): boolean
    local def = questDefinitions[questId]
    if not def then return false end

    local userQuests = playerQuests[player]
    if not userQuests then return false end

    local existing = userQuests[questId]
    if existing and existing.State ~= "Available" then return false end

    local objTable: { [string]: Objective } = {}
    for objId, objDef in pairs(def.Objectives) do
        objTable[objId] = {
            Id = objId,
            Type = objDef.Type,
            TargetId = objDef.TargetId,
            Current = 0,
            Required = objDef.Required
        }
    end

    userQuests[questId] = {
        QuestId = questId,
        State = "Active",
        Objectives = objTable,
        StartTime = os.time()
    }

    QuestManager.SyncClient(player, questId)
    return true
end

function QuestManager.RecordAction(player: Player, actionType: string, targetId: string, amount: number)
    local userQuests = playerQuests[player]
    if not userQuests then return end

    for questId, quest in pairs(userQuests) do
        if quest.State == "Active" then
            local allComplete = true
            for _, obj in pairs(quest.Objectives) do
                if obj.Type == actionType and obj.TargetId == targetId then
                    obj.Current = math.clamp(obj.Current + amount, 0, obj.Required)
                end
                if obj.Current < obj.Required then
                    allComplete = false
                end
            end

            if allComplete then
                quest.State = "Completable"
            end
            QuestManager.SyncClient(player, questId)
        end
    end
end

function QuestManager.SyncClient(player: Player, questId: string)
    -- 클라이언트로 직렬화된 퀘스트 상태 딕셔너리 전송 (RemoteEvent)
end

return QuestManager

3. 분기형 대화 트리: 방향 그래프 기반 내러티브 엔진

단순 일방통행식 독백은 플레이어의 몰입을 해칩니다. 분기형 대화는 유저에게 주도권과 서사적 무게감을 부여합니다:

4. DataStore 직렬화 및 엣지 케이스 방어

퀘스트 데이터는 플레이어의 핵심 진행 자산이므로 저장 오류나 동기화 누락을 원천 방어해야 합니다:

5. 상용 배포 검증 및 시스템 QA 체크리스트

다단계 퀘스트 시스템을 프로덕션 환경에 배포하기 전 필수적으로 점검해야 할 항목입니다:

Frequently Asked Questions

퀘스트 진행도를 왜 클라이언트에서 직접 연산하거나 전송하면 안 되나요?

클라이언트가 RemoteEvent로 '몬스터를 5마리 잡았다'거나 '퀘스트를 완료했다'고 서버에 보내면 핵 유저가 해당 이벤트를 반복 호출하여 모든 퀘스트를 즉시 완료하고 보상을 무한 복사할 수 있기 때문입니다.

로블록스 스튜디오에서 대화 트리를 구성하는 가장 좋은 방법은 무엇인가요?

문자열 ID(예: 'start', 'accept_quest', 'decline')를 키로 갖는 노드 테이블을 작성하고, 각 노드 내에 다음 NodeId로 연결되는 선택지 배열과 해금 조건(Condition)을 정의하는 방향 그래프 방식을 사용하는 것이 가장 확장성이 뛰어납니다.

서버가 튕기거나 크래시가 났을 때 퀘스트 데이터 손실을 막으려면 어떻게 해야 하나요?

ProfileService와 같은 세션 락 지원 라이브러리를 사용하고, 퀘스트 상태와 진행 카운트만을 컴팩트하게 압축하여 플레이어 기본 세이브 데이터에 포함해 정기 자동 저장해야 합니다.

선택지에 따라 스토리가 갈라지는 비선형 분기 퀘스트도 상태 머신으로 만들 수 있나요?

네 가능합니다. 상태 머신 내에서 조건 플래그를 검사하여 A 퀘스트 완료 시 B 퀘스트를 영구 잠금하고 C 퀘스트를 활성화하는 방식으로 깊이 있는 진영 간 분기 스토리를 완벽히 구현할 수 있습니다.

Explore More Interactive Tests & Guides

Discover personalized cognitive assessments, stress evaluations, and game psychology tools on DopaBrain.

Go to Portal Hub