Jump to content
  • 0

Сапёр на движке Phaser от и до.


Great Rash
 Share

Question

Делаем сапёра на движке Phaser. Часть 1: подготовка графики.

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

С другой стороны когда мы делаем клон игры (особенно такой простой как сапёр), можно смело пропускать "этап тетрадки" и сразу начать придумывать внешний вид. Ведь это гораздо интересней :)

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

Шаг 1.

Открываем фотошоп и создаём новый файл, единицы измерения пиксели, размер 640х1136:

post-8922-0-50761000-1457112490_thumb.pn

Почему такой размер? Потому что я решил делать мобильную игру. На своём опыте я выяснил, что самое "безопасное" разрешение для большинства смартфонов - это разрешение iPhone 5. Теперь откройте браузер Google Chrome и откройте средства разработчика (F12 если вы на Windows). Внизу вы увидите иконку смартфона с подсказкой Toggle device mode, после того кк вы кликните на неё экран уменьшится до размеров устройства, которое можно выбрать в выпадающем меню сверху, вам нужно найти в этом меню опцию Apple iPhone 5:

post-8922-0-34451000-1457112519_thumb.pn

Но ведь там 320х568? Да, но рядом вы можете заметить цифру 2, которая означает плотность пикселей выбранного устройтсва. Если бы мы сделали макет размером 320х568, то на телефоне все картинки выглядели бы размытыми. Чтобы добиться чёткости на необходимо создать макет в два раза больше по размерам: 640 (320 * 2) на 1136 (568 * 2).

Шаг 2.

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

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

Сразу хочу сказать, что модульные сетки лучше всего подходят для мобильных сайтов и приложений. Мы с вами не будем углубляться в дебри проектирования интерфейсов, однако мы почерпнём одну очень важную для нашего проекта деталь из гайдлайнов самого гугла. А именно минимальный размер модуля - 8 пикселей. Оказывается, что разрешение которое мы выбрали (640х1136) идеально делится на 8 (что по ширине, что по высоте). Поэтому откройте в фотошопе ваш файл и зайдите в настройки сетки: в верхнем меню выберите Edit > Preferences > Guides, Grids & Slices...

post-8922-0-11073700-1457112724_thumb.pn

Затем, в меню сверху выберите View > Show > Grid (или нажмите CTRL+' если у вас Windows), теперь ваш документ будет выглядеть так:

post-8922-0-54161100-1457112747_thumb.pn

Теперь вам надо найти центр вашего макета. Наврядли кто-то из сдесь присутсвующих макеты делает, но если что, то я рекомендую это делать всегда. Для этого вам надо убедиться что у вас включено отображение линеек (rulers, это те которые сверху и справа у окна макета). Выберите в меню сверху View > Rulers (или просто нажмите CTRL+R если вы на Windows). Убедитесь, что единицы измерения у вас выставлены как пиксели (по умолчанию там сантиметры вроде), для этого кликните правой кнопкой мышки по любой линейке и выберите в выпадающем меню пиксели. Затем убедитесь, что у вас включена привязка (snap), для этого в вехнем меню выберите View > Snap To > Document Bounds. После всех настроек можете смело начинать находить центр макета. Для этого кликните левой кнопкой мыши по верхней линейке и, не отпуская кнопки, потяните мышку вниз. У вас должен появиться "гайдлайн":

post-8922-0-09812600-1457112763_thumb.pn

Если вы будете тянуть его достаточно медленно, то увидите как он "прилипнет" к середине макета (сработала привязка). Проделайте ту же операцию с левой линейкой. Ваш докумен теперь должен выглядеть так:

post-8922-0-64468900-1457112768_thumb.pn

Самое главное в сапёре - клетки. В тех же гайдлайнах гугла сказано, что идеальный размер иконки на смартфоне 48х48 пикселей. Почему, спросите вы? Гугл объясняет это очень просто - такую площадь занимает пятно контакта указательного пальца у среднестатистического пользователя. "Умно!", - подумал я когда впервые об этом прочитал. Так почему бы и нам не сделать размер клетки 48х48? Создайте новый слой, выбрав в верхнем меню Layer > New > Layer..., или просто кликнув по иконке на палитре слоёв:

post-8922-0-39147700-1457112777_thumb.pn

Затем переименуйте новый слой в "Helper Cells". Далее выберите инструметн прямоугольного выделения (Rectangular Marquee Tool - шорткат M). Чтобы выделение получилось квадратным, а не прямоугольным, зажмите Shift перед тем как тянуть. Выделите в любом месте экрана прямоугольник размером 48х48 пикселей. Если у вас до сих пор активен инструмент прямоугольного выделения, то вы сможете перетаскивать выделение. Перетащите его в центр макета. В форошопе CC 2014 гайдлайны при этом должны стать фиолетовыми (так вы поймёте, что попали в центр). После этого залейте выделение любым цветом (я залил чёрным):

post-8922-0-04304100-1457112784_thumb.pn

После этого я начал экспериментировать с количеством клеток. Экспериментальным путём я выяснил, что красиво получается если добавить ещё по 4 клетки справа и слева. Но только не вплотную, а с отступом в один модуль (мы помним, что размер модуля у нас минимален - 8px).

post-8922-0-57788300-1457112789_thumb.pn

Абсолютно через такие же эксперименты, я надобавлял клеток сверху и снизу:

post-8922-0-91185200-1457112793_thumb.pn

Вот и готово наше игровое поле :) Нужно убегать домой. Продолжение следует (после праздников). Надеюсь я вас заинтриговал.

Для ленивых.

  • Like 5
Link to comment
Share on other sites

8 answers to this question

Recommended Posts

  • 0

Шаг 3.

Теперь добавим цвета. Я не стал ничего изобретать, а просто зашел в поиск по картинкам гугла и вбил запрос "minesweeper number colors", первая же картинка мне идеально подошла. Открываем её в фотошопе и вырезаем клетку. Для этого выберите инструмент прямоугольного выделения (M), выделите клетку (ширина и высота выделения у меня получились 128х128) и скопируйте выделение (CTRL+C если у вас винда). Затем переключитесь назад на наш макет и вставьте скопированное (CTRL+V). Теперь макет должен выглядеть так:

post-8922-0-02992700-1457509228_thumb.pn

