Skip to content
On this page

循环与计时器

ChaUI Script 可以用有界循环一次处理多项页面状态,也可以按游戏 tick 或真实毫秒安排后续动作。配合响应式文字、布局和显隐条件,页面只需要修改变量,界面就会自动显示最新结果。

自定义方法不会改变循环安全边界。从 repeattimer 或周期 interval 中调用的方法同样不能绕过对应限制,也不能通过方法间接创建嵌套延时任务。页面关闭、reload 或退出预览时,当前作用域的全部普通延时任务都会立即停止。只有显式 task(name) { ... } 的有限一次性任务可以把延迟后的服务端动作继续到页面关闭之后。

组件事件启动方法、循环或延时任务时,会一直记住原始触发组件。回调中的 self 仍指向当时触发事件的组件,parent 仍按该组件执行时的当前父级读取;页面级事件启动的任务则继续得到 null。因此可以安全地写:

js
timeout("restore_button", 500) {
    self.enabled = true
    parent.child(0).visible = true
}

任务只记住组件 ID,不会保留已经失效的旧组件对象。如果等待期间组件被删除,回调再次访问 selfparent 时会报告运行时错误并停止当前语句,后续页面和其他任务不受影响。

重复执行

repeat 会立即执行指定次数。次数可以是数字,也可以是包含页面变量、只读常量、四则运算、括号和数学函数的表达式。

js
repeat(vars.rewardCount, vars.i) {
    vars.total = vars.total + vars.i
}

第二个参数是可选的索引变量。上例会在每一轮把 012……写入 vars.i;循环结束后,索引变量保留最后一次的值。次数为 0 时不会执行循环,也不会修改索引变量。

索引变量也可以拼接组件 ID,批量修改当前页面中按编号命名的组件:

js
repeat(6, vars.i) {
    component("reward_" + vars.i).visible = vars.i < vars.unlocked
    component("reward_text_" + vars.i).text.value = "奖励 " + (vars.i + 1)
    component("reward_" + vars.i).layout.x = 12 + vars.i * 20
}

component(...) 的参数会在每轮执行时重新求值。实际 ID 不存在,或者目标组件不支持所访问的类型字段时,只跳过当前失败语句并记录去重 warning,循环中的后续语句和轮次仍会继续。

repeat 是同步执行,不会在每轮之间插入渲染帧;同一次循环多次修改同一组件时,画面只会显示循环结束后的最终状态。需要玩家看到逐步变化时,应使用命名 timerinterval

不需要索引时可以省略第二个参数:

js
repeat(vars.flashCount) {
    vars.total = vars.total + 1
}

中文别名为 重复

js
重复(变量.rewardCount, 变量.i) {
    变量.total = 变量.total + 变量.i
}

循环次数只在开始时计算一次。即使循环体修改了 vars.rewardCount,已经开始的这次循环也不会临时增减轮数。

命名计时器

timer 会按固定 tick 间隔重复执行一段脚本。计时器必须填写名称,便于后续停止、重启和管理生命周期。

js
timer("countdown", 20) {
    vars.remaining = vars.remaining - 1
    if vars.remaining <= 0 {
        stopTimer("countdown")
    }
}

Minecraft 正常运行时每秒通常有 20 个游戏 tick,因此 20 通常约为 1 秒。计时器启动后会先等待完整间隔,再执行第一次;之后继续按同样的间隔运行。

间隔也可以使用表达式:

js
timer("status_refresh", vars.refreshTicks) {
    vars.refreshCount = vars.refreshCount + 1
}

间隔表达式只在启动时计算一次。若要使用新的间隔,再次启动同名计时器即可;同一页面作用域内的新计时器会重启并替换旧计时器,不会同时保留两份回调。

中文别名为 计时器停止计时器

js
计时器("倒计时", 20) {
    变量.remaining = 变量.remaining - 1
    if 变量.remaining <= 0 {
        停止计时器("倒计时")
    }
}

毫秒延时任务

需要比游戏 tick 更直观的等待时间时,可以使用 sleepdelaytimeoutinterval。时间单位都是毫秒,允许 13600000,也就是最短 1 毫秒、最长 1 小时。实际动作只会在客户端主线程的安全更新点执行,不会早于填写的时间;客户端短暂卡顿时,周期任务也不会补跑已经错过的次数。

sleep:暂停当前脚本链

