第十四部分:任务系统基础:Quest Resource、NPC 接任务、收集物品、完成任务、奖励
这一部分开始做 RPG 里非常重要的“目标系统”。
前面我们已经有了:
NPC 对话
背包
金币
物品拾取
怪物掉落
药水使用
现在我们要把这些串起来:
NPC 发布任务
↓
玩家接任务
↓
玩家收集物品 / 击败怪物
↓
任务进度更新
↓
回 NPC 交任务
↓
获得金币 / 物品奖励
这就是 RPG 最经典的任务闭环。
1. 任务系统要解决什么问题
如果没有任务系统,玩家做事情就很散。
比如玩家打史莱姆,只是:
打死史莱姆
掉落 slime_gel
捡起来
但有了任务系统,就变成:
村民:森林里的史莱姆太多了,请帮我收集 5 个史莱姆凝胶。
↓
玩家接任务
↓
打史莱姆
↓
收集 slime_gel 0/5、1/5、2/5...
↓
回去找村民
↓
交任务
↓
获得金币和药水
这一下就有目标、有反馈、有奖励了。
所以任务系统不是“复杂剧情系统”,它最基础的作用是:
给玩家一个明确目标
记录目标完成情况
在完成后发奖励
2. 第一版任务系统先做什么
第一版先做最简单的收集任务。
目标:
NPC 发布任务
玩家接任务
任务要求收集某个物品
背包里数量足够时可以完成
完成后扣除物品
奖励金币
奖励物品
任务标记为 completed
先不要做:
复杂分支
多阶段任务
任务链
选择奖励
剧情条件
地图标记
任务追踪 UI
自动寻路
失败条件
限时任务
这些后面都可以加。
第一阶段只做:
收集物品任务
这是最适合接上背包系统的任务类型。
3. 任务数据也适合用 Resource
和物品一样,任务也适合用 Resource。
原因是:
任务本质是数据
可以保存成 .tres
可以在 Inspector 里编辑
可以被 NPC 引用
可以被 QuestManager 读取
Godot 的 Resource 主要就是数据容器,可以保存到磁盘,也可以嵌套其他 Resource;自定义 Resource 也可以通过 class_name 和导出属性在 Inspector 里创建和编辑,非常适合物品、任务、角色配置这类数据。(Godot Engine documentation)
我们可以做:
QuestData.gd
↓
collect_slime_gel.tres
就像前面的:
ItemData.gd
↓
small_potion.tres
slime_gel.tres
4. QuestData 是任务定义
先理解两层:
QuestData:任务定义
QuestState:玩家当前这个任务的状态
比如:
QuestData:
任务 ID:collect_slime_gel
标题:收集史莱姆凝胶
描述:帮村民收集 5 个史莱姆凝胶
目标物品:slime_gel
需要数量:5
奖励金币:50
奖励物品:small_potion x2
而玩家当前任务状态是:
QuestState:
任务:collect_slime_gel
状态:进行中
当前数量:3/5
是否已完成:否
所以不要把任务“定义”和“进度”混在一起。
QuestData = 这是什么任务
QuestState = 玩家做到哪一步了
5. QuestData.gd
创建:
res://scripts/quests/quest_data.gd
class_name QuestData
extends Resource
enum QuestType {
COLLECT_ITEM,
KILL_ENEMY,
TALK_TO_NPC
}
@export var id: String = ""
@export var title: String = ""
@export_multiline var description: String = ""
@export var quest_type: QuestType = QuestType.COLLECT_ITEM
@export var required_item: ItemData
@export var required_quantity: int = 1
@export var reward_gold: int = 0
@export var reward_items: Array[ItemData] = []
@export var reward_item_quantities: Array[int] = []
这里先支持收集物品任务。
后面预留了:
KILL_ENEMY
TALK_TO_NPC
但第一版不用。
6. reward_items 和 reward_item_quantities 为什么是两个数组
因为 Godot 的导出数据里,简单一点可以这样做:
reward_items:
- small_potion
- slime_gel
reward_item_quantities:
- 2
- 3
表示:
奖励 small_potion x2
奖励 slime_gel x3
这个写法不算完美,但入门阶段简单。
更好的做法是做一个 QuestReward Resource:
QuestReward
├── item
└── quantity
后面可以重构。
第一版先这样,少创建几个类。
7. 创建一个任务资源
创建:
res://data/quests/collect_slime_gel.tres
类型选择:
QuestData
设置:
id = collect_slime_gel
title = 收集史莱姆凝胶
description = 村民希望你帮他收集 5 个史莱姆凝胶。
quest_type = COLLECT_ITEM
required_item = slime_gel.tres
required_quantity = 5
reward_gold = 50
reward_items = [small_potion.tres]
reward_item_quantities = [2]
这样一个基础任务就有了。
8. QuestStatus 任务状态
玩家接了任务后,任务会有状态。
常见状态:
NOT_STARTED:未开始
IN_PROGRESS:进行中
READY_TO_COMPLETE:可完成
COMPLETED:已完成
可以写在 QuestState 里。
创建:
res://scripts/quests/quest_state.gd
class_name QuestState
extends RefCounted
enum Status {
IN_PROGRESS,
READY_TO_COMPLETE,
COMPLETED
}
var quest: QuestData
var status: Status = Status.IN_PROGRESS
var current_amount: int = 0
func _init(p_quest: QuestData = null):
quest = p_quest
这里没有 NOT_STARTED,因为没接的任务根本不放进任务列表。
也就是说:
不在 QuestManager 里 = 未开始
在 QuestManager 里 = 进行中 / 可完成 / 已完成
这样简单。
9. QuestManager 是什么
任务需要一个管理器。
它负责:
接任务
检查任务是否已接
检查任务是否已完成
刷新任务进度
完成任务
发奖励
发信号通知 UI
第一版可以把它放到 Main 下:
Main
├── QuestManager
├── World
└── UI
也可以做成 Autoload。
Godot 的 Autoload 可以让一个脚本或场景在游戏启动时自动加载到 root 下,并且在其他场景之前存在,适合做全局访问的管理器,比如全局状态、任务管理、音频管理等。(Godot Engine documentation)
不过你现在第一阶段,我建议:
先放 Main 下面
后面再改 Autoload
因为 Autoload 虽然方便,但也容易让新手什么都往全局塞,最后全局对象变成大型杂物间。
10. QuestManager.gd 基础版
创建:
res://scripts/quests/quest_manager.gd
class_name QuestManager
extends Node
signal quest_started(quest_state: QuestState)
signal quest_updated(quest_state: QuestState)
signal quest_completed(quest_state: QuestState)
var active_quests: Dictionary = {}
var completed_quests: Dictionary = {}
func start_quest(quest: QuestData) -> bool:
if quest == null:
return false
if active_quests.has(quest.id):
return false
if completed_quests.has(quest.id):
return false
var quest_state = QuestState.new(quest)
active_quests[quest.id] = quest_state
quest_started.emit(quest_state)
return true
这里用了 Dictionary:
active_quests[quest.id] = quest_state
这样可以快速通过 quest id 找任务。
11. 检查任务状态
继续添加:
func has_started(quest_id: String) -> bool:
return active_quests.has(quest_id) or completed_quests.has(quest_id)
func is_completed(quest_id: String) -> bool:
return completed_quests.has(quest_id)
func get_active_quest(quest_id: String) -> QuestState:
return active_quests.get(quest_id)
这样 NPC 可以问:
这个任务接了吗?
这个任务完成了吗?
这个任务是否可以交?
12. 刷新收集任务进度
任务进度可以根据背包实时计算。
比如任务要求:
slime_gel x5
玩家背包里有:
slime_gel x3
任务进度就是:
3 / 5
QuestManager 可以这样写:
func update_collect_quest_progress(quest_id: String, inventory: Inventory):
var quest_state: QuestState = active_quests.get(quest_id)
if quest_state == null:
return
var quest = quest_state.quest
if quest.quest_type != QuestData.QuestType.COLLECT_ITEM:
return
if quest.required_item == null:
return
var slot = inventory.find_slot_by_item_id(quest.required_item.id)
if slot == null:
quest_state.current_amount = 0
else:
quest_state.current_amount = min(slot.quantity, quest.required_quantity)
if quest_state.current_amount >= quest.required_quantity:
quest_state.status = QuestState.Status.READY_TO_COMPLETE
else:
quest_state.status = QuestState.Status.IN_PROGRESS
quest_updated.emit(quest_state)
这就是根据背包更新任务进度。
13. 更通用:刷新所有任务
每次背包变化时,可以刷新所有收集任务。
func update_all_collect_quests(inventory: Inventory):
for quest_id in active_quests.keys():
update_collect_quest_progress(quest_id, inventory)
然后当 Inventory 变化时:
inventory.inventory_changed.connect(func():
quest_manager.update_all_collect_quests(inventory)
)
这样玩家捡到 slime_gel 后,任务自动更新。
这就是 signal 的价值:某件事发生时通知相关系统,而不是所有系统互相硬查。Godot 的 signals 本来就是让对象在某些事件发生时发出消息,其他对象连接后响应,从而减少直接引用和耦合。(Godot Engine documentation)
14. 完成任务
完成任务时要做几件事:
检查任务是否存在
检查是否 READY_TO_COMPLETE
扣除任务所需物品
发放奖励金币
发放奖励物品
从 active 移到 completed
发出 quest_completed 信号
代码:
func complete_quest(quest_id: String, player: Node) -> bool:
var quest_state: QuestState = active_quests.get(quest_id)
if quest_state == null:
return false
if quest_state.status != QuestState.Status.READY_TO_COMPLETE:
return false
var quest = quest_state.quest
if quest.quest_type == QuestData.QuestType.COLLECT_ITEM:
if quest.required_item == null:
return false
var inventory: Inventory = player.get_inventory()
if not inventory.has_item(quest.required_item.id, quest.required_quantity):
return false
inventory.remove_item(quest.required_item.id, quest.required_quantity)
give_rewards(quest, player)
active_quests.erase(quest_id)
quest_state.status = QuestState.Status.COMPLETED
completed_quests[quest_id] = quest_state
quest_completed.emit(quest_state)
return true
这里要求 Player 有:
func get_inventory() -> Inventory:
return inventory
前面背包系统已经建议这么做了。
15. 发奖励
func give_rewards(quest: QuestData, player: Node):
if quest.reward_gold > 0 and player.has_method("add_gold"):
player.add_gold(quest.reward_gold)
var reward_count = quest.reward_items.size()
for i in range(reward_count):
var item = quest.reward_items[i]
if item == null:
continue
var quantity = 1
if i < quest.reward_item_quantities.size():
quantity = quest.reward_item_quantities[i]
if player.has_method("pick_up_item"):
player.pick_up_item(item, quantity)
注意这里用了 pick_up_item() 来发物品奖励。
好处是:
金币类奖励会自动进 gold
普通物品会自动进 inventory
这能复用之前的逻辑。
16. QuestManager 完整基础版
class_name QuestManager
extends Node
signal quest_started(quest_state: QuestState)
signal quest_updated(quest_state: QuestState)
signal quest_completed(quest_state: QuestState)
var active_quests: Dictionary = {}
var completed_quests: Dictionary = {}
func start_quest(quest: QuestData) -> bool:
if quest == null:
return false
if active_quests.has(quest.id):
return false
if completed_quests.has(quest.id):
return false
var quest_state = QuestState.new(quest)
active_quests[quest.id] = quest_state
quest_started.emit(quest_state)
return true
func has_started(quest_id: String) -> bool:
return active_quests.has(quest_id) or completed_quests.has(quest_id)
func is_completed(quest_id: String) -> bool:
return completed_quests.has(quest_id)
func get_active_quest(quest_id: String) -> QuestState:
return active_quests.get(quest_id)
func update_collect_quest_progress(quest_id: String, inventory: Inventory):
var quest_state: QuestState = active_quests.get(quest_id)
if quest_state == null:
return
var quest = quest_state.quest
if quest.quest_type != QuestData.QuestType.COLLECT_ITEM:
return
if quest.required_item == null:
return
var slot = inventory.find_slot_by_item_id(quest.required_item.id)
if slot == null:
quest_state.current_amount = 0
else:
quest_state.current_amount = min(slot.quantity, quest.required_quantity)
if quest_state.current_amount >= quest.required_quantity:
quest_state.status = QuestState.Status.READY_TO_COMPLETE
else:
quest_state.status = QuestState.Status.IN_PROGRESS
quest_updated.emit(quest_state)
func update_all_collect_quests(inventory: Inventory):
for quest_id in active_quests.keys():
update_collect_quest_progress(quest_id, inventory)
func complete_quest(quest_id: String, player: Node) -> bool:
var quest_state: QuestState = active_quests.get(quest_id)
if quest_state == null:
return false
if quest_state.status != QuestState.Status.READY_TO_COMPLETE:
return false
var quest = quest_state.quest
if quest.quest_type == QuestData.QuestType.COLLECT_ITEM:
if quest.required_item == null:
return false
if not player.has_method("get_inventory"):
return false
var inventory: Inventory = player.get_inventory()
if not inventory.has_item(quest.required_item.id, quest.required_quantity):
return false
inventory.remove_item(quest.required_item.id, quest.required_quantity)
give_rewards(quest, player)
active_quests.erase(quest_id)
quest_state.status = QuestState.Status.COMPLETED
completed_quests[quest_id] = quest_state
quest_completed.emit(quest_state)
return true
func give_rewards(quest: QuestData, player: Node):
if quest.reward_gold > 0 and player.has_method("add_gold"):
player.add_gold(quest.reward_gold)
var reward_count = quest.reward_items.size()
for i in range(reward_count):
var item = quest.reward_items[i]
if item == null:
continue
var quantity = 1
if i < quest.reward_item_quantities.size():
quantity = quest.reward_item_quantities[i]
if player.has_method("pick_up_item"):
player.pick_up_item(item, quantity)
这版已经能支持:
接任务
检测收集进度
完成任务
扣物品
发金币
发物品
17. NPC 怎么发布任务
NPC 现在有对话。
我们给 NPC 加任务数据:
@export var quest: QuestData
@export var quest_manager: QuestManager
NPC 交互时根据任务状态显示不同对话。
常见状态:
没接任务:
NPC:你能帮我收集 5 个史莱姆凝胶吗?
玩家按 E 后接任务
任务进行中:
NPC:你已经收集了 3/5 个史莱姆凝胶。
可完成:
NPC:太好了,你收集齐了!
完成任务,发奖励
已完成:
NPC:谢谢你的帮助。
18. QuestNPC.gd 基础版
你可以直接在 NPC.gd 里加,也可以新建 QuestNPC.gd。
先写一个带任务的 NPC:
extends Area2D
@export var prompt_text: String = "按 E 对话"
@export var npc_name: String = "村民"
@export var quest: QuestData
@export var quest_manager: QuestManager
@export var dialogue_box: Control
@export var before_quest_lines: Array[String] = [
"森林里的史莱姆越来越多了。",
"你能帮我收集 5 个史莱姆凝胶吗?"
]
@export var in_progress_lines: Array[String] = [
"你还没有收集齐史莱姆凝胶。"
]
@export var ready_to_complete_lines: Array[String] = [
"太好了,你收集齐了!",
"这是给你的奖励。"
]
@export var completed_lines: Array[String] = [
"谢谢你之前的帮助。"
]
func interact(player: Node):
if quest == null:
start_dialogue(before_quest_lines)
return
if quest_manager == null:
print("NPC 没有设置 quest_manager")
return
if quest_manager.is_completed(quest.id):
start_dialogue(completed_lines)
return
var quest_state = quest_manager.get_active_quest(quest.id)
if quest_state == null:
quest_manager.start_quest(quest)
start_dialogue(before_quest_lines)
return
if player.has_method("get_inventory"):
quest_manager.update_collect_quest_progress(quest.id, player.get_inventory())
if quest_state.status == QuestState.Status.READY_TO_COMPLETE:
var success = quest_manager.complete_quest(quest.id, player)
if success:
start_dialogue(ready_to_complete_lines)
else:
start_dialogue(in_progress_lines)
return
start_dialogue(in_progress_lines)
func start_dialogue(lines: Array[String]):
if dialogue_box == null:
print("NPC 没有设置 dialogue_box")
return
dialogue_box.start_dialogue(npc_name, lines)
这个版本能跑,但逻辑有个小瑕疵:
第一次接任务时,NPC 先接任务,然后播放“你能帮我吗?”
从玩家体验上还可以,但更自然的是:
第一次对话只是说明任务
玩家确认后才接
不过那需要选项 UI,第一版先不做。
19. 更简单的交互逻辑
如果你想第一版更直接,可以做成:
第一次按 E:
直接接任务并显示任务说明
收集中:
显示进度
收集齐:
按 E 交任务并发奖励
完全可以。
任务系统第一版不要追求“剧情味”,先追求功能闭环。
20. 显示任务进度
在进行中对话里可以动态生成文本:
func get_progress_line(quest_state: QuestState) -> String:
var quest = quest_state.quest
return "当前进度:%d / %d" % [
quest_state.current_amount,
quest.required_quantity
]
然后:
var lines = in_progress_lines.duplicate()
lines.append(get_progress_line(quest_state))
start_dialogue(lines)
改进 NPC 逻辑:
if quest_state.status == QuestState.Status.IN_PROGRESS:
var lines = in_progress_lines.duplicate()
lines.append(get_progress_line(quest_state))
start_dialogue(lines)
return
这样 NPC 会说:
你还没有收集齐史莱姆凝胶。
当前进度:3 / 5
21. QuestNPC.gd 改进版
extends Area2D
@export var prompt_text: String = "按 E 对话"
@export var npc_name: String = "村民"
@export var quest: QuestData
@export var quest_manager: QuestManager
@export var dialogue_box: Control
@export var before_quest_lines: Array[String] = [
"森林里的史莱姆越来越多了。",
"你能帮我收集一些史莱姆凝胶吗?"
]
@export var in_progress_lines: Array[String] = [
"你还没有收集齐。"
]
@export var ready_to_complete_lines: Array[String] = [
"太好了,你收集齐了!",
"这是给你的奖励。"
]
@export var completed_lines: Array[String] = [
"谢谢你之前的帮助。"
]
func interact(player: Node):
if quest == null:
start_dialogue(before_quest_lines)
return
if quest_manager == null:
print("NPC 没有设置 quest_manager")
return
if quest_manager.is_completed(quest.id):
start_dialogue(completed_lines)
return
var quest_state = quest_manager.get_active_quest(quest.id)
if quest_state == null:
quest_manager.start_quest(quest)
start_dialogue(before_quest_lines)
return
if player.has_method("get_inventory"):
quest_manager.update_collect_quest_progress(quest.id, player.get_inventory())
if quest_state.status == QuestState.Status.READY_TO_COMPLETE:
var success = quest_manager.complete_quest(quest.id, player)
if success:
start_dialogue(ready_to_complete_lines)
else:
start_progress_dialogue(quest_state)
return
start_progress_dialogue(quest_state)
func start_progress_dialogue(quest_state: QuestState):
var lines = in_progress_lines.duplicate()
lines.append(get_progress_line(quest_state))
start_dialogue(lines)
func get_progress_line(quest_state: QuestState) -> String:
if quest_state == null or quest_state.quest == null:
return ""
var quest_data = quest_state.quest
if quest_data.quest_type == QuestData.QuestType.COLLECT_ITEM:
return "当前进度:%d / %d" % [
quest_state.current_amount,
quest_data.required_quantity
]
return ""
func start_dialogue(lines: Array[String]):
if dialogue_box == null:
print("NPC 没有设置 dialogue_box")
return
dialogue_box.start_dialogue(npc_name, lines)
22. 背包变化后自动更新任务
现在我们还需要把 Inventory 和 QuestManager 连起来。
在 Main.gd 或 World.gd 里做:
@onready var player: Node = $World/Player
@onready var quest_manager: QuestManager = $QuestManager
func _ready():
var inventory = player.get_inventory()
inventory.inventory_changed.connect(func():
quest_manager.update_all_collect_quests(inventory)
)
如果你的结构是:
Main
├── QuestManager
├── World
│ └── Player
└── UI
这样可以。
如果 QuestManager 放在 World 里,也类似。
23. 任务 UI 第一版
任务系统最好有一个 UI 面板。
先做简单列表:
UI CanvasLayer
└── QuestPanel Control
└── Panel
└── QuestList VBoxContainer
默认隐藏:
visible = false
InputMap 添加:
open_quest_log
绑定:
L
24. QuestPanel.gd
extends Control
@export var quest_manager: QuestManager
@onready var quest_list: VBoxContainer = $Panel/QuestList
func _ready():
visible = false
if quest_manager != null:
quest_manager.quest_started.connect(_on_quest_changed)
quest_manager.quest_updated.connect(_on_quest_changed)
quest_manager.quest_completed.connect(_on_quest_changed)
refresh()
func toggle():
visible = not visible
func _on_quest_changed(_quest_state: QuestState):
refresh()
func refresh():
for child in quest_list.get_children():
child.queue_free()
if quest_manager == null:
return
for quest_id in quest_manager.active_quests.keys():
var quest_state: QuestState = quest_manager.active_quests[quest_id]
var label = Label.new()
label.text = get_quest_text(quest_state)
quest_list.add_child(label)
func get_quest_text(quest_state: QuestState) -> String:
var quest = quest_state.quest
if quest.quest_type == QuestData.QuestType.COLLECT_ITEM:
return "%s:%d / %d" % [
quest.title,
quest_state.current_amount,
quest.required_quantity
]
return quest.title
这个面板会显示:
收集史莱姆凝胶:3 / 5
25. UI.gd 控制任务面板打开
@onready var quest_panel: Control = $QuestPanel
func _unhandled_input(event):
if event.is_action_pressed("open_quest_log"):
quest_panel.toggle()
get_viewport().set_input_as_handled()
现在按 L 可以打开任务列表。
26. QuestPanel 如何绑定 QuestManager
UI.gd:
@export var quest_manager: QuestManager
@onready var quest_panel: Control = $QuestPanel
func _ready():
quest_panel.quest_manager = quest_manager
quest_panel.refresh()
或者你直接在 Inspector 里把 QuestManager 拖给 QuestPanel。
第一版都行。
27. 接任务时提示
可以在 QuestManager 的信号上接 UI 提示:
任务已接受:收集史莱姆凝胶
任务已完成:收集史莱姆凝胶
先在 UI 里加一个 Label:
QuestToast Label
默认隐藏。
简单版:
@onready var quest_toast: Label = $QuestToast
func _ready():
quest_toast.visible = false
quest_manager.quest_started.connect(_on_quest_started)
quest_manager.quest_completed.connect(_on_quest_completed)
func _on_quest_started(quest_state: QuestState):
show_toast("任务已接受:" + quest_state.quest.title)
func _on_quest_completed(quest_state: QuestState):
show_toast("任务已完成:" + quest_state.quest.title)
func show_toast(text: String):
quest_toast.text = text
quest_toast.visible = true
await get_tree().create_timer(2.0).timeout
quest_toast.visible = false
第一版足够。
后面可以做动画淡入淡出。
28. 任务完成时是否自动完成
有两种设计:
1. 收集齐后自动完成并发奖励
2. 收集齐后回 NPC 交任务
RPG 里更常见的是第二种:
回 NPC 交任务
因为这样有对话反馈和奖励仪式感。
所以我们现在设计的是:
收集齐 → READY_TO_COMPLETE
回 NPC → complete_quest()
这样更适合 RPG。
29. 任务物品要不要扣除
收集任务有两种:
提交型:
收集 5 个史莱姆凝胶,交任务时扣掉
检查型:
拥有 5 个史莱姆凝胶即可完成,不扣
目前我们的代码是提交型:
inventory.remove_item(quest.required_item.id, quest.required_quantity)
这适合:
村民要这些材料
如果以后要支持不扣物品,可以在 QuestData 加:
@export var consume_required_items: bool = true
然后完成任务时判断。
第一版先扣除,逻辑直观。
30. 已完成任务如何处理
现在已完成任务放在:
completed_quests
这能防止重复接同一个任务。
NPC 会根据:
quest_manager.is_completed(quest.id)
显示完成后的对话。
以后存档时,也要保存已完成任务 ID:
{
"completed_quests": ["collect_slime_gel"]
}
31. 任务 ID 也很重要
和物品一样,任务也需要稳定 id。
不要用标题做唯一标识。
不推荐:
收集史莱姆凝胶
推荐:
collect_slime_gel
原因:
标题会改
以后会多语言
任务标题可能重复
内部逻辑用 id。
玩家显示用 title。
32. 击杀任务怎么扩展
以后做击杀任务,比如:
消灭 3 只史莱姆
QuestData 需要:
@export var required_enemy_id: String = ""
@export var required_kill_count: int = 1
Enemy.gd 需要:
@export var enemy_id: String = "slime"
怪物死亡时通知 QuestManager:
quest_manager.notify_enemy_killed(enemy_id)
QuestManager:
func notify_enemy_killed(enemy_id: String):
for quest_state in active_quests.values():
var quest = quest_state.quest
if quest.quest_type != QuestData.QuestType.KILL_ENEMY:
continue
if quest.required_enemy_id != enemy_id:
continue
quest_state.current_amount += 1
if quest_state.current_amount >= quest.required_kill_count:
quest_state.status = QuestState.Status.READY_TO_COMPLETE
quest_updated.emit(quest_state)
现在不用做,知道扩展方向就行。
33. 对话任务怎么扩展
比如:
和铁匠对话
QuestData 加:
@export var target_npc_id: String = ""
NPC.gd:
@export var npc_id: String = "blacksmith"
交互时通知:
quest_manager.notify_npc_talked(npc_id)
QuestManager 更新对应任务。
这和击杀任务类似。
34. 任务链怎么扩展
任务链就是:
完成 A 后解锁 B
完成 B 后解锁 C
QuestData 可以加:
@export var next_quest: QuestData
完成任务时:
if quest.next_quest != null:
start_quest(quest.next_quest)
或者 NPC 根据 completed 状态切换发布新任务。
第一版不做。
但后面你的 RPG 很快会需要这个。
35. 任务和对话如何更好结合
现在 NPC 是:
根据任务状态选择不同 lines
这已经可以。
以后可以做更高级的:
DialogueData
QuestCondition
DialogueOption
比如:
如果未接任务 → 显示任务说明
如果任务进行中 → 显示进度
如果任务可完成 → 显示完成对话
如果任务已完成 → 显示感谢
本质还是状态判断。
不要一上来就做大型剧情编辑器。
你现在用数组台词先能跑,就非常好。
36. 当前推荐节点结构
现在结构可以是:
Main Node
├── QuestManager Node
├── World Node2D
│ ├── Ground TileMapLayer
│ ├── Obstacles TileMapLayer
│ ├── SpawnPoints Node2D
│ ├── Drops Node2D
│ ├── Enemies Node2D
│ ├── Interactables Node2D
│ │ └── QuestNPC Area2D
│ ├── Player CharacterBody2D
│ │ └── Inventory Node
│ └── Foreground TileMapLayer
└── UI CanvasLayer
├── PlayerHUD Control
├── InventoryPanel Control
├── QuestPanel Control
├── QuestToast Label
├── InteractLabel Label
├── DialogueBox Control
└── DeathPanel Control
数据资源:
res://data/items/small_potion.tres
res://data/items/slime_gel.tres
res://data/items/gold_coin.tres
res://data/quests/collect_slime_gel.tres
脚本:
ItemData.gd
InventorySlot.gd
Inventory.gd
QuestData.gd
QuestState.gd
QuestManager.gd
QuestNPC.gd
QuestPanel.gd
37. 常见错误 1:任务接了但进度不更新
排查:
Inventory.add_item 是否 emit inventory_changed
Main/World 是否连接 inventory_changed 到 quest_manager.update_all_collect_quests()
QuestData.required_item 是否设置正确
required_item.id 是否和背包里的 item.id 一致
QuestManager.active_quests 里是否真的有这个任务
最常见的是忘了连接:
inventory.inventory_changed.connect(func():
quest_manager.update_all_collect_quests(inventory)
)
38. 常见错误 2:收集够了但 NPC 不给交任务
排查:
NPC interact 时有没有调用 update_collect_quest_progress
quest_state.status 是否变成 READY_TO_COMPLETE
背包里数量是否真的够
QuestData.required_quantity 是否设置对
required_item 是否拖错了
可以临时打印:
print("任务状态:", quest_state.status)
print("当前进度:", quest_state.current_amount)
39. 常见错误 3:交任务后物品没扣
检查:
complete_quest 里是否调用 inventory.remove_item
remove_item 是否返回 true
item_id 是否一致
required_quantity 是否正确
如果你想任务不扣物品,那是设计问题,不是 bug。
但当前代码是扣的。
40. 常见错误 4:奖励物品数量不对
检查:
reward_items 和 reward_item_quantities 数组顺序是否对应
reward_item_quantities 是否少填了
give_rewards 里是否 fallback 为 1
比如:
reward_items = [small_potion]
reward_item_quantities = []
那默认给 1 个。
41. 常见错误 5:完成任务后还能重复接
检查:
completed_quests 是否记录了 quest.id
start_quest 是否检查 completed_quests
NPC 是否先判断 is_completed
quest.id 是否为空
尤其注意:
QuestData.id 不能为空
如果 id 是空字符串,所有任务都会撞在一起,直接大型喜剧事故。
42. 常见错误 6:QuestPanel 不刷新
检查:
QuestPanel.quest_manager 是否设置
quest_started / quest_updated / quest_completed 是否连接
refresh() 是否被调用
QuestList 路径是否正确
临时打印:
func refresh():
print("刷新任务面板")
43. 常见错误 7:NPC 没有打开对话框
检查:
QuestNPC.dialogue_box 是否拖了
dialogue_box 是否有 start_dialogue 方法
NPC 是否加入 interactable 组
Player 当前交互对象是不是这个 NPC
前面的交互系统仍然是基础。
任务 NPC 也必须是一个 interactable。
44. 这一部分最重要的记忆点
QuestData 是任务定义
QuestState 是玩家当前任务进度
QuestManager 负责接任务、更新进度、完成任务和发奖励
收集任务可以根据 Inventory 自动刷新进度
NPC 根据任务状态显示不同对话
任务完成后从 active_quests 移到 completed_quests
任务 ID 必须稳定,不要用标题当 ID
第一版先做收集任务,不要急着做复杂任务链
45. 你现在的小练习
做一个完整收集任务:
1. 创建 QuestData.gd
2. 创建 QuestState.gd
3. 创建 QuestManager.gd
4. Main 下添加 QuestManager
5. 创建 collect_slime_gel.tres
6. 任务要求 slime_gel x5
7. 奖励金币 50
8. 奖励 small_potion x2
9. 创建 QuestNPC
10. NPC 设置 quest = collect_slime_gel.tres
11. NPC 设置 quest_manager
12. NPC 设置 dialogue_box
13. 玩家第一次对话接任务
14. 玩家打 Slime 掉 slime_gel
15. 玩家捡 slime_gel 后背包增加
16. Inventory 变化后 QuestManager 更新进度
17. 按 L 打开 QuestPanel 查看进度
18. 收集够 5 个后回 NPC
19. NPC 完成任务
20. 扣除 5 个 slime_gel
21. 玩家获得 50 金币和 2 个小药水
22. QuestPanel 不再显示该任务
23. 再和 NPC 对话显示“谢谢你的帮助”
完成这部分后,你的 RPG 就有了非常完整的“NPC → 任务 → 战斗 → 掉落 → 背包 → 交付 → 奖励”闭环。
如果您觉得这篇文章有帮助,请点个赞吧~
评论
请登录后发表评论
去登录