Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/brockmartin/roblox-game-skill/llms.txt

Use this file to discover all available pages before exploring further.

Roblox games are built on a tree of Instances rooted at game. Every object — parts, scripts, UI elements, sounds — lives somewhere in that hierarchy, and its location determines its behavior: a Script in ServerScriptService runs exclusively on the server, while the same Script placed in StarterPlayerScripts would not run at all. Understanding which container to use, how client and server communicate, and how to organize ModuleScripts is the foundation of every maintainable Roblox codebase. This reference covers the canonical patterns from small game-jam projects all the way to large framework-based games.

Client vs. Server Execution

AspectServerClient
Runs onRoblox datacenter (one instance per game server)Each player’s device
Script typesScript (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

Service Hierarchy

Roblox replicates parts of the data model from server to clients automatically. Knowing what lives where — and who can see it — is critical for both security and performance.

Replication Summary

ServiceWho Sees ItReplicated?Use For
ServerScriptServiceServer only❌ NeverServer Scripts, server-only modules
ServerStorageServer only❌ NeverServer assets, templates, secret modules
ReplicatedStorageServer + all clients✅ AlwaysRemotes, shared modules, shared assets
ReplicatedFirstServer + all clients✅ FirstLoading screens, early client init
StarterGuiServer (template)✅ Per-playerUI ScreenGuis cloned into PlayerGui
StarterPlayerScriptsServer (template)✅ Per-playerLocalScripts cloned into PlayerScripts
StarterCharacterScriptsServer (template)✅ Per-spawnScripts cloned into character on spawn
StarterPackServer (template)✅ Per-spawnDefault tools cloned into Backpack
WorkspaceServer + all clients✅ AlwaysThe 3D world — parts, terrain, models

ServerScriptService

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), DataStore calls, server-side validation, anti-cheat enforcement, NPC/AI controllers, and server-only modules.
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

ServerStorage

A server-only container for assets and modules that clients should never download. Use for: Map models cloned into Workspace on demand, enemy/NPC templates, server-only utility libraries, data schemas.
ServerStorage/
  Maps/
    DesertArena.rbxm
    ForestArena.rbxm
  Templates/
    Loot/
      CommonChest.rbxm
  Modules/
    DataSchema.lua

ReplicatedStorage

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), assets both sides reference.
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

StarterGui

UI template container. On each player spawn, contents are cloned into that player’s PlayerGui.
Set ScreenGui.ResetOnSpawn = false for persistent UI that should not re-clone on character respawn.
Use for: HUD elements, menu screens, UI-controlling LocalScripts.
StarterGui/
  HUD.ScreenGui
    HealthBar.Frame
    ScoreLabel.TextLabel
    HUDController.client.lua    -- LocalScript managing HUD updates
  ShopMenu.ScreenGui
    ShopController.client.lua

StarterPlayerScripts and StarterCharacterScripts

LocalScript instances in StarterPlayerScripts are cloned into each player’s PlayerScripts folder once on join and persist across respawns. Scripts in StarterCharacterScripts are cloned into the player’s Character model each time the character spawns — they are destroyed on death.
StarterPlayer/
  StarterPlayerScripts/
    CameraController.client.lua   -- persists across respawns
    InputManager.client.lua
    ClientBootstrap.client.lua
  StarterCharacterScripts/
    FootstepSounds.client.lua     -- destroyed on death, recreated on respawn
    AnimationController.client.lua

Script Types

Script (Server)

Runs on the server. Valid in ServerScriptService. Full access to server APIs. Use .server.lua suffix in Rojo projects.

LocalScript (Client)

Runs on the player’s device. Valid in StarterPlayerScripts, StarterGui, StarterPack, and character. Use .client.lua suffix.

ModuleScript (Both)

Runs on whichever side require()s it. Returns exactly one value. Location determines who can access it — ReplicatedStorage for shared, ServerStorage for server-only.

Script Type Reference

TypeRuns OnValid ContainersFile Convention
ScriptServerServerScriptService, Workspace (avoid)*.server.lua
LocalScriptClientStarterPlayerScripts, StarterCharacterScripts, StarterGui, StarterPack, PlayerGui, Character*.client.lua
ModuleScriptCaller’s sideAnywhere — location controls access*.lua
-- ServerScriptService/GameManager.server.lua
local Players = game:GetService("Players")

Players.PlayerAdded:Connect(function(player)
    print(player.Name .. " joined the server")
end)
-- 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)
-- ReplicatedStorage/Modules/MathUtils.lua
local MathUtils = {}

function MathUtils.lerp(a: number, b: number, t: number): number
    return a + (b - a) * t
end

return MathUtils

-- Usage from any script:
local MathUtils = require(game:GetService("ReplicatedStorage").Modules.MathUtils)
local result = MathUtils.lerp(0, 100, 0.5) -- 50

Client-Server Communication

Communication Type Reference

TypeDirectionBlocking?Reliable?Use Case
RemoteEventClient ↔ ServerNoYesActions, notifications, state changes
RemoteFunctionClient → Server only (safe)Yes (yields)YesData requests, queries
BindableEventSame sideNoN/A (local)Decoupled same-side messaging
BindableFunctionSame sideYes (yields)N/A (local)Same-side request/response
UnreliableRemoteEventClient ↔ ServerNoNoHigh-frequency cosmetic data

