Tower Defence. Часть 4

Tower Defence. Часть 4: Время кадра и данные для рендера

Механика уже работает. Теперь делаем архитектуру устойчивой: стабилизируем шаг времени и подготавливаем понятные данные для отрисовки.

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

  1. Контроль dt через upper bound (capDeltaSeconds).
  2. Нормализация величин в диапазон (getHpRatio).
  3. Подготовка DTO для canvas-рендера.
  4. Разделение логики игры и логики рисования.
  5. Техническое состояние кадра (previous, frameCount).

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

Часть rendering тоже проверяется через проектный preview:

  1. После check функция обновляется в общем Tower workspace.
  2. В Preview видно, как меняются DTO/кадр и итоговый HUD на текущем состоянии проекта.
  3. Milestone-checkpoint фиксирует готовность рендер-модуля без блокировки прохождения курса.

Фундамент: зачем ограничивать dt

Если вкладка зависла или FPS резко просел, dt может стать слишком большим. Тогда объекты «телепортируются».

Смотри, что важно: в браузерных играх now обычно приходит из requestAnimationFrame(now), а performance.now() возвращает миллисекунды. Поэтому dt удобно считать в секундах и затем ограничивать сверху.

Решение:

const rawDt = (now - previous) / 1000; // dt в секундах
const dt = Math.min(rawDt, 0.033);

0.033 — примерно 1/30 секунды. Это защищает игру от больших скачков.

В этом проекте мы выносим это в helper capDeltaSeconds(now, previous): вход в миллисекундах (как у performance.now()), выход dt в секундах.

Фундамент: что такое DTO для рендера

DTO (Data Transfer Object) — простой объект данных для слоя отрисовки.

Пример:

{
  x: tower.x,
  y: tower.y,
  range: tower.range,
}

Смотри, что важно: DTO это "снимок" данных на кадр. В идеале:

  • только примитивы и простые объекты/массивы;
  • без методов и ссылок на "живые" игровые объекты;
  • без лишних полей, которые draw-слою не нужны.

Примеры DTO из задач (минимальный формат для canvas):

const towerDto = { x: tower.x, y: tower.y, radius: 16, range: tower.range };
const enemyDto = { x: enemy.x, y: enemy.y, radius: 14, hpRatio: getHpRatio(enemy) };
const bulletDto = { x: bullet.x, y: bullet.y, radius: 4 };

Почему это полезно:

  • draw-функции получают уже подготовленные данные;
  • боевая логика не зависит от canvas-деталей;
  • код легче расширять и тестировать.

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

  1. capDeltaSeconds.
  2. getHpRatio.
  3. buildGridLineCount.
  4. buildPathSegments.
  5. buildTowerDrawData.
  6. buildEnemyDrawData.
  7. buildBulletDrawData.
  8. shouldDrawGameOver.
  9. createFrameState.
  10. stepFrame.

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

  • buildGridLineCount(cols, rows) считает количество линий сетки: cols + 1 + rows + 1.
  • buildPathSegments(path) превращает путь в массив сегментов (пара соседних точек). Для N точек всегда N - 1 сегментов.
  • shouldDrawGameOver(state) делает намерение явным: показываем оверлей только если state.gameOver === true (строгая проверка защищает от "truthy" значений).
const path = [{ x: 0, y: 0 }, { x: 1, y: 0 }, { x: 1, y: 1 }];
// => [[path[0], path[1]], [path[1], path[2]]]

Пример простого frame-state

function createFrameState(startNow) {
  return {
    previous: startNow,
    frameCount: 0,
  };
}

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

  • previous нужен, чтобы корректно посчитать dt на следующем кадре.
  • frameCount удобно использовать для отладки, метрик и "периодических" действий.
  • stepFrame(frameState, now) возвращает dt и обновляет frameState (ожидаемая мутация служебного объекта).
function stepFrame(frameState, now) {
  const dt = capDeltaSeconds(now, frameState.previous);
  frameState.previous = now;
  frameState.frameCount += 1;
  return dt;
}

Где это видно в game.js

  • previous = performance.now();
  • в frame(now) считается dt и вызываются update(dt) + render();
  • после этого снова requestAnimationFrame(frame).

Это и есть базовый игровой цикл браузерной игры.

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

Код становится инженерно зрелым: время кадра стабильно, рендер получает чистые данные, а проект проще поддерживать.