想象一下,手动将 50 多个敌人添加到我们的场景中需要做多少工作。这不仅看起来很混乱,而且还意味着我们的场景中只有 50 个敌人,如果我们杀死他们所有人,他们就会消失,直到我们重新加载游戏。为了解决这个小难题,我们可以创建一个敌人生成器,它将在整个地图的随机位置以恒定值生成我们的敌人,这意味着我们的敌人永远不会多于或少于我们定义的敌人。每次我们杀死一个敌人并将其移除时,生成器都会生成一个新的敌人,依此类推。

我们还将使我们的生成器足够灵活,以便我们可以在 Inspector 面板中更改生成区域和最大敌人数。这意味着我们可以在主场景中实例化多个生成器,以便在不同区域生成 x 个敌人。这听起来有趣吗?好吧,你还在等什么?让我们生成一些敌人吧!

您将在本部分中学习到的内容:

· 如何使用计时器节点。
· 如何创建场景参考。
· 如何引用其他场景中的节点。
· 如何引用 TileMap 属性。
· 如何使用矩形属性。

首先,让我们从主场景中删除敌人场景的所有实例。我们不会再像这样实例化我们的敌人,因为生成器会处理好这件事。
1.jpg

让我们创建一个以Node2D为根的新场景。这将允许我们将敌人节点绘制到场景中。
2.jpg

将此 Node2D 重命名为“EnemySpawner”并将其保存在您的 Scenes 文件夹下。
3.jpg

我们需要向此场景添加一个 Timer 节点,以便我们每秒可以生成一个敌人 — 除非我们已经达到最大敌人数量。启用 AutoStart,因为我们希望此计时器在游戏开始时立即启动,以便我们开始生成敌人。
4.jpg
5.jpg

我们还需要返回到敌人场景并禁用那里的计时器,因为我们不希望敌人在完成生成之前开始移动。稍后我们将在代码中启动此计时器。
6.jpg

将脚本附加到您的根节点并将其保存在您的脚本文件夹下。
7.jpg

最后,让我们添加一个新的 Node2D 节点,我们将在下面组织生成的敌人。
8.jpg

在此脚本中,我们需要定义一些变量。这些变量将:

· 随机化敌人的生成位置。
· 设置并导出敌人最大值。
· 设置并导出敌人当前的生成数量。
· 设置对我们地图的 tilemap 属性的引用,以便我们的敌人不会出现在某些图层上(例如,我们的水和树叶)。

###EnemySpawner.gd
extends Node2D
# Node refs
@onready var spawned_enemies = $SpawnedEnemies
@onready var tilemap = get_tree().root.get_node("Main/Map") 
# Enemy stats
@export var max_enemies = 20 
var enemy_count = 0 
var rng = RandomNumberGenerator.new()

在我们的全局脚本中,我们还将加载对敌人场景的引用。

### Global.gd
extends Node
# Scene resources
@onready var pickups_scene = preload("res://Scenes/Pickup.tscn")
@onready var enemy_scene = preload("res://Scenes/Enemy.tscn")

为了生成敌人,我们需要创建一个函数,该函数将在游戏开始时生成现有敌人,​​并在计时器超时时生成其余敌人。我们可以将函数称为spawn_enemy()。在此函数中,我们将创建对敌人场景的引用实例。当我们创建场景实例时,它会将其保存为资源,而无需每次调用​​时都从磁盘加载它。这节省了空间和时间。

###EnemySpawner.gd
# older code
# ----------------------- Spawning ------------------
func spawn_enemy():
    var enemy = Global.enemy_scene.instantiate()

现在我们有了敌人场景的实例,我们可以通过add_child()方法将敌人添加为节点层次结构的子节点。简而言之,我们将敌人从敌人场景添加到生成器的场景树中。

###EnemySpawner.gd
# older code

# --------------------------------- Spawning ------------------------------------
func spawn_enemy():
    var enemy = Global.enemy_scene.instantiate()
    spawned_enemies.add_child(enemy)

如果你从逻辑上思考,你就会知道你不能随便在任意位置生成敌人。如果我们的游戏在建筑物下生成敌人怎么办?或者在水中生成敌人怎么办?我们将无法接近敌人,因此为了防止发生此问题,我们必须创建一个函数来定义敌人的有效生成位置。我们之前在 Pickups 生成器中做过这个。现在,根据你添加到 Tilemap 的图层,下一部分可能与你这边的有所不同。

