关于 roblox-luau
roblox-luau 是用于编写和审查 Roblox 现代 Luau 脚本的编码技能,强调严格类型、当前 API、安全异步操作、资源清理和性能实践。
主要功能
- 01采用严格类型和现代 Luau 语法
- 02建议使用当前 Roblox API,避免弃用接口
- 03涵盖错误处理与 task 并发
- 04指导连接和实例清理
- 05提供高负载代码的性能实践
适用场景
- 01编写新的 Roblox ModuleScripts
- 02更新使用弃用 API 的脚本
- 03检查异步调用和资源清理
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ el4cteo/ roblox-skills --skill roblox-luau安装后在 Claude Code 中输入/roblox-luau确认已加载。
附带文件references/stdlib-cheatsheet.mdreferences/type-system.md
详细介绍
面向 Roblox 的现代 Luau
Luau 是 Roblox 对 Lua 5.1 的渐进类型化衍生版本。引擎和语言迭代很快;许多 来自旧教程(以及旧训练数据)的模式已被弃用。除非项目明确采用其他约定, 否则请对你编写的每一个脚本套用以下默认规则。
所有生成代码的默认规则
- 在新的 ModuleScript(以及可行时的 Script)顶部添加
--!strict。为公开函数添加类型标注。 - 在文件顶部使用
game:GetService("Name")获取服务,绝不要用game.Name。 - 使用
task:task.wait、task.spawn、task.defer、task.delay、task.cancel。 绝不要用wait、spawn、delay(已弃用、受限流、不精确)。 - 可能失败的让出式引擎调用(DataStore、HTTP、Marketplace、Teleport、
*Async)要放在pcall内。 - 优先使用 API 的
*Async变体;Roblox 重命名了大多数让出式方法 (LoadCharacterAsync、GetProductInfoAsync、UserHasBadgeAsync……)。见下表。 - 断开连接 / 销毁你创建的实例(见下文“清理”)。
- 命名:服务、模块、类、类型和实例名用
PascalCase; 局部变量和函数用camelCase;常量用SCREAMING_SNAKE_CASE。 - 缩进用制表符(Roblox 生态中 StyLua 的默认值),字符串用双引号。
现代语法速查
local Players = game:GetService("Players")
local count = 0
count += 1 -- compound assignment: += -= *= /= //= %= ^= ..=
local label = if count > 1 then "many" else "one" -- if-expression (no `and/or` pitfalls)
print(`Player count: {#Players:GetPlayers()} ({label})`) -- string interpolation
for index, player in Players:GetPlayers() do -- generalized iteration: no pairs/ipairs needed
if player.AccountAge < 1 then
continue -- continue is a keyword
end
print(index, player.Name)
end
local half = 7 // 2 -- floor division → 3
local frozen = table.freeze({ Speed = 16 }) -- read-only table; writes throw
local list = table.create(100, 0) -- preallocated array
local copy = table.clone(frozen) -- shallow copy (unfrozen)
local mid = math.lerp(0, 10, 0.5) -- also: math.map, math.clamp, math.round, math.sign
print(half, list[1], copy.Speed, mid)
pairs/ipairs仍可用;更推荐广义迭代,它会按顺序遍历数组。#t仅对无空洞的数组有定义。在中间设置t[i] = nil会产生空洞。- 数字为双精度浮点数:整数在 2^53 以内精确。整数运算请用
math.floor///。 - 不推荐
tick():基准测试用os.clock(),跨客户端/服务端的同步时间用workspace:GetServerTimeNow(),UTC 墙上时钟用os.time()/DateTime.now()。
类型(新类型求解器)
Roblox 的新类型求解器已正式可用(2025 年 11 月),由 Workspace.UseNewLuauTypeSolver
控制;Workspace.LuauTypeCheckMode 设置默认模式。编写能在 --!strict 下通过检查的代码。
--!strict
export type Item = {
id: string,
name: string,
stack: number,
tags: { string }?, -- optional field
}
type Rarity = "Common" | "Rare" | "Legendary" -- singleton (literal) union
local function totalStack(items: { Item }, filter: ((Item) -> boolean)?): number
local total = 0
for _, item in items do
if filter == nil or filter(item) then
total += item.stack
end
end
return total
end
local function first<T>(list: { T }): T? -- generics
return list[1]
end
local rarity: Rarity = "Rare"
local part = workspace:FindFirstChild("Door") :: BasePart? -- cast with ::
print(totalStack({}, nil), first({ 1, 2 }), rarity, part)
- 使用前先收窄:
if part then ... end、if typeof(x) == "Instance" and x:IsA("BasePart") then。 typeof(v)认识 Roblox 类型("Vector3"、"Instance"……);type(v)只认识 Lua 类型。- 实例类型来自
IsA收窄或FindFirstChildOfClass/FindFirstChildWhichIsA。 - 避免
any,除非在信任边界处(远程载荷、解码的 JSON)——在那里做校验,然后再收窄。
详情:泛型、类型包、read/write 属性、用户定义类型函数、为 OOP 类添加类型,
以及将遗留代码迁移到严格模式:references/type-system.md。
模块与 require
ModuleScript 在每个环境(服务端或每个客户端)中运行一次并返回一个值,为后续每次
require 缓存。位于 ReplicatedStorage 中的模块若被两端 require,会运行两次,每端各一次,
状态相互独立。
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Signal = require(ReplicatedStorage.Packages.Signal) -- instance path (always works)
local Config = require("./Config") -- require-by-string: sibling of this script (script.Parent)
local Utils = require("@self/Utils") -- child of this script
local Shared = require("@game/ReplicatedStorage/Shared") -- absolute from game
print(Signal, Config, Utils, Shared)
- 字符串路径:
./=script.Parent,../=script.Parent.Parent,@self/=script,@game/=game。它们不会等待实例存在。当模块可能尚未复制过来时, 请用实例路径配合WaitForChild。 - 绝不要创建循环 require;它们会死锁/报错。用依赖注入或事件打破循环。
- 不要在模块顶层让出(不要对可能永不存在的对象做
WaitForChild链,不要调用 DataStore):每个 requirer 都会阻塞直到模块返回。
模块 / 类模式
--!strict
local Counter = {}
Counter.__index = Counter
export type Counter = typeof(setmetatable({} :: { value: number, step: number }, Counter))
function Counter.new(step: number?): Counter
return setmetatable({ value = 0, step = step or 1 }, Counter)
end
function Counter.increment(self: Counter): number
self.value += self.step
return self.value
end
return Counter
优先使用函数 + 数据表的普通模块,而非深层继承。使用组合。
错误与重试
local DataStoreService = game:GetService("DataStoreService")
local store = DataStoreService:GetDataStore("PlayerData")
local function retry<T...>(attempts: number, fn: () -> T...): (boolean, T...)
local delaySeconds = 1
for attempt = 1, attempts do
local result = table.pack(pcall(fn))
if result[1] then
return table.unpack(result, 1, result.n)
end
if attempt < attempts then
warn(`attempt {attempt} failed: {result[2]}`)
task.wait(delaySeconds)
delaySeconds *= 2 -- exponential backoff
end
end
return false
end
local ok, data = retry(3, function()
return store:GetAsync("Player_1")
end)
print(ok, data)
error("message", 2)将错误归咎于调用方;error({ code = "X" })抛出表以实现结构化错误。xpcall(fn, debug.traceback)会保留堆栈回溯。- 不要把所有东西都包在
pcall里——只包那些可能因你代码之外的原因而失败的调用。
并发:任务库
| 调用 | 行为 |
|---|---|
task.spawn(fn, ...) | 立即在新线程上运行(直到第一次让出)。 |
task.defer(fn, ...) | 在当前恢复周期结束时运行。适合"在这完成之后"的场景。 |
task.delay(t, fn, ...) | 在 t 秒后运行。返回一个可用 task.cancel 取消的线程。 |
task.wait(t?) | 让出 ≥ t 秒(省略时为一阵);返回经过的时间。 |
task.cancel(thread) | 停止通过 spawn/delay 创建的线程。 |
task.desynchronize() / task.synchronize() | 并行 Luau(在 Actors 内)——参见 roblox-performance。 |
- 每帧工作:
RunService.Heartbeat(物理之后)、RunService.PreSimulation/PostSimulation、RunService:BindToRenderStep(客户端,渲染之前,有序)。不要用while true do task.wait() end来做 每帧逻辑;连接到事件并使用其deltaTime。 - 编写能在延迟信号行为(
Workspace.SignalBehavior)下工作的处理器:处理器会在 帧中稍后运行,而非内联。新模板位置使用Deferred,Default将切换到它, 且服务器权威要求它。不要依赖处理器的副作用在:Fire()之后立即可见。 - 永远不要在元方法或必须同步的
Changed风格回调中让出。
清理(内存泄漏是 Roblox 的头号 bug)
local Players = game:GetService("Players")
local connections: { RBXScriptConnection } = {}
table.insert(
connections,
Players.PlayerRemoving:Connect(function(player)
print(`{player.Name} left`)
end)
)
local function cleanup()
for _, connection in connections do
connection:Disconnect()
end
table.clear(connections)
end
cleanup()
Instance:Destroy()会断开该实例的连接并锁定Parent;它不会 清除你的 Lua 引用——在PlayerRemoving时把持有玩家/实例的表置为 nil。- 使用
:Once()处理一次性连接。使用 Trove/Janitor/Maid 风格的辅助工具处理成组连接。 - 长期存在的表中以 Player/Instance 对象作为键,除非在离开/销毁时移除,否则会泄漏。
Buffer 和向量(性能敏感的数据)
local packet = buffer.create(10)
buffer.writeu16(packet, 0, 513) -- item id
buffer.writef32(packet, 2, 12.5) -- amount
buffer.writei32(packet, 6, -4)
print(buffer.readu16(packet, 0), buffer.readf32(packet, 2), buffer.len(packet))
local v = vector.create(1, 2, 3) -- native 3-float vector (same type as Vector3 at runtime)
print(vector.magnitude(v), vector.normalize(v), vector.dot(v, vector.one))
buffer 是处理紧凑网络载荷、大型网格和二进制序列化的正确工具。
标准库速查:references/stdlib-cheatsheet.md。
旧写法 → 当前写法(最常见的错误)
| 不要写 | 改为写 |
|---|---|
wait()、spawn()、delay() | task.wait()、task.spawn()、task.delay() |
game.Workspace、game.Players | workspace、game:GetService("Players") |
Instance.new("Part", parent) | 创建、设置属性,然后最后设置 .Parent |
Humanoid:LoadAnimation() | Animator:LoadAnimation() |
Model:SetPrimaryPartCFrame() | Model:PivotTo() / Model:GetPivot() |
workspace:FindPartOnRay(Ray.new(...)) | workspace:Raycast(origin, direction, params) |
BodyVelocity、BodyPosition、BodyGyro | LinearVelocity、AlignPosition、AlignOrientation |
player:LoadCharacter() | player:LoadCharacterAsync() |
MarketplaceService:GetProductInfo() | :GetProductInfoAsync() |
BadgeService:AwardBadge() | :AwardBadgeAsync() |
player:GetRankInGroup() | GroupService:GetRolesInGroupAsync() / player:IsInGroupAsync() |
TeleportService:Teleport*() 各变体 | TeleportService:TeleportAsync() |
Chat 服务 / ChatService 模块 | TextChatService |
GuiObject.Draggable | UIDragDetector |
table.getn、table.foreach、getfenv | #t、for ... in、无 |
完整的弃用对照表位于 roblox-code-review 技能中。
性能惯用法(只在关键处使用)
- 先测量(
debug.profilebegin/profileend、MicroProfiler、Script Profiler)。 - 在脚本顶部使用
--!native,或在函数上使用@native,可将热点数值代码原生编译。 用在数学密集的模块上,而非到处使用(编译时间 + 内存限制)。类型标注对它有帮助。 - 在循环外缓存
Instance查找和服务引用;避免每帧进行FindFirstChild链式调用。 - 用
table.concat或buffer构建大字符串,不要在循环中反复使用..。 - 复用
RaycastParams/OverlapParams对象,而不是每次调用都创建。
相关技能
roblox-architecture(项目结构、客户端/服务端)、roblox-networking(远程通信)、
roblox-performance(性能分析、Parallel Luau)、roblox-code-review(审查清单、完整弃用映射)。