Godot-GridGame架构逻辑层与实体/物品层篇

前言

本文章是 针对Godot-GridGame(网格游戏)的架构设计的研究(全程开源)-CSDN博客 中逻辑层 与实体**/物品层** 的详细介绍,阅读本文章前请先阅读该文章的整体架构设计,同时本文章将使用总架构设计中的数据层中提供的接口与数据 ,所以阅读本文章前也请先阅读 Godot-GridGame架构数据层篇 。本文将会使用已经构建完成的项目代码(整个)用于帮助介绍本框架逻辑层 与实体/物品层的设计。

由于逻辑层与实体/物品层有着非常大的关联(逻辑层运算依靠实体/物品层构建的运行时数据),流程上是一条完整的数据链,所以这里本文把实体/物品 层与逻辑层合篇讲解:逻辑层上层(World 及管理器)负责世界运算,实体 / 物品层作为逻辑层的数据与运行时实体层,提供 Item/TileCell/ 物品容器供上层使用

1. 逻辑层总览

1. 逻辑层与实体/物品层设计思想

逻辑层负责在数据层与实体/物品层的基础上,负责构建Godot对应的地图图集 ,管理世界区块 ,管理掉落物逻辑 ,管理玩家逻辑和判定 等,也就是负责游戏核心运行数据的计算。而为了维护逻辑层,本架构将大部分的逻辑均包装在了World类 里面,所以基本所有的逻辑层的出入口都是 World 类 (除了图集生成)。这一层之间的层内逻辑连接则使用Godot的信号机制和回调,这样设计时方便管理耦合度,也好解决逻辑层上层和逻辑层子层双引用的问题,尽量减少内存泄漏的风险。

还有为了区分逻辑层与数据层,逻辑层运算使用的数据虽然源于数据层,但是仍然进行了层层封装,比如将数据层的ItemInstance 继续包装成 Item 再包装成 DropItemData ,这样逻辑层在使用数据的时候可以减少对数据层运行实体数据的污染,也方便隔离层级,方便调试排查,而这一层就是实体/物品层。

但是需要注意的是 Godot 并没有私有数据,所以类中的私有数据建议在命名前加上下划线。

2. 基于逻辑层与实体/物品层设计思想设计的本项目实物

再详细介绍演示项目逻辑层的架构设计之前,先给出本演示项目逻辑层完整的架构示意图

由于逻辑层中涉及数量较多的功能,本文将会解释最核心的功能以及部分值得列举的子逻辑功能。

在已经完成的项目中,逻辑层主要设计了TileSetBuilder类(图集构建类) ,World类(世界类) ,而World中包含逻辑层的重要子类 DropManager(掉落物管理器) ,EntityStorage(实体管理器) ,TerrainGenerator(地形生成器按照区块)。

上述都是逻辑层上层的产物,包含了逻辑层的几乎大部分运算,而实际的逻辑层下层(逻辑层下层其实就是被拆出来的实体/物品层 )还设计了跨层接口 ,用于隔层处理数据层的数据,而尽量避免直接操作数据层数据,比如 TileCell(瓦片方块类) ,用于操作区块内单个格子而不是直接操作 TileCellInstance;还有逻辑层自己的数据类比如 PlayerData(玩家数据) ,DropItemData(掉落物数据) , EntityBase(实体数据) 等。(注意,PlayerData玩家数据下也包含背包,继承于InventoryBase 类(物品栏数据类),属于实体/物品类)

实体/物品类依据数据层生成的数据,使用工厂类,比如 EntitySpawner 类,用于生成游戏运行中的数据

对于本演示项目基于框架设计的逻辑层,整体的逻辑层启动流程为

DI 核心容器启动 :==>

TileSetBuilder类(图集构建器)构建图集,生成名字与ID图集映射表,并将图集存入 Godot 中的网格地图TileMapLayer ==> ......(其他流程) ==> 世界获得种子并创建 ==> 世界创建 TerrainGenerator 生成器(区块按需懒加载生成) ==> 生成新的掉落物管理器 ==> 生成新的实体管理器 ==> 世界内部连接各个管理器信号,并提供转发函数供外部调用

演示中,各个逻辑层的独特流程将会在逻辑层执行类中解释,同时在解释完详细的重要类后会在此基础上解释一次详细流程

2. 详细拆解设计说明

前提设计

由于本架构是 GridGame网格游戏 的架构,所以游戏需要定义网格坐标与网格区块,这一层属于逻辑层的功能,这里在解释下文设计之前先给出当前演示项目的网格计算工具 CoordinateAttributes类(网格属性类)

复制代码
extends RefCounted
## 网格属性类,定义了当前网格游戏的网格距离
class_name CoordinateAttributes
## 默认网格距离
const _DEFAULT_DISTANCE: int = 32
## 这个是全局像素坐标系统,用来确定整个游戏的像素位置的
static var _distance: int = _DEFAULT_DISTANCE

## 获取全局像素距离
static func get_global_distance() -> int:
	return _distance

## 修改全局格子大小,带合法性校验
static func set_distance(new_val: int) -> void:
	if new_val <= 0:
		push_warning("CoordinateAttributes: distance不能小于等于0!保持旧值 %s" % _distance)
		return
	_distance = new_val

## 返回对应网格距离
static func get_coordinate_distance() -> int:
	return CoordinateAttributes.get_global_distance()
	
## 计算当前网格位置
static func cal_current_grid_pos(pos_x: float, pos_y: float) -> Vector2i:
	var current_distance: int = get_coordinate_distance()
	var cal_grid_x = floori(pos_x / current_distance)
	var cal_grid_y = floori(pos_y / current_distance)
	# 生成完整坐标
	return Vector2i(cal_grid_x, cal_grid_y)
	
## 将网格坐标转化为实际坐标
static func switch_grid_position(grid_x: int, grid_y: int) -> Vector2:
	var current_distance: int = get_coordinate_distance()
	var position_x = grid_x * current_distance
	var position_y = grid_y * current_distance
	# 完整实际坐标
	return Vector2(position_x, position_y)

ChunkCoordinateUtils(区块工具类)

复制代码
extends RefCounted
## 瓦片区块工具
class_name ChunkCoordinateUtils

## 全局瓦片坐标 → 区块坐标
static func grid_to_chunk(grid_x: int, grid_y: int, chunk_size_x: int, chunk_size_y: int) -> Vector2i:
	var cx = int( floor( float(grid_x) / chunk_size_x ) )
	var cy = int( floor( float(grid_y) / chunk_size_y ) )
	return Vector2i(cx, cy)

## 全局瓦片坐标 → 区块坐标 + 区块内局部坐标
static func grid_to_chunk_and_local(grid_x: int, grid_y: int, chunk_size_x: int, chunk_size_y: int) -> Dictionary:
	var chunk_pos:Vector2i = grid_to_chunk(grid_x, grid_y, chunk_size_x, chunk_size_y)
	var lx = grid_x - chunk_pos.x * chunk_size_x
	var ly = grid_y - chunk_pos.y * chunk_size_y
	var local_pos:Vector2i = Vector2i(lx, ly)
	return {
		"chunk": chunk_pos,
		"local": local_pos
	}

1. 逻辑层数据类与实体/物品数据类

逻辑层拥有自己的数据类,同时也使用实体/物品层的数据类,用于运行时动态创建,隔离逻辑层与数据层防止数据层被污染,以下比较重要的逻辑层数据类

1. Chunk(区块数据基类)

TerrainGenerator(地形生成器按照区块) 生成地形前需要知道生成什么样的地形格式,而Chunk 是地形生成结果的存储格式。区块类存储当前区块所拥有的所有 TileCell类 (瓦片方块类,地图实际的方块),同时提供对区块操作的接口,比如修改单个方块,删除方块等,同时为了方便后续存档,区块采用缓存结构,除了存储修改过的方块数据,也临时存储种子生成的地形,减少区块生成计算量,以下为Chunk区块类

复制代码
extends RefCounted
## 区块类 Chunk, 区块数据
class_name Chunk
## 这里是网格坐标的32个格子,默认网格坐标为 Godot 32 像素距离为 1 格
const CHUNK_SIZE_X = 32
const CHUNK_SIZE_Y = 32
## 世界区块坐标再次从网格坐标上除以 CHUNK_SIZE_X, 模板默认 Vector2i(0, 0)
var world_chunk_pos:Vector2i = Vector2i(0, 0)
## 区块是否需要保存,以及区块保存的数据
var is_dirty:bool = false
var modified_cells: Dictionary = {}
## 临时原生地形缓存,不写入存档(也就是种子生成的地形)
var original_cache: Dictionary = {}

