Files
Genarrative/docs/experience/PROJECT_DEVELOPMENT_EXPERIENCE.md
kdletters cbc27bad4a
Some checks failed
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

8.8 KiB
Raw Permalink Blame History

AI Native Visual RPG 开发经验沉淀

1. 项目特点判断

这个项目不是单纯的“像素 UI 项目”,而是 4 条链路同时耦合的项目:

  1. 叙事链路AI 生成剧情文本与选项
  2. 状态链路玩家、怪物、NPC、背包、好感、同伴、场景流转
  3. 演出链路:战斗计划、动画播放、投射物、特效、镜头位移
  4. 界面链路:选择世界、选择角色、冒险页、背包页、地图弹窗、编辑器页

经验:

  • 做功能前先判断它主要影响哪几条链路。
  • 如果一个需求同时影响“状态 + 演出 + UI”不要只改一个点。
  • 像“初始同伴”这种功能,本质上不是 UI 需求,而是“选角流程 + 初始 encounter + NPC 好感 + 招募状态”的组合需求。

2. 先做数据建模,再做 UI

这一类项目最容易犯的错误,是先加按钮、再补状态。

实践下来更稳的顺序是:

  1. 先把状态字段补齐
  2. 再补工具函数
  3. 再接交互入口
  4. 最后补展示层

已经验证有效的状态字段包括:

  • playerInventory
  • npcStates
  • companions
  • currentBattleNpcId
  • currentEncounter
  • playerActionMode
  • activeCombatEffects

经验:

  • “能否交易 / 能否招募 / 送礼涨多少好感”都应该由状态和规则函数决定,不能写死在按钮文本里。
  • NPC 交互尽量走本地规则,不要依赖 AI 即时决定关键数值。

3. 复杂页面一定要拆流程层

App.tsx 一旦同时承载:

  • 游戏主状态
  • 剧情生成
  • 战斗播放
  • NPC 交互
  • 地图
  • 选角

就会迅速失控。

当前更合理的分层思路是:

  • useGameFlow 负责基础游戏状态、世界选择、角色选择、初始进入逻辑
  • useCombatFlow 负责战斗计划与播放
  • useStoryGeneration 负责剧情生成、NPC 本地交互分流、选项池管理
  • useNpcInteractionFlow 负责同伴/NPC 展示态
  • GameShell 负责主容器与选择流程
  • AdventurePanel 负责冒险页文本和选项
  • NpcModals 负责交易 / 送礼 / 放生招募等弹窗

经验:

  • 流程层优先按“职责”拆,不按“文件长度”拆。
  • 状态修改逻辑尽量集中到 hook 内,不要散落在多个组件按钮回调里。

3.1 AI 草稿数据进列表前,要先补本地稳定标识

自定义世界、角色草稿、澄清问题、生成结果卡片这类数据,在草稿态或兼容旧数据时,id 可能为空。

经验:

  • React 列表的 key 不要直接裸用这类可能为空的 id
  • 当前选中态、草稿缓存、轮播焦点也不要直接绑空 id,否则会出现“点了第二张卡,结果还是第一张卡被选中”的错位。
  • 更稳的做法是:
    • 业务数据层尽量补齐真实 id
    • UI 层再补一层本地稳定 selectionKey / fallback render key
    • fallback 至少带上 index + 名称种子,保证当前列表内唯一

4. AI 只适合生成叙事,不适合决定关键规则

实践中最稳定的策略是:

  • AI 负责:
    • storyText
    • 非 NPC 关键规则的普通探索选项文案
  • 本地规则负责:
    • NPC 交互选项
    • 交易合法性
    • 礼物好感值
    • 招募阈值
    • 战斗掉落
    • 帮助奖励

经验:

  • 凡是会影响数值平衡、背包物品、好感、队伍成员的部分,都不要交给 AI 即时决定。
  • AI 生成内容要能被本地规则兜底,否则体验会不稳定。

5. NPC 系统要“角色型 NPC”和“普通 NPC”分开处理

项目里 NPC 实际上有两种:

  1. 普通场景 NPC可用通用 Medieval NPC 渲染
  2. 角色型 NPC应该复用玩家角色对应的立绘和动画

经验:

  • 只看 encounter.kind === 'npc' 不够,还要看 encounter.characterId
  • characterId 的 NPC应该优先走 CharacterAnimator
  • 否则就会出现:
    • 选了某个同伴,但开场看到的是另一套通用 NPC 外观
    • 战斗里角色型 NPC 看起来像普通路人

6. 位置与朝向必须统一到一套坐标规则

这是这类项目里最容易反复返工的点。

实践中踩过的坑:

  • sceneMonsters 用一套坐标逻辑
  • currentEncounter 用另一套坐标逻辑
  • 结果开场 NPC 和遇怪站位不一致
  • 角色型 NPC 立绘比怪物更容易出现“脚没落地”“太小”“翻转方向错”

