hy-farm/docs/godot-architecture-flow.md
2026-05-29 19:54:56 +08:00

638 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Godot 架构流程设计
## 1. 目标
当前 `godot/FarmGodot/scripts/ui/game_scene.gd` 已经承担了主场景渲染、土地玩法、背包、商店、仓库、好友、房屋升级、狗、弹窗、动画、资源裁剪、接口调用和数据修正等职责,文件长度已超过 3000 行。继续在这个文件里堆功能会让性能优化、UI 对齐和后续功能开发都变慢。
重构目标不是立刻重写全部代码,而是建立一套可持续拆分的 Godot 客户端架构:
- 性能优先:减少节点创建、减少全量重绘、减少 `_process` 常驻逻辑、减少重复纹理加载。
- UI 一致:保留 Egret 运行截图作为验收基准,拆分不能改变视觉和动效表现。
- 业务清晰UI 只负责显示和输入,后端协议、数据归一化、玩法规则放到独立模块。
- 渐进迁移:每次只拆一个垂直模块,拆完必须跑 Godot 检查、Web 导出和截图验证。
## 2. 当前问题
### 2.1 职责混杂
`GameScene` 当前包含这些职责:
- 顶部资源栏、经验条、头像、战斗力位图字体。
- 主菜单、左侧菜单、顶部入口、弹窗层。
- 商店、仓库、好友、房屋升级、土地扩展、狗面板。
- 土地点击、长按、圆形操作菜单、成熟倒计时、种植提示。
- 播种、收获、铲除、施肥、除虫、除草等后端请求。
- 背包数据、仓库数据、消耗扣除、本地土地数据更新。
- 图集区域读取、AtlasTexture 创建、通用按钮和标签工厂。
这些职责应该拆成服务层、状态层、玩法层、UI 层和资源层。
### 2.2 性能风险
- 多处运行时动态创建 `TextureRect``Button``Label`,面板反复打开会产生 GC 和节点树抖动。
- `_process` 负责倒计时刷新,后续页面增多后容易变成常驻开销。
- 图集区域和 `AtlasTexture` 在 UI 创建阶段按需拼装,缺少统一缓存。
- 土地渲染仍有全量清空再重建的路径,后续 12 块地之外的扩展会放大成本。
- UI 面板逻辑和接口逻辑绑定在同一个脚本里,不利于做请求节流、失败回滚和本地状态差异更新。
## 3. 目标分层
```text
Bootstrap / AppRoot
-> ServiceRegistry
-> SceneRouter
-> GameSession
-> Feature Scenes
services/
网络、接口、账号、配置、日志
state/
玩家、农场、背包、仓库、UI 临时状态
domain/
纯业务模型、规则、后端数据映射
features/
farm、topbar、main_menu、shop、warehouse、friend、building、dog、sign
ui/common/
通用面板壳、按钮、标签、Tab、虚拟列表、动画工具
assets/
Egret 原始资源、替换资源、生成资源、图集索引和缓存
```
核心规则:依赖只能从上往下,不能反向依赖。
```text
feature UI -> controllers -> domain use cases -> services/state
```
UI 不直接调用 `HTTPRequest`也不直接解析后端原始字段。服务返回归一化数据状态层负责保存UI 订阅状态变化并做局部刷新。
## 4. 推荐目录
```text
godot/FarmGodot/
scenes/
bootstrap.tscn
app/
app_root.tscn
farm/
farm_scene.tscn
land_slot.tscn
crop_view.tscn
land_action_menu.tscn
plant_tip.tscn
seed_bag.tscn
ui/
top_bar.tscn
main_menu.tscn
modal_layer.tscn
common_panel_shell.tscn
tab_bar.tscn
panels/
shop_panel.tscn
warehouse_panel.tscn
friend_panel.tscn
building_panel.tscn
dog_panel.tscn
sign_panel.tscn
scripts/
app/
app_root.gd
scene_router.gd
service_registry.gd
game_session.gd
event_bus.gd
services/
api_client.gd
auth_service.gd
farm_api.gd
inventory_api.gd
shop_api.gd
friend_api.gd
building_api.gd
dog_api.gd
state/
player_state.gd
farm_state.gd
inventory_state.gd
warehouse_state.gd
ui_state.gd
domain/
models/
player_data.gd
land_data.gd
inventory_item.gd
cost.gd
rules/
land_action_rules.gd
crop_phase_rules.gd
cost_rules.gd
mappers/
login_mapper.gd
farm_mapper.gd
inventory_mapper.gd
resources/
atlas_texture_cache.gd
texture_region_catalog.gd
resource_paths.gd
node_pool.gd
ui/
common/
bitmap_font_label.gd
panel_shell.gd
texture_button_view.gd
virtual_item_list.gd
tween_runner.gd
top_bar/
top_bar_view.gd
main_menu/
main_menu_view.gd
farm/
farm_scene.gd
farm_controller.gd
land_grid_view.gd
land_slot_view.gd
crop_view.gd
land_action_menu.gd
plant_tip_view.gd
seed_bag_view.gd
panels/
shop_panel.gd
warehouse_panel.gd
friend_panel.gd
building_panel.gd
dog_panel.gd
sign_panel.gd
```
## 5. 启动流程
```mermaid
flowchart TD
A["Bootstrap"] --> B["ServiceRegistry.create"]
B --> C["AuthService.auto_login"]
C --> D["GameSession.set_player"]
D --> E["SceneRouter.open FarmScene"]
E --> F["FarmScene.setup(session)"]
F --> G["TopBarView.bind(PlayerState)"]
F --> H["FarmController.bind(FarmState, InventoryState)"]
H --> I["FarmState.init_from_login_land_data"]
I --> J["LandGridView.render_dirty_slots"]
```
启动阶段只做三件事:
- 创建全局服务。
- 登录并归一化玩家数据。
- 打开主农场场景。
`Bootstrap` 不再持有具体页面逻辑,后续切换账号、进入好友农场、回到家园都走 `SceneRouter`
## 6. 状态流
### 6.1 状态对象
状态对象只保存数据和版本号,不创建 UI 节点。
```text
PlayerState
token
user_id
nickname
level
exp
gold
diamond
power
house_level
dog_level
version
FarmState
lands_by_position
changed_positions
version
InventoryState
seeds
fertilizers
warehouse_items
version
UIState
selected_land_position
active_panel
menu_open
```
### 6.2 更新原则
- 后端请求成功后,先更新状态,再通知 UI。
- 单块土地变化只标记 `changed_positions`,禁止整块农场全量重绘。
- 玩家资源变化只刷新 TopBar不刷新主场景。
- 背包物品数量变化只刷新对应 item不重建整个 ScrollContainer。
- 失败请求不改状态,只显示 toast 或错误态。
## 7. 土地玩法流程
```mermaid
sequenceDiagram
participant LandSlot
participant FarmController
participant Rules as LandActionRules
participant Menu as LandActionMenu
participant Api as FarmApi
participant State as FarmState
LandSlot->>FarmController: pressed(position)
FarmController->>Rules: get_actions(land_data)
Rules-->>FarmController: actions
FarmController->>Menu: show(position, actions)
Menu->>FarmController: action_selected(action)
FarmController->>Api: request(action, payload)
Api-->>FarmController: normalized_result
FarmController->>State: apply_land_result(position, result)
State-->>LandSlot: dirty update
```
### 7.1 拆分模块
- `LandActionRules`:判断空地、成长中、成熟、虫草旱、可收获、可铲除。
- `CropPhaseRules`:根据 `seed_time``crop_id`、配置时间计算阶段和剩余时间。
- `FarmApi`:封装 `farm/sow-seeds``farm/gather-crop``farm/clear-land``farm/fertilize``farm/disease`
- `FarmMapper`:把后端字段归一化为 `LandData`
- `FarmController`:连接输入、规则、接口、状态。
- `LandGridView`:只管理 12 个 `LandSlotView`
- `LandSlotView`:只管单块土地的底图、作物、状态图标、点击热区。
- `LandActionMenu`:只管圆形菜单的弹出、收回、按压动效。
- `PlantTipView`:只管倒计时面板显示。
### 7.2 性能规则
- `LandSlotView` 常驻 12 个节点,初始化后不销毁。
- 作物 `TextureRect`、虫草旱状态图标从 `NodePool` 复用。
- 土地刷新入口只接受 `position`,默认不允许全量刷新。
- 成熟倒计时使用一个 `Timer` 每秒驱动,只更新可见 `PlantTipView`,不扫描全部 UI。
- 圆形菜单打开时创建或复用固定数量按钮,不在每次点击时重新加载纹理。
## 8. 面板流程
```mermaid
flowchart TD
A["MainMenuView item pressed"] --> B["ModalLayer.open(panel_id)"]
B --> C["PanelFactory.get_or_create"]
C --> D["Panel.bind(state/services)"]
D --> E["Panel.show_enter_animation"]
E --> F["Panel requests data if stale"]
F --> G["State update"]
G --> H["Panel render dirty list"]
```
### 8.1 面板拆分
每个面板是独立 `.tscn + .gd`
- `ShopPanel`:商店 tabs、商品列表、购买按钮。
- `WarehousePanel`:仓库 tabs、出售弹窗、数量步进器。
- `FriendPanel`:好友列表、排序、翻页、访问好友家园。
- `BuildingPanel`:房屋升级、土地扩展。
- `DogPanel`:狗状态、狗粮选择、喂养。
- `SignPanel`:签到面板,后续接入新签到资源。
公共能力放 `ui/common`
- `PanelShell`:标题、关闭按钮、背景九宫格。
- `TabBar`:选中态和禁用态。
- `VirtualItemList`:复用可见 item避免大量节点。
- `TextureButtonView`:统一按钮按压缩放、禁用态、音效钩子。
### 8.2 面板缓存
- 弹窗默认懒加载,第一次打开实例化,后续隐藏复用。
- 大列表只创建屏幕可见 item 加少量缓冲。
- 面板关闭只停止动画和输入,不释放资源;内存压力确认后再引入 LRU。
## 9. 资源和图集
### 9.1 资源原则
- `assets/egret` 保存原始迁移资源,不在业务代码里直接到处写路径。
- `assets/replacement` 保存替换素材。
- 后续生成的独立图标、裁剪图集和字体索引放 `assets/generated`
- 所有图集区域通过 `TextureRegionCatalog` 查找。
- 所有 `AtlasTexture` 通过 `AtlasTextureCache` 获取。
### 9.2 AtlasTextureCache
目标接口:
```gdscript
func get_region(atlas_path: String, region_name: String) -> Texture2D
func get_rect(atlas_path: String, region_name: String) -> Rect2
func preload_group(group_name: String) -> void
func clear_unused() -> void
```
性能要求:
- 同一个 `atlas_path + region_name` 只创建一次 `AtlasTexture`
- 高频 UI 的图集在场景进入时预加载。
- 不在 `_process`、列表 item 创建循环里解析 JSON。
## 10. 网络和用例
### 10.1 ApiClient
`ApiClient` 只负责:
- URL 拼接。
- Egret `msg=` base64 表单编码。
- HTTP 请求。
- JSON 解析。
- HTTP 状态和业务状态包装。
### 10.2 Feature Api
每个业务 API 包一层,避免 UI 直接传字符串接口名:
```text
AuthService.login(username, password)
InventoryApi.fetch_store_house(token)
FarmApi.sow_seed(token, position, item_id)
FarmApi.gather_crop(token, position)
FarmApi.clear_land(token, position)
FarmApi.fertilize(token, position, item_id)
FarmApi.kick_disease(token, position, type)
ShopApi.buy(token, store_id)
WarehouseApi.sell(token, item_id, count)
```
### 10.3 请求性能规则
- 同一按钮请求中必须禁用重复点击。
- 同一资源数据有缓存版本,未过期不重复拉取。
- 操作成功后用接口返回增量更新本地状态,不强制重新拉全部玩家数据。
- 失败时保留原状态,避免 UI 和服务端不一致。
## 11. 事件机制
使用轻量 `EventBus` 或状态信号,不能把所有节点互相 `get_node` 调用。
推荐事件:
```text
player_changed(player_state)
farm_land_changed(position)
inventory_changed(item_id)
warehouse_changed(item_id)
panel_opened(panel_id)
panel_closed(panel_id)
toast_requested(message)
```
高频事件禁止广播大对象,传 id 或 position再由接收方按需读取状态。
## 12. 性能基线
当前目标是海外 H5默认 Web 单线程导出,性能预算按中低端手机浏览器设计。
### 12.1 渲染预算
- 主农场常驻节点数目标小于 250。
- 打开一个普通面板后总节点数目标小于 450。
- 单次土地操作后新增临时节点小于 10并在动画结束后复用或回收。
- UI idle 状态 `_process` 逻辑为 0 或只保留一个倒计时入口。
### 12.2 刷新预算
- TopBar只有玩家资源变化时刷新。
- LandSlot只有对应土地变化时刷新。
- Bag只有打开背包或物品数量变化时刷新。
- Panel list只刷新当前 tab 和可见 item。
- Texture场景运行中禁止重复解析同一个图集 JSON。
### 12.3 动画预算
- 动效统一走 `TweenRunner`,避免散落创建不可控 Tween。
- 高频按钮按压只做 scale/modulate不做复杂 shader。
- 循环动画必须在节点隐藏时停止。
- 面板关闭时要 kill 关联 tween避免隐藏节点继续动画。
## 13. 迁移顺序
### 阶段 0冻结基准
- 保存当前 Godot 主场景截图。
- 保存新版 HTML 主场景截图。
- 记录当前可用功能:自动登录、主场景、土地菜单、播种、收获、铲除、施肥、除虫、除草、倒计时、背包、主要面板。
验收:
- `godot --headless --path godot/FarmGodot --import --quit`
- `godot --headless --path godot/FarmGodot --check-only --script res://scripts/ui/game_scene.gd`
- Web 导出和浏览器截图通过。
### 阶段 1抽资源层
先从 `GameScene` 抽出无业务风险的资源工具:
- `TextureRegionCatalog`
- `AtlasTextureCache`
- `ResourcePaths`
- `NodePool`
- 通用 `_add_texture``_add_nine_patch``_add_panel_label``_add_texture_button`
验收:
- 主场景截图不变。
- 打开商店、仓库、好友、土地菜单无缺图。
### 阶段 2抽状态和规则
抽纯逻辑,不动 UI 节点布局:
- `PlayerState`
- `FarmState`
- `InventoryState`
- `LandActionRules`
- `CropPhaseRules`
- `FarmMapper`
- `InventoryMapper`
验收:
- 播种、收获、铲除、施肥、除虫、除草结果一致。
- 土地倒计时和成熟判断一致。
### 阶段 3抽农场玩法
把土地相关 1000 行左右逻辑迁到 farm feature
- `FarmController`
- `LandGridView`
- `LandSlotView`
- `LandActionMenu`
- `PlantTipView`
- `SeedBagView`
验收:
- 点击土地菜单位置、动效、空白关闭一致。
- 成熟点击行为一致。
- 单块土地操作后只刷新单块土地。
### 阶段 4抽 TopBar 和主菜单
- `TopBarView`
- `MainMenuView`
- `LeftMenuView`
- `TopEntryMenuView`
验收:
- 顶部资源文字、战斗力位图、经验条、头像位置不变。
- 主菜单展开和收起动效不变。
### 阶段 5抽面板
按风险从低到高拆:
1. `FriendPanel`
2. `WarehousePanel`
3. `ShopPanel`
4. `BuildingPanel`
5. `DogPanel`
6. `SignPanel`
验收:
- 每拆一个面板,单独截图对比。
- 面板关闭后节点数不持续增长。
### 阶段 6删除旧 GameScene 负担
最终 `GameScene` 只保留:
- 场景组合。
- 绑定 session。
- 转发生命周期给 feature controller。
目标行数:
- `farm_scene.gd` 小于 300 行。
- 每个 panel 脚本小于 500 行。
- 每个 rules/mapper/state 文件小于 250 行。
- 单个脚本超过 800 行必须继续拆分。
## 14. 文件迁移映射
```text
game_scene.gd:_render_player
-> top_bar/top_bar_view.gd
-> state/player_state.gd
game_scene.gd:_setup_main_menu ... _on_main_menu_item_pressed
-> main_menu/main_menu_view.gd
game_scene.gd:_open_shop_panel ... _buy_shop_item
-> panels/shop_panel.gd
-> services/shop_api.gd
game_scene.gd:_open_warehouse_panel ... _sell_warehouse_item
-> panels/warehouse_panel.gd
-> state/warehouse_state.gd
game_scene.gd:_open_friend_panel ... _create_friend_row
-> panels/friend_panel.gd
-> services/friend_api.gd
game_scene.gd:_open_house_upgrade_panel ... _extend_land
-> panels/building_panel.gd
-> services/building_api.gd
game_scene.gd:_open_dog_panel ... _feed_selected_dog_food
-> panels/dog_panel.gd
-> services/dog_api.gd
game_scene.gd:_setup_farm_interaction ... _get_land_node
-> farm/farm_controller.gd
-> farm/land_grid_view.gd
-> farm/land_slot_view.gd
-> farm/land_action_menu.gd
-> farm/plant_tip_view.gd
game_scene.gd:_refresh_store_house ... _extract_warehouse_items
-> services/inventory_api.gd
-> domain/mappers/inventory_mapper.gd
-> state/inventory_state.gd
game_scene.gd:_sow_seed ... _kick_disease
-> services/farm_api.gd
-> farm/farm_controller.gd
```
## 15. 编码规则
- 每个新脚本必须有 `class_name`,方便类型标注。
- UI 脚本可以依赖 controllercontroller 不依赖具体 UI 子节点。
- 业务规则必须能脱离场景运行。
- 后端原始字段只允许 mapper 读取。
- UI 节点路径只允许 view 脚本内部使用。
- 大量路径常量统一放 `ResourcePaths`
- 不在 `_process` 里做数据解析、资源加载、节点创建。
- 不在列表 item 内部直接发请求,请求由 panel/controller 统一管理。
- 所有长动画、循环动画在 `_exit_tree` 或隐藏时停止。
## 16. 验证流程
每次拆分必须执行:
```bash
godot --headless --path godot/FarmGodot --import --quit
godot --headless --path godot/FarmGodot --check-only --script res://scripts/ui/game_scene.gd
godot --headless --path godot/FarmGodot --export-release Web /tmp/farm-godot-web/index.html
```
如果新增了脚本,对新增脚本逐个跑 `--check-only --script`
UI 改动必须保存截图:
```text
docs/ui-baseline/godot-<feature>-before.png
docs/ui-baseline/godot-<feature>-after.png
docs/ui-baseline/godot-<feature>-meta.json
```
涉及后端的功能必须记录:
- 接口路径。
- HTTP 状态。
- 业务 status。
- 关键响应字段。
- 本地状态变化。
## 17. 禁止事项
- 禁止继续把新页面直接塞进 `game_scene.gd`
- 禁止 UI 直接拼接口路径。
- 禁止功能成功后整页重载主场景。
- 禁止每次打开菜单都重新解析图集 JSON。
- 禁止隐藏面板后保留运行中的循环 tween。
- 禁止为了快速开发跳过截图验证。
- 禁止用“看起来差不多”替代新版 HTML 基准。
## 18. 下一步执行建议
最直接、风险最低的第一步是抽资源层和通用 UI 工厂,因为这部分不改变业务行为:
1. 新建 `scripts/resources/atlas_texture_cache.gd`
2. 新建 `scripts/resources/texture_region_catalog.gd`
3. 新建 `scripts/ui/common/ui_factory.gd` 或拆成更细的 view helpers。
4.`GameScene``_common_texture``_friend_texture``_rank_texture``_building_texture``_landup_texture``_sign_texture``_region_for``_atlas_texture` 迁出。
5. 保持所有 UI 截图不变后,再进入状态和农场玩法拆分。