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

Я инженер в DFINITY, но я также разработчик программного обеспечения, поэтому мне хотелось проверить эту предпосылку и оценить опыт разработки на компьютере, подключенном к Интернету, с точки зрения веб-разработчика.

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

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

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

Важно следующее: вы пишете приложение, и оно работает в Интернете.

модель программирования

Опыт разработки веб-приложений на компьютере, подключенном к Интернету, был близок к опыту разработки новых платформ, таких как (ныне несуществующий) Parse или подобных платформ.

Основная идея такой платформы — скрыть сложность создания и поддержки серверных служб (таких как HTTP-серверы, базы данных, входы пользователей и т. д.).

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

С этой точки зрения компьютеры, подключенные к Интернету, одновременно знакомы и различны.

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

  • является 100% детерминированным (если все входные данные и состояния одинаковы, выходные данные должны быть одинаковыми)

  • Прозрачное постоянство (также называемое ортогональным постоянством)

  • Общайтесь с пользователями или другими контейнерами через асинхронные сообщения (удаленные вызовы функций).

  • Обработка одного сообщения за раз (в соответствии с моделью актера)

Если мы думаем о контейнерах Docker как о виртуализации всей операционной системы (ОС), контейнер виртуализирует одну программу, скрывая почти все детали ОС.

Кажется слишком ограничительным, поскольку он не поддерживает вашу любимую ОС или базу данных. Для чего это?

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

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

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

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

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

Клиент: Серверная архитектура

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

  • Сервер размещает саму игру и управляет связью с игровыми клиентами.

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

Создание многопользовательской игры в виде веб-приложения означает, что клиент должен работать в браузере, используя протокол HTTP для передачи данных и используя Javascript (JS) для отображения пользовательского интерфейса игры в виде веб-страницы.

Для этой многопользовательской реверсивной игры я хочу реализовать следующий функционал:

  • Любые два игрока могут играть друг против друга.

  • Игроки зарабатывают очки, выигрывая игры, которые также учитываются в их накопленном счете.

  • Табло, показывающее лучших игроков

  • И, конечно же, есть обычный ход игры: поочередно принимать данные от каждого игрока, выполнять только разрешенные ходы и определять конец игры для подсчета очков.

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

внутренний сервер

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

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

Несмотря на вводящий в заблуждение термин «бессерверное», приложение по-прежнему будет играть роль «сервера», как того требует архитектура клиент-сервер.

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

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

После регистрации игроков, если какие-либо двое из них выразят желание сыграть друг с другом, новая игра будет начата вызовом start(option_name).

Затем игроки по очереди выполняют следующее действие, а другому игроку придется периодически вызывать view(), чтобы обновить свое представление в последнем состоянии игры, затем выполнить следующее действие и так далее, пока игра не закончится.

Как правило, игроки могут играть только в одну игру в любой момент времени.

Сервер должен хранить следующие наборы данных:

  • Список зарегистрированных игроков, их имена, очки и т. д.

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

Я решил реализовать сервер на Motoko, но теоретически любой язык, который может компилироваться в веб-сборку (Wasm), должен работать нормально, если он использует тот же системный API для взаимодействия с интернет-компонентами. (На момент написания этой статьи Rust SDK вот-вот будет запущен.)

Как новый язык, Motoko имеет некоторые грубые преимущества (например, его базовая библиотека немного недостаточна и еще не стабильна), но у него уже есть менеджер пакетов и поддержка протокола языкового сервера (LSP) в VSCode, что делает процесс разработки довольно приятно (это потому, что я пользователь Vim).

В этой статье я не буду обсуждать сам язык Motoko.

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

стабильная переменная

Ортогональная устойчивость (ОП) — не новая идея.

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

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

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

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

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

Тем не менее, в настоящее время существует ограничение на использование HashMaps в качестве стабильных переменных, поэтому мне приходится прибегать к массивам. Вот пример:

Я надеюсь, что будущая версия DFINITY SDK устранит это ограничение, и я смогу просто использовать стабильный проигрыватель var без каких-либо преобразований.

Аутентификация пользователя

Каждый контейнер и каждый клиент (например, командная строка dfx или браузер) получат основной идентификатор, который однозначно идентифицирует их (для клиентов такие идентификаторы автоматически генерируются из пар открытого/закрытого ключей, а библиотека DFINITY JS управляет ими и в настоящее время находится в браузере). локальное хранилище).

Motoko позволяет контейнеру идентифицировать вызывающего «общую» функцию, которую мы можем использовать для целей аутентификации.

Например, я определяю функции регистрации и просмотра следующим образом:

Выражение msg.caller дает основной идентификатор вызывающей стороны сообщения. Обратите внимание, что он отличается от идентификатора вызывающей стороны функции.

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

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

Как мы видели, для доступа к полю вызывающего сообщения необходимо использовать специальный синтаксис: общий(msg) или общий запрос(msg), где msg — формальный параметр, ссылающийся на входящее сообщение в целом.

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

Возможность доступа к уникальному идентификатору вызывающего абонента (отправителя сообщения) кажется знакомой, как файл cookie HTTP.

Но в отличие от HTTP, Интернет-компьютерный протокол фактически гарантирует, что идентификатор субъекта является криптографически безопасным и что пользовательские программы, работающие на компьютерах Интернета, могут быть полностью уверены в его подлинности.

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

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

Параллелизм и атомарность

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

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

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

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

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

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

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

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

Если бы мне пришлось создавать эту игру с использованием традиционной архитектуры, я бы, вероятно, также выбрал фреймворк актеров, такой как Akka от Java, Actix от Rust и т. д.

Motoko предлагает встроенную поддержку актеров, присоединяясь к семейству языков программирования на основе актеров, таких как Erlang и Pony.

Обновление вызовов и запрос вызовов

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

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

Разница между запросами и обновлениями заключается в задержке и параллелизме:

  • Вызов запроса может занять всего несколько миллисекунд, а вызов обновления — около двух секунд.

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

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

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

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

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

Но это не присущая проблемам компьютеров, подключенных к Интернету.

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

Я думаю, что две секунды — это переломный момент для хорошего пользовательского опыта.

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

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

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

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

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

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

внешний клиент

DFINITY SDK предоставляет интерфейс, который загружает приложения непосредственно в браузер.

Однако он отличается от обычных HTML-страниц, обслуживаемых веб-серверами.

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

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

В DFINITY SDK есть набор руководств по настройке интерфейса JS, поэтому я не буду здесь вдаваться в подробности.

За кулисами команда dfx в SDK использует Webpack для объединения ресурсов, включая JS, CSS, изображения и другие файлы, которые могут у вас быть.

Вы также можете объединить свои любимые JS-фреймворки (такие как React, AngularJS, Vue.js и т. д.) с пользовательской библиотекой DFINITY, чтобы разработать интерфейс JS для использования в браузерах или мобильных приложениях.

Основные компоненты пользовательского интерфейса

Я относительно новичок во фронтенд-разработке и имею лишь небольшой опыт работы с React.

На этот раз я взял на себя смелость изучить мифрил, потому что слышал много хорошего о мифриле, особенно о его простоте.

Для простоты я также придумал конструкцию всего с двумя экранами:

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

  • «Игровой» экран, который принимает вводимые игроком данные и взаимодействует с внутренним контейнером для визуализации инверсной доски. Он также отобразит счет игрока в конце игры, а затем вернет игрока на экран игры.

В следующем фрагменте кода показана структура интерфейса игры на JS:

Есть несколько вещей, на которые следует обратить внимание:

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

  • Также будет импортирован еще один контейнер reversi_assets. Это способ получить необходимые ресурсы в комплекте с Webpack при установке внутреннего контейнера. В данном случае у меня есть звуковой файл, который будет воспроизводиться, когда игрок размещает новый фрагмент.

  • Изображение логотипа, которое переходит прямо в него. Это необходимо настроить в Webpack с помощью url-loader — инструмента, который фактически встраивает содержимое изображения в виде строки Base64, которая будет использоваться для элемента изображения. Работает для маленьких изображений, но не для больших изображений.

  • Окончательное приложение настраивается с использованием Mithril по двум путям /play и /game. Последний принимает в качестве двух параметров имена игрока и противника, что позволяет перезагружать игровой экран в браузер, не прерывая игру.

Загрузка ресурсов из контейнера активов

Поскольку я новичок в асинхронной загрузке элементов DOM в JS, я приложил к этому некоторые усилия.

Когда DFX собирает jar, он также создает jar reversi_assets, который по сути просто упаковывает в него все, что находится в src/reversi_assets/assets/.

Я использую это для получения звукового файла, но его правильная загрузка не так проста, как размещение URL-адреса mp3-файла в поле src элемента HTML.

Вот как я его загружаю (если вы фронтенд-разработчик, вы, наверное, это уже знаете):

Когда вызывается функция запуска (из асинхронного контекста), она пытается получить файл «put.mp3» из удаленного контейнера.

После успешного извлечения он использует инструмент JS AudioContext для декодирования аудиоданных и инициализации глобальной переменной putsound.

Если putsound инициализирован правильно, вызов playAudio(putsound) будет воспроизводить настоящий звук:

Другие ресурсы могут быть загружены аналогичным образом. Я не использую никаких изображений, кроме логотипа, который небольшой и его исходный код можно встроить в Webpack, добавив следующую конфигурацию в webpack.config.js:

формат обмена данными

Концепция Мотоко — это «совместно используемые» данные, то есть данные, которые можно отправлять через границы контейнера или языка.

Очевидно, я не могу себе представить, чтобы указатель кучи в C был «совместно используемым», но для меня все, что может быть сопоставлено с JSON, является «разделяемым».

С этой целью DFINITY разработала IDL (язык описания интерфейса) под названием Candid для компьютерных приложений Интернета.

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

Например, вот (неполный) фрагмент внутреннего обратимого контейнера, описанного Candid:

Возьмем в качестве примера метод перемещения:

  • Это один из методов, экспортируемых в сервисный интерфейс контейнера.

  • Он принимает в качестве входных данных два целых числа (представляющих координату) и возвращает результат типа MoveResult.

  • MoveResult — это вариант (также известный как перечисление), который представляет результаты и ошибки, которые могут возникнуть при движении игрока.

  • В различных ветвях MoveResult GameOver указывает, что игра завершена, и принимает параметр ColorCount, который представляет количество черных и белых фигур на игровом поле.

Исходный код Motoko автоматически генерирует файл Candid для каждого контейнера и автоматически используется пользовательской библиотекой JS без участия разработчика:

  • На стороне Motoko каждый тип Candid соответствует типу Motoko, а каждый метод соответствует публичной функции.

  • На стороне JS каждый тип Candid соответствует объекту JSON, а каждый метод соответствует функции-члену импортированного объекта-контейнера.

Большинство типов Candid имеют прямое представление JS, некоторые требуют некоторого преобразования.

Например, как в Motoko, так и в Candid значение nat имеет произвольную точность, в JS оно отображается в целое число bignumber.js, поэтому его необходимо преобразовать в собственный числовой тип JS с помощью n.toNumber().

Одна проблема, с которой я столкнулся, связана с нулевыми значениями в Candid (и типе Option Motoko).

В JSON он представлен как пустой массив[], а не как собственный нулевой массив. Это сделано для того, чтобы отличить случай, когда у нас есть вложенные параметры, такие как Option >:

Candid — очень мощный инструмент, хотя на первый взгляд он очень похож на Protocolbuf или JSON.

Так почему же это необходимо?

Есть много веских причин помимо того, что представлено здесь, и я призываю всех, кто интересуется этой темой, прочитать Candid Spec.

Синхронизировать состояние игры с бэкэндом

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

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

Помимо отправки собственных ходов, клиент также должен узнать о ходах другого игрока.

Это достигается путем периодического вызова функции view() игрового контейнера, размещенного на стороне сервера.

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

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

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

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

Чтобы отображать каждое движение игрока, я решил реализовать состояние игры как последовательность действий, а не просто последнее состояние игрового поля.

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

SVG-анимация

Тема анимации с использованием масштабируемой векторной графики (SVG), возможно, не относится к этой статье, но однажды я действительно застрял на ней.

Поэтому я хочу поделиться уроками, которые я извлек.

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

Большинство онлайн-ресурсов по SVG предоставляют только примеры, которые можно повторять бесконечно или с использованием настройки повтораCount.

Они неявно предполагают, что если анимация отображается только один раз, она начинается после загрузки страницы (или установлена ​​некоторая задержка).

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

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

Я упустил это ключевое отличие и обнаружил его только после многократных попыток.

Вот как я использую Mithril для рендеринга анимированного элемента (как дочернего элемента SVG), где rx эллипса меняется с начального радиуса на 0 и обратно.

Объяснение следующее:

  • Для начала установлено неопределенное значение, чтобы анимацией можно было управлять/запускать вручную.

  • Заливка установлена ​​на заморозку, что означает, что после завершения анимации ее конечное состояние останется неизменным.

  • Значения установлены на 4 значения, где первые два повторяются как трюк для запуска анимации после задержки в 0,1 с (1/4 длительности), это связано с тем, что для начала установлено неопределенное значение.

Суть в том, что анимацию следует запускать вручную. Я запускаю его с задержкой в ​​0 с, используя setTimeout — трюк, который ждет, пока новый элемент пользовательского интерфейса, подготовленный Mithril, не будет отображен в DOM браузера:

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

Процесс развития

Я разработал игру для Linux, и первоначальная настройка заключалась в установке DFINITY SDK и следовании его инструкциям для создания проекта.

Запоминать все командные строки dfx сложно, поэтому я создал Makefile, чтобы помочь.

Отладка и тестирование в основном выполняются в браузере, поэтому требуется много console.log().

На самом деле есть способ писать юнит-тесты в Motoko, но я узнал о нем только после написания игры.

Первоначально я также разработал интерфейс на основе терминала, используя сценарии оболочки и dfx.

Я думаю, это помогает ускорить отладку без необходимости заходить в браузер.

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

играйте в игры!

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

Я советую вам зарегистрироваться, клонировать этот проект и развернуть игру самостоятельно, чтобы получить опыт разработчика из первых рук.

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

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

Я бы не советовал людям делать это самостоятельно, поскольку программное обеспечение все еще находится на стадии альфа-версии.

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

Если у вас есть какие-либо вопросы, посетите репозиторий проекта и оставьте заявку. Также приветствуются запросы на включение!

Присоединяйтесь к нашему сообществу разработчиков и начните строить на forum.dfinity.org.

IC-контент, который вам важен

Технологический прогресс | Информация о проекте Глобальные события |

Собирайте и следите за IC Binance Channel

Будьте в курсе самой последней информации