Ворог, який переслідує гравця за допомогою pathfinding-у (тема 20) і б'ється через IDamagable (тема 19).
Передумови
Скачайте проєкт-основу та працюйте у ньому.
Що ми будуємо
Ворожий краб, який переслідує гравця та має бойову логіку аналогічну EnemyCrab (тема 20).
У темі 19 EnemyCrab отримав інтерфейс IDamagable, але пересувається бездумно та без цілі. У темі 20 CrabFriendly навчився переслідувати гравця, але не б'ється. Тепер поєднаємо обидва: CrabHostile пересувається, як CrabFriendly, а б'ється, як EnemyCrab:
- стрибок на голову краба завдає йому шкоди, а гравець відскакує вгору;
- дотик збоку завдає шкоди гравцю;
- коли
Healthкраба дорівнює нулю, краб помирає; - краб переслідує гравця.
Нова файлова структура проєкту
Показано лише ті папки, де є зміни.
- .
- Code
- Objects
- DamageInfo.cs
- IDamagable.cs
- PathfindingAgent2D.cs
- PathfindingGameEntity2D.cs
- PathfindingGameEnemy2D.csстворити
- PathfindingGameFriendly2D.cs
- Scenes
- Enemies
- Crab
- EnemyCrab.cs
- EnemyCrab.tscn
- CrabHostile
- CrabHostile.csстворити
- CrabHostile.tscnстворити (копія EnemyCrab.tscn)
- Levels
- Level1.tscnзмінити
Combat логіка
Бій (Combat) винесено в окремий абстрактний клас.
- PathfindingAgent2Dрух по графу (тема 20)
- PathfindingGameEntity2Dанімації, поява та смерть (тема 20)
- PathfindingGameFriendly2Dзупинка на відстані (тема 20)
- CrabFriendly
- PathfindingGameEnemy2Dбій
- CrabHostile
Навіщо окремий базовий клас? Щоб у майбутньому нові вороги отримували бойову логіку, просто успадкувавши PathfindingGameEnemy2D.
Чому не від PathfindingGameFriendly2D? Дружній NPC зупиняється на відстані від гравця, а ворогові треба підійти впритул, щоб атакувати.
Клас - PathfindingGameEnemy2D
Основа для всіх ворогів з pathfinding-ом. Нижче - код по частинах, у кінці - повністю.
Оголошення, поля та події
public abstract partial class PathfindingGameEnemy2D : PathfindingGameEntity2D, IDamagable
{
[ExportGroup("Combat")]
[Export] public int Health = 1;
[Export] public int Damage = 1;
[Export] public string AttackAnimName = "Attack";
private Area2D hitbox;
private bool isAttacking;
public event Action<DamageInfo> Damaged;
public event Action Died;Клас успадковує PathfindingGameEntity2D і реалізує інтерфейс IDamagable. [ExportGroup("Combat")] збирає Health, Damage та AttackAnimName в окрему групу в інспекторі.
IDamagable.IsDead
// PathfindingGameEntity2D уже має protected IsDying/IsDead (обидва з private set), тому
// IDamagable.IsDead реалізовано явно, а не звичайною властивістю - інакше вона конфліктувала б
// з успадкованою (CS0108). IsDying стає true одразу на початку смерті (в StartDying()), тоді як
// успадкований IsDead встановлюється лише в Cleanup(), вже після анімації смерті - для
// IDamagable потрібен саме перший, раніший момент.
bool IDamagable.IsDead => IsDying;IDamagable.IsDead має ставати true, щойно краб починає помирати. Але в базовому класі вже є своя IsDead, яка стає true лише в самому кінці. Тому для інтерфейсу повертаємо IsDying.
Чому не просто public bool IsDead?
Така властивість сховала б успадковану IsDead, і в класі було б дві IsDead з різним значенням. Явно реалізовану властивість видно лише через посилання типу (інтерфейсу) IDamagable:
IDamagable target = crab; // crab - екземпляр CrabHostile
bool dead = target.IsDead; // true від початку анімації смертіMovementSuspended - зупинка під час атаки
/// <summary>Пропускає pathfinding не лише в станах з PathfindingGameEntity2D, а й під час атаки.</summary>
protected override bool MovementSuspended => base.MovementSuspended || isAttacking;Поки MovementSuspended повертає true, краб стоїть на місці. Базовий клас повертає true під час появи та смерті, а ворог додає ще атаку (isAttacking) у список подій які зупинять рух краба.
OnAgentReady() - пошук хітбоксу
protected override void OnAgentReady()
{
base.OnAgentReady();
if (AnimSprite == null) { return; } // base.OnAgentReady() вже викликав DisableEntity()
hitbox = GetNodeOrNull<Area2D>("Area2D");
if (hitbox == null)
{
GD.PushError($"{Name}: Required child node 'Area2D' (type: Area2D) not found. " +
"Add it as a child of this node. Entity is disabled.");
DisableEntity();
return;
}
hitbox.BodyEntered += OnHitboxBodyEntered;
}Спершу базовий метод знаходить спрайт і запускає появу/спавн (якщо така анімація присутня). Потім ворог шукає дочірній нод "Area2D" (хітбокс) і підписується на BodyEntered. Якщо хітбоксу немає - у консолі буде помилка, і краб вимкнеться.
TakeDamage() - отримання шкоди
/// <summary>
/// Реалізація IDamagable.TakeDamage - та сама механіка, що й у EnemyCrab: зменшує Health,
/// застосовує відскок до атакуючого (info.ApplyBounceToSource), повідомляє про удар, а коли
/// здоров'я досягає нуля, запускає смерть через StartDying() (успадкований з PathfindingGameEntity2D).
/// </summary>
public void TakeDamage(DamageInfo info)
{
if (IsDying) { return; } // мертвого не можна вбити двічі
Health -= info.Amount;
info.ApplyBounceToSource();
Damaged?.Invoke(info);
if (Health <= 0)
{
Died?.Invoke();
StartDying();
}
}Як у EnemyCrab: зменшити Health, відкинути гравця, викликати Damaged, а коли Health дійде до нуля - Died і StartDying(). Перевірка IsDying не дає вбити краба двічі.
OnHitboxBodyEntered() та AttackPlayer() - контакт із гравцем
/// <summary>
/// Спрацьовує, коли гравець торкається хітбоксу цього ворога. Як і в EnemyCrab: якщо гравець
/// зверху - це стрибок на голову (ворог отримує шкоду й відкидає гравця), інакше - дотик
/// збоку (ворог завдає шкоди гравцю).
/// </summary>
private void OnHitboxBodyEntered(Node2D body)
{
if (IsDying || body is not Player player) { return; }
bool playerAbove = GlobalPosition.Y - player.GlobalPosition.Y > 10f;
if (playerAbove)
{
TakeDamage(new DamageInfo(player.StompDamage, player, new Vector2(player.Velocity.X, player.StompBounceVelocity)));
}
else
{
AttackPlayer(player);
}
}
/// <summary>Завдає шкоди гравцю та на час анімації атаки зупиняє власний рух.</summary>
protected virtual void AttackPlayer(Player player)
{
isAttacking = true;
AnimSprite.Play(AttackAnimName);
player.TakeDamage(new DamageInfo(Damage, this));
}Хітбокс реагує лише на гравця. Якщо гравець знаходиться над крабом (більш ніж 10 пікселів) - це стрибок на голову, і краб отримує шкоду. Інакше це дотик збоку: AttackPlayer() відтворює анімацію атаки та завдає шкоди гравцю.
AttackPlayer() - virtual, тож інший ворог може атакувати по-своєму.
OnAnimationFinished() - кінець атаки
protected override void OnAnimationFinished()
{
if (isAttacking && AnimSprite.Animation == AttackAnimName)
{
isAttacking = false;
return;
}
base.OnAnimationFinished();
}Атака коротко:
- Гравець торкається краба збоку -
isAttacking = true. MovementSuspendedповертаєtrue- краб стоїть.- Анімація "Attack" закінчується -
isAttacking = false. - Краб знову йде за гравцем.
Інші анімації (наприклад, смерть) обробляє базовий клас.
Анімації "Attack" і "Death" не мають бути зацикленими, інакше AnimationFinished не спрацює. У EnemyCrab.tscn вони вже правильно настроєні.
Повний код
using System;
using Godot;
/// <summary>
/// Базовий клас для ворожих сутностей (NPC), які переслідують ціль (за замовчуванням - гравця,
/// див. <see cref="PathfindingGameEntity2D.TargetRef"/>) і завдають їй шкоди в разі контакту, а самі
/// можуть отримувати шкоду. Бойова частина - той самий інтерфейс IDamagable і та сама механіка
/// "стрибок на голову вбиває, дотик збоку шкодить гравцю", що й у EnemyCrab (тема 19). Різниця -
/// у русі: замість "іти вперед, доки не вдаришся об стіну" ворог тепер користується pathfinding-ом,
/// тобто вміє підійматися на платформи та перестрибувати прірви, щоб дістатися цілі.
/// </summary>
public abstract partial class PathfindingGameEnemy2D : PathfindingGameEntity2D, IDamagable
{
[ExportGroup("Combat")]
[Export] public int Health = 1;
[Export] public int Damage = 1;
[Export] public string AttackAnimName = "Attack";
private Area2D hitbox;
private bool isAttacking;
public event Action<DamageInfo> Damaged;
public event Action Died;
// PathfindingGameEntity2D уже має protected IsDying/IsDead (обидва з private set), тому
// IDamagable.IsDead реалізовано явно, а не звичайною властивістю - інакше вона конфліктувала б
// з успадкованою (CS0108). IsDying стає true одразу на початку смерті (в StartDying()), тоді як
// успадкований IsDead встановлюється лише в Cleanup(), вже після анімації смерті - для
// IDamagable потрібен саме перший, раніший момент.
bool IDamagable.IsDead => IsDying;
/// <summary>Пропускає pathfinding не лише в станах з PathfindingGameEntity2D, а й під час атаки.</summary>
protected override bool MovementSuspended => base.MovementSuspended || isAttacking;
protected override void OnAgentReady()
{
base.OnAgentReady();
if (AnimSprite == null) { return; } // base.OnAgentReady() вже викликав DisableEntity()
hitbox = GetNodeOrNull<Area2D>("Area2D");
if (hitbox == null)
{
GD.PushError($"{Name}: Required child node 'Area2D' (type: Area2D) not found. " +
"Add it as a child of this node. Entity is disabled.");
DisableEntity();
return;
}
hitbox.BodyEntered += OnHitboxBodyEntered;
}
/// <summary>
/// Реалізація IDamagable.TakeDamage - та сама механіка, що й у EnemyCrab: зменшує Health,
/// застосовує відскок до атакуючого (info.ApplyBounceToSource), повідомляє про удар, а коли
/// здоров'я досягає нуля, запускає смерть через StartDying() (успадкований з PathfindingGameEntity2D).
/// </summary>
public void TakeDamage(DamageInfo info)
{
if (IsDying) { return; } // мертвого не можна вбити двічі
Health -= info.Amount;
info.ApplyBounceToSource();
Damaged?.Invoke(info);
if (Health <= 0)
{
Died?.Invoke();
StartDying();
}
}
/// <summary>
/// Спрацьовує, коли гравець торкається хітбоксу цього ворога. Як і в EnemyCrab: якщо гравець
/// зверху - це стрибок на голову (ворог отримує шкоду й відкидає гравця), інакше - дотик
/// збоку (ворог завдає шкоди гравцю).
/// </summary>
private void OnHitboxBodyEntered(Node2D body)
{
if (IsDying || body is not Player player) { return; }
bool playerAbove = GlobalPosition.Y - player.GlobalPosition.Y > 10f;
if (playerAbove)
{
TakeDamage(new DamageInfo(player.StompDamage, player, new Vector2(player.Velocity.X, player.StompBounceVelocity)));
}
else
{
AttackPlayer(player);
}
}
/// <summary>Завдає шкоди гравцю та на час анімації атаки зупиняє власний рух.</summary>
protected virtual void AttackPlayer(Player player)
{
isAttacking = true;
AnimSprite.Play(AttackAnimName);
player.TakeDamage(new DamageInfo(Damage, this));
}
protected override void OnAnimationFinished()
{
if (isAttacking && AnimSprite.Animation == AttackAnimName)
{
isAttacking = false;
return;
}
base.OnAnimationFinished();
}
}Перевірка
Натисніть Build - помилок бути не повинно. Клас abstract, тому у сцену(рівень) його не додати - його можна лише успадкувати.
Клас та сцена - CrabHostile
Реалізований Ворог - краб.
Код
using Godot;
/// <summary>
/// Ворожий краб: та сама бойова поведінка, що й у EnemyCrab (тема 19), але з pathfinding-рухом
/// (тема 20) - підіймається на платформи й перестрибує прірви, а не просто йде вперед.
/// </summary>
public partial class CrabHostile : PathfindingGameEnemy2D
{
// Той самий випадок, що й у CrabFriendly (тема 20): спрайт краба не має анімації появи, а
// базовий StartSpawning() запустив би SpawnAnimName ("undie"), якої в краба немає. Тому
// перевизначений StartSpawning() одразу переходить до звичайної роботи.
protected override void StartSpawning()
{
IsSpawning = false;
StartPathfindTimer();
AnimSprite.Play(IdleAnimName);
}
}Увесь код успадковано. Перевизначено лише StartSpawning(): у краба немає анімації появи, тож він одразу починає рух.
Сцена
Сцену починаємо робити з копії EnemyCrab.tscn: у ній уже є спрайт, колізії та хітбокс Area2D.
- 1Скопіюйте сцену
Створіть папку
Scenes/Enemies/CrabHostile. У FileSystem: правою кнопкою наEnemyCrab.tscn→ Duplicate..., назваCrabHostile.tscn, перетягніть у нову папку. Кореневий нод назвітьCrabHostile.
Не копіюйте файл у провіднику: Godot може переплутати копію з оригіналом.
- 2Замініть скрипт
На кореневому ноді замініть
EnemyCrab.csнаCrabHostile.cs. - 3Додайте таймер
Додайте нод
Timerз назвоюPathFindTimer: Wait Time = 0.3, Autostart вимкнено. - 4Налаштуйте змінні
Натисніть Build, щоб нові змінні з'явилися в інспекторі, і задайте:
Змінна Значення Чому Speed60 За замовчуванням 300 - це забагато. IdleAnimName"Walk" Анімації спокою в краба немає. WalkAnimName"Walk" DieAnimName"Death" AttackAnimName,Health,Damageне змінювати Уже правильні.
Дерево сцени CrabHostile
- CrabHostile (CharacterBody2D, скрипт CrabHostile.cs)
- AnimatedSprite2D
- CollisionShape2DBase (CollisionShape2D)
- CollisionShape2DSeparation (CollisionShape2D)
- Area2Dхітбокс
- CollisionShape2D
- PathFindTimer (Timer)
Додаємо ворога на рівень
У Level1.tscn додайте екземпляр CrabHostile в Objects/Enemies. Дружнього краба з Objects/NPCs видаліть.
Дерево Level1 (зміни)
- Level1 (Node2D)
- Map (Node)
- Objects (Node)
- Enemies (Node)
- EnemyCrab, EnemyCrab2 ... EnemyCrab5 (екземпляри сцени)
- CrabHostile (екземпляр сцени)
- NPCs (Node)тепер порожній
- WinTrigger (Node)
- Player (Node)
- HUD (Node)
Важливо - особливості поведінки
Код навмисно простий, тому краб поводиться не ідеально:
- Шкода - лише в момент дотику. Якщо гравець стоїть упритул до краба, нової шкоди не буде, доки один із них не відійде й не торкнеться знову (через те як працює
BodyEntered). - Мертвий краб не зразу 'губить' collision, тільки піся того як анімація смерті закінчить відтворення.
- Під низькою платформою внизу рівня краб іноді застрягає: стрибаючи, він б'ється об неї головою. Це обмеження руху, а не вашого коду.
- Інші обмеження з теми 20: граф не знає про рухомі платформи, а шлях оновлюється раз на 0.3 с, і ТД.
Підсумок - EnemyCrab та CrabHostile
EnemyCrab (тема 19) | CrabHostile (тема 21) | |
|---|---|---|
| Рух | Вперед до стіни й назад. | Переслідує гравця. |
| Бій | Однаковий: стрибок на голову шкодить крабу, дотик збоку - гравцю. | |
| Після смерті | Лишається на рівні. | Зникає (QueueFree()). |
| Код | Усе в одному класі. | Наслідує PathfindingGameEnemy2D. |
build info
- compiler
- 2.0.5.1 · 4054EF9BF6C4510A
- built
- 2026-11-08 14:31:25 UTC
WebUniverse