## 获取区块内方块相对坐标, 范围应为 0 到 31
func get_cell(local_pos: Vector2i) -> TileCell:
	if not check_pos_range(local_pos):
		return null
	# 存在修改过的则返回修改状态
	if modified_cells.has(local_pos):
		return modified_cells[local_pos]
	if original_cache.has(local_pos):
		return original_cache[local_pos]
	return null

## 向区块内存入新的修改方块	
func set_cell(local_pos: Vector2i, new_cell: TileCell):
	# 边界校验
	if not check_pos_range(local_pos):
		return
	modified_cells[local_pos] = new_cell
	is_dirty = true
	
## 删除,去除更改后的方块为原方块
func revert_cell(local_pos: Vector2i):
	if modified_cells.has(local_pos):
		modified_cells.erase(local_pos)
		is_dirty = true
## 清除原生地形缓存		
func clear_original_cache():
	original_cache.clear()

## 坐标范围检查
func check_pos_range(local_pos: Vector2i) -> bool:
	if local_pos.x < 0 or local_pos.x >= CHUNK_SIZE_X or local_pos.y <0 or local_pos.y >= CHUNK_SIZE_Y:
		return false
	return true

2. Item(运行时物品类)

逻辑层包括掉落物层的 DropItemData 等特定功能的逻辑层数据,为了隔离数据层不会直接持有 ItemInstance,同时因为ItemInstance 本身只是注册物品本身,不带有自身的实际堆叠数量等游戏运行的数据 ,所以采用 Item运行时物品类作为包装存储实际运行的物品,这样子各个层级交互的时候就只需要传递Item就可以了,减少了各个层级之间每次都要传递堆叠数量这种情况的发生。这种功能的定义也就使得Item类要有实际堆叠数量,状态等游戏实际运行需要的数据 以及内部处理 ItemInstance 避免外部直接处理的函数,以下为Item类

复制代码
extends RefCounted
## 运行时物品对象,存入背包 / 世界掉落物
## 持有静态模板 ItemInstance(来自注册表)
class_name Item
## 物品静态模板
var item_instance: ItemInstance
var current_stack_num: int = 1

## 初始化:绑定物品模板
func _init(new_instance: ItemInstance) -> void:
	item_instance = new_instance

## 更换物品模板
func change_item_instance(new_item_instance: ItemInstance) -> void:
	item_instance = new_item_instance
	
## 获取物品属性模板
func get_item_attribute() -> ItemAttribute:
	if item_instance == null:
		# 兜底默认,防止空引用报错
		return ItemAttributes.OriginalAttrType.get("STONE")
	return item_instance.get_item_attribute()

## 获取物品实例模板
func get_item_instance() -> ItemInstance:
	return item_instance

## 克隆:生成一份新的Item(共用同一个ItemInstance模板引用,浅拷贝)
func copy() -> Item:
	var new_item = Item.new(item_instance)
	return new_item

## 判断是否是同一种物品:比对 ItemInstance 引用,只有完全相同实例才能堆叠
func is_same_item(other: Item) -> bool:
	if other == null:
		return false
	# 直接对比ItemInstance引用,这才是物品/方块的本体模板
	return self.item_instance == other.item_instance

## 获得当前堆叠数量
func get_current_stack_num() -> int:
	return current_stack_num
	
## 更改堆叠数量
func change_stack_num(new_stack: int) -> void:
	current_stack_num = new_stack

3.TileCell(运行时瓦片方块类)

同物品类一致,地图上的每一个方块也不直接使用 TileCellInstance,而是采用TileCell运行时瓦片方块类包装,使用包装后的数据作为区块内实际存储的类,以下是运行时瓦片方块类

复制代码
extends RefCounted
## tileCell 单个瓦片方块属性
class_name TileCell
## 存储当前瓦片方块的实际瓦片类型
var tile_cell_instance: TileCellInstance

## 初始化完整方块,需要TileCellInstance(瓦片实例)
func _init(new_tile_cell_instance: TileCellInstance) -> void:
	tile_cell_instance = new_tile_cell_instance

## 更改瓦片对应实例
func change_cell_instance(new_tile_cell_instance: TileCellInstance) -> void:
	tile_cell_instance = new_tile_cell_instance
	
## 获得瓦片属性
func get_cell_attribute() -> TileCellAttribute:
	if tile_cell_instance == null:
		return TileCellAttributes.OriginalBlockType.get("AIR")
	return tile_cell_instance.get_cell_attribute()

## 获得方块实例
func get_cell_instance() -> TileCellInstance:
	return tile_cell_instance

## 克隆方法,返回克隆属性方块
func copy() -> TileCell:
	var new_cell = TileCell.new(tile_cell_instance)
	return new_cell

4. EntityBase(实体数据基类)

实体数据基类是所有实体的数据基类,比如是 DropItemData,PlayerData 的基类,为了方便实体管理器管理,应该包含实体所通用的属性比如 UID,实体类型等数据,以下是实体数据基类的设计

复制代码
extends RefCounted
## 实体基类
class_name EntityBase
## 实体类型标识,用来桥接层判断要生成哪种视图节点
enum EntityType {
	PLAYER,
	DROP_ITEM,
	MONSTER
}
## 全局唯一uid,由EntityStorage分配
var uid: int
var is_destroyed: bool = false
## 当前实体类型
var entity_type: EntityType

## 初始化
func _init(p_uid:int):
	uid = p_uid
## 返回 uid
func get_uid() -> int:
	return uid

5. DropItemData(运行时掉落物数据类)

运行时掉落物数据类是供 DropManager(掉落物管理器) 和其他与掉落物有关的管理器使用的数据,出了包含必须的 Item 数据类来标识当前的掉落物,也包含游戏运行时的比如重生时间,当前位置,速度和吸收状态等运行属性,以下是运行时掉落物类的设计

复制代码
## 继承于实体基类
extends EntityBase

## 掉落物【数据实例】,只保存状态,无tick逻辑,业务交给DropManager
class_name DropItemData
# 唯一编号,实例构造时分配
var item: Item
var world_pos: Vector2
var velocity: Vector2 = Vector2.ZERO
var spawn_time: float # 生成时记录全局时间戳,过期判断交给管理器
var is_pending_remove:bool = false
var is_absorbing: bool = false

func _init(new_uid:int, new_item:Item, start_pos:Vector2, current_world_time:float):
	uid = new_uid
	item = new_item
	world_pos = start_pos
	spawn_time = current_world_time
	# 初始化类型
	entity_type = EntityBase.EntityType.DROP_ITEM

6. PlayerData(运行玩家数据基类)

运行时玩家数据基类是玩家在游戏进行过程中绑定的数据,其中应该包含玩家的物品栏数据(背包类 BackPack ,以及快捷栏类InventoryQuickBar),以及玩家的位置,移动速度,跳跃速度等,同时由于是纯数据,所以还需要同步函数,这样在Godot运行实体的时候可以实时将玩家信息返回,以下是运行玩家数据基类

复制代码
extends EntityBase
## 玩家数据类
class_name PlayerData
## 玩家信号转发信号
signal inventory_change(
	change_pos: Vector2i, 
	new_item_name: String, 
	new_item_num: int, 
	inv_type: InventoryBase.InventoryType
	)
## 玩家属性
var spawn_position: Vector2 = Vector2(0, 0)
var move_speed: float = 300
var jump_velocity: float = -400.0
var global_position: Vector2
var grid_pos: Vector2i = Vector2i(0, 0)
## 玩家背包,玩家自持
var back_pack: BackPack
## 玩家快捷栏
var inventory_quick_bar: InventoryQuickBar
## 玩家初始化
func _init(new_uid: int, spawn_global_pos: Vector2 = Vector2(0, 0)) -> void:
	global_position = spawn_global_pos
	spawn_position = spawn_global_pos
	# 初始化类型
	entity_type = EntityBase.EntityType.PLAYER
	back_pack = BackPack.new()
	inventory_quick_bar = InventoryQuickBar.new()
	uid = new_uid
	DebugSystem.console_common_log("玩家(uid: %s)" % uid, "创建新的背包: %s" % [back_pack])
	_connect_signal()

## 获得玩家背包
func get_player_back_pack() -> BackPack:
	return back_pack
## 获得玩家快捷栏
func get_player_inventory_quick_bar() -> InventoryQuickBar:
	return inventory_quick_bar
## 获得玩家全局位置
func get_player_global_pos() -> Vector2:
	return global_position
## 获得玩家网格位置
func get_player_grid_pos() -> Vector2i:
	return grid_pos
## 外部同步数据
func sync_data(player_global_position: Vector2) -> void:
	global_position = player_global_position
	_sync_grid_pos()
