Javascript игра с высокой имитацией кровавой легенды

внешний интерфейс игра Canvas
Javascript игра с высокой имитацией кровавой легенды

предисловие

Первая версия игры была разработана в 2014 году. На стороне браузера используется html+css+js, на стороне сервера используется asp+php, для связи используется ajax, а для хранения данных используется access+mySql. Однако из-за некоторых проблем (node ​​в то время не использовался, написание сложной логики с помощью asp действительно затруднило бы написание; в то время было мало написания на холсте, и рендеринг dom мог легко достичь узкого места в производительности ), он был заброшен. Позже была переделана версия с холстом. Эта статья была написана в 2018 году.

demo

Сводка данных

1. Подготовка перед разработкой

Зачем использовать Javascript для реализации более сложной компьютерной игры

1. Можно реализовать онлайн-игры на стороне ПК с помощью js. С обновлением аппаратной конфигурации ПК и мобильных телефонов, обновлением браузеров и разработкой различных библиотек H5 для js становится все труднее реализовать онлайн-игру. Сложность здесь в основном в двух аспектах: производительность браузера; достаточно ли просто расширить код js, чтобы удовлетворить итерацию игры с чрезвычайно сложной логикой.

2. В текущих js играх мало масштабных для справки. Большинство (почти все) игр, включающих многопользовательские онлайн-игры, хранение данных на стороне сервера и сложные взаимодействия, разрабатываются с использованием флэш-памяти. Но flash все-таки в упадке, а js стремительно развивается, и он может работать, пока есть браузер.

Почему стоит выбрать кровавую легенду 2001 года

Первая причинаностальгия по старым играм; Конечно, другая причина, более важная, заключается в том, что в другие игры я либо не умею играть, либо могу играть, но не имею материала (картинок, звуковых эффектов и т.д.). Чтобы собрать игровую карту, модель персонажа-монстра, карту предмета и экипировки, а затем снова обработать и разобрать для js-разработки, требуется много энергии, я считаю, что это пустая трата времени.

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

возможная трудность

1. Производительность браузера: это должно быть самым сложным моментом. Если игра должна поддерживать 40 кадров, то для расчета js остается только 25 мс на кадр. А поскольку рендеринг обычно потребляет больше производительности, чем вычисления, время, оставшееся для js, составляет всего около 10 миллисекунд.

2. Защита от читерства: как запретить пользователям напрямую вызывать интерфейс или подделывать данные сетевых запросов? Поскольку цель состоит в том, чтобы использовать js для реализации более сложных игр, и любая онлайн-игра должна это учитывать, должны быть относительно зрелые решения. Это не является предметом этой статьи.

2. Общий дизайн

