如果没有某种威胁或敌人需要击败,游戏就不算完整。在我们的游戏中,我们将有一个仙人掌敌人,它会以恒定值在地图上生成,这意味着在游戏循环期间,地图上的敌人数量永远不会多于或少于 x。如果玩家触碰这个敌人,它会伤害我们的玩家,它也会向我们的玩家射击。让我们开始介绍我们的敌人 AI。

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

· 如何向不可控节点添加运动。
· 如何使用计时器节点。
· 如何使用 RandomNumberGenerator 类。
· 如何使节点随机移动,以及如何向其他节点移动。

1.jpg
图 13:敌人概况

敌人场景设置

我们的敌人场景将具有与玩家场景相同的结构,以 CharacterBody2D 作为其根节点,后跟 AnimatedSprite2D 节点和 CollisionShape2D 节点。因此,我们可以继续复制我们的玩家场景并将其重命名为“敌人”或“仙人掌”。我只打算用仙人掌作为敌人,所以我将我的仙人掌称为通用术语“敌人”,但如果您要有多个不同的敌人,您可以为“仙人掌”、“匪徒”、“风滚草”等创建一个场景。
2.jpg
3.jpg

将场景根重命名为您所需的任何名称(例如敌人),然后将玩家脚本从场景中分离出来。
4.jpg

您还应该删除 Camera2D 节点,因为我们不会跟踪此节点,并且断开on_animation_finished()信号与 AnimatedSprite2D 节点的连接。您的最终场景应如下图所示。
5.jpg

必须不断监视此敌人场景以更新其行为。例如,假设我们希望它漫游 1 分钟,1 分钟后,我们希望它停止 30 秒,然后重新定向并再次漫游。我们可以使用Timer节点,该节点将在倒计时指定间隔直至达到 0 后发出其内置的timeout() 信号。因此,每次计时器超时,敌人都应该重新定向并再次漫游。
6.jpg

您的计时器有两个选项:一次性(如果为 true,计时器将在到达 0 时停止。如果为 false,它将重新启动)和自动启动(如果为 true,计时器将在进入场景树时自动启动)。我们希望此计时器在游戏开始时立即启动,因为我们想使用它在一定时间后更新敌人的动作。因此,您需要在 Timer 节点的 Inspector 面板中启用 Autostart 属性。
7.jpg

稍后,当我们为敌人的漫游添加功能时,我们将回到此计时器节点。现在,让我们像为玩家设置动画一样设置敌人的动画。我们已经为敌人设置了所有动画名称,因为他们将能够完全按照玩家所做的那样做,我们只需切换动画帧即可。
8.jpg

为此,您需要删除动画中现有的精灵帧。对所有动画执行此操作,但不要删除任何动画。
9.jpg

让我们从 attack_down 动画开始。选择“从精灵表添加新精灵帧”选项,然后在 Assets > Mobs > Cactus 目录中,您将找到所有敌人的精灵表。对于我们的 attack_down 动画,我们将使用“Cactus Front Sheet.png”表来创建动画。
10.jpg
11.jpg

我们水平计算 11 帧,垂直计算 4 帧,因此请相应地更改您的数字以正确裁剪帧。对于向下攻击,我们将使用第三行的帧。
12.jpg

我挑战你现在使用下表作为指南,自行添加其余动画。
13.jpg
14.jpg

调动敌人

现在我们希望能够自主移动敌人。为此,我们需要创建一些变量来存储敌人的方向,以及经过一段随机时间后的新方向。我们不会将其设置为等待 1 分钟后再重定向的固定时间,而是将该值随机化。我们还希望我们的敌人在与物体碰撞后重定向。

让我们将一个新脚本附加到敌人场景并将其保存在脚本文件夹下。
15.jpg

我们的敌人的移动方式与玩家的移动方式类似,因此让我们从 Player.gd 脚本中添加一些熟悉的变量来捕捉其移动速度和新方向。我们还需要创建一个变量来存储其当前方向。

### Enemy.gd

extends CharacterBody2D

# Enemy movement speed
@export var speed = 50

