В этот прекрасный, ничем не отличающийся от остальных день (если не брать в расчёт тот факт, что холодная погода разбила в осколки все мечтания о хотя бы паре тёплых дней в этом году что бы поездить на велосипеде), у меня выдалось хорошее настроение. И хорошее оно даже несмотря на то, что я отчаялся ставить хакинтош на свой ноут, и пришёл к неутешительной мысли, что если я хочу хоть что-то написать под iOS, то мне надо копить на продукцию фирмы Эппл. Но собственно пост не об этом, а о небольшой жизни крохотной игрушки, которую мы с вами и попытаемся создать. Честно говоря, в добрые времена я не так уж и много играл в предка нашего творения (которого ещё и не существует), но определённые теплые воспоминания ещё остались в моём сердце. Итак… Танки! Танчики! БатлТанкс! БатлСити!Писать будем на JavaScript с выводом на canvas.
0. Вместо введения
Ну вот вы и нажали ссылку, и дошли до момента после которого возврата уже нет, и это значит вы хоть раз, да и посмотрите результат того что мы сотворим. Как правило, те кто читают такого рода посты делятся на две категории: первые это те кто читают пост и последовательно пробуют фрагменты кода (для них собственно и пост), и те кто бегло читают пост и в итоге предпочитают глянуть на результат, который я выложу отдельно. Как известно html файлы и JavaScript есть ни что иное, как обычные текстовые файлы, если вы никогда не писали ни первых, ни вторых (в чём я слегка сомневаюсь учитывая аудиторию хабра, но мало ли) то в принципе можно использовать любую блокнотообразную программу для заполнения этих файлов, я вот например пишу в IDE Geany и он практически во всём меня устаривает, браузер для испытаний тоже особо не принципиален, требование к нему лишь одно – что б он был относительно современный, а желательно последней версии. Написание игры разделим на четыре поста:
- Часть первая:
0. Вместо введения;
1. Что мы хотим увидеть;
2. Структура папки игры и HTML файл;
3. Рисуем карту. - Часть вторая:
4. Заселяем танком;
5. Приучаем малыша клоткуокружающей обстановке;
6. Стреляем в стены. - Часть третья:
7. Второй игрок (или компьютерный оппонент);
8. Управление с мобильного устройства под управлением (Android, iOS).
1. Что мы хотим увидеть
И вот для описания скромных планов на игру я выделил отдельную часть поста. В итоге должна получиться игра похожая на танки, которые всем нам знакомы с приставки NES (больше знакомая как в СНГ как Dendy). Из оригинальной игры позаимствуем внешний вид кирпичного блока, бетонный блок, а так же собственно сам первый уровень.
Работать игра должна будет на всех основных браузерах (да-да, и даже под Internet Explorer), а так же попытаемся сделать что бы можно было комфортно играть в браузерах для мобильных устройств (а если точнее, то мобильное Safari, Андроид-браузер, и опять же мобильный ослик).
2. Структура папки игры и HTML файл
Изначально нам нужна папка HTML5Tank в которой разместим всего один файл index.htm, и в ней ещё одна папку data, в которой будут лежать JavaScript файлы. index.htm должен содержать приблизительно следующее:
<html> <head> <meta charset = "utf-8"> <title>HTML5Tank</title> <!-- Сюда будем вписывать наши файлы скриптов, пока добавим лишь одну строку на файл init.js --> <script src="data/init.js"></script> </head> <body> <!-- Наш холст --> <canvas id="game">?!</canvas> <!-- Запуск функции init, которая будет запускать, всё необходимое для отрисовки на холсте --> <script>init();</script> </body> </html>
3. Рисуем карту
Маленький ликбез: тайл – маленький фрагмент, который можно нарисовать например на нашей карте, например фрагмент кирпичной стены, фрагмент травы и прочее. Тайлсет – это изображение на котором нанесены последовательно все необходимые фрагменты.
Файлов картинок использовать не будем, а будет эдакий тайлсет, нарисованный на холст-буффере, из которого будем брать кусочки, и рисовать их на холсте, создавая поле боя. Все элементы тайлов нарисуем прямоугольниками, для того что бы при желании можно было задать им любой размер.
Первоначально создадим в папке data файл init.js и опишем в нём функцию init, которая меняет размер холста и закрашивает его чёрным цветом. Стоит так же отметить, что сразу опишем переменную cellSize которая будет иметь значение ширины большой клетки (она же ширина танка). Создадим в папке data файл init.js и опишем в нём функцию init, которая меняет размер холста.
var size = 32; // ширина игровой ячейки function init () { var canvas = document.getElementById("game"); canvas.width = 16 * size; // Ширина игрового поля canvas.height = 14 * size; // Высота игрового поля var context = canvas.getContext("2d"); // Берём контекст context.fillStyle = "#с0с0с0"; // Цвет заливки context.fillRect(0, 0, canvas.width, canvas.height); }
Далее нам необходимо нарисовать тайлсет, о котором я уже писал, и для интереса выведем его содержимое на основной холст. Впоследствии, этого конечно не будем делать, но сейчас без этого не обойтись. Добавим в html файл, строку:
<script src="data/tiles.js"></script>
Собственно создадим файл tiles.js и нам будет необходимо в нём написать функцию drawTiles. В функции init, создадим новый холст-буффер, и нарисуем в него содержимое тайлсета. Итак, допишем в init следующее:
// Создаём холст-буффер var tileSetBuffer = document.createElement("canvas"); // Нарисуем тайлы в буффер drawTiles(tileSetBuffer); // Нарисуем тайлсет на холст, что бы убедиться, что всё работает, так как надо // После просмотра результата работы строку необходимо удалить context.drawImage (tileSetBuffer, canvas.width/2-tileSetBuffer.width/2, canvas.height/2-tileSetBuffer.height/2);
И что бы был какой-то эффект нам надо нарисовать тайлы:
// Рисуем содержимое "тайлсета" function drawTiles (canvas) { var context = canvas.getContext('2d'); context.globalAlpha = 1; // Пустая клетка var empty = function () { context.fillStyle = '#000'; context.fillRect(0, 0, size/2, size/2); } // Кирпичная стена var brick = function () { // Отрисовка основного цвета кирпича context.fillStyle = "#ffff00"; context.fillRect(0, 0, size/2, size/2); // Отрисовка теней context.fillStyle = "#808080"; context.fillRect(0 , 0 , size/2 , size/16); context.fillRect(0 , 0+size/4 , size/2 , size/16); context.fillRect(size/4 , 0 , size/16, size/4 ); context.fillRect(size/16, size/4 , size/16, size/4 ); // Отрисовка раствора между кирпичами context.fillStyle = "#c0c0c0"; context.fillRect(0 , 3*size/16, size/2 , size/16); context.fillRect(0 , 7*size/16, size/2 , size/16); context.fillRect(3*size/16, 0 , size/16, size/4 ); context.fillRect(0 , 3*size/16, size/16, size/4 ); } // Бетонный блок var hbrick = function () { // Отрисовка основного фона context.fillStyle = "#c0c0c0"; context.fillRect(0, 0, size/2, size/2); // Отрисовка Тени context.fillStyle = "#808080"; context.beginPath(); context.moveTo(0 , size/2); context.lineTo(size/2, size/2); context.lineTo(size/2, 0 ); context.fill(); // Отрисовка белого прямоугольника сверху context.fillStyle = "#fff"; context.fillRect(size/8, size/8, size/4, size/4); } // Размер холста-буффера canvas.width = 3 * size/2; // Ширина буффера canvas.height = size/2; // Высота буффера // Рисуем текстуры context.save(); empty(0, 0); context.translate(size/2, 0); brick(size/2, 0); context.translate(size/2, 0); hbrick(size, 0); context.restore(); }
Если открыть сейчас файл index.htm то можно увидеть первый результат. Вот эти прямоугольнички в центре холста и есть маленькие части нашей большой карты, составлением которой сейчас и займёмся.
Итак, поле боя имеет размеры 13 на 13 фрагментов. Зададим его двумерным массивом (а точнее в силу некоторых особенностей JavaScript массивом массивов), который будет заполнен некоторыми значениями. Договоримся на том, что 0 это будет у нас пустая клетка, 1 – кирпичный блок, 2 – блок бетона.
У меня вышел вот такой массив:
[ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 2, 2, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 2, 2, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1], [2, 2, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 2, 2], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] ];
Но прежде мы создадим некоторый метод, который будет пробегаться по этим значениям и вырисовывать их… опять в буффер. А уже из буффера на основной холст. Зачем это нужно? Сейчас пока не имеет особого значения куда именно рисовать, на холст сразу или в буффер, но в будущем это нам слегка облегчит жизнь.
Итак нам нужен ещё один файл map.js который опять таки создаём в папке data и добавляем строку в index.htm
<script src="data/map.js"></script>
В файле map.js создаём новый класс map:
function map () { var arr; // массив в котором будет храниться карта // Задаём массив this.setArr = function (a) { arr = a; } // Метод рисует карту на холсте this.draw = function (canvas, tileSet) { var ctx = canvas.getContext('2d'); canvas.height = 13 * size; canvas.width = 13 * size; ctx.globalAlpha = 1; // Цикл обрабатывающий массив в котором содержатся значения элементов карты // если попадается 1 то рисуется кирпичный блок // если 2, то бетонная стена for (var j = 0; j < 26; j++) { for (var i = 0; i < 26; i++) { switch (arr[j][i]) { case 0: ctx.drawImage(tileSet, 0, 0, size/2, size/2, i*size/2, j*size/2, size/2, size/2); break; case 1: ctx.drawImage(tileSet, size/2, 0, size/2, size/2, i*size/2, j*size/2, size/2, size/2); break; case 2: ctx.drawImage(tileSet, size, 0, size/2, size/2, i*size/2, j*size/2, size/2, size/2); break; } } } } }
Теперь внесём изменения в функцию init, добавив всё необходимое для того что бы нарисовать карту на холсте:
// Создаём холст-буффер для карты var mapBuffer = document.createElement("canvas"); // Создаём объект field var field = new map(); // Передаём массив-карту field.setArr([ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 2, 2, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 2, 2, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1], [2, 2, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 2, 2], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] ]); // Рисуем карту в буффер field.draw(mapBuffer, tileSetBuffer); context.save(); context.translate(size/2, size/2); // Нарисуем тайлсет на холсте context.drawImage (mapBuffer, 0, 0); context.restore();
Результат

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