Как стать автором
Обновить

Пишем игру на JavaScript, наконец-то до конца! (ну или типа того...)

Перед тем, как перейти к делу, небольшое вступление. Этот пост я решил написать по трём причинам:

  • Я хочу поделиться с читателями и сам структурировать данные по полученному результату в своей голове
  • Я так и не нашёл ни одной статьи про геймдев на JavaScript, которая была бы дописана до конца
  • Я таки корыстно хочу получить инвайт :)

Я надеюсь, что люди, которые решат ознакомиться с этим материалом, имеют базовое представление о работе c html, javascript и элементом canvas… Эй… Эй! Ну куда же вы!? Стойте, стойте! Не пугайтесь, я всё равно всё разжую, просто уберу это под спойлеры.

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

«… — Это пока прототип. — Прототип? — Да, прототип… »


Нет, правда, чем больше ссылок и комментариев по коду, тем радостнее я буду. Серьёзно изучать JS я начал с этого проекта, по этому он сырой и не оптимизированный. Конечно, пока это не совсем игра, а скорее технчиеское демо, но это уже больше информации, чем я получил из статей по разработке игр на Хабре (да и, честно говоря, вообще в интернете). Всё гуглилось по ходу дела, все алгоритмы движения я сочинял сам, попутно вспоминая школьный курс тригонометрии. После такого долгого оправдания, для самых нетерпеливых даю ссылку на тех. демо (да, пожалуй остановлюсь на этом термине)

И ещё раз перед самым хабракатом: я не профи и не гуру, я только учусь, если Вам кажется, что я где-то неправ, пожалуйста, не сливайте меня без объяснения причин, мне будет очень интересно узнать что именно не так. А так же Ваш гневный комментарий сбережёт других читателей от ошибки пользования моим мануалом не вникая в суть :) Ну, поехали!


Для начала самое простое. Конец.


Итак, мы будем делать не самый изощрённый симулятор железной дороги. В 2D, вид сверху, графику будем собирать из блоков 64 х 64px.



Начнём, конечно, с самого простого, а так же с того, чем обычно заканчиваются все найденные мной уроки и примеры: нарисуем поле для нашей будущей игры.

Или нет? На собственном опыте я очень хорошо понял, что сразу бросаться в бой не стоит, хорошенько продумайте структуру будущей игры и размещение файлов. Думайте над тем, как называете функции и переменные. Думайте, какие именно аттрибуты в ваши функции можно и нужно передавать. Не начинайте писать ваши замечательные функции как придётся, с мыслью «поправлю потом». У меня таких «поправлю» до сих пор в достатке, о каждой я обязательно расскажу.

А на самом деле...
… сначала мы, конечно же, создадим html-документ примерно следующего вида:

<html>
    <head>
        <meta charset="UTF-8">
        <title>Test</title>
        <script src="src/js/image_load.js" charset="UTF-8"></script>
    </head>
    <body>
        <div>
            <canvas id="trains">Обновите браузер</canvas>
            <script src="src/js/trains_main.js" charset="UTF-8"></script><script src="src/js/trains_playgr.js" charset="UTF-8"></script><script src="src/js/trains_train.js" charset="UTF-8"></script>
        </div>
        <input type="button" onclick="setInterval(t1.calcMove,20); animate();" value="Запустить">
        <div class="info" id="speed"></div>
        <div class="info" id="angle"></div>
        <div class="info" id="posX"></div>
        <div class="info" id="posY"></div>
    </body>
</html>

Тут всё очень просто. Тэг html без доп. пояснений в новых стандартах сообщает браузеру, что нужно двигаться в светлое будущее с HTML5. Тэг canvas будет нашим будущим полотном для работы, а параметр id поможет обратиться к нему с помощью javascript. Так же тут есть много src тегов, какие-то странно обозванные div-ы, и даже один input. На что кто и где ссылается и почему именно в таком порядке, я объясню чуть позже

Ну так вот, давайте-ка для начала подумаем, какая перед нами сейчас стоит задача:

  • Загрузить наши картинки-блоки
  • Написать скрипт для отрисовки основных элементов (на данном этапе только игрового поля)
  • Отрисовать само поле

Итак, уже видно, что можно с чистой совестью создавать три отдельных .js файла. В моём случае это были image_load.js, trains_main.js, trains_playgr.js

