一、踩坑前的准备:先搞懂什么是Godot里的自定义Resource
先明确我们全程用的技术栈是Godot 4.x + GDScript,所有示例都基于这个环境写,不会乱换工具。
很多刚接触Godot的开发者,第一次看到Resource这个东西会懵:这玩意儿到底是啥?其实不用想复杂,你可以把它理解成「可以单独存、单独用、还能重复用的‘数据模板’」。比如你做一个RPG游戏,里面的「普通长剑」属性(攻击力10、耐久50、售价200),你不想每次用到长剑都重新写一遍这些数据,就可以把这些数据做成一个Resource,需要的时候直接拿过来用就行。
那自定义Resource是什么?就是你自己做的「专属数据模板」。Godot自带的Resource(比如图片、音频)是给通用数据用的,如果你要存游戏里的装备、技能、角色属性这些自定义数据,就得自己做一个自定义Resource。
先给个最基础的自定义Resource示例,帮你建立直观认知:
# 技术栈:Godot 4.x + GDScript
# 自定义装备Resource,用来存武器的基础属性
extends Resource
class_name EquipmentResource # 给这个Resource起个全局名字,方便到处调用
# 定义武器的属性
@export var name: String # 武器名称
@export var attack: int # 攻击力
@export var durability: int # 耐久度
@export var price: int # 售价
写完这个脚本后,在Godot的文件管理器里右键新建,就能看到「EquipmentResource」这个选项,新建一个之后,就能在Inspector面板里给它填具体的数值,比如填成「普通长剑」的属性,这个文件就是一个可以复用的「长剑模板」。
二、Resource复用的正确姿势:从模板到实例
自定义Resource最大的优势就是复用,不然你做它干嘛?但很多人刚开始用的时候,会搞混「模板本身」和「模板实例」的区别,踩很多坑。
2.1 什么是Resource复用?
复用的意思是:同一个自定义Resource模板,可以被多个对象引用。比如你有10个NPC,每个NPC都拿「普通长剑」,你不用给每个NPC单独做一个长剑的属性,只要让这10个NPC都引用同一个「普通长剑」的Resource就行。
举个实际的例子,假设你做一个商店系统,商店里卖的商品都是装备,每个商品都需要显示装备的属性,还需要在购买的时候生成装备实例:
# 技术栈:Godot 4.x + GDScript
# 商店商品的脚本,每个商品都对应一个装备Resource
extends Node
@export var item: EquipmentResource # 引用具体的装备模板
# 显示商品的名称和价格
func _ready() -> void:
print("商品名称:", item.name)
print("商品价格:", item.price)
# 购买装备时,生成装备的实例
func buy_item() -> EquipmentResource:
# 错误写法:直接返回模板本身,会导致所有买这个装备的角色共用同一个模板
# return item
# 正确写法:用duplicate()方法复制模板,生成独立的实例
return item.duplicate()
这里的关键是duplicate()方法,Godot里的Resource默认是「值类型」吗?不是,它是「引用类型」。也就是说,如果你直接把模板赋值给某个变量,这个变量和模板本身是指向同一个内存地址的,你改其中一个,另一个也会跟着变。
比如你做一个「装备升级」的功能,升级后攻击力会加10,如果多个角色共用同一个模板,那一个角色升级后,所有拿这个装备的角色攻击力都会变,这肯定不对。所以必须用duplicate()复制出独立的实例,每个角色的装备都是自己的,互不影响。
2.2 复用的好处和注意事项
好处很明显:一是节省内存,多个对象引用同一个模板,只会存一份模板的数据;二是改起来方便,如果你要调整「普通长剑」的基础攻击力,只要改模板本身,所有引用这个模板的地方都会自动更新;三是开发效率高,不用重复写重复的代码。
但要注意两个坑:第一,绝对不要在模板上直接改数据,模板是给大家共用的,改模板会影响所有引用它的对象;第二,需要独立数据的时候一定要用duplicate(),别图省事直接引用模板。
三、sub_resource的坑点:看不见的复制陷阱
很多人用自定义Resource的时候,会用到sub_resource(子资源),这玩意儿的坑特别多,很多人踩了都不知道为什么。
3.1 什么是sub_resource?
sub_resource是Resource的一种特殊形式,它不是单独存成一个文件的,而是「嵌套在某个Resource里面的」。比如你做一个「套装」的Resource,套装里包含多个装备,这些装备不需要单独存成文件,而是直接嵌套在套装里,这时候这些装备就是套装的sub_resource。
举个例子,做一个「勇者套装」的Resource,里面包含头盔、胸甲、武器三个装备:
# 技术栈:Godot 4.x + GDScript
# 自定义套装Resource
extends Resource
class_name SetResource
@export var set_name: String # 套装名称
@export var helmet: EquipmentResource # 套装里的头盔,是sub_resource
@export var chest_armor: EquipmentResource # 套装里的胸甲,是sub_resource
@export var weapon: EquipmentResource # 套装里的武器,是sub_resource
新建一个SetResource文件后,在Inspector面板里,你可以直接给helmet、chest_armor、weapon这三个属性创建EquipmentResource,这些EquipmentResource不会单独存成文件,而是直接存在SetResource文件里面,这就是sub_resource。
3.2 sub_resource的坑点1:复制时的深拷贝问题
sub_resource最大的坑,就是用duplicate()复制包含sub_resource的Resource时,默认是「浅拷贝」还是「深拷贝」?答案是浅拷贝。
什么是浅拷贝?就是只复制父对象本身,里面的sub_resource还是原来的引用。比如你复制一个「勇者套装」的实例,套装里的头盔、胸甲、武器这些sub_resource,还是和原来的套装共用同一个模板,如果你改其中一个套装里的头盔攻击力,另一个套装里的头盔也会跟着变。
举个实际的例子,假设你做一个「套装升级」的功能,升级后套装里的武器攻击力加20:
# 技术栈:Godot 4.x + GDScript
# 玩家的脚本
extends Node
@export var player_set: SetResource # 玩家拥有的套装
func upgrade_set() -> void:
# 先复制套装模板,生成玩家自己的套装实例
var set_instance = player_set.duplicate()
# 给套装里的武器攻击力加20
set_instance.weapon.attack += 20
# 打印玩家套装的武器攻击力
print("玩家套装武器攻击力:", set_instance.weapon.attack) # 输出会比原来多20
# 现在看原来的套装模板的武器攻击力,会不会变?
print("原套装模板武器攻击力:", player_set.weapon.attack) # 输出也会比原来多20!
你会发现,改了玩家套装里的武器攻击力,原套装模板的武器攻击力也跟着变了,这就是浅拷贝的坑。因为duplicate()默认只复制SetResource本身,里面的weapon、helmet这些sub_resource还是原来的引用,相当于玩家的套装和原套装模板的sub_resource是同一个东西,改一个就会影响另一个。
那怎么解决?很简单,duplicate()方法有一个参数叫deep,把它设为true就是深拷贝,会把所有嵌套的sub_resource也复制一遍,生成独立的实例:
# 正确的复制方式,深拷贝所有嵌套的sub_resource
var set_instance = player_set.duplicate(deep = true)
改了这个之后,再测试,你会发现改玩家套装里的武器攻击力,原套装模板的不会变了,问题解决。
3.3 sub_resource的坑点2:sub_resource本身不能复用
sub_resource是嵌套在父Resource里的,所以它不能被其他Resource引用。比如你有一个「勇者套装」里的头盔,你不能让另一个「法师套装」也引用这个头盔,因为这个头盔是「勇者套装」的sub_resource,是私有的,不能单独拿出来用。
如果你需要多个套装共用同一个头盔,那这个头盔就不能做成sub_resource,必须单独存成一个EquipmentResource文件,让多个套装都引用这个单独的文件。
举个反例,错误的做法:
# 技术栈:Godot 4.x + GDScript
# 勇者套装的Resource,头盔是sub_resource
extends Resource
class_name WarriorSetResource
@export var helmet: EquipmentResource # 头盔是sub_resource
# 法师套装的Resource,想引用勇者套装里的头盔(错误!)
extends Resource
class_name MageSetResource
@export var helmet: EquipmentResource = preload("res://warrior_set.tres/helmet") # 会报错,不能直接引用sub_resource
正确的做法是把头盔单独存成一个文件:
# 技术栈:Godot 4.x + GDScript
# 勇者套装的Resource,头盔引用单独的头盔文件
extends Resource
class_name WarriorSetResource
@export var helmet: EquipmentResource = preload("res://common_helmet.tres") # 引用单独的头盔文件
# 法师套装的Resource,也引用同一个单独的头盔文件
extends Resource
class_name MageSetResource
@export var helmet: EquipmentResource = preload("res://common_helmet.tres") # 可以复用
四、项目实践中的坑点总结和最佳实践
4.1 常见坑点汇总
- 直接修改Resource模板:很多人测试功能的时候,会直接改模板里的数据,导致所有引用这个模板的地方都出问题,比如攻击力突然变高、耐久度变低。
- 复制Resource时忘记用
deep = true:尤其是包含sub_resource的Resource,浅拷贝会导致嵌套的数据被多个实例共用,改一个影响所有。 - 把需要复用的数据做成sub_resource:sub_resource是私有的,不能被其他地方引用,导致重复写重复的数据。
- 混淆Resource模板和实例:很多人以为
duplicate()是生成一个新的模板,其实是生成模板的实例,模板本身是不变的。
4.2 最佳实践
- 模板和实例严格分开:模板只用来存基础数据,绝对不要改模板,所有用到模板的地方,需要独立数据的时候都用
duplicate(deep = true)生成实例。 - 什么时候用sub_resource?只有当这个数据只属于某个父Resource,不需要被其他地方复用的时候,才用sub_resource,比如套装里的专属装备,其他地方不会用到,就可以做成sub_resource。
- 统一复制方法:写一个全局的工具函数,用来复制自定义Resource,避免每次都写
duplicate(deep = true),比如:
# 技术栈:Godot 4.x + GDScript
# 全局工具函数,用来复制自定义Resource
extends Node
class_name ResourceUtils
# 复制任意自定义Resource,默认深拷贝
static func duplicate_resource(resource: Resource) -> Resource:
if resource == null:
return null
return resource.duplicate(deep = true)
用的时候直接调用ResourceUtils.duplicate_resource(item)就行,不会忘加deep = true。
4. 模板文件单独存一个文件夹:把所有自定义Resource模板都放在一个叫「resources」的文件夹里,方便管理,改模板的时候也不会找不到。
五、应用场景、优缺点和注意事项
5.1 应用场景
自定义Resource设计模式特别适合数据驱动的游戏开发,比如:
- RPG游戏的装备、技能、角色属性、套装、任务、关卡数据;
- 模拟经营游戏的建筑、道具、商品、角色属性;
- 动作游戏的武器、招式、关卡配置;
- 所有需要大量重复使用、需要批量修改的数据。
5.2 技术优缺点
优点:
- 开发效率高:不用重复写重复的数据,改模板就能批量更新;
- 内存占用低:多个对象引用同一个模板,只存一份数据;
- 维护方便:所有数据都集中在模板里,改的时候很方便;
- 可扩展性强:可以给自定义Resource加各种方法,比如装备的「升级方法」、技能的「释放方法」,让数据更灵活。
缺点:
- 学习成本:刚开始用的时候容易搞混模板和实例、浅拷贝和深拷贝,踩很多坑;
- 性能问题:如果有大量的自定义Resource实例,内存占用会比纯数据类高,不过一般游戏里的数量级都不会有问题;
- 调试麻烦:如果模板改了,所有引用的地方都会变,调试的时候很难找到问题出在哪。
5.3 注意事项
- 绝对不要在模板上直接改数据,模板是给大家共用的;
- 复制包含sub_resource的Resource时,一定要用
duplicate(deep = true); - 需要复用的数据不要做成sub_resource,要单独存成文件;
- 给自定义Resource加注释,说明每个属性的作用,方便后续维护;
- 定期清理不用的模板,避免文件太多太乱。
六、文章总结
自定义Resource设计模式是Godot里做数据驱动开发的核心技术,用好了能大幅提高开发效率,节省内存,但很多人因为不了解模板和实例的区别、sub_resource的坑点,踩了很多不必要的坑。
核心的逻辑其实很简单:模板是用来复用的,绝对不能改;实例是每个对象自己的,改实例不会影响模板;sub_resource是嵌套在父Resource里的,私有的,不能复用;复制包含sub_resource的Resource时一定要深拷贝。
只要记住这些点,再结合实际项目多练几次,就能熟练掌握这个技术,再也不会踩这些坑了。
评论
围绕“自定义Resource设计模式:GDScript数据驱动开发中的资源复用与sub_resource实例化坑点记录,从项目实践中总结。”参与讨论