Очевидно, что клетка слишком большая, нам нужно её уменьшить. Для этого нажмите CTRL+T или выберите в меню Edit > Transform > Scale. Чтобы при уменьшении не потерялся пиксельный стиль клетки, в меню трансформации в выпадающем меню интерполяции выберите режим Nearest Neighbor.

post-8922-0-58810500-1457509237_thumb.pn

Уменьшите клетку до размера 56х56 пикселей и перетащите её в верхний левый угол нашего поля. Такой размер я выбрал потому, что в сапёре всё-таки нет зазоров между клетками и я тоже решил их не делать. После всех этих операций надобавляйте оставшихся клеток, копируя получившуюся, слейте полученные слои (Layer > Merge Down или CTRL+E) и переименуйте слой с клетками в "Cells":

post-8922-0-22503500-1457509243_thumb.pn

Теперь вернёмся к документу с картинкой из гугла и вырешем иконки флажка и цифр. Для этого воспользуемся инструментом Magic Wand (волшебная палочка - W). В настройках палочки снимите галку с чекбокса Anti-alias и установисе галку в чекбокс Contiguous:

post-8922-0-12864100-1457509248_thumb.pn

Иконки флажка и мины - двухцветные, поэтому кликать палочкой на них нужно зажав Shift, чтобы добавлять выделение к уже существующему. Выделите флажок и вырежте его на наше игровое поле (так же как клетку). После этого уменьшите его (CTRL+T). Чтобы сохранить пропорции нужно включить опцию Maintain aspect ratio. Я уменьшил иконку до сорока процентов от первоначального размера.

post-8922-0-51979700-1457509253_thumb.pn

Переместите иконку в левый верхний угол игрового поля. И проделайте всё тоже самое для остальных иконок. Теперь наш документ выглядит так:

post-8922-0-31376600-1457509258_thumb.pn

Ссылка на макет.

  • Like 3
Link to comment
Share on other sites

  • 0

Шаг 4.

Теперь будем готовить спрайт для собственно игры. Создайте новый макет (CTRL+N) с размерами 672х56 пикселей. 56 потому, что это высота клетки, а 672 потому что вариаций клеток у нас 12 штрук, если считать пустую (56 * 12 = 672). Для удобства позиционирования измените размер грида до 56х56 (см. предыдущие шаги), жмём Edit > Preferences > Guides, Grids & Slices... и в поле Gridline Every вводим 56. Далее копируем все клетки по очереди, для удобства копирования можно на предыдущем макете слить слои в один (Layer > Merge Visible). Вот что получилось у меня:

post-8922-0-03870900-1457595097_thumb.pn

Мина, флажок и пустая клетка находятся в конце потому что так будет удобней раздавать индексы клеткам: единица будет означать едницу, двойка - двойку и т.п., индекс 12 будет означать пустую клетку.

Часть 2: устанавливаем Phaser.

Phaser - это игровой HTML5-движок, написанный на JS, который использует в качестве рендера другой движок Pixi. В принципе можно написать игру используя только Pixi, но поверьте, с фазером всё становится гораздо проще. Там где это возможно, Phaser использует для рендера WebGL, если браузер не поддерживает эту технологию, то используется тупо Canvas с 2D-контекстом. Тем, кто первый раз слышит про Phaser рекомендую пролистать примеры того, что на нём можно сделать. Самым большим минусом этого движка я считаю неудобную документацию, поиск по которой может быть очень затруднительным. Однако этот недостаток компенсируется отличным коммюнити (если вы понимаете английский конечно), мало того, на форуме частенько даёт ответы автор движка.

Фазер из коробки имеет три физических движка: Arcade Physics, Ninja Physics и P2 Physics. Arcade Physics идеален для простых игр, если вам требуется обработка коллизий AABB, то это то что вам нужно. Ninja Physics идеален для платформеров и других игр, основанных на тайлах. P2 Physics - это уже полноценный опенсорсный физический движок, альтернатива известному Box2D, у него есть отдельный репозиторий на гитхабе, если вы решили делать клона Angry Birds, то это ваш выбор. Если вам этого показалось мало, то за деньги можно приобрести плагин, добавляющий поддержку Box2D.

Для установки движка Phaser вам достаточно зайти на страницу закачки и либо клонировать проект с гитхаба, либо скачать архив, либо просто скачать файл .js (минифицированный или обычный). Я предпочитаю последний вариант, вы же делайте как привыкли. После того как вы закачали файл, подключаем его обычным способом:

Моя структура файлов:

Game
---- assets
----------- js
----------- img
---- index.html

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
 
  <title>Minesweeper</title>
 
  <style>
    body {
      margin: 0;
    }
  </style>
 
  <!-- Game Engine -->
  <script src="assets/js/phaser.min.js"></script>
</head>
<body>
 
 
</body>
</html>
  • Like 2
Link to comment
Share on other sites

  • 0

Шаг 1.

Давайте проверим работает ли в нашем браузере игра (я использую Chrome последней версии). Для этого нам нужно вызвать конструктор Phaser.Game (часть кода я опущу для краткости):

<!-- ... -->

<body>

  <script>
    (function() {
      var game = new Phaser.Game(640, 1136, Phaser.AUTO);
    })(); // функция вызывает сама себя
  </script>

</body>

<!-- ... -->

Параметры Phaser.Game:
 
1) ширина канваса
2) высота канваса
3) рендерер (можно не задавать). AUTO - означает, что фазер сам определит использовать WebGL или нет.
 
После этого откройте консоль браузера (F12 на Windows) и проверьте, что у вас есть подобная строка:
 
post-8922-0-57903900-1457682109_thumb.pn
 
Если вы увидели на странице чёрный прямоугольник, а в консоли подобное сообщение и никаких ошибок, значит всё работает.
 
Шаг 2.
 
Теперь настало время поговорить про игровые состояния (States). Вообще мне кажется, что тут больше подходит термин сцена (который кстати используется в движке Unity 3D), пожалуй описание сцены больше подходит под термин "игрового состояния" в движке Phaser. Поэтому я приведу тут выдержку из документации Unity:
 

Цитата

