一键导入
roblox-architecture
Service hierarchy, 7 foundational patterns, cross-platform input. Client-server architecture, module patterns, framework options.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Service hierarchy, 7 foundational patterns, cross-platform input. Client-server architecture, module patterns, framework options.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
DataStores, ProfileStore, session locking, data persistence patterns.
Animations, particles, tweens, ContentProvider, visual effects.
GUI systems, layout, responsiveness, cross-platform UI. ScreenGuis, UIListLayout, constraint-based design.
Luau language fundamentals, type system, OOP, deprecation table, error patterns.
ProcessReceipt correctness, prompt APIs, purchase reconciliation, session-lock interaction.
Server-authoritative networking, RemoteEvent validation, rate limiting, exploit prevention, security hardening.
| name | roblox-architecture |
| description | Service hierarchy, 7 foundational patterns, cross-platform input. Client-server architecture, module patterns, framework options. |
| last_reviewed | "2026-05-21T00:00:00.000Z" |
Load this reference when:
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 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).
| Aspect | Server | Client |
|---|---|---|
| Runs on | Roblox 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 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 ServerScriptService or ServerStorage |
Roblox automatically replicates (syncs) parts of the data model from server to clients:
Workspace, ReplicatedStorage, ReplicatedFirst, Lighting, SoundService, Chat, and Teams is visible to every connected client.ServerScriptService and ServerStorage are never sent to clients. This is the correct place for server logic and secret assets.PlayerGui (cloned from StarterGui), PlayerScripts (cloned from StarterPlayerScripts), StarterGear (cloned from StarterPack), and Backpack.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).
Purpose: Contains Script instances that run exclusively on the server. Clients cannot see or access anything inside.
Use for:
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
Purpose: A server-only container for assets and modules that clients should never see or download.
Use for:
ServerStorage/
Maps/
DesertArena.rbxm
ForestArena.rbxm
Templates/
Loot/
CommonChest.rbxm
Modules/
DataSchema.lua
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)ModuleScript modules (utilities, constants, types, shared classes)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
Purpose: Scripts here run on the client before anything else loads. Content replicates to clients first.
Use for:
ReplicatedFirst/
LoadingScreen.client.lua -- LocalScript: shows loading UI
Purpose: UI template container. On each player spawn (or respawn, depending on ResetOnSpawn), the contents are cloned into that player's PlayerGui.
Use for:
StarterGui/
HUD.ScreenGui
HealthBar.Frame
ScoreLabel.TextLabel
HUDController.client.lua -- LocalScript managing HUD updates
ShopMenu.ScreenGui
ShopController.client.lua
Note: Set
ScreenGui.ResetOnSpawn = falsefor persistent UI that should not re-clone on character respawn.
Purpose: LocalScript instances here are cloned into each player's PlayerScripts folder once on join. They persist across respawns.
Use for:
StarterPlayer/
StarterPlayerScripts/
CameraController.client.lua
InputManager.client.lua
ClientBootstrap.client.lua
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:
StarterPlayer/
StarterCharacterScripts/
FootstepSounds.client.lua
AnimationController.client.lua
Purpose: Tool instances here are cloned into each player's Backpack on spawn.
Use for:
StarterPack/
Sword.Tool
SwordClient.client.lua
SwordServer.server.lua
Handle.Part
Purpose: The 3D world. Everything visible in the game exists here at runtime: parts, models, terrain, cameras.
Use for:
Workspace.CurrentCamera)Do NOT place Scripts directly in Workspace. Use ServerScriptService instead. Workspace scripts are accessible to exploiters and create organizational chaos.
ServerScriptService, ServerStorage (when parented under certain conditions), Workspace (discouraged)*.server.lua in Rojo projectsDataStoreService, MessagingService, HttpService, etc.)-- ServerScriptService/GameManager.server.lua
local Players = game:GetService("Players")
Players.PlayerAdded:Connect(function(player)
print(`{player.Name} joined the server`)
end)
By default, Script runs on the server. Setting Script.RunContext changes this:
Enum.RunContext.Server (default) — runs on the server, only in server-valid containersEnum.RunContext.Client — runs on the client, can be placed ANYWHERE (Workspace, ReplicatedStorage, etc.)RunContext = Client is powerful for workspace-local effects, proximity scripts, or anything the client sees but doesn't belong in StarterPlayerScripts:
-- 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)
Note:
LocalScriptdoes NOT have RunContext. It always runs on the client in client-valid containers only. UseScript+RunContext = Clientwhen you need client scripts outside the usual client containers.
StarterPlayerScripts, StarterCharacterScripts, StarterGui, StarterPack, a player's Backpack, PlayerGui, PlayerScripts, or Character*.client.lua in Rojo projectsUserInputService, ContextActionService, Camera, local player's GUI)-- 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)
require()s it (server if required by a Script, client if required by a LocalScript)ServerScriptService or ServerStorage -- server-only modulesReplicatedStorage -- shared modules (accessible by both sides)StarterPlayerScripts -- client-only modules*.lua (no .server or .client suffix) in Rojo projects-- 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:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local MathUtils = require(ReplicatedStorage.Modules.MathUtils)
local result = MathUtils.lerp(0, 100, 0.5) -- 50
For implementation details (RemoteEvent, RemoteFunction, UnreliableRemoteEvent, BindableEvent, validation patterns), see roblox-networking → Client-Server Communication.
Conceptual overview:
| Type | Direction | Blocking? | Use Case |
|---|---|---|---|
RemoteEvent | Client ↔ Server | No | Actions, notifications, state changes |
RemoteFunction | Client → Server only (safe) | Yes (yields) | Data requests, queries |
BindableEvent | Same side | N/A (local) | Decoupled same-side messaging |
UnreliableRemoteEvent | Client ↔ Server | No | High-frequency cosmetic data |
Core rules:
RemoteEvent for fire-and-forget. Use RemoteFunction only when the caller needs a return value.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.Every ModuleScript returns a single table. Functions and data are fields on that table.
-- 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
require() Pattern-- 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():
require() calls return the cached result.ReplicatedStorage, it runs on the server. If a LocalScript requires the same module, a separate client-side copy runs.Circular dependencies occur when Module A requires Module B, and Module B requires Module A. This causes an infinite loop or returns nil.
Problem:
-- 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
-- ModuleA.lua
local ModuleA = {}
local ModuleB -- forward declaration
function ModuleA.init(modules)
ModuleB = modules.ModuleB
end
function ModuleA.doSomething()
ModuleB.helper()
end
return ModuleA
-- 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.
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().
-- 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
-- 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
One Script on the server, one LocalScript on the client. Each requires a "loader" module that initializes all other modules in order.
-- ServerScriptService/Main.server.lua
require(game:GetService("ServerStorage").Loader)
-- 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
| 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 |
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/
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/
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
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.
One module = one job. If a module handles inventory AND crafting AND trading, split it into InventoryModule, CraftingModule, and TradingModule.
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.
The server is the single source of truth for all game state. Clients render and predict, but the server validates and authorizes.
For implementation details (type checking, range checking, ownership, rate limiting), see roblox-networking → Client Validation.
Core rules:
OnServerEvent handler must validate types, ranges, and ownership before processing.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.
Create all RemoteEvents and RemoteFunctions in one place rather than scattering Instance.new("RemoteEvent") across multiple scripts.
-- 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
Define shared constants and Luau types in ReplicatedStorage so both sides use the same definitions.
-- 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
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.
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.
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.
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.
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).
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").
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.
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.
task LibraryFor deprecated wait()/spawn()/delay() vs the task library, see roblox-luau-mastery → Task Library.
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.
-- 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.