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

18 KiB
Raw Permalink Blame History

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 性能风险

  • 多处运行时动态创建 TextureRectButtonLabel,面板反复打开会产生 GC 和节点树抖动。
  • _process 负责倒计时刷新,后续页面增多后容易变成常驻开销。
  • 图集区域和 AtlasTexture 在 UI 创建阶段按需拼装,缺少统一缓存。
  • 土地渲染仍有全量清空再重建的路径,后续 12 块地之外的扩展会放大成本。
  • UI 面板逻辑和接口逻辑绑定在同一个脚本里,不利于做请求节流、失败回滚和本地状态差异更新。

3. 目标分层

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 原始资源、替换资源、生成资源、图集索引和缓存

核心规则:依赖只能从上往下,不能反向依赖。

feature UI -> controllers -> domain use cases -> services/state

UI 不直接调用 HTTPRequest也不直接解析后端原始字段。服务返回归一化数据状态层负责保存UI 订阅状态变化并做局部刷新。

4. 推荐目录

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. 启动流程

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 节点。

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. 土地玩法流程

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_timecrop_id、配置时间计算阶段和剩余时间。
  • FarmApi:封装 farm/sow-seedsfarm/gather-cropfarm/clear-landfarm/fertilizefarm/disease
  • FarmMapper:把后端字段归一化为 LandData
  • FarmController:连接输入、规则、接口、状态。
  • LandGridView:只管理 12 个 LandSlotView
  • LandSlotView:只管单块土地的底图、作物、状态图标、点击热区。
  • LandActionMenu:只管圆形菜单的弹出、收回、按压动效。
  • PlantTipView:只管倒计时面板显示。

7.2 性能规则

  • LandSlotView 常驻 12 个节点,初始化后不销毁。
  • 作物 TextureRect、虫草旱状态图标从 NodePool 复用。
  • 土地刷新入口只接受 position,默认不允许全量刷新。
  • 成熟倒计时使用一个 Timer 每秒驱动,只更新可见 PlantTipView,不扫描全部 UI。
  • 圆形菜单打开时创建或复用固定数量按钮,不在每次点击时重新加载纹理。

8. 面板流程

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

目标接口:

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 直接传字符串接口名:

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 调用。

推荐事件:

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. 文件迁移映射

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. 验证流程

每次拆分必须执行:

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 改动必须保存截图:

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 截图不变后,再进入状态和农场玩法拆分。