Сцены (в случае движка Phaser - игровые состояния) содержат объекты вашей игры. Они могут использоваться для создания главного меню, отдельных уровней и для других целей. Можно считать каждый файл сцены отдельным игровым уровнем. В каждой сцене можно разместить объекты окружения, заграждения, декорации, по кусочкам создавая дизайн и саму игру.

 
Для управления игровыми состояниями в движке Phaser есть объект StateManager, к которому мы имеем доступ через game.state. Для каждого состояния есть предопределённый набор зарезервированных функций, которые выполняются в строго определённом порядке:
 
1) preload - вызывается саамой первой, в ней загружаются все ресурсы для вашей игры
2) loadUpdate - вызывается во время работы preload, нужна для обновления счётчика/ползунка загрузки
3) loadRender - нукжна для рендера счётчика/ползунка загрузки
4) create - вызывается как только отработает функция preload (обычно тут создаются все объекты необходимые на текущей сцене)
5) update - вызывается каждый кадр, в идеале 60 раз в секунду (собственно здесь работает вся механика игры)
6) render - вызывается после рендера в канвасе или WebGL, нужна для постэффектов или дебажной информации
7) resize - вызывается только если включен режим масштабирования и размеры окна просмотра меняются
8) shutdown - вызывается когда мы переключается на другое игровое состояние (на другую сцену)
 
Для начала давайте сделаем три состояния: экран загрузки лоадера, сам лоадер и собственно игру. Для этого в папке assets/js создайте три файла: boot.js, preload.js и minesweeper.js:


boot.js

var boot = function(game) {};

boot.prototype = {
  preload: function() {
    // тут будем загружать каринку лоадера
  },

  create: function() {
    // отладочная информация, если в консоли появилась эта надпись,
    // значит сцена создана успешно

    // %с - означает, что к инфе в консоли будут применены стили
    // которые передаются во втором параметре
    console.log('%cSTATE::BOOT', 'color: #fff; background: #f00;');

    // запускаем следующую сцену (прелоадер)
    this.game.state.start('Preload');
  }
};

preload.js

var preload = function(game) {};

preload.prototype = {
  preload: function() {
    // тут будем загружать спрайты для нашей игры
    // заодно тут мы инициалиализируем счётчи загрузки
  },

  create: function() {
    // отладочная информация, если в консоли появилась эта надпись,
    // значит сцена создана успешно
    console.log('%cSTATE::PRELOAD', 'color: #fff; background: #0f0;');

    // запускаем следующую сцену (игру)
    this.game.state.start('Game');
  }
};

minesweeper.js

var minesweeper = function(game) {};

minesweeper.prototype = {
  // preload не нужна ибо загружать нам тут уже ничего не надо

  create: function() {
    // отладочная информация, если в консоли появилась эта надпись,
    // значит сцена создана успешно
    console.log('%cSTATE::GAME', 'color: #fff; background: #f0f;');

    // тут будут создаваться объекты для игры (например игровое поле)
  }
};

Затем подключаем созданные скрипты в наш index.html:

<!-- ... -->
<head>
  <meta charset="UTF-8" />

  <title>Minesweeper</title>

  <style>
    body {
      margin: 0;
    }
  </style>

  <!-- Game Engine -->
  <script src="assets/js/phaser.min.js"></script>

  <!-- Game States -->
  <script src="assets/js/boot.js"></script>
  <script src="assets/js/preload.js"></script>
  <script src="assets/js/minesweeper.js"></script>
</head>
<body>

  <script>
    (function() {
      var game = new Phaser.Game(640, 1136, Phaser.AUTO);

      // добавляем в игру созданные сцены (игровые состояния)
      game.state.add('Boot', boot);
      game.state.add('Preload', preload);
      game.state.add('Game', minesweeper);

      // запускаем первое состояние
      game.state.start('Boot');
    })(); // функция вызывает сама себя
  </script>

</body>
<!-- ... -->

Параметры метода .add:

1) идентификатор состояния
2) объект состояния (они у нас в отдельных файлах скриптов)
3) автозапуск состояния (необязательный параметр, по умолчанию выключен), запускает состояние автоматом как только встречает его в коде

Если вы всё сделали правильно, то в консоли вы увидите следующее:
 
post-8922-0-51988000-1457682125_thumb.pn
 
Поздравляю, наша игра работает :)

  • Like 1
Link to comment
Share on other sites

  • 0

Шаг 6.

Давайте попробуем загрузить в нашу игру спрайт. Откройте файл preload.js и напишите код загрузки спрайта:

var preload = function(game) {};

preload.prototype = {
  preload: function() {
    // Setup loading bar
    // ...

    // Load all game assets
    this.game.load.spritesheet('cells', 'assets/img/cells.png', 56, 56, 12);
  },

  create: function() {
    console.log('%cSTATE::PRELOAD', 'color: #fff; background: #0f0;');

    this.game.state.start('Game');
  }
};

Параметры this.game.load.spritesheet:

1) ключ - идентификатор спрайта, по нему игра будет получать доступ к спрайту
2) url - путь до ресурса
3) ширина кадра
4) высота кадра
5) количество кадров - если вы помните у нас 12 разновидностей клеток, этот параметр необязательный, если его не указать, то фазер сам попытается расчитать количество кадров в спрайте

Теперь надо добавить загруженный спрайт в игру. Для этого откройте основной скрипт игры - minesweeper.js и напишите код добавления спрайта:
 

var minesweeper = function(game) {};

minesweeper.prototype = {
  create: function() {
    console.log('%cSTATE::GAME', 'color: #fff; background: #f0f;');

    // добавим спрайт 'cells' в координаты 0, 0 (верхний левый угол)
    this.game.add.sprite(0, 0, 'cells');
  }
};

 

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

Цитата

Uncaught SecurityError: Failed to execute 'texImage2D' on 'WebGLRenderingContext': The cross-origin image at file:///C:/~/Tutorial/assets/img/cells.png may not be loaded.

 

Дело в том, что политика безопасности хрома не позволяет грузить ресурсы с урла вида file:///. Чтобы избавиться от этой ошибки вам понадобится web-сервер. Этим сервером может быть WAMP, Endels (Духовный наследник Denwer) или что-то иное. Но я пошел самым простым путём: установил себе Node.js. После того как инсталляция завершилась, я нажал на клавиатуре комбинацию Win+R и в появившемся окне ввёл cmd, после нажатия Enter у меня открылась командная строка где я ввёл следующую команду:

npm install http-server -g

