今日速览

  • Godot 官方对 Resource 的定义很关键:「节点(Node)提供功能,资源(Resource)是数据容器」。所以「物品」天生就该是数据,而不是挂在场景里的节点。
  • 同一个 Resource 从磁盘只会被加载一份,再次 load() 返回的是同一份内存对象——这让「用 Resource 定义道具」天然省内存、天然可共享。
  • 背包的本质是「容器 + 规则」:堆叠上限、添加/移除、查找;界面只是它的一个视图,改界面不该动数据。
  • 存档只写「物品 id + 数量 + 槽位」这种纯数据,绝不存节点路径;这样重做关卡或改版也不会把老存档读崩。
  • 把「数据 / 容器 / 视图」拆成三层,DeepSeek 才能一次只专注一件事,生成出来的代码质量明显更稳。

核心学习内容

1. 先定数据:Item 用自定义 Resource

按官方 Resources 文档,任何「引擎要存/读的东西」都是 Resource。于是我们给物品建一个自定义资源脚本:

# item_data.gd
class_name ItemData
extends Resource

@export var id: StringName = &""          # 唯一标识,存档只认它
@export var display_name: String = ""
@export var icon: Texture2D
@export_multiline var description: String = ""
@export var max_stack: int = 1            # 1 表示不可堆叠
@export var rarity: int = 0

在编辑器里「新建资源 → ItemData」,就能像做 .tres 素材一样做出「红药水.tres / 铁剑.tres」。好处:策划填表,程序不改代码;id 稳定,存档只依赖它。

2. 再做容器:Inventory 的规则

背包不该关心「图标多大」,只关心「能不能放进去」。核心是 add_item 的返回值和堆叠逻辑:

# inventory.gd
class_name Inventory
extends RefCounted

signal changed                       # 数据变了就发信号,UI 自己刷新

const SLOT_COUNT := 24
var slots: Array[Dictionary] = []    # {item: ItemData, count: int}

func _init() -> void:
	for i in SLOT_COUNT:
		slots.append({})

# 返回「没放进去的剩余数量」,放得下就是 0
func add_item(item: ItemData, amount: int) -> int:
	var remain := amount
	# 1) 先并进已有的同类堆
	for s in slots:
		if remain <= 0: break
		if s.get("item") == item and s.count < item.max_stack:
			var space := item.max_stack - s.count
			var put := mini(space, remain)
			s.count += put
			remain -= put
	# 2) 再找空槽
	for s in slots:
		if remain <= 0: break
		if s.is_empty():
			var put := mini(item.max_stack, remain)
			s["item"] = item
			s["count"] = put
			remain -= put
	if remain != amount:
		changed.emit()
	return remain

要点:先合并、再开新槽;用 RefCounted 而非 Node(它没有位置,不需要进场景树);用 signal 让 UI 被动刷新,而不是背包去 get_node 找 UI。

3. 最后做视图:GridContainer + 格子场景

界面用 GridContainer(设 columns = 6),每个格子是一个独立场景 slot_ui.tscn(TextureRect 图标 + 右下角 Label 数量)。UI 订阅 inventory.changed,收到信号就重画一遍:

func _refresh() -> void:
	for i in slots.size():
		var ui := grid.get_child(i) as SlotUI
		var s := inventory.slots[i]
		ui.set_slot(s.get("item"), s.get("count", 0))

数据流是单向的:交互(拾取/使用)→ 改 Inventory → 发信号 → UI 刷新。永远不要让 UI 直接改数据,否则一定会出现「界面和存档对不上」。

4. 存进本地 / Steam 云:只序列化数据

按官方 Saving games 文档的思路,用 JSON 把数据串起来即可:

func to_dict() -> Array:
	var out := []
	for i in slots.size():
		var s := slots[i]
		if not s.is_empty():
			out.append({"id": String(s.item.id), "count": s.count, "slot": i})
	return out

