# 叙事架构代码实现指南
Confidence: high
Last verified: 2026-04-28
Generation: human_only
# 叙事架构代码实现指南
本文档提供游戏叙事系统的核心代码实现示例,覆盖 Etudes 层级化状态机、对话树、变量管理和分支剧情等关键模块。所有代码均可直接运行或稍作修改后集成到项目中。
---
## 目录
1. [Etudes 系统(Owlcat Games)](#1-etudes-系统owlcat-games)
2. [对话系统代码结构](#2-对话系统代码结构)
3. [变量管理方案](#3-变量管理方案)
4. [分支剧情代码示例](#4-分支剧情代码示例)
5. [叙事工具集成](#5-叙事工具集成)
---
## 1. Etudes 系统(Owlcat Games)
Owlcat Games 在《开拓者》系列中使用 Etudes 系统替代传统全局 Flag,管理超过 30,000 个叙事变量。Etudes 是层级化的状态机,通过优先级排序和冲突检测实现可控的叙事状态管理。
### 1.1 核心概念:层级化状态机
与传统扁平的布尔 Flag 不同,Etudes 使用树状结构组织叙事状态。每个节点代表一个叙事片段或状态,子节点继承父节点的上下文。
```csharp
// Etudes系统核心数据模型(C#)
using System;
using System.Collections.Generic;
using System.Linq;
namespace NarrativeSystem.Etudes
{
///
/// Etude节点状态枚举
///
public enum EtudeState
{
Inactive, // 未激活(灰色)
Pending, // 等待条件满足(橙色)
Active // 已激活(绿色)
}
///
/// Etude优先级定义
/// 高优先级Etude可以覆盖低优先级的状态变更
///
public enum EtudePriority
{
Background = 0, // 背景叙事,最低优先级
Normal = 100, // 普通任务
Important = 200, // 重要剧情
Critical = 300, // 关键节点,最高优先级
Blocking = 400 // 阻塞级,暂停其他叙事
}
///
/// Etude节点:叙事状态的基本单位
///
public class EtudeNode
{
public string Id { get; set; } // 唯一标识符
public string Name { get; set; } // 显示名称
public string Description { get; set; } // 描述
public EtudePriority Priority { get; set; } // 优先级
public EtudeState State { get; private set; } // 当前状态
public EtudeNode Parent { get; set; } // 父节点
public List Children { get; set; } // 子节点列表
public List Conditions { get; set; } // 激活条件
public List OnActivate { get; set; } // 激活时动作
public List OnDeactivate { get; set; } // 取消激活时动作
// 冲突规则:哪些Etude与此节点互斥
public List ConflictsWith { get; set; }
public EtudeNode()
{
Children = new List();
Conditions = new List();
OnActivate = new List();
OnDeactivate = new List();
ConflictsWith = new List();
State = EtudeState.Inactive;
}
///
/// 评估当前节点是否应该激活
/// 同时检查父节点状态和自身条件
///
public bool Evaluate(EtudeManager manager)
{
// 如果父节点未激活,子节点不能激活
if (Parent != null && Parent.State != EtudeState.Active)
return false;
// 检查所有条件是否满足
return Conditions.All(c => c.Check(manager));
}
///
/// 激活此Etude节点
///
public void Activate(EtudeManager manager)
{
if (State == EtudeState.Active) return;
State = EtudeState.Active;
foreach (var action in OnActivate)
{
action.Execute(manager);
}
}
///
/// 取消激活此Etude节点
///
public void Deactivate(EtudeManager manager)
{
if (State == EtudeState.Inactive) return;
// 递归取消激活所有子节点
foreach (var child in Children)
{
child.Deactivate(manager);
}
State = EtudeState.Inactive;
foreach (var action in OnDeactivate)
{
action.Execute(manager);
}
}
}
///
/// 条件接口:所有激活条件必须实现
///
public interface IEtudeCondition
{
bool Check(EtudeManager manager);
}
///
/// 动作接口:激活/取消激活时执行的操作
///
public interface IEtudeAction
{
void Execute(EtudeManager manager);
}
}
```
### 1.2 JSON 配置示例
Etudes 适合通过数据驱动配置,策划可以在 JSON 中定义复杂的叙事状态树。
```json
{
"$schema": "etude-config-v1",
"description": "第一章主线任务Etudes配置",
"rootEtudes": [
{
"id": "chapter_01",
"name": "第一章:觉醒",
"description": "游戏开场章节",
"priority": "Normal",
"conditions": [
{ "type": "GameStarted", "value": true }
],
"onActivate": [
{ "type": "SetFlag", "key": "chapter", "value": 1 },
{ "type": "UnlockMap", "mapId": "village" }
],
"children": [
{
"id": "c01_intro",
"name": "开场动画",
"priority": "Critical",
"conditions": [
{ "type": "EtudeActive", "etudeId": "chapter_01" }
],
"onActivate": [
{ "type": "PlayCutscene", "cutsceneId": "intro_01" }
],
"children": [
{
"id": "c01_talk_elder",
"name": "与长老对话",
"priority": "Important",
"conditions": [
{ "type": "CutsceneFinished", "cutsceneId": "intro_01" }
],
"onActivate": [
{ "type": "SpawnNPC", "npcId": "elder", "location": "village_center" },
{ "type": "AddDialogueOption", "npcId": "elder", "dialogueId": "c01_greeting" }
]
},
{
"id": "c01_find_artifact",
"name": "寻找古代遗物",
"priority": "Important",
"conditions": [
{ "type": "DialogueCompleted", "dialogueId": "c01_greeting" }
],
"onActivate": [
{ "type": "AddQuest", "questId": "q01_artifact" },
{ "type": "SpawnItem", "itemId": "ancient_artifact", "location": "cave_depths" }
]
}
]
},
{
"id": "c01_secret_ending",
"name": "隐藏结局触发",
"priority": "Critical",
"conditions": [
{ "type": "FlagEquals", "key": "karma", "value": -100 },
{ "type": "EtudeActive", "etudeId": "chapter_01" }
],
"conflictsWith": ["c01_intro"],
"onActivate": [
{ "type": "TriggerEnding", "endingId": "secret_dark" }
]
}
]
}
]
}
```
### 1.3 优先级排序与冲突检测
```csharp
using System;
using System.Collections.Generic;
using System.Linq;
namespace NarrativeSystem.Etudes
{
///
/// Etudes管理器:核心控制器
/// 每帧或每个叙事事件发生时重新评估所有Etude状态
///
public class EtudeManager
{
// 所有已注册的Etude节点
private Dictionary _etudes = new Dictionary();
// 全局变量存储
private Dictionary _globalFlags = new Dictionary();
// 冲突日志,用于调试
public List ConflictLogs { get; private set; } = new List();
///
/// 注册Etude节点到管理器
///
public void RegisterEtude(EtudeNode etude)
{
if (_etudes.ContainsKey(etude.Id))
{
throw new ArgumentException($"Etude {etude.Id} 已存在!");
}
_etudes[etude.Id] = etude;
}
///
/// 设置全局变量
///
public void SetFlag(string key, object value)
{
_globalFlags[key] = value;
// 变量变更后重新评估所有Etude
ReevaluateAll();
}
///
/// 获取全局变量
///
public T GetFlag(string key)
{
if (_globalFlags.TryGetValue(key, out var value) && value is T typed)
{
return typed;
}
return default;
}
///
/// 核心评估方法
/// 1. 按优先级排序所有待激活Etude
/// 2. 检测冲突
/// 3. 执行状态变更
///
public void ReevaluateAll()
{
// 收集所有可以激活的Etude(条件满足)
var candidates = new List();
foreach (var etude in _etudes.Values)
{
if (etude.Evaluate(this))
{
candidates.Add(etude);
}
else if (etude.State == EtudeState.Active)
{
// 如果已激活但条件不再满足,取消激活
etude.Deactivate(this);
}
}
// 按优先级降序排序:高优先级先处理
candidates.Sort((a, b) => b.Priority.CompareTo(a.Priority));
// 已激活的Etude集合(用于冲突检测)
var activatedIds = new HashSet();
ConflictLogs.Clear();
foreach (var candidate in candidates)
{
// 检查是否与已激活的Etude冲突
bool hasConflict = false;
foreach (var conflictId in candidate.ConflictsWith)
{
if (activatedIds.Contains(conflictId))
{
hasConflict = true;
ConflictLogs.Add(new ConflictLog
{
EtudeA = candidate.Id,
EtudeB = conflictId,
Resolution = candidate.Priority >= _etudes[conflictId].Priority
? "高优先级覆盖"
: "低优先级被跳过"
});
// 如果当前优先级更高,覆盖冲突的Etude
if (candidate.Priority >= _etudes[conflictId].Priority)
{
_etudes[conflictId].Deactivate(this);
activatedIds.Remove(conflictId);
}
else
{
// 优先级低,跳过此候选
break;
}
}
}
if (!hasConflict || candidate.Priority >= candidate.ConflictsWith
.Where(c => activatedIds.Contains(c))
.Select(c => _etudes[c].Priority)
.DefaultIfEmpty(EtudePriority.Background)
.Max())
{
candidate.Activate(this);
activatedIds.Add(candidate.Id);
}
}
}
}
///
/// 冲突记录,用于可视化调试
///
public class ConflictLog
{
public string EtudeA { get; set; }
public string EtudeB { get; set; }
public string Resolution { get; set; }
public DateTime Timestamp { get; set; } = DateTime.Now;
}
}
```
### 1.4 可视化调试工具
```python
# Etudes 状态树可视化(Python + 命令行)
# 可用于运行时调试,输出类似文件树的结构
# 绿色 = Active, 橙色 = Pending, 灰色 = Inactive
from enum import Enum
from typing import List, Dict, Optional
from dataclasses import dataclass, field
class EtudeState(Enum):
INACTIVE = "inactive"
PENDING = "pending"
ACTIVE = "active"
# ANSI 颜色代码
COLOR_MAP = {
EtudeState.ACTIVE: "\033[32m", # 绿色
EtudeState.PENDING: "\033[33m", # 橙色
EtudeState.INACTIVE: "\033[90m", # 灰色
}
RESET = "\033[0m"
@dataclass
class EtudeNode:
id: str
name: str
state: EtudeState
priority: int = 100
children: List['EtudeNode'] = field(default_factory=list)
conflicts: List[str] = field(default_factory=list)
class EtudeVisualizer:
"""Etudes 状态树可视化器"""
def __init__(self, root_nodes: List[EtudeNode]):
self.roots = root_nodes
self.conflict_logs: List[Dict] = []
def _colorize(self, text: str, state: EtudeState) -> str:
"""根据状态给文本上色"""
return f"{COLOR_MAP[state]}{text}{RESET}"
def _render_node(self, node: EtudeNode, prefix: str = "", is_last: bool = True) -> str:
"""递归渲染单个节点"""
connector = "└── " if is_last else "├── "
state_icon = {
EtudeState.ACTIVE: "[+]",
EtudeState.PENDING: "[~]",
EtudeState.INACTIVE: "[ ]",
}[node.state]
line = f"{prefix}{connector}{state_icon} {node.name} ({node.id})"
colored_line = self._colorize(line, node.state)
# 如果有冲突,附加警告
if node.conflicts and node.state == EtudeState.ACTIVE:
conflict_info = f" [冲突: {', '.join(node.conflicts)}]"
colored_line += self._colorize(conflict_info, EtudeState.PENDING)
result = [colored_line]
# 递归渲染子节点
child_prefix = prefix + (" " if is_last else "│ ")
for i, child in enumerate(node.children):
is_last_child = (i == len(node.children) - 1)
result.append(self._render_node(child, child_prefix, is_last_child))
return "\n".join(result)
def render(self) -> str:
"""渲染完整的Etudes状态树"""
lines = []
lines.append("=" * 50)
lines.append("Etudes 状态树可视化")
lines.append(f"图例: {self._colorize('[+] 激活', EtudeState.ACTIVE)} | "
f"{self._colorize('[~] 等待', EtudeState.PENDING)} | "
f"{self._colorize('[ ] 未激活', EtudeState.INACTIVE)}")
lines.append("=" * 50)
for root in self.roots:
lines.append(self._render_node(root))
lines.append("")
# 输出冲突日志
if self.conflict_logs:
lines.append("-" * 50)
lines.append("冲突日志:")
for log in self.conflict_logs:
lines.append(f" [{log['timestamp']}] {log['a']} vs {log['b']}: {log['resolution']}")
return "\n".join(lines)
# ===== 使用示例 =====
if __name__ == "__main__":
# 构建示例Etudes树
child1 = EtudeNode("c01_talk", "与长老对话", EtudeState.ACTIVE, priority=200)
child2 = EtudeNode("c01_fight", "战斗事件", EtudeState.INACTIVE, priority=150)
root = EtudeNode("chapter_01", "第一章:觉醒", EtudeState.ACTIVE, priority=100,
children=[child1, child2],
conflicts=["chapter_02"])
visualizer = EtudeVisualizer([root])
visualizer.conflict_logs = [
{"timestamp": "10:23:05", "a": "chapter_01", "b": "chapter_02", "resolution": "高优先级覆盖"}
]
print(visualizer.render())
```
**可视化输出示例:**
```text
==================================================
Etudes 状态树可视化
图例: [+] 激活 | [~] 等待 | [ ] 未激活
==================================================
└── [+] 第一章:觉醒 (chapter_01) [冲突: chapter_02]
├── [+] 与长老对话 (c01_talk)
└── [ ] 战斗事件 (c01_fight)
--------------------------------------------------
冲突日志:
[10:23:05] chapter_01 vs chapter_02: 高优先级覆盖
```
---
## 2. 对话系统代码结构
### 2.1 对话树数据结构
```csharp
// 对话树核心数据结构(C#)
using System;
using System.Collections.Generic;
using System.Linq;
namespace NarrativeSystem.Dialogue
{
///
/// 对话节点基类
/// 所有对话节点(文本、选择、条件、动作)的抽象基类
///
public abstract class DialogueNode
{
public string Id { get; set; }
public string OwnerId { get; set; } // 说话者ID
public List NextNodes { get; set; } = new List();
public List Conditions { get; set; } = new List();
///
/// 检查此节点是否可以显示
///
public virtual bool CanDisplay(IDialogueContext context)
{
return Conditions.All(c => c.Evaluate(context));
}
///
/// 节点被访问时执行
///
public abstract void OnVisit(IDialogueContext context);
}
///
/// 文本节点:显示NPC对话文本
///
public class TextNode : DialogueNode
{
public string Text { get; set; } // 对话文本(支持本地化Key)
public string Emotion { get; set; } // 情绪标签(影响立绘/动画)
public float AutoAdvanceDelay { get; set; } // 自动推进延迟(-1表示手动)
public override void OnVisit(IDialogueContext context)
{
context.DisplayText(OwnerId, Text, Emotion);
if (AutoAdvanceDelay > 0)
{
context.ScheduleAdvance(AutoAdvanceDelay, NextNodes.FirstOrDefault());
}
}
}
///
/// 玩家选择节点:提供分支选项
///
public class ChoiceNode : DialogueNode
{
public List Choices { get; set; } = new List();
public override void OnVisit(IDialogueContext context)
{
// 过滤掉不满足条件的选项
var availableChoices = Choices.Where(c => c.CanDisplay(context)).ToList();
context.DisplayChoices(availableChoices);
}
}
///
/// 单个选项定义
///
public class DialogueChoice
{
public string Id { get; set; }
public string Text { get; set; }
public string Tooltip { get; set; } // 鼠标悬停提示
public List Conditions { get; set; } = new List();
public List Actions { get; set; } = new List();
public string NextNodeId { get; set; }
public bool CanDisplay(IDialogueContext context)
{
return Conditions.All(c => c.Evaluate(context));
}
}
///
/// 动作节点:执行无副作用的叙事动作
///
public class ActionNode : DialogueNode
{
public List Actions { get; set; } = new List();
public override void OnVisit(IDialogueContext context)
{
foreach (var action in Actions)
{
action.Execute(context);
}
// 自动跳转到下一个节点
context.AdvanceTo(NextNodes.FirstOrDefault());
}
}
///
/// 条件节点:根据条件走向不同分支
///
public class BranchNode : DialogueNode
{
public List Branches { get; set; } = new List();
public string DefaultNodeId { get; set; } // 默认分支(无条件满足时)
public override void OnVisit(IDialogueContext context)
{
foreach (var branch in Branches)
{
if (branch.Condition.Evaluate(context))
{
context.AdvanceTo(branch.TargetNodeId);
return;
}
}
context.AdvanceTo(DefaultNodeId);
}
}
public class ConditionalBranch
{
public DialogueCondition Condition { get; set; }
public string TargetNodeId { get; set; }
}
}
```
### 2.2 条件判断系统
```csharp
namespace NarrativeSystem.Dialogue
{
///
/// 条件接口:所有对话条件的基类
///
public abstract class DialogueCondition
{
public abstract bool Evaluate(IDialogueContext context);
}
///
/// 变量比较条件
///
public class VariableCondition : DialogueCondition
{
public string VariableName { get; set; }
public ComparisonOperator Operator { get; set; }
public object ExpectedValue { get; set; }
public override bool Evaluate(IDialogueContext context)
{
var actualValue = context.GetVariable(VariableName);
return CompareValues(actualValue, ExpectedValue, Operator);
}
private bool CompareValues(object actual, object expected, ComparisonOperator op)
{
if (actual == null || expected == null) return false;
var comparable = Comparer