Godot游戏练习01-第9节-游戏轮次

0 阅读3分钟

今天我给游戏添加了轮次, 大概是关卡的意思, 每一关有一个固定时间, 当玩家杀完轮次内生成的所有敌人, 则完成当前轮次, 游戏进入下一个轮次, 随着轮次增加, 游戏难度也会逐渐增加

看看效果

当前游戏轮次的调整不明显, 主要是增加机制, 看它是否运行正常 (可以观察日志打印)

动画5.gif

大致实现思路

轮次时间控制

在上一次实现的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)

记得处理好服务器逻辑与客户端逻辑

这样我们的轮次结构就完成了, 后续开发中可以基于轮次让游戏内容逐渐产生一些变化, 慢慢变得好玩有趣起来