roblox-luau-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Luau Patterns & Architecture

Luau模式与架构

When to Use

使用场景

Load this skill when the task involves:
  • Designing classes with metatables (constructors, methods, inheritance)
  • Async control flow (Promises, coroutines, pcall/xpcall, retry patterns)
  • Module structure and organization (service pattern, singletons)
  • Roblox-specific patterns (Instance creation, service access, events, task library)
  • Choosing between architectural approaches (OOP vs modules vs flat functions)
  • Error handling strategy (pcall wrapping, fallbacks, retry logic)
Hand off to other skills when:
  • Pure syntax, tables, string patterns, sharp edges →
    roblox-luau-core
  • Type annotations, generics, inference, strictness →
    roblox-luau-types
  • Networking, data persistence, security →
    roblox-networking
    ,
    roblox-data
    ,
    roblox-security
  • Performance profiling and optimization →
    roblox-performance
当任务涉及以下内容时,使用此技能:
  • 使用元表设计类(构造函数、方法、继承)
  • 异步控制流(Promise、协程、pcall/xpcall、重试模式)
  • 模块结构与组织(服务模式、单例)
  • Roblox特有模式(Instance创建、服务访问、事件、task库)
  • 架构方案选择(面向对象编程 vs 模块 vs 扁平函数)
  • 错误处理策略(pcall包装、降级方案、重试逻辑)
在以下场景下移交至其他技能:
  • 纯语法、表、字符串模式、进阶特性 →
    roblox-luau-core
  • 类型注解、泛型、类型推断、严格模式 →
    roblox-luau-types
  • 网络通信、数据持久化、安全 →
    roblox-networking
    ,
    roblox-data
    ,
    roblox-security
  • 性能分析与优化 →
    roblox-performance

Decision Rules