-g означает, что пакет http-server будет установлен глобально, т.е. вам не придётся заходить в папку с программой чтобы запустить её, вместо этого программу можно будет запускать из любого места.

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

cd Tutorial

А теперь запускаем сервер, для этого в консоли пишем следующее:

http-server -o --cors

-o означает, что после запуска сервера автоматом откроется браузер
--cors означает, что будет включен CORS

Вот теперь вы увидите наш спрайт:

post-8922-0-59973800-1457944542_thumb.pn

  • Like 1
Link to comment
Share on other sites

  • 0

Шаг 7.

Теперь давайте настроим масштабирование. Для нужд масштабирования в движке Phaser есть специальный класс ScaleManager. Настраивать его нужно внутри состояния boot.js для того, чтобы на этапе загрузки (preload.js) ресурсов игра уже имела необходимый масштаб. Итак открываем файл boot.js:

var boot = function(game) {};

boot.prototype = {
  preload: function() {

  },

  create: function() {
    console.log('%cSTATE::BOOT', 'color: #fff; background: #f00;');

    this.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL; // включаем нужный режим мастабирования
    this.scale.pageAlignHorizontally = true; // выравнивание канваса  по центру по горизонтали
    this.scale.pageAlignVertically = true; // ... и по вертикали

    this.game.state.start('Preload');
  }
};

Существуют следующие режимы масштабирования:

1) Phaser.ScaleManager.EXACT_FIT - канвас растягивается на 100% по ширине и высоте (контент внутри канваса пр этом деформируется)
2) Phaser.ScaleManager.NO_SCALE - отключает масштабирование
3) Phaser.ScaleManager.RESIZE - канвас растягивается на 100% по ширине и высоте (контент при этом не деформируется, но всё, что не влезает в экран обрезается)
4) Phaser.ScaleManager.SHOW_ALL - аналог background-size: contain; из CSS, т.е. канвас масштабируется с сохранением соотношения сторон и при этом не обрезается
5) Phaser.ScaleManager.USER_SCALE - канвас масштабируется согласно правилам, заданным в методе setUserScale

Очевидно, что самым простым и подходящем методом масштабирования для нас будет SHOW_ALL.

Шаг 8.

Теперь можно подумать о шрифтах. Давайте посмотрим на интерфейс классического сапёра:

Minesweeper-D--Shep-Poor_1.png

В своём проекте я решил использовать шрифт, которым написаны красные цифры. Поискав в интернетах, я нашел вот такой бесплатный шрифт. Там 4 варианта начертания, я выбрал последний: DS-DIGIB.ttf (жирный без курсива). Зайдя на страницу с примерами, можно увидеть раздел про то как Phaser работает с текстом. Самым удобным для подключения шрифта мне показался вариант с подключением пиксельного шрифта (Bitmap Font). Для генерации пиксельного шрифта существуют специализированные программы для Windows и MacOS, но есть и совершенно бесплатный онлайн-генератор Littera (к слову, рекомендованный к использованию разработчиками фазера).

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

post-8922-0-75629300-1458051144_thumb.pn

В папке assets создайте папку font и распакуйте туда архив со шрифтом.

Шаг 9.

Теперь можно начинать делать экран загрузки, для которого я сделал супер крутой макет:

post-8922-0-96331300-1458051150_thumb.pn

10 минусов снизу - это наш индикатор загрузки. Он будет заполняться по мере загрузки ресурсов. Чтобы вы не заморачивались, я приложу для вас готовую картинку:

post-8922-0-99288300-1458051155_thumb.pn

После того как скачаете её, не забудьте переименовать картинку в loading_bar.png и переложить её в папку assets/img. Затем снова откройте boot.js:

var boot = function(game) {};

boot.prototype = {
  preload: function() {
    // загружаем картинку полосы загрузки
    this.game.load.image('loading', 'assets/img/loading_bar.png');
    // и шрифт для надписи LOADING
    this.game.load.bitmapFont('ds_digital', 'assets/font/ds_digital.png', 'assets/font/ds_digital.fnt');
  },

  create: function() {
    console.log('%cSTATE::BOOT', 'color: #fff; background: #f00;');

    this.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
    this.scale.pageAlignHorizontally = true;
    this.scale.pageAlignVertically = true;

    this.game.state.start('Preload');
  }
};

Затем откройте файл preload.js:

var preload = function(game) {};

preload.prototype = {
  preload: function() {
    // Вставляем спрайт полоски загрузки
    var loadingBar = this.game.add.sprite(154, 588, 'loading');
    loadingBar.anchor.setTo(0, 0.5);

    // говорим фазеру, какую картинку использовать для отображения процесса загрузки
    this.game.load.setPreloadSprite(loadingBar);

    // Вставляем текст LOADING
    // обратите внимание на то что написан он в нижнем регистре
    // ибо буквы для верхнего регистра я не генерил (в шрифте его всё равно нет),
    // а так спрайт шрифта весит существенно меньше
    var loadingText = this.game.add.bitmapText(320, 525, 'ds_digital', 'loading', 72);
	loadingText.anchor.setTo(0.5, 0);

    // Я навставлял рандомных картинок из гугла (большие панорамные изображения)
    // чтобы процесс загрузки был виден, иначе вы бы просто не увидели экран загрузки
    // т.к. все наши ресурсы весят всего пару килобайт
    this.game.load.image('test_0', 'https://upload.wikimedia.org/wikipedia/commons/c/cd/View_from_connors_hill_panorama.jpg');
    this.game.load.image('test_1', 'http://photoblogstop.com/wp-content/uploads/2012/07/Sierra_HDR_Panorama_DFX8048_2280x819_Q40_wm_mini.jpg');
    this.game.load.image('test_2', 'http://www.larkinweb.co.uk/panoramas/lake_placid/Lake_Placid_south_medium_res_panorama.jpg');
    this.game.load.image('test_3', 'http://peraalto.se/wp-content/uploads/2013/03/PanoramaRaps.jpg');
    this.game.load.image('test_4', 'http://parkerlab.bio.uci.edu/pictures/photography%20pictures/2008_12_19_select/Untitled_Panorama1.jpg');

    // Загружаем ресурсы игры
    this.game.load.spritesheet('cells', 'assets/img/cells.png', 56, 56, 12);
  },

  create: function() {
    console.log('%cSTATE::PRELOAD', 'color: #fff; background: #0f0;');

    this.game.state.start('Game');
  }
};

