Перед тем, как перейти к делу, небольшое вступление. Этот пост я решил написать по трём причинам:
Я надеюсь, что люди, которые решат ознакомиться с этим материалом, имеют базовое представление о работе c html, javascript и элементом canvas… Эй… Эй! Ну куда же вы!? Стойте, стойте! Не пугайтесь, я всё равно всё разжую, просто уберу это под спойлеры.
Так же я буду рад всем комментариям, замечаниям, пожеланиям и ссылкам на полезные и хорошие статьи, связанные с затрагиваемой мной темой.
Нет, правда, чем больше ссылок и комментариев по коду, тем радостнее я буду. Серьёзно изучать JS я начал с этого проекта, по этому он сырой и не оптимизированный. Конечно, пока это не совсем игра, а скорее технчиеское демо, но это уже больше информации, чем я получил из статей по разработке игр на Хабре (да и, честно говоря, вообще в интернете). Всё гуглилось по ходу дела, все алгоритмы движения я сочинял сам, попутно вспоминая школьный курс тригонометрии. После такого долгого оправдания, для самых нетерпеливых даю ссылку на тех. демо (да, пожалуй остановлюсь на этом термине)
Итак, мы будем делать не самый изощрённый симулятор железной дороги. В 2D, вид сверху, графику будем собирать из блоков 64 х 64px.

Начнём, конечно, с самого простого, а так же с того, чем обычно заканчиваются все найденные мной уроки и примеры: нарисуем поле для нашей будущей игры.
Или нет? На собственном опыте я очень хорошо понял, что сразу бросаться в бой не стоит, хорошенько продумайте структуру будущей игры и размещение файлов. Думайте над тем, как называете функции и переменные. Думайте, какие именно аттрибуты в ваши функции можно и нужно передавать. Не начинайте писать ваши замечательные функции как придётся, с мыслью «поправлю потом». У меня таких «поправлю» до сих пор в достатке, о каждой я обязательно расскажу.
Ну так вот, давайте-ка для начала подумаем, какая перед нами сейчас стоит задача:
Итак, уже видно, что можно с чистой совестью создавать три отдельных .js файла. В моём случае это были image_load.js, trains_main.js, trains_playgr.js
Перед подробным разбором кода, расскажу, как имено будем рисовать игровое поле. Для 2д-вид-сверху-игр оптимальным мне кажется следующий вариант: представляем игровое поле в виде квадратной матрицы A[m,n], где каждая клетка A[i,j] это поле определённого типа со своими свойствами. Далее пишем (или генерируем, есть у меня желание такое реализовать, но не добрался пока) нужную нам матрицу и проходимся по ней циклом, рисуя в нужных местах нужные картинки. Вот так, легко и просто. Более подробно для симулятора поезда значения для матрицы у меня следующие:
Сам код тут прост до безобразия, я даже не уверен, что он требует каких-то комментариев:
Создаём объект изображение, присваиваем его аттрибуту src (источник) ссылку на нужную картинку. У меня они лежали в папке src/img, как видно из кода.

Как же так? В таблице аж семь значений, а тут всего три картинки! Но если подумать, больше нам и не нужно, все остальные мы можем получить, повернув исходные railsTurnUL (изображение поворота) и railsStraight изображение прямого участка рельс. Об этом чуть дальше.
Код ерунда, что важно, так это размещение тэга script со ссылкой на этот код в html документе. Вместо использования onload-ов для этих объектов вместе с функциями для подсчёта все ли картинки загрузились, я просто решил разместить тэг script в шапке html документа, а весь остальной код, как обычно, под тегом canvas.
Вот тут уже становится чуть интереснее:
В начале идут различные переменные, к которым в процессе будет удобнее обращаться, чем к их значениями напрямую. Далее очень удобная функция, которая поворачивает заданное изображение на нужный угол и вставляет его в нужном месте.
За ней следом отдельные функции для каждого нужного типа картинки. Да, можно было бы обойтись и без этого, но так намного удобнее разбираться в коде, а мы тут вроде как учимся :)
Эта часть кода отвечает за всё, связанное с игровым полем. Опять таки всё довольно просто. В самом начале кода задаётся массив игрового поля по принципу, который описан выше по тексту, далее цикл проходится по этому массиву и рисует нужные нам куски поля
Вот и всё! Вызов функции