sleep 会暂停当前这一次脚本,等待时间到达后从下一条语句继续。它必须有名称,方便其他事件取消这段等待。

js
log("准备显示提示")
sleep("show_tip", 500)
component("tip").visible = true
log("提示已经显示")

上例先输出第一条消息,等待至少 500 毫秒,再显示 tip 并输出第二条消息。sleep 不会阻塞游戏画面、输入或其他页面脚本。

中文写法完全等价:

js
日志("准备显示提示")
等待("显示提示", 500)
组件("tip").visible = true

如果等待期间执行 cancel("show_tip"),这条 sleep 会被取消,后面的显示与日志也不会继续执行。

delay:无需名称的单次等待

只需要在当前脚本中暂停一次时,可以写 delay(milliseconds)。它与 sleep 一样会从下一条语句继续,但不需要任务名称,也不能通过 cancel(name) 取消。中文写法是 延迟(milliseconds)

js
log("准备刷新")
delay(500)
component("status").text.value = "刷新完成"

delay 也可以直接写在 repeat 中。ChaUI 会保存当前循环进度,每一轮等待结束后才继续本轮剩余语句,再进入下一轮;不会一次并行登记所有等待:

js
repeat(4, vars.i) {
    component("step_" + vars.i).visible = true
    delay(100)
}

上例按 step_0step_3 的顺序逐轮显示,每轮之间至少等待 100 毫秒。命名 sleep("step", 100) 也支持同样的循环续执行语义;需要从另一个事件取消等待时,使用命名 sleep,否则优先使用更简洁的 delay

timeout:稍后执行一次

timeout 不暂停当前脚本,而是登记一段只执行一次的回调。适合自动隐藏提示、延后刷新状态或在动画结束后做一次收尾。

js
component("tip").visible = true
timeout("hide_tip", 1500) {
    component("tip").visible = false
}
vars.requestRegistered = true

vars.requestRegistered 会立即更新,隐藏动作在至少 1500 毫秒后执行一次。中文别名是 延时

js
延时("隐藏提示", 1500) {
    组件("tip").visible = false
}

interval:按毫秒周期执行

interval 会先等待完整间隔,再周期执行回调。它适合需要毫秒单位的轮播、闪烁和客户端状态刷新。

js
vars.flashCount = 0
interval("flash", 250) {
    vars.flashCount = vars.flashCount + 1
    if component("tip").visible {
        component("tip").visible = false
    } else {
        component("tip").visible = true
    }
    if vars.flashCount >= 8 {
        cancel("flash")
        component("tip").visible = true
    }
}

这个示例每隔至少 250 毫秒切换一次显示状态,执行 8 次后停止。中文别名是 间隔取消

debounce:等操作停下来再执行

debounce(防抖)适合搜索输入、筛选条件和频繁变化的设置。相同名称在等待期间再次触发时,会重新开始计时,并把待执行内容替换为最新一次;只有最后一次触发后安静经过完整时间,代码块才会执行一次。

js
debounce("search_refresh", 500) {
    vars.committedKeyword = vars.keyword
    packet("更新搜索", vars.committedKeyword)
}

如果这段脚本在 500 毫秒内连续触发多次,前面的等待都会被最新一次替换,最终只提交最后的 vars.keyword。中文写法完全等价:

js
防抖("刷新搜索", 500) {
    变量.committedKeyword = 变量.keyword
}

throttle:一段时间内只接受第一次

throttle(节流)适合按钮冷却、防止连点或限制高频本地效果。第一次触发会立即执行;同一名称在时间窗口内再次触发会被忽略,也不会把窗口向后延长。窗口结束后的下一次触发会再次立即执行,不会自动补一次尾随执行。

js
throttle("submit_order", 500) {
    packet("提交当前选择", vars.selectedItem)
    sound("sounds/click.ogg")
}

上例中,玩家第一次点击会立即提交;之后 500 毫秒内的同名点击不执行代码块。中文别名为 节流

js
节流("提交订单", 500) {
    日志("本次点击已接受")
}

防抖和节流的名称只在当前页面作用域内共享。需要区分多个按钮时,请为它们填写不同名称;希望多个入口共用同一冷却时,则使用同一个名称。

cancel:统一取消命名任务

cancel("名称") 可以取消 sleeptimeoutintervaldebounce,释放同名 throttle 冷却门,也可以取消旧的 tick timer。旧写法 stopTimer("名称") / 停止计时器("名称") 仍然可用,但只用于 tick timer;新页面推荐使用更通用的 cancel

