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
game
(class
DataModel
). The hierarchy determines behavior: a
Script
placed in
ServerScriptService
runs on the server, but the same
Script
placed in
StarterPlayerScripts
will not run at all (only
LocalScript
and client-side
Script
run there).
Roblox游戏基于实例树构建。每个对象(部件、脚本、UI元素、音效)都是一个Instance,位于以
game
DataModel
类)为根的层级结构中。层级结构决定行为:放在
ServerScriptService
中的
Script
会在服务器运行,但放在
StarterPlayerScripts
中的同一个
Script
完全不会运行(只有
LocalScript
和客户端侧
Script
能在此运行)。

Client vs. Server Execution

客户端 vs 服务器执行

AspectServerClient
Runs onRoblox datacenter (one instance per game server)Each player's device
Script types
Script
(Luau),
ModuleScript
(when required by a server script)
LocalScript
,
ModuleScript
(when required by a local script)
Trust levelAuthoritative -- owns game state, data stores, physics arbitrationUntrusted -- can be exploited; never trust client input blindly
AccessCan see everything in the data modelCannot see
ServerScriptService
or
ServerStorage
维度服务器客户端
运行位置Roblox数据中心(每个游戏服务器一个实例)每个玩家的设备
脚本类型
Script
(Luau语言)、
ModuleScript
(被服务器脚本引用时)
LocalScript
ModuleScript
(被本地脚本引用时)
信任级别权威方——拥有游戏状态、数据存储、物理仲裁权不可信——可能被利用;绝不盲目信任客户端输入
访问权限可查看数据模型中的所有内容无法查看
ServerScriptService
ServerStorage

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
    ,
    Chat
    , and
    Teams
    is visible to every connected client.
  • Server-only (hidden from clients):
    ServerScriptService
    and
    ServerStorage
    are never sent to clients. This is the correct place for server logic and secret assets.
  • Per-player: Each player gets their own
    PlayerGui
    (cloned from
    StarterGui
    ),
    PlayerScripts
    (cloned from
    StarterPlayerScripts
    ),
    StarterGear
    (cloned from
    StarterPack
    ), and
    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
Script
instances that run exclusively on the server. Clients cannot see or access anything inside.
Use 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.lua

ReplicatedStorage

ReplicatedStorage

Purpose: Shared container visible to both server and client. Content is replicated to every client on join.
Use for:
  • RemoteEvent
    and
    RemoteFunction
    instances (the communication bridge)
  • Shared
    ModuleScript
    modules (utilities, constants, types, shared classes)
  • 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.rbxm

ReplicatedFirst

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:显示加载UI

StarterGui

StarterGui

Purpose: UI template container. On each player spawn (or respawn, depending on
ResetOnSpawn
), the contents are cloned into that player's
PlayerGui
.
Use 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.lua
Note: Set
ScreenGui.ResetOnSpawn = false
for persistent UI that should not re-clone on character respawn.
用途:UI模板容器。在每个玩家生成(或重生,取决于
ResetOnSpawn
设置)时,其中内容会被克隆到该玩家的
PlayerGui
中。
适用场景
  • 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:
LocalScript
instances here are cloned into each player's
PlayerScripts
folder once on join. They persist across respawns.
Use 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
用途:此处的
LocalScript
实例会在玩家加入时克隆到每个玩家的
PlayerScripts
文件夹,且会在重生后保留。
适用场景
  • 相机控制器
  • 输入处理系统
  • 客户端游戏管理器
  • 音乐/环境音效控制器
StarterPlayer/
  StarterPlayerScripts/
    CameraController.client.lua
    InputManager.client.lua
    ClientBootstrap.client.lua

StarterPlayer / StarterCharacterScripts

StarterPlayer / StarterCharacterScripts

Purpose: Scripts here are cloned into the player's
Character
model each time the character spawns. They are destroyed when the character dies.
Use 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.lua

StarterPack

StarterPack

Purpose:
Tool
instances here are cloned into each player's
Backpack
on spawn.
Use 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
用途:此处的
Tool
实例会在玩家生成时克隆到每个玩家的
Backpack
中。
适用场景
  • 所有玩家初始拥有的默认武器或道具
  • 嵌入LocalScript和Script的工具
StarterPack/
  Sword.Tool
    SwordClient.client.lua
    SwordServer.server.lua
    Handle.Part

Workspace

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
ServerScriptService
instead. Workspace scripts are accessible to exploiters and create organizational chaos.

