联网 App 第 3 课:点击挑战排行榜,字典存成绩、破纪录才写回、按分数排序(附 .aia 源码)

第 3 课 点击挑战排行榜:点「开始」后 10 秒内尽量多点大按钮,成绩上榜,显示前 10 名和自己排第几。

下载 Leaderboard.aia 返回联网 App 专题

这一课学什么

  • 字典存数据:成绩表是「名字 → 记录」,按名字直接查,不用一条条找。
  • 只写有变化的数据:没破自己的纪录就不写服务器,省流量也减少覆盖别人的机会。
  • 排序:用列表的「按比较规则排序」积木,从高到低排出名次。
点击挑战排行榜:游戏区点击挑战排行榜:榜单

图为由最终 .aia 执行真实积木、连真实服务端生成的布局预览(左:玩完一局;右:往下滑看榜单),不是真机截图。

准备

服务端照常运行。下载 Leaderboard.aia 导入,填服务地址。「服务地址」卡片与前两课相同。

第一步:10 秒小游戏(这一课新增)

计时器的「计时间隔」设成 100 毫秒,每触发一次倒计时减 0.1 秒,100 次就是 10 秒。

global 剩余 = 0
global 次数 = 0

when 按钮_开始.Click() {
  if trim(文本输入框_名字.Text) == "" {
    底部提示条1.Show("先填名字,成绩才能上榜")
  } else {
    次数 = 0
    剩余 = 100
    按钮_开始.Enabled = false
    按钮_点我.Enabled = true
    计时器1.TimerEnabled = true
  }
}

when 按钮_点我.Click() {
  if 剩余 > 0 {
    次数 = 次数 + 1
    标签_成绩.Text = join(次数, " 次")
  }
}

when 计时器1.Timer() {
  剩余 = 剩余 - 1
  标签_倒计时.Text = join(decimals(剩余 / 10, 1), " 秒")
  if 剩余 <= 0 {
    计时器1.TimerEnabled = false
    按钮_点我.Enabled = false
    按钮_开始.Enabled = true
    标签_成绩.Text = join("时间到!你点了 ", 次数, " 次")
  }
}

中途验证:不填服务地址也能玩,10 秒到大按钮变灰,显示「时间到!你点了 N 次」。

第二步:成绩表是一个字典

服务器上标签 scores 的值长这样:

{
  "老王": {"score": 61, "time": "01-15 16:02"},
  "小红": {"score": 55, "time": "01-15 16:09"}
}

一个人只有一条记录,名字就是键。和第 2 课的列表比,好处是按名字直接查到这个人的最好成绩,不用遍历。

第三步:破纪录才写回

时间到时(.aia 里是「结束」过程),先取最新的成绩表(先读后写,和第 2 课一样),再比较:

global 待提交 = -1

when 网络微数据库1.GotValue(tagFromWebDB, valueFromWebDB) {
  var 成绩表 = choose(isdict(valueFromWebDB), valueFromWebDB, {})
  if 待提交 >= 0 {
    var 名字 = trim(文本输入框_名字.Text)
    var 旧成绩 = dictget(dictget(成绩表, 名字, {}), "score", -1)
    if 待提交 > 旧成绩 {
      dictset(成绩表, 名字, {"score": 待提交, "time": 计时器1.FormatDateTime(计时器1.Now(), "MM-dd HH:mm")})
      网络微数据库1.StoreValue("scores", 成绩表)
      标签_成绩.Text = join("时间到!你点了 ", 待提交, " 次,新纪录!")
    } else {
      底部提示条1.Show(join("这次 ", 待提交, " 次,没超过你的最好成绩 ", 旧成绩, " 次"))
    }
    待提交 = -1
  }
  // 最后调用第四步的「显示」过程排出榜单
}
  • dictget(dictget(成绩表, 名字, {}), "score", -1):先按名字取这个人的记录,没有就给空字典;再取分数,没有就是 -1。两层默认值让「第一次上榜」不用单独判断。
  • 待提交 用 -1 表示「这次读取不是为了提交」(只是刷新榜单)。
  • 没破纪录时什么都不写,服务器上的数据保持原样。

