О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть

в 13:19, , рубрики: HeroWO, javascript, php, Блог компании Soletude, Герои Меча и Магии, Программирование, разработка игр
О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 1

TL;DR для тех, кому некогда читать™:


Итак, началось всё с хорошо продуманного плана… Подождите, это из другой вселенной. В нашей вселенной всё началось с 2020 года, когда смартфоны ещё продавались официально, но на работу ходить уже было нельзя. В какой-то момент я понял, что нужно переключить свою голову с логической оценки происходящего и проблемы четырёх стен на любую — какую угодно! — задачу трёх тел (главное, чтобы не буквально). Или, перефразируя отца ядерной физики, идея должна была быть достаточно безумной, чтобы за неё взяться. Идея нашлась — и винить в этом следует популярнейшую статью 2018 года «Герои Меча и Магии» в браузере: долго, сложно и невыносимо интересно — написать «клон» третьих «Героев». За месяц.

О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 2

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

(1) День первый

А жив ли мальчик? Расчехляем OSINT

NB: В этой статье я использую две параллельные группы заголовков: одна временна́я, другая смысловая. Чтобы облегчить восприятие, номера дней я указываю так, как будто бы работа шла без перерыва каждый день. Однако дни не нормализованы — в один день я мог провести за проектом 1 час, а в иной — 11 (мой личный рекорд).

Начал я со сбора информации о существующих проектах. Сходу нашёл две очень старые и поныне живущие русские пошаговые MMO (heroeswm с заявленным онлайном в 5 000 игроков и heroesland с онлайном поменьше), русскую же экономическую инкарнацию в виде mlgame, а также heroes-online.com от Ubisoft, аккурат в 2020 благополучно почившую в бозе. Из открытых движков, помимо всем известного VCMI, был относительно новый и очень активный fheroes2 (в отличие от первого, воссоздаёт не третьих «Героев», а вторых). Этим список живых проектов исчерпывался, если не считать многочисленных модов на основе оригинальных «Героев», кучи мобильных игр, мимикрирующих под оригинал, и появившегося в 2020 FreeHeroes от Владимира Смирнова (mapron).

Зато сайтов по тематике игры — великое множество:

Даже на прогрессивном Хабре астрологи регулярно объявляют месяц статей о «Героях» — в прошлый раз это было в феврале (что, впрочем, не удивительно).

Поясню для тех, кто не в курсе нынешней кухни «Героев». Самым старым модом считается WoG (In the Wake of Gods) — в прошлом году была даже статья автора на 20-летие проекта. В середине нулевых WoG перестал развиваться, но на его основе появился скриптовый движок ERA, встраивающийся внутрь процесса «Героев» и творящий всевозможные безобразия нестандартные вещи (с точки зрения оригинала).

На базе ERA делается много мелких модификаций, но более масштабные обычно используют свою платформу. В первую очередь это HotA (Horn of the Abyss), воспринимаемая многими как «современные Герои 3», в особенности по части PvP. (ИЧЗХ, и WoG, и HotA имеют русские корни.)

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

Увидев такое буйство флоры и фауны, я понял, что проект может рассчитывать на свою нишу.

— Очень приятно, Ниша

Ситуация вокруг «Героев» сложилась любопытная. В одном рейтинге игр для ПК «Герои 3» стоят на 4-м месте, обгоняя Warcraft 3 и Mass Effect — но даже исповедуя здоровый скептицизм, нельзя не признать, что игра до сих пор популярна.

О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 3

Все четыре официальных продолжения провалились. Новых мало кто ждёт. Реинкарнации от сторонних разработчиков появляются регулярно, но славы оригинала никто не стяжал даже близко. Весь фандом сосредоточен исключительно на модах к оригинальной игре, что накладывает свои ограничения (только Windows, неадаптивный UI, жёсткие рамки оригинального геймплея, крайняя трудоёмкость разработки).

Получается, миру нужны современные оригинальные «Герои» с широкими возможностями для моддинга, а также с чем-то, чего нет ни у VCMI с его 15-летней историей и 3 057 звёздами на GitHub, ни у очень вкусного fheroes2 с 1 971 звездой. Почему народ не спешит портировать туда свои модификации? Что я могу предложить нового? И есть ли шанс в одиночку довести дело хотя бы до альфа-версии?