Перед подробным разбором кода, расскажу, как имено будем рисовать игровое поле. Для 2д-вид-сверху-игр оптимальным мне кажется следующий вариант: представляем игровое поле в виде квадратной матрицы A[m,n], где каждая клетка A[i,j] это поле определённого типа со своими свойствами. Далее пишем (или генерируем, есть у меня желание такое реализовать, но не добрался пока) нужную нам матрицу и проходимся по ней циклом, рисуя в нужных местах нужные картинки. Вот так, легко и просто. Более подробно для симулятора поезда значения для матрицы у меня следующие:
Значение Что это?
0 пустая клетка (трава)
1 прямой участок Запад-Восток
2 прямой участок Север-Юг
3 поворот Юг-Восток
4 поворот Юг-Запад
5 поворот Север-Запад
6 поворот Север-Восток

image_load.js

Сам код тут прост до безобразия, я даже не уверен, что он требует каких-то комментариев:

 var grass = new Image();
 grass.src = "src/img/grass_3.png";
   
 var railsTurnUL = new Image();
 railsTurnUL.src = "src/img/rails_turn_test.png";
   
 var railsStraight = new Image();
 railsStraight.src = "src/img/rails_straight_f.png";

Создаём объект изображение, присваиваем его аттрибуту src (источник) ссылку на нужную картинку. У меня они лежали в папке src/img, как видно из кода.



Как же так? В таблице аж семь значений, а тут всего три картинки! Но если подумать, больше нам и не нужно, все остальные мы можем получить, повернув исходные railsTurnUL (изображение поворота) и railsStraight изображение прямого участка рельс. Об этом чуть дальше.

Код ерунда, что важно, так это размещение тэга script со ссылкой на этот код в html документе. Вместо использования onload-ов для этих объектов вместе с функциями для подсчёта все ли картинки загрузились, я просто решил разместить тэг script в шапке html документа, а весь остальной код, как обычно, под тегом canvas.

(Вот тут я, кстати, не уверен, что действую правильно, но подробного разбора подобных ситуаций найти не удалось, да и данный способ ни разу не давал сбоев, возможно в следующих версиях я таки добавлю счётчик объектов)

Почему «Как обычно после canvas»? При чём тут вообще порядок?
Как раз недавно на Хабре появилась очень хорошая статья на эту тему, дело просто в порядке загрузки скриптов на странице. В моём случае некоторые параметры canvas-а задавались через train_main.js сразу во время его загрузки. Можно было бы разместить плашку, просящую нерадивого пользователя не жать на кнопку «Плей» раньше времени, можно было бы опять же разбираться с методом onload. Я решил использовать самый, как мне кажется, надёжный способ: просто не загружать скрипт, пока не загрузится canvas элемент

trains_main.js

Вот тут уже становится чуть интереснее:

var main = document.getElementById("trains"); //Наш канвас элемент
var context = main.getContext("2d"); //Его содержимое
 
var cellsize = 64; //Размер одной клетки в пикселях
var widthC = 11; //Ширина игрового поля
var heightC = 8; //Высота игрового поля
var toRad = Math.PI/180; //Удобная переменная для перевода углы в радианы

//Очень полезная функция для поворота загружаемого изображения на определённый угол, которое при этом не трогает всё остальное пространство, подробности ниже

function drawRotatedImage(image, x, y, angle) { 
    context.save(); 

    context.translate(x+cellsize/2, y+cellsize/2);
    context.rotate(angle * toRad);
    
    context.drawImage(image, -cellsize/2, -cellsize/2);
    
    context.restore(); 
 }
   
//Всяческие функции для отрисовки. Рисуют разные элементы игрового поля в указанных координатах X и Y

 function drawUD(x,y) {
    context.drawImage(railsStraight,x,y);
 }
    
 function drawLR(x,y) {
    drawRotatedImage(railsStraight,x,y,-90);
 }
    
function drawDR(x,y) {
    context.drawImage(railsTurnUL,x,y);
}
    
function drawDL(x,y) {
    drawRotatedImage(railsTurnUL,x,y,90);
}
    
function drawUL(x,y) {
    drawRotatedImage(railsTurnUL,x,y,180);
}
    
function drawUR(x,y) {
    drawRotatedImage(railsTurnUL,x,y,270);
}

function drawGrass(x,y) {
    context.drawImage(grass,x,y);
}

//Функция, которая очищает игровое поле

function clear(){
    context.clearRect(0,0,main.width,main.height);
}

