news 2026/10/30 8:27:23

Godot-GridGame架构DI容器全局联动设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Godot-GridGame架构DI容器全局联动设计

前言

本文是 针对Godot-GridGame(网格游戏)架构设计的研究(全程开源)-CSDN博客 中联动各层级的DI核心容器的设计,阅读前请先阅读该文章的总体架构设计,同时本文将会默认读者已经阅读过本架构的5层详细设计,这5个层级设计可以从主架构文章中跳转链接查看。

1. DI 核心设计总览

1. DI 核心容器设计思想

DI核心容器是各层之间执行注入操作以及初始化操作的总容器,信号连接,大部分层级类注入都在这里完成。此外,容器可以操作每一个层级的出入口,认识所有的层级,大部分层级之间的连接均在DI容器完成,同时大部分层级的tick比如逻辑层的tick由DI容器统一驱动

对于DI容器注入,在本框架中,一共有4种注入方式:

1. 场景节点注入:在Godot中使用 @export 可以将场景中的节点挂载进DI容器,然后DI容器将挂载的节点注入到需要注入的层级中

2. 构造注入:在DI容器中将需要注入的数据在对应层级类初始化时传入初始化数据,构造对应类。

3. 引用注入:类已经完成创建后,将数据引用传入类中

4. 资源注入:层级对应的子场景注入,比如掉落物桥需要的掉落物实体场景注入到管理器中(前几篇已经演示过),与场景节点注入类似,不过此种注入目前不归DI管理,是直接通过 @export 注入到对应管理器中

DI 容器需要负责场景的加载流程,是游戏的总入口,对于本框架,加载顺序一般为

加载数据层数据 ==> 加载逻辑层数据,逻辑层依据数据层生成实体/物品层数据 ==> 建立逻辑层与桥接层,UI层的连接 ==> 调用逻辑层接口设置给予的世界参数并渲染地图

2. 基于DI核心容器设计思想设计的本项目实物

在详细介绍本项目的DI容器设计前,先给出本项目的DI容器框架图

对于本演示项目,DI容器使用场景节点注入持有TileMapLayer,InputController,DropBridge,PlayerBridge,UiBridge,GlobalCamera,使用构造注入持有RenderBridge,World,其中InputController 与 RenderBridge同样也被DI容器使用引用注入分别注入 GlobalCamera 与 World。资源注入为各桥接层和UI层自己持有,本项目DI容器不负责注入。

本演示项信号连接方式分为死信号和活信号。死信号是DI容器在启动时一键连接并后续不再更改的信号,活信号则是连接后会根据游戏当前情况动态换绑,比如玩家和UI层的信号绑定,DI容器会根据当前控制的玩家动态切换渲染的玩家数据为当前玩家

2. DI容器详细设计

本项目完整的DI容器启动游戏流程为

_tile_cell_load() 加载瓦片方块资源==>>_item_load() 加载物品方块集==>>构建Godot的TileMapLayer图集并载入==>>使用TileMapLayer构造渲染桥==>>_load_world()使用种子加载当前世界==>>引用注入全局相机到 InputController==>>_debug_start()所有核心是否加载==>>World世界注入到渲染桥==>>_connect_signals()各个重要层级类建立信号连接==>>_render_map() 渲染地图==>>_set_world()设置世界重力等基本属性并重生玩家

需要注意的是:渲染桥的 World 注入是五层中唯一打破信号驱动的直持引用(渲染每帧高频读方块,走信号经容器中转有性能损失,详见桥接层篇)

对于加载顺序中的具体加载,如下

1. 资源加载

_tile_cell_load() 与 _item_load() 分别使用数据层的对外接口,流程为

调用瓦片属性与物品属性的 reset_counter() 刷新属性id计数器==>>使用瓦片属性与物品属性的加载接口自动构建数据层属性映射表==>>调用瓦片实例与物品实体的加载接口自动构建实例的映射表

