В данной статье будет рассказана история разработки одной мобильной игры. Также будут освещены следующие вопросы:
Стоит ли использовать jQuery?
Стоит ли вообще разрабатывать мобильные игры на JS с нуля?
Итак, прежде чем начать говорить о разработке, следует немного рассказать следующее:
Как вообще появилась идея разработки мобильной игры?
Почему были выбраны эти инструменты для разработки?
Почему не стоит делать так же?
С чего все началось?
Учебный год в университете подходил к концу, студенты закрывали сессии и долги, а также думали о том, как бы пополнить портфолио и подать на стипендию. Я не был исключением и как раз смог устроиться в один международный проект. Работа заключалась в верстке сайта, переносе и настройке его под WordPress.
К сожалению, одного проекта было недостаточно и как раз проводился международный конкурс. Это был шанс получить ценный опыт и возможность занять призовое место. Номинации были в различных сферах: машинное обучение, 1С, мобильная разработка, Big Data и др. Из всех конкурсов был выбран конкурс «веб-дизайн». Необходимо было продумать и разработать два макета по заданным требованиям, один для мобильных устройств и один для десктопа. Мои товарищи и одногруппники выразили желание поучаствовать в конкурсе и вместе мы записались на конкурс «мобильная разработка». Сначала я пытался везде успеть, но, к сожалению, успеть везде было крайне сложно и в итоге было принято решение сделать упор на командном конкурсе по мобильной разработке. Команда была крайне ответственной и поэтому собраться и поработать не составляло проблем, однако возникла другая проблема: все мы работали с разными технологиями. В итоге у нас была команда из Java, Python и JS-разработчиков. Причем каждый из нас только постигал инструменты и был в состоянии поиска себя как программиста. Я бы сказал, что наш уровень был «Junior--» ?.
На конкурс было отведено 4 недели, первую неделю мы занимались учебой и важными делами. Вторую неделю мы выбирали, что и как мы будем делать. Было много различных идей, но в итоге пришли к тому, что надо попробовать сделать мобильную игру. С учетом того, что в конкурсе участвовало 140 команд от 1 до 4 человек, было принято решение сделать упор на дизайн. Мы, люди, оцениваем в первую очередь внешний вид, причем как людей, так и всего остального. Я убежден в том, что о дизайне нужно заботиться в первую очередь. На теме дизайна и его влияния останавливаться не будем, возможно, на эту тему будет отдельная статья. Нужно было придумать, что наша команда будет разрабатывать, и тут я вспомнил, что хотел когда-то сделать игру про алхимию, где нужно собирать ингредиенты, убивать мобов, прокачивать персонажа и т.д. В старых файлах на компьютере нашлись кое-какие материалы по данной теме, иконки и некоторые наработки. Но их было слишком мало, и потому всю неделю мы занимались поиском иконок и материалов, которые можно было бы использовать, не покупая лицензий. В итоге собрали всяких продуктов, склянок, зелий и прочего.
Далее наша команда занялась проектированием и распределила задачи по важности:
Затем дорисовали ресурсы, сделали рамки и поделили их по качеству:
В итоге получилось 70+ зелий и 150+ ресурсов. Мы продумали для зелий схемы смешивания, после чего занялись интерфейсом. Особо опыта ни у кого не было, но мы постарались как могли, и в итоге был получен следующий дизайн:
Выбор инструментов разработки
Итак, пришло время обсудить инструменты для разработки. Изначально было много всяких вариантов – Gdevelop, Construct, PixiJS, PhaserJS и т.п. Оставалось всего две недели, мы посмотрели некоторые из вышеописанных библиотек и пришли к выводу, что разбираться с ними времени нет. Нам нужен был полный контроль и возможность реализовать всё так, как было в разработанном нами дизайне, потому придумывать ничего не стали и решили использовать старый добрый JavaScript. Приложение достаточно простое, поэтому мы решили сделать его в виде сайта, то есть использовать HTML и CSS для верстки, JS для написания логики, а также Apache Cordova для того, чтобы упаковать всё в apk-файл. С продукцией Apple никто особо знаком не был, потому лезть туда даже не стали. Также подключили Firebase для хранения данных пользователей и jQuery, чтобы было удобнее делать всякие всплывашки, переходы и т.п. В итоге стек был следующий:
HTML, CSS – верстка;
JavaScript – логика приложения;
jQuery – придание интерактивности интерфейсу;
Firebase – БД для хранения данных пользователя;
Apache Cordova – система сборки итогового APK-файла.
Стоит ли использовать jQuery?
Для некоторых это, возможно, спорный вопрос, но для меня ответ очевиден: нет, не стоит. Конечно, если вы поддерживаете старый проект с jQuery, то, само собой, не стоит все переписывать на JS. Однако если вы разрабатываете проекты с нуля, то стоит задуматься об использовании нативного JS. Во-первых, вы будете больше практиковаться и чувствовать себя комфортнее, если вам нужно будет дорабатывать проекты на нативном JS, во-вторых, если jQuery «умрет», то с JS такое вряд ли случится, поэтому с навыками JS вы сможете при надобности быстро изучить любую библиотеку. Этот ответ может показаться очевидным, однако я до сих пор замечаю, как люди используют данную библиотеку, при этом отказываясь изучать JS. При этом изучать нужно в первую очередь именно JS, потому что все библиотеки написаны на нем, и если знать сам язык, то разобраться в новой для себя библиотеке никогда не составит труда.
Процесс разработки
Вот и добрались до разработки, в этом разделе будет показан программный код и почему (с точки зрения автора) он был написан именно так. На верстке и стилях останавливаться не будем и перейдем сразу к главному и единственному файлу с логикой игры. Прежде чем запускать весь код, необходимо было проверить, что устройство готово и Cordova полностью загрузилась. Для этого Apache Cordova предоставляет событие «deviceready»:
document.addEventListener("deviceready", function () { /* остальной код */ });
Далее в callback-функцию помещаем весь остальной код. Приложение использует Firebase и для его работы необходимо описать config со всеми данными. Также инициализируем локальное хранилище (Local Storage), в него будут помещаться все изменения, а при выходе из приложения будет осуществляться сохранение уже в базу данных, тем самым позволяя уменьшить количество запросов на сервер:
document.addEventListener("pause", saveGame, false);
var storage = window.localStorage;
var firebaseConfig = {
// подключение к firebase
}
firebase.initializeApp(firebaseConfig);
Как видно, при событии «pause» вызывается функция «saveGame». Данная функция отвечает за сохранение данных в Local Storage. Событие «pause» предоставляет Cordova, и срабатывает оно в момент сворачивания приложения, а точнее, когда приложения переходит в фоновый режим. Далее рассмотрим главный объект «gameState» со всеми параметрами:
let gameState = {
crystal: 0, // донатная валюта
money: 0, // основная валюта
currentStamina: 10, // текущая выносливость
maxStamina: 10, // максимальная выносливость
rechargeStaminaTime: 60, // время восстановления ед. выносливости
exitGameTime: Math.round(Date.now() / 1000), // время выхода из игры
inventorySize: 0, // текущая заполненность инвентаря
maxInventorySize: 20, // максимальное количество ячеек в инвентаре
inventory: [], // инвентарь
recipes: [], // открытиые рецепты зелий
recipesMax: 0, // максимальное количество рецептов
storeItems: [], // предметы в магазине
books: {
common: true, // книга зелий (обычная)
rare: false, // книга зелий (редкая)
mythical: false, // книга зелий (мифическая)
legendary: false, // книга зелий (легендарная)
},
booksDesc: [
// Описание и параметры книг
{
id: "rare-scroll",
name: "Редкий свиток",
cost: 2500,
category: ["rare", "редкое"],
isBuy: false,
},
{
id: "mythical-scroll",
name: "Превосходный свиток",
cost: 5000,
category: ["mythical", "превосходное"],
isBuy: false,
},
{
id: "legendary-scroll",
name: "Легендарный свиток",
cost: 7500,
category: ["legendary", "легендарное"],
isBuy: false,
},
],
search_area: [
// зоны поиска ресурсов и параметры затрат энергии и времени
{
id: "forest_area",
stamina: 1,
time: 15,
},
{
id: "grot_area",
stamina: 2,
time: 15,
},
{
id: "ruin_area",
stamina: 3,
time: 15,
},
],
chestPrice: {
// сундуки и стоимость
chestGold: 15,
chestCrystall: 1,
},
donat: [
// донатные предметы
{
id: "plus_item",
name: "Зелье выпадения вещей",
desc: "Увеличивает на 1 количество выпадаемых предметов (макс 3).",
count: 1, // бонус предмета
cost: 2, // стоимость
buyCount: 0, // количество купленных
},
{
id: "plus_percent",
name: "Зелье увеличения шанса",
desc: "Увеличивает шанс выпадения более редких предметов на 10% (макс 5)",
count: 0.1,
cost: 2,
buyCount: 0,
},
{
id: "plus_stamina",
name: "Зелье восстановления",
desc: "Восстанавливает 10 ед энергии",
count: 10,
cost: 2,
buyCount: 0,
},
{
id: "plus_inventory",
name: "Увеличение рюкзака",
desc: "Увеличивает вместимость инвентаря на 5 (макс увеличений 10)",
count: 5,
cost: 2,
costGold: 400,
buyCount: 0,
},
],
maxItemDrop: 2, // количество стака выпадаемых предметов
maxItemStack: 5, // количество максимальных стаков в инвентаре
itemDropCount: 2, // количество выпадаемых вещей
chestItemDrop: 1, // количество выпадаемых вещей из сундука
percentItemDrop: [
// параметры для рассчеты шансов выпадения вещей
{
name: "common",
dropChance: 0.65,
},
{
name: "rare",
dropChance: 0.15,
},
{
name: "mythical",
dropChance: 0.07,
},
{
name: "legendary",
dropChance: 0.005,
},
],
percentItemDropMulti: [
// множитель шанса для локации и сундуков
{
id: "forest_area",
multi: 1,
},
{
id: "grot_area",
multi: 1.3,
},
{
id: "ruin_area",
multi: 1.7,
},
{
id: "chestGold",
multi: 1.25,
},
{
id: "chestCrystall",
multi: 2,
},
]
}
Тут должно быть все более-менее понятно, а вот дальше уже пойдут страшные вещи.
Рассмотрим методы объекта «gameState». Честно говоря, я не помню логическую последовательность созданных методов ?, поэтому пойдем сверху вниз.
Первый метод реализует продажу предметов:
sellItem(itemID, count) {
let elem = this.inventory.find((el) => el.id == itemID);
if (elem.count >= count) {
elem.count -= count;
this.money += elem.sell * count;
$(".money").find("span").text(this.money);
this.updateInventory();
}
}
Данный метод получает id предмета и его количество (подразумевается, что можно продать как 1 ед., так и всё сразу). В игре это выглядело следующим образом:
Далее достаем элемент из инвентаря и сравниваем его количество с переданным количеством на продажу, затем уменьшаем на переданное количество, прибавляем деньги, обновляем поле в HTML и вызываем метод обновления инвентаря.
Если с методом по «продаже» все понятно, то вот о методе «покупки» такого не скажешь:
buyItem(itemID, num) {
let fItem = itemPack[itemID];
let item = { ...fItem, count: num };
let elem = this.inventory.find((i) => i.id == item.id);
if (gameState.money >= item.buy) {
if (elem && this.inventorySize <= this.maxInventorySize) {
elem.count += item.count;
this.updateInventory();
if (this.inventorySize > this.maxInventorySize) {
elem.count -= item.count;
gameState.showMsg("Инвентарь полон!", "warning");
} else {
this.money -= item.buy * num;
$(".money").find("span").text(this.money);
gameState.showMsg("Товар добавлен в рюкзак!", "success");
if (elem.count % 5 == 0) {
$("#bag").append(`
<div>
<span class="count">${item.count}</span>
<img src="./img/source/${item.category[0]}/${item.id}.png"
alt="${item.name}">
</div>
`);
} else {
$("#bag")
.find("img")
.each(function (i, e) {
let key = e.src.split("/");
let k = key[key.length - 1].split(".")[0];
if (k == elem.id) {
$(this)
.parent()
.find("span")
.removeClass("hidden")
.text(elem.count);
}
});
}
}
} else if (this.inventorySize < this.maxInventorySize) {
this.inventory.push(item);
this.money -= item.buy * num;
$(".money").find("span").text(this.money);
$("#bag").append(`
<div>
<span class="count">${item.count}</span>
<img src="./img/source/${item.category[0]}/${item.id}.png"
alt="${item.name}">
</div>
`);
gameState.showMsg("Товар добавлен в рюкзак!", "suссess");
} else {
gameState.showMsg("Инвентарь полон!", "warning");
}
} else {
gameState.showMsg("Недостаточно средств!", "warning");
}
this.updateInventory();
}
Первое, что бросается в глаза внимательному читателю – а что за itemPack? На самом деле я тоже сначала не мог понять ?. Как оказалось, на 934 строке была загрузка предметов из базы:
let itemPack = "";
let recMax = 0;
function loadItems() {
firebase
.database()
.ref("ingridients")
.once("value")
.then((snapshot) => {
itemPack = snapshot.val();
for (key in itemPack) {
itemPack[key].components ? recMax++ : "";
}
gameState.recipesMax = recMax;
});
}
loadItems();
Догадаться разделить зелья и ингредиенты по отдельным таблицам было невероятно сложно (сарказм ?), и потому есть странный цикл, который подсчитывает, что из всего лежащего является зельем (если есть свойство components у элемента, то это зелье) Странно, никто даже не додумался ввести поле «тип» и дальше по нему определять тип предмета (ингредиент/зелье). Количество рецептов («recMax») мы подсчитывали для того, чтобы отобразить сколько всего зелий можно открыть:
Думаю, многие уже поняли, что так лучше не делать, к чему эти лишние циклы ведь можно просто вынести все элементы в отдельную таблицу и просто подсчитать сколько там элементов или добавить тип, по которому можно определить тот или иной элемент и т.д.
Вернемся к методу «buyItem», с «fItem» все понятно: достаем элемент из общего массива предметов, далее создаем новый объект item с дополнительным свойством count для того, чтобы туда можно было записать количество предмета (они могут объединяться в пачку). Далее нужно найти элемент в инвентаре, чтобы объединить предметы в одну пачку, если предмет уже есть. Ух, перейдем к условиям:
В первом условии проверяем, хватает ли денег, и если не хватает, то показываем сообщение с ошибкой.
Во втором условии проверяем, есть ли уже покупаемый предмет в инвентаре и хватает ли места в инвентаре, если нет, то показываем сообщение, что нет места. Если место есть, то объединяем предметы в одну пачку и проверяем, не переполнился ли инвентарь, и если переполнился, то откатываемся обратно. Если место есть, то отнимаем деньги, обновляем отображение и выводим сообщение, что товар добавлен.
Далее проверяем, кратно ли количество элементов в пачке пяти, если да, то закидываем сгенерированный HTML в инвентарь, если нет, то ищем все изображения внутри инвентаря, а затем берем номер изображения и сравниваем с id элемента, после чего у span удаляем класс hidden, который скрывал количество предметов:
Не спрашивайте, почему всё так плохо, я и сам сейчас не понимаю ?. Ну, и заключительное условие уже просто проверяет есть ли место в инвентаре и если есть, то добавляет предмет.
Чтобы вы понимали масштабы, мы рассмотрели только 200 строк кода, а дальше еще чуть больше 1000 строк кода такого же формата, где-то лучше, где-то даже хуже.
В итоге за 2 недели мы достигли следующего результата:
Заключение
В данной статье я постарался показать вам, насколько все плохо внутри и якобы хорошо снаружи можно сделать. Если вы помните, то проект создавался для конкурса, в котором участвовало 140 команд. Чтобы пройти отборочный этап, нужно было попасть в топ-30 команд. И как вы думаете, смогли мы попасть в топ-50 или хотя бы в топ-100? Конечно ?, смогли, и, более того, мы были на 27 месте по результатам отборочных испытаний. К сожалению, доработать проект было уже нельзя, поэтому мы сосредоточили свои силы на презентации и выступлении. По итогам финала мы были на 9 месте, и это с учетом того, что все, кто был выше нас, либо уже зарабатывали на своих приложениях, либо имели хорошие возможности для заработка. Конечно же, на это повлияло множество факторов: выступление, презентация, внешний вид конечного продукта. Если оценивать только код, то, конечно, мы и не поднялись бы дальше 100го места.
Ну, и в заключение отвечу на вопрос: стоит ли вообще разрабатывать мобильные игры с нуля?
Конечно же, невозможно просто ответить в двух словах, на это влияет множество факторов и на эту тему можно написать отдельную большую статью. В сторону геймдева лезть не будем, но все же можно дать следующие рекомендации:
Определите размер и сложность своей игры. Если вы хотите сделать 2D/3D RPG с большим количеством динамики, анимации и т.п., при этом вы знаете, скажем, только один язык программирования, то следует поискать и изучить инструменты для создания игр, анимации на этом языке.
Продумайте структуру проекта. Если вы решили использовать JS, то сразу продумайте структуру, разделите все на компоненты, вынесите логику и отрисовку в отдельные файлы. Не жалейте на это времени, в дальнейшем вы будете себе благодарны, и, вернувшись к проекту через какое-то время, вы не будете тратить недели, чтобы понять, что и как там происходит.
Займитесь планированием. Потратьте время на планирование проекта, в противном случае вам придется додумывать на ходу, а это всегда плохо. И настанет момент, когда это «додумывание» превратится в «ладно, пусть будет пока так, а потом додумаю» (знакомо, да?! ?).
Ну, собственно, на этом всё. Сейчас эта версия проекта убрана в ящик истории, и идеи, которые лежали в ее основе, были использованы для нового проекта, в ходе разработки которого я стараюсь учитывать все ошибки и недочеты, часть из которых я изложил в этой статье.
Цель же написания этой статьи состоит в том, чтобы показать, что, если у вас есть идея и желание воплотить ее, но пока нет соответствующих навыков, никогда не стоит бояться хотя бы попытаться претворить ее в жизнь. Ошибки, недочеты и неоптимальные решения неизбежны, но все они в конечном счете станут тем ценным опытом, который поможет вам идти дальше, расти над собой, повышать свою квалификацию, а также в конце получать радость выполненной работы.
Также хочется выразить огромную благодарность моему товарищу Александру Леонову который не пожалел времени и сил, чтобы отредактировать данную статью и исправить недочеты.
Разрабатывайте, господа, разрабатывайте чаще!