Godot-GridGame架构数据层篇

前言

本文章是 针对Godot-GridGame(网格游戏)架构设计的研究(全程开源)-CSDN博客中数据层的详细介绍,阅读本文章前请先阅读该文章的总体架构设计。本文将会使用已经构建完成的项目代码(整个)用于帮助介绍本框架数据层的设计。

1. 数据层总览

1. 数据层设计思想

数据层在本框架中最先被加载,为后续所有操作提供源数据支持。由于设计的是网格游戏,所以数据层面需要保存 物品数据,网格方块数据 等数据,本文将介绍物品与方块的数据层设计,同时为了方便后续注册新物品或者方块,同时给物品和方块提供一些可以自己添加的自定义数据,所以方块 / 物品被拆为三层:属性(Attribute) → 实例(Instance) →运行时实体(Item/TileCell),数据层只承载前两层,因此本架构中数据层最主要的数据表类为:

ItemAttributes (物品属性表类)、ItemInstances (物品实例表类)、TileCellAttributes (瓦片属性表类)、TileCellInstances(瓦片实例表类)

表中存储的是当前表类对应的数据类,后续需要的所有数据均从已经加载好的表中取用。

为了方便数据层随时可以扩展物品或者瓦片方块,所有数据均是启动时从 JSON 文件中加载属性并现场创建。

2. 基于数据层思想设计的本项目实物

在详细介绍数据层的设计之前,先给出本架构数据层完整的结构示意图如下
完整数据层概览图

当前已经完成的项目中,数据层的源数据类主要设计了ItemAttribute类,ItemInstance类,TileCellAttribute类,TileCellInstance类,这些是数据表类存储的主要数据,其中 Instance 类会包含 Attribute 类,随后 Instance 又会被拿去包给上层真正的在游戏中运行的实体。

对于上层 Instance 包装了下层的 Attribute ,上层的 JSON 的文件中也应该出现对于的 Attribute 的名字,同时加载顺序必须是先加载下层再加载上层 ,因为上层 Instance 在 Attributes属性表 构建完成后会直接拿取数据类成品用来构造

演示中详细的设计规范和接口说明将会放在详细设计说明的最后解释

2. 详细设计说明

1. 单个数据类: Attribute 类设计

属性类是实例的属性,不是真正的实例,可以给多个实例共用,这样子可以防止大量注册实例的时候每个实例都要初始化一遍属性,同时减少了实例的数据负担,看着更清晰

1. 物品属性类

单个物品数据属性类应该包含当前物品的类型 (瓦片方块,盔甲,工具等),物品属性ID ,物品最大堆叠数量等通用数据。需要注意的是,物品名字不被包含在属性中(除了映射表中要用大写名字对应,好找一点),这里给出 ItemAttribute 的设计:

复制代码
## 纯计数类,逻辑类,干净脚本
extends RefCounted
## 物品属性类
class_name ItemAttribute
## 物品自动分配物品ID,没有给定手动ID则自动从最大开始增长分配
static var current_num: int = 0
## 物品类型枚举 BLOCK: 方块物品 TOOL: 工具
enum ItemType {
	BLOCK = 0,
	TOOL = 1,
}
## 默认堆叠数量
const DEFAULT_STACK: int = 64

## 物品属性,物品属性ID,最大堆叠数量,类型
var item_id: int
var max_stack: int = DEFAULT_STACK
var type: ItemType = ItemType.BLOCK

## 初始化父类模板,后续盔甲,工具类也有父类基础数据的初始化
func _init(new_id: int, new_stack: int = DEFAULT_STACK) -> void:
	if new_id >= 0:
		# 使用数据表中显式指定的ID,保证重复加载时ID稳定
		item_id = new_id
		if current_num <= new_id:
			current_num = new_id + 1
	else:
		# 未指定ID时自动增长
		item_id = current_num
		current_num = current_num + 1
	
	max_stack = new_stack

## 获得最大堆叠数量
func get_max_stack_num() -> int:
	return max_stack
## 重置注册表ID
static func reset_counter():
	current_num = 0

2. 瓦片方块属性类

单个瓦片方块属性类应该包含方块的ID ,方块当前血量 ,方块的状态(固体,液体 ),照明强度等,同时注意方块名字同样不应该被属性记录,这里给出方块属性类的设计

