传统 TRPG 跑团需要一位经验丰富的真人城主(DM)全程控场——构建世界观、扮演 NPC、裁决行动、掌控节奏,一场战役动辄数小时,对人的精力和创造力都是极大消耗。深渊纪元把这位城主交给了端到端具身交互智能:玩家说出任何行动,城主即时构建世界观响应,同时记住整场战役的角色关系、已探索地图、NPC 好感度,并在探索、社交、战斗、休憩四种阶段之间无缝切换叙事风格。

本文从数据建模到三栏界面,完整拆解这套 AI 城主的工程实现。


一、星云控制台配置:六步完成城主全链路

在动手写代码之前,需要先在魔珐星云控制台完成具身应用的全链路配置。以下六步决定了城主的形象、声音、大脑与交互能力。

1、创建具身应用

在魔珐星云控制台创建一个新的端到端应用,作为 AI 城主的具身交互智能体运行载体。

2、形象配置

为城主选择一位身披暗紫斗篷的神秘形象,契合地下城世界的奇幻调性。端到端 SDK 会自动处理形象的实时渲染——口型、表情、手势全部由平台统一驱动,无需前端手动编排。

3、场景配置

设定城主的交互场景参数,包括背景环境与镜头角度,让城主置身于地下城氛围的光影之中,增强跑团的沉浸感。

4、音色配置

选择低沉而富有叙事感的音色,与地下城城主的身份匹配。项目的参数流 TTS 与 SDK 紧密耦合,语音合成与具身表达在同一次交互中统一生成,不需要单独对接 TTS 服务。

5、表演配置

调整城主的表达风格——语速沉稳、手势克制、表情随叙事阶段自然变化,让具身表达与 TRPG 城主「掌控全局、营造氛围」的调性一致。

6、服务配置:ASR 与大脑

为当前应用配置语音识别和大模型服务。这一步体现了星云的可插拔架构——可插拔并非简单的 API 拼接,平台依靠标准化接口契约与统一状态管理,保障模块替换后会话状态、事件流转、交互闭环不受破坏。ASR 支持 Xmov 自研、豆包、腾讯等多家供应商,大脑支持魔珐自研、豆包、OpenAI 兼容协议等多种接入模式。

配置完成后,记录应用 ID 与密钥,后续代码中通过环境变量注入即可。接下来进入前端工程——用代码构建城主的战役引擎与三栏交互界面。


二、技术选型:一个依赖撑起一场战役

深渊纪元的项目依赖极简到只有一个运行时依赖:

// package.json
{
  "dependencies": {
    "vue": "3.5.18"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "6.0.1",
    "typescript": "~5.8.3",
    "vite": "7.1.2",
    "vue-tsc": "3.0.5"
  }
}

来源:package.json

没有 Three.js,没有 OpenAI SDK,没有额外的状态管理库。具身表达由魔珐星云端到端 SDK 单文件自包含——在 index.html 中以 defer 脚本引入,SDK 自动挂载 window.XingyunAvatarAgent,业务侧只需调用 init / ask / startASR 三个方法。

<!-- index.html -->
<!-- 端到端具身交互智能 SDK(单文件自包含,defer 保证先于模块入口执行) -->
<script defer src="https://media.xingyun3d.com/xingyun3d/general/litesdk/xmovAvatar_e2e@latest.js"></script>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>

来源:index.html

defer 脚本先于 type=module 执行,确保应用挂载时 SDK 已可用。如果顺序反了,module 天然 defer 会先执行,new XingyunAvatarAgent 时全局变量尚不存在,连接将永久挂起。


三、项目架构:三层结构一览

