원본 저장소의 제목, 예시, 코드, 표, 링크, 이미지를 유지해 표시합니다.
Roblox game architecture
The model in one paragraph
Every Roblox game is a server (authoritative, runs Scripts, sees everything, owns DataStores) plus one client per player (runs LocalScripts / client Scripts, renders, reads input). The server's DataModel replicates to clients automatically. Client changes do not replicate back, except: physics of parts the client has network ownership of (its character by default), its own Humanoid state/animations, and remote calls. Assume every client is compromised (see roblox-security). Design every feature as: client requests → server validates and decides → state replicates → clients render.
Where things go
| Container | Runs/visible on | Put here |
|---|---|---|
ServerScriptService | Server only | Server Scripts and server-only ModuleScripts (game logic, data, anti-cheat). |
ServerStorage | Server only | Server-only assets: maps, NPC templates, tools to clone, secrets-adjacent config. |
ReplicatedStorage | Both | Shared modules (types, config, pure utils), RemoteEvents, assets clients clone. |
ReplicatedFirst | Client, first | Loading screen only. Replicates before everything else. |
StarterPlayer.StarterPlayerScripts | Client (copied once to PlayerScripts) | Client entry point and controllers. |
StarterPlayer.StarterCharacterScripts | Client (copied per spawn into character) | Per-character client scripts (rarely needed). |
StarterGui | Client (copied to PlayerGui) | ScreenGuis. Set ResetOnSpawn = false unless you want reset on death. |
Workspace | Both (streamed to clients) | The 3D world only. Not a place for code or templates. |
Lighting, SoundService, Teams, TextChatService | Both | Their respective config objects. |
Rules:
- Code in
ReplicatedStorageis downloadable by exploiters. Never put server logic, secrets,
admin lists, or anti-cheat thresholds there.
Script.RunContext:Legacy(default: behavior depends on the container),Server,Client, or
Plugin. A non-Legacy RunContext makes a Script run regardless of container (e.g. a Client Script in ReplicatedStorage runs on every client) — so templates you clone must not contain such scripts unintentionally. Legacy Scripts don't run in ReplicatedStorage/ServerStorage.
Project layout (Rojo-style, recommended)
src/
server/ -> ServerScriptService.Server (init.server.luau = entry point)
Services/ -> one ModuleScript per system (DataService, CombatService, ...)
client/ -> StarterPlayer.StarterPlayerScripts.Client (init.client.luau)
Controllers/ -> one ModuleScript per client system (InputController, UIController, ...)
shared/ -> ReplicatedStorage.Shared (types, config, pure logic, network definitions)
Packages/ -> ReplicatedStorage.Packages (Wally/pesde dependencies)
ServerPackages/ -> ServerScriptService.ServerPackagesOne entry script per side, everything else is a ModuleScript. This gives deterministic load order, testable modules, and a single place to wire dependencies. Roblox's own recommendation is equivalent: one Script (RunContext = Server) in ServerScriptService and one Script (RunContext = Client) in ReplicatedStorage, each requiring modules and calling their start(). Tooling setup: roblox-tooling.
Bootstrapping services (no framework needed)
--!strict
-- ServerScriptService/Server/init.server.luau
type Service = {
init: ((self: Service) -> ())?, -- sync setup; may reference other services; must not yield
start: ((self: Service) -> ())?, -- runs after every init; may yield / connect events
}
local servicesFolder = script:WaitForChild("Services")
local services: { [string]: Service } = {}
for _, module in servicesFolder:GetChildren() do
if module:IsA("ModuleScript") then
services[module.Name] = (require :: any)(module)
end
end
for name, service in services do
if service.init then
debug.setmemorycategory(name)
service:init()
debug.resetmemorycategory()
end
end
for name, service in services do
if service.start then
task.spawn(function()
debug.setmemorycategory(name)
service:start()
end)
end
end- Services reference each other with plain
require(no cycles) or receive dependencies ininit. - The client mirrors this with
Controllers. Keep names symmetrical (ShopService/ShopController). - Frameworks like Knit are unnecessary for this; if a project already uses one, follow its conventions.
Player and character lifecycle
--!strict
local Players = game:GetService("Players")
local function onCharacterAdded(player: Player, character: Model)
local humanoid = character:WaitForChild("Humanoid") :: Humanoid
humanoid.Died:Once(function()
print(`{player.Name} died`)
end)
end
local function onPlayerAdded(player: Player)
-- Load data first (see roblox-data-stores), then set up character handling.
if player.Character then
task.spawn(onCharacterAdded, player, player.Character)
end
player.CharacterAdded:Connect(function(character)
onCharacterAdded(player, character)
end)
end
local function onPlayerRemoving(player: Player)
-- Save data, release session locks, clear every table keyed by this player.
print(`{player.Name} left`)
end
Players.PlayerAdded:Connect(onPlayerAdded)
Players.PlayerRemoving:Connect(onPlayerRemoving)
for _, player in Players:GetPlayers() do -- players who joined before this script connected
task.spawn(onPlayerAdded, player)
end
game:BindToClose(function()
-- Server shutdown: flush saves for remaining players (max ~30 s total).
end)- Always handle players/characters that already exist when your code starts.
- Characters respawn: anything bound to a character must be re-created on
CharacterAdded. - On the client, character descendants may not exist yet (streaming):
WaitForChildthem.
Components with CollectionService tags
Tag instances (Studio Tags section or CollectionService:AddTag) and attach behavior in code. This scales better than copying scripts into every model and works with streaming because the added/removed signals fire as instances stream in and out.
--!strict
local CollectionService = game:GetService("CollectionService")
local TAG = "KillBrick"
local cleanups: { [Instance]: () -> () } = {}
local function onAdded(instance: Instance)
if not instance:IsA("BasePart") then
return
end
local connection = instance.Touched:Connect(function(hit)
local humanoid = hit.Parent and hit.Parent:FindFirstChildOfClass("Humanoid")
if humanoid then
humanoid.Health = 0
end
end)
cleanups[instance] = function()
connection:Disconnect()
end
end
local function onRemoved(instance: Instance)
local cleanup = cleanups[instance]
if cleanup then
cleanup()
cleanups[instance] = nil
end
end
CollectionService:GetInstanceAddedSignal(TAG):Connect(onAdded)
CollectionService:GetInstanceRemovedSignal(TAG):Connect(onRemoved)
for _, instance in CollectionService:GetTagged(TAG) do
task.spawn(onAdded, instance)
endConfigure components with attributes (instance:GetAttribute("Damage"), GetAttributeChangedSignal) instead of IntValue/StringValue children. Attributes replicate server → client, are cheaper, and show in the Properties panel.
Sharing state
| Need | Use |
|---|---|
| Small per-instance/per-player values visible to clients | Attributes on the instance / Player (server sets). |
| Events and requests | RemoteEvent / UnreliableRemoteEvent (see roblox-networking). |
| Large or per-player private state (inventory, quests) | Server-owned tables + remotes that send snapshots/diffs to that player only. |
| Same-side communication | Module function calls or a typed Signal module — not BindableEvent, _G, or shared. |
| Persistent data | roblox-data-stores. Cross-server: MessagingService / MemoryStore. |
Streaming (on by default for new places)
With Workspace.StreamingEnabled, clients only have nearby parts. Client code must:
WaitForChild(with a timeout when it may never arrive) or nil-check anything inWorkspace.- Use
ModelStreamingMode = Atomicfor models whose parts scripts need together;
Persistent only when a model must always be present (costs memory).
- Treat
GetChildren, raycasts, and property reads of streamed-out parts as partial/stale; do full-world
queries on the server.
- Pre-stream destinations on the server with
player:RequestStreamAroundAsync(position)before teleporting a character.
Full pattern catalog and recommended Workspace settings: references/streaming.md.
Anti-patterns
- Dozens of loose
Scripts inside parts/models (use tags + one service). - Polling loops (
while task.wait(0.1) do check() end) instead of events orHeartbeatwithdeltaTime. - Trusting the client for game state, or firing remotes every frame.
_G/sharedglobals, circular requires, yielding at module top level.- Cloning maps into
Workspacefrom the client (client-only copies never get server updates). - Storing per-player data in the character (it's destroyed on death).
Library ecosystem (signals, cleanup, networking, UI, ECS, data): references/ecosystem.md.
Related skills
roblox-networking, roblox-security, roblox-data-stores, roblox-tooling, roblox-performance, roblox-game-systems.

