关于 scenario-roblox-luau
面向 Roblox Studio 开发者的 Luau 工程技能,配合 scenario-roblox-expert toolkit 使用。重点在 Deferred 安全行为、严格类型、资源清理和实测优化,并提供审计、类型检查、探测与基准测试流程。
主要功能
- 01审计调度器与事件用法
- 02使用两种 Luau 求解器检查类型
- 03追踪资源清理与泄漏
- 04在游玩测试中探测行为
- 05基准测试原生和并行代码
- 06采用缓冲区前先测量
适用场景
- 01排查 Deferred 事件处理问题
- 02跨求解器检查严格类型
- 03验证玩家或对象移除后的清理
- 04测量优化是否带来收益
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ scenario-labs/ skills --skill scenario-roblox-luau安装后在 Claude Code 中输入/scenario-roblox-luau确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/critique.mdexpert-notes.mdgui-paths.mdprocedures.mdsources.mdscripts/rbx_luau.pyscripts/agentkit/luau/Bench.luauBufferCodec.luauLeak.luauLuauAudit.luaulune_luau_audit.luauParallel.luauProbe.luauscripts/agentkit/luau/templates/Class.luauParallelWorker.server.luau详细介绍
Roblox Luau 工程
在 macOS 上使用 Roblox Studio(2026 年 10 月版本),通过 scenario-roblox-expert 工具包驱动。
此处的专业 Luau,意味着代码在 Deferred 事件和任务调度器下仍能保持正确、通过两种求解器的类型检查、释放创建的所有内容,并且每项优化都有在此版本上测得的数据支撑。请在游戏测试数据模型中验证行为,而不是沿用 Immediate 时代的经验进行推断。
如果你当前可用的技能中缺少此处提到的同级技能,请让用户安装它
(npx skills add scenario-labs/skills --skill <name>);若无人值守,请依据工具架构继续操作,并指出这一缺失。
必备背景知识: scenario-roblox-expert(频道、审查流程、工具包、当前 Roblox 的常见陷阱)。
立场(专家补充)
task.spawn不等于并发。 它会先运行主体代码,直到首次让出执行权,然后调用方才继续(Crusherfire)。要实现“此步骤之后”,请使用task.defer:它会按调用顺序排队。task.wait()和task.delay(0)会在下一个 Heartbeat 恢复:从 PreSimulation 开始时是同一帧,从 Heartbeat 开始时则是下一帧(Crusherfire;实测分别为 0.07 ms 和 16.4 ms)。- 编写可安全用于 Deferred 的代码。 处理程序会在下一个恢复点运行,绝不会在
:Fire()或设置属性时运行(Deferred events 文档)。嵌套触发会按队列顺序运行;在处理程序内连接的监听器会错过同一步中的触发事件;Destroying或CharacterRemoving处理程序看到的Parent为 nil(Crusherfire;均已实测)。在处理程序内读取状态,使用:Once(),并在Destroy前保存清理所需的信息。仅使用 Deferred,就将某款游戏的帧耗时从 6 到 8 ms 降至 4 到 5 ms(Drew Hamilton,RDC 2026)。 - 只在允许且代价低的地方让出执行权。 绝不要在
__index等元方法、table.sort比较器或gsub回调中让出执行权:会出现“attempt to yield across metamethod/C-call boundary”。绝不要在逐帧处理程序中让出执行权:在 Heartbeat 中使用task.wait(0.25),1 s 内就累积了 16 个线程[新增,实测]。pcall无法解决问题:在pcall下失败的task.wait仍会让其线程注册在调度器中[新增,实测]。 - 兼顾两种求解器进行类型标注。 新模块以
--!strict开头。方法定义为function Class.name(self: Class, ...):冒号方法会让字段拼写错误在两种求解器下都漏过检查[新增,已测试]。优先使用组合:带类型的元表继承在两种求解器下都会产生误报。使用IsA或FindFirstChildOfClass收窄类型,而不是:: any。同时在旧版和新版求解器下进行测试,因为严格脚本会遵循Workspace.UseNewLuauTypeSolver,而脚本无法读取该属性。 - 每个对象都负责自己的清理。
Destroy是幂等的,并会断开它创建的所有连接(Roblox Learn,OOP)。以玩家为键的表应在PlayerRemoving时清空。旧角色应销毁:在 Default 设置下,重生后的角色会脱离父级,但其连接仍然存在(实测)。内存泄漏指内存始终无法回落;仅仅增长并不算泄漏(Roblox Learn,memory leaks)。 - Parallel Luau 用于计算;串行代码负责写入。 每个独立单元使用一个 Actor,在并行阶段之前执行
require,并在task.synchronize()后写入(文档;Suphi Kaner;Stewiepfing)。每个 Actor 都有自己的 Luau VM,因此模块状态不会共享。对 SharedTable 执行读-改-写时,应使用SharedTable.increment或update:8 个 Actor 执行普通的+= 1会导致 79% 的更新丢失(实测)。 - 测量后再使用原生代码和缓冲区。
--!native对带类型的数值代码效果显著:数字循环快 3.3 倍,带类型的Vector3循环快 4.9 倍,而Vector3参数未标注类型时则没有提升(实测;文档建议添加类型标注)。到处启用原生代码会增加启动开销,并占用全局内存预算(Roblox Learn,native)。缓冲区在持续吞吐量超过约 50 KB/s 后才有优势(Crusherfire);整数写入会静默回绕,因此需要进行范围检查。 - 注入的代码块不等于游戏本身。 Server 上的
execute_luau代码块有自己的模块缓存:它读取到的Counter.n = 0,而游戏 Scripts 读取到的是 41[新增,实测]。请通过属性或 BindableFunction 钩子读取游戏状态。
先确认
- **Signal 模式:**Workspace > SignalBehavior。脚本无法读取该属性;请检查 Properties 面板、Rojo 项目文件或
rbx_luau.audit(place)。Default表示 Immediate。未设置该属性的 Rojo 构建场景使用 Default。 - 服务器权威:
Workspace.AuthorityMode = Server会强制使用 Deferred 和固定步长模拟;此时游戏逻辑应归属 scenario-roblox-gameplay。 - 类型设置:
Workspace.LuauTypeCheckMode、UseNewLuauTypeSolver,以及 Rojo 项目是否提供 sourcemap。 - **代码运行位置:**Server、Client 或 Actor 下。原生代码只对服务器脚本有帮助。
- **优化前的基准数值:**热点路径每帧耗时(ms)、网络传输速率(KB/s)、每个周期的堆增长斜率。
- PlayerCharacterDestroyBehavior(Properties 面板中设置;不可通过脚本设置)。
- 未知时的默认值:Deferred、
--!strict、两个类型求解器均设为门控、无原生代码、无 Actor、无缓冲区。
流程
- **审计现有内容。**目标:在试玩前找出调度器、让出、Deferred、并行、原生代码和泄漏方面的问题。方法:离线运行
rbx_luau.audit("place.rbxl" or "src/", "warn")(Lune);在 Studio 中运行rbx_luau.run(s, chunk, dm="Edit", kit=["LuauAudit"])和LuauAudit.tree(game);工具包的Audit会检查旧版 API。门槛:错误级发现为 0;每条警告都已修复或用一行说明原因。 - **修正惯用写法。**目标:使用当前的调度器和事件惯用写法。方法:只用
task.*;用PreSimulation、PostSimulation、PreRender替代Stepped和RenderStepped;使用:Once();帧处理程序中不执行让出操作(使用 busy flag,或一个已生成的工作循环);元方法不得让出(预先计算,或显式使用可能让出的get)。门槛:审计干净;在 Deferred 下行为不变(步骤 5)。 - **类型检查。**目标:让两个类型求解器都能在编辑时捕获错误。方法:使用
--!strict、Fields类型和typeof(setmetatable({} :: Fields, Class))、带点号类型的self、组合,以及table.create(n, value);然后运行rbx_luau.typecheck(paths, sourcemap=rbx_luau.sourcemap(project))。门槛:old和new中的错误均为 0。 - **所有权与清理。**目标:任何对象的存活时间都不超过其所有者。方法:使用类模板(将
Destroy连接到Destroying);清理PlayerRemoving;销毁角色。在 Server 试玩中用Leak.retained(make, release, n)和Leak.sample(cycle, 20),以及Leak.slope验证。门槛:释放后alive为 0;20 个或更多周期内堆增长斜率持平;Destroy后剩余连接数为 0。 - **探测依赖执行顺序的行为。**目标:获取此场景中的事实,而非凭空假设。方法:在一次试玩中,通过
rbx_luau.run(..., dm="Server")和dm="Client"运行Probe.scheduler()、Probe.delayTiming()、Probe.signals()(见流程 P3)。门槛:已记录结果;没有代码依赖探测结果显示会被 Deferred 改变的执行顺序。 - **只依据数值进行优化。**目标:只保留经测量有效的优化。方法:在 Server 数据模型中运行
Bench.run({plain=..., native=...}, 7)(交错轮次、预热、中位数);使用Parallel.workers,对比 1 个与 N 个 Actor,以及一个工作模块;比较BufferCodec大小与表载荷。门槛:原生代码比率至少为 1.2 [新增阈值],并行加速比至少为 1.5 [新增阈值],缓冲区超过带宽门槛;否则回退。 - **交付时附上证据。**目标:评审者无需重新运行即可信任结果。方法:附上类型检查输出、审计 JSON、探测和基准测试结果,并注明构建、数据模型和日期;如改动可见,则通过
rbx_review附上试玩录屏。门槛:交付说明中的每项声明都有数值或测试名称作为依据。
数值
| 数值 | 测量值或说明 | 对比基准 |
|---|---|---|
task.delay(0) 恢复执行 | 从 PreSimulation 起 0.07 ms,从 Heartbeat 起 16.42 ms | Studio 0.743 Server,60 Hz |
| 让出执行权的 Heartbeat 处理程序 | 1 秒内有 16 个并发线程 | 每次调用 task.wait(0.25) |
| 延迟执行带来的帧耗时提升 | 从 6 到 8 ms 降至 4 到 5 ms | 一款已发布的游戏(RDC 2026) |
| Native 加速比,数值循环 | 3.3x | 纯 Luau,Server,n = 200,000 |
Native 加速比,类型化 Vector3 循环 | 4.9x;非类型化为 0.96x | 同一内核,但不带 --!native |
| Native 大小限制 | 每个代码块 64K 条指令,每个函数 32K 个代码块,每个脚本 1M 条指令 | 文档;另有全局上限 |
| 并行加速比 | 7.6x(144.5 ms 降至 19.0 ms) | 1 个与 8 个 Actor,32 个纯计算任务,18 核 Mac |
SharedTable 普通 += 1 | 丢失 160,000 次更新中的 127,107 次 | SharedTable.increment 未丢失更新 |
| Buffer 带宽限制 | 持续入站约 50 KB/s | Crusherfire |
| 通过远程传输的数值 | Number 为 9 字节;Color3 为 13 字节,打包后为 5 字节 | Crusherfire |
| SharedTable 键 | 字符串或 0 到 2^32 - 1 的整数 | SharedTable 参考资料 |
| 内存预算 | Server 约 6.25 GB;手机约 2 GB,其中 700 MB 被操作系统占用 | Roblox Learn,内存泄漏 |
| 重入限制 | 10(文档);自我重新触发的处理程序达到 50 次且未报错 | Deferred,BindableEvent |
质量门槛
可测量项:
rbx_luau.typecheck:两个求解器均为 0 个错误,并使用项目的 sourcemap。rbx_luau.audit:0 个错误;警告已解决。- Toolkit
Audit:未发现旧版调度器或弃用 API。 - 每次修复清理逻辑后,
Leak.retained的存活对象数为 0,且Leak.slope持平。 - 记录该场景信号模式在 Server 和 Client 上的探测结果。
- 保留 Native 代码或 Actor 前,记录 Bench 和 Parallel 的比率。
BufferCodec往返转换结果完全一致(或在 f32 精度范围内),并测试拒绝情况。
视觉项(当行为可见时):通过 rbx_review 录制的试玩画面,展示对象正在移动、已清理,或展示你所声称的状态;对于性能分析相关的声明,需手动录制 MicroProfiler 或 Script Profiler 画面(GUI 路径)。
常见错误
| 错误 | 症状 | 修复 |
|---|---|---|
wait()、spawn()、delay() | 被限流、恢复执行延迟 | task.wait、task.spawn、task.delay |
:Fire() 后的逻辑假定处理程序已运行 | Deferred 模式下状态过时 | 在处理程序内部操作,或使用 task.defer |
Destroying 处理程序读取 Parent | nil 错误 | 在 Destroy 前捕获 |
Heartbeat 处理程序中使用 task.wait | 线程堆积、工作量翻倍 | 使用忙碌标志,或单个工作循环 |
在 __index 或排序比较器中让出 | "attempt to yield across metamethod/C-call boundary" | 预先计算;使用显式 getter |
严格模式代码中使用 function C:m() | 字段拼写错误未被发现 | function C.m(self: C) |
(x :: any).Child | 拼写错误未被检查器发现 | 使用 IsA、FindFirstChildOfClass 缩窄类型 |
| 类型化继承链 | 对有效调用提示 "does not have key" | 使用组合 |
未使用 PlayerRemoving 就执行 players[player] = ... | 每次加入后服务器内存都会增加 | 在 PlayerRemoving 时清除 |
在 Actor 外使用 ConnectParallel | 报错;处理程序从未运行 | 将脚本设为 Actor 的子级 |
desynchronize 后执行 require 或写入 Instance | "not safe to write in parallel" | 先执行 require;在 synchronize 后写入 |
在 pcall 内调用 task.synchronize() | 某次运行中位置发生崩溃(原因未隔离) | 不要在 pcall 内切换阶段 |
在未类型化的 Vector3 代码上使用 --!native | 没有加速 | 为参数添加类型注解 |
buffer.writeu8(b, 0, 300) | 存储为 44 | 检查范围,或使用 BufferCodec |
collectgarbage("collect") | 在 Roblox 中报错 | 使用 gcinfo() |
交接
- 来自 scenario-roblox-architecture: 模块布局、清理库选择(Trove、Janitor、 Maid)、信号模块。本技能会检查其中的 Luau 代码。
- 交接至 scenario-roblox-networking:
BufferCodec模式和实测载荷大小;远程对象、速率 限制和线路格式归该技能负责。 - 交接至 scenario-roblox-performance: Bench 和 Parallel 结果、原生代码候选项、泄漏趋势; 帧预算和性能分析会话归该技能负责。
- 与 scenario-roblox-gameplay 双向交接: 固定步长和 Server Authority 代码;本技能 提供调度器和 Deferred 规则。
- 来自 scenario-roblox-data: 以玩家为键的会话状态;本技能检查
PlayerRemoving清理。 - 交接至 scenario-roblox-tooling: 类型门禁和用于 CI 的
lune_luau_audit.luau。
Roblox 当前说明
- Deferred 是模板默认值;
Default仍映射到 Immediate;Server Authority 会强制使用 Deferred。SignalBehavior为 NotScriptable。 - 新类型求解器:于 2025-11-20 面向非 strict 和 nocheck 模式正式发布;strict 模式遵循
UseNewLuauTypeSolver(NotScriptable)。旧求解器将保留至 2026 年。 wait、spawn、delay是已弃用的全局函数;Stepped和RenderStepped已被取代。- 原生代码生成在服务器脚本中运行;
debug.dumpcodesize()仅可从命令栏(Server 视图)调用。 - 在 Roblox 中,
vector.create返回Vector3;缓冲区序列化为 JSON 时格式为{"m":null,"t":"buffer","base64":...}。 - 在 Roblox 脚本中,
collectgarbage仅接受"count"。
参考资料
- references/expert-notes.md:按主题整理的原则,附来源、时间戳和实时结果。
- references/procedures.md:P1 到 P10,包含完整代码、测试路径和结果。
- references/critique.md:自我审查准则。
- references/gui-paths.md:相关工作中 Properties、分析器和命令栏的路径。
- references/sources.md:专家、文档和工具及其 URL。
- scripts/rbx_luau.py:
typecheck(两种求解器)、sourcemap、audit(Lune)、带工具包的run。 - scripts/agentkit/luau/LuauAudit.luau:静态规则;lune_luau_audit.luau 可离线运行这些规则。
- scripts/agentkit/luau/Probe.luau:调度器、信号、让出和堆积探测。
- scripts/agentkit/luau/Leak.luau:保留和堆斜率检查。
- scripts/agentkit/luau/Bench.luau:交错式 A/B 计时。
- scripts/agentkit/luau/Parallel.luau 与 templates/ParallelWorker.server.luau:Actor 工作线程。
- scripts/agentkit/luau/BufferCodec.luau:带范围检查的缓冲区架构。
- scripts/agentkit/luau/templates/Class.luau:带幂等
Destroy的类型化类。
评论
支持 Markdown