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

  • Стоит ли использовать 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. Займитесь планированием. Потратьте время на планирование проекта, в противном случае вам придется додумывать на ходу, а это всегда плохо. И настанет момент, когда это «додумывание» превратится в «ладно, пусть будет пока так, а потом додумаю» (знакомо, да?! ????).

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

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

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

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

Комментарии (7)


  1. RikSagara
    28.04.2022 10:21

    Зашквар какойто, в гейм деве такое прям максимум неделя одному делать, ну и делать движок самомум... это прямо большая студия нужна с кучей проектов, чтобы было рентабельно, нет большой студии - unity or unreal выбираем в замисимости от рендора.


    1. DarkStyleee Автор
      28.04.2022 14:14

      Думаю, Вы поспешно написали данный комментарий. Мне кажется, в статье всё описано, и я считаю, что на тот момент за неделю без знаний C#, а уж тем более C++ кроме как потраченного времени мы бы не получили. Поэтому я не совсем понимаю к чему этот комментарий. Если Вы имеете ввиду, что нет смысла писать всё с нуля, то в статье также говориться об этом.


  1. savostin
    28.04.2022 21:18
    -1

    А если я уже знаю ванильный Javascript, можно мне все-таки использовать jQuery? Ну пожалуйста! ;) Он, кстати, переживет еще много библиотек. Да и есть куча форков и аналогов более легковесных под каждые нужды, но с похожим интерфейсом. Лично я никак с него слезть не могу, ну удобно и всё тут. Все эти state-машины, MVС и прочие ништяки ну никак не ускоряют разработку. А всякие React'ы и пр. со своими сборками и кучей файлов, разбросанных по папкам, сводят с ума. Старая школа :(


    1. DarkStyleee Автор
      28.04.2022 21:55

      :), прекрасно Вас понимаю, сам начинал с jQuery и думал что к JS не притронусь. В итоге отказался от jQuery, начал изучать JS и даже начал поглядывать в сторону TypeScript. К сожалению, сейчас все сводится как раз таки к скорости работы, поддержке, оптимизации и типизации. По итогу имеем всякие React, Vue и прочие инструменты, которые по итогу усложняют вход в веб-разработку. С другой стороны, на сегодняшний день приложения настолько большие, что без этих инструментов их поддерживать становится невозможным.


      1. savostin
        28.04.2022 22:25

        Я как раз начинал когда jQuery еще и в проектах не было. Но так к нему и прикипел. Может просто ничего по-настоящему серьезного так и не сделал...


  1. Nikanorburlaku
    29.04.2022 13:09
    +1

    Спасибо, очень интересная статья


    1. DarkStyleee Автор
      29.04.2022 14:32

      Благодарю Вас.