今天我给游戏添加了轮次, 大概是关卡的意思, 每一关有一个固定时间, 当玩家杀完轮次内生成的所有敌人, 则完成当前轮次, 游戏进入下一个轮次, 随着轮次增加, 游戏难度也会逐渐增加
看看效果
当前游戏轮次的调整不明显, 主要是增加机制, 看它是否运行正常 (可以观察日志打印)
大致实现思路
轮次时间控制
在上一次实现的EnemySpawnComponent组件基础上新增一个RoundTimer, 用于轮次计时
并且新增一些变量, 用于轮次的时间/参数调整
const BASE_ROUND_TIME: float = 10
const ROUND_TIME_GROWTH: float = 5
const BASE_MIN_SPAWN_INTERVAL: float = 2.0
const BASE_MAX_SPAWN_INTERVAL: float = 5.0
const SPAWN_INTERVAL_GROWTH: float = -0.2
var round_count: int = 0
var round_min_spawn_interval: float = BASE_MIN_SPAWN_INTERVAL
var round_max_spawn_interval: float = BASE_MAX_SPAWN_INTERVAL
var enemy_count: int = 0
新增的常量和变量名都比较有描述性
- 轮次初始时间为10秒, 每完成一个轮次, 下次增加5秒时间
- 初始的敌人生成随机最小最大间隔为2-5秒, 每增加一个轮次, 间隔时间的最小最大值均减少0.2秒 (敌人生成加快)
- 当前轮次计数, 时间与配置基于轮次数调整
- 记录敌人数量, 依据敌人数量判断轮次是否完成
轮次开启与结束
开启时计数加1, 并且调整Timer时间与参数, 同时开启敌人生成Timer
在ready中调用_start_round自动开启第一个轮次
func _start_round() -> void:
round_count += 1
print("Round %s start" % round_count)
round_min_spawn_interval = BASE_MIN_SPAWN_INTERVAL + (round_count - 1) * SPAWN_INTERVAL_GROWTH
round_max_spawn_interval = BASE_MAX_SPAWN_INTERVAL + (round_count - 1) * SPAWN_INTERVAL_GROWTH
round_timer.start(BASE_ROUND_TIME + (round_count - 1) * ROUND_TIME_GROWTH)
spawn_timer.start(randf_range(round_min_spawn_interval, round_max_spawn_interval))
轮次的结束是round_timer计时停止, 停止后也同时停止敌人生成计时
func _on_round_timer_timeout() -> void:
print("Round %s end" % round_count)
spawn_timer.stop()
_check_round_completed()
轮次完成检测
判断轮次完成有两个条件
- 轮次计时完毕
- 该轮次的敌人被全部消灭
func _check_round_completed() -> void:
if !round_timer.is_stopped():
return
if enemy_count == 0:
print("Round %s completed!" % round_count)
_start_round()
若检测到轮次完成, 则自动开始下一轮次
在轮次Timer结束时已经进行过一次检测, 还应该在每个敌人死亡时也进行计数和检测
func _on_enemy_died() -> void:
enemy_count -= 1
_check_round_completed()
这就涉及到一个问题, 在该组件中怎样获取到敌人死亡的信号?
全局信号总线
有人坚决反对使用信号总线, 说这不符合Godot的设计哲学
但是我会以务实为主, 我认为信号总线应该在必要时使用, 但要避免乱用
在这里, 敌人实例与该组件实例不是简单的上下级或者兄弟级, 如果通过祖先节点传递信号, 会相当麻烦
我认为在这里使用信号总线是非常合适的
使用Autoload实现一个全局信号总线
新建一个脚本, 在ProjectSettings中的Globals中加载并启用, 之后就可以全局访问
extends Node
signal enemy_died
func emit_enemy_died() -> void:
enemy_died.emit()
脚本很简单, 主要是为了跨模块访问
敌人死亡通知
敌人的生命管理被我用一个HealthComponent组件管理了, 与之前的逻辑一样, 生命值归0之后发出信号, 封装组件都是为了方便后续复用
在生命值归0后发出全局信号, 并销毁自身
func _on_health_ended() -> void:
GameEvents.emit_enemy_died()
queue_free()
全局信号监听
与普通信号监听一样, 只是我们不用再找信号的源头是谁
GameEvents.enemy_died.connect(_on_enemy_died)
记得处理好服务器逻辑与客户端逻辑
这样我们的轮次结构就完成了, 后续开发中可以基于轮次让游戏内容逐渐产生一些变化, 慢慢变得好玩有趣起来