如果敌人只是以静态图像的形式漂浮在我们的地图上,那它有什么用呢?这不行,所以让我们开始着手实现动画,让我们的敌人活过来。我还给你准备了一个惊喜:我们已经编写了大部分敌人的动画代码。嗯,不是真的,我的意思是它仍然在 Player 代码中,但这意味着我们可以继续复制和粘贴一些代码,这极大地加快了我们的开发时间。

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

· 如何向不可控节点添加动画。
· 进一步练习向量。

在您的 Player 脚本中,我们想要将两个完整的函数复制到我们的 Enemy 脚本中。您应该复制的第一个函数是您的 func player_animations(direction: Vector2)函数,第二个函数是您的func returned_direction(direction: Vector2)函数。将Enemy 脚本中的player_animations()函数重命名为enemy_animations()。
1.jpg

### Enemy.gd

extends CharacterBody2D

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

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

# older code

# Animation Direction
func returned_direction(direction : Vector2):
    #it normalizes the direction vector to make sure it has length 1 (1, or -1 up, down, left, and right) 
    var normalized_direction  = direction.normalized()
    var default_return = "side"
    
    if normalized_direction.y > 0:
        return "down"
    elif normalized_direction.y < 0:
        return "up"
    elif normalized_direction.x > 0:
        #(right)
        $AnimatedSprite2D.flip_h = false
        return "side"
    elif normalized_direction.x < 0:
        #flip the animation for reusability (left)
        $AnimatedSprite2D.flip_h = true
        return "side"
        
    #default value is empty
    return default_return
    
# Animations
func enemy_animations(direction : Vector2):
    #Vector2.ZERO is the shorthand for writing Vector2(0, 0).
    if direction != Vector2.ZERO:
        #update our direction with the new_direction
        new_direction = direction
        #play walk animation, because we are moving
        animation = "walk_" + returned_direction(new_direction)
        animation_sprite.play(animation)
    else:
        #play idle animation, because we are still
        animation  = "idle_" + returned_direction(new_direction)
        animation_sprite.play(animation)

要为敌人的移动激活这些动画,我们必须首先检查是否有其他动画正在播放(例如攻击或死亡动画),如果没有,我们将在physics_process()函数中播放它们。我们再次为玩家角色做了同样的事情,所以这对您来说应该不会太难理解。

从玩家代码中复制is_attacking变量。

### Enemy.gd

extends CharacterBody2D

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

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

然后在physics_process()函数中,如果敌人没有攻击,我们就调用enemy_animations()函数来播放敌人的动画。

### 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)

    #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
    #plays animations only if the enemy is not attacking
    if !is_attacking:
        enemy_animations(direction)

如果你要从在这里,你可能会注意到敌人会试图攻击你的玩家——但问题是,即使他们背对着我们的玩家,他们也会试图攻击你的玩家。
如果你从这里运行你的敌人场景,你可能会注意到敌人会试图攻击你的玩家 - 但问题是,即使他们背对着我们的玩家,他们也会试图攻击你的玩家。

在我们的timeout()函数中,我们为动画设置了 new_direction,但这并没有准确更新我们的 new_direction。为了解决这个问题,我们需要确保在攻击期间准确设置我们的 new_direction,并且它与敌人所面对的方向同步。

为此,我们将创建一个新函数,将我们的 new_direction 与敌人的实际移动方向同步。然后,每当敌人移动或旋转时,我们都会调用此函数。这将确保我们的 new_direction 准确地表示敌人攻击时所面对的方向。

让我们创建一个新函数来同步我们的 new_direction。您可以在 timeout() 函数上方执行此操作。

### Enemy.gd

#older code 

#syncs new_direction with the actual movement direction and is called whenever the enemy moves or rotates
func sync_new_direction():
    if direction != Vector2.ZERO:
        new_direction = direction.normalized()

然后,我们将在func _on_timer_timeout():函数中调用此函数,这是敌人决定其行为(是否应该攻击、追逐或随机漫游)的地方。这确保每当敌人更新其行为时,它也会相应地更新其动画方向。

### Enemy.gd

# older code

# -------------- Movement & Direction -----------
func _on_timer_timeout():
    # Calculate the distance of the player's relative position to the enemy's position
    var player_distance = player.position - position
    #turn towards player so that it can attack
    if player_distance.length() <= 20:
        new_direction = player_distance.normalized()
        sync_new_direction() 
        direction = Vector2.ZERO
    #chase/move towards player to attack them
    elif player_distance.length() <= 100 and timer == 0:
        direction = player_distance.normalized()    
        sync_new_direction()    
    #random roam radius
    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 between 0 and 2π radians (0 to 360°). 
            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)
            sync_new_direction()

请记住,敌人会朝玩家开始攻击时最后已知位置的方向发起攻击,因此,在攻击时,他们转向玩家的新位置时出现延迟是正常的。例如,如果他们开始攻击时你在他们左边,然后突然跑向右边,他们会先完成向左的攻击动画,然后再转向右边。

如果你现在运行场景,你会看到敌人角色空转并按照其当前方向进行动画处理。
2.jpg

您可能会注意到此处的另一个问题:我们的侧面动画从未播放。这是因为我们现有的returned_direction()函数首先检查 y 方向,然后检查 x 方向。这意味着如果有任何 y 方向的移动,它将始终优先考虑上下动画而不是侧面动画。为了解决这个问题,我们应该在 x 方向占主导地位时优先考虑它:

### Enemy.gd

# older code

# ------------------------- Movement & Direction ---------------------
# Animation Direction
func returned_direction(direction : Vector2):
    var normalized_direction  = direction.normalized()
    var default_return = "side"
    if abs(normalized_direction.x) > abs(normalized_direction.y):
        if normalized_direction.x > 0:
            #(right)
            $AnimatedSprite2D.flip_h = false
            return "side"
        else:
            #flip the animation for reusability (left)
            $AnimatedSprite2D.flip_h = true
            return "side"
    elif normalized_direction.y > 0:
        return "down"
    elif normalized_direction.y < 0:
        return "up"
    #default value is empty
    return default_return

现在我们的敌人将会播放他们的侧面动画。
3.jpg

现在就这些了,因为我们将在接下来的几部分中实现攻击动画。在第 11 部分中,我们将添加我们的敌人生成器场景,这样我们就不必在场景中手动实例化x个敌人。记得保存你的游戏项目,下一部分见。

本节代码下载。

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

添加新评论