src/
├── components/        # 6 个 Vue 组件
│   ├── AgentPanel.vue     # 右栏:城主智能体渲染
│   ├── AppIcon.vue        # SVG 图标集
│   ├── CharacterSheet.vue # 左栏:角色卡 + 背包 + NPC 关系
│   ├── GameStage.vue      # 中栏:阶段指示 + 叙事日志 + 行动输入
│   ├── LandingPage.vue    # 首页:可滚动介绍页
│   └── WorldMap.vue       # 左栏:迷雾地图
├── constants/         # 应用配置 + 城主人设提示词
├── data/              # 初始角色卡 + NPC + 地图
├── services/          # SDK 封装(唯一通道)
├── stores/            # 状态管理(游戏引擎 + SDK 串联)
├── types/             # TypeScript 类型定义
├── utils/             # 游戏引擎(纯函数 + 持久化)
├── App.vue            # 双视图切换 + 三栏布局
├── main.ts            # 入口
└── style.css          # 全局暗色奇幻主题

来源:项目目录结构

整个项目分为三层:数据与引擎层(types + data + gameEngine)负责游戏世界的规则与状态流转;服务层(avatar.ts)封装 SDK 连接与交互;界面层(6 个组件 + App.vue)负责三栏呈现与双视图切换。


四、数据建模:一个完整的地下城世界

类型系统

深渊纪元的类型定义勾勒出一个完整的 TRPG 世界骨架:

// src/types/index.ts
// 战役阶段
export type CampaignPhase = 'exploration' | 'social' | 'combat' | 'rest'

// NPC 身份
export interface NPC {
  id: string
  name: string
  role: 'ally' | 'neutral' | 'enemy'
  description: string
  personality: string
  relationship: number  // -100 ~ 100
}

// 地图区域
export interface MapArea {
  id: string
  name: string
  description: string
  discovered: boolean
  connections: string[]
}

// 游戏状态
export interface GameState {
  phase: CampaignPhase
  currentArea: string
  turnCount: number
  npcs: Record<string, NPC>
  mapAreas: Record<string, MapArea>
  narrativeLog: NarrativeEntry[]
  character: Character
  combatState: {
    enemyId: string | null
    enemyHp: number
    enemyMaxHp: number
  }
}

来源:src/types/index.ts

四个战役阶段决定了城主的叙事风格切换;NPC 的 relationship 字段在 -100 到 100 之间动态变化,城主在回应中会自然引用好感度;MapArea 的 discovered 标记驱动地图迷雾系统;combatState 追踪战斗中的敌人血量。

内置战役数据

项目内置了一个完整的初始战役——游侠从断剑酒馆出发,面对四位性格迥异的 NPC:

// src/data/campaign.ts
/** 内置 NPC 阵容 */
export const INITIAL_NPCS: Record<string, NPC> = {
  elara: {
    id: 'elara',
    name: '艾拉拉',
    role: 'ally',
    description: '银发精灵法师,精通预言系法术,受雇于学术公会调查地下遗迹',
    personality: '冷静理性、博学多闻,偶尔流露出对古代文明的好奇与痴迷',
    relationship: 30,
  },
  gronk: {
    id: 'gronk',
    name: '格隆克',
    role: 'neutral',
    description: '半兽人佣兵,左脸有深深的爪痕,据说曾独闯巨魔巢穴',
    personality: '粗犷直率、重义气,但对背叛零容忍',
    relationship: 0,
  },
  shadow: {
    id: 'shadow',
    name: '暗影·薇拉',
    role: 'enemy',
    description: '黑斗篷下的神秘女子,据传是深渊教团的使徒',
    personality: '阴冷优雅、言辞间暗藏试探,从不直接表露真实意图',
    relationship: -40,
  },
  innkeeper: {
    id: 'innkeeper',
    name: '老酒馆·巴特',
    description: '「断剑酒馆」老板,消息灵通,是冒险者们的信息枢纽',
    personality: '热情健谈、爱八卦,但关键时刻守口如瓶',
    relationship: 20,
  },
}

来源:src/data/campaign.ts

四位 NPC 从盟友到敌对,好感度起点各不相同。玩家与他们的每一次互动都会改变关系值,城主会在后续回应中自然引用这些变化——比如格隆克在好感度低于 -30 时可能拒绝提供帮助,而艾拉拉在高好感度时会主动分享预言线索。

地图由 6 个区域组成,从断剑酒馆出发,经镇中心广场可达暮光神殿、黑市集市,最终深入地下城入口与深渊大厅。每个区域有连接关系,玩家每踏出一步都可能触发新剧情。