在我们添加地图的部分,我们创建了以下图层:
9.jpg

正如您所知,我们为每个图层分配了一个 ID。我们从 0 开始计数,这意味着water == 0, grass == 1, sand == 2,foliage ==3。我们只希望敌人出现在草(1)或沙子(2)图层上。我们不希望其他图层(0、3)出现的地方出现它们。

由于我们将在 Pickup 生成功能和 Enemy 生成中引用这些层,因此让我们在 Global 脚本中重新定义 Main 脚本中的层常量。然后,我们还需要在 Main 脚本中正确引用这些常量。

### Global.gd
extends Node
# Scene resources
@onready var pickups_scene = preload("res://Scenes/Pickup.tscn")
@onready var enemy_scene = preload("res://Scenes/Enemy.tscn")
# Pickups
enum Pickups { AMMO, STAMINA, HEALTH }
# TileMap layers
const WATER_LAYER = 0
const GRASS_LAYER = 1
const SAND_LAYER = 2
const FOLIAGE_LAYER = 3
const EXTERIOR_1_LAYER = 4
const EXTERIOR_2_LAYER = 5
### Main.gd
# older code
# --------------------- Pickup spawning ------------------
# Valid pickup spawn location
func is_valid_spawn_location(layer, position):
    var cell_coords = Vector2(position.x, position.y)    
    # Check if there's a tile on the water, foliage, or exterior layers
    if map.get_cell_source_id(Global.WATER_LAYER, cell_coords) != -1 || map.get_cell_source_id(Global.FOLIAGE_LAYER, cell_coords) != -1 || map.get_cell_source_id(Global.EXTERIOR_1_LAYER, cell_coords) != -1  || map.get_cell_source_id(Global.EXTERIOR_2_LAYER, cell_coords) != -1:
        return false    
    # Check if there's a tile on the grass or sand layers
    if map.get_cell_source_id(Global.GRASS_LAYER, cell_coords) != -1 ||  map.get_cell_source_id(Global.SAND_LAYER, cell_coords) != -1:
        return true    
    return false

现在在我们的 EnemySpawner 脚本中,让我们从主脚本中复制并粘贴整个is_valid_spawn_location()函数。用 tilemap 替换变量map。

###EnemySpawner.gd
# --------------------------------- Spawning ------------------------------------
# older code
# Valid spawn location
func is_valid_spawn_location(layer, position):
    var cell_coords = Vector2(position.x, position.y)
    # Check if there's a tile on the water, foliage, or exterior layers
    if tilemap.get_cell_source_id(Global.WATER_LAYER, cell_coords) != -1 || tilemap.get_cell_source_id(Global.FOLIAGE_LAYER, cell_coords) != -1 || tilemap.get_cell_source_id(Global.EXTERIOR_1_LAYER, cell_coords) != -1 || tilemap.get_cell_source_id(Global.EXTERIOR_2_LAYER, cell_coords) != -1:
        return false
    # Check if there's a tile on the grass or sand layers
    if tilemap.get_cell_source_id(Global.GRASS_LAYER, cell_coords) != -1 ||  tilemap.get_cell_source_id(Global.SAND_LAYER, cell_coords) != -1:
        return true
    return false

现在,在我们的 spawn_enemy 函数中,我们需要随机选择地图上的一个位置。然后,我们需要使用 is_valid_spawn_location 函数检查该位置是否是有效的生成位置。如果有效,我们将在该位置生成敌人。如果无效,我们将尝试另一个随机位置。这再次与我们生成拾取物时所做的类似。

###EnemySpawner.gd
# --------------------------------- Spawning ------------------------------------
func spawn_enemy():
    var attempts = 0
    var max_attempts = 100  # Maximum number of attempts to find a valid location
    var spawned = false

    while not spawned and attempts < max_attempts:
        # Randomly select a position on the map
        var random_position = Vector2(
            rng.randi() % tilemap.get_used_rect().size.x,
            rng.randi() % tilemap.get_used_rect().size.y
        )
        # Check if the position is a valid spawn location
        if is_valid_spawn_location(Global.GRASS_LAYER, random_position) || is_valid_spawn_location(Global.SAND_LAYER, random_position):
            # Spawn enemy
            var enemy = Global.enemy_scene.instantiate()
            enemy.position = tilemap.map_to_local(random_position) + Vector2(16, 16) / 2
            spawned_enemies.add_child(enemy)
            spawned = true
        else:
            attempts += 1
    if attempts == max_attempts:
        print("Warning: Could not find a valid spawn location after", max_attempts, "attempts.")