复制代码
extends RefCounted
## 方块属性类,用于注册方块
class_name TileCellAttribute
## 方块自增ID,同样用于未指定ID时从最大ID开始分配
static var current_num: int = 0

## 基础数据初始化,分配ID,当然这里也可以使用枚举类型定义方块类型然后作为父类初始化
func _init(new_hp: int = -1, new_solid_state:bool = true, new_light: int = 0, new_id: int = -1) -> void:
	if new_id >= 0:
		# 使用数据表中显式指定的ID,保证重复加载时ID稳定
		tile_type_id = new_id
		if current_num <= new_id:
			current_num = new_id + 1
	else:
		# 未指定ID时自动增长
		tile_type_id = current_num
		current_num = current_num + 1
	# 填入原版状态
	hp = new_hp
	is_solid = new_solid_state
	light_emission = new_light

## 重置注册表ID
static func reset_counter():
	current_num = 0

## 这里是方块的基本属性
## id : 0 默认空气
## hp : -1 不可破坏(特殊值)
## solid: 方块类型
## light_emission 照明强度
var tile_type_id: int = 0 
var hp: int = -1
var is_solid: bool = false
var light_emission: int = 0

2. 单个数据类: Instance 类设计

实例类是游戏中真正被注册的物品,但不是被使用的物品,实例类是作为标识器告诉当前的实例具体是什么实例。

1. 物品实例类

物品实例类需要包含当前物品的属性,以及辨识当前独特的物品的名字,方便游戏识别,在这一层可以尝试单独继承物品实例并添加需要的数据,以下是物品实例类的设计和解释

复制代码
extends RefCounted
## 物品实体父类
class_name ItemInstance
## 需要包含物品属性
var attribute: ItemAttribute
## 当前注册的实例的名字(标识符)
var name: String

## 初始化物品实体
func _init(new_name: String, new_attribute: ItemAttribute) -> void:
	name = new_name
	attribute = new_attribute
## 更改物品属性
func change_item_attribute(newAttribute: ItemAttribute) -> void:
	attribute = newAttribute
## 获得物品属性
func get_item_attribute() -> ItemAttribute:
	return attribute
## 获得物品名字
func get_item_name() -> String:
	return name
## 克隆方法,返回克隆属性物品
func copy() -> ItemInstance:
	var new_cell = ItemInstance.new(name, attribute)
	return new_cell

2. 瓦片方块实例类

同理瓦片方块实例类也要包含瓦片方块的属性,同时具备当前注册瓦片的名字方便游戏识别,以下是瓦片实例类的设计与解释

复制代码
extends RefCounted

## 瓦片实例,包含名字,以及属性
class_name TileCellInstance
## 瓦片实例名字(标识符)
var name: String = ""
## drop_item 类型为 String,通过查找工具(MappingTool)直接查找对应物品实例表内的实例
var drop_item = null
## 瓦片方块属性
var attribute: TileCellAttribute

## 初始化完整方块
func _init(new_name: String, newAttribute: TileCellAttribute, new_drop_item) -> void:
	name = new_name
	attribute = newAttribute
	drop_item = new_drop_item

## 更改瓦片属性
func change_cell_attribute(newAttribute: TileCellAttribute) -> void:
	attribute = newAttribute
## 获得瓦片属性
func get_cell_attribute() -> TileCellAttribute:
	return attribute
## 获得瓦片掉落物映射
func get_tile_drop_item() -> String:
	return drop_item
## 克隆方法,返回克隆属性方块
func copy() -> TileCellInstance:
	var new_cell = TileCellInstance.new(name, attribute, drop_item)
	return new_cell

3. 注册表类设计

数据表类是游戏加载数据的关键类,负责加载JSON中指定的数据与指定路径下的数据。

本架构数据表的加载流程为:

读取属性数据表并解析 ==>> 校验并遍历创建单个数据并填入映射表 ==>> 读取实例数据表并解析 ==>> 校验并遍历创建单个实例并填入数据表

可以看出,数据表类分为属性数据表类 和实例数据表类,在本演示中加载和存储在同一类实现

