计时与在线时长(Timer)¶
给服务器搞计时相关的玩法:统计玩家在线时长、执行定时任务(每分钟/每天)、做延时效果(几秒后触发),是运营服务器的常见需求。这篇教程从零写一个完整的计时体系:
- 在线时长统计:记录每个玩家累计在线时间,
!在线时长随时查询; - 定时任务:全服公告、每日零点重置签到/日常,全部安排;
- 延时执行:进服 3 秒后欢迎、传送后延时,通用计时器模式;
- 联动:在线时长解锁称号(配合 29 篇「实现称号系统」)。
💡 这篇是「实现称号系统(29 篇)」的好搭档——在线时长解锁称号的完整写法在第五节。
1. 在线时长统计¶
PlayerEvents.tick 每 1/20 秒触发一次,直接在里面往 persistentData 写会疯狂刷存档(一秒 20 次写盘,服务器要炸)。正确姿势:内存里累加,攒够 1 分钟再落盘一次。
// server_scripts/timer_online.js —— 在线时长统计
const onlineCache = {} // 内存缓存:uuid -> { ticks, lastSave },不直接刷存档
PlayerEvents.tick(event => { // 每 tick(1/20 秒)触发
let player = event.player
let uid = player.uuid // 玩家唯一 ID(uuid)
if (!onlineCache[uid]) { // 第一次见这个玩家,初始化缓存
onlineCache[uid] = { ticks: 0, lastSave: 0 }
}
let cache = onlineCache[uid]
cache.ticks++ // 内存里 tick 数 +1
if (cache.ticks >= 1200) { // 攒够 1200 tick = 1 分钟
cache.ticks = 0
let data = player.persistentData // 玩家存档(重启不丢)
data.onlineMinutes = (data.onlineMinutes || 0) + 1 // 在线分钟 +1,这里才写盘
}
})
💡 每 1200 tick(=1 分钟)才写一次存档,全服几百人也就每秒几次写盘,完全没压力。
查询指令(聊天指令方式,跟 29 篇的 !称号 一个套路,不用 OP):
// server_scripts/timer_online.js —— 追加:!在线时长 查询
PlayerEvents.chat(event => { // 监听聊天
if (event.message !== '!在线时长') return // 不是这个指令就跳过
let minutes = event.player.persistentData.onlineMinutes || 0 // 读累计分钟
let hours = Math.floor(minutes / 60) // 换算成小时
let mins = minutes % 60 // 剩下的分钟
event.player.tell('§a你累计在线 ' + hours + ' 小时 ' + mins + ' 分钟') // 告诉玩家
})
效果:你累计在线 123 小时 45 分钟
💡 想要「今天在线多久」而不是「累计」?把
persistentData.onlineMinutes换成persistentData.todayMinutes,再配合下面的「每日重置」逻辑清零即可。
2. 定时任务(ServerEvents.tick 计数器)¶
服务端全局的定时逻辑用 ServerEvents.tick(每 1/20 秒触发一次,和玩家无关)。用计数器实现「每秒 / 每分钟 / 每天」的节奏:
// server_scripts/timer_schedule.js —— 定时任务
let tickCount = 0 // 全局 tick 计数器
ServerEvents.tick(event => {
tickCount++ // 每 tick +1
if (tickCount % 20 === 0) { // 每 20 tick = 每秒
// —— 每秒执行:比如存档自动保存提示、每秒计分板刷新 ——
}
if (tickCount % 1200 === 0) { // 每 1200 tick = 每分钟
// —— 每分钟执行:比如给在线玩家发一次小提示 ——
event.server.players.forEach(p => { // 遍历所有在线玩家
if (tickCount % 6000 === 0) { // 每 5 分钟
p.tell('§e⏰ 每 5 分钟全服公告:别忘了今天的日常任务!')
}
})
}
// 每日重置:判断「游戏日」变了没
let day = Math.floor(event.server.overworld.dayTime / 24000) // 今天第几天(29 篇签到同款写法)
if (day !== lastDay) { // 新的一天开始了
lastDay = day
// —— 每天零点执行:重置签到、清零今日时长、刷新每日商店 ——
event.server.players.forEach(p => {
p.persistentData.todayMinutes = 0 // 今日在线时长清零(配合第一节)
p.tell('§6🌅 新的一天开始了!')
})
}
})
let lastDay = -1 // 记录上次的"第几天"(放脚本顶部或这里都行)
⚠️
lastDay变量要在ServerEvents.tick外面声明(脚本顶部),否则每次 tick 都重置成 -1,永远判断不了"变了没"。
3. 延时执行(限时效果)¶
「3 秒后执行」这种延时需求,做一个通用计时器:把"几秒后要干的事"存进一个对象,tick 里倒数,到 0 就执行并删除。
// server_scripts/timer_delay.js —— 通用延时计时器
const timers = {} // 延时任务表:key -> 剩余 tick 数
function after(seconds, key, callback) { // 通用函数:seconds 秒后执行 callback(key 用来区分任务,比如玩家 uuid)
timers[key] = seconds * 20 // 秒换算成 tick(1 秒 = 20 tick)
timers[key + ':cb'] = callback // 把要执行的函数也存起来
}
ServerEvents.tick(event => {
for (let key in timers) { // 遍历所有延时任务
if (key.endsWith(':cb')) continue // 跳过存函数的条目
timers[key]-- // 倒数
if (timers[key] <= 0) { // 时间到了
let cb = timers[key + ':cb'] // 取出要执行的函数
delete timers[key] // 清理
delete timers[key + ':cb']
cb() // 执行!
}
}
})
// 用法示例:玩家进服 3 秒后发欢迎消息
PlayerEvents.loggedIn(event => {
after(3, 'welcome:' + event.player.uuid, () => { // 3 秒后执行
let p = event.player // 注意:这里要用闭包捕获的 player,不能 event.player(事件早结束了)
p.tell('§b欢迎回来!这是进服 3 秒后的问候~')
})
})
💡 闭包小知识:
event.player在事件结束后就失效了,所以要在after()之前先把let p = event.player存下来,回调里用p。上面例子里event.player其实在回调触发时已经不能用了,正确写法是let p = event.player放外面。完整正确的写法见第六节完整案例。
4. 屏幕计时提示(聊天栏版)¶
想在玩家屏幕上持续显示计时信息,最稳的方式是 player.tell 聊天栏提示(1.20.1 KubeJS 的 ActionBar/BossBar API 不稳定,不瞎用)。注意控制频率,别每 tick 刷屏:
// server_scripts/timer_hud.js —— 每 10 秒在聊天栏报一次在线时长
let hudTick = 0 // 提示节流计数器
PlayerEvents.tick(event => {
hudTick++
if (hudTick % 200 !== 0) return // 每 200 tick = 10 秒才提示一次
let minutes = event.player.persistentData.onlineMinutes || 0
let hours = Math.floor(minutes / 60)
let mins = minutes % 60
event.player.tell('§7⏱ 已在线 ' + hours + ' 小时 ' + mins + ' 分钟')
})
💡 想要更漂亮的 HUD?配合 25 篇「UI 绘制 (Painter API)」画在屏幕角落,思路一样,只是把
player.tell换成 UI 绘制。
5. 联动:在线时长解锁称号¶
在线时长是最常见的称号解锁条件——挂机也算「老玩家」。用 29 篇的 unlockTitle 思路,往 unlocked 数组里塞 id 就解锁了:
// server_scripts/timer_title.js —— 累计在线 10 小时解锁「老玩家」称号
// 注意:这个脚本要配合 29 篇的称号系统(unlockTitle 函数)一起用
PlayerEvents.tick(event => {
let player = event.player
let data = player.persistentData
// 在线分钟数由第一节的统计脚本维护(onlineMinutes)
if ((data.onlineMinutes || 0) >= 600 && // 累计 10 小时 = 600 分钟
!(data.unlocked || []).includes('veteran')) { // 还没解锁「老玩家」(id 见 29 篇称号清单)
unlockTitle(player, 'veteran', true) // 调用 29 篇的通用解锁函数
}
})
💡 在线 10 小时才解锁,但每 tick 都检查也没关系——
unlockTitle内部有"已解锁就跳过"的判断,不会重复提示。想加更多时间称号,照葫芦画瓢:onlineMinutes >= 1440(24 小时)解锁「挂机王」之类。
6. 完整案例:一键全装¶
把上面的功能整合成一个文件,复制到 server_scripts/timer.js 就能用(含在线时长统计 + 查询 + 每日重置 + 延时欢迎 + 解锁称号):
// server_scripts/timer.js —— 计时体系完整案例
// 配合 29 篇称号系统:需要 unlockTitle 函数(在 titles.js 里)
// —— 在线时长统计(内存累加,每分钟落盘一次)——
const onlineCache = {}
PlayerEvents.tick(event => {
let player = event.player
let uid = player.uuid
if (!onlineCache[uid]) onlineCache[uid] = { ticks: 0 }
onlineCache[uid].ticks++
if (onlineCache[uid].ticks >= 1200) {
onlineCache[uid].ticks = 0
let data = player.persistentData
data.onlineMinutes = (data.onlineMinutes || 0) + 1
data.todayMinutes = (data.todayMinutes || 0) + 1
}
})
// —— 查询指令:!在线时长 ——
PlayerEvents.chat(event => {
if (event.message !== '!在线时长') return
let data = event.player.persistentData
let total = data.onlineMinutes || 0
let today = data.todayMinutes || 0
event.player.tell('§a累计在线 ' + Math.floor(total / 60) + ' 小时 ' + (total % 60) + ' 分钟,今天 ' + Math.floor(today / 60) + ' 小时 ' + (today % 60) + ' 分钟')
})
// —— 定时任务:每 5 分钟公告 + 每日零点重置 ——
let tickCount = 0
let lastDay = -1
ServerEvents.tick(event => {
tickCount++
if (tickCount % 6000 === 0) { // 每 5 分钟
event.server.players.forEach(p => p.tell('§e⏰ 定时公告:记得做日常任务!'))
}
let day = Math.floor(event.server.overworld.dayTime / 24000)
if (day !== lastDay) { // 新的一天
lastDay = day
event.server.players.forEach(p => {
p.persistentData.todayMinutes = 0 // 今日时长清零
p.tell('§6🌅 新的一天,今日在线时长已重置')
})
}
})
// —— 延时欢迎:进服 3 秒后 ——
const timers = {}
function after(seconds, key, callback) {
timers[key] = seconds * 20
timers[key + ':cb'] = callback
}
ServerEvents.tick(event => {
for (let key in timers) {
if (key.endsWith(':cb')) continue
timers[key]--
if (timers[key] <= 0) {
let cb = timers[key + ':cb']
delete timers[key]
delete timers[key + ':cb']
cb()
}
}
})
PlayerEvents.loggedIn(event => {
let p = event.player // 先存下来,闭包要用
after(3, 'welcome:' + p.uuid, () => p.tell('§b欢迎回来!服务器会记住你的在线时长~'))
})
// —— 联动:在线 10 小时解锁「老玩家」称号(需要 29 篇的 unlockTitle)——
PlayerEvents.tick(event => {
let data = event.player.persistentData
if ((data.onlineMinutes || 0) >= 600 && !(data.unlocked || []).includes('veteran')) {
unlockTitle(event.player, 'veteran', true)
}
})
7. 速查表¶
| 功能 | 关键代码 | 说明 |
|---|---|---|
| 在线统计(内存) | onlineCache[uid].ticks++ |
tick 数先在内存累加,不刷存档 |
| 落盘 | data.onlineMinutes = (data.onlineMinutes \|\| 0) + 1 |
每 1200 tick(1 分钟)写一次 |
| 查询指令 | !在线时长 |
PlayerEvents.chat 解析,非 OP 可用 |
| 每秒/每分钟任务 | tickCount % 20 === 0 / % 1200 === 0 |
ServerEvents.tick 计数器 |
| 每日重置 | Math.floor(server.overworld.dayTime / 24000) 变化时 |
判断"第几天"变了就执行 |
| 延时执行 | after(seconds, key, callback) |
通用计时器,倒数 tick 到 0 执行 |
| 解锁称号 | unlockTitle(player, 'veteran', true) |
配合 29 篇称号系统 |
| 屏幕提示 | player.tell('...') |
聊天栏提示,注意节流(每 200 tick 一次) |
还能玩什么? 每日任务倒计时(结合 28 篇配置存储)、限时活动称号(延时计时器 + 到期收回)、排行榜在线时长榜(配合 21 篇)、AFK 检测(长时间无操作判定,PlayerEvents.tick 里对比玩家位置)——思路都是:tick 计数器 + persistentData,稳得很。