.anchor - это якорь спрайта (текста), т.е. это точка относительно которой будет позиционироваться спрайт (или текст). Что такое якорь наглядней всего показано в этой демонстрации - спрайт позиционируется относительно зелёной точки, которая расположена в координатах 300, 300.

Если вы теперь обновите страницу, то вы увидите сперва надпись LOADING, а затем наше игровое поле, состоящее пока из одной клетки.

  • Like 1
Link to comment
Share on other sites

  • 0

Часть 3. Генерация игрового поля и игра.

Шаг 1.

Перед тем как рассказать про генерацию игрового поля (или игровой доски) необходимо рассказать об одном важнейшем элементе движка Phaser - группах. Группы - это мощнейший инструмент управления массивами объектов. Если ваша игра основана на тайлах, если вам нужны несколько объектов с одинаковым поведением, то вы скорее всего не сможете обойтись без групп. Вот некоторые возможности групп:

1) позиционирование нескольких объектов одновременно

2) сортировка по определённому пользователем признаку

3) фильтрация (выборка) элементов по определённому признаку

4) итерация по элементам группы

5) навешивание обработчиков событий всем элементам группы сразу

6) возможность вкладывать группы в группы

Чтобы создать группу достаточно вызвать метод game.add.group().

Шаг 2.

Переходим к генерации игрового поля. Для начала начнём с простого поля 3х3 клетки. Как правило его представляют так:

var gameBoard = [
  [0, 0, 0],
  [0, 0, 0],
  [0, 0, 0]
];

Однако внутри группы у нас элементы располагаются в одномерном массиве, т.е. в нашем случае поле будет выглядеть так:

var gameBoard = [0, 0, 0, 0, 0, 0, 0, 0, 0];

Мы можем пойти двумя путями:

1) создать два массива: первый группа клеток для отображения в игре, второй - чисто для хранения данных

2) превратить одномерный массив в двумерный

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

post-8922-0-88736100-1458203714_thumb.pn

Надеюсь идея понятна? Давайте применим её на практике, откройте minesweeper.js:

var minesweeper = function(game) {};

minesweeper.prototype = {
  create: function() {
    console.log('%cSTATE::GAME', 'color: #fff; background: #f0f;');
    
    // размер поля для начала 3х3
    // переменные будут доступны из любого места
    this.game.boardWidth = 3;
    this.game.boardHeight = 3;

    // создаём группу, для хранения клеток
    this.game.board = this.game.add.group();

    // поле будем создавать внутри отдельной функции
    this.placeCells();
  },

  // конвертируем индекс массива в координату X
  indexToX: function(i) {
    return i % this.game.boardWidth;
  },

  // конвертируем индекс массива в координату Y
  indexToY: function(i) {
    return Math.floor(i / this.game.boardWidth);
  },

  placeCells: function() {
    // сколько всего клеток (площадь игровой доски)
    var len = this.game.boardWidth * this.game.boardHeight;
    var x = 0;
    var y = 0;
    var cell = null;

    for (var i = 0; i < len; i++) {
      x = this.indexToX(i);
      y = this.indexToY(i);

      // создаём клетку игрового поля
      // последний параметр - это номер кадра у спрайта (10 кадр - это закрытая клетка)
      cell = this.game.add.sprite(x * 56, y * 56, 'cells', 10);
      // запоминаем координаты клетки (чтобы не рассчитывать их каждый раз)
      cell.posX = x;
      cell.posY = y;
      
      // и добавляем клетку в группу
      this.game.board.add(cell);
    }
  }
};

Вот и готово наше игровое поле:

post-8922-0-93360700-1458203720_thumb.pn

  • Like 1
Link to comment
Share on other sites

  • 0

Шаг 3.

Теперь напишем расстановку мин. Для начала расставим 5 мин. Итак внутри minesweeper.js пишем:

var minesweeper = function(game) {};

minesweeper.prototype = {
  create: function() {
    console.log('%cSTATE::GAME', 'color: #fff; background: #f0f;');
    
    // вернул нормальные размеры игровому полю
    this.game.boardWidth = 9;
    this.game.boardHeight = 11;

    this.game.board = this.game.add.group();
    // сдвигаем группу клеток так, чтобы они были как в макете (по центру)
    this.game.board.x = 72;
    this.game.board.y = 264;

    // максимальное количество мин (для начала 5 штук)
    this.game.maxBombs = 5;

    this.placeCells();
    // после генерации поля расставляем мины
    this.placeBombs();
  },

  placeCells: function() {
    var len = this.game.boardWidth * this.game.boardHeight;
    var x = 0;
    var y = 0;

    var cell = null;

    for (var i = 0; i < len; i++) {
      x = i % this.game.boardWidth;
      y = Math.floor(i / this.game.boardWidth);

      cell = this.game.add.sprite(x * 56, y * 56, 'cells', 10);
      cell.posX = x;
      cell.posY = y;
      // есть ли в этой клетке мина
      cell.isBomb = false;
      // количество мин вокруг клетки
      cell.numBombs = 0;
      // запоминаем индекс клетки в массиве
      cell.index = i;

      this.game.board.add(cell);
      // добавляем клетку в хеш (массив),
      // для того чтобы мы могли в любой момент найти её по индексу
      this.game.board.addТоHash(cell);
    }
  },

  placeBombs: function() {
    // сколько мин нам ещё осталось установить
    var bombsToPlace = this.game.maxBombs;
    var cell = null;

    do {
      // получаем рандомную клетку
      // .getRandom() - стандартный метод для любой группы в Phaser
      cell = this.game.board.getRandom();

      // если в ней ещё нету мины
      if (cell.isBomb === false) {
        // то ставим спрайту 9-ый кадр (картинка с миной, для наглядности)
        cell.frame = 9;
        // и отмечаем, что в данной клетке бомба
        cell.isBomb = true;

        // нам осталось расставить на одну мину меньше
        bombsToPlace--;
      }
    } while (bombsToPlace > 0); // повтояем цикл до тех пор пока все мины не будут расставлены
  }
};

Вот что получилось у меня в итоге:

post-8922-0-66389700-1458286083_thumb.pn

