18 KiB
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. 目标分层
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_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. 面板流程
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 --quitgodot --headless --path godot/FarmGodot --check-only --script res://scripts/ui/game_scene.gd- Web 导出和浏览器截图通过。
阶段 1:抽资源层
先从 GameScene 抽出无业务风险的资源工具:
TextureRegionCatalogAtlasTextureCacheResourcePathsNodePool- 通用
_add_texture、_add_nine_patch、_add_panel_label、_add_texture_button
验收:
- 主场景截图不变。
- 打开商店、仓库、好友、土地菜单无缺图。
阶段 2:抽状态和规则
抽纯逻辑,不动 UI 节点布局:
PlayerStateFarmStateInventoryStateLandActionRulesCropPhaseRulesFarmMapperInventoryMapper
验收:
- 播种、收获、铲除、施肥、除虫、除草结果一致。
- 土地倒计时和成熟判断一致。
阶段 3:抽农场玩法
把土地相关 1000 行左右逻辑迁到 farm feature:
FarmControllerLandGridViewLandSlotViewLandActionMenuPlantTipViewSeedBagView
验收:
- 点击土地菜单位置、动效、空白关闭一致。
- 成熟点击行为一致。
- 单块土地操作后只刷新单块土地。
阶段 4:抽 TopBar 和主菜单
TopBarViewMainMenuViewLeftMenuViewTopEntryMenuView
验收:
- 顶部资源文字、战斗力位图、经验条、头像位置不变。
- 主菜单展开和收起动效不变。
阶段 5:抽面板
按风险从低到高拆:
FriendPanelWarehousePanelShopPanelBuildingPanelDogPanelSignPanel
验收:
- 每拆一个面板,单独截图对比。
- 面板关闭后节点数不持续增长。
阶段 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 脚本可以依赖 controller,controller 不依赖具体 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 工厂,因为这部分不改变业务行为:
- 新建
scripts/resources/atlas_texture_cache.gd。 - 新建
scripts/resources/texture_region_catalog.gd。 - 新建
scripts/ui/common/ui_factory.gd或拆成更细的 view helpers。 - 把
GameScene中_common_texture、_friend_texture、_rank_texture、_building_texture、_landup_texture、_sign_texture、_region_for、_atlas_texture迁出。 - 保持所有 UI 截图不变后,再进入状态和农场玩法拆分。