В данной статье будет рассказана история разработки одной мобильной игры. Также будут освещены следующие вопросы:
-
Стоит ли использовать 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, то сразу продумайте структуру, разделите все на компоненты, вынесите логику и отрисовку в отдельные файлы. Не жалейте на это времени, в дальнейшем вы будете себе благодарны, и, вернувшись к проекту через какое-то время, вы не будете тратить недели, чтобы понять, что и как там происходит.
-
Займитесь планированием. Потратьте время на планирование проекта, в противном случае вам придется додумывать на ходу, а это всегда плохо. И настанет момент, когда это «додумывание» превратится в «ладно, пусть будет пока так, а потом додумаю» (знакомо, да?! ?).
Ну, собственно, на этом всё. Сейчас эта версия проекта убрана в ящик истории, и идеи, которые лежали в ее основе, были использованы для нового проекта, в ходе разработки которого я стараюсь учитывать все ошибки и недочеты, часть из которых я изложил в этой статье.
Цель же написания этой статьи состоит в том, чтобы показать, что, если у вас есть идея и желание воплотить ее, но пока нет соответствующих навыков, никогда не стоит бояться хотя бы попытаться претворить ее в жизнь. Ошибки, недочеты и неоптимальные решения неизбежны, но все они в конечном счете станут тем ценным опытом, который поможет вам идти дальше, расти над собой, повышать свою квалификацию, а также в конце получать радость выполненной работы.
Также хочется выразить огромную благодарность моему товарищу Александру Леонову который не пожалел времени и сил, чтобы отредактировать данную статью и исправить недочеты.
Разрабатывайте, господа, разрабатывайте чаще!
ссылка на оригинал статьи https://habr.com/ru/post/663316/
Добавить комментарий