4 September 2011

Верхом на танке — Часть 1 — Поле боя

JavaScriptHTMLCanvas
Tutorial
В этот прекрасный, ничем не отличающийся от остальных день (если не брать в расчёт тот факт, что холодная погода разбила в осколки все мечтания о хотя бы паре тёплых дней в этом году что бы поездить на велосипеде), у меня выдалось хорошее настроение. И хорошее оно даже несмотря на то, что я отчаялся ставить хакинтош на свой ноут, и пришёл к неутешительной мысли, что если я хочу хоть что-то написать под 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();

Результат



Ну вот и на первый пост я думаю достаточно, код писался практически одновременно с написание поста и поэтому кое где может прихрамывать. В следующем посте на карту поставим танк и научим его ездить по карте, стрелять и рушить стены и делать ещё немного всяких разных пакостей. Ну вот собственно пока и всё. А так как автор копит на макМини (что бы завалить аппСтор своими графическими хеллоуВорлдами), то и подрабатывать ему иногда надо. Всем спасибо.
Tags:танкиcanvashtml5tutorial
Hubs: JavaScript HTML Canvas
+103
8.1k 164
Comments 49