Ответ напрашивался сам собой…

О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 4

Если вы хотите расширяемости, то это не про C++. Это даже не очень про Java (см. Xposed). Но это очень даже про JavaScript, особенно если использовать Sqimitive. Вместо API — вся кодовая база; вызывай что хочешь, перекрывай как знаешь. Кто-то спросит — разве можно так писать? Да, если трактористы — женщины! ведь не забудьте: у нас уже есть альтернативы на C++, написанные по всем правилам. Значит, будем писать не по правилам!

Минутка самопиара. Я очень люблю универсальные технологии. Фреймворк Sqimitive — на котором построено всё здание HeroWO и несколько моих проектов поменьше — основан на идее, что любой метод любого объекта есть событие, а на события можно подписываться (с заданным приоритетом), перекрывать имеющиеся обработчики, откладывать их вызов и даже пакетировать (batch). Это позволяет решать множество прикладных задач через единый механизм: например, наследование класса есть просто изменение списка слушателей в подклассе, а множественное наследование — серия таких изменений, и делать их можно после объявления класса (aka mix-ins). Размер всего фреймворка — порядка 1 500 строк, зато документация, описывающая возможности применения — порядка 200 страниц.

Интерпретируемый язык сам по себе сократит время разработки и объём кода, но ведь мы говорим о JavaScript в браузере — а современные браузеры специально предназначены для презентации (очень) сложного контента. HTML и CSS для не очень динамичной игры вроде «Героев» — это что-то уровня червоточины в пространстве: с их помощью я смогу перепрыгнуть через целые пласты игровых подсистем. А проект, может быть, даже сможет выжить.

Продолжая мысль, если расширяемость ставить во главу угла, то ядро движка должно быть гибким, иначе мы получим тех же «Героев» или VCMI, только в профиль. И чем более гибким, тем больше шанс, что в будущем «Герои» в форме HeroWO выйдут-таки из стазиса и станут современной игрой с хардкорным олдскульным нутром, а может даже вберут в себя существующие модификации, а то и целиком старые RTS вроде Disciples!

О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 5

Картинка получалось захватывающая. Решив, что корованы того стоят, я взялся за дело.

(2) День второй

Игровые архивы и форматы данных

Как-то так получилось, что имея пример великолепной статьи lekzd перед глазами, я изначально документировал весь процесс разработки, так что восстановить последовательность событий не составляет труда.

Бо́льшую часть второго дня я провёл за безуспешными попытками скомпилировать h3m-map-convertor и его зависимость homm3tools. «Герои» хранят игровые карты в файлах с расширением .h3m («Heroes 3 Map»), и, насколько мне известно, общедоступных парсеров этого формата существует ровно один — h3mlib от homm3tools (в 2020 ещё появился FreeHeroes со своим парсером). Качество кода h3mlib удручает: ручной разбор данных на Си — занятие неблагодарное, но когда это Си с макросами, то призыв четырёх пони апокалипсиса становится вопросом времени. Впрочем, главная проблема была в том, что даже после танцев с бубном и успешной компиляции библиотека отказывалась читать официальные карты, причём ошибка возникала где-то внутрях Zlib. В процессе всего этого акта меня не покидало ощущение, что h3m-map-convertor, автором которого, собственно, и является lekzd, базировался на какой-то подпольной более новой версии homm3tools, которая не была доступна простым смертным вроде меня.

В конце концов, я оставил попытки собрать собственный конвертер .h3m в JSON, решив, что 11-ти уже сконвертированных карт, которые я смог вытащить с демо-сайта lekzd, мне вполне хватит для начала работы. Забегая вперёд, скажу: мне хватило лишь одной, той самой — «Adventures of Jared Haret» (можете запустить её в HeroWO), а на 105-й день я написал свой парсер, с рулеткой и мета-данными, сотнями проверок, компилятором и прочими шплюшками.

(4) День четвёртый

