
Писать будем на 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();
Результат

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