А теперь немного об этих «потом поменяю» элементах, о которых я говорил чуть выше. Так вот, проблемы есть прямо в функции drawField, менял я её прямо по ходу написания этой статьи, а дело в том, что в неё нельзя было передать массив с полем, он в тупую задавался прямо в функции. Думаю не стоит объяснять, почему это плохо, ну или как минимум неудобно.
Сейчас я уже думаю о том, что стоило бы сделать просто отдельный класс игрового поля, в котором помимо матрицы, можно было бы сразу указывать его размер а так же метод отрисовки. Это избавило бы от необходимости менять вручную размер поля при смене матрицы поля да и выглядело бы намного опрятнее.
Иии… нет. Прости, Хабр, я честно хотел написать одну большую статью, но это слишком сложно. Писать обучающие статьи оказалось много труднее, чем я думал сначала (на эту я потратил где-то пять часов). Поэтому я предлагаю договор: если эта статья «пойдет», если вам понравится, я обещаю, учитывая всю критику, написать вторую часть как можно быстрее, а в ней будет самое интересное — тонкости просчёта движения и вывода картинки на экран, разжёвывание моего нелепого алгоритма движения поезда, ну и конечно прохладные истории про муки моего вечно спящего мозга при сочинении механики движения поезда по рельсам.
- Я хочу поделиться с читателями и сам структурировать данные по полученному результату в своей голове
- Я так и не нашёл ни одной статьи про геймдев на JavaScript, которая была бы дописана до конца
- Я таки корыстно хочу получить инвайт :)
Я надеюсь, что люди, которые решат ознакомиться с этим материалом, имеют базовое представление о работе c html, javascript и элементом canvas… Эй… Эй! Ну куда же вы!? Стойте, стойте! Не пугайтесь, я всё равно всё разжую, просто уберу это под спойлеры.
Так же я буду рад всем комментариям, замечаниям, пожеланиям и ссылкам на полезные и хорошие статьи, связанные с затрагиваемой мной темой.
«… — Это пока прототип. — Прототип? — Да, прототип… »
Нет, правда, чем больше ссылок и комментариев по коду, тем радостнее я буду. Серьёзно изучать JS я начал с этого проекта, по этому он сырой и не оптимизированный. Конечно, пока это не совсем игра, а скорее технчиеское демо, но это уже больше информации, чем я получил из статей по разработке игр на Хабре (да и, честно говоря, вообще в интернете). Всё гуглилось по ходу дела, все алгоритмы движения я сочинял сам, попутно вспоминая школьный курс тригонометрии. После такого долгого оправдания, для самых нетерпеливых даю ссылку на тех. демо (да, пожалуй остановлюсь на этом термине)
И ещё раз перед самым хабракатом: я не профи и не гуру, я только учусь, если Вам кажется, что я где-то неправ, пожалуйста, не сливайте меня без объяснения причин, мне будет очень интересно узнать что именно не так. А так же Ваш гневный комментарий сбережёт других читателей от ошибки пользования моим мануалом не вникая в суть :) Ну, поехали!
Для начала самое простое. Конец.
Итак, мы будем делать не самый изощрённый симулятор железной дороги. В 2D, вид сверху, графику будем собирать из блоков 64 х 64px.

Начнём, конечно, с самого простого, а так же с того, чем обычно заканчиваются все найденные мной уроки и примеры: нарисуем поле для нашей будущей игры.
Или нет? На собственном опыте я очень хорошо понял, что сразу бросаться в бой не стоит, хорошенько продумайте структуру будущей игры и размещение файлов. Думайте над тем, как называете функции и переменные. Думайте, какие именно аттрибуты в ваши функции можно и нужно передавать. Не начинайте писать ваши замечательные функции как придётся, с мыслью «поправлю потом». У меня таких «поправлю» до сих пор в достатке, о каждой я обязательно расскажу.
А на самом деле...… сначала мы, конечно же, создадим html-документ примерно следующего вида:
… сначала мы, конечно же, создадим 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. На что кто и где ссылается и почему именно в таком порядке, я объясню чуть позже
<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>
Ну так вот, давайте-ка для начала подумаем, какая перед нами сейчас стоит задача:
- Загрузить наши картинки-блоки
- Написать скрипт для отрисовки основных элементов (на данном этапе только игрового поля)
- Отрисовать само поле
Итак, уже видно, что можно с чистой совестью создавать три отдельных .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 элемент
Как раз недавно на Хабре появилась очень хорошая статья на эту тему, дело просто в порядке загрузки скриптов на странице. В моём случае некоторые параметры 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();
}
На самом деле эта функция делает буквально следующее:
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, менял я её прямо по ходу написания этой статьи, а дело в том, что в неё нельзя было передать массив с полем, он в тупую задавался прямо в функции. Думаю не стоит объяснять, почему это плохо, ну или как минимум неудобно.
Сейчас я уже думаю о том, что стоило бы сделать просто отдельный класс игрового поля, в котором помимо матрицы, можно было бы сразу указывать его размер а так же метод отрисовки. Это избавило бы от необходимости менять вручную размер поля при смене матрицы поля да и выглядело бы намного опрятнее.
Прибытие поезда на вокзал Ла-Сьота́
Иии… нет. Прости, Хабр, я честно хотел написать одну большую статью, но это слишком сложно. Писать обучающие статьи оказалось много труднее, чем я думал сначала (на эту я потратил где-то пять часов). Поэтому я предлагаю договор: если эта статья «пойдет», если вам понравится, я обещаю, учитывая всю критику, написать вторую часть как можно быстрее, а в ней будет самое интересное — тонкости просчёта движения и вывода картинки на экран, разжёвывание моего нелепого алгоритма движения поезда, ну и конечно прохладные истории про муки моего вечно спящего мозга при сочинении механики движения поезда по рельсам.