js
cancel("hide_tip")
取消("自动刷新")

同一页面作用域中再次用相同名称启动任务时,新任务会重启并替换旧任务。不同页面会话、HUD 和世界页面实例仍然相互隔离。

用本地日志检查计时器

遇到“计时器似乎没有运行”时,可以先用 log 在当前客户端聊天栏输出回调结果。下面的脚本每隔 20 tick 重新读取文本组件当前的局部 Y,向下移动 10 个 GUI 像素,再显示写回后的最新值:

text
timer("t2d", 20) {
  component("player_name_value").layout.y = component("player_name_value").layout.y + 10
  vars.currentY = component("player_name_value").layout.y
  log("当前 Y:{vars.currentY}")
}

如果聊天栏持续出现递增的 Y,说明计时器回调和组件赋值都已经执行。组件不存在、布局值无法计算或右侧公式无效时,该次赋值会跳过;可以继续增加变量日志缩小排查范围。log 只显示给当前客户端玩家,不会把调试内容发送到服务端。

上例每次计时器回调都重新读取当前 Y,再用普通 = 保存新的数字,所以适合做逐步移动。如果希望组件在计时器结束后仍然持续跟随另一个组件,应绑定公式:

text
component("tip").layout.x = formula(component("panel").layout.x + component("panel").layout.width + 1)

关闭页面、关闭 HUD 或世界页面实例、退出预览以及 reload 更换页面时,这个计时器仍会按既有规则立即停止并释放。普通毫秒任务也遵循相同生命周期;跨关闭的一次性 task 由服务端单独调度,不属于页面本地计时器。

可跨页面关闭的一次性 task

task(name) { ... } / 任务(name) { ... } 会立即执行块内流程,并允许一个顶层 sleepdelay 之后的页面跳转与指令动作在原页面关闭后继续完成。它适合 GUI 切换,不用于动画循环或常驻轮询:

js
task("switch_menu") {
    close()
    delay(1000)
    open("target_page")
}

延迟后的动作会由服务端根据已保存脚本重新授权。该部分不能访问旧页面变量、组件或其他本地状态,也不能再创建延时回调。不要在任务中写死循环、周期任务或嵌套任务;需要重复执行时使用页面作用域的 timerinterval,并接受页面关闭时自动停止的生命周期。

页面关闭时自动停止

普通计时器属于启动它的当前页面作用域。以下情况都会自动停止并释放对应的全部普通计时器:

  • 关闭普通页面或编辑器;
  • 关闭 HUD 页面;
  • 关闭某个世界页面实例;
  • 替换同一个页面实例;
  • 玩家退出当前客户端会话。

不同页面会话、HUD 和世界实例相互独立。它们即使使用相同的计时器名称,也不会互相停止或覆盖。

使用限制

限制数值
单次 repeat 最大次数1000
脚本结构最大嵌套16
单次事件或延时回调最大执行量4096 个脚本节点
tick 计时器间隔172000 tick
毫秒任务时间13600000 毫秒
每个页面作用域的活动延时任务tick timer、毫秒任务、防抖与节流门合计最多 32

repeat 可以嵌套,也可以写在计时器中。循环正文允许使用会暂停当前执行链的 sleepdelay,并按轮次顺序恢复;但不能在 repeat 或另一个延时回调内创建 timeoutintervaldebouncethrottle 或 tick timer。需要多个独立任务时,应在事件的顶层分别启动并命名。

循环、tick timer 与周期 interval 内可以修改变量、组件状态、播放本地音效、控制视频或发送自定义包,但不能使用以下服务端高权限动作:

  • close / 关闭
  • open / 打开
  • cmd / 指令
  • opcmd / OP指令
  • consolecmd / 控制台指令

这些限制会在保存页面时由服务端检查,避免脚本循环批量触发页面跳转或高权限命令。

与响应式界面配合

推荐让计时器只负责更新变量,让文本组件自己显示变量:

yaml
vars:
  remaining: 10

elements:
  - id: countdown_text
    type: text
    layout:
      x: 20
      y: 20
      width: 160
      height: 20
    text:
      value: "剩余 {vars.remaining} 秒"

启动倒计时后,text.value 会随着 vars.remaining 自动更新,不需要在每次回调中手动拼接文字。继续阅读响应式变量与实时文字,可以把同样的方式应用到布局、按钮文字和显隐条件。