玩家账号

给你的游戏玩家一个能在每一款用 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$) 会创建一个没有邮箱的账号,保存在这个浏览器里。

这是一个真实的账号:它可以保存,可以读取,之后也可以被认领,玩家添加一个邮箱和一个密码,已有的存档不需要迁移就能跟着走。

一个诚实的代价是:清除浏览器的站点数据会丢失这个账号,而且无法找回。这是"无需注册"要付出的代价,你的游戏应该主动说明,而不是让玩家自己去发现。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)

Engine 示例列表里的 Player Account 演示,是一个大约一百行就能完成的完整可用界面。打开它,读一读,然后把每一个 Print() 换成你自己的美术素材。

在这一切生效之前#

这款游戏必须先在你的 Aukimi 账号下声明,在我的游戏里(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

登录完成之后的一切,都和这页其他内容完全一样: PlayerSaveAsyncPlayerLoadAsyncPlayerName 以及其他命令都不知道、也不关心是哪个服务器给出的应答。

你的服务器必须回答什么#

不管你提供哪种登录方式,都需要四个地址:

用途游戏期待什么
以访客身份游玩交还一个没有邮箱的账号
这名玩家是谁显示名称、id,以及是否是访客
读取存档已保存的数据
写入存档存储游戏发来的内容

然后,视你选择提供哪种方式而定:

  • 弹窗方式需要你服务器上有一个登录页面,完成后告诉游戏是谁登录了。
  • 直接方式需要再多一个地址,接收一个用户名和一个密码,并回答是或否。

具体的地址、每个应答的结构,以及错误码,都在代码仓库的 docs/engine-player-accounts.md 里。那份文件是契约本身;这一页是地图。

注意: 这一切在原生导出中都不生效。Player* 系列命令需要一个可以对话的服务器和一个可以传递信息的浏览器,而一次原生构建两者都没有。

命令一览#

命令作用
PlayerAvailable()这款游戏是否配置了账号服务
PlayerLoginAsync()打开登录窗口,返回一个任务
PlayerLoginAsync(user, pass)直接登录,仅限你自己的服务器,返回一个任务
PlayerGuestAsync()创建一个没有邮箱的账号,返回一个任务
PlayerLoggedIn()此刻是否有人登录着
PlayerName()他们的显示名称
PlayerID()他们在这款游戏里专属的不透明 id
PlayerIsGuest()这是否是一个没有邮箱的账号
PlayerSaveAsync()写入存档,返回一个任务
PlayerLoadAsync()读回存档
PlayerRefreshAsync()在令牌过期前刷新它
PlayerLogout()在这台设备上登出
PlayerLastError()上一次调用失败的原因