接下来,我们需要生成敌人。我们可以在 Timer 节点的timeout()信号中执行此操作。将信号连接到场景根,您将看到func _on_timer_timeout():函数添加到脚本末尾。
10.jpg

如果敌人数量不超过最大敌人数量,我们将调用spawn_enemies函数。这将每 1 秒生成一个敌人,直到生成允许的最大敌人数量。

###EnemySpawner.gd
# --------------------------------- Spawning ------------------------------------
# Spawn enemy
func _on_timer_timeout():
    if enemy_count < max_enemies:
        spawn_enemy()
        enemy_count = enemy_count + 1

现在如果你在主场景中创建 EnemySpawner 场景的实例(确保主场景中没有敌人场景),然后运行游戏,你会注意到敌人出现了。
11.jpg

如果您不想让敌人出现在场景中,只需在 Inspector 面板中将您的生成器值设置为 0。
12.jpg
13.jpg

确保 TileMap 节点的变换属性设置为 (0,0) — 否则你的敌人将会以偏移量生成!
14.jpg

拾取生成器

趁此机会,让我们创建一个包含 PickupSpawner 的新场景。这将使我们的项目更具动态性和可重复使用性。重新创建您为 EnemySpawner 采取的步骤(创建新场景、添加节点、连接脚本、连接超时信号)。请不要向此场景添加计时器节点
15.jpg
16.jpg
17.jpg

现在,在您的 PickupSpawner.gd 脚本中,将您在主脚本中添加的代码复制到您新创建的脚本中。

### PickupSpawner.gd
extends Node2D
# Node refs
@onready var map = get_tree().root.get_node("Main/Map")
@onready var spawned_pickups = $SpawnedPickups
var rng = RandomNumberGenerator.new()
func _ready():
    # Spawn between 5 and 10 pickups
    var spawn_pickup_amount = rng.randf_range(5, 10)
    spawn_pickups(spawn_pickup_amount)  

# ------------------------ Pickup spawning ------------
# Valid pickup spawn location
func is_valid_spawn_location(layer, position):
    var cell_coords = Vector2(position.x, position.y)
    # Check if there's a tile on the water, foliage, or exterior layers
    if map.get_cell_source_id(Global.WATER_LAYER, cell_coords) != -1 || map.get_cell_source_id(Global.FOLIAGE_LAYER, cell_coords) != -1 || map.get_cell_source_id(Global.EXTERIOR_1_LAYER, cell_coords) != -1 || map.get_cell_source_id(Global.EXTERIOR_2_LAYER, cell_coords) != -1:
        return false
    # Check if there's a tile on the grass or sand layers
    if map.get_cell_source_id(Global.GRASS_LAYER, cell_coords) != -1 ||  map.get_cell_source_id(Global.SAND_LAYER, cell_coords) != -1:
        return true
    return false
# Spawn pickup
func spawn_pickups(amount):
    var spawned = 0
    var attempts = 0
    var max_attempts = 1000  # Arbitrary number, adjust as needed
    while spawned < amount and attempts < max_attempts:
        attempts += 1
        var random_position = Vector2(randi() % map.get_used_rect().size.x, randi() % map.get_used_rect().size.y)
        var layer = randi() % 2  
        if is_valid_spawn_location(layer, random_position):
            var pickup_instance = Global.pickups_scene.instantiate()
            pickup_instance.item = Global.Pickups.values()[randi() % 3]
            pickup_instance.position = map.map_to_local(random_position) + Vector2(16, 16) / 2
            spawned_pickups.add_child(pickup_instance)
            spawned += 1

然后,在主场景中删除 SpawnedPickups 节点,并实例化 PickupSpawner。您的主脚本应如下所示。
18.jpg

### Main.gd
extends Node2D

现在,当你运行场景时,你的拾取物和敌人应该会出现!如果你靠近敌人,敌人应该仍然会四处游荡并追逐玩家。
19.jpg
接下来,我们将为玩家添加射击和伤害敌人的功能。记得保存你的游戏项目,下一部分见。

本节代码下载。

标签: Godot, 2D游戏, 游戏开发, 游戏自学

添加新评论