Шаг 4.

Теперь первая нетривиальная часть игры - расстановка цифр вокруг мин. В своём старинном уроке я пошел немного неправильным путём. Я циклом обходил все клетки и смотрел есть ли вокруг выбранной клетки мины, если рядом с ней есь мина, то я прибавлял к клетке единичку. Немного поразмыслив, я догадался, что гораздо быстрей будет обойти все мины, ведь их сильно меньше чем пустых клеток. В нашем тестовом случае всего 5 штук. Теперь у меня осталась еще одна тема для размышлений: как в одномерном массиве получить соседние клетки?

Давайте для начала вернёмся к полю 3х3. В нашей игре оно представляет собой массив, который упрощённо выглядит так:

var cells = [0, 1, 2, 3, 4, 5, 6, 7, 8];

Давайте перепишем его немного, чтобы он стал похож на игровое поле:

var cells = [
  0, 1, 2,
  3, 4, 5,
  6, 7, 8
];

Допустим нам надо получить соседей, для клетки с индексом 4. Ну с соседями слева и справа всё просто: достаточно просто вычесть/прибавить единицу, чтобы получить соседа слева/справа. А вот с соседями сверху и снизу я сперва никак не мог догнать что делать. Подозреваю, что многие из вас не такие слоупоки как я, и уже давно догадались, что надо делать. Для тех же кто похож на меня поясню: нужно всего-лишь вычесть или прибавить к индексу клетки ширину нашего поля (в данном случае 3). Ну а с диагоналями всё снова становится элементарно. Итак распишу:

клетка - 4

лево   - 4 - 1 = 3
право  - 4 + 1 = 5
верх   - 4 - 3 = 1
низ    - 4 + 3 = 7

лево верх  - верх - 1 = 0
право верх - верх + 1 = 2
лево низ   - низ  - 1 = 6
право низ  - низ  + 1 = 8

Элементарная математика. Ну что ж, приступим к написанию кода:

var minesweeper = function(game) {};

minesweeper.prototype = {
  // ...

  placeBombs: function() {
    var bombsToPlace = this.game.maxBombs;
    var cell = null;

    do {
	  cell = this.game.board.getRandom();

      if (cell.isBomb === false) {
        cell.frame = 9;
        cell.isBomb = true;

        // как только установили мину
        // сразу расставляем циферки вокруг неё
        this.placeNumbers(cell);

        bombsToPlace--;
      }
    } while (bombsToPlace > 0);
  },

  placeNumbers: function(cell) {
    var i = cell.index;

    // верх
    var u = i - this.game.boardWidth;
    // низ
    var d = i + this.game.boardWidth;
    // лево
    var l = i - 1;
    // право
    var r = i + 1;

    // верх лево
    var ul = u - 1;
    // верх право
    var ur = u + 1;
    // низ лево
    var dl = d - 1;
    // низ право
    var dr = d + 1;

    // влево мы можем идти только если клетка не у края поля
    if (cell.posX > 0) {
      this.addNumberToCell(l); // добавляем цифру в клетку

	  // вверх влево можно идти только если сверху от клетки что-то есть
      if (cell.posY > 0) {
        this.addNumberToCell(ul);
      }

	  // вниз можно только если там есть место
      if (cell.posY < this.game.boardHeight - 1) {
        this.addNumberToCell(dl);
      }
    }

    if (cell.posX < this.game.boardWidth - 1) {
      this.addNumberToCell(r);

      if (cell.posY > 0) {
        this.addNumberToCell(ur);
      }

      if (cell.posY < this.game.boardHeight - 1) {
        this.addNumberToCell(dr);
      }
    }

    if (cell.posY > 0) {
      this.addNumberToCell(u);
    }

    if (cell.posY < this.game.boardHeight - 1) {
      this.addNumberToCell(d);
    }
  },

  addNumberToCell: function(i) {
    var cell = null;

    cell = this.game.board.hash[i];

    // добавляем цифру только туда где нет мины
    if (cell.isBomb === false) {
      cell.numBombs++;

      // для наглядности делаем цифры видимыми
      cell.frame = cell.numBombs - 1;
    }
  }
};

Вот итог наших раздумий:

post-8922-0-09105600-1458286090_thumb.pn

А тут я увеличил количество мин:

post-8922-0-79168500-1458286094_thumb.pn

  • Like 1
Link to comment
Share on other sites

  • 0

Шаг 5.

Чтобы написать код открытия клеток нам придётся провести небольшой рефакторинг. Для начала я лоханулся со спрайтом клеток, наверное вы уже давно поняли что не так :) Так что первым делом забираем новый спрайт:

tut_29.png.355aa8b25aba7b338a57581e528c5

Вторым делом перепишем метод .placeNumbers(), нам нужно отделить от него метод поиска соседних клеток, так как он понадобится нам при открытии клеток (как водится, часть кода я опущу):

var minesweeper = function(game) {};

minesweeper.prototype = {
  // ...

  // теперь соседние клетки ищет отдельный метод
  getNeighbours: function(cell) {
    var i = cell.index;

    var u = i - this.game.boardWidth;
    var d = i + this.game.boardWidth;
    var l = i - 1;
    var r = i + 1;

    var ul = u - 1;
    var ur = u + 1;
    var dl = d - 1;
    var dr = d + 1;

    // в этот массив будем собирать найденных соседей клетки
    var indexArr = [];

    if (cell.posX > 0) {
      indexArr.push(l);

      if (cell.posY > 0) {
        indexArr.push(ul);
      }

      if (cell.posY < this.game.boardHeight - 1) {
        indexArr.push(dl);
      }
    }

    if (cell.posX < this.game.boardWidth - 1) {
      indexArr.push(r);

      if (cell.posY > 0) {
        indexArr.push(ur);
      }

      if (cell.posY < this.game.boardHeight - 1) {
        indexArr.push(dr);
      }
    }

    if (cell.posY > 0) {
      indexArr.push(u);
    }

    if (cell.posY < this.game.boardHeight - 1) {
      indexArr.push(d);
    }

    // возвращаем соседей
    return indexArr;
  },

  // метод расстановки цифер теперь совсем маленький
  placeNumbers: function(cell) {
    var neighbours = this.getNeighbours(cell);

    for (var i = 0; i < neighbours.length; i++) {
      this.addNumberToCell(neighbours[i]);
    }
  },

  // ...
};