五、游戏引擎:四阶段纯函数状态机

数据建模完成后,游戏的核心挑战转向状态管理——玩家的每一步探索、每一次与 NPC 的交锋、每一滴生命值的增减,这些状态需要在内存与持久化之间同步,并在每次向城主提问时组装成完整上下文。

游戏引擎由 src/utils/gameEngine.ts 驱动,核心设计原则是纯函数 + localStorage:每个状态变更函数接收旧状态、返回新状态,不修改原对象。

// src/utils/gameEngine.ts
/** 切换战役阶段 */
export function setPhase(state: GameState, phase: CampaignPhase): GameState {
  return { ...state, phase, turnCount: state.turnCount + 1 }
}

/** 调整 NPC 好感度 */
export function adjustRelationship(state: GameState, npcId: string, delta: number): GameState {
  const npc = state.npcs[npcId]
  if (!npc) return state
  const newRel = Math.max(-100, Math.min(100, npc.relationship + delta))
  return {
    ...state,
    npcs: {
      ...state.npcs,
      [npcId]: { ...npc, relationship: newRel },
    },
  }
}

/** 移动到新区域 */
export function moveToArea(state: GameState, areaId: string): GameState {
  const area = state.mapAreas[areaId]
  if (!area) return state
  const discovered = area.discovered ? state.mapAreas : {
    ...state.mapAreas,
    [areaId]: { ...area, discovered: true },
  }
  return {
    ...state,
    currentArea: areaId,
    mapAreas: discovered,
    turnCount: state.turnCount + 1,
  }
}

来源:src/utils/gameEngine.ts

纯函数的好处是可预测、可测试——给定相同输入永远得到相同输出。状态持久化通过 loadState / saveState 完成,刷新页面后战役进度不会丢失:

// src/utils/gameEngine.ts
/** 从 localStorage 恢复状态,失败则返回初始状态 */
export function loadState(): GameState {
  try {
    const raw = localStorage.getItem(STORAGE_KEY)
    if (raw) return JSON.parse(raw) as GameState
  } catch { /* 损坏则忽略 */ }
  return createInitialState()
}

/** 持久化状态到 localStorage */
export function saveState(state: GameState): void {
  try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)) } catch { /* 存储满则忽略 */ }
}

来源:src/utils/gameEngine.ts


六、城主人设提示词:让智能体成为即兴叙事大师

游戏引擎构建好上下文后,下一步是定义城主的身份与行为准则。深渊纪元的城主不是通用聊天机器人,而是一位有明确叙事风格的即兴叙事大师。提示词定义了它的核心行为:

// src/constants/index.ts
/** 城主人设:即时构建世界观,记住一切 */
export const SYSTEM_PROMPT = `你是深渊纪元,一位经验丰富的 AI 地下城城主(DM)。
## 你的身份
- 名称:深渊纪元
- 身份:TRPG 跑团的 AI 城主,负责构建世界观、扮演 NPC、裁决行动
- 风格:叙事沉浸、细节丰富,善于营造氛围

## 你的行为准则
1. 根据当前战役阶段切换表达风格:探索时注重环境描写,社交时让 NPC 有鲜明个性,战斗时节奏紧凑,休憩时温暖舒缓
2. 玩家说出任何行动,你都要即时构建合理的世界观响应——不能拒绝合理行动
3. 记住所有 NPC 的关系值、已探索的地图区域、玩家背包内容,并在回应中自然引用
4. 掷骰结果由你根据行动难度合理裁定,不机械依赖数值
5. 每次回应控制在 3-5 句,营造沉浸感但不拖沓
6. 不能剧透主线剧情走向,不能替玩家做决定`

来源:src/constants/index.ts

关键设计点:第 1 条要求城主根据阶段切换风格——探索时描写环境细节,战斗时节奏紧凑,这让同一位智能体在不同场景下呈现截然不同的叙事质感。第 3 条强调记忆——NPC 好感度、已探索地图、背包内容都必须在回应中自然引用,而不是等玩家提醒。