## 同步网格坐标(内部调用,在外部传入实际位置的时候)
func _sync_grid_pos() -> void:
	grid_pos = CoordinateAttributes.cal_current_grid_pos(global_position.x, global_position.y)
## 尝试拾取物品
func try_pickup_item(item:Item) -> bool:
	# 未来可以在这里加玩家状态拦截
	# if cannot_pickup: return false
	var result = inventory_quick_bar.try_add_item(item)
	if !result:
		result = back_pack.try_add_item(item)
	return result
	
## =============== 玩家信号转发 ===================
## 内部信号连接
func _connect_signal() -> void:
	DebugSystem.console_common_log("玩家(uid: %s)" % uid, "绑定背包&快捷栏信号转发")
	# 玩家信号转发内部信号
	back_pack.inventory_changed.connect(inventory_change.emit)
	inventory_quick_bar.inventory_changed.connect(inventory_change.emit)
## 断开信号,销毁玩家前调用
func _disconnect_signal() -> void:
	if back_pack != null:
		back_pack.inventory_changed.disconnect(inventory_change.emit)
	if inventory_quick_bar != null:
		inventory_quick_bar.inventory_changed.disconnect(inventory_change.emit)

7. ItemSlot(运行时物品栏单个格子类)

运行时物品类格子类是物品栏设计中提供给物品类存储的单个数据类,主要存储当前格子所持有的 Item类,这样子单独拆分格子可以方便后续扩展箱子,和自定义格子等功能,以下是运行时物品类单个格子类的设计

复制代码
extends RefCounted
## 物品栏槽位,存储一个物品格子,内含 Item
class_name ItemSlot
## 槽位存储的实际物品
var item: Item = null
## 槽位锁定标志
var locked: bool = false

## 设置槽内物品
func set_item(new_item: Item) -> void:
	item = new_item
	
## 清空槽
func clear() -> void:
	item = null
## 获取当前物品
func get_item() -> Item:
	return item
## 判断槽是否为空
func is_empty() -> bool:
	return item == null
 ## 添加对应物品
func add_item(new_item: Item):
	item = new_item
	
## 增加堆叠数量
## 返回实际成功增加的数量(受物品模板最大堆叠限制)
func add_stack(amount: int) -> int:
	if item == null or amount <= 0:
		return 0
	var max_stack = item.item_instance.get_item_attribute().max_stack
	var can_add = min(amount, max_stack - item.current_stack_num)
	item.current_stack_num += can_add
	return can_add

## 减少堆叠数量
## 返回实际扣除数量;扣到0自动清空槽
func subtract_stack(amount: int) -> int:
	if item == null or amount <= 0:
		return 0
	var reduce = min(amount, item.current_stack_num)
	item.current_stack_num -= reduce
	if item.current_stack_num <= 0:
		clear()
	return reduce

8. InventoryBase(物品栏逻辑以及数据基类)

物品类逻辑以及数据基类是所有与物品栏有关的类的基类(比如 BackPack类 ,InventoryQuickBar类),拥有物品栏的通用操作比如添加物品,交换物品等,还需要有对应物品栏内位置计算的工具函数(因为Godot没有二维数组,只能用一维数据存储,对应xy坐标需要有对应工具换算),以下是物品类逻辑以及数据基类

复制代码
extends RefCounted
## 物品栏类父类,存储 Item 类,保存实体身上的物品
## 其他背包,快捷栏可继承物品栏类
## 需要注意的是,背包使用的查找坐标为 列 + 行
class_name InventoryBase
## 区分当前物品栏种类
enum InventoryType {
	BACKPACK,
	QUICK_BAR,
	EQUIPMENT, # 预留装备栏,以后直接扩展
	STORAGE_BOX # 预留储物箱
}
## 物品发生变更时触发
## 参数:变更坐标、物品名称、物品数量、容器类型
signal inventory_changed(
	change_pos: Vector2i, 
	new_item_name: String, 
	new_item_num: int,
	inv_type: InventoryType
	)
## 容器大小,供给外部获取(列,行)
var inventory_size: Vector2i = Vector2i(0, 0)
## 实际容器,存储物品槽
var slot_storage: Array[ItemSlot] = []
var inventory_type: InventoryType

## 提供尺寸初始化物品类
func _init(size: Vector2i) -> void:
	inventory_size = size
	## 构建容器
	var container_size = inventory_size.x * inventory_size.y
	slot_storage.resize(container_size)
	for i in range(slot_storage.size()):
		slot_storage[i] = ItemSlot.new()

## 获得背包尺寸
func get_inventory_size() -> Vector2i:
	return inventory_size
		
## 尝试往物品栏里面添加物品,支持堆叠
## 返回true成功;false背包已满无法放入
func try_add_item(new_item: Item) -> bool:
	# 第一步:优先寻找【同物品且还有堆叠空位】的槽位
	for index in range(slot_storage.size()):
		var slot = slot_storage[index]
		var slot_item = slot.get_item()
		# 槽位有物品,并且是同一种物品
		if slot_item != null and slot_item.is_same_item(new_item):
			var max_stack_num = slot_item.get_item_attribute().get_max_stack_num()
			var remain_stack = max_stack_num - slot_item.get_current_stack_num()
			if remain_stack > 0:
				# 可以堆叠,直接叠加数量
				var add_count = min(new_item.get_current_stack_num(), remain_stack)
				slot_item.change_stack_num(slot_item.get_current_stack_num() + add_count)
				new_item.change_stack_num(new_item.get_current_stack_num() - add_count) 
				# 发生堆叠则发射信号 ( 位置: 列 + 行 )
				var emit_pos: Vector2i = index_to_grid(index)
				var slot_item_name: String = slot_item.get_item_instance().get_item_name()
				var slot_num: int = slot_item.get_current_stack_num()
				# 发送信号
				inventory_changed.emit(emit_pos, slot_item_name, slot_num, inventory_type)
				DebugSystem.console_common_log("物品栏", "物品堆叠, 槽位%d, 叠加%d个, 总共堆叠: %d, 剩余待放:%d" % [index, add_count, slot_item.get_current_stack_num(), remain_stack])
				# 待放入物品已经全部塞完,直接返回成功
				if new_item.current_stack_num <= 0:
					return true
	
	# 第二步:没有可堆叠槽位,寻找空槽位放入新物品
	for index in range(slot_storage.size()):
		var slot = slot_storage[index]
		var slot_item = slot.get_item()
		if slot_item == null:
			slot.add_item(new_item)
			DebugSystem.console_common_log("物品栏", "空槽放入物品(槽位%s): %s" % [index, new_item])
			var emit_pos: Vector2i = index_to_grid(index)
			var slot_item_name: String = new_item.get_item_instance().get_item_name()
			var slot_num: int = new_item.get_current_stack_num()
			# 发送信号
			inventory_changed.emit(emit_pos, slot_item_name, slot_num, inventory_type)
			return true
	
	# 没有可堆叠位置、也没有空槽 → 背包满
	DebugSystem.console_common_log("物品栏", "物品栏已满,无法放入物品")
	return false

## 将本容器idx的物品移动到目标inventory的target_idx槽
func try_move_to_other_inventory(target_inv: InventoryBase, from_idx: int, target_idx: int) -> bool:
	var source_slot = get_slot_by_index(from_idx)
	var dest_slot = target_inv.get_slot_by_index(target_idx)
	if source_slot == null or dest_slot == null:
		return false
	
	var move_item = source_slot.get_item()
	if move_item == null:
		return false
	# 目标槽已有物品,不能直接覆盖,走swap逻辑
	if dest_slot.get_item() != null:
		# 取出目标槽物品,互换
		var dest_item = dest_slot.get_item()
		source_slot.set_item(dest_item)
		dest_slot.set_item(move_item)
	else:
		# 目标是空槽,直接移过去,原槽清空
		dest_slot.set_item(move_item)
		source_slot.clear()
	
	# ========== 修改:分别发送源槽、目标槽更新信号 ==========
	var src_pos = index_to_grid(from_idx)
	var dst_pos = target_inv.index_to_grid(target_idx)
	# 源容器源槽
	_emit_slot_update(src_pos, source_slot)
	# 目标容器目标槽
	target_inv._emit_slot_update(dst_pos, dest_slot)

	DebugSystem.console_common_log("物品栏", "跨容器移动:源槽%d → 目标槽%d" % [from_idx, target_idx])
	return true

