От Zanathel
«Передвижение белого квадрата по темному полю»
JavaScript – очень мощный язык, гораздо более мощный, чем можно предположить. Я понял, на что способны скриптовые языки после того, как поэкспериментировал с обработчиками событий от клавиатуры и полной перерисовкой экрана в сжатые сроки. В IE 5.0 я поставил перерисовку экрана каждые 10 милисекунд, и это не вызвало ни задержек, ни увеличения потребляемого объема памяти. Когда я это увидел, я понял, что просто обязан углубиться в изучение JavaScript. В результате получилась в чем-то простая, а в чем-то сложная игра, которая пользовалась успехом среди моих друзей. Сейчас я собираюсь попытаться создать универсальную относительно браузера игру с дружественным интерфейсом. Пусть семейство браузеров с открытым кодом представляет FireFox, как наиболее стандартный из них.
Взгляните на нижеследующий код. Он корректно работает и в IE, и в FireFox. Что меня поразило, так это то, что FireFox по сравнению с IE предоставляет расширенную поддержку обработки событий:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" /> <title>Browser hooking capabilities</title> <script language="javascript" type="text/javascript"> <!-- var isIE = (String(typeof(document.all)) != "undefined"); function keyhookDown(ev) { var key = new Number(); if (isIE) key = event.keyCode; else key = ev.which; alert("Down: " + key.toString()); } function keyhookUp(ev) { var key = new Number(); if (isIE) key = event.keyCode; else key = ev.which; alert("Up: " + key.toString()); } function prepareKeyHook() { document.onkeyup = keyhookUp; document.onkeydown = keyhookDown; } --> </script> </head> <body id="gSurface" onload="prepareKeyHook()" name="gSurface"> </body> </html>
Это отличная новость.
Начинать следует с передвижения объекта (квадрата) по экрану. Для этого потребуются обработчики событий, передвигаемый объект, перерисовка экрана и система координат. Перед тем как создать систему координат, нужно получить реальные размеры клиентской области окна. В IE они хранятся в document.body.offsetWidth и document.body.offsetHeight. А как в Firefox? Воспользовавшись Google, я нашел свойства window.innerWidth и window.innerHeight. И все прекрасно работает. Итак, высота и ширина системы координат определена. Теперь надо расположить наш объект точно в центре экрана. Как? Я попробовал разные способы, но наиболее подходящей мне показалась система позиционирования CSS. Перерисовывая конкретный участок экрана (ограниченный ячейкой таблицы), можно добиться высокой эффективности перерисовки.
В обычных языках программирования Вы не перерисовываете постоянно весь экран. Вы перерисовываете конкретные участки (блиттинг). Эмуляция блиттинга в JavaScript заключается в том, что движущийся элемент располагается внутри отдельного слоя, который в общем случае может заполнять весь экран. Это позволяет не перерисовывать статические объекты, такие как дома, фон, статистика игры, сообщения и т.д. и спасает от мерцания картинки.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" /> <title>Простой движущийся слой!</title> <script language="javascript" type="text/javascript"> <!--<![CDATA[ var surface = null; var SCREEN_X = 0, SCREEN_Y = 1; var bIE = (String(typeof(document.all)) != "undefined"); var iMs = 25; //////////////////////////// Загрузка игры ////////////////////////// function loadGame() { surface = document.getElementById("gSurface"); if (surface == null) { alert("Не могу найти нужную поверхность!\nОтмена загрузки..."); return; } } //]]>--> </script> <style type="text/css" media="all"> <!--<![CDATA[ html, body { width: 100%; height: 100%; padding: 0; margin: 0; overflow: hidden; } body { background-color: black; } #gSurface { position: relative; width: 100%; height: 100%; } #gBox { position: absolute; z-index: 2; width: 100px; height: 100px; background-color: white; } /*]]>*/--> </style> </head> <body id="gContainer" onload="loadGame()"> <div id="gSurface"> </div> </body> </html>
ID тела документа – gContainer, а ID перерисовываемой поверхности – gSurface. Я также создал функцию, выполняющуюся всякий раз при загрузке страницы (не обновлении, а именно загрузке). В ней инициализируется передвигаемый слой (gBox).
Инициализация сцены.
Мы хотим расположить объект точно в центре документа, поэтому нам нужны точные координаты. В этом случае нельзя использовать метод приближенного центрирования. Воспользуемся следующей формулой:
(ширина поверхности – ширина объекта)/2
(высота поверхности – высота объекта)/2
Перед тем как записать эту формулу на JavaScript, нужно определить класс движущегося объекта. Я назвал его «_box» (я ставлю символ подчеркивания в начале имен всех моих классов JavaScript). Он содержит x и y-координаты слоя, пропорции, скорость (в пикселях за такт) и параметры движения.
//////////////////////////// Классы //////////////////////////// function _box(x, y, w, h, speed) { this.x = x; this.y = y; this.w = w; this.h = h; this.speed = speed; this.ix = false; this.dx = false; this.iy = false; this.dy = false; }
Ix, dx, iy и dy – флаги увеличения (increase) и уменьшения (decrease) координат x и y. Так как сначала объект должен покоиться, установим все четыре флага в false. Объект не движется ни вниз, ни вверх, ни влево, ни вправо. Теперь давайте напишем функцию, создающую движущийся объект. Для этого понадобится объявить глобальную переменную box.
var surface = null, box = null;
Объявив её, можно перейти к написанию функции:
//////////////////////////// Загрузка игры ////////////////////////// function prepareBox() { box = new _box(0, 0, 100, 100, 8); box.x = (getScreenSize(SCREEN_X) - box.w) / 2; box.y = (getScreenSize(SCREEN_Y) - box.h) / 2; }
Если Вы новичок в объектно-ориентированном программировании на JavaScript, поясняю, что я определил экземпляр класса _box как переменную box. Параметрами конструктора являются начальные значения свойств. Обратиться к свойству созданного экземпляра можно посредством точки и имени нужного свойства. Как Вы могли заметить, я использовал ещё не определенную функцию getScreenSize. Нам нужно быстро получать размеры экрана, а так как соответствующие команды для разных браузеров различны, я решил выделить определение размеров в самостоятельную функцию:
//////////////////////////// Экран ///////////////////////////// function getScreenSize(s) { if (s == SCREEN_X) { if (bIE) return document.body.offsetWidth; return window.innerWidth; } else { if (bIE) return document.body.offsetHeight; return window.innerHeight; } }
У нас есть класс движущегося объекта, функция для его создания и функция определения размеров экрана. Что осталось? Нужна функция, которая будет вызываться всеми нашими обрабтчиками событий, такими как OnKeyUp и OnKeyDown. Нужна также функция, которая будет вызываться каждые 25 милисекунд (это значение хранится в переменной iMs, см. второй листинг). Ну и, разумеется, нужна функция для вывода объекта на экран и передвижения его в целевую точку в соответствии с заданными параметрами движения. Начнем с методов-обработчиков событий. Расположим их перед функцией PrepareBox().
function attachEvents() { document.onkeydown = keyDown; document.onkeyup = keyUp; }
Просто? Да, но больше ничего и не требуется. Теперь надо обработать события клавиатуры:
//////////////////////////// Обработка событий клавиатуры /////////////////////////// function keyDown(e) { switch ((bIE) ? event.keyCode : e.which) { case 37: // left box.ix = false; box.dx = true; break; case 38: // up box.iy = true; box.dy = false; break; case 39: // right box.ix = true; box.dx = false; break; case 40: // down box.iy = false; box.dy = true; break; } } function keyUp(e) { switch ((bIE) ? event.keyCode : e.which) { case 37: // left box.dx = false; break; case 38: // up box.iy = false; break; case 39: // right box.ix = false; break; case 40: // down box.dy = false; break; } }
А теперь позвольте мне пояснить приведеннй код, так как он может оказаться непрозрачным. Здесь я меняю параметры движения объекта. Когда пользователь нажимает на правую стрелку, х-координата объекта должна увеличиваться, а сам объект – двигаться от левой границы экрана к правой. Поэтому я отслеживаю нажатие 39-ой кнопки (это и есть правая стрелка), и как только она нажата, сбрасываю флаг уменьшения х-координаты (нам не нужно двигаться влево) и устанавливаю флаг её увеличения (двигаемся вправо). Прямо противоположные действия нужно осуществить по нажатию левой стрелки (37-ой кнопки). У-координата равна нулю в самом низу экрана. Если я хочу поднять объект вверх, я должен увеличивать у-координату. Для этого я устанавливаю флаг увеличения у-координаты (true) и сбрасываю флаг уменьшения (false). Так как я хочу, чтобы объект двигался только пока нажаты соответствующие кнопки, нужно сбрасывать все флаги, когда кнопку отпускают. Например, если я отпускаю верхнюю стрелку, то это означает, что я не хочу, чтобы объект продолжал двигаться вверх. Поэтому я сбрасываю флаг увеличения у-координаты, что немедленно остановит дальнейшее движение объекта вверх. Если в это время я продолжаю нажимать, например, левую стрелку, то движение влево не остановится, пока я и её не отпущу. А сейчас напишем функцию вывода объекта на экран! При изменении координат следует учитывать, что объект не должен выходить за пределы экрана.
//////////////////////////// Перерисовка ///////////////////// function printBox() { if (box.ix) { if (box.x + box.w + box.speed <= getScreenSize(SCREEN_X)) box.x += box.speed; } if (box.dx) { if (box.x - box.speed >= 0) box.x -= box.speed; } if (box.iy) { if (box.y + box.h + box.speed <= getScreenSize(SCREEN_Y)) box.y += box.speed; } if (box.dy) { if (box.y - box.speed >= 0) box.y -= box.speed; } surface.innerHTML = '<div id="gBox" style="left:' + box.x + 'px;bottom:' + box.y + 'px"></div>'; }
Система координат в HTML/CSS начинается в левом нижнем углу (см. рис. 1, «0»)
H X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X O X X X X X W
Рис. 1 – Система координат
С помощью выражений «X > ширина экрана» и «У > высота экрана» невозможно определить, что объект вышел за пределы экрана. В этом случае объект будет останавливаться только тогда, когда границы экрана достигнет его точка «0» (см. рис. 1). Чтобы объект останавливался по достижении границ экрана точками «H» и «W», нужно добавлять к координате Х в условиях остановки высоту и ширину объекта. То есть, вместо «Х > ширина экрана» используем «W > ширина экрана». Тогда никакая часть объекта не покинет область рисования. Передвинув координаты объекта, выведем его как слой. Заметьте, что я значительно сократил код, поместив всю статическую информацию об объекте в тег style. Мы почти достигли финала! Остался последний шаг. Нужна функция, которая будет вызываться через определенный интервал (25 милисекунд). После этого нужно будет вызвать все написанные функции в теле функции loadGame(), которая пока пуста.
function updateScreen() { surface.innerHTML = ''; printBox(); window.setTimeout('updateScreen()', iMs); }
Очистим поверхность и выведем новый объект. Повторяйте все время! А теперь – последний штрих!
function loadGame() { surface = document.getElementById("gSurface"); if (surface == null) { alert("Can't find the required surface!\nCancelling..."); return; } attachEvents(); prepareBox(); updateScreen(); }
Мы отследили все события, подготовили объект к движению (поместили его в центр экрана и инициализировали глобальную переменную для дальнейшего использования) и вызвали бесконечно повторяющуюся функцию updateScreen(). Вот готовый код:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" /> <title>Простой движущийся слой!</title> <script language="javascript" type="text/javascript"> <!--<![CDATA[ var box = null, surface = null; var SCREEN_X = 0, SCREEN_Y = 1; var bIE = (String(typeof(document.all)) != "undefined"); var iMs = 25; //////////////////////////// Классы //////////////////////////// function _box(x, y, w, h, speed) { this.x = x; this.y = y; this.w = w; this.h = h; this.speed = speed; this.ix = false; this.dx = false; this.iy = false; this.dy = false; } //////////////////////////// Экран ///////////////////////////// function getScreenSize(s) { if (s == SCREEN_X) { if (bIE) return document.body.offsetWidth; return window.innerWidth; } else { if (bIE) return document.body.offsetHeight; return window.innerHeight; } } //////////////////////////// Перерисовка ///////////////////// function printBox() { if (box.ix) { if (box.x + box.w + box.speed <= getScreenSize(SCREEN_X)) box.x += box.speed; } if (box.dx) { if (box.x - box.speed >= 0) box.x -= box.speed; } if (box.iy) { if (box.y + box.h + box.speed <= getScreenSize(SCREEN_Y)) box.y += box.speed; } if (box.dy) { if (box.y - box.speed >= 0) box.y -= box.speed; } surface.innerHTML = '<div id="gBox" style="left:' + box.x + 'px;bottom:' + box.y + 'px"></div>'; } function updateScreen() { surface.innerHTML = ''; printBox(); window.setTimeout('updateScreen()', iMs); } //////////////////////////// События /////////////////////////// function keyDown(e) { switch ((bIE) ? event.keyCode : e.which) { case 37: // влево box.ix = false; box.dx = true; break; case 38: // вверх box.iy = true; box.dy = false; break; case 39: // вправо box.ix = true; box.dx = false; break; case 40: // вниз box.iy = false; box.dy = true; break; } } function keyUp(e) { switch ((bIE) ? event.keyCode : e.which) { case 37: // влево box.dx = false; break; case 38: // вверх box.iy = false; break; case 39: // вправо box.ix = false; break; case 40: // вниз box.dy = false; break; } } //////////////////////////// Загрузка игры ////////////////////////// function prepareBox() { box = new _box(0, 0, 100, 100, 8); box.x = (getScreenSize(SCREEN_X) - box.w) / 2; box.y = (getScreenSize(SCREEN_Y) - box.h) / 2; } function attachEvents() { document.onkeydown = keyDown; document.onkeyup = keyUp; } function loadGame() { surface = document.getElementById("gSurface"); if (surface == null) { alert("Не могу найти нужную поверхность!\nОтмена загрузки..."); return; } attachEvents(); prepareBox(); updateScreen(); } //]]>--> </script> <style type="text/css" media="all"> <!--<![CDATA[ html, body { width: 100%; height: 100%; padding: 0; margin: 0; overflow: hidden; } body { background-color: black; } #gSurface { position: relative; width: 100%; height: 100%; } #gBox { position: absolute; z-index: 2; width: 100px; height: 100px; background-color: white; } /*]]>*/--> </style> </head> <body id="gContainer" onload="loadGame()"> <div id="gSurface"> </div> </body> </html>
Наслаждайтесь!