七、具身表达层:SDK 服务封装

游戏引擎负责「想什么」,SDK 服务层负责「怎么说」——它需要将游戏引擎构建的上下文转化为城主的语音和表情。src/services/avatar.ts 是整个项目与魔珐星云 SDK 交互的唯一通道。

generation 令牌:防止新旧连接回调交叉污染

端到端 SDK 通过回调函数通知状态变化。当玩家断开重连时,旧连接的回调可能延迟到达,与新连接的回调交叉污染。深渊纪元用 generation 计数器解决这个问题:

// src/services/avatar.ts
export async function connectAvatar(container: HTMLElement, appId: string, appSecret: string) {
  if (avatarState.status === 'connecting' || avatarState.status === 'online') return
  if (!appId.trim() || !appSecret.trim()) throw new Error('请填写有效的应用凭证')
  const token = ++generation
  avatarState.status = 'connecting'
  avatarState.error = ''
  let candidate: Agent | undefined
  let timer: ReturnType<typeof setTimeout> | undefined
  try {
    await loadSDK()
    if (token !== generation) return
    candidate = new window.XingyunAvatarAgent!({
      container, appId: appId.trim(), appSecret: appSecret.trim(),
      gatewayServer: SDK_CONFIG.GATEWAY_URL,
      agentCallbacks: {
        onAgentStateChange: (state: string | { type?: string }) => {
          if (token !== generation) return
          const value = typeof state === 'string' ? state : state?.type ?? ''
          avatarState.speaking = value === 'speak' || value === 'speaking'
        },
        onASRResult: (result: { text?: string; isFinal?: boolean }) => {
          if (token !== generation) return
          avatarState.subtitle = result.text ?? ''
          if (result.isFinal && result.text?.trim()) asrFinalHandler?.(result.text.trim())
        },
        onError: () => {
          if (token !== generation) return
          avatarState.error = '城主连接异常,请检查网络、应用配置或平台额度后重连'
          avatarState.status = 'error'
          void disconnectAvatar(true)
        },
      },
    })
    pendingAgent = candidate
    const active = candidate
    await Promise.race([
      active.init({}).then(async () => { if (token !== generation) await active.destroy('cancelled') }),
      new Promise<never>((_, reject) => { timer = setTimeout(() => reject(new Error('城主连接超时,请检查网络、凭证与平台额度')), 30000) }),
    ])
    if (token !== generation) return
    agent = candidate
    pendingAgent = undefined
    avatarState.status = 'online'
  } catch (error) {
    if (token !== generation) return
    generation += 1
    avatarState.status = 'error'
    avatarState.error = error instanceof Error ? error.message : '连接失败'
    void candidate?.destroy('failed').catch(() => {})
    agent = undefined
    pendingAgent = undefined
  } finally { clearTimeout(timer) }
}

来源:src/services/avatar.ts

每次连接递增 generation 并捕获当前值 token,所有回调内先校验 token === generation,不匹配则静默丢弃。这确保旧连接的回调永远不会污染新连接的状态。

连接采用 Promise.race 加 30 秒超时兜底。SDK 的 init 方法需要建立感知、大脑、表达三条 WebSocket 连接,任一被服务端拒绝则 Promise 永久 pending,超时机制确保玩家不会卡在加载界面。

每次提问都注入完整上下文

深渊纪元不缓存提示词——每次调用 askAvatar 都重新构建完整上下文,确保城主的回应始终基于最新战役进度:

// src/services/avatar.ts
export async function askAvatar(text: string, context: string) {
  if (!agent || avatarState.status !== 'online') return
  avatarState.subtitle = text
  await agent.ask(`${SYSTEM_PROMPT}\n当前战役上下文:\n${context}\n玩家行动:${text}`)
}

来源:src/services/avatar.ts

上下文由 gameEngine 的 buildCampaignContext 构建,包含当前阶段、所在位置、角色状态、背包内容、已探索区域、NPC 关系、战斗状态、最近事件——城主在回应时可以自然引用其中任何一项。


