Мобильная игра на HTML, CSS, JavaScript, jQuery, Apache Cordova и Firebase. Как сделать красиво снаружи и плохо внутри

от автора

В данной статье будет рассказана история разработки одной мобильной игры. Также будут освещены следующие вопросы:

  • Стоит ли использовать 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 для того, чтобы туда можно было записать количество предмета (они могут объединяться в пачку). Далее нужно найти элемент в инвентаре, чтобы объединить предметы в одну пачку, если предмет уже есть. Ух, перейдем к условиям:

  1. В первом условии проверяем, хватает ли денег, и если не хватает, то показываем сообщение с ошибкой.

  2. Во втором условии проверяем, есть ли уже покупаемый предмет в инвентаре и хватает ли места в инвентаре, если нет, то показываем сообщение, что нет места. Если место есть, то объединяем предметы в одну пачку и проверяем, не переполнился ли инвентарь, и если переполнился, то откатываемся обратно. Если место есть, то отнимаем деньги, обновляем отображение и выводим сообщение, что товар добавлен.

  3. Далее проверяем, кратно ли количество элементов в пачке пяти, если да, то закидываем сгенерированный HTML в инвентарь, если нет, то ищем все изображения внутри инвентаря, а затем берем номер изображения и сравниваем с id элемента, после чего у span удаляем класс hidden, который скрывал количество предметов:

Не спрашивайте, почему всё так плохо, я и сам сейчас не понимаю ?. Ну, и заключительное условие уже просто проверяет есть ли место в инвентаре и если есть, то добавляет предмет.

Чтобы вы понимали масштабы, мы рассмотрели только 200 строк кода, а дальше еще чуть больше 1000 строк кода такого же формата, где-то лучше, где-то даже хуже.

В итоге за 2 недели мы достигли следующего результата:

Заключение

В данной статье я постарался показать вам, насколько все плохо внутри и якобы хорошо снаружи можно сделать. Если вы помните, то проект создавался для конкурса, в котором участвовало 140 команд. Чтобы пройти отборочный этап, нужно было попасть в топ-30 команд. И как вы думаете, смогли мы попасть в топ-50 или хотя бы в топ-100? Конечно ?, смогли, и, более того, мы были на 27 месте по результатам отборочных испытаний. К сожалению, доработать проект было уже нельзя, поэтому мы сосредоточили свои силы на презентации и выступлении. По итогам финала мы были на 9 месте, и это с учетом того, что все, кто был выше нас, либо уже зарабатывали на своих приложениях, либо имели хорошие возможности для заработка. Конечно же, на это повлияло множество факторов: выступление, презентация, внешний вид конечного продукта. Если оценивать только код, то, конечно, мы и не поднялись бы дальше 100го места.

Ну, и в заключение отвечу на вопрос: стоит ли вообще разрабатывать мобильные игры с нуля?

Конечно же, невозможно просто ответить в двух словах, на это влияет множество факторов и на эту тему можно написать отдельную большую статью. В сторону геймдева лезть не будем, но все же можно дать следующие рекомендации:

  1. Определите размер и сложность своей игры. Если вы хотите сделать 2D/3D RPG с большим количеством динамики, анимации и т.п., при этом вы знаете, скажем, только один язык программирования, то следует поискать и изучить инструменты для создания игр, анимации на этом языке.

  2. Продумайте структуру проекта. Если вы решили использовать JS, то сразу продумайте структуру, разделите все на компоненты, вынесите логику и отрисовку в отдельные файлы. Не жалейте на это времени, в дальнейшем вы будете себе благодарны, и, вернувшись к проекту через какое-то время, вы не будете тратить недели, чтобы понять, что и как там происходит.

  3. Займитесь планированием. Потратьте время на планирование проекта, в противном случае вам придется додумывать на ходу, а это всегда плохо. И настанет момент, когда это «додумывание» превратится в «ладно, пусть будет пока так, а потом додумаю» (знакомо, да?! ?).

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

Цель же написания этой статьи состоит в том, чтобы показать, что, если у вас есть идея и желание воплотить ее, но пока нет соответствующих навыков, никогда не стоит бояться хотя бы попытаться претворить ее в жизнь. Ошибки, недочеты и неоптимальные решения неизбежны, но все они в конечном счете станут тем ценным опытом, который поможет вам идти дальше, расти над собой, повышать свою квалификацию, а также в конце получать радость выполненной работы.

Также хочется выразить огромную благодарность моему товарищу Александру Леонову который не пожалел времени и сил, чтобы отредактировать данную статью и исправить недочеты.

Разрабатывайте, господа, разрабатывайте чаще!


ссылка на оригинал статьи https://habr.com/ru/post/663316/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *