afrxo/roblox-agent-skills

roblox-dev

How Roblox behaves at runtime.

소스 보기

사용 방법

  1. 설치 명령을 복사하고 자신의 에이전트 환경에 설치하기 전에 소스 패키지를 확인하세요.
  2. 필요한 도구, 인증 정보, 지원 환경은 원본 문서에서 확인하세요.
  3. 입력과 원하는 산출물을 설명하고 출처와 결과를 대조하세요.
npx skills add https://github.com/afrxo/roblox-agent-skills --skill roblox-dev

시작 전 확인

  • 설치에는 모델 구독, API 한도, 파일 접근 권한이 포함되지 않습니다.
  • 별점 수와 설치 수는 활동 지표이며 독립적인 품질·보안 검증이 아닙니다.
원본 Skill 문서

원본 저장소의 제목, 예시, 코드, 표, 링크, 이미지를 유지해 표시합니다.

Roblox Engine

How the Roblox platform behaves at runtime. Sources: create.roblox.com/docs for APIs and concepts; devforum.roblox.com for engineer clarifications and RFCs.

For deep dives see:


Integration Policy

This skill describes preferred patterns; it does not authorize rewriting existing code. Apply rules to new code and code the user explicitly asks to refactor. Match the codebase's existing style (casing, indentation, quote style, module shape) before applying skill defaults. Don't introduce tooling the project doesn't already use; surface conflicts once and default to the existing pattern.

Full rules: `../../shared/integration-policy.md`.


Principles

  • Server is authoritative. The client lies. Anything that affects game state (currency, inventory, damage, position-to-trust) lives on the server. The client signals intent; the server validates and applies.
  • State ownership is exclusive. Every piece of state has one owner — server, the controlling client, or replication. Two writers means a race.
  • Replication is async. Anything created server-side appears on clients on a future frame, not the same one. Anything created client-side does not replicate to the server.
  • Yielding is observable. A yielding call inside an event handler runs other handlers in between. Order is not what you wrote.
  • Verify before quoting an API. Roblox's API surface is huge and shifts. State uncertainty rather than hallucinate property names.

Services — GetService Everything

luau
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Workspace = game:GetService("Workspace")
local Players = game:GetService("Players")
  • Use game:GetService("Name") always. Not game.ServiceName.
  • Some services aren't available at script runtime by default — GetService creates them; dot-indexing throws.
  • Some services don't share their class name with their accessor (e.g. RunService is internally "Run Service").
  • Prefer `Workspace` via `GetService` over the lowercase `workspace` global. Both work; the global predates GetService and exists for compatibility. Sticking to GetService keeps every service access one consistent shape and surfaces typos as undefined-variable errors.
  • Put GetService calls at the top of the file, alphabetical, no others scattered later.

DataModel & Instances

  • The DataModel is the tree rooted at game. Services are top-level children. Everything else is an Instance.
  • An Instance is alive until its Parent = nil AND no script holds a reference. Setting `Parent = nil` is not destruction. Use :Destroy() to disconnect events, mark it as destroyed, and set Parent to nil.
  • `:Destroy()` is irreversible. A destroyed Instance can't be re-parented; further property access throws.
  • :Clone() requires Archivable = true (the default). Cloning a model includes its descendants; references between siblings are NOT rebound.