В начале идут различные переменные, к которым в процессе будет удобнее обращаться, чем к их значениями напрямую. Далее очень удобная функция, которая поворачивает заданное изображение на нужный угол и вставляет его в нужном месте.

Не совсем
На самом деле эта функция делает буквально следующее:

function drawRotatedImage(image, x, y, angle) { 
// 1) Сохраняем параметры трансформации и свойства холста на данный момент
    context.save(); 

// 2) Переносим начало системы координат в центр нужной клетки
    context.translate(x+cellsize/2, y+cellsize/2); 
// 3) Поворачиваем систему координат на нужный угол
    context.rotate(angle * toRad);  

// 4) Рисуем нашу картинку сместив её так, что бы центр координат на данный момент был в центре картинки    
    context.drawImage(image, -cellsize/2, -cellsize/2); 

// 5) Восстанавливаем параметры нашего canvas-а, начало системы координат снова в левом верхнем углу    
    context.restore(); 
 }

За ней следом отдельные функции для каждого нужного типа картинки. Да, можно было бы обойтись и без этого, но так намного удобнее разбираться в коде, а мы тут вроде как учимся :)

trains_playgr.js

Эта часть кода отвечает за всё, связанное с игровым полем. Опять таки всё довольно просто. В самом начале кода задаётся массив игрового поля по принципу, который описан выше по тексту, далее цикл проходится по этому массиву и рисует нужные нам куски поля

var playgr_1=[
    [0,0,0,0,0,0,0,0],
    [0,3,1,1,1,1,4,0],
    [0,2,0,0,0,0,2,0],
    [0,6,1,4,0,0,2,0],
    [0,0,0,6,1,1,5,0],
    [0,0,0,0,0,0,0,0]
];

function drawField(array) {

    for (var i=0;i<widthC;i++){
        for (var j=0;j<heightC;j++){
            switch (array[j][i]) {
                case 0:
                    drawGrass(cellsize*i,cellsize*j);
                    break;
                case 1:
                    drawGrass(cellsize*i,cellsize*j);
                    drawLR(cellsize*i,cellsize*j);
                    break;
                case 2:
                    drawGrass(cellsize*i,cellsize*j);
                    drawUD(cellsize*i,cellsize*j);
                    break;
                case 3:
                    drawGrass(cellsize*i,cellsize*j);
                    drawDR(cellsize*i,cellsize*j);
                    break;                    
                case 4:
                    drawGrass(cellsize*i,cellsize*j);
                    drawDL(cellsize*i,cellsize*j);
                    break;    
                case 5:
                    drawGrass(cellsize*i,cellsize*j);
                    drawUL(cellsize*i,cellsize*j);
                    break;
                case 6:
                    drawGrass(cellsize*i,cellsize*j);
                    drawUR(cellsize*i,cellsize*j);
                    break;
                default:
                    break;
            }
        }
    }
}


С полем покончено


Вот и всё! Вызов функции drawField(playgr_1); нарисует нам поле с назначенными элементами.



А теперь немного об этих «потом поменяю» элементах, о которых я говорил чуть выше. Так вот, проблемы есть прямо в функции drawField, менял я её прямо по ходу написания этой статьи, а дело в том, что в неё нельзя было передать массив с полем, он в тупую задавался прямо в функции. Думаю не стоит объяснять, почему это плохо, ну или как минимум неудобно.

Сейчас я уже думаю о том, что стоило бы сделать просто отдельный класс игрового поля, в котором помимо матрицы, можно было бы сразу указывать его размер а так же метод отрисовки. Это избавило бы от необходимости менять вручную размер поля при смене матрицы поля да и выглядело бы намного опрятнее.

Прибытие поезда на вокзал Ла-Сьота́


Иии… нет. Прости, Хабр, я честно хотел написать одну большую статью, но это слишком сложно. Писать обучающие статьи оказалось много труднее, чем я думал сначала (на эту я потратил где-то пять часов). Поэтому я предлагаю договор: если эта статья «пойдет», если вам понравится, я обещаю, учитывая всю критику, написать вторую часть как можно быстрее, а в ней будет самое интересное — тонкости просчёта движения и вывода картинки на экран, разжёвывание моего нелепого алгоритма движения поезда, ну и конечно прохладные истории про муки моего вечно спящего мозга при сочинении механики движения поезда по рельсам.

Спасибо Никите Москвину за графику для игры :)
Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.