在本架构中注册表类所有属性和方法均为静态方法,这样方便全局获取游戏的统一数据,同时数据表类加载的路径由 ResourcePath路径类 统一提供。

以下是 ResourcePath 路径类的内容:

复制代码
extends RefCounted
## 全局资源路径
class_name ResourcePath

## 属性映射路径
static var attributes_path: String = "res://Resources/tileCells/attributes/tileCellAttributes.json"
static var item_attributes_path: String = "res://Resources/Items/attrubutes/itemsAttributes.json"
## 实例映射路径
static var tile_cell_instances_path: String = "res://Resources/tileCells/tileCellInstances/tileCellInstances.json"
static var item_instances_path: String = "res://Resources/Items/ItemInstances/ItemInstances.json"
## 材质路径
static var tile_cell_texture_path: String = "res://Resources/texture/tileCells/"
static var item_texture_path: String = "res://Resources/texture/items/"
static var drop_item_texture_path: String = "res://Resources/texture/dropItems/"
static var ui_texture_path: String = "res://Resources/texture/uiTexture/"

## 常量池,对应拥有的路径的子路径
static var UI_TEXTURE_SUBPATH: Dictionary = {
	"InventorySlotBackGround": "inventory/backGround/"
}

1. 属性注册表类

1. 物品属性注册表类

物品属性注册表类负责加载 物品属性JSON 中的数据,并遍历创建上文的单个物品属性类并存储在物品属性表格中,方便后续物品实例注册表等其他需求方取用。具体设计如下

复制代码
extends RefCounted
## 存原版所有方块物品属性模板
class_name ItemAttributes
## 映射表 name(String类型) -> ItemAttribute
static var OriginalAttrType: Dictionary = {}
## 加载映射表
static func load_mapping_attributes() -> bool:
	# 从指定路径打开映射表,在ResourcePath里面新增item_attributes_path
	var file_path: String = ResourcePath.item_attributes_path
	var mapping_file = FileAccess.open(file_path, FileAccess.READ)
	if mapping_file == null:
		DebugSystem.console_error_log("物品属性映射表", "映射文件打不开,路径:%s" % file_path)
		return false
	
	DebugSystem.console_common_log("物品属性映射表", "打开映射文件,路径:%s" % file_path)
	# 读取指定数据,即转换读取的文件为 String 形式
	var text = mapping_file.get_as_text()
	mapping_file.close()
	# 将 String 数据类型的 JSON 文件解析成数组方便后续处理
	var parse_result = JSON.parse_string(text)
	if parse_result == null:
		DebugSystem.console_error_log("物品属性映射表", "JSON解析错误,文本格式非法")
		return false
		
	# 获取格式数据并转换,这里调用了下文的转换函数,注意这里是直接返回转换完整的映射表
	var input_dict: Dictionary = convert_item_array_to_dict(parse_result)
	DebugSystem.console_common_log("物品属性映射表", "获得转换映射: %s" % input_dict)
	# 将本地映射表替换
	OriginalAttrType = input_dict.duplicate()
	return true
	
## 【独立转换函数】输入物品属性定义数组,输出 name -> ItemAttribute 字典
static func convert_item_array_to_dict(input_array:Array) -> Dictionary:
	var result_dict: Dictionary = {}
	# 遍历JSON转化过来的数组
	for item_def in input_array:
		if not item_def is Dictionary:
			DebugSystem.console_warn_log("物品属性表", "跳过无效物品条目,不是字典类型")
			continue
		
		# 注册表查询用的name(字符串key,和方块属性表保持一致)
		var attr_name:String = item_def.get("name", "")
		if attr_name.is_empty():
			DebugSystem.console_warn_log("物品属性表", "物品name为空,跳过")
			continue
		
		# 读取持久int id,无则传-1自动分配
		var persistent_id:int = item_def.get("id", -1)
		var max_stack:int = item_def.get("max_stack", 64)
		var raw_type:String = item_def.get("item_type", "BLOCK")
		
		var attr:ItemAttribute = null
		# 这里按照JSON中对应的物品类型转化
		match raw_type:
			"BLOCK":
				attr = BlockItemAttribute.new(persistent_id, max_stack)
				attr.type = ItemAttribute.ItemType.BLOCK
			# 后续TOOL在这里扩展
			# "TOOL":
			# 	attr = ToolItemAttribute.new(persistent_id, max_stack)
			# 	attr.type = ItemAttribute.ItemType.TOOL
			_:
				DebugSystem.console_warn_log("物品属性表", "未知物品类型 %s,跳过" % raw_type)
				continue
		# 按照名字和属性映射进结果
		result_dict[attr_name] = attr
	return result_dict
