roblox-sharp-edges
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinese<!-- Source: brockmartin/roblox-game-skill (MIT) -->
<!-- 来源:brockmartin/roblox-game-skill(MIT协议) -->
Roblox Sharp Edges (Gotchas) Reference
Roblox 常见陷阱参考手册
Every entry here represents a real production footgun that has caused data loss, exploits, crashes, or hours of debugging in Roblox games.Severity Levels:
- Critical - Data loss, security breach, or revenue loss. Fix before shipping.
- High - Server instability, degraded experience, or exploit surface. Fix in current sprint.
- Medium - Correctness bugs, performance issues, or dev confusion. Fix before scale.
- Low - Code quality, maintainability, or minor timing issues. Fix when convenient.
这里的每一条记录都代表Roblox游戏中真实发生过的生产级陷阱问题,曾导致数据丢失、漏洞利用、崩溃或数小时的调试工作。严重程度等级:
- 严重 - 数据丢失、安全 breach 或收入损失。上线前必须修复。
- 高 - 服务器不稳定、体验下降或存在漏洞风险。当前迭代必须修复。
- 中 - 正确性bug、性能问题或开发者困惑。规模化前必须修复。
- 低 - 代码质量、可维护性或轻微时序问题。方便时修复。
SE-1 | Critical | DataStore Data Loss from Session Handling
SE-1 | 严重 | 会话处理不当导致DataStore数据丢失
See roblox-data → Session Locking and ProfileStore for full details.
When a player server-hops, the old server may still be saving while the new server loads stale data. ProfileStore handles session locking automatically - only one server owns a player's data at a time. Never use raw DataStoreService for player data without session locking.
详情请查看 roblox-data → Session Locking and ProfileStore。
当玩家跨服务器跳转时,旧服务器可能仍在保存数据,而新服务器加载的是过期数据。ProfileStore会自动处理会话锁定——同一时间只有一个服务器拥有玩家数据的控制权。绝不使用未加会话锁定的原生DataStoreService来存储玩家数据。
SE-2 | Critical | Client-Side Currency Manipulation
SE-2 | 严重 | 客户端货币篡改
See roblox-networking → Security Hardening for full details.
Currency and all authoritative game state must live exclusively on the server. Never accept currency amounts from the client. The server computes all transactions internally and pushes display-only updates to the client. This is the single most common exploit in Roblox games.
详情请查看 roblox-networking → Security Hardening。
货币及所有权威游戏状态必须完全存储在服务器端。绝不要接受客户端传来的货币数值。服务器应在内部计算所有交易,仅向客户端推送用于展示的更新。这是Roblox游戏中最常见的漏洞类型。
SE-3 | Critical | ProcessReceipt Mishandling
SE-3 | 严重 | ProcessReceipt处理不当
See roblox-monetization → ProcessReceipt for full details.
MarketplaceService.ProcessReceiptPurchaseGrantedPurchaseGranted详情请查看 roblox-monetization → ProcessReceipt。
MarketplaceService.ProcessReceiptPurchaseGrantedSE-4 | High | Memory Leaks from Undisconnected Events
SE-4 | 高 | 未断开事件导致内存泄漏
Problem
问题
Every returns an . If you never it, the connection persists for the script's lifetime - even after the object is destroyed. In per-player systems, memory grows linearly with every player who has ever joined.
:Connect()RBXScriptConnection:Disconnect()每个都会返回一个对象。如果从不调用,该连接会在脚本生命周期内持续存在——即使关联对象已被销毁。在按玩家实例化的系统中,内存会随着每一位加入过的玩家线性增长。
:Connect()RBXScriptConnection:Disconnect()Symptoms
症状
- Server memory climbs steadily over time.
- Server FPS degrades after hours.
- Callbacks fire for players who left.
- 服务器内存持续攀升
- 服务器FPS在数小时后下降
- 已离开玩家的回调仍会触发
Solution
解决方案
Use the Trove module (Sleitnick/RbxUtil, install via Wally) to group connections per-player and clean them all on :
PlayerRemovingluau
local Players = game:GetService("Players")
local Trove = require(game.ReplicatedStorage.Packages.Trove)
local playerTroves: { [Player]: typeof(Trove.new()) } = {}
local function onPlayerAdded(player: Player)
local trove = Trove.new()
playerTroves[player] = trove
trove:Connect(player.CharacterAdded, function(character)
local humanoid = character:WaitForChild("Humanoid")
trove:Connect(humanoid.Died, function()
task.wait(3)
player:LoadCharacter()
end)
end)
end
local function onPlayerRemoving(player: Player)
local trove = playerTroves[player]
if trove then
trove:Clean()
playerTroves[player] = nil
end
end
Players.PlayerAdded:Connect(onPlayerAdded)
Players.PlayerRemoving:Connect(onPlayerRemoving)使用Trove模块(Sleitnick/RbxUtil,可通过Wally安装)按玩家分组管理连接,并在时统一清理:
PlayerRemovingluau
local Players = game:GetService("Players")
local Trove = require(game.ReplicatedStorage.Packages.Trove)
local playerTroves: { [Player]: typeof(Trove.new()) } = {}
local function onPlayerAdded(player: Player)
local trove = Trove.new()
playerTroves[player] = trove
trove:Connect(player.CharacterAdded, function(character)
local humanoid = character:WaitForChild("Humanoid")
trove:Connect(humanoid.Died, function()
task.wait(3)
player:LoadCharacter()
end)
end)
end
local function onPlayerRemoving(player: Player)
local trove = playerTroves[player]
if trove then
trove:Clean()
playerTroves[player] = nil
end
end
Players.PlayerAdded:Connect(onPlayerAdded)
Players.PlayerRemoving:Connect(onPlayerRemoving)SE-5 | High | RemoteEvent Flooding
SE-5 | 高 | RemoteEvent 洪水攻击
Problem
问题
RemoteEvents have no built-in rate limiting. Exploiters can fire thousands of times per second, flooding the server with DataStore calls, instance creation, or raycasts.
RemoteEvent没有内置的速率限制。攻击者每秒可触发数千次请求,导致服务器被DataStore调用、实例创建或射线检测请求淹没。
Solution
解决方案
Implement per-player, per-remote rate limiting on the server. See roblox-networking → Rate Limiting for production patterns.
Minimal inline example:
luau
local lastFire: { [Player]: number } = {}
local COOLDOWN = 0.1
AttackRemote.OnServerEvent:Connect(function(player: Player, targetId: number)
local now = os.clock()
if lastFire[player] and now - lastFire[player] < COOLDOWN then return end
lastFire[player] = now
-- process attack
end)在服务器端为每个玩家、每个RemoteEvent实现速率限制。查看roblox-networking → Rate Limiting获取生产环境可用的实现模式。
极简内联示例:
luau
local lastFire: { [Player]: number } = {}
local COOLDOWN = 0.1
AttackRemote.OnServerEvent:Connect(function(player: Player, targetId: number)
local now = os.clock()
if lastFire[player] and now - lastFire[player] < COOLDOWN then return end
lastFire[player] = now
-- 处理攻击逻辑
end)SE-6 | High | BindToClose Timeout
SE-6 | 高 | BindToClose 超时
See roblox-data → Best Practices (BindToClose Handler) for full details.
game:BindToClose()task.spawn详情请查看 roblox-data → Best Practices (BindToClose Handler)。
game:BindToClose()task.spawnSE-7 | Medium | Part Count on Mobile
SE-7 | 中 | 移动端零件数量问题
Mobile devices struggle above ~10,000 visible parts. Enable StreamingEnabled and configure /. Use to mark distant models as Opportunistic and gameplay-critical models as Persistent.
StreamingMinRadiusStreamingTargetRadiusModelStreamingModeSee roblox-runtime → StreamingEnabled for configuration details.
移动设备在可见零件数量超过约10000个时会出现性能问题。启用StreamingEnabled并配置/。使用将远处模型标记为Opportunistic(按需加载),将游戏关键模型标记为Persistent(持续加载)。
StreamingMinRadiusStreamingTargetRadiusModelStreamingMode查看roblox-runtime → StreamingEnabled获取配置详情。
SE-8 | Medium | Yielding in Module Require
SE-8 | 中 | 模块Require时阻塞
Problem
问题
require()WaitForChildtask.waitrequire()WaitForChildtask.waitSolution
解决方案
Never yield in a module body. Use Init/Start lifecycle:
luau
local CombatSystem = {}
function CombatSystem:Init()
-- WaitForChild is safe here (called by bootstrap, not during require)
self._remotes = game.ReplicatedStorage:WaitForChild("Remotes", 10)
end
function CombatSystem:Start()
-- Connect events after all modules are Init'd
end
return CombatSystemBootstrap script calls on all modules, then on all modules.
:Init():Start()绝不在模块主体中执行阻塞操作。使用Init/Start生命周期模式:
luau
local CombatSystem = {}
function CombatSystem:Init()
-- WaitForChild在此处是安全的(由启动脚本调用,而非require阶段)
self._remotes = game.ReplicatedStorage:WaitForChild("Remotes", 10)
end
function CombatSystem:Start()
-- 所有模块初始化完成后再连接事件
end
return CombatSystem启动脚本先调用所有模块的方法,再调用所有模块的方法。
:Init():Start()SE-9 | Medium | Table Length with Nil Gaps
SE-9 | 中 | 含Nil间隙的表格长度问题
Problem
问题
#tbl[3] = nil#tbl#tbl[3] = nil#tblSolution
解决方案
- Never set array elements to . Use
nilto shift elements.table.remove() - Use generalized iteration () instead of
for _, v in tbl do.for i = 1, #tbl - For sparse data, use dictionary keys instead of integer indices.
- 绝不将数组元素设为Nil。使用来移动元素。
table.remove() - 使用通用迭代方式()替代
for _, v in tbl do。for i = 1, #tbl - 对于稀疏数据,使用字典键而非整数索引。
SE-10 | Low | Deprecated wait()/spawn()/delay()
SE-10 | 低 | 已废弃的wait()/spawn()/delay()
See roblox-luau-mastery → Task Library for full details.
Replace → , → , → . Legacy functions have minimum yield issues, unpredictable timing, and swallow errors.
wait()task.wait()spawn()task.spawn()delay()task.delay()详情请查看 roblox-luau-mastery → Task Library。
将替换为,替换为,替换为。旧版函数存在最小阻塞时间问题、时序不可预测且会吞掉错误。
wait()task.wait()spawn()task.spawn()delay()task.delay()SE-11 | Medium | Infinite Yield Warning
SE-11 | 中 | 无限阻塞警告
Problem
问题
WaitForChild(name)不带超时参数的会在子对象永远不存在时无限阻塞。常见于实例重命名、启用StreamingEnabled或存在竞态条件的场景。
WaitForChild(name)Solution
解决方案
Always pass a timeout. Handle return:
nilluau
local folder = ReplicatedStorage:WaitForChild("Weapons", 10)
if not folder then
warn("[Init] Weapons folder not found after 10s")
return
end始终传入超时参数,并处理返回值为Nil的情况:
luau
local folder = ReplicatedStorage:WaitForChild("Weapons", 10)
if not folder then
warn("[初始化] 10秒后仍未找到Weapons文件夹")
return
endSE-12 | Low | String Patterns vs Regex
SE-12 | 低 | 字符串模式与正则表达式的区别
Problem
问题
Luau uses Lua string patterns, not regex. doesn't work - use . Escape with not . No alternation (), no non-greedy (use instead), no lookahead.
\d%d%\|*?-Luau使用Lua字符串模式,而非正则表达式。无法生效——需使用。用而非转义特殊字符。不支持分支()、非贪婪匹配(需用替代)、前瞻断言。
\d%d%\|*?-Key Differences
核心差异
- Digits: not
%d\d - Word chars: not
%w\w - Whitespace: not
%s\s - Escape special chars: not
%.\. - Non-greedy: not
.-.*? - Literal :
%%%
- 数字:而非
%d\d - 单词字符:而非
%w\w - 空白字符:而非
%s\s - 转义特殊字符:而非
%.\. - 非贪婪匹配:而非
.-.*? - 字面量:
%%%
SE-13 | Medium | Local Function Declaration Order
SE-13 | 中 | 局部函数声明顺序
Problem
问题
Luau has no hoisting. A is invisible to code above its declaration. AI assistants frequently place helper functions below the functions that call them, causing nil-value runtime errors.
local functionLuau不支持变量提升。在其声明位置之前的代码中不可见。AI助手常将辅助函数放在调用它的函数之后,导致运行时出现Nil值错误。
local functionRule
规则
Callees above callers. Always. If calls , then must be declared first.
functionA()helperB()helperBluau
-- BAD: helperB is nil when functionA runs
local function functionA()
helperB() -- ERROR: attempt to call a nil value
end
local function helperB()
print("I'm a helper")
end
-- GOOD: helper declared first
local function helperB()
print("I'm a helper")
end
local function functionA()
helperB() -- works
end被调用函数必须在调用函数之前声明。 如果调用,则必须先声明。
functionA()helperB()helperBluau
-- 错误:functionA运行时helperB为Nil
local function functionA()
helperB() -- 错误:尝试调用Nil值
end
local function helperB()
print("我是辅助函数")
end
-- 正确:辅助函数先声明
local function helperB()
print("我是辅助函数")
end
local function functionA()
helperB() -- 正常运行
endWhen you need mutual recursion
处理递归调用
Use forward declaration:
luau
local functionB -- forward declare
local function functionA()
functionB()
end
function functionB() -- note: no 'local' (already declared above)
functionA()
end使用前置声明:
luau
local functionB -- 前置声明
local function functionA()
functionB()
end
function functionB() -- 注意:此处无需加'local'(已在上方声明)
functionA()
endQuick Reference
快速参考
CRITICAL (fix before shipping):
SE-1 DataStore session locking → Use ProfileStore
SE-2 Client-side currency → Server-authoritative only
SE-3 ProcessReceipt order → Grant THEN PurchaseGranted
HIGH (fix in current sprint):
SE-4 Undisconnected events → Trove pattern (RbxUtil)
SE-5 RemoteEvent flooding → Per-player rate limiter
SE-6 BindToClose 30s timeout → Parallel saves with task.spawn
MEDIUM (fix before scale):
SE-7 Mobile part count → StreamingEnabled + <10K parts
SE-8 Yielding in module require → Init/Start lifecycle pattern
SE-9 Table # with nil gaps → table.remove or explicit length
SE-11 Infinite yield WaitForChild → Always pass timeout parameter
SE-13 Local function order → Callees above callers (no hoisting)
LOW (fix when convenient):
SE-10 Deprecated wait/spawn/delay → task.wait/spawn/delay
SE-12 String patterns vs regex → %d not \d, % not \严重(上线前必须修复):
SE-1 DataStore会话锁定 → 使用ProfileStore
SE-2 客户端货币处理 → 仅由服务器权威控制
SE-3 ProcessReceipt执行顺序 → 先发放道具再返回PurchaseGranted
高(当前迭代必须修复):
SE-4 未断开事件连接 → 使用Trove模式(RbxUtil)
SE-5 RemoteEvent洪水攻击 → 实现按玩家速率限制
SE-6 BindToClose 30秒超时 → 用task.spawn并行保存数据
中(规模化前必须修复):
SE-7 移动端零件数量 → 启用StreamingEnabled + 控制在10000个以内
SE-8 模块Require时阻塞 → 使用Init/Start生命周期模式
SE-9 含Nil间隙的表格#运算 → 使用table.remove或显式长度计算
SE-11 WaitForChild无限阻塞 → 始终传入超时参数
SE-13 局部函数声明顺序 → 被调用函数在前(无变量提升)
低(方便时修复):
SE-10 已废弃的wait/spawn/delay → 替换为task.wait/spawn/delay
SE-12 字符串模式与正则的区别 → 使用%d而非\d,%而非\