Поскольку я не собирался делать клон движка lekzd, то рассматривал его JSON-ы как промежуточный формат. В этот день я написал конвертер «lekzd-json» в формат HeroWO, где сделал самое простое преобразование: пересчёт координат объектов с указания правой нижней точки (как в оригинальных «Героях» и во многих других старых играх) на указание левой верхней точки.

О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 6

В этот же день я изучил файлы, задающие параметры различных игровых механик. Вкратце, «Герои» хранят данные в архивах с расширением .lod (а также идентичных .snd и .vid). Таких архивов имеется четыре (открываются через MMArchive):

О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 7

  • H3bitmap.lod — статические изображения (например, портреты героев) и текстовые файлы с константами. Текстовики можно открыть либо в Excel, либо в TxtEdit. Рисунки — 8-битные PCX (это такой BMP эпохи DOS). Да, в «Героях» используется всего 256 цветов! Сразу и не скажешь, правда?

О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 8

  • H3sprite.lod — анимированные изображения с расширением .def. По сути, 8-битная графика с разными способами сжатия. DefPreview умеет их показывать и экспортировать в BMP, а DefTool умеет их собирать.

О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 9

  • Heroes3.snd — игровые звуки (музыка находится в стандартных MP3-шках в самой папке игры); стандартный WAV, но в модуляции DVI-ADPCM — браузеры и многий софт (включая oggenc) её не понимают. Получить «обычный» WAV можно с помощью моей утилитки adpcm2pcm или Audacity или SoX.
  • VIDEO.VID — видеоролики в ныне канувшем в лету (купленном Epic), а на рубеже веков чрезвычайно популярном формате Bink Video. Официальные RAD Game Tools до сих пор запускаются на Windows 10 и позволяют экспортировать кадры и звук.

В репозитории HeroWO есть папка databank с текстовыми файлами — в них я детально описываю данные «Героев», в том числе содержимое архивов и типы используемых файлов и назначение графических и звуковых файлов. В Сети до сих пор нет единого места с такой информацией, поэтому если вы можете что-то дополнить или исправить — пожалуйста, присылайте PR! Обещаю, никто не уйдёт обиженным.

Пользуясь случаем, хочу сказать спасибо нашему соотечественнику Сергею Роженко aka grayface, создавшему в нулевые годы десятки утилит для работы с данными «Героев», исходники которых он выложил на GitHub. Сергей, да пребудет с тобой навечно Сила Delphi!

Разобравшись с форматами в начальном приближении, под конец дня я написал самый первый код для клиентской части: 230 строк (под спойлером), отрисовывающих карту «Приключений Жареда Харета» в базовой версии (как на КДПВ). В последующие годы я до того насмотрелся на эту карту, что мог бы по памяти нарисовать стартовый экран, где героя в проходе зажали Троглодиты… но мы отвлеклись.

Скрытый текст