## 将本容器(col,row)的物品移动到目标容器的(t_col,t_row)网格位置
func try_move_to_other_inventory_grid(target_inv: InventoryBase, col: int, row: int, t_col: int, t_row: int) -> bool:
	var from_idx = grid_to_index(col, row)
	var target_idx = target_inv.grid_to_index(t_col, t_row)
	if from_idx == -1 or target_idx == -1:
		return false
	return try_move_to_other_inventory(target_inv, from_idx, target_idx)

## 尝试交换当前容器内两个槽位物品
## @param idx_a 源槽索引
## @param idx_b 目标槽索引
## @return true 交换成功,false 交换失败
func try_swap_slot(idx_a: int, idx_b: int) -> bool:
	# 同一个槽,无需操作
	if idx_a == idx_b:
		return true
	
	var slot_a = get_slot_by_index(idx_a)
	var slot_b = get_slot_by_index(idx_b)
	# 索引非法
	if slot_a == null or slot_b == null:
		return false
	# 取出两个槽里面当前物品
	var item_a: Item = slot_a.get_item()
	var item_b: Item = slot_b.get_item()
	# 互换
	slot_a.set_item(item_b)
	slot_b.set_item(item_a)
	DebugSystem.console_common_log("物品栏", "交换槽位 %d <-> %d" % [idx_a, idx_b])

	# ========== 修改:交换两个槽,分别触发两次更新信号 ==========
	var pos_a = index_to_grid(idx_a)
	var pos_b = index_to_grid(idx_b)
	_emit_slot_update(pos_a, slot_a)
	_emit_slot_update(pos_b, slot_b)
	return true
	
## 尝试交换当前容器内两个网格位置的物品(列+行形式)
## @param col_a, row_a 源槽:列、行
## @param col_b, row_b 目标槽:列、行
## @return true交换成功;false失败(越界等)
func try_swap_slot_grid(col_a: int, row_a: int, col_b: int, row_b: int) -> bool:
	# 网格转一维索引
	var idx_a = grid_to_index(col_a, row_a)
	var idx_b = grid_to_index(col_b, row_b)
	
	# 越界检查
	if idx_a == -1 or idx_b == -1:
		return false
	
	# 调用已经写好的一维索引交换函数
	return try_swap_slot(idx_a, idx_b)

## 网格坐标(col列,row行) → 一维索引
func grid_to_index(col: int, row: int) -> int:
	# 边界检查,超出范围直接返回 -1
	if col < 0 or col >= inventory_size.x or row <0 or row >= inventory_size.y:
		return -1
	return row * inventory_size.x + col

## 一维索引 → 网格坐标 (col, row)
func index_to_grid(idx: int) -> Vector2i:
	if idx < 0 or idx >= slot_storage.size():
		return Vector2i(-1,-1)
	var row: int = idx / inventory_size.x
	var col: int = idx % inventory_size.x
	return Vector2i(col, row)

## 通过索引获取槽,自带越界保护
func get_slot_by_index(idx: int) -> ItemSlot:
	if idx < 0 or idx >= slot_storage.size():
		return null
	return slot_storage[idx]

## 通过网格坐标获取槽
func get_slot_by_grid(col: int, row: int) -> ItemSlot:
	var idx = grid_to_index(col, row)
	if idx == -1:
		return null
	return get_slot_by_index(idx)

## 设置指定索引槽内物品,自动触发信号
func set_slot_item(idx:int, new_item:Item) -> bool:
	var slot = get_slot_by_index(idx)
	if slot == null:
		return false
	slot.set_item(new_item)
	var pos = index_to_grid(idx)
	_emit_slot_update(pos, slot)
	return true

## 清空指定索引槽
func clear_slot(idx:int) -> bool:
	var slot = get_slot_by_index(idx)
	if slot == null:
		return false
	slot.clear()
	var pos = index_to_grid(idx)
	_emit_slot_update(pos, slot)
	return true

## 【私有辅助函数】统一发射槽更新信号
## 读取槽内当前物品信息,填充参数发送 inventory_changed
func _emit_slot_update(grid_pos:Vector2i, slot:ItemSlot) -> void:
	var item = slot.get_item()
	var item_name:String
	var item_count:int
	if item == null:
		# 空槽:名字为空字符串,数量0
		item_name = ""
		item_count = 0
	else:
		item_name = item.get_item_instance().get_item_name()
		item_count = item.get_current_stack_num()
	inventory_changed.emit(grid_pos, item_name, item_count, inventory_type)

9. BackPack(背包数据类)

背包数据类是玩家持有的背包类,继承于物品栏逻辑类,负责玩家背包的存储。以下是背包逻辑类

复制代码
extends InventoryBase
## 背包子类,玩家背包
class_name BackPack
## 背包初始化
func _init(size: Vector2i = Vector2i(9, 4)) -> void:
	inventory_type = InventoryType.BACKPACK
	DebugSystem.console_common_log("背包", "创建新背包,尺寸: %s" % size)
	super(size)

10. InventoryQuickBar(快捷栏数据类)

快捷栏数据类是玩家当前持有的快捷栏,应该持有当前快捷栏的选中等信息,这里没做。以下是快捷栏逻辑类

复制代码
extends InventoryBase
## 快捷栏(玩家手持物品栏)
class_name InventoryQuickBar

## 快捷栏默认 9 列 一行
func _init(size: Vector2i = Vector2i(9, 1)) -> void:
	inventory_type = InventoryType.QUICK_BAR
	DebugSystem.console_common_log("快捷栏", "创建新快捷栏,尺寸: %s" % size)
	super(size)

2. 逻辑层执行类(逻辑层上层)

逻辑层执行类是游戏真正的逻辑,包括上文所说的地图生成,图集构建等游戏逻辑功能

1. TileSetBuilder(图集构建类)

Godot 中网格地图可以使用 TileMapLayer 节点作为地图绘制节点,其中TileMapLayer节点需要使用图集来标识地图上当前绘画的网格贴图 ,而图集在TileMapLayer中使用数字ID编号 ,本框架数据层的贴图使用的是 String 名字作为贴图的名字,所以为了在地图上渲染的瓦片对应物品的贴图名字,需要程序按照 TextureName -> ID 的映射方式,构建图集并填入 TileMapLayer节点,构建完的映射表放置于图集构建类的静态数据保存,以下是 TileSetBuilder的设计

复制代码
extends RefCounted
## tileSet 图集生成
class_name TileSetBuilder
## 产物最终输出
static var built_tileset: TileSet
## 映射表,提供给渲染桥
## 映射表结构: { "name": [source_id, atlas_pos] }
static var name_to_source_id: Dictionary[String, Array] = {}
## 映射数组内数据
enum name_mapping{
	INDEX = 0,
	POS = 1
}
## 生成图集以及映射表
static func build_tileset() -> void:
	# 新建空TileSet
	built_tileset = TileSet.new()
	built_tileset.tile_size = Vector2i(
		CoordinateAttributes.get_coordinate_distance(),
		CoordinateAttributes.get_coordinate_distance()
	)

	# add_physics_layer,自动生成物理层
	built_tileset.add_physics_layer()
	built_tileset.set_physics_layer_collision_layer(0, 1)
	built_tileset.set_physics_layer_collision_mask(0, 1)
	
	name_to_source_id.clear()
	
	# 获取实例方块中所有的实例
	var tile_cell_instances: Dictionary = TileCellInstances.OriginalInstancesType
	DebugSystem.console_common_log("图集生成器", "获得属性集: %s" % tile_cell_instances)
	# 循环加入图集
	for name in tile_cell_instances:
		# 跳过空气
		if name == "air":
			continue
		get_texture_atlas_source(ResourcePath.tile_cell_texture_path, name)
	DebugSystem.console_common_log("图集生成器", "生成图集映射: %s 图集: %s" % [name_to_source_id, built_tileset])

## 添加对应图集
static func get_texture_atlas_source(texture_path: String, name: String) -> void:
	texture_path = "%s%s.png" % [texture_path, name]
	var tex: Texture2D = load(texture_path)
	if tex == null:
		DebugSystem.console_warn_log("图集生成器", "找不到贴图资源:%s" % texture_path)
		return
	# 创建图集源
	var atlas_source = TileSetAtlasSource.new()
	atlas_source.texture = tex
	# 单图,图集图块大小 为网格距离 x 网格距离
	var grid_distance: int = CoordinateAttributes.get_coordinate_distance()
	atlas_source.texture_region_size = Vector2i(
		grid_distance,
		grid_distance
	)
	var atlas_pos = Vector2i(0,0)
	# 注册这个图集内的瓦片
	atlas_source.create_tile(atlas_pos)
	
	# 添加source进tileset,拿到source_id
	var source_id = built_tileset.add_source(atlas_source)
	
	# 添加碰撞箱,通过检测物体属性(is_solid)
	var instance = TileCellInstances.OriginalInstancesType.get(name)
	if instance != null:
		# 固体添加碰撞箱
		var is_coll: bool = instance.get_cell_attribute().is_solid
		if is_coll:
			# 3. 给图集里面(0,0)这个方块加上碰撞
			var tile_data: TileData = atlas_source.get_tile_data(atlas_pos ,0)
			# 添加碰撞箱
			if tile_data != null:
				tile_data.add_collision_polygon(0)
				var grid_half: int = grid_distance / 2
				var rect_poly = PackedVector2Array(
					[
					Vector2(-grid_half, -grid_half),
					Vector2(grid_half, -grid_half),
					Vector2(grid_half, grid_half),
					Vector2(-grid_half, grid_half)
					]
				)
				tile_data.set_collision_polygon_points(0,0, rect_poly)

	# 存入名字映射
	name_to_source_id[name] = [source_id, atlas_pos]
	DebugSystem.console_common_log("图集注册表", "注册方块 %s source_id=%d" % [name, source_id])
	