сторона браузера

  1. Рендеринг экрана использует холст.

    По сравнению с dom(div)+css, canvas может обрабатывать более сложный рендеринг сцены и управление событиями.Например, следующая сцена включает четыре изображения: игроки, животные, предметы на земле и самое нижнее изображение карты. (На самом деле есть тени на земле, соответствующие названия, которые появляются, когда мышка указывает на персонажей, животных и предметы, и тени на земле. Для простоты понимания не будем так много рассматривать.)

    复杂事件demo

    В это время, если вы хотите добиться эффекта "щелчок по животному, нападение на животное; щелчок по предмету, поднятие предмета", то вам необходимо отслеживать события для животных и предметов. Если используется метод DOM, возникает несколько сложных проблем:

    • Порядок рендеринга отличается от порядка обработки событий (иногда z-индекс мал и событие нужно обработать первым), и требуется дополнительная обработка. Например, в приведенном выше примере: при клике на монстров и предметы легко кликать по персонажам, поэтому для персонажей необходимо делать обработку «проникновение по событию клика». И порядок обработки событий не фиксирован: если у меня есть навык (например, лечение в игре), который требует освобождения персонажа, то у персонажа должен быть мониторинг событий. Следовательно, нужно ли элементу обрабатывать события и порядок, в котором они обрабатываются, зависит от состояния игры.Привязка события dom больше не может удовлетворить потребности.

    • Трудно поместить связанные элементы в один и тот же узел DOM: например, модель игрока, имя игрока и эффекты навыков на игроке, в идеале в одном узле.<div>или<section>В контейнере легко управлять (чтобы позиционирование нескольких элементов могло наследоваться родительскому элементу, и не нужно заниматься положением отдельно). Но в этом случае с z-index может быть сложно иметь дело. Например, если игрок A находится поверх игрока B, то A будет заблокирован B, поэтому z-индекс A должен быть меньше, но имя игрока A не блокируется именем или тенью B, чего нельзя достичь. Проще говоря,Ремонтопригодность конструкции dom принесет в жертву эффект отображения на экране, и наоборот.

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

  2. Логика рендеринга холста отделена от логики проекта

    Если различные операции рендеринга холста (например,drawImage,fillTextи т.д.) вместе с кодом проекта, то это неизбежно приведет к неподдерживаемому поздней части проекта. Я просмотрел несколько существующих библиотек холста в сочетании с привязкой данных + инструментами отладки vue и создал новую библиотеку холста, Easycanvas (гитхаб-адрес) и поддерживает отладку элементов на холсте с помощью плагина, такого как vue.

    Таким образом, часть рендеринга всей игры становится намного проще: нужно только управлять текущим состоянием игры и обновлять данные в соответствии с данными, отправленными обратно из сокета сервером.«Изменение данных вызывает изменение представления» Эта ссылка отвечает за Easycanvas. Например, в реализации упакованных предметов игрока на рисунке ниже нам нужно только задать положение контейнера пакета, правила расположения каждого элемента в рюкзаке, а затем привязать каждый упакованный предмет к массиву, а затем управлять массивом, а именно Да (за процесс отображения данных на экран отвечает Easycanvas).

    包裹demo

    Например, стиль с 40 элементами в 5 рядах и 8 столбцах может быть передан в Easycanvas в следующей форме (index — это индекс элемента, расстояние между элементами в направлении x равно 36, а расстояние в направлении y это 32). И эта логика неизменяема, как бы ни менялся массив элементов или куда перетаскивался пакет, относительное положение каждого элемента фиксируется. Что касается рендеринга на холсте, то его не нужно учитывать самим проектом, поэтому ремонтопригодность лучше.

    style: {
        tw: 30, th: 30,
        tx: function () {
            return 40 + index % 8 * 36;
        },
        ty: function () {
            return 31 + Math.floor(index / 8) * 32;
        }
    }
    
  3. Послойный рендеринг холста

    Предположение: игре нужно поддерживать 40 кадров, браузер 800 в ширину и 600 в высоту, а площадь 480 000 (далее 480 000 как 1 площадь экрана).

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

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

    так какПрактически невозможно, чтобы кадр был неотличим от предыдущего кадра., экран всегда обновляется.

    Поэтому в этот раз я использовала 3 расположения полотна внахлест. Поскольку обработка событий Easycanvas поддерживает доставку, даже если щелкнуть верхний холст, если ни один элемент не завершает щелчок, последующий холст также может получить это событие. 3 полотна отвечают за UI, землю (карту), спрайты (персонажи, животные, эффекты умений и т.д.):

    layers

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

    • Например, в слое пользовательского интерфейса, поскольку многие пользовательские интерфейсы обычно стационарны, даже если они перемещаются, они не потребуют слишком точной прорисовки, поэтому количество кадров можно соответствующим образом уменьшить, например, до 20. Таким образом, если физическая сила игрока уменьшится со 100 до 20, представление может быть обновлено в течение 50 мс, и игрок не почувствует переключение через 50 мс. Потому что, как физическая силаДанные слоя пользовательского интерфейса трудно изменить несколько раз за короткий промежуток времени, а задержку в 50 мс трудно воспринять людям, поэтому их не нужно часто рисовать.. Если мы сохраняем 20 кадров в секунду, то можно сохранить 10 рисунков области экрана.

    • Другой пример — земля, карта меняется только при движении игрока. Это экономит 1 область экрана на кадр, если игрок не двигается. В связи с необходимостью обеспечения плавности движения игрока максимальное количество кадров на земле не должно быть слишком низким. Если земля 30 кадров, то когда игрок не двигается, он может экономить 30 рисунков области экрана в секунду (в этом проекте карта почти рисуется на экране). И движение других игроков и животных не изменит землю, и нет необходимости перерисовывать слой земли.

    • Максимальная частота кадров слоя спрайтов не может быть уменьшена.Этот слой будет отображать основные части игры, такие как движения персонажей, поэтому максимальная частота кадров установлена ​​на 40.

    Таким образом, область, отрисовываемая в секунду, может составлять от 80 до 100 областей экрана, когда игрок движется, и только 50 областей экрана, когда игрок не движется. В игре игроки останавливаются, чтобы сражаться с монстрами, печатать, упорядочивать предметы и использовать навыки, все стоя на месте.Отрисовка земли не будет запускаться в течение длительного времени, что значительно экономит производительность..

Сервис-терминал

  1. Поскольку цель состоит в том, чтобы реализовать многопользовательскую онлайн-игру на js, сервер использует Node и использует сокет для связи с браузером. В этом также есть преимущество, заключающееся в том, что некоторыеОбщая логика может быть повторно использована на обоих концах, например, определение наличия препятствия в точке с координатами на карте.

  2. Данные, связанные с игрой, такие как игроки и сцены на стороне Node, все хранятся и хранятся в памяти и регулярно синхронизируются с файлами. При каждом запуске службы Node данные считываются из файла в память. Таким образом, когда игроков много, частота чтения и записи файлов увеличивается в геометрической прогрессии, что приводит к проблемам с производительностью. (Позже, для повышения стабильности, был добавлен буфер для чтения и записи файлов, а также был использован метод «память-файл-резервная копия», чтобы избежать повреждения файла, вызванного перезапуском сервера во время чтения и записи).

  3. Сторона Node разделена на несколько уровней, таких как интерфейсы, данные и экземпляры. «Интерфейс» отвечает за взаимодействие с браузером. «Данные» — это некоторые статические данные, такие как название и действие наркотика, скорость и выносливость монстра, и являются частью правил игры. «Экземпляр» — это текущее состояние в игре, например, наркотик на игроке — это экземпляр «данных о наркотиках». Например, «Экземпляр оленя» имеет атрибут «Текущий HP», Олень A может быть 10, Олень B может быть 14, а сам «Олень» имеет только «Начальный HP».