Не забудьте сделать цифры невидимыми, для этого внутри .addNumberToCell() уберите строку cell.frame.

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

var minesweeper = function(game) {};

minesweeper.prototype = {
  // ...

  placeCells: function() {
    var len = this.game.boardWidth * this.game.boardHeight;

    var x = 0;
    var y = 0;

    var cell = null;

    for (var i = 0; i < len; i++) {
      x = i % this.game.boardWidth;
      y = Math.floor(i / this.game.boardWidth);

      cell = this.game.add.sprite(x * 56, y * 56, 'cells', 10);
      cell.posX = x;
      cell.posY = y;
      cell.isBomb = false;
      cell.numBombs = 0;
      cell.index = i;
      // отмечаем клетку непроверенной
      cell.isChecked = false;

      // разрешаем клетке обрабатывать события
      // (без этого флага Phaser события не обрабатывает)
      cell.inputEnabled = true;
      // навешиваем обработчик клика на клетку
      cell.events.onInputDown.add(this.onClick, this);

      this.game.board.add(cell);
      this.game.board.addToHash(cell);
    }
  },

  // обработчик клика по клетке
  onClick: function(cell) {
    // если мы кликнули не по мине
    if (!cell.isBomb) {
      // то открываем клетку
      this.openCell(cell);
    }
  },

  // рекурсивный поиск пустых клеток
  openCell: function(cell) {
    // соседи текущей клетки
    var neighbours = [];
    // текущий сосед
    var neighbour = null;

	// если в клетке мина или мы уже проверяли такую клетку
    if (cell.numBombs > 0 || cell.isChecked) {
      // открываем её цифру
      cell.frame = cell.numBombs - 1;

      // и выходим из рекурсии
      return;
    }

    // отмечаем клетку как проверенную
    cell.isChecked = true;
    // открываем клетку (11 кадр - значит пустая, без цифр)
    cell.frame = 11;

    // получаем соседние клетки
    neighbours = this.getNeighbours(cell);

    // для каждого соседа
    for (var i = 0; i < neighbours.length; i++) {
      // находим его в группе
      neighbour = this.game.board.hash[neighbours[i]];

      // если там нет мины
      if (!neighbour.isBomb) {
        // открываем его цифру
        neighbour.frame = neighbour.numBombs - 1;

        // и рекурсивно вызываем открытие его соседей
        this.openCell(neighbour);
      }
    }
  },

  // ...
};

Алгоритм, используемый при открытии клеток, называется Flood Fill. Я использовал связанность по 8 направлениям, но подозреваю, что хватит и 4-х. Однако нам ни к чему делать лишние усложнения в методе .getNeighbours(), всё-таки наша игра слишком проста, чтобы такие оптимизации как-то сильно повлияли на производительность.

Вот что у меня получилось (кликнул в верхний левый угол и в нижний правый):

tut_30.png.fef972d96eeac6f34a340a7572d1d

Ссылка на архив с игрой.

  • Like 1
Link to comment
Share on other sites