Querying Descendants

  • :GetChildren() — direct children only.
  • :GetDescendants() — full subtree, unfiltered. Cheap for small trees, costly for large ones (e.g. a streamed Workspace).
  • :FindFirstChild(name) / :FindFirstChild(name, true) — second arg = recursive.
  • :FindFirstChildOfClass(class) / :FindFirstChildWhichIsA(class) — class-specific direct child.
  • `:QueryDescendants(selector: string)` → { Instance }. Engine-side filtered descendant query with a CSS-like selector grammar (ClassName, .Tag, #Name, [prop = val], [$attr], [$attr = val], combinators > >> ,, pseudo-classes :not(...) :has(...)). Prefer this over GetDescendants + Luau-side filter for large trees. Thread-unsafe. Full grammar + examples in `references/query-descendants.md`.
luau
  Workspace:QueryDescendants("MeshPart.SwordPart")              -- MeshParts tagged SwordPart
  Workspace:QueryDescendants("Model >> [$OnFire = true]")       -- attribute-matching descendants of any Model
  Workspace:QueryDescendants(":not(SpotLight, PointLight)")     -- excluding lights

WaitForChild Rules

  • On the client, children of replicated containers (e.g. ReplicatedStorage, Workspace) may not exist on the first frame. Use :WaitForChild("Name") for anything you didn't put there yourself.
  • On the server, every Instance you placed in Studio exists at script start; :FindFirstChild is enough.
  • Never call WaitForChild without a timeout in production code — it yields forever if the child never arrives. :WaitForChild("Name", 5) returns nil after 5 seconds.

Script Types & Locations

Three script types:

  • `Script` — runs on server or client depending on Script.RunContext and location.
  • RunContext = Legacy (default) → server-only, only runs in server containers (ServerScriptService, Workspace, etc.).
  • RunContext = Server → server, can run from ReplicatedStorage too (but don't — clients can read source).
  • RunContext = Client → client, runs from ReplicatedStorage. Recommended for new client scripts (clearer than LocalScript).
  • `LocalScript` — client-only. No RunContext. Runs only in client containers: StarterPlayerScripts, StarterCharacterScripts, StarterGui, StarterPack, ReplicatedFirst.
  • `ModuleScript` — require()-able from either side. Runs in the requirer's context.

Recommended layout

WhereWhat goes hereRunContext
ServerScriptServiceServer scripts + server-only ModuleScriptsServer
ServerStorageServer-only data (assets, tables) — clients never see contentsn/a
ReplicatedStorageShared ModuleScripts (server + client) and client scriptsClient for client scripts
ReplicatedFirstMinimal client loader (e.g. loading screen)Client
StarterPlayerScripts / StarterCharacterScripts / StarterGui / StarterPackLocalScripts that run per-playern/a
WorkspaceWorld content. Server-side Scripts for object behaviors.Server if you want explicit

Common practice: keep most code as ModuleScripts in ReplicatedStorage (shared) or ServerStorage/server-only spots, with one server entry-point Script in ServerScriptService that requires the modules, and one client entry-point in ReplicatedStorage (RunContext = Client).

Rules

  • Don't put server logic in `ReplicatedStorage`. Clients can read source; assume anything there is leaked.
  • Set `RunContext` explicitly on scripts inside models or packages — removes ambiguity when the model is reparented or shared.
  • Use `RunService:IsServer()` / `:IsClient()` for runtime branching in shared ModuleScripts. Don't infer from script.Parent.
  • `LocalScript`s in `Workspace` don't run. Client-runnable containers only. Common bug.
  • Server scripts in `Workspace` run, but RunContext = Server in ServerScriptService is clearer and less surprising when geometry moves.

Source: create.roblox.com/docs/scripting/locations.


Replication

  • Server → client is automatic for replicated containers (Workspace, ReplicatedStorage, Players[X].PlayerGui, StarterGui-cloned-to-PlayerGui). Property changes and parenting changes replicate.
  • Client → server is NOT. Anything a client creates or modifies stays local — the server never sees it. Exception: Players[X].Character (filtering era + character ownership), input events, and explicit RemoteEvent calls.
  • Replication is per-property, not all-or-nothing. Some containers exist on both sides but their contents are client-local (Player.PlayerScripts, Player.PlayerGui after initial replication of StarterGui).
  • Set properties before parenting. Once an Instance is in a replicated container, every subsequent property change replicates as its own update — clients can observe an intermediate state. Build the Instance fully, then set Parent.
  • Signal behavior matters. Workspace.SignalBehavior = Enum.SignalBehavior.Deferred (the modern default in new places) batches signal firings until the next resumption point — code that assumes synchronous-fire ChildAdded is brittle. Test with deferred signals on.
  • See `references/replication-model.md` for the full table.

RemoteEvents & RemoteFunctions

luau
-- Server
local TakeDamage = ReplicatedStorage:WaitForChild("TakeDamage") :: RemoteEvent
TakeDamage.OnServerEvent:Connect(function(player, amount: unknown)
    if typeof(amount) ~= "number" or amount < 0 or amount > 100 then return end
    -- validated; apply
end)

-- Client
TakeDamage:FireServer(25)
  • `OnServerEvent` first arg is always the firing `Player`. It's authentic — Roblox provides it. Don't trust anything else.
  • Type every other parameter as unknown and validate at the boundary. Typing as number makes the type checker accept exploiter input. (See luau-expert skill for the full pattern.)
  • `RemoteFunction` is `:InvokeServer()` / `:InvokeClient()` — synchronous (yields). Avoid client→server InvokeClient when you can; a malicious client can hang the call. Prefer RemoteEvent + reply event.
  • Rate-limit every Remote. Track per-player call counts; reject obvious spam. Default: bounded queue, drop oldest or reject when full.
  • One Remote per logical action. Don't multiplex with a kind field — types stay clearer and validation simpler.

Character Lifecycle

luau
local function onCharacterAdded(character: Model)
    local humanoid = character:WaitForChild("Humanoid") :: Humanoid
    -- ...
end

Players.PlayerAdded:Connect(function(player)
    if player.Character then onCharacterAdded(player.Character) end
    player.CharacterAdded:Connect(onCharacterAdded)
end)
  • A player's first character may already exist when PlayerAdded fires. Check player.Character and connect to future CharacterAdded.
  • Each respawn is a new `Model`. References to the old character become stale. Reconnect every per-character signal in CharacterAdded; use character.AncestryChanged or humanoid.Died to clean up the previous set.
  • Humanoid.Died fires once per character. After death the model lingers (default ~5s, controlled by Players.RespawnTime) before automatic respawn destroys it.
  • Player.CharacterRemoving fires before the model leaves Workspace — last chance to read state from it.

Streaming

Modern places enable Workspace.StreamingEnabled. Parts of the world load and unload around each player.

  • The client may not see a part the server sees. Never assume client geometry exists — WaitForChild (with timeout) or nil-check.
  • Per-Model streaming is controlled via Model.ModelStreamingMode:
  • Default — model streams normally, descendants stream independently.
  • Atomic — the whole model streams in/out as a single unit (no partial loads).
  • Persistent — the model is always loaded for every client.
  • PersistentPerPlayer — always loaded for players added via the relevant Add/Remove API; off by default for others.
  • Use Atomic for objects whose descendants must arrive together (vehicle assemblies, NPC rigs). Use Persistent sparingly — it costs memory on every client.
  • Replication of property changes still occurs for streamed-out parts; the client just doesn't render them. Don't drive BasePart.CFrame updates on streamed-out objects — wasted bandwidth.
  • See `references/streaming-enabled.md` for chunk semantics and ModelStreamingMode interactions.

Physics Ownership

  • Each unanchored BasePart has a network owner. Default: server. When a player's character touches/steps on/holds a part, ownership transfers to that player's client.
  • The owner simulates physics; the result replicates back. A client-owned part is not server-authoritative for position — never trust its CFrame for game-state decisions.
  • Force ownership server-side: part:SetNetworkOwner(player) — pass nil to give it back to the server. :GetNetworkOwner() reads it.
  • Anchored parts have no owner. SetNetworkOwner errors on an anchored part. Check with BasePart:CanSetNetworkOwnership() first if you don't control the input.
  • Auto-transfer is the default. Once you call :SetNetworkOwner(player) or :SetNetworkOwner(nil), auto-transfer is off for that part — ownership stays where you set it. Call :SetNetworkOwnershipAuto() to put it back on auto.
  • For anything the player must not be able to teleport (chests, NPCs, projectiles, anti-cheat-relevant parts) — SetNetworkOwner(nil) once, never SetNetworkOwnershipAuto().

Cloud Services (Persistence & Cross-Server)

Roblox offers several storage options. Pick by access pattern, not by reflex.

ServicePersistenceScopeUse for
Standard data stores (DataStoreService:GetDataStore)PermanentCross-serverUser progress, inventory, save data. Numbers/strings/booleans/tables. NoSQL-like.
Ordered data stores (:GetOrderedDataStore)PermanentCross-serverAll-time leaderboards (numbers only, sortable).
Memory stores (MemoryStoreService)≤ 45 daysCross-serverMatchmaking, daily/monthly leaderboards, ephemeral cross-server queues. Faster than data stores.
Configs (Creator Hub)PermanentCross-serverFeature flags, tunable values. Read-only from in-game.
Secrets storesPermanentCross-serverAPI keys, third-party tokens. Read-only from in-game.
In-memory Luau tableSessionSingle serverTemporary state (timers, status effects, current health). Free, instant.

Cloud services are server-only. Clients can't read DataStores, MemoryStores, etc. Route any client need through a RemoteEvent.

Data-store core rules

  • Always `pcall`. Reads/writes throw on network failure, throttling, and quota exhaustion.
  • `UpdateAsync` for concurrent state (not SetAsync) — gives you the previous value and merges atomically. Return nil from the transform to abort without burning quota.
  • Session-lock pattern: read once on PlayerAdded, mutate in-memory for the session, save on PlayerRemoving + BindToClose. Don't read on every change.
  • `game:BindToClose` has a ~30-second total budget across all players. Save in parallel via task.spawn.
  • Rate limits are per-key + per-game, refilled over time. There's also a hard ~6-second-per-key rule.

Memory-store core rules

  • Ephemeral: data expires after up to 45 days. Don't put save data here.
  • Useful primitives: queues, sorted maps, hash maps. Higher write throughput than data stores.
  • Server-only API.

See `references/cloud-services.md` for the full picker (when to use which) and `references/datastore-rules.md` for data-store rate limits, retry/backoff, and session-locking patterns.

Source: create.roblox.com/docs/cloud-services/data-stores-vs-memory-stores.


Task Scheduling

The task library replaces classic wait/spawn/delay. Use it.

CallWhen
task.spawn(f, ...)Run f on a new thread immediately (resumes after current resumption point). Use to detach a yielding body from an event handler.
task.defer(f, ...)Run f after the current resumption cycle finishes — later than spawn. Use to coalesce work without yielding.
task.delay(t, f, ...)Run f after t seconds on a new thread. Replaces the legacy delay.
task.wait(t)Yield current thread for t seconds (or one frame if omitted). Replaces wait.
task.cancel(thread)Kill a thread started by task.spawn/task.delay. Pair with explicit cleanup.
task.synchronize() / task.desynchronize()Parallel-Luau actor coordination. See the Parallel Luau section below.
  • Don't use the legacy globals wait, spawn, delay. They throttle (wait clamps to ~30 Hz) and leak in patterns the task versions don't.
  • `task.spawn` inside an event handler lets fast-firing events run concurrently. Add your own queue/lock if the body mutates shared state.
  • `task.defer` before `task.spawn` when you want the work to happen after the current event-handling pass — useful inside :Connect bodies that mutate the world and trigger more signals.
  • Track threads from task.spawn/task.delay if they need cancellation; nothing auto-cancels on Instance destruction.

Buffer Library (Binary Data)

buffer is Luau's fixed-size mutable byte block. Source: `buffer.yaml` in creator-docs.

Use it for:

  • Compact RemoteEvent payloads (a packed buffer is smaller than a Luau table over the wire).
  • Custom binary serialization for DataStore values you want to keep small.
  • Reading/writing existing binary formats.
  • Replacing string.pack / string.unpack use cases.
luau
local b = buffer.create(16)        -- 16-byte buffer, zero-initialized
buffer.writeu32(b, 0, 0xCAFEBABE)  -- 4 bytes at offset 0
buffer.writef32(b, 4, 1.5)         -- 4 bytes at offset 4
buffer.writestring(b, 8, "abc")    -- 3 bytes at offset 8

local magic = buffer.readu32(b, 0)
local payload = buffer.readstring(b, 8, 3)

Key rules:

  • All offsets are byte offsets from 0. Reading/writing past the buffer raises an error.
  • Little-endian for every numeric read/write.
  • Max buffer size: 1 GiB (buffer.create rejects larger; allocations near that may fail anyway).
  • Sent across Roblox APIs (RemoteEvents, etc.) by copy. Identity is not preserved on the receiver. Don't share a buffer between Actor scripts (Parallel Luau).
  • Fixed size. No grow/shrink — allocate the right size, or copy into a new one (buffer.copy).

Functions in three groups:

  • Lifecycle: buffer.create(size), buffer.fromstring(s), buffer.tostring(b), buffer.len(b).
  • Read/write fixed-width numerics: readi8/u8/i16/u16/i32/u32/f32/f64 and matching write*.
  • Bit-level + bulk: readbits(b, bitOffset, bitCount) / writebits (0–32 bits per call), readstring(b, offset, count) / writestring(b, offset, value, count?), buffer.copy(target, targetOffset, source, sourceOffset?, count?), buffer.fill(b, offset, value, count?).

Full signature reference in `references/buffer-library.md`.


Parallel Luau (Multithreading)

By default Luau is single-threaded. Parallel Luau lets independent work run on multiple OS threads via the Actor model. Use it for CPU-bound work that can be split: per-player raycast validation, procedural generation, large per-instance computations.

Actor model

  • Scripts under an Actor Instance are eligible to run in parallel.
  • One thread per actor. Multiple scripts in the same actor still run serially with respect to each other.
  • More actors = better load balancing, even on devices with fewer cores than actors.
  • Don't nest actors as descendants of other actors — a script's owning actor is its closest ancestor.

Phases: serial vs parallel

Code runs serially by default. Switch in/out:

luau
-- Inside an Actor's script
RunService.Heartbeat:ConnectParallel(function()
    -- parallel phase
    local result = computeExpensive()
    task.synchronize()
    -- serial phase
    applyResult(result)
end)
  • `task.desynchronize()` — switch current thread to parallel. Yields, resumes at next parallel-execution opportunity.
  • `task.synchronize()` — switch back to serial. Required before mutating most Instances.
  • `Signal:ConnectParallel(callback)` — schedule a signal callback to run in parallel automatically; no need for desynchronize inside.
  • `require()` is not allowed in a parallel phase. Require modules in serial first.

Thread safety levels

API members carry a thread-safety tag:

LevelPropertiesFunctions
UnsafeNo parallel read or writeNo parallel call
Read ParallelRead OK, write notn/a
Local SafeSame-actor any; cross-actor read-onlySame-actor only
SafeRead + writeCall from anywhere

Default is Unsafe if untagged. Most read-heavy access (geometry queries, raycasts) is safe; most mutations are not. Check the API reference per-member.

Cross-thread communication

  • `Actor:SendMessage(topic, ...)` + `Actor:BindToMessage(topic, fn)` / `:BindToMessageParallel(topic, fn)` — async one-way messaging. Multiple bindings per topic allowed.
  • `SharedTable` — table-like structure shared across actors with atomic updates. No copy on send. Use for "common world state not stored in DataModel".
  • Direct DataModel writes in serial phases work but force frequent synchronization.

When not to use it

  • Long uninterrupted computations. Even on a parallel thread, a 100ms unyielding loop still blocks that thread for 100ms. Break work up.
  • Cheap per-frame work. Actor overhead dwarfs the savings for trivial tasks.
  • Code that mostly mutates the DataModel. You'll spend most of your time task.synchronize'd, defeating the point.

Full deep-dive: `references/parallel-luau.md`. Source: create.roblox.com/docs/scripting/multithreading.


Performance

Performance optimization is a cycle: design for it up front, identify hot spots with the MicroProfiler, mitigate, monitor. Below is the high-signal subset; full pitfall list in `references/performance-optimization.md`.

Frame budget

  • Target: 60 FPS = 16.67 ms per frame for the client. Server has no frame rate but RunService.Heartbeat plays the same role.
  • Anything Luau-side that consistently runs >1 ms per frame is a candidate for optimization.

Top mistakes

  • Heavy work on `RunService.Heartbeat` (or PreSimulation/PostSimulation/PreRender/PreAnimation). Heartbeat fires every frame — gate by interval (if now - last < 0.1 then return end) or move work to a slower loop.
  • Server-side `TweenService`. Replicates the tweened property every frame; stutters under latency. Tween on the client unless the result must be authoritative.
  • Connections to `Player` / character that never disconnect. Player Instances aren't auto-destroyed on leave (see Workspace.PlayerCharacterDestroyBehavior to opt in to auto-destroy). Track and disconnect explicitly, or destroy manually in PlayerRemoving.
  • Tables that grow without bounds. playerInfo[player] = ... on join without playerInfo[player] = nil on leave is a classic server leak.
  • Deep cloning large tables. Use shallow copy; mutate only the path that changed (cross-link luau-expert skill).
  • Large RemoteEvent payloads. Send deltas, not whole inventories. For high-frequency state, pack into a buffer.
  • Excessive instance creation/destruction. Cloning large rigs at runtime replicates the entire descendant tree to clients. Pre-instance pools beat per-event instantiation.
  • Unanchored parts that don't move. Anchored = true removes them from physics simulation entirely.
  • High `CollisionFidelity`. Precise is expensive in CPU and memory. Use Box/Hull for anything where the player won't notice.
  • Asset duplication. Same mesh/texture uploaded multiple times = same content in memory multiple times. Reuse asset IDs.
  • Animation metadata in published rigs. Animation Editor data on a cloned rig replicates with every clone. Strip it before publish.
  • `PreloadAsync` over the entire Workspace for a "no pop-in" loading screen. Preload only what the loading screen and starting area need.
  • Native code generation (--!native) for hot, unyielding compute paths is a low-effort win — verify the function actually runs hot first.

MicroProfiler scopes worth knowing

  • RunService.Heartbeat / PreRender / PreSimulation / PostSimulation — your scripts.
  • physicsStepped / worldStep — physics cost.
  • ProcessPackets / Allocate Bandwidth — network cost.
  • updateInvalidatedFastClusters — humanoid/skinned-mesh churn.

Tag your own scopes with debug.profilebegin("Name") / debug.profileend().

Sources: create.roblox.com/docs/performance-optimization, /improve.


Bug Catalog

Duplicate Connections

luau
-- ❌ every CharacterAdded adds another connection; old ones never disconnect
Players.PlayerAdded:Connect(function(player)
    player.CharacterAdded:Connect(function(char)
        char.Humanoid.Died:Connect(function() ... end)   -- leaks per respawn
    end)
end)

Track and disconnect connections per scope. A common pattern: store { RBXScriptConnection } keyed by the lifecycle owner (player, character, instance), iterate and :Disconnect() on cleanup.

Stale References

luau
local character = player.Character
task.wait(5)
character.Humanoid.Health = 0     -- ❌ player may have respawned; old model is destroyed

Re-fetch player.Character after any yield, or capture+verify with if character.Parent == nil then return end.

Yielding in Event Handlers

luau
RemoteEvent.OnServerEvent:Connect(function(player)
    task.wait(1)              -- ❌ all subsequent fires from this player wait behind this one
    grantReward(player)
end)

Either don't yield in handlers, or task.spawn the body so each fire runs independently. Be careful with task.spawn — concurrent state updates need a lock.

Memory Leaks

  • Instance:Destroy() disconnects events and removes from the DataModel, but doesn't break Lua-side references. Set the variable to nil if it's long-lived.
  • Connections to non-Roblox signals (custom signals, libraries) don't auto-clean. Track and disconnect them.
  • Closures capturing large tables in :Connect(...) keep those tables alive as long as the connection exists.

CollectionService Cleanup

luau
-- ❌ tag handler runs once per existing tagged instance, then once per new one
CollectionService:GetTagged("Door"):forEach(setupDoor)
CollectionService:GetInstanceAddedSignal("Door"):Connect(setupDoor)

Always pair with GetInstanceRemovedSignal for cleanup. Track per-instance state in a { [Instance]: State } map, not in closures. Use instance.AncestryChanged (when parent == nil) or the removed signal.

Race: Player Joining Mid-Setup

PlayerAdded fires for players already in the server when your script starts (binding via :Connect only catches future fires) — handle existing players manually:

luau
for _, player in Players:GetPlayers() do onPlayerAdded(player) end
Players.PlayerAdded:Connect(onPlayerAdded)

Architecture Rules

  • Validate every server boundary. RemoteEvent params, DataStore reads, MessagingService payloads, HttpService responses, anything user-influenced.
  • Server-authoritative checks beat client checks. Client-side validation is UX (instant feedback). Server-side validation is security. Never one without the other.
  • One ModuleScript = one concern. Don't aggregate into giant "Manager" modules — stack focused modules with clear ownership. (See luau-expert for module/file structure rules.)
  • One owner per piece of state. A field is server-owned, replicated-only, or client-owned. Never two writers. If multiple actors must update something, route through one module that serializes the writes.
  • Centralize replication. One module per replicated domain (inventory, currency, quest progress) — not Remotes scattered through the codebase. Easier to audit, easier to add validation/rate-limit.
  • Don't trust client-supplied UserIds. A UserId you read off any real Player Instance (the OnServerEvent first arg, Players:GetPlayers(), Players:GetPlayerByUserId) is authentic. A UserId the client sends as a RemoteEvent argument is just a number — validate or ignore.
  • Idempotent server handlers. Replays from network stutter or replays from exploiters are real. Design RemoteEvent handlers so two identical fires produce the same end state.
  • Expensive work goes server-side. Pathfinding, large queries, anything CPU-heavy. Client devices vary wildly; server is uniform.
  • Don't build cross-server state on `MessagingService` alone. It's best-effort, not guaranteed delivery, and rate-limited per-game. Use it for "interesting events" notifications, not for synchronizing state.
  • Match script container to runtime. Server logic in ServerScriptService/ServerStorage; client logic in StarterPlayerScripts/StarterCharacterScripts/StarterGui; shared modules in ReplicatedStorage. Don't put server-only logic in ReplicatedStorage — clients can read it.

Verification Rules

  • Don't quote API signatures from memory for properties beyond the most stable surface (Instance.Parent, BasePart.CFrame, Humanoid.Health, etc.). Roblox renames, deprecates, and adds. When unsure, point the user at create.roblox.com/docs and state your uncertainty.
  • Behavior beats API. Phrase rules as "the server is authoritative for…" rather than "use Workspace.StreamingMinRadius = 256" — the property may be renamed; the principle won't.
  • Distinguish stable from changing. DataModel hierarchy, services, replication direction, character lifecycle: stable for years. New APIs (e.g. recent Workspace properties, new RunService methods, scripting-related betas): verify before recommending.
  • Don't conflate engine and language. If the question is about typing or --!strict behavior, defer to luau-expert. If it's about Rojo or filesystem layout, defer to roblox-toolchain. If it's about UI libraries, defer to roblox-ui.

Anti-Patterns

❌ Don't✅ Do
game.Workspace / workspace globalgame:GetService("Workspace")
game.Players.PlayerAdded:Connect(f) onlyIterate :GetPlayers() then :Connect for joins-during-startup
child = parent:FindFirstChild("X") on client:WaitForChild("X", timeout)
Trust client RemoteEvent args by type annotationType as unknown, validate, narrow
RemoteFunction:InvokeClientRemoteEvent round-trip
One Remote multiplexing many actions via kind fieldOne Remote per action
SetAsync for concurrent stateUpdateAsync
DataStore call without pcallAlways pcall + retry/backoff
Read DataStore on every changeSession-cached; write on PlayerRemoving + BindToClose
Store character ref across yieldsRe-fetch player.Character after any yield
Connect-without-disconnect across respawnsTrack + disconnect connections per character
Instance.Parent = nil for cleanup:Destroy()
WaitForChild without timeout:WaitForChild("X", 5) and handle nil
Client-side validation onlyAlways validate on server
Yield inside an event handler that fires oftentask.spawn the body; serialize via your own queue
Auto network ownership for sensitive partsExplicit SetNetworkOwner(nil)
pairs over a tag set without removed-signal pairPair GetInstanceAddedSignal with GetInstanceRemovedSignal
Server logic in ReplicatedStorageServerScriptService / ServerStorage
LocalScript in WorkspaceClient containers (StarterPlayerScripts, etc.) or Script with RunContext = Client in ReplicatedStorage
wait / spawn / delay legacy globalstask.wait / task.spawn / task.defer / task.delay
Heavy work directly on RunService.HeartbeatThrottle / move to slower loop / task.delay
Server-side TweenService for visual tweensTween on the client; replicate target only
Cloning large rigs at runtime per eventPool / pre-instance
CollisionFidelity = Precise on small partsBox / Hull
Mutating Instances inside a parallel phasetask.synchronize() first
Storing API keys in DataStoresSecrets stores
Save data in MemoryStoreStandard data stores

Examples

관련 작업

같은 저장소의 Skills

더 많은 Skills

모든 Skills