[Перевод] Вражеский ИИ: преследование игрока без Navigation2D и поиска пути A*

Создаёте игру, в которой враги должны преследовать игрока? Всё начинается с простого — заставим врага бежать к игроку. Но что произойдёт, если он находится за деревом, или за углом стены? Ну, теперь враг будет выглядеть довольно глупо — упрётся в объект, перебирая ногами на месте. Не очень хорошо!

Чтобы решить эту проблему, можно использовать встроенные в Godot ноды Navigation2D или AStar (вот туториал GDQuest, посвящённый обоим нодам). Но в Helms of Fury мы использовали другой способ, который отлично подошёл нашей игре, и мы хотим поделиться им в этом туториале. Вот как это выглядит:

2dubss3iyd2rns37pn9pzrjjybk.gif


Приступаем к работе


Будем считать, что вы создаёте врагов как объекты 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.

physics_layers.png


Дайте им любое подходящее название. После этого перейдите к объектам в игре и в боковой панели Property Inspector разверните Collision, а затем нажмите ··, чтобы назначить их. Для объектов их нужно назначать как Layers.

picking_physics_layers.png


Назначив объектам 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, то начинает пытаться рейкастить игрока, и если на его пути ничего нет, преследует его! Если на пути что-то есть, то он переходит к следам запаха и пытается рейкастить каждый из них, пока не найдёт один из них, а затем — преследует его!

twitter-700x400.gif


И теперь всё работает, враги превратились в ищеек. Чтобы усовершенствовать систему, можно реализовать между врагами систему избегания коллизий, но это уже тема для отдельной статьи.

© Habrahabr.ru