跳转至

计时与在线时长(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,稳得很。