2. Godot图集加载与渲染桥构建

DI容器在构件中主要负责注入节点树中的TileMapLayer节点供渲染桥渲染,具体的构建流程如下

使用TileSetBuilder调用build_tileset()构建新的TileMapLayer图集==>>将构建完成的图集载入场景树中的TileMapLayer节点==>>将场景树中的TileMapLayer作为初始化参数构建渲染桥

3. 世界逻辑加载

_load_world() 使用种子作为初始化参数构建新的World世界类,后续创建流程由逻辑层出入口World类完成

4. 玩家控制加载与核心校验

DI容器在这一步将全局相机注入到 InputController中,并使用 _debug_start()校验各个层级核心是否加载,流程如下

注入全局相机 camera 到 player_input_control中==>>校验渲染桥是否存在==>>校验World世界,逻辑层出口是否存在

5. 核心层级类信号连接

DI容器需要在完成上述加载与校验后连接各个核心层级类的信号,会调用 _connect_signals() 执行连接,目前本演示项目的连接顺序如下(当前演示并未制作统一信号连接器,所以为手动连接)

1. 玩家输入控制信号的 block_destroy_request 连接 world.try_destroy_tile_dir

2. 玩家输入控制信号的 block_place_request 连接 world.try_place_tile

3. 世界的 tile_cell_updated 连接 render_bridge.refresh_single_tile

4. 世界的 gravity_updated 连接 _on_gravity_change(DI容器函数)

5. 世界的 drop_added 连接 drop_bridge.on_drop_added

6. 世界的 drop_removed 连接 drop_bridge.on_drop_removed

7. 世界的 drop_apply_force 连接 drop_bridge.on_drop_apply_force

8. 掉落物桥的 drop_position_updated 连接 world.receive_drop_updated_info

9. 世界的 control_player_uid_changed 连接 _on_control_player_uid_change(DI容器函数)

10. 世界的 spawn_player_data 连接 sync_player_entity(DI容器函数)

上述为死信号连接,一般用于层级核心类之间固定的信号绑定连接,下面还用动态信号连接,为当前控制的玩家与UI层的信号连接,因为UI层渲染的信息应为当前控制的玩家的信息。当前DI容器的活信号连接流程为

_on_control_player_uid_change收到世界发送的主玩家变更uid==>>校验当前玩家信息,换绑全局相机跟随并调用 _set_ui()==>>调用当前缓存的旧玩家数据并执行解绑,同时换用从World中拿到的新的控制玩家数据,并重新安装UI层的数据==>>更新当前控制玩家的 uid 数据到DI

6. 地图渲染

完成上述操作后,DI容器将会进行地图渲染。需要注意的是,渲染桥没有渲染存储,只负责渲染,渲染存储在DI容器处(读者可以自行分离功能到渲染桥),而世界中存储的区块除了渲染的区块,还有未渲染的模拟区块。具体流程如下

DI容器调用 _render_map()==>>从World拿到全部玩家数据==>>遍历玩家,将全部玩家指定范围内的区块全部加入渲染存储==>>判断区块变化,调用 _render_around_player渲染区块==>>遍历使用 render_bridge.clear_chunk_render清除旧区块,遍历使用 render_bridge.render_chunk 渲染新区块

以上为一次渲染流程,DI容器运行的时候会将渲染放置于每帧 tick 中循环

7. 设置世界

完成以上所有流程后,设置世界的属性比如重力和重生玩家等,目前演示项目中调用 _set_world(),后调用 world.set_gravity 与 world.spawn_new_player 完成设置,需要注意的是,世界只是持有重力数据,完成更改后要发送信号并由DI完成实际更改(因为当前更改重力更改的是AREA_PARAM_GRAVITY,需要对场景根节点使用)

8. 完整本项目DI容器设计

本项目的完整DI容器设计如下