用途:3D游戏世界。运行时游戏中可见的所有内容都位于此处:部件、模型、地形、相机。
适用场景
  • 物理游戏世界(地形、建筑、装饰)
  • 运行时生成的实体(从ServerStorage克隆)
  • 相机(每个客户端有一个本地
    Workspace.CurrentCamera
请勿直接在Workspace中放置Script。请改用
ServerScriptService
。Workspace中的脚本可被 exploiters 获取,还会造成管理混乱。

4. Script Types

4. 脚本类型

Script (Server Script)

Script(服务器脚本)

  • Runs on: Server (default RunContext)
  • Valid containers:
    ServerScriptService
    ,
    ServerStorage
    (when parented under certain conditions),
    Workspace
    (discouraged)
  • File convention:
    *.server.lua
    in Rojo projects
  • Access: Full access to server APIs (
    DataStoreService
    ,
    MessagingService
    ,
    HttpService
    , etc.)
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
overrides where a script runs, breaking the traditional container-based restrictions:
ValueBehaviorNotes
Enum.RunContext.Legacy
Script runs only in traditional server containers (ServerScriptService, ServerStorage)Default when not set
Enum.RunContext.Server
Script runs anywhere in the place, on the serverUseful for scripts in ReplicatedStorage or Workspace
Enum.RunContext.Client
Script runs anywhere in the place, on the clientPlace in Workspace for local effects, or ReplicatedStorage for shared UI logic
Legacy
replicates the old behavior -
Script
must be in server-valid containers,
LocalScript
in client-valid containers. Setting
Server
or
Client
frees the script from those container restrictions.
RunContext = Client
is powerful for workspace-local effects or anything the client sees but doesn't belong in StarterPlayerScripts:
lua
-- 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 = Server
lets you place server scripts outside the usual server containers:
lua
-- 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:
LocalScript
does NOT have RunContext. It always runs on the client in client-valid containers only. Use
Script
+
RunContext = Client
when you need client scripts outside the usual client containers.
Script.RunContext
可覆盖脚本的运行位置,打破传统的容器限制:
行为说明
Enum.RunContext.Legacy
脚本仅在传统服务器容器(ServerScriptService、ServerStorage)中运行未设置时的默认值
Enum.RunContext.Server
脚本可在场景中的任何位置运行,且在服务器端执行适用于ReplicatedStorage或Workspace中的脚本
Enum.RunContext.Client
脚本可在场景中的任何位置运行,且在客户端执行可放在Workspace中实现本地特效,或放在ReplicatedStorage中实现共享UI逻辑
Legacy
模式复刻旧行为——
Script
必须位于服务器有效容器中,
LocalScript
必须位于客户端有效容器中。设置为
Server
Client
可解除脚本的容器限制。
RunContext = Client
适用于Workspace本地特效或任何客户端可见但不属于StarterPlayerScripts的内容:
lua
-- 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 = Server
允许将服务器脚本放在常规服务器容器之外:
lua
-- ReplicatedStorage中的Script,RunContext = Server
-- 服务器逻辑与共享模块放在一起
local SharedConfig = require(script.Parent.SharedConfig)
print(`Server running with: {SharedConfig.setting}`)
注意
LocalScript
没有RunContext属性。它始终在客户端的有效容器中运行。当需要在常规客户端容器之外放置客户端脚本时,请使用
Script
+
RunContext = Client

LocalScript (Client Script)

LocalScript(客户端脚本)

  • Runs on: Client (the specific player's device)
  • Valid containers:
    StarterPlayerScripts
    ,
    StarterCharacterScripts
    ,
    StarterGui
    ,
    StarterPack
    , a player's
    Backpack
    ,
    PlayerGui
    ,
    PlayerScripts
    , or
    Character
  • File convention:
    *.client.lua
    in Rojo projects
  • Access: Client APIs (
    UserInputService
    ,
    ContextActionService
    ,
    Camera
    , local player's GUI)
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
    PlayerScripts
    Character
  • 文件命名约定:Rojo项目中使用
    *.client.lua
  • 访问权限:客户端API(
    UserInputService
    ContextActionService
    Camera
    、本地玩家的GUI)
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
    require()
    s it (server if required by a Script, client if required by a LocalScript)
  • Valid containers: Anywhere, but location determines who can access it
    • ServerScriptService
      or
      ServerStorage
      -- server-only modules
    • ReplicatedStorage
      -- shared modules (accessible by both sides)
    • StarterPlayerScripts
      -- client-only modules
  • File convention:
    *.lua
    (no
    .server
    or
    .client
    suffix) in Rojo projects
  • 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 MathUtils
Requiring:
lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local MathUtils = require(ReplicatedStorage.Modules.MathUtils)

local result = MathUtils.lerp(0, 100, 0.5) -- 50

  • 运行位置:取决于调用
    require()
    的一侧(被Script引用则在服务器运行,被LocalScript引用则在客户端运行)
  • 有效容器:任何位置,但存放位置决定谁能访问
    • 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) -- 50

5. Client-Server Communication

5. 客户端-服务器通信

For implementation details (RemoteEvent, RemoteFunction, UnreliableRemoteEvent, BindableEvent, validation patterns), see roblox-networking → Client-Server Communication.
Conceptual overview:
TypeDirectionBlocking?Use Case
RemoteEvent
Client ↔ ServerNoActions, notifications, state changes
RemoteFunction
Client → Server only (safe)Yes (yields)Data requests, queries
BindableEvent
Same sideN/A (local)Decoupled same-side messaging
UnreliableRemoteEvent
Client ↔ ServerNoHigh-frequency cosmetic data
Core rules:
  • Server is authoritative. Never trust client input.
  • Use
    RemoteEvent
    for fire-and-forget. Use
    RemoteFunction
    only when the caller needs a return value.
  • Never use
    RemoteFunction:InvokeClient()
    - if the client errors or disconnects, the server thread hangs forever.
  • UnreliableRemoteEvent
    for cosmetic data only (cursor position, facing direction). Never for damage, purchases, or state.

关于实现细节(RemoteEvent、RemoteFunction、UnreliableRemoteEvent、BindableEvent、验证模式),请查看roblox-networking → 客户端-服务器通信。
概念概述
类型方向是否阻塞适用场景
RemoteEvent
客户端 ↔ 服务器操作、通知、状态变更
RemoteFunction
仅客户端→服务器(安全)是(会暂停线程)数据请求、查询
BindableEvent
同一侧不适用(本地)解耦的同侧消息传递
UnreliableRemoteEvent
客户端 ↔ 服务器高频 cosmetic 数据
核心规则
  • 服务器是权威方。绝不信任客户端输入。
  • 使用
    RemoteEvent
    处理触发即遗忘的操作。仅当调用方需要返回值时才使用
    RemoteFunction
  • 绝不要使用
    RemoteFunction:InvokeClient()
    ——如果客户端出错或断开连接,服务器线程会永久挂起。
  • UnreliableRemoteEvent
    仅用于cosmetic数据(光标位置、朝向)。绝不要用于伤害、购买或状态变更。

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 InventoryModule

The
require()
Pattern

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
    require()
    calls return the cached result.
  • The module runs in the context of the first requirer (server or client).
  • If a server Script requires a module in
    ReplicatedStorage
    , it runs on the server. If a LocalScript requires the same module, a separate client-side copy runs.
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
nil
.
Problem:
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 loading
Solution 1: Deferred initialization with
init()
pattern
lua
-- ModuleA.lua
local ModuleA = {}
local ModuleB -- forward declaration

function ModuleA.init(modules)
    ModuleB = modules.ModuleB
end

function ModuleA.doSomething()
    ModuleB.helper()
end

return ModuleA
lua
-- 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 ModuleA
lua
-- 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 Weapon
lua
-- 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()
end

7. 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 Loader

Comparison

对比

AspectNo Framework (Manual)Single Script Architecture
ComplexityLowMedium
BoilerplateMinimalLow
NetworkingManual RemoteEvent setupManual
Learning curveJust Roblox APIsSmall pattern to learn
ScalabilityDegrades without disciplineGood with init/start pattern
Dependency managementManual require chainsCentralized loader
Best forJams, small projects, learningMedium 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.lua

9. Best Practices

9. 最佳实践

Separation of Concerns

关注点分离

Each script or module should have a single, clear responsibility. A
CombatService
handles damage and health. A
DataService
handles saving and loading. They communicate via well-defined interfaces, not by reaching into each other's internals.
每个脚本或模块应具有单一、明确的职责。
CombatService
处理伤害和生命值,
DataService
处理保存和加载。它们通过定义清晰的接口通信,而非直接访问彼此的内部状态。

Single Responsibility

单一职责

One module = one job. If a module handles inventory AND crafting AND trading, split it into
InventoryModule
,
CraftingModule
, and
TradingModule
.
一个模块对应一个任务。如果一个模块同时处理背包、 crafting 和交易,应拆分为
InventoryModule
CraftingModule
TradingModule

Minimal 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
B.getData()
rather than reading
B._internalTable
directly.
模块应依赖抽象(函数调用、事件),而非其他模块的内部状态。如果模块A需要模块B的数据,A应调用
B.getData()
,而非直接读取
B._internalTable

Server 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
    OnServerEvent
    handler must validate types, ranges, and ownership before processing.
  • 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
Instance.new("RemoteEvent")
across multiple scripts.
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
end

Use Types and Constants

使用类型和常量

Define shared constants and Luau types in
ReplicatedStorage
so both sides use the same definitions.
lua
-- 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

ReplicatedStorage
中定义共享常量和Luau类型,让客户端和服务器使用相同的定义。
lua
-- 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

10. 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
init()
pattern (Section 6), dependency inversion, or event-driven decoupling.
问题:模块A引用模块B,模块B引用模块A。导致其中一个模块收到不完整(空表)的引用。
修复方案:使用
init()
模式(第6节)、依赖反转或事件驱动解耦。

Server Logic in ReplicatedStorage

服务器逻辑放在ReplicatedStorage

Problem: Placing server-only game logic modules in
ReplicatedStorage
. Exploiters can read the source code, understand validation logic, and craft exploits.
Fix: Keep server logic in
ServerScriptService
or
ServerStorage
. Only put genuinely shared code (types, constants, utilities) in
ReplicatedStorage
.
问题:将服务器专属游戏逻辑模块放在
ReplicatedStorage
中。Exploiters可以读取源代码,了解验证逻辑,进而制作作弊工具。
修复方案:将服务器逻辑放在
ServerScriptService
ServerStorage
中。仅将真正的共享代码(类型、常量、工具类)放在
ReplicatedStorage
中。

Scripts 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
ServerScriptService
. If you need a script to reference a specific model, have the script in
ServerScriptService
and use a path reference or CollectionService tag to find the model.
问题:将Script直接作为Workspace或Workspace中模型的子对象。这些脚本对客户端可见(Exploiters可以读取),开发时难以查找,还会造成技术债务。
修复方案:将所有服务器脚本放在
ServerScriptService
中。如果需要脚本引用特定模型,让
ServerScriptService
中的脚本通过路径引用或CollectionService标签查找模型。

Not 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 (
ReplicatedStorage
for shared,
ServerStorage
for server-only).
问题:在多个Script/LocalScript中重复逻辑,而非将共享代码提取到ModuleScript中。
修复方案:如果两个脚本共享逻辑,将其提取到合适位置的ModuleScript中(
ReplicatedStorage
用于共享代码,
ServerStorage
用于服务器专属代码)。

Trusting 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 (
.Changed
,
GetPropertyChangedSignal()
,
Died
, etc.) instead of
while true do task.wait()
loops.
关于轮询 vs 事件驱动模式,请查看roblox-luau-mastery → 反模式。
核心规则:使用事件(
.Changed
GetPropertyChangedSignal()
Died
等),而非
while true do task.wait()
循环。

Overusing RemoteFunctions

过度使用RemoteFunction

Problem: Using
RemoteFunction
for everything, including fire-and-forget actions that do not need a return value. Each
InvokeServer
call yields the client thread until the server responds.
Fix: Use
RemoteEvent
for actions that do not need a response. Reserve
RemoteFunction
for when the client genuinely needs data back from the server.
问题:将
RemoteFunction
用于所有场景,包括不需要返回值的触发即遗忘操作。每次
InvokeServer
调用都会暂停客户端线程,直到服务器响应。
修复方案:使用
RemoteEvent
处理不需要响应的操作。仅当客户端确实需要从服务器获取数据时,才使用
RemoteFunction

Ignoring
task
Library

忽略
task

For deprecated
wait()
/
spawn()
/
delay()
vs the
task
library, see roblox-luau-mastery → Task Library.
关于已弃用的
wait()
/
spawn()
/
delay()
vs
task
库,请查看roblox-luau-mastery → Task库。

Instance.new with Parent Argument

Instance.new传入Parent参数

Problem:
Instance.new("Part", workspace)
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.
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 last
Rule: Always set
Parent
last. Create the instance, configure all properties, then parent it.
问题
Instance.new("Part", workspace)
将Parent作为第二个参数传入。这会导致实例在构造时立即被设置父对象,内部会产生暂停,还可能引发竞态条件——在你完成属性设置前,实例就已复制到客户端。
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
。先创建实例,配置所有属性,再设置父对象。