2. 瓦片方块属性注册表类

瓦片方块属性注册表类的功能与上文物品属性注册表的功能一致,不过当前的方块没有继承,所以直接跳过了物品属性注册表的 match 创建不同属性的片段,以下是瓦片方块属性注册表

复制代码
extends RefCounted
## 存原版所有方块属性
class_name TileCellAttributes
## 映射表 name -> Attribute
static var OriginalBlockType: Dictionary = {}

## 加载映射表
static func load_mapping_attributes() -> bool:
	# 从指定路径打开映射表
	var file_path: String = ResourcePath.attributes_path
	var mapping_file = FileAccess.open(file_path, FileAccess.READ)
	if mapping_file == null:
		DebugSystem.console_error_log("瓦片映射表", "映射文件打不开,路径:%s" % file_path)
		return false
	
	DebugSystem.console_common_log("瓦片映射表", "打开映射文件,路径:%s" % file_path)
	# 读取指定数据,获得 String 类型的 JSON
	var text = mapping_file.get_as_text()
	mapping_file.close()
	var parse_result = JSON.parse_string(text)
	if parse_result == null:
		DebugSystem.console_error_log("瓦片映射表", "JSON解析错误,文本格式非法")
		return false
		
	# 获取格式数据并转换, 同样调用下文的转换函数,直接返回整个映射表
	var input_dict: Dictionary = convert_block_array_to_dict(parse_result)
	DebugSystem.console_common_log("瓦片映射表", "获得转换映射: %s" % input_dict)
	OriginalBlockType = input_dict.duplicate()
	return true
	
## 【独立转换函数】输入方块定义数组,输出 name -> TileCellAttribute 字典
static func convert_block_array_to_dict(input_array:Array) -> Dictionary:
	var result_dict: Dictionary = {}
	
	for block_def in input_array:
		if not block_def is Dictionary:
			DebugSystem.console_warn_log("方块属性表", "跳过无效方块条目,不是字典类型")
			continue
		
		# 查找属性注册名字
		var block_name:String = block_def.get("name", "")
		if block_name.is_empty():
			DebugSystem.console_warn_log("方块属性表", "方块name为空,跳过")
			continue
		
		# 直接创建瓦片方块属性,不分类
		var attr = TileCellAttribute.new(
			block_def.get("hp", -1),
			block_def.get("is_solid", false),
			block_def.get("light_emission", 0),
			block_def.get("id", -1),
		)
		# 同理按照JSON中的名字映射对应属性
		result_dict[block_name] = attr
	return result_dict

2. 实例注册表类

1. 物品实例注册表类

物品实例注册表负责在物品属性注册表完成属性注册后,将对应的物品属性和实例独特数据载入实例注册表,实例对应的属性同样写在JSON中,直接对应属性注册表中的名字映射的属性,以下是物品实例注册表类的设计

复制代码
extends RefCounted
## 存 ItemInstance 的映射表
class_name ItemInstances
## 映射 name(String) -> ItemInstance(BlockItemInstance / ToolItemInstance)
static var OriginalInstancesType: Dictionary = {}
## 加载映射表
static func load_mapping_instance() -> bool:
	# 从指定路径打开映射表
	var file_path: String = ResourcePath.item_instances_path
	var mapping_file = FileAccess.open(file_path, FileAccess.READ)
	if mapping_file == null:
		DebugSystem.console_error_log("物品实例映射表", "映射文件打不开,路径:%s" % file_path)
		return false
	
	DebugSystem.console_common_log("物品实例映射表", "打开映射文件,路径:%s" % file_path)
	# 读取指定数据
	var text = mapping_file.get_as_text()
	mapping_file.close()
	var parse_result = JSON.parse_string(text)
	if parse_result == null:
		DebugSystem.console_error_log("物品实例映射表", "JSON解析错误,文本格式非法")
		return false
		
	# 获取格式数据并转换
	var input_dict: Dictionary = convert_item_array_to_dict(parse_result)
	DebugSystem.console_common_log("物品实例映射表", "获得转换映射: %s" % input_dict)
	OriginalInstancesType = input_dict.duplicate()
	return true
	
