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

Создание игры Doodle Jump для Android в Intel XDK за 2 часа на JavaScript с нуля

Время на прочтение 11 мин
Количество просмотров 26K
Здравствуй, Хабр.

Непривычно для меня писать статьи спустя такое долгое время, так как я привык к записи материала на видео, где можно свободно что-либо рассказывать. И так уж получилось, что решил написать статью об Intel XDK, но не просто обзор возможностей, а разобрать среду на примере вполне конкретного проекта. Проектом таким для меня стал «клон» игры «Doodle Jump».

image

Часть 1. Подготовка проекта


Разработку я буду вести на JavaScript с использованием движка PointJS (если что, в гугле находится за минуту).

Самое начало создать новый пустой проект. Тут в принципе все достаточно просто:

2. 1. Внизу находим кнопку «Start a new project», и затем во вкладке «Templates» выбираем «Standart HTML5», и жмем «Create».

image

2. Теперь хитрым щелчком перейдем в настройки проекта (да-да, жать по этой синей папке):

image

3. Дальше в целом все логически понятно:

image

Поясню некоторые пункты:
App ID — это уникальный идентификатор вашего приложения, или же Company Domain
Whitelist — разрешенные запросы, которые приложению будет разрешено совершать
Developer Certificate — сертификат приложения. Если он отсутствует, создайте с помощью этого выпадающего меню:

image

Заполните достоверными данными, во избежание дальнейших проблем.
После заполнения сертификата его можно будет выбрать в качестве сертификата приложения.
Crosswalk Runtime — Определяет, будет ли Cordova интегрирована в ваше приложения (Embedded), или нет (Shared)

4. Заполните иконки:

image

Помните, что размеры и формат должны строго соответствовать требованием среды.

По поводу Splash Screen, это экраны загрузки вашего приложения, которые отображаются пользователю, пока ваше приложение грудится. Как правило это занимает обычно чуть менее 2-х секунд, но за это время вы можете показать, например, логотип чего-нибудь или вставить рекламу.

Так как игру мы делаем для Android, остальные ОС и платформы мы отключаем:

image

На этом с настройкой приложения мы закончили. Переходим к подготовке самого проекта.

Часть 2. Подготовка файлов


Я для создания игры воспользуюсь игровым движком PointJS, удобная штука весом в 60кб, полностью кроссплатформенная и неплохо оптимизированная. Подготовим проект:

1. Откроем папку проекта в ОС (Show in OS):

image

Затем перейдем в папку «www», это и есть наша рабочая папка. Удаляем из нее все, и вставляем движок и с файлом index.html:

image

Эти файлы я скачал с сайта PointJS.

Во вкладке «Develop» мы с вами попадаем в (для кого-то привычный) редактор Brackets, и в файле index.html видим следующий код:

Первоначальный вид index.html
<!DOCTYPE html>
<html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
	<meta name="viewport" content="width=device-width,user-scalable=no"/>
	<title>Минимальный игровой код PointJS</title>
</head>
<body>
<script type="text/javascript" src="point.js"></script>


<script type="text/javascript">

// Объявление движка
var pjs = new PointJS('2d', 400, 400);

// Развертывание игры на всю страницу
pjs.system.initFullPage();

// Ссылка на вектор
var vector = pjs.vector;

// Ссылка на системный лог
var log = pjs.system.log;

// Ссылка на управление игрой
var game = pjs.game;

// Ссылка на конструктор точки
var point = vector.point;

// Ссылка на управление камерой
var camera = pjs.camera;

// Ссылка на управление рисованием
var brush = pjs.brush;

// Ссылка на менеджер объектов
var OOP = pjs.OOP;

// Ссылка на математические методы
var math = pjs.math;

// Ссылка на контроллер и его инициализация
var touch = pjs.touchControl;
touch.initTouchControl();

// Определение игрового цикла 'game'
game.newLoop('game', function () {
        // Заливка фона сплошным цветом
	game.fill('#D9D9D9');

        // Отрисовка надписи "Привет, Мир!"
	brush.drawText({
		x : 10, y : 10,
		text : 'Привет, Мир!',
		size : 30,
		color : '#515151'
	});


});

// Запуск игрового цикла с 60fps
game.startLoop('game');

</script>

</body>
</html>



Тут уже добавлены какие-то переменные, что-то определено заранее (читаем комментарии в коде). В графе «Emulate» настраиваем понравившийся нам телефон и его ориентацию с масштабированием, заодно убеждаемся, что все работает:

image

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

Часть 3. Создание игры
Наверное заметно, что у нас много объявлений разных ссылок и переменных, вынесем их в отдельный файл (новые строки прокомментированы):
init.js
var pjs = new PointJS('2d', 400, 400);
pjs.system.initFullPage();

