[Перевод] Вражеский ИИ: преследование игрока без Navigation2D и поиска пути A*
Создаёте игру, в которой враги должны преследовать игрока? Всё начинается с простого — заставим врага бежать к игроку. Но что произойдёт, если он находится за деревом, или за углом стены? Ну, теперь враг будет выглядеть довольно глупо — упрётся в объект, перебирая ногами на месте. Не очень хорошо!
Чтобы решить эту проблему, можно использовать встроенные в Godot ноды Navigation2D или AStar (вот туториал GDQuest, посвящённый обоим нодам). Но в Helms of Fury мы использовали другой способ, который отлично подошёл нашей игре, и мы хотим поделиться им в этом туториале. Вот как это выглядит:
Приступаем к работе
Будем считать, что вы создаёте врагов как объекты KinematicBody2D и используете для управления их состояниями конечный автомат. Не знаете, что такое конечный автомат? Мне нравится эта статья с объяснением конечных автоматов, а также способов их использования. Вот ещё одна статья о реализации простых конечных автоматов в Godot.
Начнём с простого состояния Chase для глупого врага, который просто бежит к своей цели и застревает где-нибудь на пути:
# ChaseState.gd
func _init(enemy, params):
enemy.dir = (enemy.target.position - enemy.position).normalized()
func _physics_process(delta):
var motion = enemy.dir * enemy.speed
enemy.move_and_slide(motion)
Следы запаха
Чтобы усовершенстовать состояние, мы заставим игрока оставлять при движении след из его предыдущих позиций. Благодаря этому, когда враг не будет видеть игрока, он будет проверять, можно ли увидеть какие-то из его прошлых позиций, а затем следовать по ним к игроку. Так как это похоже на то, как берёт след собака, мы назовём это следом запаха.
Чтобы след запаха работал, нам нужно добавить к игроку нод Timer, включить его автоматический запуск и задать wait_time (мы использовали 0.1s), а затем добавить код, чтобы при завершении отсчёта игрок оставлял запах.
# Player.gd
extends KinematicBody2D
const scent_scene = preload("res://Player/Scent.tscn")
var scent_trail = []
func _ready():
$ScentTimer.connect("timeout", self, "add_scent")
func add_scent():
var scent = scent_scene.instance()
scent.player = player
scent.position = player.position
Game.level.effects.add_child(scent)
scent_trail.push_front(scent)
Затем нужно добавить сам оставляемый Scent.tscn. Это простая сцена Node2D, содержащая Timer, чтобы срок действия запаха заканчивался.
# Player.gd
extends KinematicBody2D
const scent_scene = preload("res://Player/Scent.tscn")
var scent_trail = []
func _ready():
$ScentTimer.connect("timeout", self, "add_scent")
func add_scent():
var scent = scent_scene.instance()
scent.player = player
scent.position = player.position
Game.level.effects.add_child(scent)
scent_trail.push_front(scent)
Если вы хотите, чтобы запахи были видимы при отладке, то можно добавить к ним нод ColorRect, а затем просто скрыть его. Сделав это, вы увидите, как при беге за игроком остаётся след запаха.
Теперь нам нужно пробудить во врагах внутренних ищеек, чтобы они следовали за этими новыми запахами, когда не видят игрока. Но для этого нам понадобится добавить врагам нод RayCast2D, и настроить Physics Layers в Godot таким образом, чтобы луч знал, с чем он может выполнять коллизии.
Physics Layers
Чтобы настроить Physics Layers в Godot, нужно нажать в верхнем меню на Project, а затем на Project Settings, затем перейти к разделу Layer Names в левом нижнем углу, а затем выбрать 2D Physics.
Дайте им любое подходящее название. После этого перейдите к объектам в игре и в боковой панели Property Inspector разверните Collision, а затем нажмите ··, чтобы назначить их. Для объектов их нужно назначать как Layers.
Назначив объектам Physics Layers, нужно изменить RayCast2D врагов так, чтобы он проверял те слои, через которые не могут двигаться враги (в нашем случае это solid, object, crate, hole, gate_closed).
После настройки Physics Layers последним шагом будет изменение состояния Chase.
# ChaseState.gd
func _init(enemy, params):
chase_target()
func chase_target():
var look = enemy.get_node("RayCast2D")
look.cast_to = (enemy.target.position - enemy.position)
look.force_raycast_update()
# if we can see the target, chase it
if !look.is_colliding():
enemy.dir = look.cast_to.normalized()
# or chase first scent we can see
else:
for scent in enemy.target.scent_trail:
look.cast_to = (scent.position - enemy.position)
look.force_raycast_update()
if !look.is_colliding():
enemy.dir = look.cast_to.normalized()
break
func _physics_process(delta):
var motion = enemy.dir * enemy.speed
enemy.move_and_slide(motion)
Теперь, когда враг переходит в состояние Chase, то начинает пытаться рейкастить игрока, и если на его пути ничего нет, преследует его! Если на пути что-то есть, то он переходит к следам запаха и пытается рейкастить каждый из них, пока не найдёт один из них, а затем — преследует его!
И теперь всё работает, враги превратились в ищеек. Чтобы усовершенствовать систему, можно реализовать между врагами систему избегания коллизий, но это уже тема для отдельной статьи.