extends Node2D ## 主节点DI容器 ## 场景节点注入 @export var tile_map_layer: TileMapLayer @export var player_input_control: InputController @export var drop_bridge: DropBridge @export var player_bridge: PlayerBridge @export var ui_bridge: UiBridge @export var camera:GlobalCamera ## 核心类(渲染器,世界) var render_bridge: RenderBridge var world: World var player_chunk_pos:Vector2i var render_distance: int = 1 # 1 = 3x3 区域,2 = 5x5 区域 ## 玩家所在区块 var is_render_ready: bool = true ## 保存当前已经绘制在TileMap上的区块 var rendered_chunks:Dictionary = {} ## 存储上一个绑定的玩家数据实例(用于安全解绑信号) var bound_player_data:PlayerData = null ## 存储上一个主玩家uid(仅日志/调试) var last_control_uid: int = -1 ## 启动检查 func _ready() -> void: # 加载资源ca # 先加载属性再加载实例最后图集贴图 _tile_cell_load() _item_load() TileSetBuilder.build_tileset() # 加载图集 tile_map_layer.tile_set = TileSetBuilder.built_tileset # 创建世界 render_bridge = RenderBridge.new(tile_map_layer) _load_world() DebugSystem.console_common_log("核心节点", "创建世界: %s" % world) # 注入节点给玩家控制检测(玩家并没有创建) player_input_control.camera = camera # 测试所需核心是否加载 _debug_start() render_bridge.world = world if is_render_ready: _connect_signals() _render_map() _set_world() ## 瓦片加载 func _tile_cell_load() -> void: TileCellAttribute.reset_counter() TileCellAttributes.load_mapping_attributes() TileCellInstances.load_mapping_instance() ## 物品加载 func _item_load() -> void: ItemAttribute.reset_counter() ItemAttributes.load_mapping_attributes() ItemInstances.load_mapping_instance() ## 创建并加入玩家 func sync_player_entity(new_player_data: PlayerData) -> void: # 同步玩家实体与数据:PlayerEntity视图节点持有,DI本身不持有 player_bridge.on_add_player(new_player_data) DebugSystem.console_common_log("核心节点", "向玩家桥发送玩家实体创建信息: %s" % [new_player_data]) ## 世界加载 func _load_world() -> void: world = World.new(54213) ## 世界属性设置 func _set_world() -> void: world.set_gravity(980.665) world.spawn_new_player() ## Ui 加载(这里ui信号连接不放在 _connect_signals, 方便统一更改, 活信号) func _set_ui() -> void: # 优先用缓存的实例引用解绑,不再通过uid去查找 if bound_player_data != null: bound_player_data.inventory_change.disconnect(_inventory_change_transfer) DebugSystem.console_common_log("核心节点", "解绑旧玩家物品信号,uid=%s" % bound_player_data.get_uid()) bound_player_data = null var temp_player_data: PlayerData = world.get_player_data_by_uid(world.get_control_player_uid()) if temp_player_data == null: DebugSystem.console_warn_log("核心节点", "获取控制玩家数据失败,UI无法初始化") return bound_player_data = temp_player_data # 初始化UI全量加载背包数据 ui_bridge.setup(temp_player_data) # 绑定新玩家物品变更信号 temp_player_data.inventory_change.connect(_inventory_change_transfer) # 更新记录uid last_control_uid = temp_player_data.get_uid() ## 连接信号 (死信号,一般是固定管理器连接) func _connect_signals() -> void: # 方块破坏 player_input_control.block_destroy_request.connect(world.try_destroy_tile_dir) # 方块放置 player_input_control.block_place_request.connect(world.try_place_tile) # 信号转发(更改通知渲染) world.tile_cell_updated.connect(render_bridge.refresh_single_tile) world.gravity_updated.connect(_on_gravity_change) # 连接掉落物信号 world.drop_added.connect(drop_bridge.on_drop_added) world.drop_removed.connect(drop_bridge.on_drop_removed) world.drop_apply_force.connect(drop_bridge.on_drop_apply_force) drop_bridge.drop_position_updated.connect(world.receive_drop_updated_info) # 玩家创建信号 world.control_player_uid_changed.connect(_on_control_player_uid_change) world.spawn_player_data.connect(sync_player_entity) ## 物理帧循环 func _physics_process(delta: float) -> void: # 向世界提供主循环 world.tick_world(delta) ## 渲染帧循环 func _process(delta: float) -> void: if !is_render_ready: return _render_map() ## 多人完整区块渲染 func _render_map() -> void: # 从世界实体存储器拿到全部玩家数据 var player_dict = world.get_player_entity_dict() var target_chunks:Dictionary = {} # 遍历所有玩家,各自生成周围区块,自动合并去重 for uid in player_dict: var p_data:PlayerData = player_dict[uid] var tile_pos:Vector2i = p_data.get_player_grid_pos() var cx:int = int( floor( float(tile_pos.x) / Chunk.CHUNK_SIZE_X ) ) var cy:int = int( floor( float(tile_pos.y) / Chunk.CHUNK_SIZE_Y ) ) var chunk_center = Vector2i(cx, cy) # 当前玩家render_distance范围区块 for dx in range(-render_distance, render_distance + 1): for dy in range(-render_distance, render_distance + 1): var c_pos = Vector2i(chunk_center.x + dx, chunk_center.y + dy) target_chunks[c_pos] = true # 字典key自动去重 # 和上一帧渲染区块对比,加载/卸载,原有逻辑原样保留 var need_refresh:bool = false # 判断集合是否变化 if target_chunks.size() != rendered_chunks.size(): need_refresh = true else: for old_c in rendered_chunks: if !target_chunks.has(old_c): need_refresh = true break if need_refresh: _render_around_player(target_chunks) # 重载,接收合并后的目标区块字典 func _render_around_player(target_chunks:Dictionary): # 清除旧区块 for old_chunk in rendered_chunks: if !target_chunks.has(old_chunk): render_bridge.clear_chunk_render(old_chunk) rendered_chunks.erase(old_chunk) # 新增区块 for new_chunk in target_chunks: if !rendered_chunks.has(new_chunk): render_bridge.render_chunk(new_chunk) rendered_chunks[new_chunk] = true # 维护区块输出 # DebugSystem.console_common_log("测试世界", "当前维护区块: %s" % rendered_chunks) ## 检查各项参数 func _debug_start() -> void: # 检查渲染桥 if render_bridge != null: DebugSystem.console_common_log("核心节点", "渲染搭桥存在") else: DebugSystem.console_error_log("核心节点", "渲染搭桥不存在") is_render_ready = false if world != null: DebugSystem.console_common_log("核心节点", "世界本体存在") else: DebugSystem.console_error_log("核心节点", "世界本体不存在") is_render_ready = false ## 显示渲染状态 DebugSystem.console_common_log("核心节点", "渲染状态: %s" % is_render_ready) ## ========================= 核心 DI 容器全局信号 ====================== ## 重力数据变更 → 调用引擎物理服务器 func _on_gravity_change(new_g:float): var world_2d = get_world_2d() if world_2d == null: DebugSystem.console_warn_log("核心节点: 重力", "无法获取2D世界空间") return var space_rid = world_2d.space # 设置重力 PhysicsServer2D.area_set_param(space_rid, PhysicsServer2D.AREA_PARAM_GRAVITY, new_g) PhysicsServer2D.area_set_param(space_rid, PhysicsServer2D.AREA_PARAM_GRAVITY_VECTOR, Vector2(0, 1)) DebugSystem.console_common_log("核心节点: 重力", "设置重力: %s" % new_g) ## 当前控制玩家发生变化 func _on_control_player_uid_change(new_uid: int) -> void: DebugSystem.console_common_log("核心节点", "当前控制玩家 uid 更改为: %s" % new_uid) var target_entity = player_bridge.get_player_uid_entity_map().get(new_uid) if target_entity != null: camera.set_follow_case(target_entity) # 这里注意新的值由 ui 设置赋予给当前存储的主玩家数据 _set_ui() ## 物品栏(背包更改信号中转) func _inventory_change_transfer( change_pos: Vector2i, new_item_name: String, new_item_num: int, inv_type: InventoryBase.InventoryType ) -> void: DebugSystem.console_common_log("核心节点", "执行物品栏数据与对应UI转发") ## 物品栏传给ui渲染对应信息 ui_bridge.update_inventory_slot(change_pos, new_item_name, new_item_num, inv_type)