var vector = pjs.vector;
var log = pjs.system.log;
var game = pjs.game;
var point = vector.point;
var size = vector.size;
var camera = pjs.camera;
var brush = pjs.brush;
var OOP = pjs.OOP;
var math = pjs.math;

var touch = pjs.touchControl;
touch.initTouchControl();

// ширина и высота экрана после развертывания игры
var width = game.getWH().w;
var height = game.getWH().h;

// для единого размера подгонять размеры всех объектов будем с учетом коэффициента
var del = height / 1000 / 5;

// сюда будем записывать delta-time для плавности движения в игре
var dt;

// счет нашего игрока во время прохождения игры
var score = 0;

// После проигрыша с.да запишем счет, набравшийся за уровень
var levelScore = 0;

// получим счет из локального хранилища (если есть) и запомним его как лучший рекорд в oldScore
var tmpScore = localStorage.getItem('score');
var oldScore = tmpScore ? tmpScore : 0;



Фоном в игре «Doodle Jump» служит «тетрадный лист», то есть клеточки, для создания такого эффекта создадим файл grid.js со следующим содержанием:
grid.js
// определим размеры ячейки (в будущем учитываем, что все размеры надо учитывать с коэффициентом del)
var sizeX = 200 * del,
		sizeY = 200 * del;
// сама функция отрисовки
var drawGrid = function () {

        // узнаем, сколько раз нам надо повторить клеточку по X и Y
	var x = width / sizeX,
			y = height / sizeY;
        // Обращаемся к команде отрисовки прямоугольника
	OOP.forXY(x, y, function (x, y) { // первые два аргумента - это повторения по X и Y, затем функция
		brush.drawRectS({
			x : (sizeX + 2)*x, // позиция по X
			y : (sizeY + 2)*y, // по Y
			fillColor : 'white', // цвет заливки
			w : sizeX, h : sizeY // ширина и высота
		});
	});
};


Помним, что фон у нас закрашивается серым цветом, потому, если отрисовать белые прямоугольники на расстоянии 5px друг от друга, то из-за этих промежутков между ними и получится сеточка.

Теперь, чтобы отрисовать сетку, нам достаточно в любом игровом цикле вызвать функцию drawGrid() и у нас фон зальется сеткой:

image

Хорошо, заливать фон сеткой, это конечно хорошо, но мы ведь игру делаем! Поэтому идем дальше!
Теперь, хорошо бы, сделать меню для игры, чтобы игрок видел свой рекорд, видел сколько он набрал за один уровень и смог начать игру сначала, если проиграет (при проигрыше выкидывать в это же меню). Для этого создадим еще один файл и заполним его:

menu.js

// создадим объект, по нажатию на который игрока перекинет в игру
var newGame = game.newTextObject({
	text : 'Новая игра', // надпись на объекте
	font : 'serif', // шрифт надписи
	size : 300*del, // размер шрифта (помним про del)
	color : '#363636' // цвет текста
});

// отпозиционируем объект относительно экранных координат центром в центр
newGame.setPositionCS(point(width/2, height/2));

// и объявим функцию, которая  всеё это дело отрисует и обрабботает
var drawMenu = function () {
        // рисуем объект, созданный выше
	newGame.draw();

        // рисуем рекорды в две строки
	brush.drawTextLinesS({
		x : newGame.x, // выравниваем текст по границе надписи "Новая игра"
		y : newGame.y + newGame.h, // и устанавливаем верхнюю границу отрисовки текста прям под ним
		lines : ['Рекорд: '+Math.abs(oldScore), 'За уровень: '+Math.abs(levelScore)], // помните наши переменные?
		font : 'serif', // шрифт
		size : 250*del, // размер
		color : '#363636' // цвет
	});
	
        // мы с вами создавали ссылку на объект touch, пришло время им воспользоваться
        // проверяем, а не нажал ли игрок по "Новой игре"
	if (touch.isPeekObject(newGame)) { // если все-таки нажал
		createLevel(10); // создаем уровень из начальныхх десяти блоков (потом эту функцию оъявим)
		return game.setLoop('game'); // выходим из текущего игрового цикла в цикл под названием 'game'
	}
	
};



Но мы помним, что файле index у нас запускается сразу цикл «game», давайте это исправим:

Переписываем файл index.html
<!DOCTYPE html>
<html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
	<meta name="viewport" content="width=device-width,user-scalable=no"/>
	<title>Минимальный игровой код PointJS</title>
</head>
<body>
<script type="text/javascript" src="point.js"></script>
<script type="text/javascript" src="init.js"></script>
<script type="text/javascript" src="grid.js"></script>
<script type="text/javascript" src="menu.js"></script>


<script type="text/javascript">
	
game.newLoop('menu', function () {
	game.fill('#D9D9D9');
	drawGrid(); // вот наша сетка
	drawMenu(); // а вот и меню с обработчиком
});

