roblox-architecture
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinese<!-- Source: brockmartin/roblox-game-skill (MIT) -->
<!-- 来源:brockmartin/roblox-game-skill(MIT协议) -->
Roblox Game Architecture Reference
Roblox游戏架构参考
1. Overview
1. 概述
Load this reference when:
- Starting a new Roblox game from scratch and need to decide where code and assets go
- Organizing or refactoring an existing codebase that has grown unwieldy
- Answering architecture questions: "Where should this script go?", "How should client talk to server?", "How do I structure modules?"
- Onboarding onto a Roblox project and need to understand the standard conventions
- Choosing between a flat script layout and a modular loader pattern
This document covers the Roblox data model, service hierarchy, script types, client-server communication, module patterns, framework options, folder organization, best practices, and anti-patterns.
在以下场景使用本参考文档:
- 从零开始创建新Roblox游戏,需要确定代码和资源的存放位置
- 整理或重构已变得杂乱无章的现有代码库
- 解答架构相关问题:「这个脚本应该放在哪里?」「客户端应如何与服务器通信?」「我该如何组织模块?」
- 加入Roblox项目,需要了解标准约定
- 在扁平脚本布局和模块化加载器模式之间做选择
本文档涵盖Roblox数据模型、服务层级、脚本类型、客户端-服务器通信、模块模式、框架选项、文件夹组织、最佳实践和反模式。
Quick Reference
快速参考
Load Full Reference below only when you need specific folder layouts or framework comparisons.
Key rules:
- ServerScriptService: server logic (never visible to client)
- ServerStorage: server-only assets/data
- ReplicatedStorage: shared modules, RemoteEvents, assets both sides need
- StarterPlayerScripts: client controllers (run once per player)
- StarterGui: ScreenGuis (cloned to PlayerGui on spawn)
- Script types: Script (server), LocalScript (client), ModuleScript (shared, returns one table)
- Communication: RemoteEvent (fire-and-forget), RemoteFunction (request-response, avoid for client→server)
- Module pattern: return a table of functions. One module = one responsibility.
- Avoid circular requires. Use events/signals for cross-module communication.
- Single entry point per side: one server Script requires service modules, one LocalScript requires controllers.
仅在需要特定文件夹布局或框架对比时,才查看下方完整参考内容。
核心规则:
- ServerScriptService:存放服务器逻辑(对客户端完全不可见)
- ServerStorage:存放服务器专属资源/数据
- ReplicatedStorage:存放共享模块、RemoteEvent、客户端和服务器都需要的资源
- StarterPlayerScripts:存放客户端控制器(每个玩家运行一次)
- StarterGui:存放ScreenGui(玩家生成时会克隆到PlayerGui)
- 脚本类型:Script(服务器端)、LocalScript(客户端)、ModuleScript(共享,返回一个表)
- 通信方式:RemoteEvent(触发即遗忘)、RemoteFunction(请求-响应,避免客户端→服务器方向使用)
- 模块模式:返回一个函数表。一个模块对应一个职责。
- 避免循环引用。使用事件/信号实现跨模块通信。
- 每端单一入口点:一个服务器Script加载服务模块,一个LocalScript加载控制器。
Full Reference
完整参考
2. Core Concepts
2. 核心概念
The Data Model
数据模型
Roblox games are built on a tree of Instances. Every object (parts, scripts, UI elements, sounds) is an Instance that lives somewhere in a hierarchy rooted at (class ). The hierarchy determines behavior: a placed in runs on the server, but the same placed in will not run at all (only and client-side run there).
gameDataModelScriptServerScriptServiceScriptStarterPlayerScriptsLocalScriptScriptRoblox游戏基于实例树构建。每个对象(部件、脚本、UI元素、音效)都是一个Instance,位于以(类)为根的层级结构中。层级结构决定行为:放在中的会在服务器运行,但放在中的同一个完全不会运行(只有和客户端侧能在此运行)。
gameDataModelServerScriptServiceScriptStarterPlayerScriptsScriptLocalScriptScriptClient vs. Server Execution
客户端 vs 服务器执行
| Aspect | Server | Client |
|---|---|---|
| Runs on | Roblox datacenter (one instance per game server) | Each player's device |
| Script types | | |
| Trust level | Authoritative -- owns game state, data stores, physics arbitration | Untrusted -- can be exploited; never trust client input blindly |
| Access | Can see everything in the data model | Cannot see |
| 维度 | 服务器 | 客户端 |
|---|---|---|
| 运行位置 | Roblox数据中心(每个游戏服务器一个实例) | 每个玩家的设备 |
| 脚本类型 | | |
| 信任级别 | 权威方——拥有游戏状态、数据存储、物理仲裁权 | 不可信——可能被利用;绝不盲目信任客户端输入 |
| 访问权限 | 可查看数据模型中的所有内容 | 无法查看 |
Replication Model
复制模型
Roblox automatically replicates (syncs) parts of the data model from server to clients:
- Server to all clients: Anything in ,
Workspace,ReplicatedStorage,ReplicatedFirst,Lighting,SoundService, andChatis visible to every connected client.Teams - Server-only (hidden from clients): and
ServerScriptServiceare never sent to clients. This is the correct place for server logic and secret assets.ServerStorage - Per-player: Each player gets their own (cloned from
PlayerGui),StarterGui(cloned fromPlayerScripts),StarterPlayerScripts(cloned fromStarterGear), andStarterPack.Backpack - Client to server: Clients can modify their own character and certain local objects, but those changes do not replicate to the server unless the server has granted network ownership of that instance.
Key rule: If the server creates or changes an instance in a replicated container, all clients see it. If a client creates something locally, only that client sees it (unless the server replicates it).
Roblox会自动将数据模型的部分内容从服务器复制(同步)到客户端:
- 服务器到所有客户端:、
Workspace、ReplicatedStorage、ReplicatedFirst、Lighting、SoundService和Chat中的内容对所有连接的客户端可见。Teams - 服务器专属(对客户端隐藏):和
ServerScriptService永远不会发送给客户端。这是存放服务器逻辑和保密资源的正确位置。ServerStorage - 每个玩家专属:每个玩家会获得自己的(从
PlayerGui克隆)、StarterGui(从PlayerScripts克隆)、StarterPlayerScripts(从StarterGear克隆)和StarterPack。Backpack - 客户端到服务器:客户端可以修改自己的角色和某些本地对象,但这些修改不会同步到服务器,除非服务器授予该实例的网络所有权。
核心规则:如果服务器在可复制容器中创建或修改实例,所有客户端都会看到。如果客户端在本地创建内容,只有该客户端能看到(除非服务器主动复制)。
3. Service Hierarchy
3. 服务层级
ServerScriptService
ServerScriptService
Purpose: Contains instances that run exclusively on the server. Clients cannot see or access anything inside.
ScriptUse for:
- Core game logic (round systems, match management, scoring)
- Data persistence (DataStoreService calls)
- Server-side validation of player actions
- Anti-cheat enforcement
- NPC/AI controllers
- Server-side modules required only by server scripts
ServerScriptService/
GameManager.server.lua -- Script: round system
DataManager.server.lua -- Script: save/load player data
Modules/
CombatService.lua -- ModuleScript: damage calculations
ShopService.lua -- ModuleScript: purchase validation用途:存放仅在服务器运行的实例。客户端无法查看或访问其中任何内容。
Script适用场景:
- 核心游戏逻辑(回合系统、匹配管理、计分)
- 数据持久化(调用DataStoreService)
- 玩家操作的服务器端验证
- 反作弊执行
- NPC/AI控制器
- 仅被服务器脚本引用的服务器端模块
ServerScriptService/
GameManager.server.lua -- Script:回合系统
DataManager.server.lua -- Script:玩家数据保存/加载
Modules/
CombatService.lua -- ModuleScript:伤害计算
ShopService.lua -- ModuleScript:购买验证ServerStorage
ServerStorage
Purpose: A server-only container for assets and modules that clients should never see or download.
Use for:
- Map models that get cloned into Workspace on demand
- Enemy/NPC models before spawning
- Server-only ModuleScripts (utility libraries, data schemas)
- Templates that should not exist on the client until needed
ServerStorage/
Maps/
DesertArena.rbxm
ForestArena.rbxm
Templates/
Loot/
CommonChest.rbxm
Modules/
DataSchema.lua用途:服务器专属的资源和模块容器,客户端永远无法查看或下载其中内容。
适用场景:
- 按需克隆到Workspace的地图模型
- 生成前的敌人/NPC模型
- 服务器专属ModuleScript(工具库、数据模式)
- 在需要前不应出现在客户端的模板
ServerStorage/
Maps/
DesertArena.rbxm
ForestArena.rbxm
Templates/
Loot/
CommonChest.rbxm
Modules/
DataSchema.luaReplicatedStorage
ReplicatedStorage
Purpose: Shared container visible to both server and client. Content is replicated to every client on join.
Use for:
- and
RemoteEventinstances (the communication bridge)RemoteFunction - Shared modules (utilities, constants, types, shared classes)
ModuleScript - Assets both sides reference (item models, particle effects, shared animations)
- Configuration values / game settings that both sides need
ReplicatedStorage/
Remotes/
DamageEvent.RemoteEvent
ShopPurchase.RemoteFunction
Modules/
ItemData.lua -- shared item definitions
MathUtils.lua -- shared utility functions
Types.lua -- shared type definitions
Assets/
Effects/
HitEffect.rbxm用途:客户端和服务器均可访问的共享容器。内容会在玩家加入时复制到每个客户端。
适用场景:
- 和
RemoteEvent实例(通信桥梁)RemoteFunction - 共享模块(工具类、常量、类型、共享类)
ModuleScript - 客户端和服务器都需要引用的资源(道具模型、粒子特效、共享动画)
- 客户端和服务器都需要的配置值/游戏设置
ReplicatedStorage/
Remotes/
DamageEvent.RemoteEvent
ShopPurchase.RemoteFunction
Modules/
ItemData.lua -- 共享道具定义
MathUtils.lua -- 共享工具函数
Types.lua -- 共享类型定义
Assets/
Effects/
HitEffect.rbxmReplicatedFirst
ReplicatedFirst
Purpose: Scripts here run on the client before anything else loads. Content replicates to clients first.
Use for:
- Loading screens (show UI while the game streams in)
- Early client initialization
- Keeping it minimal -- only what is needed before the game fully loads
ReplicatedFirst/
LoadingScreen.client.lua -- LocalScript: shows loading UI用途:此处的脚本会在其他所有内容加载前在客户端运行。内容会优先复制到客户端。
适用场景:
- 加载界面(游戏流式加载时显示UI)
- 客户端早期初始化
- 保持内容精简——仅放置游戏完全加载前必需的内容
ReplicatedFirst/
LoadingScreen.client.lua -- LocalScript:显示加载UIStarterGui
StarterGui
Purpose: UI template container. On each player spawn (or respawn, depending on ), the contents are cloned into that player's .
ResetOnSpawnPlayerGuiUse for:
- HUD elements (health bars, score displays, minimaps)
- Menu screens (settings, inventory, shop)
- UI-controlling LocalScripts that live inside ScreenGui
StarterGui/
HUD.ScreenGui
HealthBar.Frame
ScoreLabel.TextLabel
HUDController.client.lua -- LocalScript managing HUD updates
ShopMenu.ScreenGui
ShopController.client.luaNote: Setfor persistent UI that should not re-clone on character respawn.ScreenGui.ResetOnSpawn = false
用途:UI模板容器。在每个玩家生成(或重生,取决于设置)时,其中内容会被克隆到该玩家的中。
ResetOnSpawnPlayerGui适用场景:
- HUD元素(生命值条、分数显示、小地图)
- 菜单界面(设置、背包、商店)
- 控制UI的LocalScript(位于ScreenGui内部)
StarterGui/
HUD.ScreenGui
HealthBar.Frame
ScoreLabel.TextLabel
HUDController.client.lua -- LocalScript:管理HUD更新
ShopMenu.ScreenGui
ShopController.client.lua注意:对于角色重生时不应重新克隆的持久化UI,设置。ScreenGui.ResetOnSpawn = false
StarterPlayer / StarterPlayerScripts
StarterPlayer / StarterPlayerScripts
Purpose: instances here are cloned into each player's folder once on join. They persist across respawns.
LocalScriptPlayerScriptsUse for:
- Camera controllers
- Input handling systems
- Client-side game managers
- Music/ambient sound controllers
StarterPlayer/
StarterPlayerScripts/
CameraController.client.lua
InputManager.client.lua
ClientBootstrap.client.lua用途:此处的实例会在玩家加入时克隆到每个玩家的文件夹,且会在重生后保留。
LocalScriptPlayerScripts适用场景:
- 相机控制器
- 输入处理系统
- 客户端游戏管理器
- 音乐/环境音效控制器
StarterPlayer/
StarterPlayerScripts/
CameraController.client.lua
InputManager.client.lua
ClientBootstrap.client.luaStarterPlayer / StarterCharacterScripts
StarterPlayer / StarterCharacterScripts
Purpose: Scripts here are cloned into the player's model each time the character spawns. They are destroyed when the character dies.
CharacterUse for:
- Per-character behaviors (footstep sounds, animation controllers)
- Character-specific effects (trails, auras)
- Anything that should reset on death
StarterPlayer/
StarterCharacterScripts/
FootstepSounds.client.lua
AnimationController.client.lua用途:此处的脚本会在玩家角色每次生成时克隆到角色模型中,角色死亡时会被销毁。
适用场景:
- 角色专属行为(脚步声、动画控制器)
- 角色专属特效(轨迹、光环)
- 任何需要在死亡后重置的内容
StarterPlayer/
StarterCharacterScripts/
FootstepSounds.client.lua
AnimationController.client.luaStarterPack
StarterPack
Purpose: instances here are cloned into each player's on spawn.
ToolBackpackUse for:
- Default weapons or items every player starts with
- Tools with embedded LocalScripts and Scripts
StarterPack/
Sword.Tool
SwordClient.client.lua
SwordServer.server.lua
Handle.Part用途:此处的实例会在玩家生成时克隆到每个玩家的中。
ToolBackpack适用场景:
- 所有玩家初始拥有的默认武器或道具
- 嵌入LocalScript和Script的工具
StarterPack/
Sword.Tool
SwordClient.client.lua
SwordServer.server.lua
Handle.PartWorkspace
Workspace
Purpose: The 3D world. Everything visible in the game exists here at runtime: parts, models, terrain, cameras.
Use for:
- The physical game world (terrain, buildings, decorations)
- Spawned entities at runtime (cloned from ServerStorage)
- The Camera (each client has a local )
Workspace.CurrentCamera
Do NOT place Scripts directly in Workspace. Use instead. Workspace scripts are accessible to exploiters and create organizational chaos.
ServerScriptService用途:3D游戏世界。运行时游戏中可见的所有内容都位于此处:部件、模型、地形、相机。
适用场景:
- 物理游戏世界(地形、建筑、装饰)
- 运行时生成的实体(从ServerStorage克隆)
- 相机(每个客户端有一个本地)
Workspace.CurrentCamera
请勿直接在Workspace中放置Script。请改用。Workspace中的脚本可被 exploiters 获取,还会造成管理混乱。
ServerScriptService4. Script Types
4. 脚本类型
Script (Server Script)
Script(服务器脚本)
- Runs on: Server (default RunContext)
- Valid containers: ,
ServerScriptService(when parented under certain conditions),ServerStorage(discouraged)Workspace - File convention: in Rojo projects
*.server.lua - Access: Full access to server APIs (,
DataStoreService,MessagingService, etc.)HttpService
lua
-- ServerScriptService/GameManager.server.lua
local Players = game:GetService("Players")
Players.PlayerAdded:Connect(function(player)
print(`{player.Name} joined the server`)
end)- 运行位置:服务器(默认RunContext)
- 有效容器:、
ServerScriptService(特定条件下作为父容器时)、ServerStorage(不推荐)Workspace - 文件命名约定:Rojo项目中使用
*.server.lua - 访问权限:完全访问服务器API(、
DataStoreService、MessagingService等)HttpService
lua
-- ServerScriptService/GameManager.server.lua
local Players = game:GetService("Players")
Players.PlayerAdded:Connect(function(player)
print(`{player.Name} joined the server`)
end)RunContext (Script Behavior Override)
RunContext(脚本行为覆盖)
Script.RunContext| Value | Behavior | Notes |
|---|---|---|
| Script runs only in traditional server containers (ServerScriptService, ServerStorage) | Default when not set |
| Script runs anywhere in the place, on the server | Useful for scripts in ReplicatedStorage or Workspace |
| Script runs anywhere in the place, on the client | Place in Workspace for local effects, or ReplicatedStorage for shared UI logic |
LegacyScriptLocalScriptServerClientRunContext = Clientlua
-- A Script in Workspace with RunContext = Client
-- Runs on every client, perfect for local visual effects
local part = script.Parent
local RunService = game:GetService("RunService")
RunService.Heartbeat:Connect(function(dt)
part.CFrame *= CFrame.Angles(0, math.rad(30 * dt), 0)
end)RunContext = Serverlua
-- A Script in ReplicatedStorage with RunContext = Server
-- Server logic lives alongside shared modules
local SharedConfig = require(script.Parent.SharedConfig)
print(`Server running with: {SharedConfig.setting}`)Note:does NOT have RunContext. It always runs on the client in client-valid containers only. UseLocalScript+Scriptwhen you need client scripts outside the usual client containers.RunContext = Client
Script.RunContext| 值 | 行为 | 说明 |
|---|---|---|
| 脚本仅在传统服务器容器(ServerScriptService、ServerStorage)中运行 | 未设置时的默认值 |
| 脚本可在场景中的任何位置运行,且在服务器端执行 | 适用于ReplicatedStorage或Workspace中的脚本 |
| 脚本可在场景中的任何位置运行,且在客户端执行 | 可放在Workspace中实现本地特效,或放在ReplicatedStorage中实现共享UI逻辑 |
LegacyScriptLocalScriptServerClientRunContext = Clientlua
-- Workspace中的Script,RunContext = Client
-- 在每个客户端运行,非常适合本地视觉特效
local part = script.Parent
local RunService = game:GetService("RunService")
RunService.Heartbeat:Connect(function(dt)
part.CFrame *= CFrame.Angles(0, math.rad(30 * dt), 0)
end)RunContext = Serverlua
-- ReplicatedStorage中的Script,RunContext = Server
-- 服务器逻辑与共享模块放在一起
local SharedConfig = require(script.Parent.SharedConfig)
print(`Server running with: {SharedConfig.setting}`)注意:没有RunContext属性。它始终在客户端的有效容器中运行。当需要在常规客户端容器之外放置客户端脚本时,请使用LocalScript+Script。RunContext = Client
LocalScript (Client Script)
LocalScript(客户端脚本)
- Runs on: Client (the specific player's device)
- Valid containers: ,
StarterPlayerScripts,StarterCharacterScripts,StarterGui, a player'sStarterPack,Backpack,PlayerGui, orPlayerScriptsCharacter - File convention: in Rojo projects
*.client.lua - Access: Client APIs (,
UserInputService,ContextActionService, local player's GUI)Camera
lua
-- StarterPlayerScripts/InputManager.client.lua
local UserInputService = game:GetService("UserInputService")
UserInputService.InputBegan:Connect(function(input, gameProcessed)
if gameProcessed then return end
if input.KeyCode == Enum.KeyCode.E then
print("Player pressed E")
end
end)- 运行位置:客户端(特定玩家的设备)
- 有效容器:、
StarterPlayerScripts、StarterCharacterScripts、StarterGui、玩家的StarterPack、Backpack、PlayerGui或PlayerScriptsCharacter - 文件命名约定:Rojo项目中使用
*.client.lua - 访问权限:客户端API(、
UserInputService、ContextActionService、本地玩家的GUI)Camera
lua
-- StarterPlayerScripts/InputManager.client.lua
local UserInputService = game:GetService("UserInputService")
UserInputService.InputBegan:Connect(function(input, gameProcessed)
if gameProcessed then return end
if input.KeyCode == Enum.KeyCode.E then
print("Player pressed E")
end
end)ModuleScript
ModuleScript
- Runs on: Whichever side s it (server if required by a Script, client if required by a LocalScript)
require() - Valid containers: Anywhere, but location determines who can access it
- or
ServerScriptService-- server-only modulesServerStorage - -- shared modules (accessible by both sides)
ReplicatedStorage - -- client-only modules
StarterPlayerScripts
- File convention: (no
*.luaor.serversuffix) in Rojo projects.client - Returns: Exactly one value (typically a table/dictionary acting as a module)
lua
-- ReplicatedStorage/Modules/MathUtils.lua
local MathUtils = {}
function MathUtils.lerp(a: number, b: number, t: number): number
return a + (b - a) * t
end
function MathUtils.clamp(value: number, min: number, max: number): number
return math.max(min, math.min(max, value))
end
return MathUtilsRequiring:
lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local MathUtils = require(ReplicatedStorage.Modules.MathUtils)
local result = MathUtils.lerp(0, 100, 0.5) -- 50- 运行位置:取决于调用的一侧(被Script引用则在服务器运行,被LocalScript引用则在客户端运行)
require() - 有效容器:任何位置,但存放位置决定谁能访问
- 或
ServerScriptService——服务器专属模块ServerStorage - ——共享模块(客户端和服务器均可访问)
ReplicatedStorage - ——客户端专属模块
StarterPlayerScripts
- 文件命名约定:Rojo项目中使用(无
*.lua或.server后缀).client - 返回值:恰好一个值(通常是作为模块的表/字典)
lua
-- ReplicatedStorage/Modules/MathUtils.lua
local MathUtils = {}
function MathUtils.lerp(a: number, b: number, t: number): number
return a + (b - a) * t
end
function MathUtils.clamp(value: number, min: number, max: number): number
return math.max(min, math.min(max, value))
end
return MathUtils引用方式:
lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local MathUtils = require(ReplicatedStorage.Modules.MathUtils)
local result = MathUtils.lerp(0, 100, 0.5) -- 505. Client-Server Communication
5. 客户端-服务器通信
For implementation details (RemoteEvent, RemoteFunction, UnreliableRemoteEvent, BindableEvent, validation patterns), see roblox-networking → Client-Server Communication.
Conceptual overview:
| Type | Direction | Blocking? | Use Case |
|---|---|---|---|
| Client ↔ Server | No | Actions, notifications, state changes |
| Client → Server only (safe) | Yes (yields) | Data requests, queries |
| Same side | N/A (local) | Decoupled same-side messaging |
| Client ↔ Server | No | High-frequency cosmetic data |
Core rules:
- Server is authoritative. Never trust client input.
- Use for fire-and-forget. Use
RemoteEventonly when the caller needs a return value.RemoteFunction - Never use - if the client errors or disconnects, the server thread hangs forever.
RemoteFunction:InvokeClient() - for cosmetic data only (cursor position, facing direction). Never for damage, purchases, or state.
UnreliableRemoteEvent
关于实现细节(RemoteEvent、RemoteFunction、UnreliableRemoteEvent、BindableEvent、验证模式),请查看roblox-networking → 客户端-服务器通信。
概念概述:
| 类型 | 方向 | 是否阻塞 | 适用场景 |
|---|---|---|---|
| 客户端 ↔ 服务器 | 否 | 操作、通知、状态变更 |
| 仅客户端→服务器(安全) | 是(会暂停线程) | 数据请求、查询 |
| 同一侧 | 不适用(本地) | 解耦的同侧消息传递 |
| 客户端 ↔ 服务器 | 否 | 高频 cosmetic 数据 |
核心规则:
- 服务器是权威方。绝不信任客户端输入。
- 使用处理触发即遗忘的操作。仅当调用方需要返回值时才使用
RemoteEvent。RemoteFunction - 绝不要使用——如果客户端出错或断开连接,服务器线程会永久挂起。
RemoteFunction:InvokeClient() - 仅用于cosmetic数据(光标位置、朝向)。绝不要用于伤害、购买或状态变更。
UnreliableRemoteEvent
6. Module Script Architecture
6. 模块脚本架构
Basic Module Pattern
基础模块模式
Every ModuleScript returns a single table. Functions and data are fields on that table.
lua
-- ReplicatedStorage/Modules/InventoryModule.lua
local InventoryModule = {}
local playerInventories: { [Player]: { [string]: number } } = {}
function InventoryModule.init()
-- Called once during startup to wire up connections
game:GetService("Players").PlayerRemoving:Connect(function(player)
playerInventories[player] = nil
end)
end
function InventoryModule.addItem(player: Player, itemId: string, quantity: number)
local inv = playerInventories[player]
if not inv then
inv = {}
playerInventories[player] = inv
end
inv[itemId] = (inv[itemId] or 0) + quantity
end
function InventoryModule.getItem(player: Player, itemId: string): number
local inv = playerInventories[player]
if not inv then return 0 end
return inv[itemId] or 0
end
function InventoryModule.getAll(player: Player): { [string]: number }
return playerInventories[player] or {}
end
return InventoryModule每个ModuleScript返回一个单独的表。函数和数据作为该表的字段存在。
lua
-- ReplicatedStorage/Modules/InventoryModule.lua
local InventoryModule = {}
local playerInventories: { [Player]: { [string]: number } } = {}
function InventoryModule.init()
-- 启动时调用一次,用于连接事件
game:GetService("Players").PlayerRemoving:Connect(function(player)
playerInventories[player] = nil
end)
end
function InventoryModule.addItem(player: Player, itemId: string, quantity: number)
local inv = playerInventories[player]
if not inv then
inv = {}
playerInventories[player] = inv
end
inv[itemId] = (inv[itemId] or 0) + quantity
end
function InventoryModule.getItem(player: Player, itemId: string): number
local inv = playerInventories[player]
if not inv then return 0 end
return inv[itemId] or 0
end
function InventoryModule.getAll(player: Player): { [string]: number }
return playerInventories[player] or {}
end
return InventoryModuleThe require()
Pattern
require()require()
模式
require()lua
-- ServerScriptService/Main.server.lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerStorage = game:GetService("ServerStorage")
-- Shared modules (both sides can require these)
local ItemData = require(ReplicatedStorage.Modules.ItemData)
-- Server-only modules
local InventoryModule = require(ServerStorage.Modules.InventoryModule)
-- Initialize modules that need setup
InventoryModule.init()Key facts about :
require()- The module runs once. Subsequent calls return the cached result.
require() - The module runs in the context of the first requirer (server or client).
- If a server Script requires a module in , it runs on the server. If a LocalScript requires the same module, a separate client-side copy runs.
ReplicatedStorage
lua
-- ServerScriptService/Main.server.lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerStorage = game:GetService("ServerStorage")
-- 共享模块(两侧均可引用)
local ItemData = require(ReplicatedStorage.Modules.ItemData)
-- 服务器专属模块
local InventoryModule = require(ServerStorage.Modules.InventoryModule)
-- 初始化需要设置的模块
InventoryModule.init()关于的核心要点:
require()- 模块仅运行一次。后续调用返回缓存结果。
require() - 模块在第一个调用方的上下文中运行(服务器或客户端)。
- 如果服务器Script引用ReplicatedStorage中的模块,模块在服务器运行。如果LocalScript引用同一个模块,会运行一个独立的客户端副本。
Avoiding Circular Dependencies
避免循环依赖
Circular dependencies occur when Module A requires Module B, and Module B requires Module A. This causes an infinite loop or returns .
nilProblem:
lua
-- ModuleA.lua
local ModuleB = require(script.Parent.ModuleB) -- ModuleB hasn't finished loading
-- ...
return ModuleA
-- ModuleB.lua
local ModuleA = require(script.Parent.ModuleA) -- ModuleA hasn't finished loadingSolution 1: Deferred initialization with pattern
init()lua
-- ModuleA.lua
local ModuleA = {}
local ModuleB -- forward declaration
function ModuleA.init(modules)
ModuleB = modules.ModuleB
end
function ModuleA.doSomething()
ModuleB.helper()
end
return ModuleAlua
-- Main.server.lua (the bootstrapper)
local ModuleA = require(path.to.ModuleA)
local ModuleB = require(path.to.ModuleB)
-- Wire up cross-references after all modules are loaded
local modules = { ModuleA = ModuleA, ModuleB = ModuleB }
ModuleA.init(modules)
ModuleB.init(modules)
-- Now start the game
ModuleA.start()Solution 2: Dependency inversion -- extract shared logic into a third module
Instead of A and B depending on each other, extract the shared concern into module C that both depend on. Dependencies flow in one direction.
Solution 3: Event-driven decoupling with BindableEvents
Instead of calling directly into another module, fire a BindableEvent that the other module listens to. Neither module requires the other.
当模块A引用模块B,同时模块B引用模块A时,会出现循环依赖。这会导致无限循环或返回。
nil问题示例:
lua
-- ModuleA.lua
local ModuleB = require(script.Parent.ModuleB) -- ModuleB尚未加载完成
-- ...
return ModuleA
-- ModuleB.lua
local ModuleA = require(script.Parent.ModuleA) -- ModuleA尚未加载完成解决方案1:使用模式延迟初始化
init()lua
-- ModuleA.lua
local ModuleA = {}
local ModuleB -- 前置声明
function ModuleA.init(modules)
ModuleB = modules.ModuleB
end
function ModuleA.doSomething()
ModuleB.helper()
end
return ModuleAlua
-- Main.server.lua(启动器)
local ModuleA = require(path.to.ModuleA)
local ModuleB = require(path.to.ModuleB)
-- 所有模块加载完成后建立交叉引用
local modules = { ModuleA = ModuleA, ModuleB = ModuleB }
ModuleA.init(modules)
ModuleB.init(modules)
-- 现在启动游戏
ModuleA.start()解决方案2:依赖反转——将共享逻辑提取到第三个模块
不再让A和B互相依赖,而是将共享关注点提取到模块C,让A和B都依赖C。依赖关系单向流动。
解决方案3:使用BindableEvent实现事件驱动解耦
不要直接调用另一个模块的方法,而是触发一个BindableEvent,让另一个模块监听该事件。两个模块都无需互相引用。
OOP Module Pattern (Class-based)
OOP模块模式(基于类)
For metatable-based classes, type annotations, inheritance, and the vs conventions, see roblox-luau-mastery → OOP Patterns.
.:The architecture-specific pattern: modules that return a class table. The class is defined in a ModuleScript, required by whoever needs it, and instances are created via .
ClassName.new()lua
-- ReplicatedStorage/Modules/Weapon.lua
local Weapon = {}
Weapon.__index = Weapon
export type Weapon = typeof(setmetatable({} :: {
name: string,
damage: number,
cooldown: number,
_lastFired: number,
}, Weapon))
function Weapon.new(name: string, damage: number, cooldown: number): Weapon
local self = setmetatable({}, Weapon)
self.name = name
self.damage = damage
self.cooldown = cooldown
self._lastFired = 0
return self
end
function Weapon:canFire(): boolean
return (os.clock() - self._lastFired) >= self.cooldown
end
function Weapon:fire(): number?
if not self:canFire() then return nil end
self._lastFired = os.clock()
return self.damage
end
return Weaponlua
-- Usage
local Weapon = require(ReplicatedStorage.Modules.Weapon)
local sword = Weapon.new("Iron Sword", 25, 0.8)
if sword:canFire() then
local dmg = sword:fire()
end关于元表类、类型注解、继承以及和的约定,请查看roblox-luau-mastery → OOP模式。
.:架构相关模式:返回类表的模块。类在ModuleScript中定义,被需要的方引用,通过创建实例。
ClassName.new()lua
-- ReplicatedStorage/Modules/Weapon.lua
local Weapon = {}
Weapon.__index = Weapon
export type Weapon = typeof(setmetatable({} :: {
name: string,
damage: number,
cooldown: number,
_lastFired: number,
}, Weapon))
function Weapon.new(name: string, damage: number, cooldown: number): Weapon
local self = setmetatable({}, Weapon)
self.name = name
self.damage = damage
self.cooldown = cooldown
self._lastFired = 0
return self
end
function Weapon:canFire(): boolean
return (os.clock() - self._lastFired) >= self.cooldown
end
function Weapon:fire(): number?
if not self:canFire() then return nil end
self._lastFired = os.clock()
return self.damage
end
return Weapon使用示例:
lua
local Weapon = require(ReplicatedStorage.Modules.Weapon)
local sword = Weapon.new("Iron Sword", 25, 0.8)
if sword:canFire() then
local dmg = sword:fire()
end7. Framework Patterns
7. 框架模式
Single Script Architecture (SSA)
单脚本架构(SSA)
One Script on the server, one LocalScript on the client. Each requires a "loader" module that initializes all other modules in order.
lua
-- ServerScriptService/Main.server.lua
require(game:GetService("ServerStorage").Loader)lua
-- ServerStorage/Loader.lua
local Loader = {}
local modules = {
require(script.Parent.Modules.DataService),
require(script.Parent.Modules.CombatService),
require(script.Parent.Modules.ShopService),
}
-- Init phase (no cross-dependencies yet)
for _, mod in modules do
if mod.init then mod:init() end
end
-- Start phase (all modules available)
for _, mod in modules do
if mod.start then mod:start() end
end
return Loader服务器端一个Script,客户端一个LocalScript。每个脚本引用一个“加载器”模块,按顺序初始化所有其他模块。
lua
-- ServerScriptService/Main.server.lua
require(game:GetService("ServerStorage").Loader)lua
-- ServerStorage/Loader.lua
local Loader = {}
local modules = {
require(script.Parent.Modules.DataService),
require(script.Parent.Modules.CombatService),
require(script.Parent.Modules.ShopService),
}
-- 初始化阶段(尚无交叉依赖)
for _, mod in modules do
if mod.init then mod:init() end
end
-- 启动阶段(所有模块已就绪)
for _, mod in modules do
if mod.start then mod:start() end
end
return LoaderComparison
对比
| Aspect | No Framework (Manual) | Single Script Architecture |
|---|---|---|
| Complexity | Low | Medium |
| Boilerplate | Minimal | Low |
| Networking | Manual RemoteEvent setup | Manual |
| Learning curve | Just Roblox APIs | Small pattern to learn |
| Scalability | Degrades without discipline | Good with init/start pattern |
| Dependency management | Manual require chains | Centralized loader |
| Best for | Jams, small projects, learning | Medium projects, teams wanting control |
| 维度 | 无框架(手动) | 单脚本架构 |
|---|---|---|
| 复杂度 | 低 | 中 |
| 样板代码 | 极少 | 少 |
| 网络通信 | 手动设置RemoteEvent | 手动设置 |
| 学习曲线 | 只需掌握Roblox API | 只需学习一个小模式 |
| 可扩展性 | 缺乏规范时会下降 | 使用init/start模式时表现良好 |
| 依赖管理 | 手动引用链 | 集中式加载器 |
| 最佳适用场景 | 游戏 jam、小型项目、学习阶段 | 中型项目、希望自主控制的团队 |
8. Folder Organization
8. 文件夹组织
Small Game (Simple / Flat)
小型游戏(简单/扁平结构)
For game jams, prototypes, or games with under ~10 scripts.
game
+-- ServerScriptService
|| +-- GameManager.server.lua
|| +-- DataHandler.server.lua
+-- ServerStorage
|| +-- Maps/
+-- ReplicatedStorage
|| +-- Remotes/
|| | +-- DamageEvent (RemoteEvent)
|| | +-- ShopPurchase (RemoteFunction)
|| +-- SharedConfig.lua (ModuleScript)
+-- ReplicatedFirst
|| +-- LoadingScreen.client.lua
+-- StarterGui
|| +-- HUD (ScreenGui)
+-- StarterPlayer
|| +-- StarterPlayerScripts
|| +-- CameraController.client.lua
+-- Workspace
+-- Map/
+-- SpawnLocations/适用于游戏 jam、原型或脚本数量少于10个的游戏。
game
+-- ServerScriptService
|| +-- GameManager.server.lua
|| +-- DataHandler.server.lua
+-- ServerStorage
|| +-- Maps/
+-- ReplicatedStorage
|| +-- Remotes/
|| | +-- DamageEvent (RemoteEvent)
|| | +-- ShopPurchase (RemoteFunction)
|| +-- SharedConfig.lua (ModuleScript)
+-- ReplicatedFirst
|| +-- LoadingScreen.client.lua
+-- StarterGui
|| +-- HUD (ScreenGui)
+-- StarterPlayer
|| +-- StarterPlayerScripts
|| +-- CameraController.client.lua
+-- Workspace
+-- Map/
+-- SpawnLocations/Medium Game (Modular)
中型游戏(模块化结构)
Organized into feature folders. Each feature has its own server module, client module, and shared definitions.
game
+-- ServerScriptService
|| +-- Main.server.lua -- Bootstrapper: requires and inits all modules
|| +-- Services/
|| | +-- CombatService.lua
|| | +-- DataService.lua
|| | +-- ShopService.lua
|| | +-- RoundService.lua
|| +-- Components/
|| +-- LootDrop.lua
|| +-- DoorSystem.lua
+-- ServerStorage
|| +-- Assets/
|| | +-- Maps/
|| | +-- NPCs/
|| +-- Modules/
|| +-- DataSchema.lua
+-- ReplicatedStorage
|| +-- Remotes/ -- All RemoteEvents/Functions here
|| +-- Modules/
|| | +-- ItemData.lua
|| | +-- Constants.lua
|| | +-- Types.lua
|| | +-- MathUtils.lua
|| +-- Assets/
|| +-- Effects/
|| +-- Animations/
+-- ReplicatedFirst
|| +-- LoadingScreen.client.lua
+-- StarterGui
|| +-- HUD (ScreenGui)
|| +-- ShopMenu (ScreenGui)
|| +-- SettingsMenu (ScreenGui)
+-- StarterPlayer
|| +-- StarterPlayerScripts
|| +-- ClientMain.client.lua -- Client bootstrapper
|| +-- Controllers/
|| +-- CameraController.lua
|| +-- InputController.lua
|| +-- UIController.lua
+-- Workspace
+-- World/
+-- Lighting/按功能文件夹组织。每个功能有自己的服务器模块、客户端模块和共享定义。
game
+-- ServerScriptService
|| +-- Main.server.lua -- 启动器:引用并初始化所有模块
|| +-- Services/
|| | +-- CombatService.lua
|| | +-- DataService.lua
|| | +-- ShopService.lua
|| | +-- RoundService.lua
|| +-- Components/
|| +-- LootDrop.lua
|| +-- DoorSystem.lua
+-- ServerStorage
|| +-- Assets/
|| | +-- Maps/
|| | +-- NPCs/
|| +-- Modules/
|| +-- DataSchema.lua
+-- ReplicatedStorage
|| +-- Remotes/ -- 所有RemoteEvent/Function存放于此
|| +-- Modules/
|| | +-- ItemData.lua
|| | +-- Constants.lua
|| | +-- Types.lua
|| | +-- MathUtils.lua
|| +-- Assets/
|| +-- Effects/
|| +-- Animations/
+-- ReplicatedFirst
|| +-- LoadingScreen.client.lua
+-- StarterGui
|| +-- HUD (ScreenGui)
|| +-- ShopMenu (ScreenGui)
|| +-- SettingsMenu (ScreenGui)
+-- StarterPlayer
|| +-- StarterPlayerScripts
|| +-- ClientMain.client.lua -- 客户端启动器
|| +-- Controllers/
|| +-- CameraController.lua
|| +-- InputController.lua
|| +-- UIController.lua
+-- Workspace
+-- World/
+-- Lighting/Large Game (Framework-Based with Rojo)
大型游戏(基于框架+Rojo)
Uses a loader pattern (SSA), Rojo for file sync, and a strict folder convention. File system mirrors the Roblox hierarchy.
src/
+-- server/ --> syncs to ServerScriptService
|| +-- Main.server.lua -- Bootstrapper
|| +-- services/
|| | +-- PlayerDataService.lua
|| | +-- CombatService.lua
|| | +-- EconomyService.lua
|| | +-- MatchService.lua
|| | +-- LeaderboardService.lua
|| +-- components/
|| +-- Destructible.lua
|| +-- Interactable.lua
+-- client/ --> syncs to StarterPlayerScripts
|| +-- ClientMain.client.lua -- Client bootstrapper
|| +-- controllers/
|| | +-- InputController.lua
|| | +-- CameraController.lua
|| | +-- UIController.lua
|| | +-- SoundController.lua
|| +-- ui/
|| +-- components/
|| | +-- Button.lua
|| | +-- HealthBar.lua
|| +-- screens/
|| +-- ShopScreen.lua
|| +-- InventoryScreen.lua
+-- shared/ --> syncs to ReplicatedStorage
|| +-- modules/
|| | +-- ItemData.lua
|| | +-- Constants.lua
|| | +-- Types.lua
|| | +-- Util.lua
|| +-- network/
|| | +-- Remotes.lua -- Central remote definitions
|| +-- assets/
+-- serverStorage/ --> syncs to ServerStorage
|| +-- assets/
|| | +-- maps/
|| | +-- npcs/
|| +-- modules/
|| +-- DataSchema.lua
+-- starterGui/ --> syncs to StarterGui
|| +-- HUD.gui.lua
+-- first/ --> syncs to ReplicatedFirst
+-- Loading.client.lua使用加载器模式(SSA)、Rojo进行文件同步,并遵循严格的文件夹约定。文件系统镜像Roblox层级结构。
src/
+-- server/ --> 同步到ServerScriptService
|| +-- Main.server.lua -- 启动器
|| +-- services/
|| | +-- PlayerDataService.lua
|| | +-- CombatService.lua
|| | +-- EconomyService.lua
|| | +-- MatchService.lua
|| | +-- LeaderboardService.lua
|| +-- components/
|| +-- Destructible.lua
|| +-- Interactable.lua
+-- client/ --> 同步到StarterPlayerScripts
|| +-- ClientMain.client.lua -- 客户端启动器
|| +-- controllers/
|| | +-- InputController.lua
|| | +-- CameraController.lua
|| | +-- UIController.lua
|| | +-- SoundController.lua
|| +-- ui/
|| +-- components/
|| | +-- Button.lua
|| | +-- HealthBar.lua
|| +-- screens/
|| +-- ShopScreen.lua
|| +-- InventoryScreen.lua
+-- shared/ --> 同步到ReplicatedStorage
|| +-- modules/
|| | +-- ItemData.lua
|| | +-- Constants.lua
|| | +-- Types.lua
|| | +-- Util.lua
|| +-- network/
|| | +-- Remotes.lua -- 集中式远程定义
|| +-- assets/
+-- serverStorage/ --> 同步到ServerStorage
|| +-- assets/
|| | +-- maps/
|| | +-- npcs/
|| +-- modules/
|| +-- DataSchema.lua
+-- starterGui/ --> 同步到StarterGui
|| +-- HUD.gui.lua
+-- first/ --> 同步到ReplicatedFirst
+-- Loading.client.lua9. Best Practices
9. 最佳实践
Separation of Concerns
关注点分离
Each script or module should have a single, clear responsibility. A handles damage and health. A handles saving and loading. They communicate via well-defined interfaces, not by reaching into each other's internals.
CombatServiceDataService每个脚本或模块应具有单一、明确的职责。处理伤害和生命值,处理保存和加载。它们通过定义清晰的接口通信,而非直接访问彼此的内部状态。
CombatServiceDataServiceSingle Responsibility
单一职责
One module = one job. If a module handles inventory AND crafting AND trading, split it into , , and .
InventoryModuleCraftingModuleTradingModule一个模块对应一个任务。如果一个模块同时处理背包、 crafting 和交易,应拆分为、和。
InventoryModuleCraftingModuleTradingModuleMinimal Coupling
最小耦合
Modules should depend on abstractions (function calls, events), not on internal state of other modules. If module A needs data from module B, A calls rather than reading directly.
B.getData()B._internalTable模块应依赖抽象(函数调用、事件),而非其他模块的内部状态。如果模块A需要模块B的数据,A应调用,而非直接读取。
B.getData()B._internalTableServer Owns State
服务器拥有状态
The server is the single source of truth for all game state. Clients render and predict, but the server validates and authorizes.
服务器是所有游戏状态的单一可信源。客户端负责渲染和预测,但服务器负责验证和授权。
Validate All Client Input
验证所有客户端输入
For implementation details (type checking, range checking, ownership, rate limiting), see roblox-networking → Client Validation.
Core rules:
- Every handler must validate types, ranges, and ownership before processing.
OnServerEvent - Never let the client set currency, health, or any authoritative value directly.
- Client sends intent ("I attacked target X"), server calculates outcome.
关于实现细节(类型检查、范围检查、所有权、速率限制),请查看roblox-networking → 客户端验证。
核心规则:
- 每个处理程序在处理前必须验证类型、范围和所有权。
OnServerEvent - 绝不要让客户端直接设置货币、生命值或任何权威值。
- 客户端发送意图(「我攻击了目标X」),服务器计算结果。
Use ModuleScripts Everywhere
尽可能使用ModuleScript
Avoid putting game logic directly in Scripts or LocalScripts. Instead, keep Scripts/LocalScripts as thin bootstrappers that require and initialize ModuleScripts. This makes code reusable, testable, and organized.
避免将游戏逻辑直接放在Script或LocalScript中。相反,让Script/LocalScript作为薄启动器,引用并初始化ModuleScript。这会让代码更具复用性、可测试性和组织性。
Centralize Remote Definitions
集中式远程定义
Create all RemoteEvents and RemoteFunctions in one place rather than scattering across multiple scripts.
Instance.new("RemoteEvent")lua
-- ServerScriptService/CreateRemotes.server.lua (runs first)
local remoteFolder = Instance.new("Folder")
remoteFolder.Name = "Remotes"
remoteFolder.Parent = game:GetService("ReplicatedStorage")
local remotes = {
"DamageEvent",
"ShopPurchase",
"ChatMessage",
"PlayerReady",
}
for _, name in remotes do
local remote = Instance.new("RemoteEvent")
remote.Name = name
remote.Parent = remoteFolder
end在一个位置创建所有RemoteEvent和RemoteFunction,而非在多个脚本中分散使用。
Instance.new("RemoteEvent")lua
-- ServerScriptService/CreateRemotes.server.lua(优先运行)
local remoteFolder = Instance.new("Folder")
remoteFolder.Name = "Remotes"
remoteFolder.Parent = game:GetService("ReplicatedStorage")
local remotes = {
"DamageEvent",
"ShopPurchase",
"ChatMessage",
"PlayerReady",
}
for _, name in remotes do
local remote = Instance.new("RemoteEvent")
remote.Name = name
remote.Parent = remoteFolder
endUse Types and Constants
使用类型和常量
Define shared constants and Luau types in so both sides use the same definitions.
ReplicatedStoragelua
-- ReplicatedStorage/Modules/Constants.lua
local Constants = {
MAX_HEALTH = 100,
WALK_SPEED = 16,
SPRINT_SPEED = 24,
MAX_INVENTORY_SLOTS = 20,
INTERACTION_RANGE = 10,
ItemRarity = {
Common = 1,
Uncommon = 2,
Rare = 3,
Epic = 4,
Legendary = 5,
},
}
return Constants在中定义共享常量和Luau类型,让客户端和服务器使用相同的定义。
ReplicatedStoragelua
-- ReplicatedStorage/Modules/Constants.lua
local Constants = {
MAX_HEALTH = 100,
WALK_SPEED = 16,
SPRINT_SPEED = 24,
MAX_INVENTORY_SLOTS = 20,
INTERACTION_RANGE = 10,
ItemRarity = {
Common = 1,
Uncommon = 2,
Rare = 3,
Epic = 4,
Legendary = 5,
},
}
return Constants10. Anti-Patterns
10. 反模式
God Scripts
上帝脚本
Problem: One massive script (500+ lines) that handles spawning, combat, data, UI, and everything else. Impossible to debug, modify, or collaborate on.
Fix: Break into ModuleScripts with clear responsibilities. The main script becomes a thin bootstrapper.
问题:一个庞大的脚本(500+行)处理生成、战斗、数据、UI等所有内容。调试、修改和协作都极为困难。
修复方案:拆分为职责明确的ModuleScript。主脚本变为薄启动器。
Circular Requires
循环引用
Problem: Module A requires Module B, which requires Module A. Causes one of them to receive an incomplete (empty table) reference.
Fix: Use the pattern (Section 6), dependency inversion, or event-driven decoupling.
init()问题:模块A引用模块B,模块B引用模块A。导致其中一个模块收到不完整(空表)的引用。
修复方案:使用模式(第6节)、依赖反转或事件驱动解耦。
init()Server Logic in ReplicatedStorage
服务器逻辑放在ReplicatedStorage
Problem: Placing server-only game logic modules in . Exploiters can read the source code, understand validation logic, and craft exploits.
ReplicatedStorageFix: Keep server logic in or . Only put genuinely shared code (types, constants, utilities) in .
ServerScriptServiceServerStorageReplicatedStorage问题:将服务器专属游戏逻辑模块放在中。Exploiters可以读取源代码,了解验证逻辑,进而制作作弊工具。
ReplicatedStorage修复方案:将服务器逻辑放在或中。仅将真正的共享代码(类型、常量、工具类)放在中。
ServerScriptServiceServerStorageReplicatedStorageScripts in Workspace
Script放在Workspace
Problem: Placing Scripts directly as children of Workspace or models in Workspace. These are visible to clients (exploiters can read them), are hard to find during development, and create organizational debt.
Fix: Put all server scripts in . If you need a script to reference a specific model, have the script in and use a path reference or CollectionService tag to find the model.
ServerScriptServiceServerScriptService问题:将Script直接作为Workspace或Workspace中模型的子对象。这些脚本对客户端可见(Exploiters可以读取),开发时难以查找,还会造成技术债务。
修复方案:将所有服务器脚本放在中。如果需要脚本引用特定模型,让中的脚本通过路径引用或CollectionService标签查找模型。
ServerScriptServiceServerScriptServiceNot Using ModuleScripts
不使用ModuleScript
Problem: Duplicating logic across multiple Scripts/LocalScripts instead of extracting shared code into ModuleScripts.
Fix: If two scripts share logic, extract it into a ModuleScript in the appropriate location ( for shared, for server-only).
ReplicatedStorageServerStorage问题:在多个Script/LocalScript中重复逻辑,而非将共享代码提取到ModuleScript中。
修复方案:如果两个脚本共享逻辑,将其提取到合适位置的ModuleScript中(用于共享代码,用于服务器专属代码)。
ReplicatedStorageServerStorageTrusting Client Data
信任客户端数据
Problem: Server blindly applies whatever the client sends (damage values, item quantities, positions).
Fix: The server must independently validate and calculate. The client sends intent ("I want to attack this target"), not outcome ("I dealt 500 damage").
问题:服务器盲目应用客户端发送的任何内容(伤害值、道具数量、位置)。
修复方案:服务器必须独立验证和计算。客户端发送意图(「我想攻击这个目标」),而非结果(「我造成了500点伤害」)。
Polling Instead of Events
使用轮询而非事件
For polling vs event-driven patterns, see roblox-luau-mastery → Anti-Patterns.
Core rule: Use events (, , , etc.) instead of loops.
.ChangedGetPropertyChangedSignal()Diedwhile true do task.wait()关于轮询 vs 事件驱动模式,请查看roblox-luau-mastery → 反模式。
核心规则:使用事件(、、等),而非循环。
.ChangedGetPropertyChangedSignal()Diedwhile true do task.wait()Overusing RemoteFunctions
过度使用RemoteFunction
Problem: Using for everything, including fire-and-forget actions that do not need a return value. Each call yields the client thread until the server responds.
RemoteFunctionInvokeServerFix: Use for actions that do not need a response. Reserve for when the client genuinely needs data back from the server.
RemoteEventRemoteFunction问题:将用于所有场景,包括不需要返回值的触发即遗忘操作。每次调用都会暂停客户端线程,直到服务器响应。
RemoteFunctionInvokeServer修复方案:使用处理不需要响应的操作。仅当客户端确实需要从服务器获取数据时,才使用。
RemoteEventRemoteFunctionIgnoring task
Library
task忽略task
库
taskFor deprecated // vs the library, see roblox-luau-mastery → Task Library.
wait()spawn()delay()task关于已弃用的// vs 库,请查看roblox-luau-mastery → Task库。
wait()spawn()delay()taskInstance.new with Parent Argument
Instance.new传入Parent参数
Problem: passes the parent as the second argument. This causes the instance to be parented immediately during construction, which yields internally and can create race conditions - the instance replicates to clients before you've finished setting its properties.
Instance.new("Part", workspace)luau
-- BAD: parent during construction, yields, race condition
local part = Instance.new("Part", workspace)
part.Size = Vector3.new(4, 1, 4) -- may already be visible to clients
part.Anchored = true
part.BrickColor = BrickColor.new("Bright red")
-- GOOD: create, configure, then parent
local part = Instance.new("Part")
part.Size = Vector3.new(4, 1, 4)
part.Anchored = true
part.BrickColor = BrickColor.new("Bright red")
part.Parent = workspace -- parent lastRule: Always set last. Create the instance, configure all properties, then parent it.
Parent问题:将Parent作为第二个参数传入。这会导致实例在构造时立即被设置父对象,内部会产生暂停,还可能引发竞态条件——在你完成属性设置前,实例就已复制到客户端。
Instance.new("Part", workspace)luau
-- 错误:构造时设置父对象,产生暂停,存在竞态条件
local part = Instance.new("Part", workspace)
part.Size = Vector3.new(4, 1, 4) -- 可能已对客户端可见
part.Anchored = true
part.BrickColor = BrickColor.new("Bright red")
-- 正确:创建、配置,最后设置父对象
local part = Instance.new("Part")
part.Size = Vector3.new(4, 1, 4)
part.Anchored = true
part.BrickColor = BrickColor.new("Bright red")
part.Parent = workspace -- 最后设置父对象规则:始终最后设置。先创建实例,配置所有属性,再设置父对象。
Parent