八、状态管理:战役全链路流程控制

src/stores/app.ts 是战役的流程控制器,将游戏引擎、SDK 服务层和 UI 状态串联成完整链路。

自然语言指令检测

玩家不需要记忆按钮操作——在输入框中说到「探索」「前进」时自动切换到探索阶段,说到「攻击」「战斗」时进入战斗阶段,说到「交谈」「对话」时进入社交阶段:

// src/stores/app.ts
/** 自然语言指令检测 */
const PHASE_KEYWORDS: Record<CampaignPhase, string[]> = {
  exploration: ['探索', '调查', '搜查', '查看', '前进', '移动', '走去'],
  social: ['交谈', '对话', '询问', '聊天', '交涉', '说服'],
  combat: ['攻击', '战斗', '挥剑', '施法', '防御', '闪避'],
  rest: ['休息', '扎营', '恢复', '治疗', '睡觉', '篝火'],
}

function detectPhase(text: string): CampaignPhase | null {
  for (const [phase, keywords] of Object.entries(PHASE_KEYWORDS)) {
    if (keywords.some(kw => text.includes(kw))) return phase as CampaignPhase
  }
  return null
}

来源:src/stores/app.ts

核心链路:向城主提问

askDM 函数是整个游戏的核心——检测阶段切换、记录玩家行动、构建上下文、发送给城主:

// src/stores/app.ts
/** 向城主提问(核心链路) */
async function askDM(text: string) {
  // 检测阶段切换
  const detectedPhase = detectPhase(text)
  if (detectedPhase && detectedPhase !== appState.game.phase) {
    appState.game = setPhase(appState.game, detectedPhase)
    appState.game = addNarrative(appState.game, `进入${
      { exploration: '探索', social: '社交', combat: '战斗', rest: '休憩' }[detectedPhase]
    }阶段`, 'system')
  }

  // 记录玩家行动
  appState.game = addNarrative(appState.game, text, 'player')

  // 构建上下文并发送
  const context = buildCampaignContext(appState.game)
  await askAvatar(text, context)

  persist()
}

来源:src/stores/app.ts

这段代码展示了端到端架构的简洁——阶段检测、状态更新、上下文构建、SDK 调用、持久化,五步操作在一个函数内完成,不需要跨模块的状态同步胶水代码。


九、三栏交互界面:角色卡、叙事舞台、城主同屏呈现

深渊纪元的游戏界面采用三栏布局,将战役所需的全部信息平铺在同一屏幕:

<!-- src/App.vue -->
<script setup lang="ts">
import { computed, onBeforeUnmount, ref } from 'vue'
import { APP_CONFIG } from './constants'
import { appState, appStore } from './stores/app'
import Icon from './components/AppIcon.vue'
import LandingPage from './components/LandingPage.vue'
import CharacterSheet from './components/CharacterSheet.vue'
import WorldMap from './components/WorldMap.vue'
import GameStage from './components/GameStage.vue'
import AgentPanel from './components/AgentPanel.vue'

const avatar = appStore.avatarState
const view = ref<'landing' | 'game'>('landing')

function enterGame() {
  view.value = 'game'
}

// 顶栏时钟
const now = ref(new Date())
const clockTimer = setInterval(() => { now.value = new Date() }, 30000)
onBeforeUnmount(() => clearInterval(clockTimer))
const clockText = computed(() => now.value.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }))
const dateText = computed(() => now.value.toLocaleDateString('zh-CN', { month: 'long', day: 'numeric', weekday: 'long' }))
</script>

