第 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(排名)) |
| 成绩变低了 | 没比较旧成绩就直接写回。照第三步只在 待提交 > 旧成绩 时写 |
| 改了名字成绩没了 | 成绩按名字记录,换名字就是新的人。名字存在手机上,下次打开会自动填好 |
动手改一改
- 加一个「清空我的成绩」按钮:先读成绩表,删掉自己的名字(
dictdelete),再存回去。 - 榜单前三名在名次后面加「(冠军)」「(亚军)」「(季军)」。
下一课
第 4 课 图片上传:网络微数据库存不下图片文件,要换个思路——图片交给服务端保存,数据库里只存图片地址。
用到的组件
网络微数据库 · 微数据库 · 计时器 · 列表显示框 · 底部提示条
验证范围
最终 .aia 已通过正式编辑器载入与保存重开(0 错误)、后台导入和 Android APK 编译;连真实服务端执行积木检查 22 项(倒计时与计数、破纪录才写回、11 人排名与同分次序、只显示前 10 名、第 11 名看自己名次、断网、重启恢复等),见验证记录。真机运行与 AI 伴侣联调尚未测试。
扫码添加客服咨询