game.startLoop('menu'); // стартуем игровой цикл menu

</script>

</body>
</html>



Смотрим на результат:

image

При нажатии на «Новая игра» у нас возникает ошибка, ведь у нас не определен игровой цикл «game» и нет функции «createLevel», но нас это пока не волнует, создадим с вами игровой цикл «game» и временно поставим запуск игры на него, подправив команду «startLoop».

Опишем игровой цикл:
game.newLoop('game', function () {
        // если не забыли, то сюда мы записываем Delta-Time нашей игре
	dt = game.getDT(20);
	game.fill('#D9D9D9'); // залили фон
	drawGrid(); // отрисовали сеточку
	drawLevel(); // - тут у нас выскочит ошибка
	drawPlayer(); // - тут тоже будет ошибка

        // помним что делает эта команда? Рисуем счет
	brush.drawTextS({
		x : 10, y : 10, // позиция (будет в самом верху экрана)
		text : 'Score: '+Math.abs(score), // текст
		size : 300*del, // размер
		color : '#515151',// цвет
		font : 'serif' // шрифт
	});

});


Сейчас, если игру запустить, у нас полетят ошибки, поэтому функцию drawLevel пока закомментируем, а вот функцию «drawPlayer» сейчас опишем. Создаем новый файл:

pl.js
// первым делом создадим объект плеера
var pl = game.newImageObject({
	x : 0, y : 0, // начальная позиция, но по сути не требуется
	w : 744 * del, h : 670 * del, // размеры. У меня картинка размером 744 на 670, потом я ее сильно уменьшу
	file : 'img/pl.png' // путь к картинке
});

var dy = 2 * del; // сила, движущая объект вниз или вверх
var dmax = 50 * del; // максимальное ускорение
var dx = 0; // вила, движущая объект влево или вправо

// определим ту самую функцию отрисовки персонажа
var drawPlayer = function () {

        // тут подсчитываем счет. Помним, что вверх ось Y уменьшается!
	if (pl.y < score) {
		score = Math.ceil(pl.y);
	}

	pl.draw(); // рисуем персонажа
	pl.move(point(0, dy*dt)); // двигаем его вниз или верх с учетомdelta-time
	dy += dy < dmax ? del*dt : 0; // ускоряем, если максимум скорости вниз еще не достигнут (гравитация)

        // если игрок нажимает на тач (экран)
	if (touch.isDown()) {
		if (touch.getPositionS().x > width/2) // нажатие по правой области экрана (больше половины ширины экрана)
			dx = 30*del; // установим скорость движения положительным числом (вправо двигаться)
		else // иначе
			dx = -30*del; // иначе устанавливаем скорость отрицательным числом (влево)

                // теперь проверим, в какую сторону у нас двигается объект
		if (dx > 0) { // если вправо
			pl.setFlip(0, 0); // сбрасываем зеркалирование (ведь по умолчанию картинка "смотрит" вправо)
			if (pl.x > width) { // если он вышел за пределы экрана
				pl.x = -pl.w; // телепортируем его на другую сторону
			}
		}
		else if (dx < 0) { // если двигается влево
			pl.setFlip(1, 0); // зеркалим картинку
			if (pl.x+pl.w < 0) { // опять же, если вышел за пределы экрана, телепортируем на другую сторону
				pl.x = width; // вот тут
			}
		}


		pl.move(point(dx*dt, 0)); // двигаем влево или вправо уже по факту сам объект
	}

         // помним, что наш счет уменьшается по мере того, как объект поднимается вверх, и если 
         // объект вдруг спустился ниже, чем его максимальная позиция на определенный уровень, то считаем, что он упал
         // а это значит, что игрок проиграл
	if (pl.getPositionS().y > score + 5000 * del) {
		levelScore = score; // устанавливаем счет уровня (помни же, что в меню это выводится?)
		if (score < oldScore) { // но если текущий счет больше, чем предыдущий
			oldScore = score; // устанавливаем новый рекорд
			localStorage.setItem('score', score); // и сразу его сохраняем
		}
		return game.setLoop('menu'); // ну и так как человек проиграл, выкидываем его в меню
	}

       // при отрисовке персонажа, нам желательно так же следить за ним камерой, ведт постоянно поднимается выше и выше.
	camera.moveTimeC(vector.pointPlus(point(width/2, pl.getPositionC().y), point(0, -500*del)), 10);
};


Сначала картинка игрока:

image

Функцию смещения камеры вы можете посмотреть в API к движку. Кратко скажу, что мы плавно двигаем камеру к позиции игрока, при этом слегка сместив камеру ниже на 500*del пикселей, чтобы он был немного внизу. При этом влево и вправо камеру не двигаем, её центр всегда смотрит на половину ширины экрана (width/2).

Если запустим, то увидим следующую картину:

image