## 根据方块名字查询source_id、atlas坐标,渲染用【改为static】
static func get_tile_draw_info(name: String) -> Array:
	if name_to_source_id.has(name):
		return name_to_source_id[name]
	else:
		# 空气,返回-1代表不绘制
		return [-1, Vector2i(-1,-1)]

2. TerrainGenerator(地形生成器)

地形生成器是逻辑层中负责按照种子生成地图的管理器,本演示项目中采用 Simplex 噪点生成演示洞穴地图的方式生成区块地图。地形生成器需要提供对单个区块的渲染,以及单个方块渲染的方法,最后返回计算完成的区块。以下是地形生成器的设计

复制代码
extends RefCounted
## 地形生成器
class_name TerrainGenerator
## 地形种子
var seed: int
var noise:Noise

## 初始化地图生成器的种子与噪声生成器
func _init(p_seed: int) -> void:
	seed = p_seed
	noise = FastNoiseLite.new()
	noise.seed = seed
	# 2D Simplex 噪声,地形生成
	noise.noise_type = FastNoiseLite.TYPE_SIMPLEX
	noise.fractal_type = FastNoiseLite.FRACTAL_FBM
	noise.fractal_octaves = 3
	noise.fractal_gain = 0.45
	noise.fractal_lacunarity = 2.0
	noise.frequency = 0.025
	
## 完整原始地形(完整区块)
func generate_chunk(chunk_world_pos: Vector2i, chunk_size_x: int, chunk_size_y: int) -> Dictionary:
	var result:Dictionary = {}
	for local_x in range(chunk_size_x):
		for local_y in range(chunk_size_y):
			var local_pos = Vector2i(local_x, local_y)
			# 计算全局瓦片坐标,保证跨区块连续
			var global_tile_x = chunk_world_pos.x * chunk_size_x + local_x
			var global_tile_y = chunk_world_pos.y * chunk_size_y + local_y
			var tile = _generate_single_tile(global_tile_x, global_tile_y)
			
			##if chunk_world_pos.x < 0:
			##	DebugSystem.console_common_log("生成器", "当前生成坐标x: %s %s" % [global_tile_x, global_tile_y])
			##	DebugSystem.console_common_log("生成器生成", "当前生成对应物品ID: %s" % tile.tile_type_id)
			result[local_pos] = tile
	return result
	
## 单个方块生成
func _generate_single_tile(gx:int, gy:int) -> TileCell:
	# FastNoiseLite.Get_noise_2d,输入浮点数
	var n_val = noise.get_noise_2d(float(gx), float(gy))
    # 生成 TileCell 运行时瓦片方块类
	var cell = TileCell.new(TileCellInstances.OriginalInstancesType.get("air"))
	# n_val范围 -1 ~ 1
	if n_val > 0.1:
		cell.change_cell_instance(TileCellInstances.OriginalInstancesType.get("stone"))
	else:
		cell.change_cell_instance(TileCellInstances.OriginalInstancesType.get("air"))
	return cell

3. DropManager(掉落物生成器/管理器)

掉落物生成器/管理器负责执行掉落物的逻辑包括生成,施加效果等逻辑。掉落物管理器自己会存有一份掉落物映射表,用于后续给本架构后面几层渲染,使用 UID -> DropItemData 的映射方式,同时需要注意的是,掉落物管理器会同时同步当前掉落物实体给实体管理器,实体管理器不操控掉落物,只是负责查表。掉落物生成器/管理器里面的掉落物生成,吸收,删除等信号会通过World中转出去,这里是DropManager的设计

复制代码
extends RefCounted

## 世界的副管理器,掉落物
class_name DropManager
## 掉落物删除原因
enum DropDestroyReason {
	NATURE = 0,
	PICKUP = 1
}
## 映射: uid -> DropItemData
var drop_dict:Dictionary = {}

## 不再定义 drop_added / drop_removed 信号
## 改用回调函数,由World注入回调
## 回调委托,由World传入
signal on_drop_added(new_drop: DropItemData)
signal on_drop_removed(uid: int, item: Item, destory: DropDestroyReason)
signal on_drop_apply_force(uid: int, absorb_force: Vector2)
signal on_entity_add(new_drop: DropItemData)
signal on_entity_destroy(uid: int, entity_type: EntityBase.EntityType)

## 掉落物控制属性: 掉落物拾取范围,吸收速度,吸收半径,存活时间
var PICKUP_RANGE: float = CoordinateAttributes.get_coordinate_distance() * 4
var ABSORB_SPEED: float = 1200 * 60
var PICKUP_THRESHOLD: float = CoordinateAttributes.get_coordinate_distance() * 1
var DROP_LIFETIME: float = 300

## 生成掉落物
func spawn(new_uid: int, item:Item, spawn_pos:Vector2, world_time:float, spawn_vel:Vector2=Vector2.ZERO) -> void:
	## 使用工厂类生成掉落物
	var new_drop = EntitySpawner.spawn_drop_item(new_uid, item, spawn_pos, world_time)
	new_drop.velocity = spawn_vel
	drop_dict[new_drop.get_uid()] = new_drop
	DebugSystem.console_common_log("世界[掉落物管理器]", "生成掉落物: uid: %s , 物品: %s, 对应名字: %s" % [new_drop.get_uid(), item, item.get_item_instance().get_item_name()])
	on_entity_add.emit(new_drop)
	# 回调给World
	on_drop_added.emit(new_drop)

## 单物品自持监测掉落物清除以及掉落物操作
## 主要是tick时间,是掉落物计数器
func tick(delta:float, world_time:float, player_dict:Dictionary) -> void:
	var to_remove_nature: Array[int] = []
	var to_remove_pick_up: Array[int] = []
	
	# 遍历所有玩家
	for uid in drop_dict:
		var drop:DropItemData = drop_dict[uid]
		if drop.is_pending_remove:
			continue
		
		# 生命周期超时,标记删除
		if world_time - drop.spawn_time >= DROP_LIFETIME:
			to_remove_nature.append(uid)
			drop.is_pending_remove = true
			continue

		# 【多人核心】查找距离这个掉落物最近的玩家
		var nearest_p_pos:Vector2
		var nearest_uid:int = -1
		var min_dist:float = INF
		for p_uid in player_dict:
			var p_data:PlayerData = player_dict[p_uid]
			var p_pos:Vector2 = p_data.get_player_global_pos()
			var d:float = drop.world_pos.distance_to(p_pos)
			if d < min_dist:
				min_dist = d
				nearest_p_pos = p_pos
				nearest_uid = p_uid
		
		# 没有任何玩家,跳过吸附拾取
		if nearest_uid == -1:
			continue
	
		var dist:float = min_dist
		
		# 进入拾取范围开启吸收标记;离开范围关闭
		if dist < PICKUP_RANGE and not drop.is_absorbing:
			drop.is_absorbing = true
		if dist > PICKUP_RANGE and drop.is_absorbing:
			drop.is_absorbing = false
		
		# 吸收掉落物,施加力的信号
		if drop.is_absorbing:
			var dir = (nearest_p_pos - drop.world_pos).normalized()
			var absorb_force:Vector2 = dir * ABSORB_SPEED * delta
			on_drop_apply_force.emit(uid, absorb_force)
		
		# 距离足够近,拾取成功
		if dist < PICKUP_THRESHOLD:
			to_remove_pick_up.append(uid)
			drop.is_pending_remove = true
			continue
	
	# 批量清理(自然超时)
	for uid in to_remove_nature:
		var drop_instance: DropItemData = drop_dict.get(uid)
		on_drop_removed.emit(uid, drop_instance.item, DropDestroyReason.NATURE)
		on_entity_destroy.emit(uid, drop_instance.entity_type)
		drop_dict.erase(uid)
	
	# 拾取清理
	for uid in to_remove_pick_up:
		var drop_instance: DropItemData = drop_dict.get(uid)
		# 【新增】物品进对应玩家背包
		var target_p_uid:int = -1
		var min_d:float = INF
		for p_uid in player_dict:
			var pd:PlayerData = player_dict[p_uid]
			var d = drop_instance.world_pos.distance_to(pd.get_player_global_pos())
			if d < min_d:
				min_d = d
				target_p_uid = p_uid
		if target_p_uid != -1:
			var target_player:PlayerData = player_dict[target_p_uid]
			DebugSystem.console_common_log("世界[掉落物管理器]", "尝试向玩家背包[uid:%s]添加物品: %s" % [target_p_uid, drop_instance.item])
			# 判断是否完全将物品添加进背包
			var add_ok:bool = target_player.try_pickup_item(drop_instance.item)
			if not add_ok:
				# 拾取
				drop_instance.is_pending_remove = false
				continue
			
		on_drop_removed.emit(uid, drop_instance.item, DropDestroyReason.PICKUP)
		on_entity_destroy.emit(uid, drop_instance.entity_type)
		drop_dict.erase(uid)

## 手动通过 uid 清除掉落物
func manual_remove_drop(uid:int, reason:DropDestroyReason):
	if not drop_dict.has(uid):
		return
	var drop = drop_dict[uid]
	drop.is_pending_remove = true
	on_drop_removed.emit(uid, drop.item, reason)
	on_entity_destroy.emit(uid, drop.entity_type)
	drop_dict.erase(uid)

## 世界信号: 更新掉落物信息
func updated_drop_info(uid: int, new_position: Vector2, in_linear_velocity: Vector2) -> void:
	var drop_instance: DropItemData = drop_dict.get(uid)
	if drop_instance == null:
		## DebugSystem.console_warn_log("世界[掉落物管理器]", "掉落物[uid]: %s 不存在" % uid)
		return
	drop_instance.world_pos = new_position
	drop_instance.velocity = in_linear_velocity
	## DebugSystem.console_common_log("世界[掉落物管理器]", "掉落物[uid]: %s 信息同步成功" % uid)

4. EntityStorage(实体存储器)

实体存储器存储游戏中所有的实体,包括玩家实体,掉落物实体,主要是用来方便外部获取世界内所有的实体,不负责逻辑,但是需要注意的是,实体的UID由实体存储器分配,这样子好管理,每个世界持有自己的实体管理器,这样每个世界的UID和映射就不会冲突。实体存储器需要提供一键按照需求查表的功能,以下是实体存储器的设计

复制代码
extends RefCounted
## 实体存储器,仅供查询,存储所有实体
class_name EntityStorage

## 实体表: 玩家表,掉落物表
var _players : Dictionary = {}
var _drops   : Dictionary = {}
var next_uid:int = 1 # 这个属于当前存档世界的uid,跟着存档保存/加载

## 实体信号,当实体被加入,移除的时候发出
signal entity_spawned(uid:int, entity:EntityBase)
signal entity_despawned(uid:int, entity:EntityBase)

## 添加实体(外部调用)
func add_entity(entity:EntityBase) -> void:
	DebugSystem.console_common_log("世界[实体管理查询器]", "添加实体[uid]: %s, 类型: %s" % [entity.get_uid(), entity.entity_type])
	match entity.entity_type:
		EntityBase.EntityType.PLAYER:
			_players[entity.get_uid()] = entity
			DebugSystem.console_common_log("世界[实体管理查询器]", "当前玩家实体表: %s" % _players)
		EntityBase.EntityType.DROP_ITEM:
			_drops[entity.get_uid()] = entity
			# DebugSystem.console_common_log("世界[实体管理查询器]", "当前物品实体表: %s" % _drops)
	entity_spawned.emit(entity.get_uid(), entity)
	
## 分配并获取下一个可用UID(新建实体调用)
func allocate_uid() -> int:
	var ret = next_uid
	next_uid += 1
	return ret
	
## 获得所有玩家实体(返回实体表)
func get_all_players() -> Dictionary:
	return _players
## 获得玩家uid对应的数据
func get_player_by_uid(uid: int) -> PlayerData:
	return get_all_players().get(uid)
## 获得掉落物实体
func get_drop(uid:int) -> DropItemData:
	return _drops.get(uid)
## 内部直接查找玩家存在
func has_player(uid:int) -> bool:
	return get_all_players().has(uid)
	
## 移除实体(外部业务管理器调用,只读存储,被动执行删除)
func remove_entity(uid:int, type:EntityBase.EntityType) -> bool:
	var ent:EntityBase = null
	DebugSystem.console_common_log("世界[实体管理查询器]", "移除实体[uid]: %s, 类型: %s" % [uid, type])
	match type:
		EntityBase.EntityType.PLAYER:
			ent = _players.get(uid)
			if ent == null:
				return false
			_players.erase(uid)
		EntityBase.EntityType.DROP_ITEM:
			ent = _drops.get(uid)
			if ent == null:
				return false
			_drops.erase(uid)
	
	if ent.is_destroyed:
		return false
	ent.is_destroyed = true
	entity_despawned.emit(uid, ent)
	return true

5. EntitySpawner(实体工厂类)

实体工厂类是负责一键产出对应的实体/物品数据运行时格式的类,设计如下

复制代码
extends RefCounted
## 实体生成器,用于统一生成实体并分配uid
class_name EntitySpawner

# 生成玩家实体数据
static func spawn_player(new_uid: int, start_pos:Vector2 = Vector2(0, 0)) -> PlayerData:
	var entity = PlayerData.new(new_uid, start_pos)
	return entity

# 生成掉落物实体数据
static func spawn_drop_item(new_uid: int, item:Item, pos:Vector2, world_time:float) -> DropItemData:
	var entity = DropItemData.new(new_uid, item, pos, world_time)
	return entity

3. World 世界类(逻辑层出口)

世界类是大部分逻辑类的出口,包括 DropManager,EntityStorage,TerrainGenerator 的运算数据和逻辑出口。同时世界类负责以上大部分逻辑执行类的初始化,每个世界类自己持有一份独立的各自逻辑执行类,为当前世界的逻辑数据,同时世界类还负责保存当前世界的属性比如重力等,以及转发内部管理器的信号,向外部提供结构(比如掉落物管理器发送的生成掉落物信号由世界类中转发出)

对于外部发送的请求,世界类收到请求后调用内部的逻辑执行类,外部不能直接调用内部逻辑执行类,同时世界类保持持有大部分逻辑执行类,但是逻辑执行类不持有世界类(防止双引用内存泄漏)

逻辑执行类之间也不相互持有,全部通过世界类用信号在初始化时创建连接,与其同时世界类向内部逻辑执行类提供tick,而世界类的tick由DI容器提供。以下是世界类的设计

复制代码
extends RefCounted
## 世界类,大部分逻辑类的出口
class_name World
## 方块数据发生变更时触发,携带修改的全局方块坐标
signal tile_cell_updated(global_tile_pos:Vector2i)
## 世界数据变更
signal gravity_updated(new_gravity:float)
## World对外暴露信号,DropBridge监听这一组(后续的桥接层,UI层监听)
signal drop_added(new_drop:DropItemData)
signal drop_removed(uid:int, item: Item, remove_reason: DropManager.DropDestroyReason)
signal drop_apply_force(uid:int, absorb_force: Vector2)
signal drop_info_updated(uid: int, new_position: Vector2, linear_velocity: Vector2)
signal control_player_uid_changed(uid: int)
signal spawn_player_data(player_data: PlayerData)
## 世界基本属性
var seed: int
var gravity:float = 980
var world_time:float = 0.0
var drop_manager:DropManager
var entity_storage: EntityStorage
## 区块清理
var chunk_prune_timer:float = 0.0
const PRUNE_INTERVAL:float = 2 # 2秒扫描一次区块,可调整
## 区块生成器 
var terrain_generator: TerrainGenerator
## 已加载的区块 Key: 区块坐标Vector2i, value: Chunk
var loaded_chunks: Dictionary[Vector2i, Chunk] = {}
var data_unload_radius: int = 5
## 玩家区块坐标,由外部每一帧写入玩家网格坐标后计算(World本身不持有player节点)
var player_chunk_pos: Vector2i = Vector2i.ZERO
## 当前玩家控制角色uid,由世界创建给予或者网络传输赋予
var control_player_uid: int = -1