决策规则

  • Use metatable OOP when you need multiple instances with shared behavior
  • Use module singletons (flat table with functions) for services that exist once
  • Use Promises for async chains with error propagation; use raw pcall for one-shot fallible calls
  • Prefer
    task.*
    over deprecated globals (
    wait
    ,
    spawn
    ,
    delay
    ) unconditionally
  • Set
    Instance.Parent
    last after configuring all properties
  • Clean up connections and instances when no longer needed (memory leaks are silent killers)
  • Validate all data received from clients on the server
  • 当需要多个具有共享行为的实例时,使用元表实现面向对象编程
  • 对于仅存在一次的服务,使用模块单例(带函数的扁平表)
  • 对于带错误传播的异步链,使用Promise;对于单次易失败调用,使用原生pcall
  • 无条件优先使用
    task.*
    替代已废弃全局函数(
    wait
    ,
    spawn
    ,
    delay
  • 配置完所有属性后,最后设置
    Instance.Parent
  • 不再需要时清理连接和实例(内存泄漏是隐形杀手)
  • 在服务器端验证所有从客户端接收的数据

Philosophy

设计理念

Roblox games are long-running, stateful applications. Patterns should optimize for:
  1. Clarity over cleverness. A new team member should understand the code structure in minutes.
  2. Cleanup by default. Every connection, instance, and timer should have a clear owner and a clear destruction path.
  3. Fail gracefully. DataStores, HTTP, and remote calls can all fail. Wrap them. Have fallbacks.
  4. Server authority. The server is the source of truth. Clients request, servers validate and execute.
Roblox游戏是长期运行的有状态应用。模式应围绕以下目标优化:
  1. 清晰优于技巧。新团队成员应能在几分钟内理解代码结构。
  2. 默认支持清理。每个连接、实例和计时器都应有明确的所有者和销毁路径。
  3. 优雅降级。DataStore、HTTP和远程调用都可能失败。对其进行包装,准备降级方案。
  4. 服务器权威。服务器是事实来源。客户端发起请求,服务器验证并执行。

Recommended Libraries

推荐库

These open-source libraries are commonly used in production Roblox games. Install via Wally, Pesde, or manually:
  • Promise (evaera/roblox-lua-promise) — async control flow, retry, chaining. Use instead of raw coroutines.
  • Trove (Sleitnick/RbxUtil) — cleanup/lifecycle management. Use instead of manually tracking connections.
  • Signal (Sleitnick/RbxUtil) — typed custom signals. Use instead of BindableEvent for module-to-module communication.
  • Comm (Sleitnick/RbxUtil) — typed client-server remotes. Use instead of raw RemoteEvent/RemoteFunction.
  • Component (Sleitnick/RbxUtil) — CollectionService tag binding with lifecycle.
  • ProfileStore (loleris/MadStudioRoblox) — session-locked DataStore with retry. Use instead of raw DataStoreService.
  • t (osyrisrblx/t) — runtime type checking for RemoteEvent validation, function arguments, DataStore schemas.
The AI will recommend these when relevant. If the project already has equivalents or uses different libraries, follow the existing patterns.

以下开源库常用于生产环境的Roblox游戏中。可通过Wally、Pesde或手动方式安装:
  • Promise (evaera/roblox-lua-promise) — 异步控制流、重试、链式调用。替代原生协程使用。
  • Trove (Sleitnick/RbxUtil) — 清理/生命周期管理。替代手动跟踪连接的方式。
  • Signal (Sleitnick/RbxUtil) — 类型化自定义信号。模块间通信时替代BindableEvent使用。
  • Comm (Sleitnick/RbxUtil) — 类型化客户端-服务器远程调用。替代原生RemoteEvent/RemoteFunction使用。
  • Component (Sleitnick/RbxUtil) — 基于CollectionService标签的绑定与生命周期管理。
  • ProfileStore (loleris/MadStudioRoblox) — 会话锁定的DataStore,支持重试。替代原生DataStoreService使用。
  • t (osyrisrblx/t) — 运行时类型检查,用于RemoteEvent验证、函数参数、DataStore schema。
AI会在相关场景下推荐这些库。如果项目已有等效库或使用不同技术栈,遵循现有模式即可。

OOP Patterns

面向对象编程模式

Metatable-Based Classes

基于元表的类

luau
-- Standard OOP pattern using metatables
local Weapon = {}
Weapon.__index = Weapon

export type Weapon = typeof(setmetatable(
    {} :: {
        name: string,
        damage: number,
        durability: number,
        maxDurability: number,
    },
    Weapon
))

-- Constructor uses . (static - no instance yet)
function Weapon.new(name: string, damage: number, durability: number): Weapon
    local self = setmetatable({}, Weapon)
    self.name = name
    self.damage = damage
    self.durability = durability
    self.maxDurability = durability
    return self
end

-- Methods use : (self is implicit, don't write it as a parameter)
function Weapon:attack(target: Humanoid): boolean
    if self.durability <= 0 then
        warn(`{self.name} is broken!`)
        return false
    end

    target:TakeDamage(self.damage)
    self.durability -= 1
    return true
end

function Weapon:repair()
    self.durability = self.maxDurability
end

function Weapon:toString(): string
    return `{self.name} (DMG: {self.damage}, DUR: {self.durability}/{self.maxDurability})`
end

-- Usage: . for constructor, : for methods
local sword = Weapon.new("Iron Sword", 25, 100)
sword:attack(targetHumanoid)
print(sword:toString())
luau
-- Standard OOP pattern using metatables
local Weapon = {}
Weapon.__index = Weapon

export type Weapon = typeof(setmetatable(
    {} :: {
        name: string,
        damage: number,
        durability: number,
        maxDurability: number,
    },
    Weapon
))

-- Constructor uses . (static - no instance yet)
function Weapon.new(name: string, damage: number, durability: number): Weapon
    local self = setmetatable({}, Weapon)
    self.name = name
    self.damage = damage
    self.durability = durability
    self.maxDurability = durability
    return self
end

-- Methods use : (self is implicit, don't write it as a parameter)
function Weapon:attack(target: Humanoid): boolean
    if self.durability <= 0 then
        warn(`{self.name} is broken!`)
        return false
    end

    target:TakeDamage(self.damage)
    self.durability -= 1
    return true
end

function Weapon:repair()
    self.durability = self.maxDurability
end

function Weapon:toString(): string
    return `{self.name} (DMG: {self.damage}, DUR: {self.durability}/{self.maxDurability})`
end

-- Usage: . for constructor, : for methods
local sword = Weapon.new("Iron Sword", 25, 100)
sword:attack(targetHumanoid)
print(sword:toString())

Inheritance via Metatable Chaining

通过元表链实现继承

luau
-- Base class
local Entity = {}
Entity.__index = Entity

export type Entity = typeof(setmetatable(
    {} :: {
        name: string,
        health: number,
        maxHealth: number,
        position: Vector3,
    },
    Entity
))

function Entity.new(name: string, health: number, position: Vector3): Entity
    local self = setmetatable({}, Entity)
    self.name = name
    self.health = health
    self.maxHealth = health
    self.position = position
    return self
end

function Entity:takeDamage(amount: number)
    self.health = math.max(0, self.health - amount)
end

function Entity:isAlive(): boolean
    return self.health > 0
end

-- Derived class
local Enemy = {}
Enemy.__index = Enemy
setmetatable(Enemy, { __index = Entity }) -- inherit from Entity

export type Enemy = typeof(setmetatable(
    {} :: {
        name: string,
        health: number,
        maxHealth: number,
        position: Vector3,
        -- Enemy-specific fields
        attackDamage: number,
        aggroRange: number,
    },
    Enemy
))

function Enemy.new(name: string, health: number, position: Vector3, attackDamage: number): Enemy
    -- Call the parent constructor logic manually
    local self = setmetatable({}, Enemy) :: any
    self.name = name
    self.health = health
    self.maxHealth = health
    self.position = position
    self.attackDamage = attackDamage
    self.aggroRange = 50
    return self
end

function Enemy:attackTarget(target: Entity)
    local distance = (target.position - self.position).Magnitude
    if distance <= self.aggroRange then
        target:takeDamage(self.attackDamage)
    end
end

-- Usage: inherited methods also use :
local goblin = Enemy.new("Goblin", 50, Vector3.new(0, 0, 0), 10)
goblin:takeDamage(20)       -- inherited from Entity
goblin:attackTarget(player) -- defined on Enemy
print(goblin:isAlive())     -- inherited from Entity
luau
-- Base class
local Entity = {}
Entity.__index = Entity

export type Entity = typeof(setmetatable(
    {} :: {
        name: string,
        health: number,
        maxHealth: number,
        position: Vector3,
    },
    Entity
))

function Entity.new(name: string, health: number, position: Vector3): Entity
    local self = setmetatable({}, Entity)
    self.name = name
    self.health = health
    self.maxHealth = health
    self.position = position
    return self
end

function Entity:takeDamage(amount: number)
    self.health = math.max(0, self.health - amount)
end

function Entity:isAlive(): boolean
    return self.health > 0
end

-- Derived class
local Enemy = {}
Enemy.__index = Enemy
setmetatable(Enemy, { __index = Entity }) -- inherit from Entity

export type Enemy = typeof(setmetatable(
    {} :: {
        name: string,
        health: number,
        maxHealth: number,
        position: Vector3,
        -- Enemy-specific fields
        attackDamage: number,
        aggroRange: number,
    },
    Enemy
))

function Enemy.new(name: string, health: number, position: Vector3, attackDamage: number): Enemy
    -- Call the parent constructor logic manually
    local self = setmetatable({}, Enemy) :: any
    self.name = name
    self.health = health
    self.maxHealth = health
    self.position = position
    self.attackDamage = attackDamage
    self.aggroRange = 50
    return self
end

function Enemy:attackTarget(target: Entity)
    local distance = (target.position - self.position).Magnitude
    if distance <= self.aggroRange then
        target:takeDamage(self.attackDamage)
    end
end

-- Usage: inherited methods also use :
local goblin = Enemy.new("Goblin", 50, Vector3.new(0, 0, 0), 10)
goblin:takeDamage(20)       -- inherited from Entity
goblin:attackTarget(player) -- defined on Enemy
print(goblin:isAlive())     -- inherited from Entity

When to Use OOP vs Modules

面向对象编程 vs 模块的适用场景

  • OOP (metatables): Multiple instances with shared behavior. Enemies, weapons, UI components, data models.
  • Module singleton: One instance, acts as a service. CombatService, InventoryManager, MatchManager.
  • Flat functions: Stateless utilities. Math helpers, string formatters, validation functions.

  • 面向对象编程(元表): 多个具有共享行为的实例。例如敌人、武器、UI组件、数据模型。
  • 模块单例: 仅一个实例,作为服务使用。例如CombatService、InventoryManager、MatchManager。
  • 扁平函数: 无状态工具类。例如数学助手、字符串格式化器、验证函数。

Module-Based Service Pattern

基于模块的服务模式

luau
-- A common Roblox pattern: modules that act as singletons/services
-- File: ServerScriptService/Services/CombatService.lua

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")

local CombatService = {}

local activeBuffs: { [Player]: { string } } = {}

function CombatService.init()
    Players.PlayerRemoving:Connect(function(player: Player)
        activeBuffs[player] = nil -- cleanup on leave
    end)
end

function CombatService.calculateDamage(attacker: Player, baseDamage: number): number
    local multiplier = 1.0
    local buffs = activeBuffs[attacker]
    if buffs then
        for _, buff in buffs do
            if buff == "strength" then
                multiplier += 0.5
            end
        end
    end
    return math.floor(baseDamage * multiplier)
end

function CombatService.addBuff(player: Player, buffName: string)
    if not activeBuffs[player] then
        activeBuffs[player] = {}
    end
    table.insert(activeBuffs[player], buffName)
end

function CombatService.removeBuff(player: Player, buffName: string)
    local buffs = activeBuffs[player]
    if not buffs then
        return
    end
    local index = table.find(buffs, buffName)
    if index then
        table.remove(buffs, index)
    end
end

return CombatService
luau
-- A common Roblox pattern: modules that act as singletons/services
-- File: ServerScriptService/Services/CombatService.lua

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")

local CombatService = {}

local activeBuffs: { [Player]: { string } } = {}

function CombatService.init()
    Players.PlayerRemoving:Connect(function(player: Player)
        activeBuffs[player] = nil -- cleanup on leave
    end)
end

function CombatService.calculateDamage(attacker: Player, baseDamage: number): number
    local multiplier = 1.0
    local buffs = activeBuffs[attacker]
    if buffs then
        for _, buff in buffs do
            if buff == "strength" then
                multiplier += 0.5
            end
        end
    end
    return math.floor(baseDamage * multiplier)
end

function CombatService.addBuff(player: Player, buffName: string)
    if not activeBuffs[player] then
        activeBuffs[player] = {}
    end
    table.insert(activeBuffs[player], buffName)
end

function CombatService.removeBuff(player: Player, buffName: string)
    local buffs = activeBuffs[player]
    if not buffs then
        return
    end
    local index = table.find(buffs, buffName)
    if index then
        table.remove(buffs, index)
    end
end

return CombatService

Module Structure Template

模块结构模板

luau
-- Standard module template
-- File: ReplicatedStorage/Modules/InventoryManager.lua

-- Services at the top
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")

-- Dependencies
local Types = require(ReplicatedStorage.Shared.Types)
local Signal = require(ReplicatedStorage.Packages.Signal)

-- Constants
local MAX_SLOTS = 20
local STACK_LIMIT = 99

-- Module table
local InventoryManager = {}

-- Private state
local inventories: { [Player]: Types.Inventory } = {}

-- Public API with type annotations
function InventoryManager.getInventory(player: Player): Types.Inventory?
    return inventories[player]
end

function InventoryManager.addItem(player: Player, itemId: string, quantity: number): boolean
    local inventory = inventories[player]
    if not inventory then
        return false
    end
    -- ... implementation
    return true
end

-- Initialization
function InventoryManager.init()
    Players.PlayerAdded:Connect(function(player: Player)
        inventories[player] = { slots = {}, gold = 0 }
    end)

    Players.PlayerRemoving:Connect(function(player: Player)
        inventories[player] = nil
    end)
end

return InventoryManager

luau
-- Standard module template
-- File: ReplicatedStorage/Modules/InventoryManager.lua

-- Services at the top
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")

-- Dependencies
local Types = require(ReplicatedStorage.Shared.Types)
local Signal = require(ReplicatedStorage.Packages.Signal)

-- Constants
local MAX_SLOTS = 20
local STACK_LIMIT = 99

-- Module table
local InventoryManager = {}

-- Private state
local inventories: { [Player]: Types.Inventory } = {}

-- Public API with type annotations
function InventoryManager.getInventory(player: Player): Types.Inventory?
    return inventories[player]
end

function InventoryManager.addItem(player: Player, itemId: string, quantity: number): boolean
    local inventory = inventories[player]
    if not inventory then
        return false
    end
    -- ... implementation
    return true
end

-- Initialization
function InventoryManager.init()
    Players.PlayerAdded:Connect(function(player: Player)
        inventories[player] = { slots = {}, gold = 0 }
    end)

    Players.PlayerRemoving:Connect(function(player: Player)
        inventories[player] = nil
    end)
end

return InventoryManager

Roblox-Specific Patterns

Roblox特有模式

Instance Creation

Instance创建

luau
-- Create, configure, then ALWAYS set Parent last (avoids replication race)
local part = Instance.new("Part")
part.Name = "Floor"
part.Size = Vector3.new(50, 1, 50)
part.Anchored = true
part.Parent = workspace -- Parent last!
luau
-- Create, configure, then ALWAYS set Parent last (avoids replication race)
local part = Instance.new("Part")
part.Name = "Floor"
part.Size = Vector3.new(50, 1, 50)
part.Anchored = true
part.Parent = workspace -- Parent last!

Service Access

服务访问

luau
-- GetService is the canonical way to access Roblox services
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerStorage = game:GetService("ServerStorage")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local TweenService = game:GetService("TweenService")
local HttpService = game:GetService("HttpService")
local CollectionService = game:GetService("CollectionService")
local PhysicsService = game:GetService("PhysicsService")
local MarketplaceService = game:GetService("MarketplaceService")
local DataStoreService = game:GetService("DataStoreService")
local Debris = game:GetService("Debris")

-- Services should be declared at the top of each script
-- and stored in local variables for performance and clarity
luau
-- GetService is the canonical way to access Roblox services
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerStorage = game:GetService("ServerStorage")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local TweenService = game:GetService("TweenService")
local HttpService = game:GetService("HttpService")
local CollectionService = game:GetService("CollectionService")
local PhysicsService = game:GetService("PhysicsService")
local MarketplaceService = game:GetService("MarketplaceService")
local DataStoreService = game:GetService("DataStoreService")
local Debris = game:GetService("Debris")

-- Services should be declared at the top of each script
-- and stored in local variables for performance and clarity

Event Connections

事件连接

luau
-- Connecting to events returns an RBXScriptConnection
local Players = game:GetService("Players")

local connection: RBXScriptConnection
connection = Players.PlayerAdded:Connect(function(player: Player)
    print(`{player.Name} joined the game`)
end)

-- Disconnecting when no longer needed (prevents memory leaks)
connection:Disconnect()

-- One-shot connection with :Once()
Players.PlayerAdded:Once(function(player: Player)
    print(`First player to join: {player.Name}`)
    -- Automatically disconnects after firing once
end)

-- Waiting for an event to fire (yields the current thread)
local player = Players.PlayerAdded:Wait()
print(`{player.Name} joined`)

-- Common event patterns
local RunService = game:GetService("RunService")

-- Heartbeat fires every frame after physics (use for most game logic)
RunService.Heartbeat:Connect(function(deltaTime: number)
    -- deltaTime is seconds since last frame
end)

-- Stepped fires every frame before physics
RunService.Stepped:Connect(function(elapsedTime: number, deltaTime: number)
    -- use for input processing or pre-physics logic
end)

-- Property change events
local part = workspace:FindFirstChild("MyPart") :: Part
part:GetPropertyChangedSignal("Position"):Connect(function()
    print(`Part moved to {part.Position}`)
end)

-- Child events
workspace.ChildAdded:Connect(function(child: Instance)
    print(`New child: {child.Name}`)
end)
luau
-- Connecting to events returns an RBXScriptConnection
local Players = game:GetService("Players")

local connection: RBXScriptConnection
connection = Players.PlayerAdded:Connect(function(player: Player)
    print(`{player.Name} joined the game`)
end)

-- Disconnecting when no longer needed (prevents memory leaks)
connection:Disconnect()

-- One-shot connection with :Once()
Players.PlayerAdded:Once(function(player: Player)
    print(`First player to join: {player.Name}`)
    -- Automatically disconnects after firing once
end)

-- Waiting for an event to fire (yields the current thread)
local player = Players.PlayerAdded:Wait()
print(`{player.Name} joined`)

-- Common event patterns
local RunService = game:GetService("RunService")

-- Heartbeat fires every frame after physics (use for most game logic)
RunService.Heartbeat:Connect(function(deltaTime: number)
    -- deltaTime is seconds since last frame
end)

-- Stepped fires every frame before physics
RunService.Stepped:Connect(function(elapsedTime: number, deltaTime: number)
    -- use for input processing or pre-physics logic
end)

-- Property change events
local part = workspace:FindFirstChild("MyPart") :: Part
part:GetPropertyChangedSignal("Position"):Connect(function()
    print(`Part moved to {part.Position}`)
end)

-- Child events
workspace.ChildAdded:Connect(function(child: Instance)
    print(`New child: {child.Name}`)
end)

Instance Tree Traversal

Instance树遍历

luau
-- FindFirstChild: returns first direct child with name (or nil)
local head = character:FindFirstChild("Head")
if head then
    print("Found head")
end

-- FindFirstChild with recursive flag
local sword = workspace:FindFirstChild("Sword", true) -- searches entire subtree

-- FindFirstChildOfClass: by ClassName
local humanoid = character:FindFirstChildOfClass("Humanoid")

-- FindFirstChildWhichIsA: by class hierarchy (includes inherited classes)
local basePart = model:FindFirstChildWhichIsA("BasePart")

-- WaitForChild: yields until child exists (with optional timeout)
local tool = player.Backpack:WaitForChild("Sword")
local toolOrNil = player.Backpack:WaitForChild("Sword", 5) -- 5 second timeout

-- GetChildren: returns array of direct children
local children = workspace:GetChildren()
for _, child in children do
    print(child.Name)
end

-- GetDescendants: returns array of ALL descendants (recursive)
local allParts: { BasePart } = {}
for _, descendant in workspace:GetDescendants() do
    if descendant:IsA("BasePart") then
        table.insert(allParts, descendant)
    end
end

-- Filtering with CollectionService (tag-based)
local CollectionService = game:GetService("CollectionService")
local enemies = CollectionService:GetTagged("Enemy")
for _, enemy in enemies do
    print(enemy.Name)
end

-- Listen for tagged instances
CollectionService:GetInstanceAddedSignal("Enemy"):Connect(function(instance)
    setupEnemy(instance)
end)

CollectionService:GetInstanceRemovedSignal("Enemy"):Connect(function(instance)
    cleanupEnemy(instance)
end)
luau
-- FindFirstChild: returns first direct child with name (or nil)
local head = character:FindFirstChild("Head")
if head then
    print("Found head")
end

-- FindFirstChild with recursive flag
local sword = workspace:FindFirstChild("Sword", true) -- searches entire subtree

-- FindFirstChildOfClass: by ClassName
local humanoid = character:FindFirstChildOfClass("Humanoid")

-- FindFirstChildWhichIsA: by class hierarchy (includes inherited classes)
local basePart = model:FindFirstChildWhichIsA("BasePart")

-- WaitForChild: yields until child exists (with optional timeout)
local tool = player.Backpack:WaitForChild("Sword")
local toolOrNil = player.Backpack:WaitForChild("Sword", 5) -- 5 second timeout

-- GetChildren: returns array of direct children
local children = workspace:GetChildren()
for _, child in children do
    print(child.Name)
end

-- GetDescendants: returns array of ALL descendants (recursive)
local allParts: { BasePart } = {}
for _, descendant in workspace:GetDescendants() do
    if descendant:IsA("BasePart") then
        table.insert(allParts, descendant)
    end
end

-- Filtering with CollectionService (tag-based)
local CollectionService = game:GetService("CollectionService")
local enemies = CollectionService:GetTagged("Enemy")
for _, enemy in enemies do
    print(enemy.Name)
end

-- Listen for tagged instances
CollectionService:GetInstanceAddedSignal("Enemy"):Connect(function(instance)
    setupEnemy(instance)
end)

CollectionService:GetInstanceRemovedSignal("Enemy"):Connect(function(instance)
    cleanupEnemy(instance)
end)

Task Library

Task库

The
task
library is the modern replacement for deprecated globals
wait()
,
spawn()
, and
delay()
.
luau
-- task.wait: yields the current thread for a duration (returns actual elapsed time)
local elapsed = task.wait(2) -- waits ~2 seconds
print(`Actually waited {elapsed} seconds`)

-- task.spawn: runs a function immediately in a new thread (resumes caller after)
task.spawn(function()
    print("This runs immediately in a new coroutine")
    task.wait(5)
    print("This runs 5 seconds later")
end)
print("This also runs immediately, after the spawned function yields")

-- task.delay: runs a function after a delay
task.delay(3, function()
    print("This runs after 3 seconds")
end)

-- task.defer: runs a function at the end of the current resumption cycle
-- Useful for deferring work without a delay
task.defer(function()
    print("This runs after the current thread and any task.spawn calls finish")
end)

-- task.cancel: cancels a thread created by task.spawn or task.delay
local thread = task.delay(10, function()
    print("This will never run")
end)
task.cancel(thread)

-- task.synchronize / task.desynchronize: for Parallel Luau
-- task.synchronize() -- switch to serial execution
-- task.desynchronize() -- switch to parallel execution

task
库是已废弃全局函数
wait()
spawn()
delay()
的现代替代方案。
luau
-- task.wait: yields the current thread for a duration (returns actual elapsed time)
local elapsed = task.wait(2) -- waits ~2 seconds
print(`Actually waited {elapsed} seconds`)

-- task.spawn: runs a function immediately in a new thread (resumes caller after)
task.spawn(function()
    print("This runs immediately in a new coroutine")
    task.wait(5)
    print("This runs 5 seconds later")
end)
print("This also runs immediately, after the spawned function yields")

-- task.delay: runs a function after a delay
task.delay(3, function()
    print("This runs after 3 seconds")
end)

-- task.defer: runs a function at the end of the current resumption cycle
-- Useful for deferring work without a delay
task.defer(function()
    print("This runs after the current thread and any task.spawn calls finish")
end)

-- task.cancel: cancels a thread created by task.spawn or task.delay
local thread = task.delay(10, function()
    print("This will never run")
end)
task.cancel(thread)

-- task.synchronize / task.desynchronize: for Parallel Luau
-- task.synchronize() -- switch to serial execution
-- task.desynchronize() -- switch to parallel execution

Async Patterns

异步模式

pcall and xpcall for Error Handling

用于错误处理的pcall和xpcall

luau
-- pcall wraps a function call and catches errors
local success, result = pcall(function()
    return game:GetService("DataStoreService"):GetDataStore("PlayerData")
end)

if success then
    print("Got data store:", result)
else
    warn("Failed to get data store:", result)
end

-- pcall with arguments (passed after the function)
local success, data = pcall(dataStore.GetAsync, dataStore, "player_123")

-- xpcall provides a custom error handler with stack trace
local success, result = xpcall(function()
    error("Something went wrong")
end, function(err)
    -- err is the error message
    warn("Error:", err)
    warn("Stack:", debug.traceback())
    return err -- returned as 'result' if success is false
end)

-- Pattern: retry with pcall
local function retryAsync<T>(maxAttempts: number, delayBetween: number, fn: () -> T): T?
    for attempt = 1, maxAttempts do
        local success, result = pcall(fn)
        if success then
            return result
        end
        if attempt < maxAttempts then
            warn(`Attempt {attempt} failed: {result}. Retrying in {delayBetween}s...`)
            task.wait(delayBetween)
        else
            warn(`All {maxAttempts} attempts failed. Last error: {result}`)
        end
    end
    return nil
end

-- Usage: retry DataStore calls
local data = retryAsync(3, 1, function()
    return dataStore:GetAsync("player_123")
end)
luau
-- pcall wraps a function call and catches errors
local success, result = pcall(function()
    return game:GetService("DataStoreService"):GetDataStore("PlayerData")
end)

if success then
    print("Got data store:", result)
else
    warn("Failed to get data store:", result)
end

-- pcall with arguments (passed after the function)
local success, data = pcall(dataStore.GetAsync, dataStore, "player_123")

-- xpcall provides a custom error handler with stack trace
local success, result = xpcall(function()
    error("Something went wrong")
end, function(err)
    -- err is the error message
    warn("Error:", err)
    warn("Stack:", debug.traceback())
    return err -- returned as 'result' if success is false
end)

-- Pattern: retry with pcall
local function retryAsync<T>(maxAttempts: number, delayBetween: number, fn: () -> T): T?
    for attempt = 1, maxAttempts do
        local success, result = pcall(fn)
        if success then
            return result
        end
        if attempt < maxAttempts then
            warn(`Attempt {attempt} failed: {result}. Retrying in {delayBetween}s...`)
            task.wait(delayBetween)
        else
            warn(`All {maxAttempts} attempts failed. Last error: {result}`)
        end
    end
    return nil
end

-- Usage: retry DataStore calls
local data = retryAsync(3, 1, function()
    return dataStore:GetAsync("player_123")
end)

Coroutines

协程

luau
-- Coroutines allow cooperative multitasking
local function producer(): ()
    for i = 1, 5 do
        coroutine.yield(i)
    end
end

local co = coroutine.create(producer)
for i = 1, 5 do
    local success, value = coroutine.resume(co)
    print(value) --> 1, 2, 3, 4, 5
end

-- coroutine.wrap creates a function that resumes automatically
local nextValue = coroutine.wrap(producer)
print(nextValue()) --> 1
print(nextValue()) --> 2

-- Practical example: staggered initialization
local function initSystems(systems: { { name: string, init: () -> () } })
    for _, system in systems do
        task.spawn(function()
            local success, err = pcall(system.init)
            if not success then
                warn(`Failed to initialize {system.name}: {err}`)
            else
                print(`{system.name} initialized`)
            end
        end)
    end
end
luau
-- Coroutines allow cooperative multitasking
local function producer(): ()
    for i = 1, 5 do
        coroutine.yield(i)
    end
end

local co = coroutine.create(producer)
for i = 1, 5 do
    local success, value = coroutine.resume(co)
    print(value) --> 1, 2, 3, 4, 5
end

-- coroutine.wrap creates a function that resumes automatically
local nextValue = coroutine.wrap(producer)
print(nextValue()) --> 1
print(nextValue()) --> 2

-- Practical example: staggered initialization
local function initSystems(systems: { { name: string, init: () -> () } })
    for _, system in systems do
        task.spawn(function()
            local success, err = pcall(system.init)
            if not success then
                warn(`Failed to initialize {system.name}: {err}`)
            else
                print(`{system.name} initialized`)
            end
        end)
    end
end

Promise Pattern (roblox-lua-promise)

Promise模式(roblox-lua-promise)

The
Promise
library is the community-standard for async control flow in Roblox. It must be installed as a module (e.g., via Wally or manually).
luau
local Promise = require(ReplicatedStorage.Packages.Promise)

-- Creating a Promise
local function loadPlayerData(player: Player)
    return Promise.new(function(resolve, reject, onCancel)
        local key = `player_{player.UserId}`

        -- Support cancellation
        local cancelled = false
        onCancel(function()
            cancelled = true
        end)

        local success, data = pcall(dataStore.GetAsync, dataStore, key)
        if cancelled then
            return
        end

        if success then
            resolve(data or {})
        else
            reject(`Failed to load data: {data}`)
        end
    end)
end

-- Chaining promises
loadPlayerData(player)
    :andThen(function(data)
        print("Data loaded:", data)
        return processData(data)
    end)
    :andThen(function(processed)
        applyData(player, processed)
    end)
    :catch(function(err)
        warn("Error:", err)
    end)
    :finally(function()
        print("Load attempt complete")
    end)

-- Promise.all: wait for multiple promises
Promise.all({
    loadPlayerData(player),
    loadInventory(player),
    loadSettings(player),
}):andThen(function(results)
    local data, inventory, settings = results[1], results[2], results[3]
    -- All loaded successfully
end):catch(function(err)
    warn("One or more loads failed:", err)
end)

-- Promise.race: first to resolve wins
Promise.race({
    fetchFromPrimary(),
    Promise.delay(5):andThen(function()
        return fetchFromBackup()
    end),
})

-- Promise.retry
Promise.retry(function()
    return loadPlayerData(player)
end, 3):andThen(function(data)
    print("Loaded after retry")
end)

-- Wrapping yielding code in a Promise
local function waitForCharacter(player: Player)
    return Promise.new(function(resolve)
        local character = player.Character or player.CharacterAdded:Wait()
        resolve(character)
    end)
end

Promise
库是Roblox社区标准的异步控制流方案。必须作为模块安装(例如通过Wally或手动方式)。
luau
local Promise = require(ReplicatedStorage.Packages.Promise)

-- Creating a Promise
local function loadPlayerData(player: Player)
    return Promise.new(function(resolve, reject, onCancel)
        local key = `player_{player.UserId}`

        -- Support cancellation
        local cancelled = false
        onCancel(function()
            cancelled = true
        end)

        local success, data = pcall(dataStore.GetAsync, dataStore, key)
        if cancelled then
            return
        end

        if success then
            resolve(data or {})
        else
            reject(`Failed to load data: {data}`)
        end
    end)
end

-- Chaining promises
loadPlayerData(player)
    :andThen(function(data)
        print("Data loaded:", data)
        return processData(data)
    end)
    :andThen(function(processed)
        applyData(player, processed)
    end)
    :catch(function(err)
        warn("Error:", err)
    end)
    :finally(function()
        print("Load attempt complete")
    end)

-- Promise.all: wait for multiple promises
Promise.all({
    loadPlayerData(player),
    loadInventory(player),
    loadSettings(player),
}):andThen(function(results)
    local data, inventory, settings = results[1], results[2], results[3]
    -- All loaded successfully
end):catch(function(err)
    warn("One or more loads failed:", err)
end)

-- Promise.race: first to resolve wins
Promise.race({
    fetchFromPrimary(),
    Promise.delay(5):andThen(function()
        return fetchFromBackup()
    end),
})

-- Promise.retry
Promise.retry(function()
    return loadPlayerData(player)
end, 3):andThen(function(data)
    print("Loaded after retry")
end)

-- Wrapping yielding code in a Promise
local function waitForCharacter(player: Player)
    return Promise.new(function(resolve)
        local character = player.Character or player.CharacterAdded:Wait()
        resolve(character)
    end)
end

Best Practices

最佳实践

Naming Conventions

命名规范

luau
-- PascalCase: classes, modules, services, types, enums
local CombatService = {}
local WeaponManager = require(script.WeaponManager)
type PlayerData = { name: string, level: number }

-- camelCase: variables, function names, method names, parameters
local playerHealth = 100
local function calculateDamage(baseDamage: number): number end
function Weapon:getDurability(): number end

-- UPPER_CASE: constants
local MAX_HEALTH = 100
local RESPAWN_DELAY = 5
local DEFAULT_SPEED = 16

-- Prefix private methods with underscore (convention, not enforced)
function MyClass:_internalMethod() end
local _cachedValue = nil
luau
-- PascalCase: classes, modules, services, types, enums
local CombatService = {}
local WeaponManager = require(script.WeaponManager)
type PlayerData = { name: string, level: number }

-- camelCase: variables, function names, method names, parameters
local playerHealth = 100
local function calculateDamage(baseDamage: number): number end
function Weapon:getDurability(): number end

-- UPPER_CASE: constants
local MAX_HEALTH = 100
local RESPAWN_DELAY = 5
local DEFAULT_SPEED = 16

-- Prefix private methods with underscore (convention, not enforced)
function MyClass:_internalMethod() end
local _cachedValue = nil

Method Definitions

方法定义

  • Use
    :
    (colon) for instance methods - self is implicit
  • Use
    .
    (dot) for constructors and static methods - self must be explicit
luau
-- : for instance methods (self is implicit)
function MyClass:methodName()
    -- self refers to the instance
end

-- . for constructors and static methods (self must be explicit)
function MyClass.new()
    local self = setmetatable({}, MyClass)
    return self
end

-- Calling conventions match definition
obj:methodName()        -- colon: self passed implicitly
MyClass.new()           -- dot: no self
Key rule:
:
is syntactic sugar for
.
with automatic
self
injection.
obj:method(a)
is equivalent to
obj.method(obj, a)
.
  • 实例方法使用
    :
    (冒号)——self是隐式传递的
  • 构造函数和静态方法使用
    .
    (点)——self必须显式传递
luau
-- : for instance methods (self is implicit)
function MyClass:methodName()
    -- self refers to the instance
end

-- . for constructors and static methods (self must be explicit)
function MyClass.new()
    local self = setmetatable({}, MyClass)
    return self
end

-- Calling conventions match definition
obj:methodName()        -- colon: self passed implicitly
MyClass.new()           -- dot: no self
核心规则:
:
.
的语法糖,会自动注入
self
obj:method(a)
等价于
obj.method(obj, a)

General Guidelines

通用指南

  • Use
    local
    for every variable and function declaration.
  • Add type annotations on all public module function signatures.
  • Use
    task.wait()
    /
    task.spawn()
    /
    task.delay()
    /
    task.defer()
    instead of deprecated globals.
  • Use
    typeof()
    instead of
    type()
    for Roblox-aware type checking.
  • Set
    Instance.Parent
    last after configuring all properties (avoids unnecessary replication and change events).
  • Clean up event connections and instances when no longer needed to avoid memory leaks.
  • Validate all data received from clients on the server. Never trust the client.
  • Use
    pcall
    /
    xpcall
    around any call that can fail (DataStores, HTTP, etc.).
  • Use backtick interpolation (
    {expr}
    ) for all string building. Never use
    ..
    concatenation.
  • Use
    table.freeze()
    for configuration tables that should not be modified.
  • Never use Luau reserved keywords as identifiers.
  • Declare local functions before they are called - Luau has no hoisting.

  • 所有变量和函数声明都使用
    local
  • 所有公共模块函数签名添加类型注解。
  • 使用
    task.wait()
    /
    task.spawn()
    /
    task.delay()
    /
    task.defer()
    替代已废弃全局函数。
  • 针对Roblox相关类型检查,使用
    typeof()
    而非
    type()
  • 配置完所有属性后,最后设置
    Instance.Parent
    (避免不必要的复制和变更事件)。
  • 不再需要时清理事件连接和实例,避免内存泄漏。
  • 在服务器端验证所有从客户端接收的数据。永远不要信任客户端。
  • 对所有可能失败的调用(DataStore、HTTP等)使用
    pcall
    /
    xpcall
    包裹。
  • 所有字符串构建使用反引号插值(
    {expr}
    )。永远不要使用
    ..
    拼接。
  • 对不应被修改的配置表使用
    table.freeze()
  • 永远不要使用Luau保留关键字作为标识符。
  • 调用前声明局部函数——Luau没有变量提升。

Anti-Patterns

反模式

Deprecated Global Functions

已废弃全局函数

luau
-- BAD: deprecated, unpredictable resume timing, no cancellation
wait(2)
spawn(function() end)
delay(2, function() end)

-- GOOD: modern task library equivalents
task.wait(2)
task.spawn(function() end)
task.delay(2, function() end)
luau
-- BAD: deprecated, unpredictable resume timing, no cancellation
wait(2)
spawn(function() end)
delay(2, function() end)

-- GOOD: modern task library equivalents
task.wait(2)
task.spawn(function() end)
task.delay(2, function() end)

Polling Instead of Events

使用轮询而非事件

luau
-- BAD: polling wastes CPU cycles
while true do
    local target = findNearestEnemy()
    if target then
        attack(target)
    end
    task.wait(0.1)
end

-- GOOD: use events or Heartbeat with state checks
local RunService = game:GetService("RunService")
RunService.Heartbeat:Connect(function(dt: number)
    local target = findNearestEnemy()
    if target then
        attack(target)
    end
end)

-- GOOD: use events when possible
CollectionService:GetInstanceAddedSignal("Enemy"):Connect(function(enemy)
    onEnemySpawned(enemy)
end)
luau
-- BAD: polling wastes CPU cycles
while true do
    local target = findNearestEnemy()
    if target then
        attack(target)
    end
    task.wait(0.1)
end

-- GOOD: use events or Heartbeat with state checks
local RunService = game:GetService("RunService")
RunService.Heartbeat:Connect(function(dt: number)
    local target = findNearestEnemy()
    if target then
        attack(target)
    end
end)

-- GOOD: use events when possible
CollectionService:GetInstanceAddedSignal("Enemy"):Connect(function(enemy)
    onEnemySpawned(enemy)
end)

String Concatenation in Loops

循环中使用字符串拼接

luau
-- BAD: creates a new string every iteration (O(n^2) memory)
local result = ""
for i = 1, 1000 do
    result = result .. tostring(i) .. ","
end

-- GOOD: collect into table, join once (O(n))
local parts = {}
for i = 1, 1000 do
    table.insert(parts, tostring(i))
end
local result = table.concat(parts, ",")
luau
-- BAD: creates a new string every iteration (O(n^2) memory)
local result = ""
for i = 1, 1000 do
    result = result .. tostring(i) .. ","
end

-- GOOD: collect into table, join once (O(n))
local parts = {}
for i = 1, 1000 do
    table.insert(parts, tostring(i))
end
local result = table.concat(parts, ",")

Missing pcall on Fallible Calls

易失败调用未包裹pcall

luau
-- BAD: crashes the script if the call fails
local data = dataStore:GetAsync("key")
local response = HttpService:RequestAsync({ Url = "https://api.example.com" })

-- GOOD: wrap in pcall
local success, data = pcall(dataStore.GetAsync, dataStore, "key")
if not success then
    warn("DataStore read failed:", data)
    data = {} -- fallback
end

local success, response = pcall(HttpService.RequestAsync, HttpService, {
    Url = "https://api.example.com",
})
if not success then
    warn("HTTP request failed:", response)
end
luau
-- BAD: crashes the script if the call fails
local data = dataStore:GetAsync("key")
local response = HttpService:RequestAsync({ Url = "https://api.example.com" })

-- GOOD: wrap in pcall
local success, data = pcall(dataStore.GetAsync, dataStore, "key")
if not success then
    warn("DataStore read failed:", data)
    data = {} -- fallback
end

local success, response = pcall(HttpService.RequestAsync, HttpService, {
    Url = "https://api.example.com",
})
if not success then
    warn("HTTP request failed:", response)
end

Trusting Client Input

信任客户端输入

For server-authoritative validation patterns (type checking, range checking, ownership, rate limiting), see roblox-networking → Client Validation.
Core rule: Never trust client input. Every
OnServerEvent
handler must validate types, ranges, and ownership before processing.

关于服务器权威验证模式(类型检查、范围检查、所有权、速率限制),请参阅roblox-networking → 客户端验证。
核心规则: 永远不要信任客户端输入。每个
OnServerEvent
处理器在处理前必须验证类型、范围和所有权。

Common Mistakes

常见错误

  • Forgetting
    MyClass.__index = MyClass
    (methods won't resolve)
  • Setting properties on the class table instead of
    self
    (shared across all instances)
  • Not cleaning up connections on object destruction (memory leaks)
  • Using raw coroutines where Promises would give better error propagation
  • Calling
    :Destroy()
    without disconnecting signals first (Trove solves this)
  • Forgetting pcall around DataStore/HTTP calls (silent script death)
  • Using
    wait()
    instead of
    task.wait()
    (deprecated, unpredictable timing)
  • Setting Parent before configuring properties (replication race)
  • 忘记设置
    MyClass.__index = MyClass
    (方法无法解析)
  • 在类表而非
    self
    上设置属性(所有实例共享该属性)
  • 对象销毁时未清理连接(内存泄漏)
  • 在Promise更适合的场景下使用原生协程(错误传播效果差)
  • 未先断开信号就调用
    :Destroy()
    (Trove可解决此问题)
  • DataStore/HTTP调用未包裹pcall(脚本静默崩溃)
  • 使用
    wait()
    而非
    task.wait()
    (已废弃,时序不可预测)
  • 配置属性前设置Parent(复制竞争)

Quality Checklist

质量检查清单

  • Classes have
    __index
    set correctly
  • Constructors use
    .
    , methods use
    :
  • All fallible calls wrapped in pcall/xpcall
  • Event connections have a clear cleanup path
  • Instance.Parent set last
  • No deprecated globals (wait/spawn/delay)
  • Module has clear public API vs private state
  • Async chains have error handling (
    :catch()
    or pcall)
  • Player data cleaned up on PlayerRemoving
  • 类已正确设置
    __index
  • 构造函数使用
    .
    ,方法使用
    :
  • 所有易失败调用已用pcall/xpcall包裹
  • 事件连接有明确的清理路径
  • Instance.Parent最后设置
  • 无已废弃全局函数(wait/spawn/delay)
  • 模块区分清晰的公共API与私有状态
  • 异步链有错误处理(
    :catch()
    或pcall)
  • PlayerRemoving时清理玩家数据