Логика врагов
Часть раздела Архитектура игр в Unity путешествия по Unity на Coddy. Урок 31 из 50.
Мозг врага — это конечный автомат, в котором каждому поведению соответствует отдельное состояние. Этот скелет патрулирует между точками, преследует игрока, когда тот подходит ближе чем на 6 единиц, и раз в секунду наносит удар мечом, когда находится рядом. Каждое состояние само определяет, когда из него выйти, а скелет хранит общие для них вспомогательные данные:
public class Skeleton : MonoBehaviour
{
public Transform player;
public Transform[] posts;
public float speed = 2f;
public StateMachine machine = new StateMachine();
public IState patrol, chase, attack;
void Start()
{
patrol = new PatrolState(this);
chase = new ChaseState(this);
attack = new AttackState(this);
machine.ChangeState(patrol);
}
void Update() { machine.Tick(); }
public float PlayerDistance() { return Vector3.Distance(transform.position, player.position); }
public void WalkTo(Vector3 target)
{
target.y = transform.position.y;
transform.position = Vector3.MoveTowards(transform.position, target, speed * Time.deltaTime);
}
}
public class PatrolState : IState
{
private Skeleton skel;
private int next;
public PatrolState(Skeleton skel) { this.skel = skel; }
public void Enter() { }
public void Exit() { }
public void Tick()
{
if (skel.PlayerDistance() < 6f) { skel.machine.ChangeState(skel.chase); return; }
Vector3 post = skel.posts[next].position;
post.y = skel.transform.position.y;
skel.WalkTo(post);
if (skel.transform.position == post) next = (next + 1) % skel.posts.Length;
}
}
public class ChaseState : IState
{
private Skeleton skel;
public ChaseState(Skeleton skel) { this.skel = skel; }
public void Enter() { }
public void Exit() { }
public void Tick()
{
float d = skel.PlayerDistance();
if (d > 6f) { skel.machine.ChangeState(skel.patrol); return; }
if (d < 1.5f) { skel.machine.ChangeState(skel.attack); return; }
skel.WalkTo(skel.player.position);
}
}
public class AttackState : IState
{
private Skeleton skel;
private float cooldown;
public AttackState(Skeleton skel) { this.skel = skel; }
public void Enter() { cooldown = 0f; }
public void Exit() { }
public void Tick()
{
if (skel.PlayerDistance() > 1.5f) { skel.machine.ChangeState(skel.chase); return; }
cooldown -= Time.deltaTime;
if (cooldown <= 0f) { Debug.Log("Swing!"); cooldown = 1f; }
}
}Состояния создаются один раз и существуют столько же, сколько скелет, поэтому значения их полей сохраняются между переходами в эти состояния. Значение next в PatrolState сохраняется и после погони, и скелет продолжает путь к той точке, к которой направлялся. Enter сбрасывает только то, что должно начинаться заново: в AttackState значение cooldown устанавливается в 0, поэтому первый удар наносится, как только скелет добирается до игрока.
Всё перемещение выполняется через WalkTo — здесь это MoveTowards для transform, который позволяет проходить сквозь стены. Чтобы перемещать скелет с помощью Rigidbody, WalkTo задаёт скорость тела, а скелет обновляет автомат состояний в FixedUpdate, где Time.deltaTime соответствует шагу физики. Три состояния не меняются: они указывают, куда идти, а владелец определяет, как это делать.
Задание
СреднеРыцарь охраняет двор замка. Стрелки перемещают игрока, у которого есть Health с методом TakeDamage(int amount) (оба уже готовы). В Knight.cs есть поля, которые заполняются в Inspector (waypoints от W0 до W2, player, playerHealth, speed 3 и диапазоны ниже), а также вспомогательные методы, использующие его Rigidbody: DistanceTo(point) и PlayerDistance() измеряют расстояние по земле, WalkTo(target) перемещается к точке, Stop() останавливается. Дай ему мозги:
- В
Knight.csсоздай машину состояний и три состояния (поляpatrol,chaseиattack), запусти патрулирование и вызывай обновление машины состояний в FixedUpdate. - В
KnightStates.csнапишиPatrolState,ChaseStateиAttackState, каждое из которых получает рыцаря в конструкторе. - Патрулирование: иди к
waypoints[next]; когда до него останется меньше 0.3, направляйся к следующему, а после последнего — к первому. Если игрок ближе, чемsightRange(5), преследуй его. После преследования патрулирование продолжается к путевой точке, к которой рыцарь направлялся. - Преследование: иди к игроку. Если он ближе, чем
attackRange(1.5), атакуй. Если он дальше, чемloseRange(8), патрулируй. При расстоянии от 5 до 8 преследование продолжается, поэтому у границы видимости рыцарь не переключается туда-сюда между двумя состояниями. - Атака: стой на месте и сразу нанеси игроку 1 единицу урона, затем наноси урон каждую секунду, пока он находится в пределах
attackRange; когда он окажется дальше, снова преследуй его.
Нажми Play, дай рыцарю заметить тебя и нанести удар, затем убеги и нажми Check.
Попробуйте сами
using UnityEngine;
// PatrolState, ChaseState и AttackState
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Архитектура игр в Unity
1Компоненты, а не иерархии
Небольшие компонентыБазовый скрипт врагаАбстрактные компонентыСовместная работа компонентовПовторение: модульная турель4Сериализованные данные
Сериализуемые классыСписки в InspectorПеречисления в InspectorПовторение: таблица лута7Конечные автоматы
Состояния как классыКонечный автоматПереходыЛогика враговПовторение: состояния двери2Интерфейсы в Unity
IDamageableИнтерфейсы и TryGetComponentGetComponentsПовторение: клавиша взаимодействия5ScriptableObjects
Ресурсы данныхХарактеристики оружияОбщие настройкиВолны как данныеПовторение: база предметов