循环与计时器
ChaUI Script 可以用有界循环一次处理多项页面状态,也可以按游戏 tick 或真实毫秒安排后续动作。配合响应式文字、布局和显隐条件,页面只需要修改变量,界面就会自动显示最新结果。
自定义方法不会改变循环安全边界。从 repeat、timer 或周期 interval 中调用的方法同样不能绕过对应限制,也不能通过方法间接创建嵌套延时任务。页面关闭、reload 或退出预览时,当前作用域的全部普通延时任务都会立即停止。只有显式 task(name) { ... } 的有限一次性任务可以把延迟后的服务端动作继续到页面关闭之后。
组件事件启动方法、循环或延时任务时,会一直记住原始触发组件。回调中的 self 仍指向当时触发事件的组件,parent 仍按该组件执行时的当前父级读取;页面级事件启动的任务则继续得到 null。因此可以安全地写:
timeout("restore_button", 500) {
self.enabled = true
parent.child(0).visible = true
}任务只记住组件 ID,不会保留已经失效的旧组件对象。如果等待期间组件被删除,回调再次访问 self 或 parent 时会报告运行时错误并停止当前语句,后续页面和其他任务不受影响。
重复执行
repeat 会立即执行指定次数。次数可以是数字,也可以是包含页面变量、只读常量、四则运算、括号和数学函数的表达式。
repeat(vars.rewardCount, vars.i) {
vars.total = vars.total + vars.i
}第二个参数是可选的索引变量。上例会在每一轮把 0、1、2……写入 vars.i;循环结束后,索引变量保留最后一次的值。次数为 0 时不会执行循环,也不会修改索引变量。
索引变量也可以拼接组件 ID,批量修改当前页面中按编号命名的组件:
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 是同步执行,不会在每轮之间插入渲染帧;同一次循环多次修改同一组件时,画面只会显示循环结束后的最终状态。需要玩家看到逐步变化时,应使用命名 timer 或 interval。
不需要索引时可以省略第二个参数:
repeat(vars.flashCount) {
vars.total = vars.total + 1
}中文别名为 重复:
重复(变量.rewardCount, 变量.i) {
变量.total = 变量.total + 变量.i
}循环次数只在开始时计算一次。即使循环体修改了 vars.rewardCount,已经开始的这次循环也不会临时增减轮数。
命名计时器
timer 会按固定 tick 间隔重复执行一段脚本。计时器必须填写名称,便于后续停止、重启和管理生命周期。
timer("countdown", 20) {
vars.remaining = vars.remaining - 1
if vars.remaining <= 0 {
stopTimer("countdown")
}
}Minecraft 正常运行时每秒通常有 20 个游戏 tick,因此 20 通常约为 1 秒。计时器启动后会先等待完整间隔,再执行第一次;之后继续按同样的间隔运行。
间隔也可以使用表达式:
timer("status_refresh", vars.refreshTicks) {
vars.refreshCount = vars.refreshCount + 1
}间隔表达式只在启动时计算一次。若要使用新的间隔,再次启动同名计时器即可;同一页面作用域内的新计时器会重启并替换旧计时器,不会同时保留两份回调。
中文别名为 计时器 与 停止计时器:
计时器("倒计时", 20) {
变量.remaining = 变量.remaining - 1
if 变量.remaining <= 0 {
停止计时器("倒计时")
}
}毫秒延时任务
需要比游戏 tick 更直观的等待时间时,可以使用 sleep、delay、timeout 和 interval。时间单位都是毫秒,允许 1 至 3600000,也就是最短 1 毫秒、最长 1 小时。实际动作只会在客户端主线程的安全更新点执行,不会早于填写的时间;客户端短暂卡顿时,周期任务也不会补跑已经错过的次数。
sleep:暂停当前脚本链
sleep 会暂停当前这一次脚本,等待时间到达后从下一条语句继续。它必须有名称,方便其他事件取消这段等待。
log("准备显示提示")
sleep("show_tip", 500)
component("tip").visible = true
log("提示已经显示")上例先输出第一条消息,等待至少 500 毫秒,再显示 tip 并输出第二条消息。sleep 不会阻塞游戏画面、输入或其他页面脚本。
中文写法完全等价:
日志("准备显示提示")
等待("显示提示", 500)
组件("tip").visible = true如果等待期间执行 cancel("show_tip"),这条 sleep 会被取消,后面的显示与日志也不会继续执行。
delay:无需名称的单次等待
只需要在当前脚本中暂停一次时,可以写 delay(milliseconds)。它与 sleep 一样会从下一条语句继续,但不需要任务名称,也不能通过 cancel(name) 取消。中文写法是 延迟(milliseconds)。
log("准备刷新")
delay(500)
component("status").text.value = "刷新完成"delay 也可以直接写在 repeat 中。ChaUI 会保存当前循环进度,每一轮等待结束后才继续本轮剩余语句,再进入下一轮;不会一次并行登记所有等待:
repeat(4, vars.i) {
component("step_" + vars.i).visible = true
delay(100)
}上例按 step_0 到 step_3 的顺序逐轮显示,每轮之间至少等待 100 毫秒。命名 sleep("step", 100) 也支持同样的循环续执行语义;需要从另一个事件取消等待时,使用命名 sleep,否则优先使用更简洁的 delay。
timeout:稍后执行一次
timeout 不暂停当前脚本,而是登记一段只执行一次的回调。适合自动隐藏提示、延后刷新状态或在动画结束后做一次收尾。
component("tip").visible = true
timeout("hide_tip", 1500) {
component("tip").visible = false
}
vars.requestRegistered = truevars.requestRegistered 会立即更新,隐藏动作在至少 1500 毫秒后执行一次。中文别名是 延时:
延时("隐藏提示", 1500) {
组件("tip").visible = false
}interval:按毫秒周期执行
interval 会先等待完整间隔,再周期执行回调。它适合需要毫秒单位的轮播、闪烁和客户端状态刷新。
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(防抖)适合搜索输入、筛选条件和频繁变化的设置。相同名称在等待期间再次触发时,会重新开始计时,并把待执行内容替换为最新一次;只有最后一次触发后安静经过完整时间,代码块才会执行一次。
debounce("search_refresh", 500) {
vars.committedKeyword = vars.keyword
packet("更新搜索", vars.committedKeyword)
}如果这段脚本在 500 毫秒内连续触发多次,前面的等待都会被最新一次替换,最终只提交最后的 vars.keyword。中文写法完全等价:
防抖("刷新搜索", 500) {
变量.committedKeyword = 变量.keyword
}throttle:一段时间内只接受第一次
throttle(节流)适合按钮冷却、防止连点或限制高频本地效果。第一次触发会立即执行;同一名称在时间窗口内再次触发会被忽略,也不会把窗口向后延长。窗口结束后的下一次触发会再次立即执行,不会自动补一次尾随执行。
throttle("submit_order", 500) {
packet("提交当前选择", vars.selectedItem)
sound("sounds/click.ogg")
}上例中,玩家第一次点击会立即提交;之后 500 毫秒内的同名点击不执行代码块。中文别名为 节流:
节流("提交订单", 500) {
日志("本次点击已接受")
}防抖和节流的名称只在当前页面作用域内共享。需要区分多个按钮时,请为它们填写不同名称;希望多个入口共用同一冷却时,则使用同一个名称。
cancel:统一取消命名任务
cancel("名称") 可以取消 sleep、timeout、interval、debounce,释放同名 throttle 冷却门,也可以取消旧的 tick timer。旧写法 stopTimer("名称") / 停止计时器("名称") 仍然可用,但只用于 tick timer;新页面推荐使用更通用的 cancel。
cancel("hide_tip")
取消("自动刷新")同一页面作用域中再次用相同名称启动任务时,新任务会重启并替换旧任务。不同页面会话、HUD 和世界页面实例仍然相互隔离。
用本地日志检查计时器
遇到“计时器似乎没有运行”时,可以先用 log 在当前客户端聊天栏输出回调结果。下面的脚本每隔 20 tick 重新读取文本组件当前的局部 Y,向下移动 10 个 GUI 像素,再显示写回后的最新值:
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,再用普通 = 保存新的数字,所以适合做逐步移动。如果希望组件在计时器结束后仍然持续跟随另一个组件,应绑定公式:
component("tip").layout.x = formula(component("panel").layout.x + component("panel").layout.width + 1)关闭页面、关闭 HUD 或世界页面实例、退出预览以及 reload 更换页面时,这个计时器仍会按既有规则立即停止并释放。普通毫秒任务也遵循相同生命周期;跨关闭的一次性 task 由服务端单独调度,不属于页面本地计时器。
可跨页面关闭的一次性 task
task(name) { ... } / 任务(name) { ... } 会立即执行块内流程,并允许一个顶层 sleep 或 delay 之后的页面跳转与指令动作在原页面关闭后继续完成。它适合 GUI 切换,不用于动画循环或常驻轮询:
task("switch_menu") {
close()
delay(1000)
open("target_page")
}延迟后的动作会由服务端根据已保存脚本重新授权。该部分不能访问旧页面变量、组件或其他本地状态,也不能再创建延时回调。不要在任务中写死循环、周期任务或嵌套任务;需要重复执行时使用页面作用域的 timer 或 interval,并接受页面关闭时自动停止的生命周期。
页面关闭时自动停止
普通计时器属于启动它的当前页面作用域。以下情况都会自动停止并释放对应的全部普通计时器:
- 关闭普通页面或编辑器;
- 关闭 HUD 页面;
- 关闭某个世界页面实例;
- 替换同一个页面实例;
- 玩家退出当前客户端会话。
不同页面会话、HUD 和世界实例相互独立。它们即使使用相同的计时器名称,也不会互相停止或覆盖。
使用限制
| 限制 | 数值 |
|---|---|
单次 repeat 最大次数 | 1000 |
| 脚本结构最大嵌套 | 16 层 |
| 单次事件或延时回调最大执行量 | 4096 个脚本节点 |
| tick 计时器间隔 | 1 至 72000 tick |
| 毫秒任务时间 | 1 至 3600000 毫秒 |
| 每个页面作用域的活动延时任务 | tick timer、毫秒任务、防抖与节流门合计最多 32 个 |
repeat 可以嵌套,也可以写在计时器中。循环正文允许使用会暂停当前执行链的 sleep 与 delay,并按轮次顺序恢复;但不能在 repeat 或另一个延时回调内创建 timeout、interval、debounce、throttle 或 tick timer。需要多个独立任务时,应在事件的顶层分别启动并命名。
循环、tick timer 与周期 interval 内可以修改变量、组件状态、播放本地音效、控制视频或发送自定义包,但不能使用以下服务端高权限动作:
close/关闭open/打开cmd/指令opcmd/OP指令consolecmd/控制台指令
这些限制会在保存页面时由服务端检查,避免脚本循环批量触发页面跳转或高权限命令。
与响应式界面配合
推荐让计时器只负责更新变量,让文本组件自己显示变量:
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 自动更新,不需要在每次回调中手动拼接文字。继续阅读响应式变量与实时文字,可以把同样的方式应用到布局、按钮文字和显隐条件。
猹件开发组