3. Реализация карты сцены

карта сцены

Давайте начнем представлять часть сцены карты, которая все еще зависитEasycanvasрендерить.

считать

Поскольку игрок всегда фиксируется в центре экрана, движение игрока фактически является движением карты. Например, если игрок бежит влево, карту можно сдвинуть вправо. Как только что было сказано, игрок находится в среднем слое из трех полотен, а карта принадлежит нижнему слою, поэтому игрок должен заблокировать карту.

Это кажется разумным, но если на карте есть дерево, то «игрок всегда выше дерева» неверен. На данный момент есть 2 больших решения:

  • Карта многослойная, а "земля" и "земля" разделены. Поместите игрока между двумя слоями, как на картинке ниже, левая сторона — земля, правая сторона — земля, а затем перекройте и нарисуйте, зажав персонажа посередине:

    bround

    Кажется, что это решает проблему, но на самом деле вводит 2 новые проблемы: первая заключается в том, что игрок иногда может быть заблокирован вещами «на земле» (например, деревом), а иногда ему нужно иметь возможность блокировать вещи «на земле». землю" (Например, стоя под этим деревом, голова будет загораживать дерево). Другая проблема заключается в том, что стоимость производительности рендеринга увеличивается. Поскольку игрок постоянно меняется, слой «земля» необходимо часто перерисовывать. Это также нарушает первоначальный дизайн — попытка сохранить визуализацию карты земли, что приводит к более сложному наслоению холста.

  • Карта не многослойная, "земля" рисуется вместе с "землей". Когда игрок находится за деревом, установите непрозрачность игрока на 0,5, например:

    opacity

    Минус в этом только один: тело игрока либо непрозрачное, либо полупрозрачное (что тоже бывает, когда по карте ходят монстры), и это будет не совсем реалистично. Потому что идеальный эффект — это сцена, где тело игрока частично закрыто. Но это удобно для производительности, а код прост в обслуживании, и в настоящее время я использую это решение.

Так как же определить, какие части изображения «карты» являются деревьями? В играх обычно есть большой файл описания карты (фактически массив), в котором используются числа, такие как 0, 1 и 2, для обозначения мест, где можно пройти, мест с препятствиями, мест, являющихся точками телепортации и т. д. «Файл описания» в легенде о крови описывается наименьшим размером 48х32, поэтому действия игрока в легенде будут иметь ощущение «шахматной доски». Чем меньше единица, тем она более гладкая, но чем больший объем она занимает, тем более трудоемким будет процесс генерации этого описания.

Начнем с темы.

выполнить

Я попросил друга помочь мне экспортировать карту "Пляжной провинции" в клиенте Legend of Legends. Она имеет ширину 33600 и высоту 22400, что в сотни раз превышает размер моего компьютера. Чтобы предотвратить взрыв компьютера, его необходимо разделить на несколько частей для загрузки. Поскольку наименьшая единица легенды — 48x32, мы разделили карту на 4900 (70x70) файлов изображений с разрешением 480x320.

Мы устанавливаем размер холста 800x600, так что игроку нужно всего лишь загрузить 9 изображений 3x3, чтобы покрыть весь холст. 800/480=1,67, так почему бы не 2х2? Потому что возможно, что текущая позиция игрока вызывает отображение только части изображения. Нарисовал красивую схему:

tile

Таким образом, для «заполнения» холста необходимо как минимум 9 изображений в расположении 3x3. Но в этом есть скрытая опасность, то есть объем каждого файла фрагмента карты 480х320 должен быть не менее десятков килобайт и более, если его отрисовать, когда нужно, то это заставит персонажей бегать и видеть, что блоки находятся один за другим. Выгружаются, что влияет на опыт. Поэтому я использовал 16 блоков 4x4 для заполнения холста. Это эффект панорамирования картыЗарезервируйте некоторую избыточную область для увеличения времени загрузки файлов изображений, что приводит к предварительной загрузке.. Не нужно считать, тратится ли здесь производительность рендеринга, ведь размер холста 800х600.Когда мы отрисовываем наружу (например, абсцисса какого-то блока 900-1380), то толком не будет" draw", то есть потери производительности не будет. (Давайте поговорим об этом здесь, когда я использую собственный метод холста drawImage для рисования за пределами холста, результатом моего теста является чрезвычайно низкая производительность. И я инкапсулировал собственный метод холста в библиотеке Easycanvas. : когда считается, что часть области рисования выходит за пределы холста Когда рисунок обрезается; когда область рисования полностью выходит за пределы холста, метод drawImage больше не выполняется.)

Мы добавляем контейнер карты на холст через Easycanvas (для хранения 16 тайлов). Верхний левый угол контейнера располагается выше и левее точки браузера (0,0), чтобы гарантировать, что контейнер полностью покрывает холст. Следует отметить следующее:Контейнер карты будет перемещаться лишь незначительно в пределах 1 блока, а максимальное расстояние перемещения по горизонтали и вертикали составляет 480 и 320.. Возьмем в качестве примера горизонтальное направление: если в первом ряду контейнера есть четыре блока Т15, Т16, Т17 и Т18, то когда игрок бежит вправо, четыре блока начинают двигаться влево. Когда игрок пробежит расстояние 480 (на самом деле контейнер пробежал расстояние 480), он может сразу поставить контейнер обратно (отойти на 480, чтобы вернуться в исходную точку), и тогда 4 блока станут Т16, Т17, Т18, T19 Таким образом, стиль контейнера состоит в том, чтобы взять остаток от 480 и 320, а затем добавить соответствующую коррекцию:

var $bgBox = PaintBG.add({
    name: 'backgroundBox',
    style: {
        tx: function () {
            return - ((global.pX - 240) % 480) - 320; // 这里的算法不唯一,对480取余才是重点
        },
        ty: function () {
            return - ((global.pY - 160) % 320) - 175;
        },
        tw: 480 * 4, // 作为容器,宽高可以省略不写,这里写出是便于理解
        th: 320 * 4,
        locate: 'lt', // tx、ty作为左上角顶点传给Easycanvas
    }
});

Затем добавьте в контейнер 16 блоков.Код добавления блоков относительно прост.Вот алгоритм нумерации для каждого блока (при условии, что имя файла изображения, соответствующего каждому блоку, имеет формат 15x16.jpg):

content: {
    img: function () {
        var layer1 = Math.floor((global.pX - 240) / 480) + i - 1;
        var layer2 = Math.floor((global.pY - 160) / 320) + j - 1;
        var block = `${layer1}x${layer2}`;
        return Easycanvas.imgLoader(`${block}.jpg`);
    }
}

Среди них i и j представляют порядковый номер (0-4) блока. Метод расчета слоя не уникален, и его можно корректировать согласно алгоритму контейнера.

Таким образом, когда координаты игрока pX и pY изменяются, карта будет перемещаться. Игрок бежит вправо, а карта перемещается влево (поэтому вышеприведенная tx должна добавить знак минус, tx здесь похожа на вычисленную в синтаксисе vue), позиция контейнера карты определяется игроком. координаты, и он только следует за изменениями координат игрока и перерисовывает, Ему не могут мешать никакие другие данные. так,С одной стороны, данные и представление связаны, а с другой стороны, это также гарантирует, что поток данных является однонаправленным и не будет мешать другим модулям, а также не требует вмешательства других модулей..

4. Реализация уровня пользовательского интерфейса

Затем запустите слой пользовательского интерфейса (поскольку слой спрайтов сложнее, поместите его в конец).

Реализация нижнего пользовательского интерфейса

Нижний пользовательский интерфейс Hot Blood Legend представляет собой относительно большую картинку:

bottom

В дальнейшем эта картинка именуется «нижний пользовательский интерфейс». Размер нижнего пользовательского интерфейса составляет 800x251, что эквивалентно половине площади игрового экрана. Так в начале дизайна было упомянуто, что UI вынесен на отдельный холст, а затем выполнена низкочастотная отрисовка. Так что, кнопки, окна чата и клетки крови нужно вырезать отдельно?

Например, следует ли отделить четыре маленькие синие кнопки справа от нижнего пользовательского интерфейса и написать логику рендеринга отдельно?

Кнопки в нижнем интерфейсе

мыКлюч к решению, нужно ли отделять часть от целого, заключается в том, чтобы увидеть, существует ли ситуация, в которой «целое и часть не визуализируются одновременно».. Например, нижний UI существует в какой-то момент, но кнопка исчезает, тогда кнопку нужно вырезать. Вы спросите: эту часть нужно изменить, например, когда мышка нажимает на кнопку, она светится, значит, ее нужно вырезать? Ответ - не должен. Мы могли бы просто поместить «светящуюся кнопку» там, где находится кнопка, и сделать ее непрозрачность равной 0, а когда мышь нажата, непрозрачность изменится на 1:

UI.add({
    name: 'buttomUI_button1',
    content: {
        img: './button1_hover.png'
    },
    style: {
        opacity: 0 // 宽高、位置不是重点,此处省略
    },
    events: {
        mousedown () {
            this.style.opacity = 1;
        },
        mouseup () {
            this.style.opacity = 0;
        },
        click () {
            // ...
        }
    }
});

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

Сферический бар крови

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

Например, у игрока максимум маны 100, а на данный момент осталось 30, значит можно понять, что нижние 30% рисуют картинку full.png, а верхние 70% рисуют empty.png. Для упрощения логики и производительности вы можете поместить пустой.png в нижний пользовательский интерфейс (см. Предыдущее изображение нижнего пользовательского интерфейса), а затем покрыть его полным.png в соответствии с текущим HP. Это означает, что нет слоя, соответствующего «пустому состоянию», а он используется только как фон, и на него накладывается карта «полного состояния» различной длины в соответствии с текущим состоянием.

На следующем рисунке показано, как достигается «полоска крови» путем наложения полной карты состояния:

ball

можно увидеть,Если объем крови полный, мы можем полностью покрыть полную карту состояния; когда объем крови не заполнен, мы можем вырезать часть изображения полного состояния и покрыть ее пустым шаром.. Мы связываем их диапазон обрезки (параметры sx, sy, sw, sh в Easycanvas, где s — источник, который относится к исходному изображению) со слоем данных и передаем его в Easycanvas (размер полушария в полном состоянии 46x90 ). Существует много переменных расчетов, которые объясняются ниже.