最终经验:

  • 对面实体的横向定位必须统一到“怪物那套 world-space 逻辑”
  • 也就是:
    • 位置统一用怪物侧的 anchor
    • 相机平移时统一跟随同一套计算
  • 角色型 NPC 的垂直位置不能偷懒固定
    • 应该结合角色自身 groundOffsetY
  • 朝向规则要统一使用 getFacingTowardPlayer

一句话总结:

  • 角色型 NPC 不应该单独发明一套站位系统,而应该尽量复用怪物对位系统。

7. “新增流程”不要破坏原有选择 UI

这次初始同伴功能就是一个典型经验:

  • 用户原本对“选择扮演角色”的视觉和交互已经形成预期
  • 如果为了加“初始同伴选择”直接把角色选择页改成另一种样式,会造成明显割裂

经验:

  • 新流程优先插在旧流程后面,而不是重写旧流程
  • “确认角色 -> 选择初始同伴 -> 进入冒险”比“把原选角页改成全新样式”风险小很多
  • 如果必须改 UI也要尽量保留旧页面的视觉结构和交互节奏

8. 冒险页布局要优先保证画面和选项完整可见

冒险页真正的优先级是:

  1. 上方画布要在一屏内正常显示
  2. 下方 3 个选项要在一屏内正常显示
  3. 剧情文本框剩余空间自适应

经验:

  • 文本框不能无限长撑开
  • 正确做法是:
    • 文本框高度自适应剩余空间
    • 文本超长时内部滚动
    • 不能让 storyText 把战斗画面和选项挤出首屏

9. 编辑器页和玩家页要明确隔离

当前项目里存在编辑器页面:

  • PresetEditor
  • NpcVisualEditor
  • StateFunctionEditor

玩家页和编辑器页的需求完全不同。

经验:

  • 像字体切换、视觉统一这种全局改动,不要直接打到整个站点
  • 应该只挂在非编辑器根容器上
  • 比如 fusion-pixel-app 这种类,只挂在正式游玩界面,不挂在编辑器根节点

10. 构建环境问题要项目内消化

实际踩到的构建问题:

  • Node 16 环境下Vite 构建会因为 crypto.getRandomValues 缺失报错

沉淀出的解决方式:

  • 不强依赖开发机立刻升级 Node
  • 在项目内增加 Vite 启动包装脚本
  • 统一让 dev / build / preview 都走这层 shim

经验:

  • 环境兼容问题如果能在项目内吸收,就尽量不要把负担转移给每个协作者
  • 文档里要明确记录“为什么这样做”

11. 比较稳的开发顺序

后续继续扩展功能时,建议遵守这个顺序:

  1. 写状态字段
  2. 写规则工具函数
  3. 写流程 hook
  4. 接 UI
  5. npm run lint
  6. npm run build
  7. 再做视觉微调

不要反过来做:

  • 先做 UI
  • 再补状态
  • 最后硬修流程

这种顺序在状态复杂的项目里会越改越乱。

12. 当前最值得继续坚持的原则

  • 保持 AI 生成和本地规则分工清晰
  • 保持角色型 NPC 与普通 NPC 的渲染分流
  • 保持“怪物 / encounter / 战斗 NPC”统一坐标系
  • 保持新增功能不破坏既有核心 UI 体验
  • 保持编辑器页与玩家页隔离
  • 每次大改后都用 lint + build 双重验证

13. 后续建议

下一阶段最值得继续沉淀的方向:

  1. 把 NPC 交互逻辑继续从 useStoryGeneration 中独立成更纯粹的 useNpcInteractionFlow
  2. 把角色型 NPC 的位置、缩放、贴地参数做成可配置规则,而不是继续散落在画布里微调
  3. 针对初始同伴流程补一份单独的状态图 / 时序图
  4. 对大 chunk 警告做代码分包

14. SpacetimeDB 绑定桥接层要做同名去重

server-rs/crates/spacetime-client 里有一部分内容是围绕 SpacetimeDB 生成绑定补的手写桥接层。

经验:

  • 新增 procedure、input type 或 mapper 时,先全局确认 module_bindings/mod.rsmapper.rs、业务封装文件里是否已经存在同名声明
  • module_bindings/mod.rs 同一个模块只保留一条 pub mod 和一条 pub use,不要同时放在 reducer 区和 procedure 区
  • mapper.rs 的字符串枚举解析函数、API 入参结构只保留一个权威定义,业务侧统一复用
  • 业务封装文件里同一个 procedure 只暴露一个客户端方法,避免 Rust 在编译期出现 E0428、E0252、E0119、E0592 这类重复定义错误
  • 修复重复绑定时优先删除后追加的重复块,不要重写整文件,避免影响中文注释和生成绑定附近的大段内容

15. 一句话总结

这个项目真正的开发经验不是“怎么多写一个按钮”,而是:

  • 在 AI 叙事、像素演出、战斗状态、NPC 规则、选择流程和编辑器体系同时存在的情况下,始终让每条链路各归其位。