Содержание

Создание игр на JavaScript: часть 1

От Zanathel

«Передвижение белого квадрата по темному полю»

Создайте Вашу первую игру на JavaScript!

Введение.

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>

Наслаждайтесь!