var $redBall = UI.add({
    content: {
        img: 'full_red.png'
    },
    style: {
        sx: 0,
        sw: 46,
        sy: function () {
            return (1 - hpRatio) * 90;
        },
        sh: function () {
            return hpRatio * 90;
        },
        tx: CONSTANTS.ballStartX,
        ty: function () {
            return CONSTANTS.ballStartY + (1 - hpRatio) * 90;
        },
        tw: 46,
        th: function () {
            return 90 * hpRatio;
        },
        opacity: Easycanvas.transition.pendulum(0.8, 1, 1000).loop(),
        locate: 'lt',
    },
});

Независимо от того, как изменяется объем крови, положение шарика слева фиксировано, поэтому tx и sx являются фиксированными значениями. Значение tx — это константа, измеряемая от нижнего пользовательского интерфейса, а sx равно 0, чтобы начать отрисовку с крайнего левого края исходного изображения.

Пусть отношение текущего объема крови к максимальному объему крови равно hpRatio, тогда, когда hpRatio равно 1, объем крови полный. На этом этапе вместо обрезки исходного изображения мы рисуем клетки крови в полный рост. Таким образом, нарисованная высота пропорциональна hpRatio.

Когда объем крови низкий, мы должны начать с середины исходного изображения и рисовать часть от середины к низу. Таким образом, чем меньше hpRatio, тем больше начальная точка отсечения sy. И существует взаимосвязь между начальной точкой sy отсечения в направлении y и высотой отсечения sh: sy+sh=90. Точно так же, чем меньше hpRatio, тем меньше объем крови и тем ниже начальная точка рисунка.

Что касается непрозрачности, давайте сделаем медленный цикл от 0,8 до 1. Это может дать игроку ощущение «течения» клеток крови. (Если мы анимируем несколько изображений, было бы более реалистично вращать их.)

На данный момент разработка сферических кровяных батончиков завершена. Представление полностью зависит от данных, и каждый раз, когда объем крови изменяется, мы рассчитываем новый показатель hpRatio, и клетки крови обновляются соответствующим образом. по-прежнему является односторонним потоком данных от данных к представлениям, что позволяетУбедитесь, что эффект отображения представления управляется только числовыми значениями, что удобно для последующего расширения.. Например, «игроки пьют лекарство для пополнения крови» не нужно заботиться о том, как должен измениться сферический бар крови, его нужно только связать с данными.

Рюкзак (предмет на игроке)

Рюкзаки предполагают крайне сложные взаимодействия, основные моменты:

  • Привязка представления к элементу Array. Когда данные элемента обновляются, необходимо обновить представление. Это самая основная функция.

  • Каждый элемент имеет очень сложные события. Дважды щелкните элемент, который нужно использовать. После клика по элементу предмет перемещается мышью, в это время:

    Если нажать на землю, то нужно бросить предмет на землю (по сути, это отправить запрос на сброс предмета на сервер); если нажать на слот в панели экипировки персонажа, можно одеть или заменить снаряжение; если щелкнуть по слоту на складе, событие снова изменится. Он становится предметом хранения; если щелкнуть по рюкзаку, может быть, положить предмет обратно, а может поменяться местами два предмета... и много-много ситуаций.

  • Рюкзаки можно перетаскивать куда угодно, и они могут сосуществовать с другим диалоговым интерфейсом, таким как рюкзаки. Тогда между несколькими диалоговыми окнами, такими как рюкзаки, обязательно будет иерархическая взаимосвязь вычислений. Я перетаскиваю диалоговое окно рюкзака в диалоговое окно персонажа, поэтому z-индекс рюкзака больше. Если в это время вы нажмете на диалоговое окно персонажа, то z-индекс диалогового окна персонажа должен быть выше. Что, если в это время появится другое диалоговое окно NPC?

  • В легендарной игре я перетаскиваю рюкзак в любое место, затем открываю склад, далее система автоматически устроит: склад появляется слева, а рюкзак сразу перемещается вправо, что удобно для работы игроков. Задействованы некоторые алгоритмы, благодаря которым эти диалоги кажутся игроку «умными».

Предупреждение, впереди сильное предупреждение.