<template>
  <!-- 首页:可滚动介绍页 -->
  <LandingPage v-if="view === 'landing'" @enter="enterGame" />

  <!-- 游戏视图:全屏三栏,不滚动 -->
  <div v-else class="screen game-screen">
    <header class="top-bar">
      <div class="brand">
        <span class="brand-mark"><Icon name="dice" :size="26" /></span>
        <span class="brand-name">深渊纪元<small>ABYSS EPOCH</small></span>
        <span class="brand-sub">AI 地下城城主 · {{ APP_CONFIG.STORE }}</span>
      </div>
      <div class="top-center">
        <span v-if="avatar.listening" class="voice-live"><span class="live-dot" />正在聆听,请下达行动指令</span>
        <span v-else-if="avatar.speaking" class="voice-live speaking"><span class="live-dot" />城主叙述中</span>
      </div>
      <div class="top-clock">
        <strong>{{ clockText }}</strong>
        <span>{{ dateText }}</span>
      </div>
    </header>

    <main class="columns">
      <!-- 左栏:角色卡 + 地图 -->
      <aside class="left-col">
        <CharacterSheet />
        <WorldMap />
      </aside>

      <!-- 中栏:叙事舞台 -->
      <GameStage />

      <!-- 右栏:城主智能体 -->
      <AgentPanel />
    </main>
  </div>
</template>

<style scoped>
.game-screen {
  height: 100vh;
  overflow: hidden;
}
</style>

来源:src/App.vue

双视图设计值得注意:首页是可滚动的介绍页(LandingPage),游戏视图是全屏三栏不滚动。overflow:hidden 只写在 .game-screen 上,不放在两视图的共享祖先上——这是从同系列项目踩坑中总结的经验,共享祖先的 overflow:hidden 会静默阻断长首页的滚动。

左栏:角色卡与迷雾地图

CharacterSheet 展示角色的 HP/MP 状态条、四维属性、背包物品和 NPC 好感度。所有数值随游戏进程实时更新——HP 条从满红渐变到危险红,NPC 好感度正值为绿、负值为红。

WorldMap 展示当前所在位置和可达区域,已探索的区域显示名称并可点击前往,未探索的区域显示为「未探索区域」的迷雾状态:

<!-- src/components/WorldMap.vue -->
<li
  v-for="area in currentConnections"
  :key="area.id"
  class="area-node"
  :class="{ discovered: area.discovered, fog: !area.discovered }"
  @click="area.discovered && appStore.travelTo(area.id)"
>
  <span class="area-dot">{{ area.discovered ? '◆' : '◇' }}</span>
  <span class="area-name">{{ area.discovered ? area.name : '未探索区域' }}</span>
</li>

来源:src/components/WorldMap.vue

中栏:叙事舞台

GameStage 是游戏的核心交互区——顶部阶段指示条显示当前阶段与回合数,中部叙事日志按角色分色展示城主/NPC/玩家/系统的发言,底部行动输入框支持文字和语音两种方式。叙事日志自动滚动到最新条目,让玩家始终看到最新的城主回应。

右栏:城主智能体

AgentPanel 是城主的具身表达窗口——魔珐星云 SDK 渲染的城主形象占据整个面板,页面加载即自动连接并显示,不需要玩家手动点击。语音识别终稿通过 onASRFinal 回调直接注入 askDM 链路,实现「说完即行动」的零延迟体验。


十、一局实况推演:从玩家开口到城主回应

架构描述再详细,不如跑一局看。以下是一次真实交互的全链路拆解,展示前文所有模块如何协同运转。

玩家说出:「我走向酒馆老板,问他最近镇上发生了什么怪事」

这句话进入系统后,首先经过 stores/app.ts 的自然语言检测。detectPhase 扫描四组关键词,匹配到「交谈」→ 判定为社交阶段。当前阶段是探索,阶段不一致,状态机自动切换并写入一条系统日志:「进入社交阶段」。

接下来,玩家的这句话被记录为一条叙事日志,说话者为 player。然后 buildCampaignContext 开始组装上下文——它从 GameState 中抽取当前阶段(社交)、所在位置(断剑酒馆)、角色状态(游侠 Lv.1,HP 45/45,MP 20/20)、背包内容(铁剑、皮甲、治愈药水×3、火把×5、神秘信件)、已探索区域(断剑酒馆)、相邻可达区域(镇中心广场、后巷)、NPC 关系(艾拉拉 +30、格隆克 0、暗影·薇拉 -40、老酒馆·巴特 +20),以及最近 5 条叙事日志。这些信息拼接为一段结构化文本。

