第十四部分:任务系统基础:Quest Resource、NPC 接任务、收集物品、完成任务、奖励

2026-05-19
350912 分钟
...

这一部分开始做 RPG 里非常重要的“目标系统”。

前面我们已经有了:

NPC 对话
背包
金币
物品拾取
怪物掉落
药水使用

现在我们要把这些串起来:

NPC 发布任务

玩家接任务

玩家收集物品 / 击败怪物

任务进度更新

 NPC 交任务

获得金币 / 物品奖励

这就是 RPG 最经典的任务闭环。

1. 任务系统要解决什么问题

如果没有任务系统,玩家做事情就很散。

比如玩家打史莱姆,只是:

打死史莱姆
掉落 slime_gel
捡起来

但有了任务系统,就变成:

村民森林里的史莱姆太多了请帮我收集 5 个史莱姆凝胶

玩家接任务

打史莱姆

收集 slime_gel 0/51/52/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
任务 IDcollect_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
 NPCcomplete_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 → 任务 → 战斗 → 掉落 → 背包 → 交付 → 奖励”闭环。

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
godot2026-07-27
Godot 4 常用 UI 节点详解
Godot 4 常用 UI 节点详解 在 Godot 4 中,UI 系统基于 Control(控件) 节点构建。所有 UI 节点都继承自 Control,形成一棵完整的 UI 树。与游戏引擎中常见的"Canvas + DOM"模式不同,Godot 的 UI 系统是声明式的——你在场景中搭好节点树,引擎自动完成布局计算。 一、布局系统:Container 家族 Container 是 Godot UI 的 骨架 。它决定了子节点的大小和位...
学习
godot2026-07-09
Godot 4 自动地形系统(AutoTileSet / Terrains)完全指南
前言 在 2D 游戏开发中,地形瓦片(tile)的拼接是一个绕不开的问题。想象一下:你有一片草地、一条河流、一段平台——如果每一块边缘、角落、过渡区域都要手动选择对应的瓦片图,工作量将是巨大的。 自动地形系统 就是为了解决这个问题而生的。 一、什么是自动地形(Autotiling)? 自动地形的核心思想很简单: 你只管画,引擎帮你选对瓦片 。 当你在 TileMap 上绘制地形时,引擎会自动检测每个瓦片的上下左右邻居,然后根据预设的规则...
学习
godot2026-06-25
Godot 中 zindex 和 ysort 的区别总结
在 Godot 2D 游戏开发中,角色、树木、怪物、地面、技能特效、UI 都需要正确的显示顺序。比如角色走到树前面时,角色应该挡住树;角色走到树后面时,树又应该挡住角色。 这种显示顺序主要和两个概念有关: 和 。 其中 用来手动控制图层顺序, 用来根据物体的 Y 坐标自动排序。 一句话理解 是手动分层。 是根据 Y 坐标自动排序。 简单来说: | 属性 | 作用 | 适合场景 | | | | | | | 数值越大,显示越靠前 | 地面、...
学习
godot2026-06-11
Tileset 资源图的标准和规范
一、什么是 Tileset 资源图 Tileset,中文通常叫“图块资源图”或“瓦片图”,是 2D 游戏中非常常见的一种地图资源组织方式。 简单来说,Tileset 就是把很多小图块按照固定尺寸排列在一张图片里。游戏引擎会按照固定的格子大小去切割这张图片,然后把每个小格子当成一个独立的地图块使用。 比如一个 32×32 像素的 Tileset 中,每一个 tile 都是 32×32 像素。地图编辑器或游戏引擎会按照 32×32 的网格,...
学习
godot2026-05-29
用户角色精灵图制作角色的完整流程
在 2D RPG 游戏里,角色通常不是用一张单独图片完成的,而是用一张“角色精灵图”来做。 所谓角色精灵图,通常是一张包含多个动作帧的大图。比如角色向下走有 4 帧,向左走有 4 帧,向右走有 4 帧,向上走有 4 帧。Godot 会根据这些帧不断切换图片,看起来角色就动起来了。 这篇文章主要介绍:拿到一张角色精灵图之后,如何在 Godot 中把它做成一个可以正常移动、播放动画、和地图产生遮挡关系的角色。 一、先理解角色精灵图是什么 角...
学习
godot2026-05-26
Godot 节点系统详细介绍
Godot 里最核心的东西不是“类”,也不是“组件”,而是 节点 Node 。 你可以把 Godot 的节点理解成: 在前端里,一个页面是由很多 DOM 元素组成的; 在 Godot 里,一个游戏场景是由很多 Node 节点组成的。 比如一个玩家角色,可能不是一个单独对象,而是这样的结构: 这里的 是根节点,下面挂着显示图片、播放动画、碰撞检测、摄像机、音效等子节点。 Godot 官方文档也把节点和场景放在一起讲:多个节点组成树状结构后...
学习

评论

请登录后发表评论

去登录
加载评论中...