Теперь пришло время раскоммментировать функции «createLevel» и «drawLevel», если вы их комментировали. Создадим еще один файл, и заполним его:

blocks.js
// массив с блоками, изначально пустой
var blocks = [];

// позже задействуем, тут будет храниться расстояние между последним и новым блоком
dy2 = false;

// а вот и функция createBlock для создания одного бока (добавляет его после последнего)
var createBlock = function () {
	if (dy2 === false) { // если последний блок еще не был определен, установим начальную позицию для первого блока
		dy2 = height - 60*del*4; // расположим его почти внизу экрана
	} else { // если же позиция имеется, прибавляем к ней рандомно половину высоты прыжка (получено экспериментально)
		dy2 = blocks[blocks.length-1].y - 500*del -
					math.random(500*del, 800*del); // а вот и рандом
	}
	blocks.push(game.newImageObject({ // пушим новый блок в массив
		w : 200 * del*4, h : 60 * del*4, // размеры
		file : 'img/block.png', // картинка для блока
		x : math.random(0, width - 200*del*4), // по горизонтали располагаем его рандомно
		y : dy2, // по высоте используем посчитанное ранее значение
	}));
};

// определим переменную для хранения последнего блока
// последним будет считаться блок, которые последним был перемещен наверх
var oldBlock;

// а вот и функция создания уровня, вы ведь ее уже расскомментировали? Как где? Там где кнопка "Новая игра"
var createLevel = function (i) { // аргументом передаем количество блоков для игры
	pl.y = 0; // так как мы создаем уровень сначала, то и игрока поставим в ноль
	pl.x = 0;
	score = 0; // обнулим текущий счет
	OOP.forInt(i, function () { // запустим цикл по созданию блоков
		createBlock(); // и просто вызовем функцию нужное количество раз
	});
	oldBlock = blocks[blocks.length-1] // запомним какой блок был создан последним, последний - значит самый верхний (ЭТО ВАЖНО)
};

// как только блок окажется очень сильно внизу, мы перекинем его наверх, чтобы не создавать новых
var rePositionBlock = function (el) { // тут принимаем, какой именно блок будем перекидывать
	var x = math.random(0, width - 200*del*4), // генерируем рандомно его позицию по горизонтали
			y = oldBlock.y - 500*del - // обращаем внимание на oldBlock, это последний телепортированный наверх блок
					math.random(500*del, 800*del); // а вот тут снова рандом, который прибавится к его высоте
	el.setPosition(point(x, y)); // и перепозиционируем блок наверх
	oldBlock = el; // так как теперь этот бок выше последнего, он сам становится последним (самым высоким)
};

// функция отрисовки всех блоков и проверки столкновения плеера с ними
var drawLevel = function () {

        // стартуем цикл по блокам
	OOP.forArr(blocks, function (el) {

                // проверим, не оказался ли блок сильно внизу 
		if (camera.getPosition().y + height + 2000*del < el.y) { // и если оказался
			rePositionBlock(el); // перепозиционируем его
		}

		el.draw(); // обязательно отрисуем

                // теперь проверка на столкновение с игроком, если столкнулся
		if (pl.isStaticIntersect(el.getStaticBox()) && dy > 0) { // причем при столкновении учитываем, что он должен падать вниз
			if (pl.y+pl.h < el.y+el.h/3) // возьмем нижнюю позицию нашего игрока, и проверим,  выше ли она одной трети от верха нашего блока, чтобы отталкивался он только если ноги выше центра
				dy = -50*del; // установим силу, двигающую объект
		}
	});
};



Картинка блока:

image

Вот и все, больше никаких файлов создавать не нужно. Игра в своем минимальном виде готова:

image

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

Delta-Time, он же переменная dt, это временной фактор. Время в милисекундах, прошедшее от последнего отрисованного кадра до текущего, используется для сгалживания анимации и скоростей при движении, когда игра запускается на устройствах с разной мощностью.

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

В целом тут сложностей возникнуть не должно, есть вкладка «Build», на которой вы просто выбираете для какой ОС собирать проект, указываете пароль от сертификата, и, это всё. После компиляции вам придет ссылка на загрузку готового проекта на почту, и так же скачать можно прямо из программы. После загрузки файлов с сервера они удаляются.

Скачать исходник проекта: Скачать архив

Для запуска можете открыть файл index.html в Google Chrome, открыть консоль (CTRL+SHIFT+J) и включить режим эмуляции сенсорного экрана, это позволит попробовать поиграть в игру без установки intel XDK.

image

Скачать готовый APK: Скачать APK
Его уже нужно запускать либо в эмуляторе, либо установить на реальное устройство.

Для лучшего понимания процесса разработки прикладываю видео:

Эта же статья в видеоформате
Первый час разработки:



Второй час разработки:


Теги:
Хабы:
+5
Комментарии 13
Комментарии Комментарии 13

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн