Учётные записи игроков

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

Коротко#

Игрок входит один раз на play.aukimi.com и находит эту учётную запись в каждой игре, сделанной на Aukimi, включая сохранения. Ваша игра запрашивает учётную запись; она никогда не работает с паролем напрямую.

Почему ваша игра никогда не видит пароль#

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

Поэтому пароль вводится на play.aukimi.com, и больше нигде. Ваша игра вызывает PlayerLoginAsync(), открывается окно в цветах Aukimi, игрок подтверждает вход, и это окно передаёт вашей игре токен.

Этот токен выдан только для вашей игры. Токен игры A, предъявленный игре B, отклоняется, не игнорируется, а именно отклоняется. В этом весь смысл: учётная запись общая для всех игр, доступ, нет. Без этого правила любая вредоносная игра могла бы читать сохранения игрока во всех остальных.

Что вы получаете об игроке#

Две вещи, и не больше:

Вы получаетеВы никогда не получаете
Отображаемое имя (PlayerName())Адрес электронной почты
Непрозрачный id, разный в каждой игре (PlayerID())Что-либо, позволяющее опознать игрока где-то ещё

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

Ничто не блокирует игровой цикл#

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

job = PlayerLoginAsync()

Затем, один раз за кадр:

etat$ = PlatformJobState(job)
if etat$ = "done"
    resultat$ = PlatformJobResult(job)
    PlatformJobRelease(job)
endif
if etat$ = "error"
    Print(PlatformJobError(job))
    PlatformJobRelease(job)
endif

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

Игра без учётной записи#

Не все хотят регистрироваться, прежде чем попробовать игру. PlayerGuestAsync(name$) создаёт учётную запись без email, привязанную к этому браузеру.

Это настоящая учётная запись: она сохраняет, загружает, и её можно позже подтвердить: игрок добавляет email и пароль, и его сохранения переходят без потерь.

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

Сохранение#

job = PlayerSaveAsync(donnees$)
job = PlayerLoadAsync()

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

Для нескольких значений SetCloudDataVariable() и CloudDataVariable() проще: без задачи, без JSON. Сначала проверьте CloudDataAllowed(): она возвращает 0, когда никто не вошёл в систему, а запись в этом случае молча пропадёт впустую.

Отрисовка экрана#

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

Рисуете её вы. Единственное, что обязательно должно прийти из браузера, это ввод текста, поэтому CreateEditBox() даёт вам настоящее текстовое поле, которое вы сами позиционируете:

CreateEditBox(1)
EditBoxPosition(1, 20, 42)
EditBoxSize(1, 60, 8)
nom$ = EditBoxText(1)

Демо Player Account в списке примеров Engine, это полноценный рабочий экран примерно на сотню строк. Откройте его, изучите, а затем замените каждый Print() на собственную графику.

Прежде чем это заработает#

Игра должна быть объявлена из вашей учётной записи Aukimi, в разделе My games (aukimi.com/app/games): название, адрес, по которому в неё можно играть, и адреса, на которые вашей игре разрешено возвращать игрока. Пока это не сделано, PlayerAvailable() возвращает 0, и хорошая игра должна сказать «учётные записи недоступны» вместо того, чтобы выглядеть зависшей.

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

Использование собственного сервера учётных записей#

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

В Engine: панель Multiplayer → раздел Player accounts → выберите My own server → заполните одно поле, адрес вашего сервера. Этот адрес, это настройка сцены, поэтому две игры могут использовать два разных сервиса.

Два способа авторизовать игрока#

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

Окно (то, что вы уже знаете)#

job = PlayerLoginAsync()

Открывается окно, игрок входит в нём, и оно передаёт вашей игре право действовать от его имени. С сервисом Aukimi это окно, это play.aukimi.com. С вашим собственным сервером эту страницу предстоит построить вам самим.

Напрямую, с именем пользователя и паролем#

job = PlayerLoginAsync(username$, password$)

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

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

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

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

Конкретный пример#

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

job = PlayerLoginAsync(typedName$, typedPassword$)
DO
    etat$ = PlatformJobState(job)
    if etat$ = "done"
        PlatformJobRelease(job)
        Print("Welcome " + PlayerName())
    endif
    if etat$ = "error"
        PlatformJobRelease(job)
        Print(PlayerLastError())
    endif
    Sync()
LOOP

Всё, что происходит после входа, идентично остальной части этой страницы: PlayerSaveAsync, PlayerLoadAsync, PlayerName и остальные не знают, и им не нужно знать, какой именно сервер ответил.

Что должен уметь ваш сервер#

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

Для чего это нужноЧто ожидает игра
Играть как гостьВернуть учётную запись без email
Кто этот игрокОтображаемое имя, id, признак гостя
Прочитать сохранениеСохранённые данные
Записать сохранениеСохранить то, что отправляет игра

Затем, в зависимости от того, что вы решите предложить:

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

Точные адреса, форма каждого ответа и коды ошибок находятся в репозитории, в docs/engine-player-accounts.md. Этот файл, это контракт; эта страница, это карта.

Заметка: ничего из этого не работает в нативном экспорте. Команды Player* нуждаются в сервере, к которому можно обратиться, и в браузере, через который к нему обращаются, а у нативной сборки нет ни того, ни другого.

Команды#

КомандаЧто она делает
PlayerAvailable()Настроен ли сервис учётных записей для этой игры
PlayerLoginAsync()Открывает окно входа. Возвращает задачу
PlayerLoginAsync(user, pass)Входит напрямую, только на вашем собственном сервере. Возвращает задачу
PlayerGuestAsync()Создаёт учётную запись без email. Возвращает задачу
PlayerLoggedIn()Вошёл ли кто-то в систему прямо сейчас
PlayerName()Его отображаемое имя
PlayerID()Его непрозрачный id, свой для этой игры
PlayerIsGuest()Является ли это учётной записью без email
PlayerSaveAsync()Записывает сохранение. Возвращает задачу
PlayerLoadAsync()Считывает его обратно. Возвращает задачу
PlayerRefreshAsync()Обновляет токен до того, как он истечёт
PlayerLogout()Выходит из системы на этом устройстве
PlayerLastError()Почему последний вызов завершился ошибкой