## 按照种子初始化世界
func _init(p_seed) -> void:
	seed = p_seed
	terrain_generator = TerrainGenerator.new(seed)
	DebugSystem.console_common_log("世界", "创建世界,种子: %s" % seed)
	# 创建掉落物与实例查询分配器
	drop_manager = DropManager.new()
	entity_storage = EntityStorage.new()
	DebugSystem.console_common_log("世界", "生成掉落物管理器: %s" % drop_manager)
	DebugSystem.console_common_log("世界", "生成实体分配器: %s" % entity_storage)
	
	## 内部管理器信号连接
	drop_info_updated.connect(drop_manager.updated_drop_info)
	_connect_inner_signal()
	
## 世界数据需要外接的计时器,本框架对接核心脚本
func tick_world(delta:float) -> void:
	var player_dict = entity_storage.get_all_players()
	# 所有玩家网格位置坐标数组
	# 所有玩家区块坐标数组
	var all_player_grid_positions:Array[Vector2i] = []
	var all_player_chunk_pos:Array[Vector2i] = []
	
	# 遍历全部玩家,收集网格坐标 + 区块坐标
	for uid in player_dict:
		var player_entity_data = player_dict[uid] as PlayerData
		var grid_pos = player_entity_data.get_player_grid_pos()
		all_player_grid_positions.append(grid_pos)
		# 算出该玩家所在区块坐标
		var chunk_pos = ChunkCoordinateUtils.grid_to_chunk(grid_pos.x, grid_pos.y, Chunk.CHUNK_SIZE_X, Chunk.CHUNK_SIZE_Y)
		all_player_chunk_pos.append(chunk_pos)
	
	# 向掉落物管理器提供tick
	drop_manager.tick(delta, world_time, player_dict)
	# ========= 区块清理计时器,现在传入【全部玩家区块数组】 =========
	chunk_prune_timer += delta
	if chunk_prune_timer >= PRUNE_INTERVAL:
		prune_out_of_range_chunks(all_player_chunk_pos)
		chunk_prune_timer = 0.0
		DebugSystem.console_common_log("世界", "执行区块卸载,世界时间: %s" % world_time)
	
	world_time += delta

## 获得新的实体 uid
func get_new_uid() -> int:
	return entity_storage.allocate_uid()	
## 添加新的实体
func add_new_entity(new_entity: EntityBase) -> void:
	entity_storage.add_entity(new_entity)
## 去除实体
func despawn_entity(uid:int, type:EntityBase.EntityType):
	entity_storage.remove_entity(uid, type)
## 返回玩家实体表
func get_player_entity_dict() -> Dictionary:
	return entity_storage.get_all_players()
## 获得当前控制玩家uid
func get_control_player_uid() -> int:
	return control_player_uid
## 获得当前玩家的数据 (请不要更改)
func get_player_data_by_uid(uid: int) -> PlayerData:
	return entity_storage.get_player_by_uid(uid)
	
## 生成玩家
func spawn_new_player() -> void:
	# 申请创建实体
	var new_uid = get_new_uid()
	var new_player_data = EntitySpawner.spawn_player(new_uid)
	add_new_entity(new_player_data)
	DebugSystem.console_common_log("世界", "创建玩家数据: %s, uid: %s" % [new_player_data, new_uid])
	# 创建玩家数据发送给外部(信号)
	if control_player_uid == -1:
		control_player_uid = new_uid
		DebugSystem.console_common_log("世界", "当前创建为本地控制玩家uid: %s, uid: %s" % [new_player_data, new_uid])
		spawn_player_data.emit(new_player_data)
		emit_signal("control_player_uid_changed", control_player_uid)
	else:
		spawn_player_data.emit(new_player_data)
		
## 设置挡墙被控制的玩家
func set_control_player_uid(uid:int):
	# 简单校验:实体存在才允许设置
	if entity_storage.has_player(uid):
		DebugSystem.console_common_log("世界", "修改主玩家 uid: %s 存在对应数据实体, 修改控制数据" % uid)
		control_player_uid = uid
		# 发送新控制玩家 uid 请求
		emit_signal("control_player_uid_changed", control_player_uid)
	else:
		DebugSystem.console_common_log("世界", "修改主玩家 uid: %s 不存在对应数据实体" % uid)
		
## 设置重力,通知外部世界重力发生改变
func set_gravity(new_val:float):
	if new_val < 0: return
	gravity = new_val
	DebugSystem.console_common_log("世界", "设置重力: %s" % gravity)
	emit_signal("gravity_updated", gravity)
	
## 【全局读方块入口】整个游戏读取方块统一调用这个
## world_tile_pos:全局瓦片坐标(一格=32像素)
## 这里用区块算的
func get_tile(world_tile_pos: Vector2i) -> TileCell:
	# 1. 全局瓦片坐标 → 换算成【区块坐标】+【区块内局部坐标】
	var result = ChunkCoordinateUtils.grid_to_chunk_and_local(world_tile_pos.x, world_tile_pos.y, Chunk.CHUNK_SIZE_X, Chunk.CHUNK_SIZE_Y)
	var chunk_pos: Vector2i = result.chunk
	var local_pos: Vector2i = result.local
	
	##if world_tile_pos.x < 0:
	##	DebugSystem.console_common_log("TEST", "%s" % chunk_pos)
	
	# 2. 如果区块还没加载,新建+生成原生地形
	if not loaded_chunks.has(chunk_pos):
		var new_chunk: Chunk = Chunk.new()
		new_chunk.world_chunk_pos = chunk_pos
		# 调用地形生成器,生成本区块原始地形
		var raw_terrain:Dictionary = terrain_generator.generate_chunk(chunk_pos, Chunk.CHUNK_SIZE_X, Chunk.CHUNK_SIZE_Y)
		new_chunk.original_cache = raw_terrain
		loaded_chunks[chunk_pos] = new_chunk
	
	var chunk: Chunk = loaded_chunks[chunk_pos]
	return chunk.get_cell(local_pos)
	
## 【全局写方块入口】挖方块、放置方块统一调用
func set_tile(world_tile_pos: Vector2i, new_cell: TileCell):
	var result = ChunkCoordinateUtils.grid_to_chunk_and_local(world_tile_pos.x, world_tile_pos.y, Chunk.CHUNK_SIZE_X, Chunk.CHUNK_SIZE_Y)
	var chunk_pos: Vector2i = result.chunk
	var local_pos: Vector2i = result.local
	# 不存在这个区块,先加载生成
	if not loaded_chunks.has(chunk_pos):
		var new_chunk: Chunk = Chunk.new()
		new_chunk.world_chunk_pos = chunk_pos
		var raw_terrain:Dictionary = terrain_generator.generate_chunk(chunk_pos, Chunk.CHUNK_SIZE_X, Chunk.CHUNK_SIZE_Y)
		new_chunk.original_cache = raw_terrain
		loaded_chunks[chunk_pos] = new_chunk
	
	var chunk: Chunk = loaded_chunks[chunk_pos]
	chunk.set_cell(local_pos, new_cell)
	
## 直接破坏方块(传入瓦片网格坐标)
func try_destroy_tile_dir(world_tile_pos: Vector2i) -> void:
	var target_tile_cell: TileCell = get_tile(world_tile_pos)
	# 没有方块或者是空气,则不销毁不返回
	if target_tile_cell == null or target_tile_cell.get_cell_attribute().tile_type_id == 0:
		DebugSystem.console_common_log("世界", "该位置无方块,无需销毁 %s" % str(world_tile_pos))
		return
		
	# 第一层,破坏方块
	set_tile(world_tile_pos, TileCell.new(TileCellInstances.OriginalInstancesType.get("air")))
	DebugSystem.console_common_log("世界", "方块销毁成功 %s" % str(world_tile_pos))
	emit_signal("tile_cell_updated", world_tile_pos)
	# 第二层,生成掉落物
	var grid_distance = CoordinateAttributes.get_coordinate_distance()	
	var tile_center = Vector2(
		world_tile_pos.x * grid_distance + grid_distance/2,
		world_tile_pos.y * grid_distance + grid_distance/2
		)
	var spawn_velocity = Vector2(0, -80)
	# 获取挖掘方块对应的物品实例
	var drop_item: Item = MappingTool.tile_instance_to_item(target_tile_cell.get_cell_instance())
	if drop_item == null:
		return
	# drop_manager由World持有,只有World能调用spawn
	drop_manager.spawn(entity_storage.allocate_uid(), drop_item, tile_center, world_time, spawn_velocity)
	