读档时用 id 反查 ItemData(维护一个 id -> ItemData 的字典,或扫描 res://items/*.tres)。存到 user:// 后再接上 Steam Cloud(见第 39 天),存档就能跟着玩家账号走。

🎯 DeepSeek 提示词

提示词 1:生成 ItemData 自定义 Resource

适用场景:给背包系统打地基,一次性生成可扩展的道具数据结构。

你是 Godot 4 的资深 GDScript 工程师,请帮我设计一个「物品数据」的自定义 Resource。

任务:写一个 item_data.gd,class_name 为 ItemData,extends Resource。
要求:
1) 字段用 @export,至少包含:id(StringName, 唯一标识)、display_name(String)、
   icon(Texture2D)、description(String, 多行)、max_stack(int, 默认1=不可堆叠)、
   rarity(int, 0-4)。
2) 加一个 @export var value: int 表示售价值。
3) 加静态函数 create(params) 方便代码里快速造物品。
4) 每个字段写一句中文注释说明用途。
输出格式:只输出一个 gdscript 代码块,不要解释。
约束:Godot 4.x 语法;不要 use Node;不要包含任何联网代码。

提示词 2:生成 Inventory 容器类(带堆叠与信号)

适用场景:背包的核心逻辑,需要「放不下要返还剩余」的稳健实现。

你是 Godot 4 的资深 GDScript 工程师。请实现一个背包容器 Inventory。

输入:slots 数量为 24,每个格子形如 {item: ItemData, count: int}。
任务:实现 inventory.gd(class_name Inventory, extends RefCounted),包含:
1) signal changed
2) add_item(item: ItemData, amount: int) -> int,返回没放下的剩余数量;
   逻辑必须是「先并入已有的未满堆,再找空槽」,遵循 item.max_stack。
3) remove_item(item_id: StringName, amount: int) -> int,返回实际移除数量。
4) count_of(item_id: StringName) -> int。
5) to_dict() / from_dict(data) 用于存档(只存 id、count、slot)。
输出格式:一个 gdscript 代码块,函数上方写中文注释。
约束:Godot 4.x;不依赖场景树;边界情况(amount<=0、空背包、item 为 null)要处理。

提示词 3:生成背包 UI + 存档桥接

适用场景:把容器接到界面上,并顺手把存档序列化补齐。

你是 Godot 4 的 UI 工程师。基于我已有的 Inventory(有 signal changed,
slots: Array[Dictionary])和 ItemData(有 icon、max_stack、id),完成任务:

任务A:写出 inventory_ui.gd + 对应场景结构说明。
  - 用 GridContainer,columns=6,每个格子是一个 slot_ui.tscn
    (TextureRect 显示 icon,右下角 Label 显示数量)。
  - UI 只订阅 inventory.changed 后重画,禁止直接修改 inventory。
  - 数量为 1 或 0 时隐藏数量 Label(0 显示空)。
任务B:写出 save_inventory()/load_inventory(),用 FileAccess + JSON
  存到 "user://inventory.json",加载时用 id 反查 ItemData。
输出格式:先给场景节点树(缩进列表),再给两个 gdscript 代码块。
约束:Godot 4.x;UI 与数据严格解耦;注释用中文。

学习心得

背包是「小系统里最能暴露架构问题」的一个:如果一开始就把物品做成节点、让界面直接改数据,等到要做存档、要接 Steam 云、要做交易/合成时就会全面崩盘。今天用官方 Resource 思路把它拆成数据 / 容器 / 视图三层,正好也呼应了用 DeepSeek 做游戏的正确姿势——别让 AI 一次写一整套系统,而是分层给约束、逐块生成、逐块验证。对「用 DeepSeek 从零做一款能上架 Steam 的游戏」这个目标来说,背包几乎是所有 RPG/冒险/生存类游戏的第一块通用积木,今天这块积木搭稳了,后面接商店、合成、装备就有地方挂。

明日关注

  • 把 ItemData 接到「拾取物」:地面上的可拾取体只带一个 ItemData 引用,碰到玩家就调 inventory.add_item()。
  • 给背包加「拖拽移动 / 使用物品」的交互,重点练「UI 事件 → 数据变更」的单向数据流。
  • 思考装备栏(Equipment)与背包的关系:是同一套 Slot 复用,还是单独一套?去 Godot 文档看 Resource 的 duplicate() 行为。

本文由自动化学习流水线每日生成,坚持即学即记。