После прошлого поста я что-то так проникся происходящим, что решил воссоздать ещё одну любовь детства. Но как говорил Маркус Персон: «Если вы не можете написать свой движок, то гавно вы, а не разработчики». Там, скорее всего, было как-о иначе, но суть такая. И да, с этой мыслью я в корне не согласен, но написать свой движок – задача, как минимум, интересная. Я решил, что аркадная игра для этого подходит, как ничто другое. По сути, некий аналог движка уже был реализован в тетрисе, но здесь нужна будет и какая-никакая физическая модель, и интеллект врагов и рендер всего этого безобразия в несколько слоёв и с ФПС побольше, чем 2 :)

Так что, приступим. Что нам нужно:

  • Просчёт физики
  • Работа ИИ агентов
  • Считывание действий игрока
  • Отрисовка результата работы предыдущих пунктов.

Для одного поста такая задачка звучит жирновато, поэтому, видимо, будет серия.

Я решил разделить рисования окружения (статичных объектов), врагов и игроков (динамических объектов) и UI на три разных канваса, которые просто повещены один поверх другого. Там можно будет проще и меньше перерисовывать.

Начинается наш код с проверки, готова ли страница к явлению миру нашего движка.

if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', init);
} else {
  init();
}

Если готова, то давай же скорее всё проинициализируем

Тут всё просто:

function init() {
  console.log('Battle City Remake - Initializing...');

  // Инициализация рендерера
  initRenderer();

  // Очищаем все слои
  clearBackground();
  clearForeground();
  clearUI();

  // Рисуем границу игрового поля и отладочную сетку
  renderGameFieldBorder();
  renderDebugGrid();

  console.log('Initialization complete. Starting game loop...');

  // Запускаем game loop
  requestAnimationFrame(gameLoop);
}

Но понятное дело, что кода тут мало, потому что всё вынесено в отдельные методы.

Есть ли среди них хоть что-то интересное?

Ну, renderDebugGrid и renderGameFieldBorder одним названием уже говорят, что там происходит.

Логично, что самое интересное происходит где-то тут requestAnimationFrame(gameLoop);

Но давайте сначала заглянем в инициализацию рендера

export function initRenderer() {
  // Получаем canvas элементы
  backgroundCanvas = document.getElementById('background-canvas');
  foregroundCanvas = document.getElementById('foreground-canvas');
  uiCanvas = document.getElementById('ui-canvas');

  // Получаем контексты
  backgroundCtx = backgroundCanvas.getContext('2d');
  foregroundCtx = foregroundCanvas.getContext('2d');
  uiCtx = uiCanvas.getContext('2d');

  // Определяем DPR для HiDPI экранов
  dpr = window.devicePixelRatio || 1;

  // Настраиваем все три canvas
  setupCanvas(backgroundCanvas, backgroundCtx);
  setupCanvas(foregroundCanvas, foregroundCtx);
  setupCanvas(uiCanvas, uiCtx);

  console.log(`Renderer initialized (DPR: ${dpr})`);
}

function setupCanvas(canvas, ctx) {
  // Физический размер canvas
  canvas.width = CANVAS_WIDTH * dpr;
  canvas.height = CANVAS_HEIGHT * dpr;

  // Масштабируем контекст для компенсации DPR
  ctx.scale(dpr, dpr);

  // Отключаем сглаживание для пиксельной графики
  ctx.imageSmoothingEnabled = false;

  console.log(`Canvas ${canvas.id} setup: ${canvas.width}×${canvas.height} (logical: ${CANVAS_WIDTH}×${CANVAS_HEIGHT})`);
}

CANVAS_WIDTH и CANVAS_HEIGHT — это константы, они у меня в отдельном файлике лежат, там просто 800 на 600.

В целом, страничка готова к тому, чтоб рисовать всякое

Дёргаем requestAnimationFrame и отдаём её наш gameLoop

function gameLoop(currentTime) {
  // Вычисляем deltaTime
  const deltaTime = currentTime - lastTime;
  lastTime = currentTime;

  // Накапливаем время
  accumulator += deltaTime;

  // Fixed timestep - обновляем логику с фиксированным шагом
  while (accumulator >= FRAME_TIME) {
    update(FRAME_TIME);
    accumulator -= FRAME_TIME;
  }

  // Рендерим независимо от update
  render();

  // Продолжаем loop
  requestAnimationFrame(gameLoop);
}

Я решил фиксировать ФПС на 30

export const FPS = 30;                    // Фиксированный FPS
export const FRAME_TIME = 1000 / FPS;     // ~33.33ms на кадр

Метод update содержит (ну, будет содержать) всю нашу игровую логику. Пока он только считает фпс и выводит его на экран для отладки.

function update(dt) {
  // Пока пусто - здесь будет логика игры

  // Подсчёт FPS для отладки
  frameCount++;
  fpsTimer += dt;

  if (fpsTimer >= 1000) {
    fps = frameCount;
    frameCount = 0;
    fpsTimer = 0;

    // Обновляем FPS на странице
    const fpsElement = document.getElementById('fps');
    if (fpsElement) {
      fpsElement.textContent = fps;
    }
  }
}

Ну и когда всё закончено, отрисовываем

function render() {
  // Background canvas - перерисовывается только при изменении карты
  // (пока только отладочная сетка, нарисованная в init)

  // Foreground canvas - очищаем и рисуем сущности каждый кадр
  clearForeground();
  // TODO: Здесь будет рендер танков, пуль, эффектов

  // UI canvas - очищаем и рисуем UI каждый кадр
  clearUI();
  renderUI();
}

То есть, по сути, ничего интересного, просто рисую полосочки и квадратики для UI

Результат выглядит вот так

Следующий пост будет посвящён второму этапу

И да, я упоролся и расписал себе полноценный план реализации на 13 этапов. Без ДизДока последнее время вообще не представляю как работать. А раз уж я в команде один, то сам себе и ТЗ пишу... Но лучше так, чем лепить полную отсебятину.

Вопросы замечания предложения в комментариях жду с нетерпением)

А пойду пилить обновление для Лиспублики)


Всем хороших игр!