В прошлый раз мы разобрали, что такое логгеры и зачем они нужны. Вот короткая версия:
- Логгер — это часть программы, которая реагирует на нужные нам события и записывает их, как бы ведёт летопись работы программы.
- Эти записи называются логами. Они могут появляться в консоли, записываться в файл или отправлять в другую программу.
- Логи нужны, когда надо посмотреть, что в программе пошло не так, или если надо восстановить хронологию событий.
- Есть несколько уровней логирования: сообщения об ошибках, предупреждения, штатные срабатывания и разовые ситуации.
- Если проект сложный, лучше использовать готовый логгер, чем делать свой.
Сегодня мы попробуем логгер в деле — подключим его к цветному арканоиду на JavaScript и посмотрим, что будет в логах после игры.
js-Logger — простой логгер для браузеров
Если нам нужен простой логгер для консоли браузера с делением на уровни сообщений, то проще всего использовать js-Logger. Он подключается как обычный скрипт на HTML-странице:
<script src="https://github.com/jonnyreeves/js-logger/blob/master/src/logger.js"></script>
Сразу после этого его можно использовать для логирования разных сообщений:
// ставим настройки по умолчанию
Logger.useDefaults();
// и выводим тестовые сообщения разных уровней
Logger.debug("Это обычное сообщение о каком-то событии, например для отладки");
Logger.info("Информационное сообщение, его можно привязать к разовому событию");
Logger.warn("А вот это уже серьёзно, тут логгер нас о чём-то предупреждает. Ничего критичного, но присмотреться стоит");
Logger.error("Ошибка — что-то в программе пошло не так");
Смотрим уровни логирования
В браузере Chrome можно настроить в консоли типы сообщений, которые мы хотим видеть. По умолчанию мы видим все события:
Если нам нужны только предупреждения и ошибки, то ставим галочку на Warnings (предупреждения) и Errors (ошибки). Браузер отфильтрует эти события и скроет всё остальное, но при этом сами логи никуда не исчезнут — мы просто перестанем их видеть. Это работает с любым логгером, который поддерживает разные уровни сообщений:
Добавляем логгер в проект
Теперь расставим сообщения логгера в разных местах в коде арканоида и посмотрим, что будет в консоли, если мы немного в неё поиграем. Мы не будем комментировать каждый шаг, а добавим вызов логгера в ключевых местах: при касании платформы, нажатии игроком на кнопки, потере или добавлении жизней и при вылете шарика за пределы поля. Чтобы было интереснее, сделаем события на разных уровнях. Единственное, чего у нас не будет, — сообщений об ошибках, потому что мы не обрабатывали их в исключениях.
Поначалу кажется, что всё нормально и с игрой всё в порядке: мы видим каждое нажатие клавиши и реакцию игры на все события:
Но как только мы потратим последнюю жизнь, то увидим странное: начнётся бесконечный повтор событий «Шарик улетел за границы поля» → «Потеряли жизнь». При этом внешне игра остановится штатно: появится чёрный экран с надписью «Game over»:
Оказывается, программа вошла в бесконечный цикл: ей кажется, что шарик всё время улетает за границы поля и это нужно обработать. Без логгера мы этого бы не заметили: кажется, что игра просто остановилась, но внутри продолжает кипеть работа. Эту ошибку легко поправить: перед завершением игры нужно виртуально вернуть шарик на поле. Попробуйте сделать это самостоятельно и проверьте себя, заглянув в консоль.
<!-- Лицензия CC0 1.0 Universal-->
<!-- оригинал — https://gist.github.com/straker/98a2aed6a7686d26c04810f08bfaf66b -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Arkanoid</title>
<style>
/* стили для всей страницы */
html, body {
height: 100%;
margin: 0;
}
/* отдельные параметры для фона */
body {
background: black;
display: flex;
align-items: center;
justify-content: center;
}
</style>
</head>
<body>
<!-- рисуем игровое поле -->
<canvas width="400" height="500" id="game"></canvas>
<!-- сам скрипт с игрой -->
<script src="https://github.com/jonnyreeves/js-logger/blob/master/src/logger.js"></script>
<script>
// ставим настройки по умолчанию
Logger.useDefaults();
// и выводим тестовые сообщения разных уровней
Logger.debug("Это обычное сообщение о каком-то событии, например для отладки");
Logger.info("Информационное сообщение, его можно привязать к разовому событию");
Logger.warn("А вот это уже серьёзно, тут логгер нас о чём-то предупреждает. Ничего критичного, но присмотреться стоит");
Logger.error("Ошибка — что-то в программе пошло не так");
// переменная для работы с холстом, на котором будет нарисована игра
const canvas = document.getElementById('game');
const context = canvas.getContext('2d');
// каждый ряд состоит из 14 кирпичей. На уровне будут 6 пустых рядов, а затем 8 рядов с кирпичами
// цвета кирпичей: красный, оранжевый, зелёный и жёлтый
// буква в массиве означает цвет кирпича
const level1 = [
[],
[],
[],
[],
[],
[],
['R','R','R','R','R','R','R','R','R','R','R','R','R','R'],
['R','R','R','R','R','R','R','R','R','R','R','R','R','R'],
['O','O','O','O','O','O','O','O','O','O','O','O','O','O'],
['O','O','O','O','O','O','O','O','O','O','O','O','O','O'],
['G','G','G','G','G','G','G','G','G','G','G','G','G','G'],
['G','G','G','G','G','G','G','G','G','G','G','G','G','G'],
['Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y'],
['Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y']
];
// сопоставляем буквы (R, O, G, Y) с цветами
const colorMap = {
'R': 'red',
'O': 'orange',
'G': 'green',
'Y': 'yellow'
};
// делаем зазор в 2 пикселя между кирпичами, чтобы отделить их друг от друга
const brickGap = 2;
// размеры каждого кирпича
const brickWidth = 25;
const brickHeight = 12;
// ширина стены должна занимать оставшееся место на холсте с каждой стороны
// у нас 14 кирпичей по 25 пикселей и 13 промежутков по 2 пикселя, а общая ширина холста — 400 пикселей
// получаем общую ширину стен: 400 - (14 * 25 + 2 * 13) = 24px.
// разделим пополам, чтобы получить ширину каждой стены, и получим 12px
const wallSize = 12;
// основной массив для игры
const bricks = [];
// количество набранных очков за одну попытку
score = 0;
// количество жизней на старте
lives = 3;
Logger.info("Объявили все переменные")
// создадим уровень так: обработем весь массив level1
// и те места, которые обозначены каким-либо цветом, поместим в игровой массив.
// там же будем хранить координаты начала каждого кирпича и его цвет
// в эту функцию мы поместим всё, что связано с касанием кирпичей
function touchdown(t_brick) {
Logger.debug('Коснулись кирпича')
// начисляем очки в зависимости от цвета кирпича
switch(t_brick.color) {
case "yellow" : score += 1; break;
case "green" : score += 2; break;
case "orange" : score += 3; break;
case "red" : score += 4;
}
// за каждые 25 очков — увеличиваем размер платформы на 2 пикселя
if (score % 25 == 0) {
paddle.width += 2;
}
// а за каждые 100 очков в одной попытке — прибавляем ещё одну жизнь
if (score % 100 == 0){
lives += 1;
Logger.info('Получили ещё одну жизнь')
// и усложняем игру — увеличиваем скорость шарика
ball.speed += 1;
}
}
function lost() {
// уменьшаем количество жизней
lives = lives - 1;
Logger.info('Потеряли жизнь')
// обнуляем набранные очки
score = 0;
}
Logger.debug('Переходим к обработке массива с уровнем')
// пока у нас есть необработанные элементы в массиве с уровнем — обрабатываем их
for (let row = 0; row < level1.length; row++) {
for (let col = 0; col < level1[row].length; col++) {
// находим цвет кирпича
const colorCode = level1[row][col];
// создаём новый элемент игрового массива — с координатами кирпича, цветом, шириной и высотой кирпича
bricks.push({
x: wallSize + (brickWidth + brickGap) * col,
y: wallSize + (brickHeight + brickGap) * row,
color: colorMap[colorCode],
width: brickWidth,
height: brickHeight
});
}
}
// платформа, которой управляет игрок
const paddle = {
// ставим её внизу по центру поля
x: canvas.width / 2 - brickWidth / 2,
y: 440,
// делаем её размером с кирпич
width: brickWidth,
height: brickHeight,
// пока платформа никуда не движется, поэтому направление движения равно нулю
dx: 0
};
// шарик, который отскакивает от платформы и уничтожает кирпичи
ball = {
// стартовые координаты
x: 130,
y: 260,
// высота и ширина (для простоты это будет квадратный шарик)
width: 5,
height: 5,
// скорость шарика по обоим координатам
speed: 2,
// на старте шарик пока никуда не смещается
dx: 0,
dy: 0
};
// проверка на пересечение объектов
// взяли отсюда: https://developer.mozilla.org/en-US/docs/Games/Techniques/2D_collision_detection
function collides(obj1, obj2) {
return obj1.x < obj2.x + obj2.width &&
obj1.x + obj1.width > obj2.x &&
obj1.y < obj2.y + obj2.height &&
obj1.y + obj1.height > obj2.y;
}
Logger.info('Входим в главный цикл игры')
// главный цикл игры
function loop() {
// на каждом кадре — очищаем поле и рисуем всё заново
requestAnimationFrame(loop);
context.clearRect(0,0,canvas.width,canvas.height);
// двигаем платформу с нужной скоростью
paddle.x += paddle.dx;
// при этом смотрим, чтобы она не уехала за стены
if (paddle.x < wallSize) {
paddle.x = wallSize
}
else if (paddle.x + paddle.width > canvas.width - wallSize) {
paddle.x = canvas.width - wallSize - paddle.width;
}
// шарик тоже двигается со своей скоростью
ball.x += ball.dx;
ball.y += ball.dy;
// и его тоже нужно постоянно проверять, чтобы он не улетел за границы стен
// смотрим левую и правую стенки
if (ball.x < wallSize) {
ball.x = wallSize;
ball.dx *= -1;
}
else if (ball.x + ball.width > canvas.width - wallSize) {
ball.x = canvas.width - wallSize - ball.width;
ball.dx *= -1;
}
// проверяем верхнюю границу
if (ball.y < wallSize) {
ball.y = wallSize;
ball.dy *= -1;
}
// перезагружаем шарик, если он улетел вниз, за край игрового поля
if (ball.y > canvas.height) {
Logger.warn('Шарик улетел за границы поля')
// обрабатываем падение шарика
lost();
if (lives <= 0){
// рисуем чёрный прямоугольник посередине поля
context.fillStyle = 'black';
context.globalAlpha = 0.75;
context.fillRect(0, canvas.height / 2 - 30, canvas.width, 60);
// пишем надпись белым моноширинным шрифтом по центру
context.globalAlpha = 1;
context.fillStyle = 'white';
context.font = '36px monospace';
context.textAlign = 'center';
context.textBaseline = 'middle';
context.fillText('GAME OVER!', canvas.width / 2, canvas.height / 2);
return; };
console.log(lives);
ball.x = 130;
ball.y = 260;
ball.dx = 0;
ball.dy = 0;
}
// проверяем, коснулся ли шарик платформы, которой управляет игрок. Если коснулся — меняем направление движения по y на противоположное
if (collides(ball, paddle)) {
ball.dy *= -1;
// сдвигаем шарик выше платформы, чтобы на следующем кадре это снова не засчиталось за столкновение
ball.y = paddle.y - ball.height;
}
// проверяем, коснулся ли шарик цветного кирпича
// если коснулся — меняем направление движения шарика в зависимости от стенки касания
// для этого в цикле проверяем каждый кирпич на касание
for (let i = 0; i < bricks.length; i++) {
// берём очередной кирпич
const brick = bricks[i];
// если было касание
if (collides(ball, brick)) {
Logger.debug('Касание')
touchdown(brick);
// убираем кирпич из массива
bricks.splice(i, 1);
// если шарик коснулся кирпича сверху или снизу — меняем направление движения шарика по y
if (ball.y + ball.height - ball.speed <= brick.y ||
ball.y >= brick.y + brick.height - ball.speed) {
ball.dy *= -1;
}
// в противном случае меняем направление движения шарика по x
else {
ball.dx *= -1;
}
// как нашли касание — сразу выходим из цикла проверки
break;
}
}
// рисуем стены
context.fillStyle = 'lightgrey';
context.fillRect(0, 0, canvas.width, wallSize);
context.fillRect(0, 0, wallSize, canvas.height);
context.fillRect(canvas.width - wallSize, 0, wallSize, canvas.height);
// если шарик в движении — рисуем его
if (ball.dx || ball.dy) {
context.fillRect(ball.x, ball.y, ball.width, ball.height);
}
// рисуем кирпичи
bricks.forEach(function(brick) {
context.fillStyle = brick.color;
context.fillRect(brick.x, brick.y, brick.width, brick.height);
});
// рисуем платформу
context.fillStyle = 'cyan';
context.fillRect(paddle.x, paddle.y, paddle.width, paddle.height);
// Цвет текста — серый
context.fillStyle = "#777777";
// Задаём размер и шрифт
context.font = "20pt monospace";
// Сначала выводим рекорд
context.fillText('Очки: ' + score, 50, 490);
// Затем — набранные очки
context.fillText('Жизни:'+ lives, 250, 490);
}
// отслеживаем нажатия игрока на клавиши
document.addEventListener('keydown', function(e) {
Logger.info('Игрок нажал на кнопку')
// стрелка влево
if (e.which === 37) {
paddle.dx = -3;
}
// стрелка вправо
else if (e.which === 39) {
paddle.dx = 3;
}
// обрабатываем нажатие на пробел
// если шарик не запущен — запускаем его из начальной точки, сверху вниз
if (ball.dx === 0 && ball.dy === 0 && e.which === 32) {
ball.dx = ball.speed;
ball.dy = ball.speed;
}
});
// как только игрок перестал нажимать клавиши со стрелками — останавливаем платформу
document.addEventListener('keyup', function(e) {
if (e.which === 37 || e.which === 39) {
paddle.dx = 0;
}
});
// запускаем игру
requestAnimationFrame(loop);
</script>
</body>
</html>
log4javascript — продвинутый логгер для сложных задач
Если понадобится логгер посложнее, можно посмотреть на log4javascript — он также подключается как внешний скрипт и не требует установленной платформы Node.js (как это делают многие логгеры).
Особенность этого логгера в том, что он может не просто выводить сообщения в консоль, но и создать отдельное окно для логов:
Чтобы подключить логгер, его нужно скачать себе на компьютер, а потом указать полный путь к файлу, например так:
<script type="text/javascript" src="/Users/mike/Downloads/log4javascript-1.4.13/log4javascript.js"></script>
Добавим логгер в проект — для этого скопируем из руководства пример подключения логгера с выводом во внешнее окно:
// настраиваем лог на вывод сообщений в отдельное окно
var log = log4javascript.getLogger();
var popUpAppender = new log4javascript.PopUpAppender();
var popUpLayout = new log4javascript.PatternLayout("%d{HH:mm:ss} %-5p - %m%n");
popUpAppender.setLayout(popUpLayout);
log.addAppender(popUpAppender);
var ajaxAppender = new log4javascript.AjaxAppender("myloggingservlet.do");
ajaxAppender.setThreshold(log4javascript.Level.ERROR);
log.addAppender(ajaxAppender);
// выводим тестовые сообщения
log.debug('Простое сообщение отладчика');
log.info('Информационное сообщение');
log.warn('Предупреждение, на которое нужно обратить внимание')
log.error('Ошибка! В программе что-то случилось.');
Теперь мы можем делать то же самое, что и с предыдущим логгером: отслеживать все события, смотреть логи и делать выводы о том, как работает игра. Если нужно сохранить лог в отдельный файл, можно добавить специальный модуль — он сохранит весь вывод с момента запуска.
Что дальше
В следующий раз добавим логгер в проект на Python — там больше возможностей для отладки и настройки таких сообщений.