## 【独立转换函数】转换对应名字属性,载入物品实例,输出 name -> ItemInstance 字典
static func convert_item_array_to_dict(input_array:Array) -> Dictionary:
	var result_dict: Dictionary = {}
	
	for item_def in input_array:
		if not item_def is Dictionary:
			DebugSystem.console_warn_log("物品实例表", "跳过无效物品条目,不是字典类型")
			continue
		
		## 查找物品实例注册名字
		var item_name:String = item_def.get("name", "")
		if item_name.is_empty():
			DebugSystem.console_warn_log("物品实例表", "物品name为空,跳过")
			continue
		
		# 检查属性模板是否在物品属性注册表存在
		var attr_ref_name:String = item_def.get("attribute", "")
		if not ItemAttributes.OriginalAttrType.has(attr_ref_name):
			DebugSystem.console_warn_log("物品实例表", "属性表中不存在属性: %s" % attr_ref_name)
			continue
		
		var attr_template = ItemAttributes.OriginalAttrType[attr_ref_name]
		var item_ins:ItemInstance = null

		match attr_template.type:
			ItemAttribute.ItemType.BLOCK:
				var block_ins = BlockItemInstance.new(item_name, attr_template)
				# 方块物品实例专属:放置对应的方块实例name
				block_ins.place_tile_name = item_def.get("place_tile_name", "")
				item_ins = block_ins
			# 后续TOOL在这里扩展
			# ItemAttribute.ItemType.TOOL:
			# 	var tool_ins = ToolItemInstance.new(item_name, attr_template)
			# 	item_ins = tool_ins
			_:
				DebugSystem.console_warn_log("物品实例表", "不支持的物品类型,name:%s" % item_name)
				continue
		
		result_dict[item_name] = item_ins
	return result_dict
2. 瓦片方块实例注册表类

瓦片方块实例注册表类与物品实例注册表类的功能一致,同时对应属性也是从JSON文件中获取直接对应瓦片方块的属性表中对应的属性,以下是瓦片方块实例注册表类的设计

复制代码
extends RefCounted
## 存 TileCellInstance 的映射表
class_name TileCellInstances
## 映射 name(String) -> TileCellInstance
static var OriginalInstancesType: Dictionary = {}
## 加载映射表
static func load_mapping_instance() -> bool:
	# 从指定路径打开映射表
	var file_path: String = ResourcePath.tile_cell_instances_path
	var mapping_file = FileAccess.open(file_path, FileAccess.READ)
	if mapping_file == null:
		DebugSystem.console_error_log("瓦片实例映射表", "映射文件打不开,路径:%s" % file_path)
		return false
	
	DebugSystem.console_common_log("瓦片实例映射表", "打开映射文件,路径:%s" % file_path)
	# 读取指定数据
	var text = mapping_file.get_as_text()
	mapping_file.close()
	var parse_result = JSON.parse_string(text)
	if parse_result == null:
		DebugSystem.console_error_log("瓦片映射表", "JSON解析错误,文本格式非法")
		return false
		
	# 获取格式数据并转换
	var input_dict: Dictionary = convert_block_array_to_dict(parse_result)
	DebugSystem.console_common_log("瓦片映射表", "获得转换映射: %s" % input_dict)
	OriginalInstancesType = input_dict.duplicate()
	return true
	
