让我们通过制作 RPG 来学习 Godot 4 — 第 10 部分:动画 AI 运动
如果敌人只是以静态图像的形式漂浮在我们的地图上,那它有什么用呢?这不行,所以让我们开始着手实现动画,让我们的敌人活过来。我还给你准备了一个惊喜:我们已经编写了大部分敌人的动画代码。嗯,不是真的,我的意思是它仍然在 Player 代码中,但这意味着我们可以继续复制和粘贴一些代码,这极大地加快了我们的开发时间。
您将在本部分中学习到的内容:
· 如何向不可控节点添加动画。
· 进一步练习向量。
在您的 Player 脚本中,我们想要将两个完整的函数复制到我们的 Enemy 脚本中。您应该复制的第一个函数是您的 func player_animations(direction: Vector2)函数,第二个函数是您的func returned_direction(direction: Vector2)函数。将Enemy 脚本中的player_animations()函数重命名为enemy_animations()。
### 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()
请记住,敌人会朝玩家开始攻击时最后已知位置的方向发起攻击,因此,在攻击时,他们转向玩家的新位置时出现延迟是正常的。例如,如果他们开始攻击时你在他们左边,然后突然跑向右边,他们会先完成向左的攻击动画,然后再转向右边。
如果你现在运行场景,你会看到敌人角色空转并按照其当前方向进行动画处理。
您可能会注意到此处的另一个问题:我们的侧面动画从未播放。这是因为我们现有的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
现在我们的敌人将会播放他们的侧面动画。
现在就这些了,因为我们将在接下来的几部分中实现攻击动画。在第 11 部分中,我们将添加我们的敌人生成器场景,这样我们就不必在场景中手动实例化x个敌人。记得保存你的游戏项目,下一部分见。