roblox-opencode 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -0
- package/commands/setup-game.md +108 -0
- package/commands/sync-check.md +53 -0
- package/core/roblox-core.md +93 -0
- package/dist/server.js +167 -0
- package/package.json +35 -0
- package/skills/roblox-analytics/SKILL.md +277 -0
- package/skills/roblox-analytics/references/event-batcher.luau +75 -0
- package/skills/roblox-animation-vfx/SKILL.md +1325 -0
- package/skills/roblox-architecture/SKILL.md +863 -0
- package/skills/roblox-architecture/references/combat-systems.md +1381 -0
- package/skills/roblox-code-review/SKILL.md +687 -0
- package/skills/roblox-data/SKILL.md +889 -0
- package/skills/roblox-data/references/inventory-systems.md +1729 -0
- package/skills/roblox-debug/SKILL.md +99 -0
- package/skills/roblox-gui/SKILL.md +1103 -0
- package/skills/roblox-gui-fusion/SKILL.md +150 -0
- package/skills/roblox-gui-fusion/references/inventory.luau +427 -0
- package/skills/roblox-gui-fusion/references/settings-menu.luau +579 -0
- package/skills/roblox-gui-fusion/references/shop.luau +411 -0
- package/skills/roblox-luau-mastery/SKILL.md +1519 -0
- package/skills/roblox-monetization/SKILL.md +1084 -0
- package/skills/roblox-monetization/references/process-receipt.luau +131 -0
- package/skills/roblox-networking/SKILL.md +669 -0
- package/skills/roblox-networking/references/remote-validator.luau +193 -0
- package/skills/roblox-publish-checklist/SKILL.md +128 -0
- package/skills/roblox-runtime/SKILL.md +753 -0
- package/skills/roblox-sharp-edges/SKILL.md +295 -0
- package/skills/roblox-sync/SKILL.md +126 -0
- package/skills/roblox-testing/SKILL.md +943 -0
- package/skills/roblox-tooling/SKILL.md +150 -0
- package/vendor/LICENSES/ProfileStore-LICENSE +201 -0
- package/vendor/LICENSES/RbxUtil-LICENSE +7 -0
- package/vendor/LICENSES/promise-LICENSE +21 -0
- package/vendor/LICENSES/t-LICENSE +21 -0
- package/vendor/LICENSES/testez-LICENSE +201 -0
- package/vendor/README.md +84 -0
- package/vendor/fusion/Animation/ExternalTime.luau +84 -0
- package/vendor/fusion/Animation/Spring.luau +322 -0
- package/vendor/fusion/Animation/Stopwatch.luau +128 -0
- package/vendor/fusion/Animation/Tween.luau +187 -0
- package/vendor/fusion/Animation/getTweenDuration.luau +27 -0
- package/vendor/fusion/Animation/getTweenRatio.luau +47 -0
- package/vendor/fusion/Animation/lerpType.luau +164 -0
- package/vendor/fusion/Animation/packType.luau +100 -0
- package/vendor/fusion/Animation/springCoefficients.luau +80 -0
- package/vendor/fusion/Animation/unpackType.luau +103 -0
- package/vendor/fusion/Colour/Oklab.luau +70 -0
- package/vendor/fusion/Colour/sRGB.luau +55 -0
- package/vendor/fusion/External.luau +168 -0
- package/vendor/fusion/ExternalDebug.luau +70 -0
- package/vendor/fusion/Graph/Observer.luau +114 -0
- package/vendor/fusion/Graph/castToGraph.luau +29 -0
- package/vendor/fusion/Graph/change.luau +81 -0
- package/vendor/fusion/Graph/depend.luau +33 -0
- package/vendor/fusion/Graph/evaluate.luau +56 -0
- package/vendor/fusion/Instances/Attribute.luau +58 -0
- package/vendor/fusion/Instances/AttributeChange.luau +47 -0
- package/vendor/fusion/Instances/AttributeOut.luau +63 -0
- package/vendor/fusion/Instances/Child.luau +21 -0
- package/vendor/fusion/Instances/Children.luau +148 -0
- package/vendor/fusion/Instances/Hydrate.luau +33 -0
- package/vendor/fusion/Instances/New.luau +53 -0
- package/vendor/fusion/Instances/OnChange.luau +50 -0
- package/vendor/fusion/Instances/OnEvent.luau +54 -0
- package/vendor/fusion/Instances/Out.luau +69 -0
- package/vendor/fusion/Instances/applyInstanceProps.luau +149 -0
- package/vendor/fusion/Instances/defaultProps.luau +194 -0
- package/vendor/fusion/LICENSE +21 -0
- package/vendor/fusion/Logging/formatError.luau +49 -0
- package/vendor/fusion/Logging/messages.luau +52 -0
- package/vendor/fusion/Logging/parseError.luau +25 -0
- package/vendor/fusion/Memory/checkLifetime.luau +134 -0
- package/vendor/fusion/Memory/deriveScope.luau +24 -0
- package/vendor/fusion/Memory/deriveScopeImpl.luau +45 -0
- package/vendor/fusion/Memory/doCleanup.luau +79 -0
- package/vendor/fusion/Memory/innerScope.luau +34 -0
- package/vendor/fusion/Memory/legacyCleanup.luau +18 -0
- package/vendor/fusion/Memory/needsDestruction.luau +17 -0
- package/vendor/fusion/Memory/poisonScope.luau +34 -0
- package/vendor/fusion/Memory/scopePool.luau +55 -0
- package/vendor/fusion/Memory/scoped.luau +27 -0
- package/vendor/fusion/Memory/whichLivesLonger.luau +75 -0
- package/vendor/fusion/RobloxExternal.luau +98 -0
- package/vendor/fusion/State/Computed.luau +139 -0
- package/vendor/fusion/State/For/Disassembly.luau +211 -0
- package/vendor/fusion/State/For/ForTypes.luau +30 -0
- package/vendor/fusion/State/For/init.luau +110 -0
- package/vendor/fusion/State/ForKeys.luau +94 -0
- package/vendor/fusion/State/ForPairs.luau +97 -0
- package/vendor/fusion/State/ForValues.luau +94 -0
- package/vendor/fusion/State/Value.luau +88 -0
- package/vendor/fusion/State/castToState.luau +26 -0
- package/vendor/fusion/State/peek.luau +31 -0
- package/vendor/fusion/State/updateAll.luau +1 -0
- package/vendor/fusion/Types.luau +314 -0
- package/vendor/fusion/Utility/Contextual.luau +91 -0
- package/vendor/fusion/Utility/Safe.luau +23 -0
- package/vendor/fusion/Utility/isSimilar.luau +29 -0
- package/vendor/fusion/Utility/merge.luau +35 -0
- package/vendor/fusion/Utility/nameOf.luau +35 -0
- package/vendor/fusion/Utility/never.luau +14 -0
- package/vendor/fusion/Utility/nicknames.luau +11 -0
- package/vendor/fusion/Utility/xtypeof.luau +27 -0
- package/vendor/fusion/init.luau +82 -0
- package/vendor/profilestore/init.luau +2243 -0
- package/vendor/promise/init.luau +1982 -0
- package/vendor/rbxutil/buffer-util/Buffer.test.luau +25 -0
- package/vendor/rbxutil/buffer-util/BufferReader.luau +228 -0
- package/vendor/rbxutil/buffer-util/BufferWriter.luau +269 -0
- package/vendor/rbxutil/buffer-util/DataTypeBuffer.luau +223 -0
- package/vendor/rbxutil/buffer-util/Types.luau +60 -0
- package/vendor/rbxutil/buffer-util/index.d.ts +153 -0
- package/vendor/rbxutil/buffer-util/init.luau +41 -0
- package/vendor/rbxutil/buffer-util/package.json +16 -0
- package/vendor/rbxutil/buffer-util/wally.toml +9 -0
- package/vendor/rbxutil/comm/Client/ClientComm.luau +232 -0
- package/vendor/rbxutil/comm/Client/ClientRemoteProperty.luau +156 -0
- package/vendor/rbxutil/comm/Client/ClientRemoteSignal.luau +109 -0
- package/vendor/rbxutil/comm/Client/init.luau +135 -0
- package/vendor/rbxutil/comm/Server/RemoteProperty.luau +295 -0
- package/vendor/rbxutil/comm/Server/RemoteSignal.luau +211 -0
- package/vendor/rbxutil/comm/Server/ServerComm.luau +211 -0
- package/vendor/rbxutil/comm/Server/init.luau +140 -0
- package/vendor/rbxutil/comm/Types.luau +18 -0
- package/vendor/rbxutil/comm/Util.luau +27 -0
- package/vendor/rbxutil/comm/init.luau +35 -0
- package/vendor/rbxutil/comm/wally.toml +13 -0
- package/vendor/rbxutil/component/init.luau +759 -0
- package/vendor/rbxutil/component/init.test.luau +311 -0
- package/vendor/rbxutil/component/wally.toml +14 -0
- package/vendor/rbxutil/concur/init.luau +542 -0
- package/vendor/rbxutil/concur/init.test.luau +364 -0
- package/vendor/rbxutil/concur/wally.toml +8 -0
- package/vendor/rbxutil/enum-list/init.luau +101 -0
- package/vendor/rbxutil/enum-list/init.test.luau +91 -0
- package/vendor/rbxutil/enum-list/wally.toml +8 -0
- package/vendor/rbxutil/find/index.d.ts +20 -0
- package/vendor/rbxutil/find/init.luau +44 -0
- package/vendor/rbxutil/find/package.json +17 -0
- package/vendor/rbxutil/find/wally.toml +8 -0
- package/vendor/rbxutil/input/Gamepad.luau +559 -0
- package/vendor/rbxutil/input/Keyboard.luau +124 -0
- package/vendor/rbxutil/input/Mouse.luau +278 -0
- package/vendor/rbxutil/input/PreferredInput.luau +91 -0
- package/vendor/rbxutil/input/Touch.luau +120 -0
- package/vendor/rbxutil/input/init.luau +33 -0
- package/vendor/rbxutil/input/wally.toml +12 -0
- package/vendor/rbxutil/loader/index.d.ts +15 -0
- package/vendor/rbxutil/loader/init.luau +137 -0
- package/vendor/rbxutil/loader/wally.toml +8 -0
- package/vendor/rbxutil/log/index.d.ts +38 -0
- package/vendor/rbxutil/log/init.luau +746 -0
- package/vendor/rbxutil/log/wally.toml +8 -0
- package/vendor/rbxutil/net/init.luau +190 -0
- package/vendor/rbxutil/net/wally.toml +8 -0
- package/vendor/rbxutil/option/index.d.ts +44 -0
- package/vendor/rbxutil/option/init.luau +489 -0
- package/vendor/rbxutil/option/init.test.luau +342 -0
- package/vendor/rbxutil/option/wally.toml +8 -0
- package/vendor/rbxutil/pid/index.d.ts +53 -0
- package/vendor/rbxutil/pid/init.luau +195 -0
- package/vendor/rbxutil/pid/package.json +16 -0
- package/vendor/rbxutil/pid/wally.toml +9 -0
- package/vendor/rbxutil/quaternion/index.d.ts +117 -0
- package/vendor/rbxutil/quaternion/init.luau +570 -0
- package/vendor/rbxutil/quaternion/package.json +16 -0
- package/vendor/rbxutil/quaternion/wally.toml +9 -0
- package/vendor/rbxutil/query/index.d.ts +43 -0
- package/vendor/rbxutil/query/init.luau +117 -0
- package/vendor/rbxutil/query/package.json +18 -0
- package/vendor/rbxutil/query/wally.toml +9 -0
- package/vendor/rbxutil/sequent/index.d.ts +28 -0
- package/vendor/rbxutil/sequent/init.luau +340 -0
- package/vendor/rbxutil/sequent/package.json +16 -0
- package/vendor/rbxutil/sequent/wally.toml +9 -0
- package/vendor/rbxutil/ser/init.luau +175 -0
- package/vendor/rbxutil/ser/init.test.luau +50 -0
- package/vendor/rbxutil/ser/wally.toml +11 -0
- package/vendor/rbxutil/shake/index.d.ts +36 -0
- package/vendor/rbxutil/shake/init.luau +532 -0
- package/vendor/rbxutil/shake/init.test.luau +267 -0
- package/vendor/rbxutil/shake/package.json +16 -0
- package/vendor/rbxutil/shake/wally.toml +9 -0
- package/vendor/rbxutil/signal/index.d.ts +100 -0
- package/vendor/rbxutil/signal/init.luau +432 -0
- package/vendor/rbxutil/signal/init.test.luau +190 -0
- package/vendor/rbxutil/signal/package.json +17 -0
- package/vendor/rbxutil/signal/wally.toml +9 -0
- package/vendor/rbxutil/silo/TableWatcher.luau +65 -0
- package/vendor/rbxutil/silo/Util.luau +55 -0
- package/vendor/rbxutil/silo/init.luau +338 -0
- package/vendor/rbxutil/silo/init.test.luau +215 -0
- package/vendor/rbxutil/silo/wally.toml +8 -0
- package/vendor/rbxutil/spring/index.d.ts +40 -0
- package/vendor/rbxutil/spring/init.luau +97 -0
- package/vendor/rbxutil/spring/package.json +17 -0
- package/vendor/rbxutil/spring/wally.toml +8 -0
- package/vendor/rbxutil/stream/index.d.ts +88 -0
- package/vendor/rbxutil/stream/init.luau +597 -0
- package/vendor/rbxutil/stream/package.json +18 -0
- package/vendor/rbxutil/stream/wally.toml +9 -0
- package/vendor/rbxutil/streamable/Streamable.luau +202 -0
- package/vendor/rbxutil/streamable/StreamableUtil.luau +80 -0
- package/vendor/rbxutil/streamable/init.luau +8 -0
- package/vendor/rbxutil/streamable/wally.toml +12 -0
- package/vendor/rbxutil/symbol/init.luau +56 -0
- package/vendor/rbxutil/symbol/init.test.luau +37 -0
- package/vendor/rbxutil/symbol/wally.toml +8 -0
- package/vendor/rbxutil/table-util/init.luau +938 -0
- package/vendor/rbxutil/table-util/init.test.luau +439 -0
- package/vendor/rbxutil/table-util/wally.toml +8 -0
- package/vendor/rbxutil/task-queue/index.d.ts +27 -0
- package/vendor/rbxutil/task-queue/init.luau +97 -0
- package/vendor/rbxutil/task-queue/wally.toml +8 -0
- package/vendor/rbxutil/timer/index.d.ts +81 -0
- package/vendor/rbxutil/timer/init.luau +249 -0
- package/vendor/rbxutil/timer/init.test.luau +73 -0
- package/vendor/rbxutil/timer/wally.toml +11 -0
- package/vendor/rbxutil/tree/index.d.ts +15 -0
- package/vendor/rbxutil/tree/init.luau +137 -0
- package/vendor/rbxutil/tree/wally.toml +8 -0
- package/vendor/rbxutil/trove/index.d.ts +46 -0
- package/vendor/rbxutil/trove/init.luau +787 -0
- package/vendor/rbxutil/trove/init.test.luau +203 -0
- package/vendor/rbxutil/trove/wally.toml +8 -0
- package/vendor/rbxutil/typed-remote/init.luau +196 -0
- package/vendor/rbxutil/typed-remote/wally.toml +8 -0
- package/vendor/rbxutil/wait-for/index.d.ts +17 -0
- package/vendor/rbxutil/wait-for/init.luau +257 -0
- package/vendor/rbxutil/wait-for/init.test.luau +182 -0
- package/vendor/rbxutil/wait-for/wally.toml +11 -0
- package/vendor/t/t.lua +1350 -0
- package/vendor/testez/Context.lua +26 -0
- package/vendor/testez/Expectation.lua +311 -0
- package/vendor/testez/ExpectationContext.lua +38 -0
- package/vendor/testez/LifecycleHooks.lua +89 -0
- package/vendor/testez/Reporters/TeamCityReporter.lua +102 -0
- package/vendor/testez/Reporters/TextReporter.lua +106 -0
- package/vendor/testez/Reporters/TextReporterQuiet.lua +97 -0
- package/vendor/testez/TestBootstrap.lua +147 -0
- package/vendor/testez/TestEnum.lua +28 -0
- package/vendor/testez/TestPlan.lua +304 -0
- package/vendor/testez/TestPlanner.lua +40 -0
- package/vendor/testez/TestResults.lua +112 -0
- package/vendor/testez/TestRunner.lua +188 -0
- package/vendor/testez/TestSession.lua +243 -0
- package/vendor/testez/init.lua +40 -0
|
@@ -0,0 +1,863 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: roblox-architecture
|
|
3
|
+
description: Service hierarchy, 7 foundational patterns, cross-platform input. Client-server architecture, module patterns, framework options.
|
|
4
|
+
last_reviewed: 2026-05-21
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<!-- Source: brockmartin/roblox-game-skill (MIT) -->
|
|
8
|
+
|
|
9
|
+
# Roblox Game Architecture Reference
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 1. Overview
|
|
14
|
+
|
|
15
|
+
**Load this reference when:**
|
|
16
|
+
|
|
17
|
+
- Starting a new Roblox game from scratch and need to decide where code and assets go
|
|
18
|
+
- Organizing or refactoring an existing codebase that has grown unwieldy
|
|
19
|
+
- Answering architecture questions: "Where should this script go?", "How should client talk to server?", "How do I structure modules?"
|
|
20
|
+
- Onboarding onto a Roblox project and need to understand the standard conventions
|
|
21
|
+
- Choosing between a flat script layout and a modular loader pattern
|
|
22
|
+
|
|
23
|
+
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.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Quick Reference
|
|
28
|
+
|
|
29
|
+
**Load Full Reference below only when you need specific folder layouts or framework comparisons.**
|
|
30
|
+
|
|
31
|
+
Key rules:
|
|
32
|
+
- ServerScriptService: server logic (never visible to client)
|
|
33
|
+
- ServerStorage: server-only assets/data
|
|
34
|
+
- ReplicatedStorage: shared modules, RemoteEvents, assets both sides need
|
|
35
|
+
- StarterPlayerScripts: client controllers (run once per player)
|
|
36
|
+
- StarterGui: ScreenGuis (cloned to PlayerGui on spawn)
|
|
37
|
+
- Script types: Script (server), LocalScript (client), ModuleScript (shared, returns one table)
|
|
38
|
+
- Communication: RemoteEvent (fire-and-forget), RemoteFunction (request-response, avoid for client→server)
|
|
39
|
+
- Module pattern: return a table of functions. One module = one responsibility.
|
|
40
|
+
- Avoid circular requires. Use events/signals for cross-module communication.
|
|
41
|
+
- Single entry point per side: one server Script requires service modules, one LocalScript requires controllers.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Full Reference
|
|
46
|
+
|
|
47
|
+
## 2. Core Concepts
|
|
48
|
+
|
|
49
|
+
### The Data Model
|
|
50
|
+
|
|
51
|
+
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).
|
|
52
|
+
|
|
53
|
+
### Client vs. Server Execution
|
|
54
|
+
|
|
55
|
+
| Aspect | Server | Client |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| **Runs on** | Roblox datacenter (one instance per game server) | Each player's device |
|
|
58
|
+
| **Script types** | `Script` (Luau), `ModuleScript` (when required by a server script) | `LocalScript`, `ModuleScript` (when required by a local script) |
|
|
59
|
+
| **Trust level** | Authoritative -- owns game state, data stores, physics arbitration | Untrusted -- can be exploited; never trust client input blindly |
|
|
60
|
+
| **Access** | Can see everything in the data model | Cannot see `ServerScriptService` or `ServerStorage` |
|
|
61
|
+
|
|
62
|
+
### Replication Model
|
|
63
|
+
|
|
64
|
+
Roblox automatically replicates (syncs) parts of the data model from server to clients:
|
|
65
|
+
|
|
66
|
+
- **Server to all clients:** Anything in `Workspace`, `ReplicatedStorage`, `ReplicatedFirst`, `Lighting`, `SoundService`, `Chat`, and `Teams` is visible to every connected client.
|
|
67
|
+
- **Server-only (hidden from clients):** `ServerScriptService` and `ServerStorage` are never sent to clients. This is the correct place for server logic and secret assets.
|
|
68
|
+
- **Per-player:** Each player gets their own `PlayerGui` (cloned from `StarterGui`), `PlayerScripts` (cloned from `StarterPlayerScripts`), `StarterGear` (cloned from `StarterPack`), and `Backpack`.
|
|
69
|
+
- **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.
|
|
70
|
+
|
|
71
|
+
**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).
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 3. Service Hierarchy
|
|
76
|
+
|
|
77
|
+
### ServerScriptService
|
|
78
|
+
|
|
79
|
+
**Purpose:** Contains `Script` instances that run exclusively on the server. Clients cannot see or access anything inside.
|
|
80
|
+
|
|
81
|
+
**Use for:**
|
|
82
|
+
- Core game logic (round systems, match management, scoring)
|
|
83
|
+
- Data persistence (DataStoreService calls)
|
|
84
|
+
- Server-side validation of player actions
|
|
85
|
+
- Anti-cheat enforcement
|
|
86
|
+
- NPC/AI controllers
|
|
87
|
+
- Server-side modules required only by server scripts
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
ServerScriptService/
|
|
91
|
+
GameManager.server.lua -- Script: round system
|
|
92
|
+
DataManager.server.lua -- Script: save/load player data
|
|
93
|
+
Modules/
|
|
94
|
+
CombatService.lua -- ModuleScript: damage calculations
|
|
95
|
+
ShopService.lua -- ModuleScript: purchase validation
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### ServerStorage
|
|
99
|
+
|
|
100
|
+
**Purpose:** A server-only container for assets and modules that clients should never see or download.
|
|
101
|
+
|
|
102
|
+
**Use for:**
|
|
103
|
+
- Map models that get cloned into Workspace on demand
|
|
104
|
+
- Enemy/NPC models before spawning
|
|
105
|
+
- Server-only ModuleScripts (utility libraries, data schemas)
|
|
106
|
+
- Templates that should not exist on the client until needed
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
ServerStorage/
|
|
110
|
+
Maps/
|
|
111
|
+
DesertArena.rbxm
|
|
112
|
+
ForestArena.rbxm
|
|
113
|
+
Templates/
|
|
114
|
+
Loot/
|
|
115
|
+
CommonChest.rbxm
|
|
116
|
+
Modules/
|
|
117
|
+
DataSchema.lua
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### ReplicatedStorage
|
|
121
|
+
|
|
122
|
+
**Purpose:** Shared container visible to both server and client. Content is replicated to every client on join.
|
|
123
|
+
|
|
124
|
+
**Use for:**
|
|
125
|
+
- `RemoteEvent` and `RemoteFunction` instances (the communication bridge)
|
|
126
|
+
- Shared `ModuleScript` modules (utilities, constants, types, shared classes)
|
|
127
|
+
- Assets both sides reference (item models, particle effects, shared animations)
|
|
128
|
+
- Configuration values / game settings that both sides need
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
ReplicatedStorage/
|
|
132
|
+
Remotes/
|
|
133
|
+
DamageEvent.RemoteEvent
|
|
134
|
+
ShopPurchase.RemoteFunction
|
|
135
|
+
Modules/
|
|
136
|
+
ItemData.lua -- shared item definitions
|
|
137
|
+
MathUtils.lua -- shared utility functions
|
|
138
|
+
Types.lua -- shared type definitions
|
|
139
|
+
Assets/
|
|
140
|
+
Effects/
|
|
141
|
+
HitEffect.rbxm
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### ReplicatedFirst
|
|
145
|
+
|
|
146
|
+
**Purpose:** Scripts here run on the client **before** anything else loads. Content replicates to clients first.
|
|
147
|
+
|
|
148
|
+
**Use for:**
|
|
149
|
+
- Loading screens (show UI while the game streams in)
|
|
150
|
+
- Early client initialization
|
|
151
|
+
- Keeping it minimal -- only what is needed before the game fully loads
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
ReplicatedFirst/
|
|
155
|
+
LoadingScreen.client.lua -- LocalScript: shows loading UI
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### StarterGui
|
|
159
|
+
|
|
160
|
+
**Purpose:** UI template container. On each player spawn (or respawn, depending on `ResetOnSpawn`), the contents are **cloned** into that player's `PlayerGui`.
|
|
161
|
+
|
|
162
|
+
**Use for:**
|
|
163
|
+
- HUD elements (health bars, score displays, minimaps)
|
|
164
|
+
- Menu screens (settings, inventory, shop)
|
|
165
|
+
- UI-controlling LocalScripts that live inside ScreenGui
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
StarterGui/
|
|
169
|
+
HUD.ScreenGui
|
|
170
|
+
HealthBar.Frame
|
|
171
|
+
ScoreLabel.TextLabel
|
|
172
|
+
HUDController.client.lua -- LocalScript managing HUD updates
|
|
173
|
+
ShopMenu.ScreenGui
|
|
174
|
+
ShopController.client.lua
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
> **Note:** Set `ScreenGui.ResetOnSpawn = false` for persistent UI that should not re-clone on character respawn.
|
|
178
|
+
|
|
179
|
+
### StarterPlayer / StarterPlayerScripts
|
|
180
|
+
|
|
181
|
+
**Purpose:** `LocalScript` instances here are cloned into each player's `PlayerScripts` folder once on join. They persist across respawns.
|
|
182
|
+
|
|
183
|
+
**Use for:**
|
|
184
|
+
- Camera controllers
|
|
185
|
+
- Input handling systems
|
|
186
|
+
- Client-side game managers
|
|
187
|
+
- Music/ambient sound controllers
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
StarterPlayer/
|
|
191
|
+
StarterPlayerScripts/
|
|
192
|
+
CameraController.client.lua
|
|
193
|
+
InputManager.client.lua
|
|
194
|
+
ClientBootstrap.client.lua
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### StarterPlayer / StarterCharacterScripts
|
|
198
|
+
|
|
199
|
+
**Purpose:** Scripts here are cloned into the player's `Character` model each time the character spawns. They are destroyed when the character dies.
|
|
200
|
+
|
|
201
|
+
**Use for:**
|
|
202
|
+
- Per-character behaviors (footstep sounds, animation controllers)
|
|
203
|
+
- Character-specific effects (trails, auras)
|
|
204
|
+
- Anything that should reset on death
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
StarterPlayer/
|
|
208
|
+
StarterCharacterScripts/
|
|
209
|
+
FootstepSounds.client.lua
|
|
210
|
+
AnimationController.client.lua
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### StarterPack
|
|
214
|
+
|
|
215
|
+
**Purpose:** `Tool` instances here are cloned into each player's `Backpack` on spawn.
|
|
216
|
+
|
|
217
|
+
**Use for:**
|
|
218
|
+
- Default weapons or items every player starts with
|
|
219
|
+
- Tools with embedded LocalScripts and Scripts
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
StarterPack/
|
|
223
|
+
Sword.Tool
|
|
224
|
+
SwordClient.client.lua
|
|
225
|
+
SwordServer.server.lua
|
|
226
|
+
Handle.Part
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Workspace
|
|
230
|
+
|
|
231
|
+
**Purpose:** The 3D world. Everything visible in the game exists here at runtime: parts, models, terrain, cameras.
|
|
232
|
+
|
|
233
|
+
**Use for:**
|
|
234
|
+
- The physical game world (terrain, buildings, decorations)
|
|
235
|
+
- Spawned entities at runtime (cloned from ServerStorage)
|
|
236
|
+
- The Camera (each client has a local `Workspace.CurrentCamera`)
|
|
237
|
+
|
|
238
|
+
**Do NOT place Scripts directly in Workspace.** Use `ServerScriptService` instead. Workspace scripts are accessible to exploiters and create organizational chaos.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## 4. Script Types
|
|
243
|
+
|
|
244
|
+
### Script (Server Script)
|
|
245
|
+
|
|
246
|
+
- **Runs on:** Server (default RunContext)
|
|
247
|
+
- **Valid containers:** `ServerScriptService`, `ServerStorage` (when parented under certain conditions), `Workspace` (discouraged)
|
|
248
|
+
- **File convention:** `*.server.lua` in Rojo projects
|
|
249
|
+
- **Access:** Full access to server APIs (`DataStoreService`, `MessagingService`, `HttpService`, etc.)
|
|
250
|
+
|
|
251
|
+
```lua
|
|
252
|
+
-- ServerScriptService/GameManager.server.lua
|
|
253
|
+
local Players = game:GetService("Players")
|
|
254
|
+
|
|
255
|
+
Players.PlayerAdded:Connect(function(player)
|
|
256
|
+
print(`{player.Name} joined the server`)
|
|
257
|
+
end)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### RunContext (Script Behavior Override)
|
|
261
|
+
|
|
262
|
+
By default, `Script` runs on the server. Setting `Script.RunContext` changes this:
|
|
263
|
+
|
|
264
|
+
- **`Enum.RunContext.Server`** (default) - runs on the server, only in server-valid containers
|
|
265
|
+
- **`Enum.RunContext.Client`** - runs on the client, can be placed ANYWHERE (Workspace, ReplicatedStorage, etc.)
|
|
266
|
+
|
|
267
|
+
`RunContext = Client` is powerful for workspace-local effects, proximity scripts, or anything the client sees but doesn't belong in StarterPlayerScripts:
|
|
268
|
+
|
|
269
|
+
```lua
|
|
270
|
+
-- A Script in Workspace with RunContext = Client
|
|
271
|
+
-- Runs on every client, perfect for local visual effects
|
|
272
|
+
local part = script.Parent
|
|
273
|
+
local RunService = game:GetService("RunService")
|
|
274
|
+
|
|
275
|
+
RunService.Heartbeat:Connect(function(dt)
|
|
276
|
+
part.CFrame *= CFrame.Angles(0, math.rad(30 * dt), 0)
|
|
277
|
+
end)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
> **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.
|
|
281
|
+
|
|
282
|
+
### LocalScript (Client Script)
|
|
283
|
+
|
|
284
|
+
- **Runs on:** Client (the specific player's device)
|
|
285
|
+
- **Valid containers:** `StarterPlayerScripts`, `StarterCharacterScripts`, `StarterGui`, `StarterPack`, a player's `Backpack`, `PlayerGui`, `PlayerScripts`, or `Character`
|
|
286
|
+
- **File convention:** `*.client.lua` in Rojo projects
|
|
287
|
+
- **Access:** Client APIs (`UserInputService`, `ContextActionService`, `Camera`, local player's GUI)
|
|
288
|
+
|
|
289
|
+
```lua
|
|
290
|
+
-- StarterPlayerScripts/InputManager.client.lua
|
|
291
|
+
local UserInputService = game:GetService("UserInputService")
|
|
292
|
+
|
|
293
|
+
UserInputService.InputBegan:Connect(function(input, gameProcessed)
|
|
294
|
+
if gameProcessed then return end
|
|
295
|
+
if input.KeyCode == Enum.KeyCode.E then
|
|
296
|
+
print("Player pressed E")
|
|
297
|
+
end
|
|
298
|
+
end)
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### ModuleScript
|
|
302
|
+
|
|
303
|
+
- **Runs on:** Whichever side `require()`s it (server if required by a Script, client if required by a LocalScript)
|
|
304
|
+
- **Valid containers:** Anywhere, but location determines who can access it
|
|
305
|
+
- `ServerScriptService` or `ServerStorage` -- server-only modules
|
|
306
|
+
- `ReplicatedStorage` -- shared modules (accessible by both sides)
|
|
307
|
+
- `StarterPlayerScripts` -- client-only modules
|
|
308
|
+
- **File convention:** `*.lua` (no `.server` or `.client` suffix) in Rojo projects
|
|
309
|
+
- **Returns:** Exactly one value (typically a table/dictionary acting as a module)
|
|
310
|
+
|
|
311
|
+
```lua
|
|
312
|
+
-- ReplicatedStorage/Modules/MathUtils.lua
|
|
313
|
+
local MathUtils = {}
|
|
314
|
+
|
|
315
|
+
function MathUtils.lerp(a: number, b: number, t: number): number
|
|
316
|
+
return a + (b - a) * t
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
function MathUtils.clamp(value: number, min: number, max: number): number
|
|
320
|
+
return math.max(min, math.min(max, value))
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
return MathUtils
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Requiring:**
|
|
327
|
+
```lua
|
|
328
|
+
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
329
|
+
local MathUtils = require(ReplicatedStorage.Modules.MathUtils)
|
|
330
|
+
|
|
331
|
+
local result = MathUtils.lerp(0, 100, 0.5) -- 50
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## 5. Client-Server Communication
|
|
337
|
+
|
|
338
|
+
For implementation details (RemoteEvent, RemoteFunction, UnreliableRemoteEvent, BindableEvent, validation patterns), see **roblox-networking** → Client-Server Communication.
|
|
339
|
+
|
|
340
|
+
**Conceptual overview:**
|
|
341
|
+
|
|
342
|
+
| Type | Direction | Blocking? | Use Case |
|
|
343
|
+
|---|---|---|---|
|
|
344
|
+
| `RemoteEvent` | Client ↔ Server | No | Actions, notifications, state changes |
|
|
345
|
+
| `RemoteFunction` | Client → Server only (safe) | Yes (yields) | Data requests, queries |
|
|
346
|
+
| `BindableEvent` | Same side | N/A (local) | Decoupled same-side messaging |
|
|
347
|
+
| `UnreliableRemoteEvent` | Client ↔ Server | No | High-frequency cosmetic data |
|
|
348
|
+
|
|
349
|
+
**Core rules:**
|
|
350
|
+
- Server is authoritative. Never trust client input.
|
|
351
|
+
- Use `RemoteEvent` for fire-and-forget. Use `RemoteFunction` only when the caller needs a return value.
|
|
352
|
+
- Never use `RemoteFunction:InvokeClient()` - if the client errors or disconnects, the server thread hangs forever.
|
|
353
|
+
- `UnreliableRemoteEvent` for cosmetic data only (cursor position, facing direction). Never for damage, purchases, or state.
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## 6. Module Script Architecture
|
|
358
|
+
|
|
359
|
+
### Basic Module Pattern
|
|
360
|
+
|
|
361
|
+
Every ModuleScript returns a single table. Functions and data are fields on that table.
|
|
362
|
+
|
|
363
|
+
```lua
|
|
364
|
+
-- ReplicatedStorage/Modules/InventoryModule.lua
|
|
365
|
+
local InventoryModule = {}
|
|
366
|
+
|
|
367
|
+
local playerInventories: { [Player]: { [string]: number } } = {}
|
|
368
|
+
|
|
369
|
+
function InventoryModule.init()
|
|
370
|
+
-- Called once during startup to wire up connections
|
|
371
|
+
game:GetService("Players").PlayerRemoving:Connect(function(player)
|
|
372
|
+
playerInventories[player] = nil
|
|
373
|
+
end)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
function InventoryModule.addItem(player: Player, itemId: string, quantity: number)
|
|
377
|
+
local inv = playerInventories[player]
|
|
378
|
+
if not inv then
|
|
379
|
+
inv = {}
|
|
380
|
+
playerInventories[player] = inv
|
|
381
|
+
end
|
|
382
|
+
inv[itemId] = (inv[itemId] or 0) + quantity
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
function InventoryModule.getItem(player: Player, itemId: string): number
|
|
386
|
+
local inv = playerInventories[player]
|
|
387
|
+
if not inv then return 0 end
|
|
388
|
+
return inv[itemId] or 0
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
function InventoryModule.getAll(player: Player): { [string]: number }
|
|
392
|
+
return playerInventories[player] or {}
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
return InventoryModule
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### The `require()` Pattern
|
|
399
|
+
|
|
400
|
+
```lua
|
|
401
|
+
-- ServerScriptService/Main.server.lua
|
|
402
|
+
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
403
|
+
local ServerStorage = game:GetService("ServerStorage")
|
|
404
|
+
|
|
405
|
+
-- Shared modules (both sides can require these)
|
|
406
|
+
local ItemData = require(ReplicatedStorage.Modules.ItemData)
|
|
407
|
+
|
|
408
|
+
-- Server-only modules
|
|
409
|
+
local InventoryModule = require(ServerStorage.Modules.InventoryModule)
|
|
410
|
+
|
|
411
|
+
-- Initialize modules that need setup
|
|
412
|
+
InventoryModule.init()
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
**Key facts about `require()`:**
|
|
416
|
+
- The module runs **once**. Subsequent `require()` calls return the cached result.
|
|
417
|
+
- The module runs in the **context** of the first requirer (server or client).
|
|
418
|
+
- 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.
|
|
419
|
+
|
|
420
|
+
### Avoiding Circular Dependencies
|
|
421
|
+
|
|
422
|
+
Circular dependencies occur when Module A requires Module B, and Module B requires Module A. This causes an infinite loop or returns `nil`.
|
|
423
|
+
|
|
424
|
+
**Problem:**
|
|
425
|
+
```lua
|
|
426
|
+
-- ModuleA.lua
|
|
427
|
+
local ModuleB = require(script.Parent.ModuleB) -- ModuleB hasn't finished loading
|
|
428
|
+
-- ...
|
|
429
|
+
return ModuleA
|
|
430
|
+
|
|
431
|
+
-- ModuleB.lua
|
|
432
|
+
local ModuleA = require(script.Parent.ModuleA) -- ModuleA hasn't finished loading
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
**Solution 1: Deferred initialization with `init()` pattern**
|
|
436
|
+
```lua
|
|
437
|
+
-- ModuleA.lua
|
|
438
|
+
local ModuleA = {}
|
|
439
|
+
local ModuleB -- forward declaration
|
|
440
|
+
|
|
441
|
+
function ModuleA.init(modules)
|
|
442
|
+
ModuleB = modules.ModuleB
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
function ModuleA.doSomething()
|
|
446
|
+
ModuleB.helper()
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
return ModuleA
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
```lua
|
|
453
|
+
-- Main.server.lua (the bootstrapper)
|
|
454
|
+
local ModuleA = require(path.to.ModuleA)
|
|
455
|
+
local ModuleB = require(path.to.ModuleB)
|
|
456
|
+
|
|
457
|
+
-- Wire up cross-references after all modules are loaded
|
|
458
|
+
local modules = { ModuleA = ModuleA, ModuleB = ModuleB }
|
|
459
|
+
ModuleA.init(modules)
|
|
460
|
+
ModuleB.init(modules)
|
|
461
|
+
|
|
462
|
+
-- Now start the game
|
|
463
|
+
ModuleA.start()
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
**Solution 2: Dependency inversion -- extract shared logic into a third module**
|
|
467
|
+
|
|
468
|
+
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.
|
|
469
|
+
|
|
470
|
+
**Solution 3: Event-driven decoupling with BindableEvents**
|
|
471
|
+
|
|
472
|
+
Instead of calling directly into another module, fire a BindableEvent that the other module listens to. Neither module requires the other.
|
|
473
|
+
|
|
474
|
+
### OOP Module Pattern (Class-based)
|
|
475
|
+
|
|
476
|
+
For metatable-based classes, type annotations, inheritance, and the `.` vs `:` conventions, see **roblox-luau-mastery** → OOP Patterns.
|
|
477
|
+
|
|
478
|
+
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()`.
|
|
479
|
+
|
|
480
|
+
```lua
|
|
481
|
+
-- ReplicatedStorage/Modules/Weapon.lua
|
|
482
|
+
local Weapon = {}
|
|
483
|
+
Weapon.__index = Weapon
|
|
484
|
+
|
|
485
|
+
export type Weapon = typeof(setmetatable({} :: {
|
|
486
|
+
name: string,
|
|
487
|
+
damage: number,
|
|
488
|
+
cooldown: number,
|
|
489
|
+
_lastFired: number,
|
|
490
|
+
}, Weapon))
|
|
491
|
+
|
|
492
|
+
function Weapon.new(name: string, damage: number, cooldown: number): Weapon
|
|
493
|
+
local self = setmetatable({}, Weapon)
|
|
494
|
+
self.name = name
|
|
495
|
+
self.damage = damage
|
|
496
|
+
self.cooldown = cooldown
|
|
497
|
+
self._lastFired = 0
|
|
498
|
+
return self
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
function Weapon:canFire(): boolean
|
|
502
|
+
return (os.clock() - self._lastFired) >= self.cooldown
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
function Weapon:fire(): number?
|
|
506
|
+
if not self:canFire() then return nil end
|
|
507
|
+
self._lastFired = os.clock()
|
|
508
|
+
return self.damage
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
return Weapon
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
```lua
|
|
515
|
+
-- Usage
|
|
516
|
+
local Weapon = require(ReplicatedStorage.Modules.Weapon)
|
|
517
|
+
local sword = Weapon.new("Iron Sword", 25, 0.8)
|
|
518
|
+
|
|
519
|
+
if sword:canFire() then
|
|
520
|
+
local dmg = sword:fire()
|
|
521
|
+
end
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
## 7. Framework Patterns
|
|
527
|
+
|
|
528
|
+
### Single Script Architecture (SSA)
|
|
529
|
+
|
|
530
|
+
One Script on the server, one LocalScript on the client. Each requires a "loader" module that initializes all other modules in order.
|
|
531
|
+
|
|
532
|
+
```lua
|
|
533
|
+
-- ServerScriptService/Main.server.lua
|
|
534
|
+
require(game:GetService("ServerStorage").Loader)
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
```lua
|
|
538
|
+
-- ServerStorage/Loader.lua
|
|
539
|
+
local Loader = {}
|
|
540
|
+
|
|
541
|
+
local modules = {
|
|
542
|
+
require(script.Parent.Modules.DataService),
|
|
543
|
+
require(script.Parent.Modules.CombatService),
|
|
544
|
+
require(script.Parent.Modules.ShopService),
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
-- Init phase (no cross-dependencies yet)
|
|
548
|
+
for _, mod in modules do
|
|
549
|
+
if mod.init then mod:init() end
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
-- Start phase (all modules available)
|
|
553
|
+
for _, mod in modules do
|
|
554
|
+
if mod.start then mod:start() end
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
return Loader
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
### Comparison
|
|
561
|
+
|
|
562
|
+
| Aspect | No Framework (Manual) | Single Script Architecture |
|
|
563
|
+
|---|---|---|
|
|
564
|
+
| **Complexity** | Low | Medium |
|
|
565
|
+
| **Boilerplate** | Minimal | Low |
|
|
566
|
+
| **Networking** | Manual RemoteEvent setup | Manual |
|
|
567
|
+
| **Learning curve** | Just Roblox APIs | Small pattern to learn |
|
|
568
|
+
| **Scalability** | Degrades without discipline | Good with init/start pattern |
|
|
569
|
+
| **Dependency management** | Manual require chains | Centralized loader |
|
|
570
|
+
| **Best for** | Jams, small projects, learning | Medium projects, teams wanting control |
|
|
571
|
+
|
|
572
|
+
---
|
|
573
|
+
|
|
574
|
+
## 8. Folder Organization
|
|
575
|
+
|
|
576
|
+
### Small Game (Simple / Flat)
|
|
577
|
+
|
|
578
|
+
For game jams, prototypes, or games with under ~10 scripts.
|
|
579
|
+
|
|
580
|
+
```
|
|
581
|
+
game
|
|
582
|
+
+-- ServerScriptService
|
|
583
|
+
|| +-- GameManager.server.lua
|
|
584
|
+
|| +-- DataHandler.server.lua
|
|
585
|
+
+-- ServerStorage
|
|
586
|
+
|| +-- Maps/
|
|
587
|
+
+-- ReplicatedStorage
|
|
588
|
+
|| +-- Remotes/
|
|
589
|
+
|| | +-- DamageEvent (RemoteEvent)
|
|
590
|
+
|| | +-- ShopPurchase (RemoteFunction)
|
|
591
|
+
|| +-- SharedConfig.lua (ModuleScript)
|
|
592
|
+
+-- ReplicatedFirst
|
|
593
|
+
|| +-- LoadingScreen.client.lua
|
|
594
|
+
+-- StarterGui
|
|
595
|
+
|| +-- HUD (ScreenGui)
|
|
596
|
+
+-- StarterPlayer
|
|
597
|
+
|| +-- StarterPlayerScripts
|
|
598
|
+
|| +-- CameraController.client.lua
|
|
599
|
+
+-- Workspace
|
|
600
|
+
+-- Map/
|
|
601
|
+
+-- SpawnLocations/
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
### Medium Game (Modular)
|
|
605
|
+
|
|
606
|
+
Organized into feature folders. Each feature has its own server module, client module, and shared definitions.
|
|
607
|
+
|
|
608
|
+
```
|
|
609
|
+
game
|
|
610
|
+
+-- ServerScriptService
|
|
611
|
+
|| +-- Main.server.lua -- Bootstrapper: requires and inits all modules
|
|
612
|
+
|| +-- Services/
|
|
613
|
+
|| | +-- CombatService.lua
|
|
614
|
+
|| | +-- DataService.lua
|
|
615
|
+
|| | +-- ShopService.lua
|
|
616
|
+
|| | +-- RoundService.lua
|
|
617
|
+
|| +-- Components/
|
|
618
|
+
|| +-- LootDrop.lua
|
|
619
|
+
|| +-- DoorSystem.lua
|
|
620
|
+
+-- ServerStorage
|
|
621
|
+
|| +-- Assets/
|
|
622
|
+
|| | +-- Maps/
|
|
623
|
+
|| | +-- NPCs/
|
|
624
|
+
|| +-- Modules/
|
|
625
|
+
|| +-- DataSchema.lua
|
|
626
|
+
+-- ReplicatedStorage
|
|
627
|
+
|| +-- Remotes/ -- All RemoteEvents/Functions here
|
|
628
|
+
|| +-- Modules/
|
|
629
|
+
|| | +-- ItemData.lua
|
|
630
|
+
|| | +-- Constants.lua
|
|
631
|
+
|| | +-- Types.lua
|
|
632
|
+
|| | +-- MathUtils.lua
|
|
633
|
+
|| +-- Assets/
|
|
634
|
+
|| +-- Effects/
|
|
635
|
+
|| +-- Animations/
|
|
636
|
+
+-- ReplicatedFirst
|
|
637
|
+
|| +-- LoadingScreen.client.lua
|
|
638
|
+
+-- StarterGui
|
|
639
|
+
|| +-- HUD (ScreenGui)
|
|
640
|
+
|| +-- ShopMenu (ScreenGui)
|
|
641
|
+
|| +-- SettingsMenu (ScreenGui)
|
|
642
|
+
+-- StarterPlayer
|
|
643
|
+
|| +-- StarterPlayerScripts
|
|
644
|
+
|| +-- ClientMain.client.lua -- Client bootstrapper
|
|
645
|
+
|| +-- Controllers/
|
|
646
|
+
|| +-- CameraController.lua
|
|
647
|
+
|| +-- InputController.lua
|
|
648
|
+
|| +-- UIController.lua
|
|
649
|
+
+-- Workspace
|
|
650
|
+
+-- World/
|
|
651
|
+
+-- Lighting/
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
### Large Game (Framework-Based with Rojo)
|
|
655
|
+
|
|
656
|
+
Uses a loader pattern (SSA), Rojo for file sync, and a strict folder convention. File system mirrors the Roblox hierarchy.
|
|
657
|
+
|
|
658
|
+
```
|
|
659
|
+
src/
|
|
660
|
+
+-- server/ --> syncs to ServerScriptService
|
|
661
|
+
|| +-- Main.server.lua -- Bootstrapper
|
|
662
|
+
|| +-- services/
|
|
663
|
+
|| | +-- PlayerDataService.lua
|
|
664
|
+
|| | +-- CombatService.lua
|
|
665
|
+
|| | +-- EconomyService.lua
|
|
666
|
+
|| | +-- MatchService.lua
|
|
667
|
+
|| | +-- LeaderboardService.lua
|
|
668
|
+
|| +-- components/
|
|
669
|
+
|| +-- Destructible.lua
|
|
670
|
+
|| +-- Interactable.lua
|
|
671
|
+
+-- client/ --> syncs to StarterPlayerScripts
|
|
672
|
+
|| +-- ClientMain.client.lua -- Client bootstrapper
|
|
673
|
+
|| +-- controllers/
|
|
674
|
+
|| | +-- InputController.lua
|
|
675
|
+
|| | +-- CameraController.lua
|
|
676
|
+
|| | +-- UIController.lua
|
|
677
|
+
|| | +-- SoundController.lua
|
|
678
|
+
|| +-- ui/
|
|
679
|
+
|| +-- components/
|
|
680
|
+
|| | +-- Button.lua
|
|
681
|
+
|| | +-- HealthBar.lua
|
|
682
|
+
|| +-- screens/
|
|
683
|
+
|| +-- ShopScreen.lua
|
|
684
|
+
|| +-- InventoryScreen.lua
|
|
685
|
+
+-- shared/ --> syncs to ReplicatedStorage
|
|
686
|
+
|| +-- modules/
|
|
687
|
+
|| | +-- ItemData.lua
|
|
688
|
+
|| | +-- Constants.lua
|
|
689
|
+
|| | +-- Types.lua
|
|
690
|
+
|| | +-- Util.lua
|
|
691
|
+
|| +-- network/
|
|
692
|
+
|| | +-- Remotes.lua -- Central remote definitions
|
|
693
|
+
|| +-- assets/
|
|
694
|
+
+-- serverStorage/ --> syncs to ServerStorage
|
|
695
|
+
|| +-- assets/
|
|
696
|
+
|| | +-- maps/
|
|
697
|
+
|| | +-- npcs/
|
|
698
|
+
|| +-- modules/
|
|
699
|
+
|| +-- DataSchema.lua
|
|
700
|
+
+-- starterGui/ --> syncs to StarterGui
|
|
701
|
+
|| +-- HUD.gui.lua
|
|
702
|
+
+-- first/ --> syncs to ReplicatedFirst
|
|
703
|
+
+-- Loading.client.lua
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
---
|
|
707
|
+
|
|
708
|
+
## 9. Best Practices
|
|
709
|
+
|
|
710
|
+
### Separation of Concerns
|
|
711
|
+
|
|
712
|
+
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.
|
|
713
|
+
|
|
714
|
+
### Single Responsibility
|
|
715
|
+
|
|
716
|
+
One module = one job. If a module handles inventory AND crafting AND trading, split it into `InventoryModule`, `CraftingModule`, and `TradingModule`.
|
|
717
|
+
|
|
718
|
+
### Minimal Coupling
|
|
719
|
+
|
|
720
|
+
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.
|
|
721
|
+
|
|
722
|
+
### Server Owns State
|
|
723
|
+
|
|
724
|
+
The server is the **single source of truth** for all game state. Clients render and predict, but the server validates and authorizes.
|
|
725
|
+
|
|
726
|
+
### Validate All Client Input
|
|
727
|
+
|
|
728
|
+
For implementation details (type checking, range checking, ownership, rate limiting), see **roblox-networking** → Client Validation.
|
|
729
|
+
|
|
730
|
+
**Core rules:**
|
|
731
|
+
- Every `OnServerEvent` handler must validate types, ranges, and ownership before processing.
|
|
732
|
+
- Never let the client set currency, health, or any authoritative value directly.
|
|
733
|
+
- Client sends intent ("I attacked target X"), server calculates outcome.
|
|
734
|
+
|
|
735
|
+
### Use ModuleScripts Everywhere
|
|
736
|
+
|
|
737
|
+
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.
|
|
738
|
+
|
|
739
|
+
### Centralize Remote Definitions
|
|
740
|
+
|
|
741
|
+
Create all RemoteEvents and RemoteFunctions in one place rather than scattering `Instance.new("RemoteEvent")` across multiple scripts.
|
|
742
|
+
|
|
743
|
+
```lua
|
|
744
|
+
-- ServerScriptService/CreateRemotes.server.lua (runs first)
|
|
745
|
+
local remoteFolder = Instance.new("Folder")
|
|
746
|
+
remoteFolder.Name = "Remotes"
|
|
747
|
+
remoteFolder.Parent = game:GetService("ReplicatedStorage")
|
|
748
|
+
|
|
749
|
+
local remotes = {
|
|
750
|
+
"DamageEvent",
|
|
751
|
+
"ShopPurchase",
|
|
752
|
+
"ChatMessage",
|
|
753
|
+
"PlayerReady",
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
for _, name in remotes do
|
|
757
|
+
local remote = Instance.new("RemoteEvent")
|
|
758
|
+
remote.Name = name
|
|
759
|
+
remote.Parent = remoteFolder
|
|
760
|
+
end
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
### Use Types and Constants
|
|
764
|
+
|
|
765
|
+
Define shared constants and Luau types in `ReplicatedStorage` so both sides use the same definitions.
|
|
766
|
+
|
|
767
|
+
```lua
|
|
768
|
+
-- ReplicatedStorage/Modules/Constants.lua
|
|
769
|
+
local Constants = {
|
|
770
|
+
MAX_HEALTH = 100,
|
|
771
|
+
WALK_SPEED = 16,
|
|
772
|
+
SPRINT_SPEED = 24,
|
|
773
|
+
MAX_INVENTORY_SLOTS = 20,
|
|
774
|
+
INTERACTION_RANGE = 10,
|
|
775
|
+
|
|
776
|
+
ItemRarity = {
|
|
777
|
+
Common = 1,
|
|
778
|
+
Uncommon = 2,
|
|
779
|
+
Rare = 3,
|
|
780
|
+
Epic = 4,
|
|
781
|
+
Legendary = 5,
|
|
782
|
+
},
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return Constants
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
---
|
|
789
|
+
|
|
790
|
+
## 10. Anti-Patterns
|
|
791
|
+
|
|
792
|
+
### God Scripts
|
|
793
|
+
|
|
794
|
+
**Problem:** One massive script (500+ lines) that handles spawning, combat, data, UI, and everything else. Impossible to debug, modify, or collaborate on.
|
|
795
|
+
|
|
796
|
+
**Fix:** Break into ModuleScripts with clear responsibilities. The main script becomes a thin bootstrapper.
|
|
797
|
+
|
|
798
|
+
### Circular Requires
|
|
799
|
+
|
|
800
|
+
**Problem:** Module A requires Module B, which requires Module A. Causes one of them to receive an incomplete (empty table) reference.
|
|
801
|
+
|
|
802
|
+
**Fix:** Use the `init()` pattern (Section 6), dependency inversion, or event-driven decoupling.
|
|
803
|
+
|
|
804
|
+
### Server Logic in ReplicatedStorage
|
|
805
|
+
|
|
806
|
+
**Problem:** Placing server-only game logic modules in `ReplicatedStorage`. Exploiters can read the source code, understand validation logic, and craft exploits.
|
|
807
|
+
|
|
808
|
+
**Fix:** Keep server logic in `ServerScriptService` or `ServerStorage`. Only put genuinely shared code (types, constants, utilities) in `ReplicatedStorage`.
|
|
809
|
+
|
|
810
|
+
### Scripts in Workspace
|
|
811
|
+
|
|
812
|
+
**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.
|
|
813
|
+
|
|
814
|
+
**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.
|
|
815
|
+
|
|
816
|
+
### Not Using ModuleScripts
|
|
817
|
+
|
|
818
|
+
**Problem:** Duplicating logic across multiple Scripts/LocalScripts instead of extracting shared code into ModuleScripts.
|
|
819
|
+
|
|
820
|
+
**Fix:** If two scripts share logic, extract it into a ModuleScript in the appropriate location (`ReplicatedStorage` for shared, `ServerStorage` for server-only).
|
|
821
|
+
|
|
822
|
+
### Trusting Client Data
|
|
823
|
+
|
|
824
|
+
**Problem:** Server blindly applies whatever the client sends (damage values, item quantities, positions).
|
|
825
|
+
|
|
826
|
+
**Fix:** The server must independently validate and calculate. The client sends **intent** ("I want to attack this target"), not **outcome** ("I dealt 500 damage").
|
|
827
|
+
|
|
828
|
+
### Polling Instead of Events
|
|
829
|
+
|
|
830
|
+
For polling vs event-driven patterns, see **roblox-luau-mastery** → Anti-Patterns.
|
|
831
|
+
|
|
832
|
+
**Core rule:** Use events (`.Changed`, `GetPropertyChangedSignal()`, `Died`, etc.) instead of `while true do task.wait()` loops.
|
|
833
|
+
|
|
834
|
+
### Overusing RemoteFunctions
|
|
835
|
+
|
|
836
|
+
**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.
|
|
837
|
+
|
|
838
|
+
**Fix:** Use `RemoteEvent` for actions that do not need a response. Reserve `RemoteFunction` for when the client genuinely needs data back from the server.
|
|
839
|
+
|
|
840
|
+
### Ignoring `task` Library
|
|
841
|
+
|
|
842
|
+
For deprecated `wait()`/`spawn()`/`delay()` vs the `task` library, see **roblox-luau-mastery** → Task Library.
|
|
843
|
+
|
|
844
|
+
### Instance.new with Parent Argument
|
|
845
|
+
|
|
846
|
+
**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.
|
|
847
|
+
|
|
848
|
+
```luau
|
|
849
|
+
-- BAD: parent during construction, yields, race condition
|
|
850
|
+
local part = Instance.new("Part", workspace)
|
|
851
|
+
part.Size = Vector3.new(4, 1, 4) -- may already be visible to clients
|
|
852
|
+
part.Anchored = true
|
|
853
|
+
part.BrickColor = BrickColor.new("Bright red")
|
|
854
|
+
|
|
855
|
+
-- GOOD: create, configure, then parent
|
|
856
|
+
local part = Instance.new("Part")
|
|
857
|
+
part.Size = Vector3.new(4, 1, 4)
|
|
858
|
+
part.Anchored = true
|
|
859
|
+
part.BrickColor = BrickColor.new("Bright red")
|
|
860
|
+
part.Parent = workspace -- parent last
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
**Rule:** Always set `Parent` last. Create the instance, configure all properties, then parent it.
|