3. 后续以及补充

当前项目主场景即是DI容器的代替,没有独立的DI容器类,想要在游戏中更改World以及渲染桥等的实现需要创建独立容器类并更改注入方式,读者可以自行尝试。

当前DI容器的注入并不完全集中在DI中,比如桥接层的子场景,同时加载顺序全部编码在 _ready中,为早期测试。读者可以更改为独立的DI容器并启用JSON规定注入顺序以及注入位置,脱离本演示测试写法

当前DI容器生命周期没有管理器,大量死信号方便测试,仅留必要的控制玩家动态管理信号,读者可以自行增加生命周期DI子管理器,负责信号生命周期的管理

当前DI容器脚本功能集中,因为早期测试无拆分,以及当前的世界创建等种子固定,无游戏主页面,读者都可以更改扩展。

当前文章为本架构文的最后一篇附属文,这里返回主架构文

针对Godot-GridGame(网格游戏)的DI注入+RefCounted数据驱动信号流架构设计的研究(全程开源)-CSDN博客

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/30 8:26:02

手把手教你:基于深度学习的滚动轴承故障诊断(2026 修订版)

手把手教你:基于深度学习的滚动轴承故障诊断(2026 修订版) 本文是《滚动轴承故障诊断》系列的第 1 篇(总览)。 系列完整目录见文末【专栏导航】。本文配套代码与分步操作手册见公众号「免费代码合集」菜单。 代码下载链…