RemoteEvent (Async, One-Way)

Use for fire-and-forget messages where the sender does not need a response.
-- SERVER: fire to one client
local NotifyEvent = game:GetService("ReplicatedStorage").Remotes.NotifyEvent

NotifyEvent:FireClient(player, "You picked up a coin!", 1)

-- SERVER: fire to ALL clients
NotifyEvent:FireAllClients("A boss has spawned!")
-- CLIENT: listen for server messages
local NotifyEvent = game:GetService("ReplicatedStorage").Remotes.NotifyEvent

NotifyEvent.OnClientEvent:Connect(function(message, amount)
    print("Server says:", message, amount)
end)

RemoteFunction (Sync, Request-Response)

Use when the client needs a return value from the server.
Never call RemoteFunction:InvokeClient(). If the client’s callback errors or the player disconnects, the server-side thread yields forever. Use RemoteEvent pairs for server-to-client request/response instead.
-- CLIENT: request data from server
local GetInventory = game:GetService("ReplicatedStorage").Remotes.GetInventory

local inventory = GetInventory:InvokeServer()
for _, item in inventory do
    print(item.Name, item.Quantity)
end
-- SERVER: handle the request
local GetInventory = game:GetService("ReplicatedStorage").Remotes.GetInventory

GetInventory.OnServerInvoke = function(player)
    -- player is auto-injected
    local data = DataManager.getPlayerData(player)
    return data.Inventory
end

BindableEvent (Same-Side Communication)

Use for decoupled communication between scripts on the same side. Does not cross the network boundary.
-- SERVER: script A fires, script B listens
local RoundEndEvent = Instance.new("BindableEvent")
RoundEndEvent.Name = "RoundEndEvent"
RoundEndEvent.Parent = game:GetService("ServerScriptService")

-- Script A
RoundEndEvent:Fire("Team Alpha", 15)

-- Script B
RoundEndEvent.Event:Connect(function(winningTeam, score)
    print(winningTeam .. " won with " .. score .. " points")
end)

UnreliableRemoteEvent (High-Frequency, Loss-Tolerant)

Use for data sent very frequently where occasional packet loss is acceptable. Do not use for damage, purchases, or any authoritative state change.
-- CLIENT: send cursor position every frame
local RunService = game:GetService("RunService")
local CursorEvent = game:GetService("ReplicatedStorage").Remotes.CursorPosition

RunService.RenderStepped:Connect(function()
    local mousePos = UserInputService:GetMouseLocation()
    CursorEvent:FireServer(mousePos)
end)
-- SERVER: relay to other players
CursorEvent.OnServerEvent:Connect(function(player, mousePos)
    for _, otherPlayer in Players:GetPlayers() do
        if otherPlayer ~= player then
            CursorEvent:FireClient(otherPlayer, player, mousePos)
        end
    end
end)

Server-Side Validation Pattern

Every OnServerEvent and OnServerInvoke handler must validate its inputs before acting.
RemoteEvent.OnServerEvent:Connect(function(player, targetId, ...)
    -- 1. Type check
    if typeof(targetId) ~= "string" then return end

    -- 2. Existence check
    local target = findEntityById(targetId)
    if not target then return end

    -- 3. Range check
    local character = player.Character
    if not character then return end
    local distance = (character.PrimaryPart.Position - target.Position).Magnitude
    if distance > MAX_INTERACTION_RANGE then return end

    -- 4. Ownership / permission check
    -- (verify the player is allowed to interact with this target)

    -- 5. Rate check
    if not canPerformAction(player) then return end

    -- 6. Process the valid request
    processAction(player, target)
end)

ModuleScript Organization

Basic Module Pattern

Every ModuleScript returns a single table. Functions and state 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

The require() Pattern

-- ServerScriptService/Main.server.lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerStorage     = game:GetService("ServerStorage")

-- Shared modules (both sides can require)
local ItemData = require(ReplicatedStorage.Modules.ItemData)

-- Server-only modules
local InventoryModule = require(ServerStorage.Modules.InventoryModule)

InventoryModule.init()
A module runs once. Subsequent require() calls return the cached result. If a server Script and a LocalScript both require the same module in ReplicatedStorage, they each get their own copy running in their respective context.

Avoiding Circular Dependencies

Circular dependencies occur when Module A requires Module B and Module B requires Module A. This causes one module to receive an incomplete (empty table) reference.
-- 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
return ModuleB

OOP Module Pattern

-- 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(self: Weapon): boolean
    return (os.clock() - self._lastFired) >= self.cooldown
end

function Weapon.fire(self: Weapon): 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

Centralize Remote Definitions

Create all RemoteEvents and RemoteFunctions in one place rather than scattering Instance.new("RemoteEvent") across 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

Framework Patterns

No Framework (Manual)

Best for game jams, prototypes, and small games with under ~10 scripts. Minimal boilerplate; full control.
ServerScriptService/
  GameManager.server.lua
  DataHandler.server.lua