Guest
This topic is now closed to further replies.
 Share

  • Similar Content

    • By Seadjus
      Добрый день!
      Огромная просьба камнями не кидаться!
      Начал изучать JS , пока нет особых продвижений в изучении.
      помогите оптимизировать игру в пары. по условиям задания необходимо реализовать игру в пары с таймером, выбором количества карт .
      Ниже
      https://codepen.io/Seadjus/pen/VwVedaP
    • By ilya1203
      Прошу помочь с заданием:
      Написать файл скрипта, который генерирует три числа, соответствующие значениям высоты, длины и ширины комнаты. Отобразить в консоли:
      a.Площадь стен комнаты
      b.Объём комнаты
      c.*Объём комнаты за вычетом случайно сгенерированного количества чисел, которые определяет объёмы случайно зашедших в комнату людей.
      (в итоге запустит скрипт последством консоли)
      (заранее огромное спасибо)
    • By Kaido
      Использую готовый плагин для модальных окон(от MaxGraph). Проблема в том, что когда у меня открыто два модальных окна, для примера Форма + Политика конфендициальности, и мне нужно закрыть политику вместе с ней закрывается и другое модальное окно. В JS я не сильно разбираюсь(собственно из за этого и использую готовый плагин), можете помочь кто работал с этим плагином? Я примерно понимаю как он работает, но реализовать чтобы закрывалось только одно не получается.
       
        <div class="content"> <button class="modal-btn" data-path="first" data-animation="fadeInUp" data-speed="1500">Открыть окно 1</button> </div> <div class="modal"> <div class="modal__wrapp" data-target="first"> <div class="modal__content"> <button class="modal__close">Закрыть</button> модальное окно <button data-path="policy">Политика</button> </div> </div> <div class="modal__wrapp" data-target="policy"> <div class="modal__content"> <button class="modal__close">Закрыть</button> политика </div> </div> </div> .modal { --transition-time: 0.3s; position: fixed; left: 0; top: 0; right: 0; bottom: 0; z-index: 1000; cursor: pointer; overflow-y: auto; overflow-x: hidden; text-align: center; opacity: 0; visibility: hidden; transition: opacity var(--transition-time), visibility var(--transition-time); } .modal__wrapp { display: none; cursor: default; width: fit-content; height: fit-content; } .modal__content{ position: absolute; left: 500px; width: 500px; height: 500px; display: flex; color: white; flex-direction: column; text-align: left; background-color: #000; } .modal__content button{ width: 200px; height: 50px; margin: 50px 0; } .modal.is-open { opacity: 1; visibility: visible; transition: opacity var(--transition-time), visibility var(--transition-time); } .modal__wrapp.modal-open { display: flex; } .disable-scroll { position: relative; overflow: hidden; height: 100vh; position: fixed; left: 0; top: 0; width: 100%; } .fade { opacity: 0; transition: opacity var(--transition-time); } .fade.animate-open { opacity: 1; transition: opacity var(--transition-time); } .fadeInUp { opacity: 0; transform: translateY(vw(-100)); transition: opacity var(--transition-time), transform var(--transition-time); } .fadeInUp.animate-open { opacity: 1; transform: translateY(0); transition: opacity var(--transition-time), transform var(--transition-time); } .modal__wrapp[data-target="policy"] .modal__content{ left: 1050px; background-color: #000; opacity: .5; } class Modal { constructor(options) { let defaultOptions = { isOpen: () => {}, isClose: () => {}, } this.options = Object.assign(defaultOptions, options); this.modal = document.querySelector('.modal'); this.speed = false; this.animation = false; this.isOpen = false; this.modalContainer = false; this.previousActiveElement = false; this.fixBlocks = document.querySelectorAll('.fix-block'); this.focusElements = [ 'a[href]', 'input', 'button', 'select', 'textarea', '[tabindex]' ]; this.events(); } events() { if (this.modal) { document.addEventListener('click', function(e){ const clickedElement = e.target.closest('[data-path]'); if (clickedElement) { let target = clickedElement.dataset.path; let animation = clickedElement.dataset.animation; if (clickedElement.classList.contains('modal-close')) { this.close(); } let speed = clickedElement.dataset.speed; this.animation = animation ? animation : 'fade'; this.speed = speed ? parseInt(speed) : 300; this.modalContainer = document.querySelector(`[data-target="${target}"]`); this.open(); return; } if (e.target.closest('.modal__close')) { this.close(); return; } }.bind(this)); window.addEventListener('keydown', function(e) { if (e.keyCode == 27) { if (this.isOpen) { this.close(); } } if (e.keyCode == 9 && this.isOpen) { this.focusCatch(e); return; } }.bind(this)); this.modal.addEventListener('click', function(e) { if (!e.target.classList.contains('modal__wrapp') && !e.target.closest('.modal__wrapp') && this.isOpen) { this.close(); } }.bind(this)); } } open() { this.previousActiveElement = document.activeElement; this.modal.style.setProperty('--transition-time', `${this.speed / 1000}s`); this.modal.classList.add('is-open'); this.disableScroll(); this.modalContainer.classList.add('modal-open'); this.modalContainer.classList.add(this.animation); setTimeout(() => { this.options.isOpen(this); this.modalContainer.classList.add('animate-open'); this.isOpen = true; this.focusTrap(); }, this.speed); } close() { if (this.modalContainer) { this.modalContainer.classList.remove('animate-open'); this.modalContainer.classList.remove(this.animation); this.modal.classList.remove('is-open'); this.modalContainer.classList.remove('modal-open'); this.enableScroll(); this.options.isClose(this); this.isOpen = false; this.focusTrap(); } } focusCatch(e) { const focusable = this.modalContainer.querySelectorAll(this.focusElements); const focusArray = Array.prototype.slice.call(focusable); const focusedIndex = focusArray.indexOf(document.activeElement); if (e.shiftKey && focusedIndex === 0) { focusArray[focusArray.length - 1].focus(); e.preventDefault(); } if (!e.shiftKey && focusedIndex === focusArray.length - 1) { focusArray[0].focus(); e.preventDefault(); } } focusTrap() { const focusable = this.modalContainer.querySelectorAll(this.focusElements); if (this.isOpen) { focusable[0].focus(); } else { this.previousActiveElement.focus(); } } disableScroll() { let pagePosition = window.scrollY; this.lockPadding(); document.body.classList.add('disable-scroll'); document.body.dataset.position = pagePosition; document.body.style.top = -pagePosition + 'px'; } enableScroll() { let pagePosition = parseInt(document.body.dataset.position, 10); this.unlockPadding(); document.body.style.top = 'auto'; document.body.classList.remove('disable-scroll'); window.scroll({ top: pagePosition, left: 0 }); document.body.removeAttribute('data-position'); } lockPadding() { let paddingOffset = window.innerWidth - document.body.offsetWidth + 'px'; this.fixBlocks.forEach((el) => { el.style.paddingRight = paddingOffset; }); document.body.style.paddingRight = paddingOffset; } unlockPadding() { this.fixBlocks.forEach((el) => { el.style.paddingRight = '0px'; }); document.body.style.paddingRight = '0px'; } } const modal = new Modal({ isOpen: (modal) => { console.log(modal); console.log('opened'); }, isClose: () => { console.log('closed'); }, });  
    • By ilyamio
      Здравствуйте. Имеется рабочий скрипт конвертера валют. https://ilyamio.github.io/currencyconverter/
      Мне необходимо изменить источник загрузки курсов на ЦБ РФ вот по этой ссылке: https://www.cbr.ru/scripts/XML_daily.asp
      А также мне необходимо установить ограничение на количество обращений к сайту ЦБ РФ - 1 раз в сутки. При частом обращении, могут заблокировать.
      Вот в этом js файле находятся настройки конвертера:
      const from_currencyEl = document.getElementById('from_currency'); const from_ammountEl = document.getElementById('from_ammount'); const to_currencyEl = document.getElementById('to_currency'); const to_ammountEl = document.getElementById('to_ammount'); const rateEl = document.getElementById('rate'); const exchange = document.getElementById('exchange'); from_currencyEl.addEventListener('change', calculate); from_ammountEl.addEventListener('input', calculate); to_currencyEl.addEventListener('change', calculate); to_ammountEl.addEventListener('input', calculate); exchange.addEventListener('click', () => { const temp = from_currencyEl.value; from_currencyEl.value = to_currencyEl.value; to_currencyEl.value = temp; calculate(); }); function calculate() { const from_currency = from_currencyEl.value; const to_currency = to_currencyEl.value; fetch(`https://api.exchangerate-api.com/v4/latest/${from_currency}`) .then(res => res.json()) .then(res => { const rate = res.rates[to_currency]; rateEl.innerText = `1 ${from_currency} = ${rate} ${to_currency}` to_ammountEl.value = (from_ammountEl.value * rate).toFixed(2); }) } calculate(); Как мне изменить настройки в этом файле чтобы курсы брались с https://www.cbr.ru/scripts/XML_daily.asp и включить ограничение на количество обращений по ссылке cbr.ru - 1 раз в сутки?
      Заранее спасибо всем, кто хотел помочь с решением этой задачи.
    • By vasyl_runner
      $(function () { var mixer = mixitup('.directions__list'); $('.directions__filter-btn').on('click', function () { $('.directions__filter-btn').removeClass('directions__filter-btn--active') $(this).addClass('directions__filter-btn--active') }) })
×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue. See more about our Guidelines and Privacy Policy