#it’s the current movement direction of the cactus enemy.
var direction : Vector2

#direction and animation to be updated throughout game state
var new_direction = Vector2(0,1) #only move one spaces

当计时器在随机倒计时结束后用完时,我们的方向就会改变。我们将使用RandomNumberGenerator类生成此随机倒计时值。顾名思义,这是一个用于生成伪随机数的类。new() 方法用于从类创建对象。

### Enemy.gd

# older code

# RandomNumberGenerator to generate timer countdown value 
var rng = RandomNumberGenerator.new()

#timer reference to redirect the enemy if collision events occur & timer countdown reaches 0
var timer = 0

如果敌人在一定半径内发现我们的玩家,我们还需要将敌人移向玩家,所以让我们添加对玩家场景的引用。

### Enemy.gd

extends CharacterBody2D

# older code

#player scene ref
var player

现在我们已经定义了变量,我们可以继续在内置的ready()函数中初始化随机数和玩家引用,因为我们希望这些对象在我们的敌人场景进入主场景时立即初始化。

我们将把玩家引用连接到主场景中的玩家节点。由于敌人场景也将在主场景中实例化 - 因此与玩家场景共享场景树,我们可以通过 get_tree ().root.get_node 方法获取玩家。Main是我们的主场景,而/Player是我们主场景中的玩家实例。

### Enemy.gd

extends CharacterBody2D

# Node refs
@onready var player = get_tree().root.get_node("Main/Player")

# Enemy stats
@export var speed = 50
var direction : Vector2 # current direction
var new_direction = Vector2(0,1) # next direction

# Direction timer
var rng = RandomNumberGenerator.new()
var timer = 0

func _ready():
    rng.randomize()

接下来,让我们添加敌人的移动代码。敌人移动的编码过程与玩家的类似。首先,我们将添加代码来移动它们。然后,我们只需添加计时器来重定向敌人,并在他们“看到”我们时将他们移向玩家。完成这些之后,我们将在此基础上根据他们的移动方向改变他们的动画。在这一部分中,我们将添加重定向和移动,但还不添加动画。
接下来,让我们添加敌人的移动代码。敌人移动的编码过程与玩家的类似。首先,我们将添加代码来移动它们。然后,我们只需添加计时器来重定向敌人,并在他们“看到”我们时将其移向玩家。完成这些事情后,我们将在此基础上根据他们的移动方向更改他们的动画。在这一部分中,我们将添加重定向和移动,但暂时不添加动画。

我们还将在physics_process()函数中添加敌人的移动代码,因为我们将与节点的移动和物理相关的所有内容都放在此代码中。让我们首先通过move_and_collide方法为它们添加移动功能,就像我们为玩家所做的那样。

### Enemy.gd

# older code

# Apply movement to the enemy
func _physics_process(delta):
    var movement = speed * direction * delta
    var collision = move_and_collide(movement)

现在,我们需要将 Timer 节点的timeout()信号连接到我们的脚本。当我们的计时器达到 0 时,此信号将发出。您会看到它在脚本末尾创建了一个func _on_timer_timeout():函数。
16.jpg

在这个超时函数中,我们需要做几件事。首先,我们需要计算玩家相对于敌人的位置。我们可以通过访问节点的变换值(位置、旋转和比例)找到以 Vector(0,0) 值返回的玩家位置。了解了这一点,我们可以通过简单地说player.position来访问玩家的位置——如果我们想要他们的旋转,我们可以说player.rotation.x ,等等。我们还可以通过简单地说position或self.position来访问当前节点的位置(即我们的敌人节点)。

获得玩家位置后,我们需要用敌人位置减去玩家位置,得到玩家与敌人之间的距离。让我们继续获取这个值。

### Enemy.gd

# older code

# ------------------------- Movement & Direction ---------------------
# Apply movement to the enemy
func _physics_process(delta):
    var movement = speed * direction * delta
    var collision = move_and_collide(movement)

func _on_timer_timeout():
    # Calculate the distance of the player relative position to the enemy's            position
    var player_distance = player.position - position

