# 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 脚本可以依赖 controller,controller 不依赖具体 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--before.png docs/ui-baseline/godot--after.png docs/ui-baseline/godot--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 截图不变后,再进入状态和农场玩法拆分。