作者头像 李华
网站建设 2026/10/30 8:25:04

C++联编机制:静态与动态绑定的核心解析

1. C联编机制的本质解析联编(Binding)是C面向对象编程中一个核心但常被忽视的概念。简单来说,它解决的是"调用哪个函数"的问题。在C中,同一个函数名可能对应多个实现——可能是重载函数,也可能是派生类覆盖的…

作者头像 李华
网站建设 2026/10/30 8:21:45

在 SAP S/4HANA 中用 Email Template、CL_BCS 与 CID 内嵌图片发送品牌化 HTML 邮件

企业项目里有一类需求很常见,业务事件发生以后,SAP 系统需要给客户、供应商或者内部员工发送一封邮件。技术上看,这件事似乎很简单,调用 CL_BCS,准备 Subject 和 Body,再执行 SEND 就结束了。 真正落到企业场景里,事情往往没这么简单。 业务部门通常不会满足于一封纯文…

作者头像 李华
网站建设 2026/10/30 8:21:27

AI 外呼系统情绪识别功能选型避坑指南

选型 AI 外呼系统的情绪识别功能,最容易被 demo 里"客户一怒,系统秒切安抚话术"的演示迷住。但真实场景里,一台成熟外呼系统的接通率要做到 56% 以上、答准率 99%、识破率低于 0.1%,情绪识别才有可信的数据基础——一旦…

作者头像 李华
网站建设 2026/10/30 8:21:18

小白程序员必学:用Prompt Engineering让AI瞬间变身“数字战友”

本文通过“超级天才”比喻解析Prompt Engineering的核心逻辑,即如何将模糊需求转化为AI可执行的高确定性指令。文章对比了坏提示词(意识流提问)与好提示词(结构化任务清单)的输出差异,并给出三套实用公式&a…

作者头像 李华