中途验证:玩一局 30 次,电脑浏览器看服务地址页面,scores 里有你的名字和 30;再玩一局 20 次,提示「没超过你的最好成绩 30 次」,服务器上还是 30。

第四步:排出名次

字典没有顺序,先把它变成 [名字, 次数, 时间] 组成的列表,再用列表分类里的按比较规则排序积木(ai2-blocks 写作 sortWith)排序:

global 排名 = []

procedure 显示(成绩表) {
  var 行 = []
  foreach 名 in dictkeys(成绩表) {
    var 记录 = dictget(成绩表, 名, {})
    listAdd(行, [名, dictget(记录, "score", 0), dictget(记录, "time", "")])
  }
  // 比较条件写 >=:同分时先上榜的排在前面
  排名 = sortWith(行, (a, b) => a[2] >= b[2])
  var 项 = []
  for i = 1 to min(10, length(排名)) {
    var 第几 = select(排名, i)
    listAdd(项, 列表显示框_榜单.CreateElement(join("第 ", i, " 名  ", select(第几, 1)), join(select(第几, 2), " 次 · ", select(第几, 3)), ""))
  }
  列表显示框_榜单.Elements = 项
}
  • (a, b) => a[2] >= b[2] 的意思是:a 的次数不少于 b,a 就排在 b 前面,所以是从高到低。a、b 是排序时临时拿来比较的两行,只在这个条件里能用。
  • 为什么写 >= 而不是 >:App Inventor 的排序是归并排序,条件为假时把后面那个放前面。写 > 时两个 42 次比较结果为假,同分的人先后会被打乱;写 >= 时相等也算真,同分的人保持原来的先后,先上榜的排前面。
  • 只想从小到大,用更简单的「排序」积木(sort(列表));按某一项从小到大,用「按键排序」积木(sortBy(行, r => r[2]))。
  • 只显示前 10 名,但「你的名次」在全部排名里找(.aia 里的「我的名次」过程),排第 11 名也能看到自己的位置。

中途验证:用两三台手机各玩一局,榜单按次数从高到低排列,标签显示「共 N 人上榜,你的最好成绩 X 次,排第 Y 名」。

跑起来看看

  • 破纪录:显示「新纪录!」和「成绩已上榜」,名次随之变化。
  • 没破纪录:提示最好成绩是多少,榜单不变。
  • 关掉服务端再玩一局:提示「成绩没提交上」,可以接着玩;这一局不会补交。
  • 重新打开 App:名字还在,自动刷新榜单。

常见问题

现象 原因与办法
倒计时一下就到 0 计时器的计时间隔不是 100,或者开始时没把「剩余」设成 100
时间到了还能加次数 「点我」里要先判断 剩余 > 0;计时到 0 时把按钮设为不可用
榜单从低到高 比较条件写反了,要写 a[2] >= b[2](a 大的排前面)
同分的人顺序乱跳 比较写成了 >,改成 >=,见第四步
运行时报「选择列表项」越界 上榜不到 10 人时循环到了 10:循环上限要写 min(10, length(排名))
成绩变低了 没比较旧成绩就直接写回。照第三步只在 待提交 > 旧成绩 时写
改了名字成绩没了 成绩按名字记录,换名字就是新的人。名字存在手机上,下次打开会自动填好

动手改一改

  1. 加一个「清空我的成绩」按钮:先读成绩表,删掉自己的名字(dictdelete),再存回去。
  2. 榜单前三名在名次后面加「(冠军)」「(亚军)」「(季军)」。

下一课

第 4 课 图片上传:网络微数据库存不下图片文件,要换个思路——图片交给服务端保存,数据库里只存图片地址。

用到的组件

网络微数据库 · 微数据库 · 计时器 · 列表显示框 · 底部提示条

验证范围

最终 .aia 已通过正式编辑器载入与保存重开(0 错误)、后台导入和 Android APK 编译;连真实服务端执行积木检查 22 项(倒计时与计数、破纪录才写回、11 人排名与同分次序、只显示前 10 名、第 11 名看自己名次、断网、重启恢复等),见验证记录。真机运行与 AI 伴侣联调尚未测试。

文档反馈