## 【独立转换函数】转换对应名字属性,载入实例方块,输出 name -> tileCellInstance 字典
static func convert_block_array_to_dict(input_array:Array) -> Dictionary:
	var result_dict: Dictionary = {}
	
	for block_def in input_array:
		if not block_def is Dictionary:
			DebugSystem.console_warn_log("方块实例表", "跳过无效方块条目,不是字典类型")
			continue
		
		# 查找属性注册名字
		var block_name:String = block_def.get("name", "")
		if block_name.is_empty():
			DebugSystem.console_warn_log("方块实例表", "方块name为空,跳过")
			continue
		# 校验,检测方块属性表中是否存在属性
		if not TileCellAttributes.OriginalBlockType.has(block_def["attribute"]):
			DebugSystem.console_warn_log("方块实例表", "属性表中不存在属性: %s" % block_def["attribute"])
			continue
		# 创建新的瓦片方块属性
		var attr = TileCellInstance.new(
			block_name,
			TileCellAttributes.OriginalBlockType[block_def["attribute"]],
			block_def.get("drop_item", "null")
		)
	
		result_dict[block_name] = attr
	return result_dict

4. JSON格式与命名规范

对于本架构中数据层加载的JSON层,对应的物品属性,瓦片方块属性等JSON需要提供游戏需要的数据。

对于物品属性和瓦片方块属性,都要提供属性ID(提供给游戏的独特ID编号) 、**名字name(提供给映射表,方便查询)**两个重要数据,比如当前游戏运行的属性JSON表如下

物品属性JSON表

复制代码
[
	{
		"name": "STONE",
		"id": 1,
		"item_type": "BLOCK",
		"max_stack": 64
	},
	{
		"name": "DIRT",
		"id": -1,
		"item_type": "BLOCK",
		"max_stack": 64
	}
]

瓦片方块属性JSON表

复制代码
[
	{
		"name": "AIR",
		"id": 0,
		"hp": 0,
		"is_solid": false,
		"light_emission": 0
	},
	{
		"name": "STONE",
		"id": 1,
		"hp": -1,
		"is_solid": true,
		"light_emission": 0
	},
	{
		"name": "DIRT",
		"id": 2,
		"hp": 1,
		"is_solid": true,
		"light_emission": 0
	},
]

对于实例JSON表,则必须名字name(供映射表用,同时也是注册的实例的游戏标识) ,**attribute(实例属性,注册的实例一定要有属性),**如下

物品实例JSON表

复制代码
[
	{
		"name": "stone",
		"attribute": "STONE",
		"place_tile_name": "stone"
	},
	{
		"name": "dirt",
		"attribute": "DIRT",
		"place_tile_name": "dirt"
	}
]

瓦片方块JSON表

复制代码
[
	{
		"name": "air",
		"attribute": "AIR",
		"drop_item": null
	},
	{
		"name": "stone",
		"attribute": "STONE",
		"drop_item": "stone"
	},
	{
		"name": "dirt",
		"attribute": "DIRT",
		"drop_item": "dirt"
	},
]

3. 补充以及结尾

以上内容是本架构数据层设计的实物,大部分解释和逻辑细节在上文代码中,如果读者是直接看文字阅读完的建议回去仔细阅读代码,到这里可以给出调试日志供读者观察刚刚的数据层构建的效果

后续层级的解释将会默认读者已经阅读完数据层的解释,这里可以跳回主文章

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

相关推荐
归-途1 小时前
C# 实现扫描件自动分栏切图
1024程序员节
java_nnnn1 小时前
Redis 核心特性与应用场景:从底层原理到分布式系统应用
数据库·redis·1024程序员节
Random Glow1 小时前
一条 SQL 从输入到返回,中间经历了什么
数据库·后端·mysql·性能优化·innodb·1024程序员节
宝绿特塑业再生科技1 小时前
板材挤出过程振动与尺寸波动:户外家具行业转向再生料后,工艺稳定性成为核心议题
科技·1024程序员节·户外家具·再生塑料·塑木桌椅·hdpe再生颗粒
comedate1 小时前
【SMLib】SMLib 五层架构深度解析:SMLib / SM_API / SM_API_USD / SmPyLib 各管什么
架构·cad·1024程序员节·openusd·smlib·nurbs
纪念 2291 小时前
C++ 算法(四)
1024程序员节
餐赢计餐饮策划设计1 小时前
如何判断国潮养生设计是否适合自己?
python·1024程序员节
lzx_0021 小时前
Linux 开发工具(六) 版本管理器git的简单使用
git·1024程序员节
讯捷蓝达1 小时前
华为 2288H V5 服务器不开机,显示 “888“ 是不是通病?
1024程序员节