<!DOCTYPE html>
<html>
  <head>
    <title>HeroWO</title>
  </head>
  <body>
    <div id="herowo"></div>

    <script src="nodash.min.js"></script>
    <script src="sqimitive.min.js"></script>

    <script>
      var ObjectStore = Sqimitive.Base.extend({
        _schema: {},
        _schemaLength: 0,
        _layers: [],
        _layerLength: 0,
        _maxLayer: 0,
        _strideX: 0,
        _strideY: 0,
        _strideXY: 0,
        _maxZ: 0,

        events: {
          // opt:
          //> schema    {prop1: 0, prop2: 1, prop3: 1}
          //> layers    [ [o1p1, o1p2, o2p1, o2p2, ...], [o3p1, o3p2, null, null] ]
          //> strideX   set to 1 if not using that coord (1D array)
          //> strideY   set to 1 if not using that coord (2D array)
          init: function (opt) {
            // Passed arrays are not cloned for performance. Clone them before
            // passing if planning to change.
            this._schema = opt.schema
            this._schemaLength = _.max(_.filter(this._schema, function (v, k) {
              return k.indexOf('.') == -1
            })) + 1
            this._layers = opt.layers
            this._layerLength = this._layers[0].length
            this._maxLayer = this._layers.length - 1
            this._strideX = opt.strideX
            this._strideY = opt.strideY
            this._strideXY = opt.strideX * opt.strideY
            this._maxZ = this._layerLength / this._strideXY / this._schemaLength - 1
            if (Math.floor(this._maxZ) !== this._maxZ) {
              throw new Error('Stride parameters do not match the number of members.')
            }
          },
        },

        // prop - either resolved to integer or name of the outermost prop
        // (not 'foo.bar'). It's used in other methods; numeric argument works
        // faster so pre-resolve property index when doing heavy calculations.
        // Doesn't check if prop exists in the schema.
        // There's no "propertyByIndex()" because multiple properties may live
        // on one index ("union").
        propertyIndex: function (prop) {
          return typeof prop == 'number' ? prop : this._schema[prop]
        },

        // Unlike advance(), doesn't check if x/y/z are within the boundaries.
        toContiguous: function (x, y, z, prop) {
          return (z * this._strideXY + y * this._strideX + x)
                  * this._schemaLength + this.propertyIndex(prop)
        },

        fromContiguous: function (n) {
          var prop = n % this._schemaLength
          n = (n - prop) / this._schemaLength
          var x = n % this._strideY
          n = (n - x) / this._strideY
          var y = n % this._strideX
          n = (n - y) / this._strideX
          return {z: n, y: y, x: x, prop: prop}
        },

        // Wraps around. Stop when returns negative.
        //
        // for (var n = toContiguous(1, 2, 3, 'foo'); n >= 0; n = advance(n, -2))
        //   for (var fooValue, l = 0; null != (fooValue = atContiguous(n, l)); l++)
        //     alert(fooValue)
        advance: function (n, by) {
          n += by * this._schemaLength
          return n >= this._layerLength ? -1 : n
        },

        // If need to retrieve multiple properties of the same object, give
        // prop = 0 and use the passed n:
        // var prop = propertyIndex('foo')
        // findWithin(..., 0, function (..., l, n) { atContiguous(n + prop, l) })
        findWithin: function (sx, sy, sz, ex, ey, ez, prop, func, cx) {
          cx = cx || this
          for (var n = this.toContiguous(sx, sy, sz, prop);
               n >= 0 && (sx != ex || sy != ey || sz != ez);
               n = this.advance(n, +1)) {
            for (var value, l = 0; null != (value = this.atContiguous(n, l)); l++) {
              value = func.call(cx, value, sx, sy, sz, l, n)
              if (value) { return value }
            }
            if (this._strideX <= ++sx) {
              sx = 0
              if (this._strideY <= ++sy) {
                sy = 0
                sz++
              }
            }
          }
        },

        find: function (prop, func, cx) {
          return this.findWithin(0, 0, 0, Infinity, Infinity, Infinity, prop, func, cx)
        },

        // Convention: x/y/z - coords, l - layer (depth), prop - property index
        // or name, n - contiguous index of x/y/z/prop (but not l).
        atCoords: function (x, y, z, prop, l) {
          return this.atContiguous(this.toContiguous(x, y, z, prop), l)
        },

        // Returns == null when there are no more objects at l and below.
        // n must be within boundaries.
        atContiguous: function (n, l) {
          return l > this._maxLayer ? null : this._layers[l][n]
        },
      })

      var HMap = Sqimitive.Base.extend({
        objects: null,    // ObjectStore; do not set

        _opt: {
          state: 'created',   // created, loading, loaded
          url: '',
          format: 0,
          origin: '',
          width: 0,
          height: 0,
          levels: 0,
          difficulty: 0,
          title: 0,
          description: 0,
        },

        fetch: function (url) {
          if (this.get('state') != 'created') {
            throw new Error('Must fetch() only on a new Map.')
          }

          this.set('url', url)
          this.set('state', 'loading')
          var async = new Sqimitive.Async.Fetch({dataType: 'json', url: url + 'map.json'})

          return async
            .whenSuccess(function () {
              this.assignResp(async.response)
              this._fetchObjects()
            }, this)
        },

        _fetchObjects: function () {
          var async = new Sqimitive.Async
          async.nest('o', new Sqimitive.Async.Fetch({dataType: 'json', url: this.get('url') + 'objects.json'}))
          //async.nest('c', new Sqimitive.Async.Fetch({dataType: 'json', url: this.get('url') + 'classes.json'}))

          return async
            .whenSuccess(function () {
              var objects = async.nested('o').response
              this.objects = new ObjectStore(objects)
              this.set('state', 'loaded')
            }, this)
        },
      })

      var el = $('#herowo')
      $('body').css('background', 'cyan')
      var map = new HMap

      map.on('change_state', function (now) {
        if (now == 'loaded') {
          var oclass = this.objects.propertyIndex('class')
          var osubclass = this.objects.propertyIndex('subclass')
          var otexture = this.objects.propertyIndex('texture')
          var owidth = this.objects.propertyIndex('width')
          var oheight = this.objects.propertyIndex('height')
          var ox = this.objects.propertyIndex('x')
          var oy = this.objects.propertyIndex('y')
          var oz = this.objects.propertyIndex('z')
          var oabove = this.objects.propertyIndex('isAbove')
          var omirrorX = this.objects.propertyIndex('mirrorX')
          var omirrorY = this.objects.propertyIndex('mirrorY')
          window.o = this.objects
          this.objects.find(0, function (val, x, y, z, l, n) {
            z = this.atContiguous(n + oz, l)
            if (z == 1) {
              var tn = 0
              var c = this.atContiguous(n + oclass, l)
              var s = this.atContiguous(n + osubclass, l)
              if (c >= 256) {
                tn = s
              }
              x = this.atContiguous(n + ox, l)
              y = this.atContiguous(n + oy, l)
              $('<div>')
                .css({
                  position: 'absolute',
                  left: x * 32,
                  top: y * 32,
                  width: this.atContiguous(n + owidth, l) * 32,
                  height: this.atContiguous(n + oheight, l) * 32,
                  zIndex: this.atContiguous(n + oabove, l) * 10000 + ( (y + this.atContiguous(n + oheight, l)) * 10 + l + 1 ),
                  background: 'rgba(255,0,0,.0) url(../../def-png/' + this.atContiguous(n + otexture, l) + '/0-' + tn + '.png)',
                  //outline: '1px dashed rgba(255, 0, 0, .2)',
                  transform:
                    'scale(' +
                    (this.atContiguous(n + omirrorX, l) ? -1 : +1) +
                    ', ' +
                    (this.atContiguous(n + omirrorY, l) ? -1 : +1) +
                    ')',
                })
                .attr({
                  title: x + ':' + y + ' ' + this.atContiguous(n + oabove, l) + ' ' + this.atContiguous(n + otexture, l)
                })
                .appendTo(el)
            }
          })
        }
      })

      map.fetch('maps/converted/')
    </script>
  </body>
