«Танчики» на html5
Пишем свой игровой движок
После прошлого поста я что-то так проникся происходящим, что решил воссоздать ещё одну любовь детства. Но как говорил Маркус Персон: «Если вы не можете написать свой движок, то гавно вы, а не разработчики». Там, скорее всего, было как-о иначе, но суть такая. И да, с этой мыслью я в корне не согласен, но написать свой движок – задача, как минимум, интересная. Я решил, что аркадная игра для этого подходит, как ничто другое. По сути, некий аналог движка уже был реализован в тетрисе, но здесь нужна будет и какая-никакая физическая модель, и интеллект врагов и рендер всего этого безобразия в несколько слоёв и с ФПС побольше, чем 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 этапов. Без ДизДока последнее время вообще не представляю как работать. А раз уж я в команде один, то сам себе и ТЗ пишу... Но лучше так, чем лепить полную отсебятину.
Вопросы замечания предложения в комментариях жду с нетерпением)
А пойду пилить обновление для Лиспублики)
гатова!
в шкафу такой лежит)
и да, если их варить в молоке, это реально небо и земля чем просто кипяток
Обнимушечки, спасибуленько!