Tower Defence. Часть 3

Tower Defence. Часть 3: Боевая логика и движение

Здесь игра становится «живой»: враги идут по пути, башни выбирают цель, пули летят и наносят урон.

Технологии именно этого урока (lesson-12-6)

  1. Вектор движения в 2D (dx, dy, distance).
  2. Нормализация направления (dx / distance, dy / distance).
  3. Движение с учетом времени кадра (step = speed * dt).
  4. Обновление массивов с удалением элементов в цикле.
  5. Приоритетный выбор цели по вычисляемому критерию.
  6. Работа со ссылками на объекты (bullet.target).
  7. Последовательный игровой update-пайплайн.

Сквозной прогресс в Practice Preview

В этой части combat-модуль продолжает тот же Tower workspace из предыдущих уроков:

  1. Проверка задачи обновляет соответствующую боевую функцию в проектном state.
  2. Во вкладке Preview можно запускать checkpoint и видеть движение/стрельбу до финала курса.
  3. Milestone по модулю combat засчитывается отдельно и не сбрасывает основной прогресс уроков.

Фундамент: как работает движение в 2D

Чтобы сдвинуть объект к цели:

  1. Считаем разницу координат:
const dx = target.x - entity.x;
const dy = target.y - entity.y;
  1. Находим расстояние до цели:
const distance = Math.hypot(dx, dy);
  1. Вычисляем длину шага за кадр:
const step = speed * dt;
  1. Если distance <= step, ставим объект точно в цель.
  2. Иначе двигаем по направлению:
entity.x += (dx / distance) * step;
entity.y += (dy / distance) * step;

Это и есть фундамент moveTowards.

Смотри, что важно:

  • если distance === 0, делить на distance нельзя (будет деление на ноль);
  • если distance <= step, мы специально "телепортируем" объект ровно в target, чтобы не пролететь точку и получить точное совпадение координат.

Это важно, потому что дальше в логике часто есть проверки вида if (enemy.x === target.x && enemy.y === target.y) или "попадание пули" через ===.

Фундамент: зачем нужен dt

dt (delta time) — время между кадрами в секундах.

Без dt на быстрых и медленных устройствах игра двигалась бы с разной скоростью. Формула speed * dt делает поведение стабильным.

Смотри, что важно: в наших задачах dt считается в секундах. Поэтому speed тоже «в пикселях в секунду», и формула step = speed * dt согласована по единицам.

Что именно ты реализуешь в задачах

  1. moveTowards — универсальный шаг к точке.
  2. updateEnemyStep — движение одного врага по маршруту.
  3. updateEnemies — обновление всех врагов и снятие жизней при прорыве.
  4. chooseTarget — выбор приоритетной цели в радиусе башни.
  5. updateTowers — стрельба, cooldown, создание пуль.
  6. moveBullet — движение пули к цели.
  7. applyEnemyDeath — удаление врага, награда, фраг.
  8. updateBullets — обработка попаданий/промахов/удалений.
  9. buildCombatReport — метрики боевого состояния.
  10. stepCombat — оркестрация боевого шага.

Почему удаление элементов делают с конца массива

В updateEnemies и updateBullets элементы могут удаляться через splice. Чтобы не пропускать элементы и не сбивать индексы, обычно идут циклом с конца:

for (let i = arr.length - 1; i >= 0; i -= 1) {
  if (/* нужно удалить */) arr.splice(i, 1);
}

Как выбирается цель башни

В game.js цель выбирается не случайно: башня ищет врага в радиусе и берет того, кто «дальше продвинулся» по пути.

Идея критерия:

const progress = enemy.pathIndex + (1 - enemy.hp / enemy.maxHp) * 0.1;

Это помогает фокусировать огонь логичнее.

Ссылки на цели: bullet.target

Пуля хранит ссылку на объект врага (bullet.target). Если враг уже умер и удален из state.enemies, пуля должна быть удалена, иначе она будет «лететь в несуществующую цель». Поэтому перед движением пули обычно проверяют, что цель еще в списке врагов.

Важный порядок в stepCombat

  1. updateEnemies
  2. updateTowers
  3. updateBullets

Этот порядок обеспечивает предсказуемый бой и уменьшает баги синхронизации.

Результат после Part 3

У тебя работает полноценный боевой цикл TD: движение, стрельба, урон, убийства, награда и метрики.