</html>

О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 10

О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 11

Части этого кода пережили все рефакторинги и их можно найти даже в текущей версии: ObjectStore, Map, DOM.Map.

Отрисовка вскрыла и первые проблемы, вторая из которых не решена до сих пор:

  • Объекты на карте могут выходить за её границы. Это очень часто случается с элементами ландшафта — горы и лес не помещаются целиком в рамки карты и должны частично обрезаться. Реализация обрезания усложнила бы циклы: стало бы недостаточно перебирать все точки объекта — нужны проверки, не находится ли точка за пределами карты, чтобы не выйти за границы массива. Альтернатива в виде создания обрезанных вариаций объектов при конвертации карты тоже имела свои недостатки. Я решил эту проблему в стиле Warcraft 3: добавил невидимую область по краям, которую движок воспринимает как полноценную часть карты, но которая перекрывается пользовательским интерфейсом.

О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 12

  • Z-координата (глубина/дальность от глаз пользователя) вычисляется неясным образом. Вначале я думал, что она зависит от порядка следования объектов внутри .h3m и их координат, но после многочасовых экспериментов с редактором карт я оставил попытки разобраться, как же именно она определяется, так что текущая формула выглядит не очень. Кто знает, расскажите!

О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 13

ObjectStore: универсальное хранилище данных HeroWO

