第十七部分:经验值与等级系统:击杀怪物获得经验、升级、属性成长、等级 UI
这一部分开始做 RPG 里非常核心的成长系统。
前面我们已经有了:
玩家能攻击
怪物能死亡
怪物能掉落物品
玩家能获得装备
玩家属性会变强
现在要继续加:
怪物死亡给经验
玩家获得经验
经验满了升级
升级提高属性
UI 显示等级和经验条
这样玩家打怪就不只是为了掉落物,也会有长期成长反馈。
1. 经验值系统要解决什么
经验系统的核心是:
玩家击败敌人
↓
获得经验值
↓
经验值达到升级需求
↓
等级提升
↓
基础属性成长
↓
玩家更强
比如:
玩家等级 1
当前经验 0 / 100
击杀史莱姆
获得 20 经验
当前经验 20 / 100
再击杀几只
经验达到 100
升级到等级 2
攻击力 +2
最大生命 +10
防御 +1
这就是 RPG 最基础的成长闭环。
2. 第一版经验系统先做什么
第一阶段先做:
1. Player 有 level
2. Player 有 current_exp
3. Player 有 exp_to_next_level
4. 怪物有 exp_reward
5. 怪物死亡时给玩家经验
6. 玩家经验满了升级
7. 升级后提高基础属性
8. UI 显示等级和经验条
先不要做:
职业系统
技能点
天赋树
属性点自由分配
等级上限突破
怪物等级压制
经验衰减
组队经验分配
这些都后面再说。
第一版目标非常简单:
打怪涨经验,经验满了升级,升级变强
3. Player 需要哪些经验字段
Player.gd 里加:
signal experience_changed(current_exp: int, exp_to_next_level: int)
signal level_changed(level: int)
@export var level: int = 1
@export var current_exp: int = 0
@export var exp_to_next_level: int = 100
含义:
level:
当前等级
current_exp:
当前等级已经积累的经验
exp_to_next_level:
升到下一级需要多少经验
比如:
level = 1
current_exp = 40
exp_to_next_level = 100
表示:
等级 1
经验 40 / 100
4. 为什么要发 signal
经验变化后 UI 要刷新。
升级后 UI 也要刷新。
所以 Player 需要发信号:
experience_changed.emit(current_exp, exp_to_next_level)
level_changed.emit(level)
不要让 UI 每帧去查 Player 经验。
经验变化时主动通知,结构更干净。
这和前面的血量系统一样:
血量变化 → health_changed
金币变化 → gold_changed
装备变化 → equipment_changed
经验变化 → experience_changed
等级变化 → level_changed
你会发现整个 Godot 项目里,signal 是非常常用的“状态变化通知机制”。
5. add_experience()
玩家获得经验时调用:
func add_experience(amount: int):
if amount <= 0:
return
current_exp += amount
print("获得经验:", amount)
while current_exp >= exp_to_next_level:
current_exp -= exp_to_next_level
level_up()
experience_changed.emit(current_exp, exp_to_next_level)
这里用了 while,不是 if。
为什么?
因为玩家可能一次获得大量经验,连续升好几级。
比如:
当前经验 90 / 100
获得经验 250
如果只用 if,最多升一级。
但实际应该:
90 + 250 = 340
升到 2 级,剩 240
如果下一级需要 120,再升到 3 级,剩 120
如果再满足条件,继续升
所以用:
while current_exp >= exp_to_next_level:
更稳。
6. level_up()
升级时要做:
等级 +1
提升基础属性
重新计算最终属性
恢复部分生命或回满生命
计算下一级经验需求
发出等级变化信号
基础版:
func level_up():
level += 1
base_attack += 2
base_defense += 1
base_max_hp += 10
exp_to_next_level = calculate_exp_to_next_level(level)
recalculate_stats()
current_hp = max_hp
level_changed.emit(level)
health_changed.emit(current_hp, max_hp)
print("升级!当前等级:", level)
这里选择了升级后生命回满。
这在 RPG 里很常见,反馈比较爽。
如果你不想回满,可以改成:
current_hp += 10
current_hp = min(current_hp, max_hp)
但第一版建议回满,简单直接。
7. calculate_exp_to_next_level()
升级所需经验可以先用简单公式:
func calculate_exp_to_next_level(current_level: int) -> int:
return 100 + (current_level - 1) * 50
效果:
1 → 2:100 经验
2 → 3:150 经验
3 → 4:200 经验
4 → 5:250 经验
这个是线性成长。
第一版很适合测试。
后面可以改成曲线:
func calculate_exp_to_next_level(current_level: int) -> int:
return int(100 * pow(1.25, current_level - 1))
效果:
等级越高,需要经验增长越快
但第一版建议先用线性公式。
数值更容易看懂。
8. 初始化经验 UI
Player _ready() 里要发一次信号,让 UI 初始化。
如果你的 Player.gd 里已经有 _ready(),把这些内容合并进去:
func _ready():
recalculate_stats()
current_hp = max_hp
health_changed.emit(current_hp, max_hp)
experience_changed.emit(current_exp, exp_to_next_level)
level_changed.emit(level)
注意:如果 UI 还没连接这些信号,可能收不到第一次 emit。
所以 UI 的 setup(player) 里也应该主动读取一次当前值。
和血条一样。
9. Player.gd 经验相关完整片段
signal experience_changed(current_exp: int, exp_to_next_level: int)
signal level_changed(level: int)
@export var level: int = 1
@export var current_exp: int = 0
@export var exp_to_next_level: int = 100
func add_experience(amount: int):
if amount <= 0:
return
current_exp += amount
print("获得经验:", amount)
while current_exp >= exp_to_next_level:
current_exp -= exp_to_next_level
level_up()
experience_changed.emit(current_exp, exp_to_next_level)
func level_up():
level += 1
base_attack += 2
base_defense += 1
base_max_hp += 10
exp_to_next_level = calculate_exp_to_next_level(level)
recalculate_stats()
current_hp = max_hp
level_changed.emit(level)
health_changed.emit(current_hp, max_hp)
print("升级!当前等级:", level)
func calculate_exp_to_next_level(current_level: int) -> int:
return 100 + (current_level - 1) * 50
10. 升级时属性怎么成长
第一版可以写死:
每升一级:
攻击 +2
防御 +1
最大生命 +10
代码:
base_attack += 2
base_defense += 1
base_max_hp += 10
这很直观。
但以后你可能需要更灵活:
战士:
攻击 +3
防御 +2
生命 +15
法师:
攻击 +1
魔法 +4
生命 +8
游侠:
攻击 +2
速度 +2
生命 +10
那时可以做:
LevelGrowthData Resource
现在先别做。
第一版先写死,跑通成长闭环。
11. 怪物要有 exp_reward
Enemy.gd 加:
@export var exp_reward: int = 20
不同怪物给不同经验:
史莱姆:20
野狼:35
哥布林:50
精英怪:120
Boss:500
在 Inspector 里调就行。
12. 怪物死亡时怎么给经验
怪物死亡时需要知道是谁击杀了它。
最简单的方式是:
Player 攻击 Hurtbox
Hurtbox 调用 Enemy.take_damage(amount, attacker)
Enemy 记录最后攻击者
死亡时给最后攻击者经验
现在我们要稍微改一下伤害传递。
13. Player 攻击时传 self
之前 Player 命中怪物可能是:
area.take_damage(get_attack_damage())
改成:
area.take_damage(get_attack_damage(), self)
完整:
func _on_attack_area_entered(area):
if current_state != PlayerState.ATTACK:
return
if not area.is_in_group("hurtbox"):
return
if hit_targets.has(area):
return
hit_targets.append(area)
if area.has_method("take_damage"):
area.take_damage(get_attack_damage(), self)
这样怪物知道攻击者是谁。
14. Hurtbox.gd 支持 attacker
之前 Hurtbox.gd 可能是:
func take_damage(amount: int):
var owner_node = get_parent()
if owner_node != null and owner_node.has_method("take_damage"):
owner_node.take_damage(amount)
改成:
func take_damage(amount: int, attacker: Node = null):
var owner_node = get_parent()
if owner_node != null and owner_node.has_method("take_damage"):
owner_node.take_damage(amount, attacker)
如果你的 Hurtbox 不是直接挂在 Enemy 下面,用 @export var owner_node 版本也一样:
@export var owner_node: Node
func take_damage(amount: int, attacker: Node = null):
if owner_node != null and owner_node.has_method("take_damage"):
owner_node.take_damage(amount, attacker)
15. Enemy.gd 记录 last_attacker
Enemy.gd 加:
var last_attacker: Node = null
take_damage 改成:
func take_damage(amount: int, attacker: Node = null):
if current_state == EnemyState.DEAD:
return
last_attacker = attacker
current_hp -= amount
current_hp = clamp(current_hp, 0, max_hp)
health_changed.emit(current_hp, max_hp)
print(name, " 受伤:", amount, " 当前血量:", current_hp)
if current_hp <= 0:
change_state(EnemyState.DEAD)
died.emit()
return
change_state(EnemyState.HURT)
死亡时给经验。
16. Enemy 死亡时发经验
在死亡逻辑里加:
func give_experience_to_attacker():
if last_attacker == null:
return
if last_attacker.has_method("add_experience"):
last_attacker.add_experience(exp_reward)
然后在 DEAD 状态进入时调用:
func enter_state(state: EnemyState):
match state:
EnemyState.DEAD:
give_experience_to_attacker()
drop_loot()
queue_free()
如果你要播放死亡动画,可以这样:
func enter_state(state: EnemyState):
match state:
EnemyState.DEAD:
give_experience_to_attacker()
drop_loot()
velocity = Vector2.ZERO
attack_area.monitoring = false
detection_area.monitoring = false
hurtbox.monitorable = false
play_animation("dead")
动画结束后再:
queue_free()
注意:经验和掉落要避免重复发。
只应该在死亡进入时发一次。
17. 防止经验重复发
如果 Enemy 的 enter_state(DEAD) 可能被调用多次,就可能重复给经验。
理论上 change_state() 已经防止同状态重复进入。
但为了更稳,可以加:
var reward_given: bool = false
func give_death_rewards():
if reward_given:
return
reward_given = true
give_experience_to_attacker()
drop_loot()
DEAD 状态:
func enter_state(state: EnemyState):
match state:
EnemyState.DEAD:
give_death_rewards()
...
这样不会重复发奖励。
18. Enemy 死亡奖励完整片段
@export var exp_reward: int = 20
var last_attacker: Node = null
var reward_given: bool = false
func take_damage(amount: int, attacker: Node = null):
if current_state == EnemyState.DEAD:
return
last_attacker = attacker
current_hp -= amount
current_hp = clamp(current_hp, 0, max_hp)
health_changed.emit(current_hp, max_hp)
if current_hp <= 0:
change_state(EnemyState.DEAD)
died.emit()
return
change_state(EnemyState.HURT)
func give_death_rewards():
if reward_given:
return
reward_given = true
give_experience_to_attacker()
drop_loot()
func give_experience_to_attacker():
if last_attacker == null:
return
if last_attacker.has_method("add_experience"):
last_attacker.add_experience(exp_reward)
19. PlayerHUD 显示等级和经验条
现在 PlayerHUD 里已有:
HPBar
HPLabel
GoldLabel
加:
LevelLabel
ExpBar
ExpLabel
结构:
PlayerHUD Control
├── HPBar ProgressBar
├── HPLabel Label
├── GoldLabel Label
├── LevelLabel Label
├── ExpBar ProgressBar
└── ExpLabel Label
显示效果:
Lv. 3
HP: 90 / 120
EXP: 40 / 200
金币:80
20. PlayerHUD.gd 添加经验显示
@onready var level_label: Label = $LevelLabel
@onready var exp_bar: ProgressBar = $ExpBar
@onready var exp_label: Label = $ExpLabel
setup 里连接:
func setup(player: Node):
if player == null:
return
if player.has_signal("health_changed"):
player.health_changed.connect(_on_player_health_changed)
if player.has_signal("gold_changed"):
player.gold_changed.connect(_on_gold_changed)
if player.has_signal("experience_changed"):
player.experience_changed.connect(_on_experience_changed)
if player.has_signal("level_changed"):
player.level_changed.connect(_on_level_changed)
if "current_hp" in player and "max_hp" in player:
_on_player_health_changed(player.current_hp, player.max_hp)
if "gold" in player:
_on_gold_changed(player.gold)
if "current_exp" in player and "exp_to_next_level" in player:
_on_experience_changed(player.current_exp, player.exp_to_next_level)
if "level" in player:
_on_level_changed(player.level)
21. PlayerHUD 经验相关方法
func _on_experience_changed(current_exp: int, exp_to_next_level: int):
exp_bar.min_value = 0
exp_bar.max_value = exp_to_next_level
exp_bar.value = current_exp
exp_label.text = "EXP: %d / %d" % [current_exp, exp_to_next_level]
func _on_level_changed(level: int):
level_label.text = "Lv. %d" % level
现在玩家获得经验后,经验条会涨。
升级后等级显示会变。
22. 升级时经验条为什么可能刷新两次
add_experience() 里:
while current_exp >= exp_to_next_level:
current_exp -= exp_to_next_level
level_up()
experience_changed.emit(current_exp, exp_to_next_level)
level_up() 里可能也 emit 了 health 和 level。
最终经验条在 add_experience() 末尾刷新。
这没问题。
如果你在 level_up() 里也 emit experience_changed,也能跑,但可能刷新多次。
建议经验刷新统一放在 add_experience() 最后。
23. 升级提示 UI
升级最好有提示。
UI 下加:
LevelUpToast Label
默认隐藏。
UI.gd 或 PlayerHUD.gd 都可以处理。
PlayerHUD.gd:
@onready var level_up_toast: Label = $LevelUpToast
如果 LevelUpToast 不在 PlayerHUD 下,而是在 UI 下,那就放 UI.gd 里处理。
这里先假设放 PlayerHUD 下。
func _on_level_changed(level: int):
level_label.text = "Lv. %d" % level
show_level_up_toast(level)
func show_level_up_toast(level: int):
level_up_toast.text = "升级!Lv. %d" % level
level_up_toast.visible = true
await get_tree().create_timer(2.0).timeout
level_up_toast.visible = false
注意:初始化时调用 _on_level_changed(level) 也会弹升级提示。
这不太好。
所以要区分初始化和真正升级。
24. 避免初始化时弹升级提示
可以加变量:
var initialized: bool = false
setup 最后:
initialized = true
然后:
func _on_level_changed(level: int):
level_label.text = "Lv. %d" % level
if initialized:
show_level_up_toast(level)
完整思路:
var initialized: bool = false
func setup(player: Node):
...
if "level" in player:
_on_level_changed(player.level)
initialized = true
这样第一次设置等级不会弹。
真正升级时会弹。
25. 升级时是否要播放音效和特效
第一版可以先用 Label 提示。
以后可以加:
升级音效
金光粒子
玩家闪光
属性增加提示
屏幕短暂停顿
现在先不做。
升级提示能显示就足够。
26. 等级上限
可以加:
@export var max_level: int = 99
add_experience 里:
func add_experience(amount: int):
if amount <= 0:
return
if level >= max_level:
return
current_exp += amount
while current_exp >= exp_to_next_level and level < max_level:
current_exp -= exp_to_next_level
level_up()
if level >= max_level:
current_exp = 0
experience_changed.emit(current_exp, exp_to_next_level)
level_up 里:
func level_up():
if level >= max_level:
return
level += 1
...
第一版可以先不设置上限。
但如果你担心无限升级,就加上。
27. 等级和装备属性如何叠加
当前属性来源有两类:
等级成长提高 base_attack / base_defense / base_max_hp
装备加成提高 final_attack / final_defense / max_hp
比如:
等级 1:
base_attack = 10
装备木剑:
attack_bonus = 5
final_attack = 15
升级到 2:
base_attack = 12
木剑仍然 +5
final_attack = 17
这个设计非常合理。
等级改变基础属性。
装备在基础属性上加成。
所以升级后一定要调用:
recalculate_stats()
否则装备和等级属性不会重新计算。
28. 经验是否应该给击杀者还是玩家固定获得
现在我们用:
last_attacker.add_experience(exp_reward)
这适合单人游戏。
如果以后有:
宠物
召唤物
陷阱
NPC 队友
那经验归属会更复杂。
比如:
召唤物杀死怪物,经验给玩家
NPC 杀死怪物,玩家不给经验
陷阱杀死怪物,谁放的陷阱获得经验
这些都后面再处理。
第一版:
谁最后打死,谁获得经验
够用。
29. 击杀任务和经验可以共用死亡事件
怪物死亡时现在会:
给经验
掉落
queue_free
以后还要:
通知任务系统击杀了某种怪物
这时可以给 Enemy 加:
signal enemy_died(enemy_id: String, killer: Node)
死亡时:
enemy_died.emit(enemy_id, last_attacker)
QuestManager 监听后更新击杀任务。
现在先不写。
但你要知道,怪物死亡是一个很重要的事件:
经验
掉落
任务
成就
图鉴
音效
特效
后面都会接到这里。
30. 经验数值怎么定
第一版可以这样:
升级经验:
1 → 2:100
2 → 3:150
3 → 4:200
怪物经验:
史莱姆:20
野狼:35
哥布林:50
大致上:
等级 1 打 5 只史莱姆升级
等级 2 需要更多
不要一开始把数值搞太复杂。
先让你自己玩起来感觉:
打几只怪能升一级
升级后明显变强
但不是强到秒杀所有怪
后面再慢慢调。
数值系统不是一锤子买卖,它是锅汤,边煮边尝。
31. 怪物等级
以后怪物也可以有 level。
Enemy.gd:
@export var level: int = 1
然后根据 level 计算:
max_hp
attack_damage
exp_reward
比如:
func setup_by_level():
max_hp = 20 + level * 10
attack_damage = 5 + level * 2
exp_reward = 15 + level * 5
第一版不用。
先手动给每种怪配置数值,更直观。
32. PlayerHUD.gd 经验完整片段
@onready var level_label: Label = $LevelLabel
@onready var exp_bar: ProgressBar = $ExpBar
@onready var exp_label: Label = $ExpLabel
@onready var level_up_toast: Label = $LevelUpToast
var initialized: bool = false
func setup(player: Node):
if player == null:
return
if player.has_signal("health_changed"):
player.health_changed.connect(_on_player_health_changed)
if player.has_signal("gold_changed"):
player.gold_changed.connect(_on_gold_changed)
if player.has_signal("experience_changed"):
player.experience_changed.connect(_on_experience_changed)
if player.has_signal("level_changed"):
player.level_changed.connect(_on_level_changed)
if "current_hp" in player and "max_hp" in player:
_on_player_health_changed(player.current_hp, player.max_hp)
if "gold" in player:
_on_gold_changed(player.gold)
if "current_exp" in player and "exp_to_next_level" in player:
_on_experience_changed(player.current_exp, player.exp_to_next_level)
if "level" in player:
_on_level_changed(player.level)
initialized = true
func _on_experience_changed(current_exp: int, exp_to_next_level: int):
exp_bar.min_value = 0
exp_bar.max_value = exp_to_next_level
exp_bar.value = current_exp
exp_label.text = "EXP: %d / %d" % [current_exp, exp_to_next_level]
func _on_level_changed(level: int):
level_label.text = "Lv. %d" % level
if initialized:
show_level_up_toast(level)
func show_level_up_toast(level: int):
level_up_toast.text = "升级!Lv. %d" % level
level_up_toast.visible = true
await get_tree().create_timer(2.0).timeout
level_up_toast.visible = false
如果你不想做升级提示,LevelUpToast 可以先不加。
33. Player.gd 经验 + 属性完整整合片段
signal health_changed(current_hp: int, max_hp: int)
signal experience_changed(current_exp: int, exp_to_next_level: int)
signal level_changed(level: int)
signal stats_changed
@export var level: int = 1
@export var current_exp: int = 0
@export var exp_to_next_level: int = 100
@export var base_attack: int = 10
@export var base_defense: int = 0
@export var base_max_hp: int = 100
var final_attack: int = 10
var final_defense: int = 0
var max_hp: int = 100
var current_hp: int = 100
func initialize_player_stats():
recalculate_stats()
current_hp = max_hp
health_changed.emit(current_hp, max_hp)
experience_changed.emit(current_exp, exp_to_next_level)
level_changed.emit(level)
func add_experience(amount: int):
if amount <= 0:
return
current_exp += amount
print("获得经验:", amount)
while current_exp >= exp_to_next_level:
current_exp -= exp_to_next_level
level_up()
experience_changed.emit(current_exp, exp_to_next_level)
func level_up():
level += 1
base_attack += 2
base_defense += 1
base_max_hp += 10
exp_to_next_level = calculate_exp_to_next_level(level)
recalculate_stats()
current_hp = max_hp
level_changed.emit(level)
health_changed.emit(current_hp, max_hp)
print("升级!当前等级:", level)
func calculate_exp_to_next_level(current_level: int) -> int:
return 100 + (current_level - 1) * 50
func recalculate_stats():
final_attack = base_attack
final_defense = base_defense
max_hp = base_max_hp
for item in equipped_items.values():
if item == null:
continue
final_attack += item.attack_bonus
final_defense += item.defense_bonus
max_hp += item.max_hp_bonus
current_hp = min(current_hp, max_hp)
stats_changed.emit()
health_changed.emit(current_hp, max_hp)
这里我把初始化写成了:
initialize_player_stats()
你可以在 _ready() 里调用它。
这样避免 _ready() 过长。
34. Enemy.gd 经验奖励完整片段
signal died
@export var exp_reward: int = 20
var last_attacker: Node = null
var reward_given: bool = false
func take_damage(amount: int, attacker: Node = null):
if current_state == EnemyState.DEAD:
return
last_attacker = attacker
current_hp -= amount
current_hp = clamp(current_hp, 0, max_hp)
health_changed.emit(current_hp, max_hp)
print(name, " 受伤:", amount, " 当前血量:", current_hp)
if current_hp <= 0:
change_state(EnemyState.DEAD)
died.emit()
return
change_state(EnemyState.HURT)
func give_death_rewards():
if reward_given:
return
reward_given = true
give_experience_to_attacker()
drop_loot()
func give_experience_to_attacker():
if last_attacker == null:
return
if last_attacker.has_method("add_experience"):
last_attacker.add_experience(exp_reward)
DEAD 状态中:
func enter_state(state: EnemyState):
match state:
EnemyState.DEAD:
give_death_rewards()
velocity = Vector2.ZERO
attack_area.monitoring = false
detection_area.monitoring = false
hurtbox.monitorable = false
play_animation("dead")
如果没有死亡动画:
func enter_state(state: EnemyState):
match state:
EnemyState.DEAD:
give_death_rewards()
queue_free()
35. 测试经验系统的最小步骤
你可以先不接 UI,只测试 Output。
做:
1. Player 有 add_experience()
2. Enemy 有 exp_reward = 20
3. Player 攻击时传 self
4. Hurtbox.take_damage(amount, attacker)
5. Enemy.take_damage(amount, attacker)
6. Enemy 死亡时 last_attacker.add_experience(exp_reward)
打死史莱姆后应该输出:
获得经验:20
如果没有,先别管 UI。
先把数据闭环跑通。
36. 常见错误 1:怪物死亡不给经验
排查:
Player 攻击时有没有传 self
Hurtbox.take_damage 是否接收 attacker
Enemy.take_damage 是否保存 last_attacker
Enemy 死亡时是否调用 give_death_rewards()
last_attacker 是否有 add_experience 方法
Enemy.exp_reward 是否大于 0
临时打印:
print("last_attacker: ", last_attacker)
如果是 null,说明 attacker 没一路传过来。
37. 常见错误 2:经验增加了但 UI 不动
排查:
Player 是否 emit experience_changed
PlayerHUD 是否连接 experience_changed
PlayerHUD 是否有 ExpBar / ExpLabel
setup(player) 是否调用
节点路径是否正确
临时打印:
func _on_experience_changed(current_exp: int, exp_to_next_level: int):
print("经验 UI 更新:", current_exp, "/", exp_to_next_level)
如果打印了但 UI 不动,是 ProgressBar 路径或显示问题。
如果没打印,是信号没连上。
38. 常见错误 3:升级后经验条异常
排查:
current_exp 是否减去了旧的 exp_to_next_level
level_up 是否更新了新的 exp_to_next_level
add_experience 最后是否 emit experience_changed
while 循环是否正确
正确顺序:
经验增加
如果够升级:
先减掉当前升级所需经验
升级
计算下一等级所需经验
最后刷新经验条
39. 常见错误 4:升级后血条最大值没变
排查:
level_up 是否增加 base_max_hp
level_up 是否调用 recalculate_stats
recalculate_stats 是否更新 max_hp
recalculate_stats 是否 emit health_changed
PlayerHUD 是否监听 health_changed
血条最大值不变,通常是 health_changed 没发。
40. 常见错误 5:升级后装备加成丢了
原因通常是你在升级时直接改了 final 属性。
比如错误写法:
final_attack += 2
然后 recalculate_stats 又覆盖掉。
正确做法:
base_attack += 2
recalculate_stats()
记住:
等级成长改 base 属性
装备加成算进 final 属性
41. 常见错误 6:一次获得大量经验只升一级
说明你用了:
if current_exp >= exp_to_next_level:
应该用:
while current_exp >= exp_to_next_level:
这样才能连续升级。
42. 常见错误 7:初始化时弹“升级”
这是 UI 初始化和真实升级没分开。
解决:
var initialized = false
setup 时先刷新 UI,再:
initialized = true
_on_level_changed() 里:
if initialized:
show_level_up_toast(level)
43. 经验系统和存档
以后存档要保存:
{
"level": 3,
"current_exp": 40,
"base_attack": 14,
"base_defense": 2,
"base_max_hp": 120
}
装备和背包也要保存。
读档后要:
恢复等级和基础属性
恢复装备
调用 recalculate_stats()
刷新 UI
现在先不做存档。
但你要知道,等级系统的数据以后一定要存。
44. 等级系统和任务奖励
任务也可以奖励经验。
QuestData 可以加:
@export var reward_exp: int = 0
QuestManager 的 give_rewards:
if quest.reward_exp > 0 and player.has_method("add_experience"):
player.add_experience(quest.reward_exp)
这样任务完成后除了金币和物品,还能给经验。
第一版可以加上,很实用。
45. QuestData 加 reward_exp
QuestData.gd:
@export var reward_exp: int = 0
QuestManager.gd:
func give_rewards(quest: QuestData, player: Node):
if quest.reward_gold > 0 and player.has_method("add_gold"):
player.add_gold(quest.reward_gold)
if quest.reward_exp > 0 and player.has_method("add_experience"):
player.add_experience(quest.reward_exp)
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)
这样任务系统也接入成长系统了。
46. 任务奖励经验的好处
如果玩家只靠打怪升级,可能会变成刷怪游戏。
任务奖励经验可以鼓励玩家:
探索
对话
收集
完成目标
推进剧情
所以 RPG 里通常:
打怪给少量经验
任务给大量经验
Boss 给大量经验
你可以设计:
史莱姆:20 经验
收集史莱姆凝胶任务:100 经验
这样玩家会愿意完成任务。
47. 当前推荐节点结构
现在 UI 会变成:
UI CanvasLayer
├── PlayerHUD Control
│ ├── HPBar ProgressBar
│ ├── HPLabel Label
│ ├── GoldLabel Label
│ ├── LevelLabel Label
│ ├── ExpBar ProgressBar
│ ├── ExpLabel Label
│ └── LevelUpToast Label
├── InventoryPanel Control
├── EquipmentPanel Control
├── QuestPanel Control
├── ShopPanel Control
├── InteractLabel Label
├── DialogueBox Control
└── DeathPanel Control
Player 现在拥有:
移动
状态机
血量
无敌
攻击
背包
金币
装备
经验
等级
这时 Player.gd 已经会比较长。
后面我们会逐步讲如何拆成组件,比如:
HealthComponent
InventoryComponent
EquipmentComponent
ExperienceComponent
CombatComponent
但现在先让功能跑通。
不要一边搭楼一边重构地基,容易把自己埋进去。
48. 这一部分最重要的记忆点
经验系统核心是 level、current_exp、exp_to_next_level
怪物需要 exp_reward
玩家获得经验用 add_experience()
升级用 level_up()
经验够了要用 while 连续升级
等级成长应该修改 base 属性
装备加成通过 recalculate_stats 算到 final 属性
怪物死亡时通过 last_attacker 给经验
PlayerHUD 监听 experience_changed 和 level_changed
任务也可以奖励经验
49. 你现在的小练习
建议按这个顺序做:
1. Player 添加 level / current_exp / exp_to_next_level
2. Player 添加 experience_changed / level_changed 信号
3. Player 写 add_experience()
4. Player 写 level_up()
5. Player 写 calculate_exp_to_next_level()
6. level_up 时 base_attack +2
7. level_up 时 base_defense +1
8. level_up 时 base_max_hp +10
9. level_up 后 recalculate_stats()
10. Enemy 添加 exp_reward
11. Player 攻击 Hurtbox 时传 self
12. Hurtbox 把 attacker 传给 Enemy
13. Enemy.take_damage 记录 last_attacker
14. Enemy 死亡时给 last_attacker 经验
15. PlayerHUD 添加 LevelLabel / ExpBar / ExpLabel
16. PlayerHUD 监听 experience_changed
17. PlayerHUD 监听 level_changed
18. 打死史莱姆后经验条增加
19. 经验满后玩家升级
20. 升级后攻击力、防御、最大生命提高
21. QuestData 添加 reward_exp
22. 完成任务后也能获得经验
完成这部分之后,你的 RPG 就有了非常重要的成长系统:
打怪 → 获得经验 → 升级 → 属性变强 → 打更强的怪如果您觉得这篇文章有帮助,请点个赞吧~
评论
请登录后发表评论
去登录