askAvatar 将城主人设提示词、战役上下文和玩家行动合并为一次 SDK 调用。魔珐星云端到端 SDK 接收到这段文本后,内部完成大模型推理、语音合成、口型驱动、表情与手势生成,全部在同一条链路中闭环。约 500ms 后,城主形象开口说话,声音低沉而沉稳,配合手势和表情,叙述酒馆老板的回应——

「巴特擦着杯子,压低声音说:『最近地下城入口那边不太平,有几个冒险者进去后再也没出来。你要是有兴趣,可以去镇中心广场打听打听。』」

整个过程中,业务侧只写了一个函数调用。没有 ASR 对接、没有 LLM 拼接、没有 TTS 分片——端到端 SDK 把感知、理解、决策、表达压缩成一次 ask,其余全部由平台内部完成。这就是端到端架构的核心价值:不是少写了几行胶水代码,而是消除了模块之间的状态断裂风险。


十一、战役设计手记:近两千行代码背后的工程抉择

回顾整个项目,有几个设计决策值得复盘。

为什么用纯函数而不是响应式状态库? 战役状态包含 NPC 好感度、地图迷雾、背包物品、叙事日志等多个互相关联的子系统。如果用分散的响应式变量管理,状态变更的副作用很难追踪。纯函数强制每次变更都是「旧状态 → 新状态」的显式转换,可预测、可回滚、可测试。localStorage 持久化只是 JSON.stringify 一行,刷新页面后战役进度不丢失。

为什么每次提问都重新构建上下文? 城主的回应质量完全取决于上下文完整度。如果缓存提示词,城主会「忘记」新发现的区域、好感度变化、战斗结果。buildCampaignContext 在每次 ask 前从 GameState 实时抽取全量信息,确保城主的每一句话都基于最新战役进度。代价是每次请求的 token 消耗略高,但对战役沉浸感来说,这笔开销完全值得。

为什么用自然语言检测而不是按钮切换? TRPG 的核心体验是自由——玩家不应该被限制在「点击探索按钮」或「点击战斗按钮」的框架里。PHASE_KEYWORDS 让玩家用自然语言描述行动,系统自动识别阶段切换。「我要攻击这个怪物」自动进入战斗阶段,「我想和艾拉拉聊聊」自动进入社交阶段。按钮仍然保留在阶段指示条上,作为快捷操作而非唯一入口。

generation 令牌解决了什么? 端到端 SDK 通过回调通知状态变化。当玩家断开重连时,旧连接的回调可能延迟到达。如果没有令牌机制,旧回调会污染新连接的状态,导致语音识别误触发、说话状态错乱。每次连接递增 generation 并捕获当前值,所有回调内先校验令牌再执行,不匹配则静默丢弃。这个模式在同系列多个项目中反复验证,是端到端 SDK 集成的关键防御机制。

最终,核心游戏逻辑(types + gameEngine + avatar.ts + stores)不到 700 行,加上 6 个 UI 组件与样式共计约 1900 行。代码量不大,但覆盖了一个完整 TRPG 战役的全链路:四阶段状态机、NPC 好感度动态追踪、地图迷雾系统、完整战役上下文注入、自然语言阶段检测、三栏同屏界面与双视图首页切换。

魔珐星云是一套端到端具身交互智能平台,让 AI 通过屏幕和机器人进入真实世界,成为能够感知、理解、表达并行动的智能体。

深渊纪元证明了同一套平台能力可以承载完全不同的产品形态——上一位城主在剧本杀里管理线索与嫌疑人,这一位在地下城裡构建世界观与 NPC 关系。身体可以变化,场景可以切换,但端到端的具身交互智能底座持续复用。


如果你想亲自体验端到端具身交互智能的魅力,或者基于魔珐星云平台搭建属于自己的具身交互智能体,欢迎访问魔珐星云官网,开启你的具身交互智能实战之旅:https://xingyun3d.com?utm_campaign=daily&utm_source=CSDNwanfen2&utm_medium=&utm_term=&utm_content=

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