现在,如果距离敌人 20 像素以内,则意味着敌人距离玩家足够近,因此无需追逐他们,而是可以继续“攻击”或“交战”玩家。您可以将此视线值设为任意数字,但我将使用 20 像素。

### Enemy.gd

# older code

func _on_timer_timeout():
    # Calculate the distance of the player relative position to the enemy's position
    var player_distance = player.position - position
    #turn towards player so that it can attack if within radius
    if player_distance.length() <= 20:
        new_direction = player_distance.normalized()

如果它们距离敌人 100 像素以内,并且计时器已用完,则意味着敌人距离玩家不够近,无法攻击玩家,因此它们必须向玩家移动并开始追逐玩家。您可以将此追逐值设为任意数字,但我将使用 100 像素。

### Enemy.gd

# older code

func _on_timer_timeout():
    # Calculate the distance of the player relative position to the enemy's position
    var player_distance = player.position - position
    #turn towards player so that it can attack if within radius
    if player_distance.length() <= 20:
        new_direction = player_distance.normalized()
    #chase/move towards player to attack them
    elif player_distance.length() <= 100 and timer == 0:
        direction = player_distance.normalized()

否则,如果玩家离敌人不近,或者不在我们的追逐半径内,那么我们的敌人就可以随意漫游。敌人的方向将通过Vector.DOWN.rotate方法随机计算,该方法将计算 0 到 360° 之间的随机角度。每次计时器超时时,此方向都会改变。

### Enemy.gd

# older code

func _on_timer_timeout():
    # Calculate the distance of the player relative position to the enemy's position
    var player_distance = player.position - position
    #turn towards player so that it can attack if within radius
    if player_distance.length() <= 20:
        new_direction = player_distance.normalized()
    #chase/move towards player to attack them
    elif player_distance.length() <= 100 and timer == 0:
        direction = player_distance.normalized()
    #random roam
    elif timer == 0:
        #this will generate a random direction value
        var random_direction = rng.randf()
        #This direction is obtained by rotating Vector2.DOWN by a random angle.
        if random_direction < 0.05:
            #enemy stops
            direction = Vector2.ZERO
        elif random_direction < 0.1:
            #enemy moves
            direction = Vector2.DOWN.rotated(rng.randf() * 2 * PI)

最后,我们需要使用来自physics_process()函数的碰撞器变量来查看敌人是否与玩家发生碰撞,并添加计时器范围以进行随机化。如果它们与玩家发生碰撞,则需要将计时器设置为 0 以触发timeout()函数,这样敌人就会追逐我们。

如果它们没有与我们的玩家发生碰撞,我们需要设置计时器随机器值以及随机化它们的方向旋转值,以便它们在与其他物体碰撞时可以转身。此旋转角度是使用randf_range()函数获得的。此角度的值介于 45° 到 90° 之间。如果您愿意,可以更改这些值以使其更平滑或更清晰。

### Enemy.gd

# older code

# Apply movement to the enemy
func _physics_process(delta):
    var movement = speed * direction * delta
    var collision = move_and_collide(movement)

    #if the enemy collides with other objects, turn them around and re-randomize the timer countdown
    if collision != null and collision.get_collider().name != "Player":
        #direction rotation
        direction = direction.rotated(rng.randf_range(PI/4, PI/2))
        #timer countdown random range
        timer = rng.randf_range(2, 5)
    #if they collide with the player 
    #trigger the timer's timeout() so that they can chase/move towards our player
    else:
        timer = 0

如果你在主场景中实例化敌人场景,并运行它,那么他们就会追逐你或四处游荡。
17.jpg
18.jpg

这样,我们就添加了一个对我们没有任何威胁的敌人。我们的敌人还没有动画或任何价值,但这些将在接下来的几部分中实现。在下一节中,我们将添加为敌人的移动添加动画的功能。幸运的是,我们已经添加了动画,因此只需快速设置即可在敌人的移动中显示这些动画!记得保存你的游戏项目,下一部分见。

本节代码下载。

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

添加新评论