Игроки также могут сделать это:

  • Откройте рюкзак, затем щелкните левой кнопкой мыши по земле, и персонаж начнет бежать. Мышь игрока перемещается и управляет персонажами, которые бегают по карте. Затем мышь перемещается к рюкзаку, останавливается на предмете, а затем поднимает левую кнопку, (@*(#)¥...@(#@#!

  • Если цифра 1 соответствует скиллу, то когда игрок перетаскивает рюкзак, он внезапно прижимает скилл к бутылочке с невинным зельем в рюкзаке (даже если игрок тупой, хотя бы следите, чтобы наш js не сообщал об ошибке) .

  • Здесь опущен случай, который невозможно четко описать в нескольких сотнях слов.

Начните писать код. В первую очередь должен быть рюкзак-контейнер:

var $dialogPack = UI.add({
    name: 'pack',
    content: {
        img: pack,
    },
    style: {
        tw: pack.width, th: pack.height,
        locate: 'lt',
        zIndex: 1,
    },
    drag: {
        dragable: true,
    },
    events: {
        eIndex: function () {
            return this.style.zIndex;
        },
        mousedown: function () {
            $this.style.zIndex = ++dialogs.currentMaxZIndex;
            return true;
        },
    }
});

О стиле сказать особо нечего, давайте просто напишем для zIndex 1. Перетаскивание — это API перетаскивания, предоставляемый Easycanvas, и сказать особо нечего. eIndex события (индекс, используемый Easycanvas для управления инициирующей последовательностью событий, event-zIndex) должен быть синхронизирован с zIndex.В конце концов, какое диалоговое окно игрок увидит выше, какое диалоговое окно должно зафиксировать событие первым. .

Однако нам нужно привязать событие к mousedown: когда игрок нажимает на этот диалог, поднять его zIndex до самого высокого из всех текущих диалогов. У нас все диалоги получают "текущий максимальный zIndex" из общего модуля диалогов. После каждой настройки максимальный zIndex увеличивается на 1 для следующего диалога.

Сначала контейнер такой, а потом содержимое начинает наполняться. Давайте сделаем массив рюкзака global.pack и воспользуемся циклом for, чтобы заполнить 40 ячеек предметами с индексом i:

$dialogPack.add({
    name: 'pack_' + i,
    content: {
        img: function () {
            if (!global.pack[i]) {
                return; // 第i个格子没有物品,就不渲染
            }
            return Easycanvas.imgLoader(global.pack[i].image);
        },
    },
    style: {
        tw: 30, th: 30,
        tx: function () {
            return 40 + i % 8 * 36;
        },
        ty: function () {
            return 31 + Math.floor(i / 8) * 32;
        },
        locate: 'center',
    },
    events: {
        mousemove: function (e) {
            if (global.pack[i]) {
                // equipDetail模块负责展示鼠标指向物品的浮层
                equipDetail.show(global.pack[i], e);
                return !global.hanging.active;
            }
            return false;
        },
        mouseout: function () {
            // 关闭浮层
            equipDetail.hide();
            return true;
        },
        click: function () {
            // 把点了什么物品告诉hang模块
            hang.start({
                $sprite: this,
                type: 'pack',
                index: i,
            });
            return true;
        },
        dblclick: function (e) {
            bottomHang.cancel();
            equipDetail.hide();

            useItem(i);

            return true;
        }
    }
});

Так как рюкзак может меняться каждую секунду, img здесь — это функция, которая динамически возвращает результат. Примечание. Я написал демонстрацию, чтобы протестировать ее, выполнить1и(function () {return 1;})()Разница в производительности потребления невелика и ею можно пренебречь.

В стиле 40 предметов расположены 8х5, а числа 40, 31 и 32 отсчитываются от карты материалов рюкзака. Размер каждой сетки 30x30, и есть 6 быстрых баров предметов (висит на нижнем интерфейсе) легенды о крови, которые также добавляются аналогичным образом, который здесь опущен. Но нужно обратить внимание:Ширина и высота в стиле каждой сетки не могут быть опущены, потому что, когда img пуст, должна быть существующая область объекта, чтобы можно было зафиксировать событие.. Если ширина и высота не указаны, то щелчок по сетке без элементов не вызовет никакого события. Ставим на пустое место предмет, который нужен для фиксации события.

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

Дважды щелкните сетку, затем выполните 3 действия: скройте наложение информации, отмените получение предмета и используйте предмет (отправьте запрос на сервер). В горячей легендарной игре игроку разрешено держать предмет А в руке, а затем дважды щелкнуть предмет Б (но нельзя использовать А вместе с А, потому что после того, как он взял А, нельзя щелкнуть А). Если вы хотите быть полностью последовательным, вы можете удалитьbottomHang.cancelЭто предложение также добавляет логику «при нажатии на сетку, если предмет в сетке уже находится в руке, предмет нельзя использовать».

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

Затем мы запускаем модуль зависания, чтобы понять, что «игрок щелкает, чтобы взять предмет A в рюкзаке, щелкает другой предмет B и меняет местами два предмета». Прежде всего, давайте проясним, с точки зрения кода,Нет разницы между "положить предмет в пустую коробку" и "поменять местами два предмета"., потому что первое можно рассматривать как обмен предметами и пустыми местами. Нам просто нужно передать индексы i и j двух сеток элементов на сервер.

Приблизительная логика такова:

// hang.js

const hang = {};

hang.isHanging = false;
hang.index = -1;
hang.lastType = '';
hang.$view = UI.add({
    name: 'hangView',
    style: {},
    zIndex: Number.MAX_SAFE_INTEGER // 多写几个9也行 
});

hang.start = function ({$sprite, type, index}) {
    if (!this.isHanging) {
        this.isHanging = true;
        this.index = index;
        this.lastType = type;
        this.$view.content.img = $sprite.content.img;
        this.$view.style = {
            tx: () => global.mouse.x, // 把鼠标坐标记录到这里的逻辑不赘述
            ty: () => global.mouse.y,
        };
    } else {
        // 这里只列出上一次点击和本次点击都来自背包的场景
        if (type === 'pack' && this.lastType === 'pack') {
            this.isHanging = false;
            // 假设toServer是发送socket消息给服务端的一个方法
            toServer('PACK_CHANGE', hang.index, index);
        }
    }
};

hang.cancel = function () {
    this.isHanging = false;
    delete this.$view.content.img;
};

export default hang;

Во-первых, модуль зависания имеет объект $view, который висит на уровне пользовательского интерфейса. При щелчке по предмету в рюкзаке передайте изображение предмета для отображения и заставьте $view следовать за указателем мыши. (Конечно, вам также нужно спрятать предмет в рюкзак в это время, поэтому я не буду здесь вдаваться в подробности.)

Когда вызывается отмена, просто уничтожьте img в представлении $ (а также уничтожьте только что упомянутую логику «скрытия предмета в рюкзаке»). Таким образом реализована функция нажатия левой кнопки и "подбора предмета". Если элемент был подобран, вызывается метод toServer для отправки индекса двух элементов на сервер.

Что нужно сделать серверу, так это проверить статус входа в систему игрока, а затем сделать что-то с массивом рюкзака.array[i]=[array[j], array[j]=array[i]][0](На самом деле это обмен элементами i-го и j-го. Я видел, что чужие пишут умнее, и воспользовался этим). (Конечно, если вы работаете с панелью быстрого доступа, вы должны также оценить тип предмета, потому что в эти позиции можно поместить только лекарства и свитки. Я не буду вдаваться в подробности.)

Наконец, сервер отправляет новый массив клиенту, и клиент может его обновить. Похоже, вы закончили?

нисколько! Если есть задержка в сети, вполне вероятно, что игрок хочет поменять местами предметы A и B, а затем отказаться от предмета B. Однако из-за проблем с сетью обмен не был завершен, и была выдана инструкция об отмене. Итак, игрок выбрасывает предмет А. Может быть, предмет А — бесценное сокровище.

Как избежать такого случая? Во-первых, то, что игрок хочет бросить, определяется на основе «изображения предмета в рюкзаке». Чего игрок не должен принимать, так это того, что после выбора предмета Б и его выбрасывания он становится предметом А. Даже если сброс не удался, лучше его перекинуть, чем неправильное выполнение.

Значит, нам нужно решить эту задачу по ID предмета. Когда игрок сбрасывает предмет, мы записываем «ID предмета, который следует за движением мыши» и отправляем его на сервер, чтобы убедиться, чтоДаже когда клиент отображает список элементов, даже если порядок индексов неверен из-за задержки, игрок не будет ошибочно использовать другой элемент.. Конечно, более безопасным способом является перенос индекса и идентификатора элемента, а сервер выполнит еще одну проверку.

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

Таким образом, подвесной модуль осуществляет обмен 2 предметами в рюкзаке. Что касается привязки рюкзака к другим диалоговым окнам, например, поместить графику в рюкзаке в слот снаряжения персонажа, то этого можно добиться, добавив логику в зависание.

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

Пользовательский интерфейс персонажа

После понимания рюкзака реализация персонажа относительно проста.

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

role拆分

затем используйтеБиблиотека EasycanvasЧтобы добавить родительский элемент в качестве рамки, а затем заполнить родительский элемент несколькими дочерними элементами. Мы используем переменную для управления тем, какие панели отображаются в данный момент:

var $role = UI.add({
    name: 'role', // role是角色的意思
    content: {
        img: 'role.png'
    },
    // 事件后面再提
});

$role.add({
    name: 'role-equip', // 第一页是人物装备
    content: {
        img: 'roleEquip.jpg'
    },
    style: {
        // 箭头函数看不习惯的话,也可以写function,当role.index为0是可见
        visible: () => role.index === 0
    }
});

$role.add({
    name: 'role-state', // 第二页是人物状态
    ……

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

Итак, как отслеживать «игрок переносит снаряжение в интерфейсе рюкзака в слот снаряжения пользовательского интерфейса персонажа»?

В первой версии игры я привязал событие "при двойном клике отправить запрос на использование предмета на сервер" только к предмету рюкзака, а игрок, одетый в снаряжение, также использует метод двойного клика снаряжение в рюкзаке (да, у Официалов тоже самое). Изначально я планировал полениться и не делать логику привязки двух UI-диалогов, но позже обнаружил, что этого не избежать, потому что сзади будет UI-склад, и игроки обязательно будут перемещать предметы вручную. Если вы позволите игроку дважды щелкнуть элемент, чтобы получить доступ к операции, я думаю, он обязательно будет помечен как «античеловеческий».

Поэтому я также привязываю событие клика к каждой сетке экипировки персонажа. Помните модуль подвешивания в пользовательском интерфейсе рюкзака? Нажмите на сетку экипировки персонажа, а также вызовите модуль подвески. Когда обнаружим, что в подвесном модуле находится предмет из рюкзака, то кликаем по снаряжению персонажа, чтобы напрямую вызвать команду «использовать снаряжение».

Итак, логика обработки события клика, которое необходимо привязать к каждой сетке в экипировке персонажа, следующая:

  • Если к этому моменту в подвесном модуле уже есть активный «Предмет из пользовательского интерфейса рюкзака», попробуйте надеть этот предмет. (Сервер обнаруживает, что в этом месте уже есть оборудование, поэтому сначала выполнит «выгрузку оборудования».)

  • Если модуль вешания в это время простаивает, а слот уже снаряжен снаряжением, то кинуть его в веш (пользователь забирает надетое на него снаряжение). И, добавьте событие для нажатия на сетку рюкзака: если вы нашли предмет из пользовательского интерфейса персонажа в зависании, выполните «выгрузить снаряжение».

  • Если в модуле зависания в это время уже есть активный "предмет из пользовательского интерфейса персонажа", то сообщите серверу, что я хочу обменять 2 предмета экипировки (например, левую и правую перчатки). Разумеется, сервер проверит, можно ли его обменять, например, обувь нельзя надевать на голову.

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

5. Реализация слоя спрайтов

Слой спрайтов включает в себя основные элементы, такие как персонажи, животные (NPC, монстры, декорации сцены) и навыки. Как упоминалось в начале, FPS этого уровня должен быть не менее 40. Ниже мы представим их по одному:

движение персонажа

Логика данных движения персонажа

Во-первых, движение персонажа будет связано с землей. Персонаж бежит, чтобы изменить координаты x и y в глобальных данных, вызывая эффект перемещения земли. Здесь задействованы два момента:

  • Когда игрок перемещает персонажа, будь то обычный проход или заблокированный препятствием, это решение должно быть сделано на стороне клиента.. Если это делается на стороне сервера, то каждый шаг будет отправлять запрос на сторону сервера, а затем сторона сервера вернет ответ об успешности, не говоря уже о том, заставит ли сетевая задержка у пользователя почувствовать, что операция выполнена. не плавно, одной частоты этого триггера достаточно, чтобы вытеснить сервер. Обычная практика клиентских игр — хранить доступные места на карте в файле, и загружать его на локальный парсинг, когда игрок устанавливает игру. Для веб-игр каждый раз, когда пользователь входит в карту или блок, сервер отправляет данные (большой массив) текущей карты или блока. Конечно, для этих данных лучше всего использовать кеш браузера (localStorage), ведь игра не может часто менять карту.

  • Клиент непрерывно сообщает координаты серверу, сервер обрабатывает их, а затем непрерывно раздает другим игрокам.. Интервал времени отчетности не должен быть слишком длинным. Если об этом сообщается раз в секунду, то игрок B, которого видит игрок A, всегда будет игроком B, который был 1 секунду назад. Вообще говоря, интервал в 0,5 секунды не очень приемлем. Я пошел в интернет-кафе с друзьями, чтобы играть онлайн больше десяти лет назад, и мы побежали вместе,На его экране он бежит немного впереди меня, а на моем экране я бегаю немного впереди, что вызвано интервалом между отчетом клиента и доставкой сервера.. Конечно, если это примерно то же самое, проблем быть не должно. (Сколько можно назвать «не много»? Это зависит от погрешности этого расстояния, влияет ли она на оценку результата навыка выпуска. Об этом будет сказано позже.)

Итак, как предотвратить подделку данных игроками, чтобы реализовать читерский метод «плавания в воде»? Например (200, 300) — это пул, в котором никто не может бегать. Но я сказал серверу в сетевом запросе: "Я сейчас на (200, 300), ты пришел меня укусить~".

Более простой способ — оценить, можно ли достичь этой координатной точки на стороне сервера, если нет, отправить сообщение клиенту и позволить клиенту обновить позицию (игрок почувствует себя застрявшим, и персонаж отскочит назад). В то же время, если мы не сохраним эти неверные данные, другие пользователи их не увидят (другим игрокам не нужно видеть, как я забегаю в пул, а потом отскакиваю обратно).Что сервер должен делать, так это записывать факты и излагать факты, вместо того, чтобы принимать всю информацию, сообщаемую игроками.. Допустим, кто-то нападает на меня на берегу, тогда в глазах сервера атака действительна. Насчет читеров, которые видят "они в пуле, их еще можно взломать", нет, что, что!Нет необходимости писать слишком много логики совместимости для читерского пользователя, потому что нет необходимости обеспечивать хороший игровой опыт для такого пользователя..

Более продвинутый подход заключается в том, что мы сначала определяем, можно ли пройти эту точку на стороне сервера, а затем определяем, есть ли вероятность того, что игрок достигнет этой точки во времени. Например, если кто-то сообщает, что в одну секунду он находится на уровне (100 100), а в следующую секунду — на уровне (900 900), тогда должна быть проблема. Делим сообщаемый интервал времени на расстояние и сравниваем со скоростью игрока. Конечно, должна быть некоторая избыточность, потому чтоУ игроков может быть нестабильная сеть, а сообщаемая частота немного дрожит, поэтому это нормально, что скорость отдельных периодов времени немного выше.. Отсюда мы также знаем, чтоВ плагине какой-то онлайн-игры, почему вообще без проблем открывается в 1.1 раза быстрее, а при скорости в 1.5 раза часто вылетает. Потому что на сервере установлена ​​10% избыточность. Конечно, этих игроков, которые «спокойно проходили небольшое расстояние каждую секунду», можно определить, оценив общее расстояние, пройденное игроками за N последовательных секунд.

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

Просмотр логики движения персонажа

(Поскольку контент слишком длинный, другой контент временно размещен в вики на github)