ReplicatedStorage/
  Remotes/   (RemoteEvent instances)
  SharedConfig.lua
StarterPlayer/
  StarterPlayerScripts/
    CameraController.client.lua

Single Script Architecture (SSA)

One server Script and one client LocalScript. 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),
}

-- Phase 1: Init (no cross-dependencies yet)
for _, mod in modules do
    if mod.init then mod:init() end
end

-- Phase 2: Start (all modules available)
for _, mod in modules do
    if mod.start then mod:start() end
end

return Loader

Knit Framework

Knit structures server code into Services and client code into Controllers with automatic networking.
-- ServerScriptService/Services/CoinService.lua
local Knit = require(game:GetService("ReplicatedStorage").Packages.Knit)

local CoinService = Knit.CreateService({
    Name = "CoinService",
    Client = {
        CoinCollected = Knit.CreateSignal(), -- auto-creates RemoteEvent
    },
})

function CoinService.Client:GetCoins(player)
    return self.Server:_getCoins(player)
end

function CoinService:_getCoins(player)
    return playerCoins[player] or 0
end

function CoinService:KnitStart()
    -- Runs after all services are initialized
end

function CoinService:KnitInit()
    -- Runs first; set up state here
end

return CoinService

Framework Comparison

AspectNo FrameworkSingle Script ArchitectureKnit
ComplexityLowMediumMedium-High
BoilerplateMinimalLowModerate
NetworkingManual RemoteEvent setupManualAutomatic (Services expose Client API)
Learning curveJust Roblox APIsSmall patternFramework-specific concepts
ScalabilityDegrades without disciplineGood with init/start patternGood, enforced structure
Dependency managementManual require chainsCentralized loaderBuilt-in service discovery
Best forJams, small projectsMedium projects, teams wanting controlLarger projects, teams wanting convention

Folder Organization

For game jams, prototypes, or games with fewer than ~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/

Best Practices

1

Server owns all state

The server is the single source of truth. Clients render and predict; the server validates and authorizes. The client sends intent (“I attacked this target”), not outcome (“deal 500 damage”).
2

Single responsibility per module

One module = one job. If a module handles inventory AND crafting AND trading, split it into InventoryModule, CraftingModule, and TradingModule.
3

Use ModuleScripts everywhere

Keep Scripts and LocalScripts as thin bootstrappers that require and initialize ModuleScripts. Never put core game logic directly in a Script.
4

Validate all client input

Every OnServerEvent and OnServerInvoke must check type, range, existence, ownership, and rate. Treat all client data as adversarial.
5

Centralize Remote definitions

Create all RemoteEvents and RemoteFunctions in a single setup script. Scatter Instance.new("RemoteEvent") calls and you will lose track of your API surface.
6

Keep server logic out of ReplicatedStorage

Only put genuinely shared code (types, constants, utilities) in ReplicatedStorage. Exploiters can read everything there. Validation logic, rate limit thresholds, and damage formulas belong in ServerScriptService or ServerStorage.
7

Define shared constants and types in ReplicatedStorage

Both sides should reference the same constants. A Constants.lua and Types.lua in ReplicatedStorage/Modules/ ensures they never drift apart.

Architecture Anti-Patterns

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 that requires and initializes them.
Problem: Module A requires Module B, which requires Module A. One of them receives an incomplete (empty table) reference, causing subtle nil-method errors.Fix: Use the deferred init() pattern (pass a modules table after all requires complete), extract shared logic into a third module, or use BindableEvent-based decoupling.
Problem: Placing server-only game logic modules in ReplicatedStorage. Exploiters can read the source code and craft targeted exploits against your validation logic.Fix: Only put genuinely shared code in ReplicatedStorage. Keep validation rules, anti-cheat logic, and server formulas in ServerScriptService or ServerStorage.
Problem: Placing Scripts directly as children of Workspace or inside Workspace models. They are visible to exploiters, hard to find, and create organizational chaos.Fix: Put all server scripts in ServerScriptService. Reference models via path or CollectionService tags from within the service.
Problem: Server blindly applies whatever the client sends — damage values, item quantities, positions.Fix: The client sends intent only. The server independently validates and calculates outcomes.
Problem: while true do task.wait(1) loops to check if something changed, instead of connecting to events.
-- BAD: polling
while true do
    task.wait(1)
    if player.Character and player.Character.Humanoid.Health <= 0 then
        handleDeath()
    end
end

-- GOOD: event-driven
humanoid.Died:Connect(function()
    handleDeath()
end)
Problem: Using RemoteFunction for every interaction, including fire-and-forget actions. Each InvokeServer yields the client thread until the server responds, adding unnecessary latency.Fix: Use RemoteEvent for actions that do not need a return value. Reserve RemoteFunction for genuine data queries.
Problem: Using deprecated wait(), spawn(), and delay() which throttle, have imprecise timing, and swallow errors.
-- BAD
wait(1)
spawn(function() doWork() end)

-- GOOD
task.wait(1)
task.spawn(function() doWork() end)

Build docs developers (and LLMs) love