В этот день было принято первое судьбоносное решение, которое легло в основу движка и, как говорят за океаном, драматично упростившее ряд вещей в дальнейшем. Решение это мне подсказала всё та же статья (раздел «Хранение данных»). Я приведу оттуда слайд, отлично иллюстрирующий концепцию:

О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 14

Я уже говорил, что люблю универсальные технологии? Так вот, в HeroWO вообще все данные хранятся в виде пары десятков огромных массивов внутри объектов класса докObjectStore. Например, objects.json — хранилище объектов на карте приключений:

О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 15

У каждого хранилища есть своя схема (таблица с именами полей для каждого индекса), размеры и массив слоёв, в каждом из которых «россыпью» хранятся данные отдельных объектов. На скриншоте выше показано начало объекта-героя «Жареда Харета»: первая выделенная строка — значение для свойства actionable (список клеток в границах объекта, с которыми можно взаимодействовать), вторая — actionableFromTop (флаг, допускающий взаимодействие с объектом с меньшим Y) и так далее.

В «Приключениях» всего 3 161 объект (включая тайлы земли); размер схемы (число элементов в массиве на один объект) — 45; итого, длина layers равняется 142 245 элементам. Второе и последнее большое хранилище — effects.json (о нём позже), там 3 046 объектов и 231 496 элементов. Если в Chrome сравнить потребление памяти с массивом из объектов (вида {actionable: "000010", actionableFromTop: true, ...}), то увидим выгоду в 32%: 1 205 004 байта против 822 840 у докObjectStore.

Правда, в текущей версии хранение плохо оптимизировано (например, для самых многочисленных объектов — тайлов — схема в два раза короче, и остальное забивается null-ами), но доработать это сравнительно легко.

Инкапсуляция всех данных в структуре одного типа позволяет делать разные полезные штуки. Например, через месяц после начала работы над проектом я добавил поддержку вложенных хранилищ (массивы слоёв с собственной схемой) — на скриншоте это поле garrison сразу под выделением, где 7 есть creature, а 10 — count. Дальше, в конце схемы (стрелка влево) видно, что два поля имеют одинаковый индекс — это объединение (union) для опциональных полей, которые не могут использоваться одновременно и потому хранятся в общей ячейке (в нашем случае, message существует только у квестовых объектов, а available — только у городов, которые ими не являются). Объединения экономят место и создаются автоматически путём анализа пересечений использования свойств в схеме; например (слева — тип значения, справа — разъединяющая характеристика объектов):

$message:
    array  *str    - quest
            str    - quest
    array  *str    - treasure
            str    - treasure
    array  *str    - event
            str    - event
    array  *str    - monster
            str    - monster
$available:
            non-layered 1D sub-store - town
            non-layered 1D sub-store - dwelling
            non-layered 1D sub-store - hero

В своём движке lekzd использовал такую систему вынужденно ради скорости; я же с самого начала положил её в основу проекта, полагая, что она радикально облегчит сериализацию игрового мира. Для примера, конвертер карт оригинальных «Героев» (h3m2json.php) занимает 4 475 строчек, плюс 602 строки с комментариями. В то же время, сохранение и загрузка карты в формате HeroWO — это просто череда вызовов JSON.stringify()/JSON.parse() без какой-либо подготовки данных. Подкупало и то, что наличие единой точки доступа к данным (в лице докObjectStore) должно было сильно упростить синхронизацию клиентов в многопользовательской игре — и действительно, сейчас там порядка 650 строк (сервер, клиент), что в разы меньше, чем один только парсер карт в homm3tools.

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

Прошло всего 4 дня, а карта уже, считай, готова. График, вроде, выдерживаем, ещё недельку — и ка-ак зарелизим! Казалось бы, что могло пойти не так… О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 16


На днях выйдет вторая часть. Если вам понравилось — пожалуйста, подпишитесь на блог компании, где будут публиковаться все остальные части! Ну, и жду бурления конструктивной полемики в комментариях!

herowo.gameФорумDiscordYouTubeGitHub

Автор: Павел

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js