Tower Defence. Часть 4
Tower Defence. Часть 4: Время кадра и данные для рендера
Механика уже работает. Теперь делаем архитектуру устойчивой: стабилизируем шаг времени и подготавливаем понятные данные для отрисовки.
Технологии именно этого урока (lesson-13-7)
- Контроль
dtчерез upper bound (capDeltaSeconds). - Нормализация величин в диапазон (
getHpRatio). - Подготовка DTO для canvas-рендера.
- Разделение логики игры и логики рисования.
- Техническое состояние кадра (
previous,frameCount).
Сквозной прогресс в Practice Preview
Часть rendering тоже проверяется через проектный preview:
- После
checkфункция обновляется в общем Tower workspace. - В
Previewвидно, как меняются DTO/кадр и итоговый HUD на текущем состоянии проекта. - 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-деталей;
- код легче расширять и тестировать.
Что именно ты реализуешь в задачах
capDeltaSeconds.getHpRatio.buildGridLineCount.buildPathSegments.buildTowerDrawData.buildEnemyDrawData.buildBulletDrawData.shouldDrawGameOver.createFrameState.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
Код становится инженерно зрелым: время кадра стабильно, рендер получает чистые данные, а проект проще поддерживать.