## 放置方块
func try_place_tile(global_tile:Vector2i, tile_cell: TileCell = TileCell.new(TileCellInstances.OriginalInstancesType.get("stone"))) -> void:
	var cell:TileCell = self.get_tile(global_tile)
	# 不能在已有固体方块上放置
	if cell != null and cell.get_cell_attribute().is_solid:
		DebugSystem.console_common_log("世界", "该位置已有方块,无法放置 %s" % str(global_tile))
		return	
	# 放置方块
	if (!tile_cell.get_cell_attribute().is_solid):
		DebugSystem.console_error_log("世界", "此类方块无法放置: 非固体")
		return 
		
	self.set_tile(global_tile, tile_cell)
	# 放置成功,发出更新信号(复用原来的tile_cell_updated)
	DebugSystem.console_common_log("World", "方块放置成功 %s" % global_tile)
	emit_signal("tile_cell_updated", global_tile)

## 区块卸载:远离玩家的时候调用,释放内存
func unload_chunk(chunk_pos:Vector2i):
	if loaded_chunks.has(chunk_pos):
		var chunk:Chunk = loaded_chunks[chunk_pos]
		chunk.clear_original_cache() #清空原生地形缓存,释放TileCell
		loaded_chunks.erase(chunk_pos)
		
## 批量获取区块内全部方块(给RenderBridge渲染TileMap用)
func get_chunk_all_tiles(chunk_pos:Vector2i) -> Dictionary:
	if not loaded_chunks.has(chunk_pos):
		var new_chunk: Chunk = Chunk.new()
		new_chunk.world_chunk_pos = chunk_pos
		var raw_terrain:Dictionary = terrain_generator.generate_chunk(chunk_pos, Chunk.CHUNK_SIZE_X, Chunk.CHUNK_SIZE_Y)
		new_chunk.original_cache = raw_terrain
		loaded_chunks[chunk_pos] = new_chunk
	var chunk:Chunk = loaded_chunks[chunk_pos]
	# 返回合并后的方块字典(modified覆盖original)
	var output:Dictionary = {}
	for local_x in range(Chunk.CHUNK_SIZE_X):
		for local_y in range(Chunk.CHUNK_SIZE_Y):
			var lp = Vector2i(local_x, local_y)
			output[lp] = chunk.get_cell(lp)
	return output
	
## 统一清理超出距离的区块数据,多人位置数组
func prune_out_of_range_chunks(player_chunk_pos_list:Array[Vector2i]):
	var to_remove:Array[Vector2i] = []
	# 遍历所有已经加载在内存的逻辑区块
	for loaded_chunk_pos in loaded_chunks:
		var keep_chunk:bool = false
		# 判断:只要距离任意一名玩家在卸载半径内 → 保留区块
		for p_chunk in player_chunk_pos_list:
			var dist:int = max(abs(loaded_chunk_pos.x - p_chunk.x), abs(loaded_chunk_pos.y - p_chunk.y))
			if dist <= data_unload_radius:
				keep_chunk = true
				break
		# 所有玩家都离这个区块很远 → 标记卸载
		if not keep_chunk:
			to_remove.append(loaded_chunk_pos)
	
	# 执行卸载
	for pos in to_remove:
		unload_chunk(pos)

## 外部信号: 更新掉落物信息
func receive_drop_updated_info(in_uid: int, in_new_position: Vector2, in_linear_velocity: Vector2) -> void:
	# DebugSystem.console_common_log("世界", "收到掉落物信息: %s %s %s" % [in_uid, in_new_position, in_linear_velocity])
	drop_info_updated.emit(in_uid, in_new_position, in_linear_velocity)

## 内部信号连接
func _connect_inner_signal() -> void:
	# 注入回调:DropManager内部事件,调用World的信号发射
	drop_manager.on_drop_added.connect(_drop_added_connect)
	drop_manager.on_drop_removed.connect(_drop_removed_connect)
	drop_manager.on_drop_apply_force.connect(_drop_apply_force_connect)
	drop_manager.on_entity_add.connect(_add_drop_entity_connect)
	drop_manager.on_entity_destroy.connect(_despawn_entity_connect)
	
## World内部信号转发以及通讯
## 掉落物信号添加连接
func _drop_added_connect(new_drop:DropItemData) -> void:
	drop_added.emit(new_drop)
## 掉落物信号移除连接
func _drop_removed_connect(uid:int, item: Item, remove_reason: DropManager.DropDestroyReason) -> void:
	drop_removed.emit(uid, item, remove_reason)
## 掉落物添加力的连接
func _drop_apply_force_connect(uid:int, absorb_force: Vector2) -> void:
	drop_apply_force.emit(uid, absorb_force)
## 掉落物实体添加,由内置掉落物管理器请求
func _add_drop_entity_connect(new_drop: DropItemData) -> void:
	add_new_entity(new_drop)
## 掉落物
func _despawn_entity_connect(uid:int, type:EntityBase.EntityType) -> void:
	despawn_entity(uid, type)

3. 逻辑层详细运行流程实例

阅读完以上详细类的设计,这里简单以挖一块石头作为例子,详细解释一下本架构逻辑层与实体/物品层的运行流程,先给出日志图

上述执行流程的详细解释如下

**1. 输入控制系统:**玩家点击具体位置方块,发送 InputController.block_destroy_request(global_tile_pos:Vector2i) 信号

2. 经过DI核心容器中转: player_input_control.block_destroy_request.connect(world.try_destroy_tile_dir)

**3. World世界类:**调用 try_destroy_tile_dir(world_tile_pos: Vector2i)

**4. DropManager类:**调用 spawn(new_uid: int, item:Item, spawn_pos:Vector2, world_time:float, spawn_vel:Vector2=Vector2.ZERO),然后发送 on_entity_add.emit(new_drop) 绑定实体管理器,on_drop_added.emit(new_drop)绑定给世界

5. EntityStorage 与 World类: World 将 on_entity_add 转发为 add_new_entity 调用,EntityStorage 新增实体,EntityStorage 发送 entity_spawned(uid:int, entity:EntityBase) 信号,World类转发DropManager的掉落物生成信号到外部(World.drop_added(new_drop:DropItemData)信号)

**6. 后续渲染桥等:**收到信号生成实物并绑定同步

而对于背包逻辑的执行流程,靠近后吸取,流程图如下

这里的背包添加流程在DropManager的tick循环里面,玩家的实体表由World世界类获取后作为参数传入掉落物管理器。流程如下

  1. World类从EntityStorage中获取玩家映射表,传入掉落物管理器

  2. 掉落物管理器遍历寻找掉落物离得最近的玩家,并发送施加力的信号

on_drop_apply_force(uid: int, absorb_force: Vector2)信号

  1. 玩家到拾取范围,掉落物管理器调用当前最近玩家的 try_pickup_item(item:Item),传入DropItemData持有的Item。

  2. 玩家的 try_pickup_item 先调用快捷栏的 try_add_item(new_item: Item) 再调用背包的try_add_item(new_item: Item)

  3. 由于背包和快捷栏都是继承于 InventoryBase,所以都会尝试拾取物品,先找相同再找空槽,最后拾取

这就是逻辑层运行一次的流程,其他的流程还包括区块懒加载流程,区块卸载流程等,这里不做演示。

4. 结尾与补充

当前的逻辑层中对于空气的判断直接使用 tile_type_id == 0 这一类判断,没有使用注册表的TileCellAttributes 中注册的 "AIR" 属性,是早期开发测试留下未改的局限,可以手动改成获取数据层的注册表然后更改判断。后续还有桥接层和UI层,对于这两层的介绍将默认读者已经了解过前三层: 数据层,逻辑层,实体/物品数据层,这里跳回主架构:

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

相关推荐
涛声依旧-底层原理研究所1 小时前
Agent 沙箱实现原理:拆解 DeepSeek Harness 的三层沙箱架构与三平台后端
人工智能·架构
tianyuanwo1 小时前
Prometheus 服务部署指南:从架构原理到生产级落地
架构·prometheus
尤水就下2 小时前
🔥说一说在线文档的 Block 架构是如何实现的
前端·javascript·架构
代码方舟2 小时前
零信任架构实战:基于天远企业二要素验证构建自动化小微快贷法人资质核验网关
运维·人工智能·架构·自动化
true...2 小时前
虚幻还原默认视角
游戏引擎·虚幻
代码山河2 小时前
调试技巧:IDEA Debug模式入门与使用
java·学习·架构·教程·面向对象·项目
2601_968708033 小时前
DreamsChat Web – Chat, Audio, Video Web APP with Admin Dashboard
前端·架构
ZhengEnCi3 小时前
把 AI 接口稳定接进业务:超时重试、密钥隔离与成本熔断实战
安全·架构·ai编程
淡海水3 小时前
07-04-源码-.NET Core GC清扫与压缩
unity·架构·c#·.netcore·gc·1024程序员节