roblox-opencode 1.0.0 → 1.0.2
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 +112 -122
- package/commands/setup-game.md +108 -108
- package/commands/sync-check.md +53 -53
- package/core/roblox-core.md +93 -93
- package/dist/server.js +189 -167
- package/package.json +35 -35
- package/skills/roblox-analytics/SKILL.md +277 -277
- package/skills/roblox-analytics/references/event-batcher.luau +75 -75
- package/skills/roblox-animation-vfx/SKILL.md +1325 -1325
- package/skills/roblox-architecture/SKILL.md +877 -863
- package/skills/roblox-architecture/references/combat-systems.md +1381 -1381
- package/skills/roblox-code-review/SKILL.md +686 -686
- package/skills/roblox-data/SKILL.md +889 -889
- package/skills/roblox-data/references/inventory-systems.md +1729 -1729
- package/skills/roblox-debug/SKILL.md +98 -98
- package/skills/roblox-gui/SKILL.md +1103 -1103
- package/skills/roblox-gui-fusion/SKILL.md +150 -150
- package/skills/roblox-gui-fusion/references/inventory.luau +427 -427
- package/skills/roblox-gui-fusion/references/settings-menu.luau +579 -579
- package/skills/roblox-gui-fusion/references/shop.luau +411 -411
- package/skills/roblox-luau-mastery/SKILL.md +1618 -1519
- package/skills/roblox-monetization/SKILL.md +1084 -1084
- package/skills/roblox-monetization/references/process-receipt.luau +131 -131
- package/skills/roblox-networking/SKILL.md +669 -669
- package/skills/roblox-networking/references/remote-validator.luau +193 -193
- package/skills/roblox-publish-checklist/SKILL.md +127 -127
- package/skills/roblox-runtime/SKILL.md +753 -753
- package/skills/roblox-sharp-edges/SKILL.md +294 -294
- package/skills/roblox-sync/SKILL.md +126 -126
- package/skills/roblox-testing/SKILL.md +943 -943
- package/skills/roblox-tooling/SKILL.md +149 -149
- package/vendor/LICENSES/ProfileStore-LICENSE +201 -201
- package/vendor/LICENSES/RbxUtil-LICENSE +7 -7
- package/vendor/LICENSES/promise-LICENSE +20 -20
- package/vendor/LICENSES/t-LICENSE +21 -21
- package/vendor/LICENSES/testez-LICENSE +200 -200
- package/vendor/README.md +83 -83
- package/vendor/fusion/Animation/ExternalTime.luau +83 -83
- package/vendor/fusion/Animation/Spring.luau +321 -321
- package/vendor/fusion/Animation/Stopwatch.luau +127 -127
- package/vendor/fusion/Animation/Tween.luau +187 -187
- package/vendor/fusion/Animation/getTweenDuration.luau +27 -27
- package/vendor/fusion/Animation/getTweenRatio.luau +47 -47
- package/vendor/fusion/Animation/lerpType.luau +163 -163
- package/vendor/fusion/Animation/packType.luau +99 -99
- package/vendor/fusion/Animation/springCoefficients.luau +80 -80
- package/vendor/fusion/Animation/unpackType.luau +102 -102
- package/vendor/fusion/Colour/Oklab.luau +70 -70
- package/vendor/fusion/Colour/sRGB.luau +54 -54
- package/vendor/fusion/External.luau +167 -167
- package/vendor/fusion/ExternalDebug.luau +69 -69
- package/vendor/fusion/Graph/Observer.luau +113 -113
- package/vendor/fusion/Graph/castToGraph.luau +28 -28
- package/vendor/fusion/Graph/change.luau +80 -80
- package/vendor/fusion/Graph/depend.luau +32 -32
- package/vendor/fusion/Graph/evaluate.luau +55 -55
- package/vendor/fusion/Instances/Attribute.luau +57 -57
- package/vendor/fusion/Instances/AttributeChange.luau +46 -46
- package/vendor/fusion/Instances/AttributeOut.luau +63 -63
- package/vendor/fusion/Instances/Child.luau +21 -21
- package/vendor/fusion/Instances/Children.luau +147 -147
- package/vendor/fusion/Instances/Hydrate.luau +32 -32
- package/vendor/fusion/Instances/New.luau +52 -52
- package/vendor/fusion/Instances/OnChange.luau +49 -49
- package/vendor/fusion/Instances/OnEvent.luau +53 -53
- package/vendor/fusion/Instances/Out.luau +69 -69
- package/vendor/fusion/Instances/applyInstanceProps.luau +148 -148
- package/vendor/fusion/Instances/defaultProps.luau +194 -194
- package/vendor/fusion/LICENSE +21 -21
- package/vendor/fusion/Logging/formatError.luau +48 -48
- package/vendor/fusion/Logging/messages.luau +51 -51
- package/vendor/fusion/Logging/parseError.luau +24 -24
- package/vendor/fusion/Memory/checkLifetime.luau +133 -133
- package/vendor/fusion/Memory/deriveScope.luau +23 -23
- package/vendor/fusion/Memory/deriveScopeImpl.luau +44 -44
- package/vendor/fusion/Memory/doCleanup.luau +78 -78
- package/vendor/fusion/Memory/innerScope.luau +33 -33
- package/vendor/fusion/Memory/legacyCleanup.luau +17 -17
- package/vendor/fusion/Memory/needsDestruction.luau +16 -16
- package/vendor/fusion/Memory/poisonScope.luau +33 -33
- package/vendor/fusion/Memory/scopePool.luau +54 -54
- package/vendor/fusion/Memory/scoped.luau +26 -26
- package/vendor/fusion/Memory/whichLivesLonger.luau +74 -74
- package/vendor/fusion/RobloxExternal.luau +97 -97
- package/vendor/fusion/State/Computed.luau +138 -138
- package/vendor/fusion/State/For/Disassembly.luau +210 -210
- package/vendor/fusion/State/For/ForTypes.luau +30 -30
- package/vendor/fusion/State/For/init.luau +109 -109
- package/vendor/fusion/State/ForKeys.luau +93 -93
- package/vendor/fusion/State/ForPairs.luau +96 -96
- package/vendor/fusion/State/ForValues.luau +93 -93
- package/vendor/fusion/State/Value.luau +87 -87
- package/vendor/fusion/State/castToState.luau +25 -25
- package/vendor/fusion/State/peek.luau +30 -30
- package/vendor/fusion/Types.luau +314 -314
- package/vendor/fusion/Utility/Contextual.luau +90 -90
- package/vendor/fusion/Utility/Safe.luau +22 -22
- package/vendor/fusion/Utility/isSimilar.luau +29 -29
- package/vendor/fusion/Utility/merge.luau +35 -35
- package/vendor/fusion/Utility/nameOf.luau +34 -34
- package/vendor/fusion/Utility/never.luau +13 -13
- package/vendor/fusion/Utility/nicknames.luau +10 -10
- package/vendor/fusion/Utility/xtypeof.luau +26 -26
- package/vendor/fusion/init.luau +82 -82
- package/vendor/profilestore/init.luau +2242 -2242
- package/vendor/promise/init.luau +1982 -1982
- package/vendor/rbxutil/buffer-util/Buffer.test.luau +25 -25
- package/vendor/rbxutil/buffer-util/BufferReader.luau +228 -228
- package/vendor/rbxutil/buffer-util/BufferWriter.luau +269 -269
- package/vendor/rbxutil/buffer-util/DataTypeBuffer.luau +223 -223
- package/vendor/rbxutil/buffer-util/Types.luau +60 -60
- package/vendor/rbxutil/buffer-util/index.d.ts +153 -153
- package/vendor/rbxutil/buffer-util/init.luau +41 -41
- package/vendor/rbxutil/buffer-util/package.json +16 -16
- package/vendor/rbxutil/buffer-util/wally.toml +9 -9
- package/vendor/rbxutil/comm/Client/ClientComm.luau +232 -232
- package/vendor/rbxutil/comm/Client/ClientRemoteProperty.luau +156 -156
- package/vendor/rbxutil/comm/Client/ClientRemoteSignal.luau +109 -109
- package/vendor/rbxutil/comm/Client/init.luau +135 -135
- package/vendor/rbxutil/comm/Server/RemoteProperty.luau +295 -295
- package/vendor/rbxutil/comm/Server/RemoteSignal.luau +211 -211
- package/vendor/rbxutil/comm/Server/ServerComm.luau +211 -211
- package/vendor/rbxutil/comm/Server/init.luau +140 -140
- package/vendor/rbxutil/comm/Types.luau +18 -18
- package/vendor/rbxutil/comm/Util.luau +27 -27
- package/vendor/rbxutil/comm/init.luau +35 -35
- package/vendor/rbxutil/comm/wally.toml +13 -13
- package/vendor/rbxutil/component/init.luau +759 -759
- package/vendor/rbxutil/component/init.test.luau +311 -311
- package/vendor/rbxutil/component/wally.toml +14 -14
- package/vendor/rbxutil/concur/init.luau +542 -542
- package/vendor/rbxutil/concur/init.test.luau +364 -364
- package/vendor/rbxutil/concur/wally.toml +8 -8
- package/vendor/rbxutil/enum-list/init.luau +101 -101
- package/vendor/rbxutil/enum-list/init.test.luau +91 -91
- package/vendor/rbxutil/enum-list/wally.toml +8 -8
- package/vendor/rbxutil/find/index.d.ts +20 -20
- package/vendor/rbxutil/find/init.luau +44 -44
- package/vendor/rbxutil/find/package.json +17 -17
- package/vendor/rbxutil/find/wally.toml +8 -8
- package/vendor/rbxutil/input/Gamepad.luau +559 -559
- package/vendor/rbxutil/input/Keyboard.luau +124 -124
- package/vendor/rbxutil/input/Mouse.luau +278 -278
- package/vendor/rbxutil/input/PreferredInput.luau +91 -91
- package/vendor/rbxutil/input/Touch.luau +120 -120
- package/vendor/rbxutil/input/init.luau +33 -33
- package/vendor/rbxutil/input/wally.toml +12 -12
- package/vendor/rbxutil/loader/index.d.ts +15 -15
- package/vendor/rbxutil/loader/init.luau +137 -137
- package/vendor/rbxutil/loader/wally.toml +8 -8
- package/vendor/rbxutil/log/index.d.ts +38 -38
- package/vendor/rbxutil/log/init.luau +746 -746
- package/vendor/rbxutil/log/wally.toml +8 -8
- package/vendor/rbxutil/net/init.luau +190 -190
- package/vendor/rbxutil/net/wally.toml +8 -8
- package/vendor/rbxutil/option/index.d.ts +44 -44
- package/vendor/rbxutil/option/init.luau +489 -489
- package/vendor/rbxutil/option/init.test.luau +342 -342
- package/vendor/rbxutil/option/wally.toml +8 -8
- package/vendor/rbxutil/pid/index.d.ts +53 -53
- package/vendor/rbxutil/pid/init.luau +195 -195
- package/vendor/rbxutil/pid/package.json +16 -16
- package/vendor/rbxutil/pid/wally.toml +9 -9
- package/vendor/rbxutil/quaternion/index.d.ts +117 -117
- package/vendor/rbxutil/quaternion/init.luau +570 -570
- package/vendor/rbxutil/quaternion/package.json +16 -16
- package/vendor/rbxutil/quaternion/wally.toml +9 -9
- package/vendor/rbxutil/query/index.d.ts +43 -43
- package/vendor/rbxutil/query/init.luau +117 -117
- package/vendor/rbxutil/query/package.json +18 -18
- package/vendor/rbxutil/query/wally.toml +9 -9
- package/vendor/rbxutil/sequent/index.d.ts +28 -28
- package/vendor/rbxutil/sequent/init.luau +340 -340
- package/vendor/rbxutil/sequent/package.json +16 -16
- package/vendor/rbxutil/sequent/wally.toml +9 -9
- package/vendor/rbxutil/ser/init.luau +175 -175
- package/vendor/rbxutil/ser/init.test.luau +50 -50
- package/vendor/rbxutil/ser/wally.toml +11 -11
- package/vendor/rbxutil/shake/index.d.ts +36 -36
- package/vendor/rbxutil/shake/init.luau +532 -532
- package/vendor/rbxutil/shake/init.test.luau +267 -267
- package/vendor/rbxutil/shake/package.json +16 -16
- package/vendor/rbxutil/shake/wally.toml +9 -9
- package/vendor/rbxutil/signal/index.d.ts +100 -100
- package/vendor/rbxutil/signal/init.luau +432 -432
- package/vendor/rbxutil/signal/init.test.luau +190 -190
- package/vendor/rbxutil/signal/package.json +17 -17
- package/vendor/rbxutil/signal/wally.toml +9 -9
- package/vendor/rbxutil/silo/TableWatcher.luau +65 -65
- package/vendor/rbxutil/silo/Util.luau +55 -55
- package/vendor/rbxutil/silo/init.luau +338 -338
- package/vendor/rbxutil/silo/init.test.luau +215 -215
- package/vendor/rbxutil/silo/wally.toml +8 -8
- package/vendor/rbxutil/spring/index.d.ts +40 -40
- package/vendor/rbxutil/spring/init.luau +97 -97
- package/vendor/rbxutil/spring/package.json +17 -17
- package/vendor/rbxutil/spring/wally.toml +8 -8
- package/vendor/rbxutil/stream/index.d.ts +88 -88
- package/vendor/rbxutil/stream/init.luau +597 -597
- package/vendor/rbxutil/stream/package.json +18 -18
- package/vendor/rbxutil/stream/wally.toml +9 -9
- package/vendor/rbxutil/streamable/Streamable.luau +202 -202
- package/vendor/rbxutil/streamable/StreamableUtil.luau +80 -80
- package/vendor/rbxutil/streamable/init.luau +8 -8
- package/vendor/rbxutil/streamable/wally.toml +12 -12
- package/vendor/rbxutil/symbol/init.luau +56 -56
- package/vendor/rbxutil/symbol/init.test.luau +37 -37
- package/vendor/rbxutil/symbol/wally.toml +8 -8
- package/vendor/rbxutil/table-util/init.luau +938 -938
- package/vendor/rbxutil/table-util/init.test.luau +439 -439
- package/vendor/rbxutil/task-queue/index.d.ts +27 -27
- package/vendor/rbxutil/task-queue/init.luau +97 -97
- package/vendor/rbxutil/task-queue/wally.toml +8 -8
- package/vendor/rbxutil/timer/index.d.ts +81 -81
- package/vendor/rbxutil/timer/init.luau +249 -249
- package/vendor/rbxutil/timer/init.test.luau +73 -73
- package/vendor/rbxutil/timer/wally.toml +11 -11
- package/vendor/rbxutil/tree/index.d.ts +15 -15
- package/vendor/rbxutil/tree/init.luau +137 -137
- package/vendor/rbxutil/tree/wally.toml +8 -8
- package/vendor/rbxutil/trove/index.d.ts +46 -46
- package/vendor/rbxutil/trove/init.luau +787 -787
- package/vendor/rbxutil/trove/init.test.luau +203 -203
- package/vendor/rbxutil/trove/wally.toml +8 -8
- package/vendor/rbxutil/typed-remote/init.luau +196 -196
- package/vendor/rbxutil/typed-remote/wally.toml +8 -8
- package/vendor/rbxutil/wait-for/index.d.ts +17 -17
- package/vendor/rbxutil/wait-for/init.luau +257 -257
- package/vendor/rbxutil/wait-for/init.test.luau +182 -182
- package/vendor/rbxutil/wait-for/wally.toml +11 -11
- package/vendor/t/t.lua +1350 -1350
- package/vendor/testez/Context.lua +26 -26
- package/vendor/testez/Expectation.lua +311 -311
- package/vendor/testez/ExpectationContext.lua +38 -38
- package/vendor/testez/LifecycleHooks.lua +89 -89
- package/vendor/testez/Reporters/TeamCityReporter.lua +101 -101
- package/vendor/testez/Reporters/TextReporter.lua +105 -105
- package/vendor/testez/Reporters/TextReporterQuiet.lua +96 -96
- package/vendor/testez/TestBootstrap.lua +146 -146
- package/vendor/testez/TestEnum.lua +27 -27
- package/vendor/testez/TestPlan.lua +304 -304
- package/vendor/testez/TestPlanner.lua +39 -39
- package/vendor/testez/TestResults.lua +111 -111
- package/vendor/testez/TestRunner.lua +188 -188
- package/vendor/testez/TestSession.lua +243 -243
- package/vendor/testez/init.lua +39 -39
|
@@ -1,753 +1,753 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: roblox-runtime
|
|
3
|
-
description: >
|
|
4
|
-
StreamingEnabled, performance optimization, memory management, object pooling, mobile targets.
|
|
5
|
-
last_reviewed: 2026-05-22
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
<!-- Source: brockmartin/roblox-game-skill (MIT) -->
|
|
9
|
-
|
|
10
|
-
# Roblox Runtime & Performance
|
|
11
|
-
|
|
12
|
-
## 1. Overview
|
|
13
|
-
|
|
14
|
-
Load this reference when:
|
|
15
|
-
|
|
16
|
-
- A game runs slowly or hitches during play (frame drops, lag spikes).
|
|
17
|
-
- Optimizing for mobile devices or low-end hardware.
|
|
18
|
-
- Conducting a performance audit before release or after adding major features.
|
|
19
|
-
- Players report high memory usage, disconnects, or long load times.
|
|
20
|
-
- Scaling a game to support more concurrent players.
|
|
21
|
-
|
|
22
|
-
Performance optimization is not a one-time task. It should be revisited after every significant content addition and tested across the full range of target devices.
|
|
23
|
-
|
|
24
|
-
---
|
|
25
|
-
|
|
26
|
-
## Quick Reference
|
|
27
|
-
|
|
28
|
-
**Load Full Reference below only when you need specific optimization techniques or benchmarks.**
|
|
29
|
-
|
|
30
|
-
Key rules:
|
|
31
|
-
- Target: 60 FPS (16.6ms/frame). Server heartbeat budget: 30 FPS (33ms).
|
|
32
|
-
- Part count: <10k visible for mobile, <50k for desktop. Use StreamingEnabled.
|
|
33
|
-
- One Heartbeat connection that dispatches, not N separate connections.
|
|
34
|
-
- Disconnect ALL event connections when done. Use Trove. Leaks = silent frame drops.
|
|
35
|
-
- Instance.Destroying event for cleanup when instances are removed.
|
|
36
|
-
- Debris:AddItem() for timed cleanup (projectiles, effects).
|
|
37
|
-
- Network: minimize RemoteEvent payload size. Batch related calls. Use UnreliableRemoteEvent for non-critical updates (positions, cosmetics).
|
|
38
|
-
- Mobile: halve particle counts, reduce draw distance, simplify meshes.
|
|
39
|
-
- Memory: avoid reference cycles (A→B→A). Weak tables for caches.
|
|
40
|
-
- String concat in loops: use table.concat, not repeated `..`
|
|
41
|
-
|
|
42
|
-
---
|
|
43
|
-
|
|
44
|
-
## Full Reference
|
|
45
|
-
|
|
46
|
-
## 2. Performance Targets
|
|
47
|
-
|
|
48
|
-
| Metric | Desktop | Mobile |
|
|
49
|
-
|---|---|---|
|
|
50
|
-
| Frame Rate | 60 fps | 30 fps minimum |
|
|
51
|
-
| Memory Budget | ~1 GB | ~500 MB |
|
|
52
|
-
| Network | Minimize remote frequency | Same, with smaller payloads |
|
|
53
|
-
| Load Time | Under 10 seconds | Under 15 seconds |
|
|
54
|
-
|
|
55
|
-
**Key principles:**
|
|
56
|
-
|
|
57
|
-
- Always measure against the *lowest-spec target device*, not your development machine.
|
|
58
|
-
- Frame budget at 60 fps is ~16.6 ms per frame. At 30 fps it is ~33.3 ms.
|
|
59
|
-
- Network: keep RemoteEvent calls under 50 per second per client. Prefer batching.
|
|
60
|
-
|
|
61
|
-
---
|
|
62
|
-
|
|
63
|
-
## 3. Part Count Optimization
|
|
64
|
-
|
|
65
|
-
### Limits
|
|
66
|
-
|
|
67
|
-
- **Per model:** aim for a maximum of ~500 parts.
|
|
68
|
-
- **Total scene:** keep the visible scene under 10,000 parts.
|
|
69
|
-
- Fewer parts means less physics simulation, less rendering overhead, and faster replication.
|
|
70
|
-
|
|
71
|
-
### MeshParts Over Unions
|
|
72
|
-
|
|
73
|
-
- `UnionOperation` recalculates collision geometry at runtime and is more expensive.
|
|
74
|
-
- Export unions as MeshParts in Studio (right-click > Export Selection) and re-import.
|
|
75
|
-
- MeshParts use a fixed collision fidelity that is cheaper to compute.
|
|
76
|
-
|
|
77
|
-
### StreamingEnabled
|
|
78
|
-
|
|
79
|
-
StreamingEnabled is **on by default** for new places. Only `BaseParts` and their descendants stream in/out. Other instances (Folders, ValueObjects, RemoteEvents, ModuleScripts) load during initial client load and never stream.
|
|
80
|
-
|
|
81
|
-
When instances stream out, they are **parented to nil** - not destroyed. Luau state persists if they stream back in. Removal signals fire, but local-only property changes may be lost.
|
|
82
|
-
|
|
83
|
-
#### Configuration
|
|
84
|
-
|
|
85
|
-
- `StreamingTargetRadius` - radius (studs) engine keeps loaded. Start at 256, tune.
|
|
86
|
-
- `StreamingMinRadius` - guaranteed radius. Set ~64 for nearby content.
|
|
87
|
-
- `StreamingPauseMode` - what happens during load (Default, Disabled, ClientPhysicsPause).
|
|
88
|
-
- `ModelStreamingMode` - per-model: `Atomic` (all descendants load together), `Persistent` (never streams out), `PersistentPerPlayer`, `Nonatomic`.
|
|
89
|
-
|
|
90
|
-
#### Critical Rules for AI-Generated Code
|
|
91
|
-
|
|
92
|
-
1. **Always use `WaitForChild()` on client** for any Workspace instance. Never use `workspace.MyPart` dot access in LocalScripts - the instance may not be loaded yet.
|
|
93
|
-
2. **Always include a timeout**: `WaitForChild("Name", 30)`. Without timeout, thread hangs forever if instance never streams in.
|
|
94
|
-
3. **Never use `math.huge` as timeout.** The instance may never stream in.
|
|
95
|
-
4. **Server has everything immediately.** WaitForChild is only needed on the client for Workspace instances.
|
|
96
|
-
5. **ReplicatedStorage/ReplicatedFirst never stream.** Always available on client.
|
|
97
|
-
6. **Handle nil returns from FindFirstChild** - instance may be streamed out.
|
|
98
|
-
7. **When instance streams out, Parent becomes nil.** Clean up connections on Parent change.
|
|
99
|
-
8. **BasePart descendants stream independently.** Only non-BasePart children are guaranteed to stream with parent.
|
|
100
|
-
9. **Use `ModelStreamingMode = Atomic`** when all parts must appear together.
|
|
101
|
-
10. **Use `Player:RequestStreamAroundAsync(location)`** to pre-fetch areas before teleporting.
|
|
102
|
-
|
|
103
|
-
#### CollectionService Pattern (Recommended by Roblox)
|
|
104
|
-
|
|
105
|
-
The official recommended pattern for streaming-aware code:
|
|
106
|
-
|
|
107
|
-
```luau
|
|
108
|
-
local CollectionService = game:GetService("CollectionService")
|
|
109
|
-
local tag = "Interactive"
|
|
110
|
-
|
|
111
|
-
local active: {[Instance]: any} = {}
|
|
112
|
-
|
|
113
|
-
CollectionService:GetInstanceAddedSignal(tag):Connect(function(instance)
|
|
114
|
-
active[instance] = true
|
|
115
|
-
-- Set up connections, UI, effects for this instance
|
|
116
|
-
end)
|
|
117
|
-
|
|
118
|
-
CollectionService:GetInstanceRemovedSignal(tag):Connect(function(instance)
|
|
119
|
-
active[instance] = nil
|
|
120
|
-
-- Cleanup: disconnect events, remove UI, stop effects
|
|
121
|
-
end)
|
|
122
|
-
|
|
123
|
-
-- Handle instances already present when script starts
|
|
124
|
-
for _, instance in CollectionService:GetTagged(tag) do
|
|
125
|
-
active[instance] = true
|
|
126
|
-
end
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
This handles stream-in and stream-out automatically. No WaitForChild needed.
|
|
130
|
-
|
|
131
|
-
#### Streamable Module (Sleitnick/RbxUtil)
|
|
132
|
-
|
|
133
|
-
For client-side observation of streaming instances with automatic cleanup:
|
|
134
|
-
|
|
135
|
-
```luau
|
|
136
|
-
local Streamable = require(ReplicatedStorage.Modules.Streamable)
|
|
137
|
-
|
|
138
|
-
local model = workspace:WaitForChild("MyModel")
|
|
139
|
-
|
|
140
|
-
local partStreamable = Streamable.new(model, "SomePart")
|
|
141
|
-
partStreamable:Observe(function(part, trove)
|
|
142
|
-
-- Called when part streams in
|
|
143
|
-
-- trove handles cleanup automatically when part streams out
|
|
144
|
-
trove:Add(function()
|
|
145
|
-
-- Cleanup code
|
|
146
|
-
end)
|
|
147
|
-
end)
|
|
148
|
-
|
|
149
|
-
-- Check existence directly
|
|
150
|
-
if partStreamable.Instance then
|
|
151
|
-
-- part is currently loaded
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
partStreamable:Destroy() -- clean up when done
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
#### Proactive Streaming
|
|
158
|
-
|
|
159
|
-
```luau
|
|
160
|
-
-- Server: pre-fetch area before teleporting player
|
|
161
|
-
Player:RequestStreamAroundAsync(targetLocation)
|
|
162
|
-
|
|
163
|
-
-- Keep an area permanently loaded for a player
|
|
164
|
-
Player:AddReplicationFocus(importantPart)
|
|
165
|
-
|
|
166
|
-
-- Remove when no longer needed
|
|
167
|
-
Player:RemoveReplicationFocus(importantPart)
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
Source: Roblox Instance Streaming docs, Sleitnick/RbxUtil Streamable (MIT)
|
|
171
|
-
|
|
172
|
-
### Anchoring
|
|
173
|
-
|
|
174
|
-
- **Anchor every static part.** Unanchored parts enter the physics solver even if they are not moving, consuming CPU every frame.
|
|
175
|
-
- Use `BasePart.Anchored = true` for terrain decorations, buildings, props, and anything that should not move.
|
|
176
|
-
|
|
177
|
-
---
|
|
178
|
-
|
|
179
|
-
## 4. Script Optimization
|
|
180
|
-
|
|
181
|
-
### Consolidated Heartbeat
|
|
182
|
-
|
|
183
|
-
Never scatter `RunService.Heartbeat:Connect(...)` across dozens of scripts. Consolidate into a single manager.
|
|
184
|
-
|
|
185
|
-
```lua
|
|
186
|
-
-- HeartbeatManager (single Script in ServerScriptService or a ModuleScript)
|
|
187
|
-
local RunService = game:GetService("RunService")
|
|
188
|
-
|
|
189
|
-
local HeartbeatManager = {}
|
|
190
|
-
HeartbeatManager._callbacks = {} :: { [string]: (dt: number) -> () }
|
|
191
|
-
|
|
192
|
-
function HeartbeatManager:Register(id: string, callback: (dt: number) -> ())
|
|
193
|
-
self._callbacks[id] = callback
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
function HeartbeatManager:Unregister(id: string)
|
|
197
|
-
self._callbacks[id] = nil
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
RunService.Heartbeat:Connect(function(dt: number)
|
|
201
|
-
for _, callback in self._callbacks do
|
|
202
|
-
callback(dt)
|
|
203
|
-
end
|
|
204
|
-
end)
|
|
205
|
-
|
|
206
|
-
return HeartbeatManager
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
Usage from other modules:
|
|
210
|
-
|
|
211
|
-
```lua
|
|
212
|
-
local HeartbeatManager = require(path.to.HeartbeatManager)
|
|
213
|
-
|
|
214
|
-
HeartbeatManager:Register("EnemyAI", function(dt: number)
|
|
215
|
-
-- update all enemies
|
|
216
|
-
end)
|
|
217
|
-
|
|
218
|
-
-- When no longer needed:
|
|
219
|
-
HeartbeatManager:Unregister("EnemyAI")
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
### Table Pre-allocation
|
|
223
|
-
|
|
224
|
-
```lua
|
|
225
|
-
-- Pre-allocate a table with 100 slots
|
|
226
|
-
local results = table.create(100)
|
|
227
|
-
for i = 1, 100 do
|
|
228
|
-
results[i] = computeValue(i)
|
|
229
|
-
end
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
### String Concatenation
|
|
233
|
-
|
|
234
|
-
```lua
|
|
235
|
-
-- BAD: creates a new string object every iteration
|
|
236
|
-
local result = ""
|
|
237
|
-
for i = 1, 1000 do
|
|
238
|
-
result = result .. tostring(i) .. ","
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
-- GOOD: build a table, join once
|
|
242
|
-
local parts = table.create(1000)
|
|
243
|
-
for i = 1, 1000 do
|
|
244
|
-
parts[i] = tostring(i)
|
|
245
|
-
end
|
|
246
|
-
local result = table.concat(parts, ",")
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
---
|
|
250
|
-
|
|
251
|
-
## 5. Memory Management
|
|
252
|
-
|
|
253
|
-
### Disconnect Events
|
|
254
|
-
|
|
255
|
-
Every `:Connect()` call returns a `RBXScriptConnection`. Store it and disconnect when done.
|
|
256
|
-
|
|
257
|
-
```lua
|
|
258
|
-
-- Event Cleanup Pattern
|
|
259
|
-
local Cleaner = {}
|
|
260
|
-
Cleaner.__index = Cleaner
|
|
261
|
-
|
|
262
|
-
function Cleaner.new()
|
|
263
|
-
local self = setmetatable({}, Cleaner)
|
|
264
|
-
self._connections = {} :: { RBXScriptConnection }
|
|
265
|
-
self._instances = {} :: { Instance }
|
|
266
|
-
return self
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
function Cleaner:Add(connection: RBXScriptConnection)
|
|
270
|
-
table.insert(self._connections, connection)
|
|
271
|
-
return connection
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
function Cleaner:AddInstance(instance: Instance)
|
|
275
|
-
table.insert(self._instances, instance)
|
|
276
|
-
return instance
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
function Cleaner:Clean()
|
|
280
|
-
for _, conn in self._connections do
|
|
281
|
-
if conn.Connected then
|
|
282
|
-
conn:Disconnect()
|
|
283
|
-
end
|
|
284
|
-
end
|
|
285
|
-
table.clear(self._connections)
|
|
286
|
-
|
|
287
|
-
for _, inst in self._instances do
|
|
288
|
-
inst:Destroy()
|
|
289
|
-
end
|
|
290
|
-
table.clear(self._instances)
|
|
291
|
-
end
|
|
292
|
-
|
|
293
|
-
return Cleaner
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
Usage:
|
|
297
|
-
|
|
298
|
-
```lua
|
|
299
|
-
local Cleaner = require(path.to.Cleaner)
|
|
300
|
-
local cleaner = Cleaner.new()
|
|
301
|
-
|
|
302
|
-
cleaner:Add(workspace.ChildAdded:Connect(function(child)
|
|
303
|
-
print(child.Name, "added")
|
|
304
|
-
end))
|
|
305
|
-
|
|
306
|
-
cleaner:Add(Players.PlayerRemoving:Connect(function(player)
|
|
307
|
-
print(player.Name, "left")
|
|
308
|
-
end))
|
|
309
|
-
|
|
310
|
-
-- When this system shuts down or the player leaves:
|
|
311
|
-
cleaner:Clean()
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
### Destroy Instances Properly
|
|
315
|
-
|
|
316
|
-
- Always call `:Destroy()` rather than setting `Parent = nil`. `:Destroy()` locks the instance, disconnects all events on it, and marks it for garbage collection.
|
|
317
|
-
- Setting `Parent = nil` keeps the instance alive if anything still references it.
|
|
318
|
-
|
|
319
|
-
### Avoid Reference Cycles
|
|
320
|
-
|
|
321
|
-
```lua
|
|
322
|
-
-- BAD: mutual references prevent garbage collection
|
|
323
|
-
local a = {}
|
|
324
|
-
local b = {}
|
|
325
|
-
a.ref = b
|
|
326
|
-
b.ref = a
|
|
327
|
-
-- Neither a nor b can be collected until both references are broken
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
Break references explicitly when done: `a.ref = nil; b.ref = nil`.
|
|
331
|
-
|
|
332
|
-
### Instance.Destroying
|
|
333
|
-
|
|
334
|
-
Use `Instance.Destroying` to run cleanup when an instance is about to be destroyed:
|
|
335
|
-
|
|
336
|
-
```lua
|
|
337
|
-
local part = Instance.new("Part")
|
|
338
|
-
part.Destroying:Connect(function()
|
|
339
|
-
-- clean up related data, disconnect connections, etc.
|
|
340
|
-
end)
|
|
341
|
-
```
|
|
342
|
-
|
|
343
|
-
### Debris Service
|
|
344
|
-
|
|
345
|
-
For timed cleanup of temporary instances (projectiles, effects):
|
|
346
|
-
|
|
347
|
-
```lua
|
|
348
|
-
local Debris = game:GetService("Debris")
|
|
349
|
-
local bullet = Instance.new("Part")
|
|
350
|
-
bullet.Parent = workspace
|
|
351
|
-
Debris:AddItem(bullet, 5) -- destroyed after 5 seconds
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
---
|
|
355
|
-
|
|
356
|
-
## 6. Network Optimization
|
|
357
|
-
|
|
358
|
-
### Minimize RemoteEvent Data Size
|
|
359
|
-
|
|
360
|
-
- Send only what changed, not full state.
|
|
361
|
-
- Use numeric IDs instead of long string keys when possible.
|
|
362
|
-
- Avoid sending Instance references when a name or ID suffices.
|
|
363
|
-
|
|
364
|
-
### Batch Related Remotes
|
|
365
|
-
|
|
366
|
-
```lua
|
|
367
|
-
-- BAD: three separate remote calls
|
|
368
|
-
remoteHealth:FireClient(player, health)
|
|
369
|
-
remoteAmmo:FireClient(player, ammo)
|
|
370
|
-
remoteStamina:FireClient(player, stamina)
|
|
371
|
-
|
|
372
|
-
-- GOOD: one call with a table
|
|
373
|
-
remotePlayerState:FireClient(player, {
|
|
374
|
-
health = health,
|
|
375
|
-
ammo = ammo,
|
|
376
|
-
stamina = stamina,
|
|
377
|
-
})
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
### UnreliableRemoteEvent
|
|
381
|
-
|
|
382
|
-
For high-frequency, non-critical data such as position or rotation updates, use `UnreliableRemoteEvent`. Dropped packets are acceptable because the next update will correct the state.
|
|
383
|
-
|
|
384
|
-
```lua
|
|
385
|
-
-- In ReplicatedStorage, create an UnreliableRemoteEvent named "PositionSync"
|
|
386
|
-
local posSync = ReplicatedStorage:WaitForChild("PositionSync")
|
|
387
|
-
|
|
388
|
-
-- Server: fire frequently without guaranteeing delivery
|
|
389
|
-
RunService.Heartbeat:Connect(function()
|
|
390
|
-
for _, player in Players:GetPlayers() do
|
|
391
|
-
posSync:FireClient(player, npcPositions)
|
|
392
|
-
end
|
|
393
|
-
end)
|
|
394
|
-
```
|
|
395
|
-
|
|
396
|
-
### Compress Large Data
|
|
397
|
-
|
|
398
|
-
- Strip unnecessary keys before sending.
|
|
399
|
-
- Use short key names (`hp` instead of `hitPoints`).
|
|
400
|
-
- Consider delta compression: send only values that changed since the last update.
|
|
401
|
-
|
|
402
|
-
### Reduce Replication
|
|
403
|
-
|
|
404
|
-
- Set visual-only properties on the client (particle colors, UI tweens).
|
|
405
|
-
- Properties changed on the server replicate to all clients automatically, which consumes bandwidth.
|
|
406
|
-
|
|
407
|
-
---
|
|
408
|
-
|
|
409
|
-
## 7. Rendering Optimization
|
|
410
|
-
|
|
411
|
-
### Level of Detail (LOD)
|
|
412
|
-
|
|
413
|
-
Create multiple versions of a model at different detail levels and swap based on distance:
|
|
414
|
-
|
|
415
|
-
```lua
|
|
416
|
-
local function setLOD(model: Model, playerPosition: Vector3)
|
|
417
|
-
local distance = (model:GetPivot().Position - playerPosition).Magnitude
|
|
418
|
-
if distance < 100 then
|
|
419
|
-
-- show high-detail version
|
|
420
|
-
elseif distance < 300 then
|
|
421
|
-
-- show medium-detail version
|
|
422
|
-
else
|
|
423
|
-
-- show low-detail version or hide
|
|
424
|
-
end
|
|
425
|
-
end
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
Roblox also has built-in `MeshPart.RenderFidelity` (Automatic, Performance, Precise) which controls mesh LOD.
|
|
429
|
-
|
|
430
|
-
### Draw Distance Limits
|
|
431
|
-
|
|
432
|
-
- Use `BasePart.CastShadow = false` on distant or small parts.
|
|
433
|
-
- Disable unnecessary `SurfaceLight`, `PointLight`, `SpotLight` on distant objects.
|
|
434
|
-
- With StreamingEnabled, the engine handles draw distance automatically.
|
|
435
|
-
|
|
436
|
-
### Particle Count Budgets
|
|
437
|
-
|
|
438
|
-
| Property | Recommended Max |
|
|
439
|
-
|---|---|
|
|
440
|
-
| Particles per emitter (`Rate`) | ~200 |
|
|
441
|
-
| Total active emitters in view | ~20 |
|
|
442
|
-
| Beam segments (`Segments`) | 10-20 |
|
|
443
|
-
| Trail `MaxLength` | Keep short for mobile |
|
|
444
|
-
|
|
445
|
-
- Set `ParticleEmitter.Enabled = false` when off-screen or far away.
|
|
446
|
-
- Use fewer, larger particles instead of many small ones.
|
|
447
|
-
|
|
448
|
-
### Texture Resolution
|
|
449
|
-
|
|
450
|
-
| Use Case | Max Resolution |
|
|
451
|
-
|---|---|
|
|
452
|
-
| General props, walls, floors | 512x512 |
|
|
453
|
-
| Hero assets (player characters, key items) | 1024x1024 |
|
|
454
|
-
| UI icons, decals | 256x256 to 512x512 |
|
|
455
|
-
| Sky/environment | 1024x1024 |
|
|
456
|
-
|
|
457
|
-
- Use `Decal` over `Texture` when the surface only needs one face covered. Decals are simpler to render.
|
|
458
|
-
- Compress textures before uploading. Avoid PNG when JPEG quality is acceptable.
|
|
459
|
-
|
|
460
|
-
---
|
|
461
|
-
|
|
462
|
-
## 8. Mobile-Specific Optimization
|
|
463
|
-
|
|
464
|
-
### Part Counts
|
|
465
|
-
|
|
466
|
-
- Target 30-50% fewer parts than desktop. If the desktop budget is 10K parts, aim for 5-7K on mobile.
|
|
467
|
-
- Use `UserInputService:GetPlatform()` or screen size to detect mobile and reduce detail.
|
|
468
|
-
|
|
469
|
-
### Simplified Particle Effects
|
|
470
|
-
|
|
471
|
-
- Halve the `Rate` of particle emitters on mobile.
|
|
472
|
-
- Reduce `Lifetime` to keep fewer active particles.
|
|
473
|
-
- Disable non-essential emitters entirely.
|
|
474
|
-
|
|
475
|
-
### Touch-Optimized UI
|
|
476
|
-
|
|
477
|
-
- Minimum touch target size: **44x44 points** (following Apple HIG).
|
|
478
|
-
- Add padding between interactive elements.
|
|
479
|
-
- Use `GuiObject.Active = true` to ensure touch events register.
|
|
480
|
-
- Avoid hover-dependent UI (mobile has no hover state).
|
|
481
|
-
|
|
482
|
-
### Reduced Draw Distance
|
|
483
|
-
|
|
484
|
-
```lua
|
|
485
|
-
if UserInputService.TouchEnabled then
|
|
486
|
-
workspace.StreamingTargetRadius = 128 -- lower than desktop
|
|
487
|
-
workspace.StreamingMinRadius = 48
|
|
488
|
-
end
|
|
489
|
-
```
|
|
490
|
-
|
|
491
|
-
### Memory-Efficient Assets
|
|
492
|
-
|
|
493
|
-
- Use lower-resolution textures on mobile (256x256 where desktop uses 512x512).
|
|
494
|
-
- Reduce mesh polygon counts for mobile LOD models.
|
|
495
|
-
- Monitor memory with `Stats():GetTotalMemoryUsageMb()` and warn/act if approaching 500 MB.
|
|
496
|
-
|
|
497
|
-
### Test on Low-End Devices
|
|
498
|
-
|
|
499
|
-
- Test on devices with 2-3 GB RAM (older iPads, budget Android phones).
|
|
500
|
-
- Use the Roblox mobile emulator in Studio, but always verify on real hardware.
|
|
501
|
-
- Check for thermal throttling during extended play sessions.
|
|
502
|
-
|
|
503
|
-
---
|
|
504
|
-
|
|
505
|
-
## 9. Profiling Tools
|
|
506
|
-
|
|
507
|
-
### MicroProfiler (Ctrl+F6 in Studio)
|
|
508
|
-
|
|
509
|
-
The MicroProfiler displays a real-time flame graph of what the engine is doing each frame.
|
|
510
|
-
|
|
511
|
-
**How to read it:**
|
|
512
|
-
|
|
513
|
-
1. Press `Ctrl+F6` to open. Press `Ctrl+P` to pause and inspect a frame.
|
|
514
|
-
2. Each horizontal bar is a task. Width represents time spent.
|
|
515
|
-
3. Look for bars that are unusually wide - these are your hot frames.
|
|
516
|
-
4. Common labels to watch:
|
|
517
|
-
- `Heartbeat` - your Heartbeat scripts. If wide, your per-frame logic is too heavy.
|
|
518
|
-
- `Physics` - collision and simulation. Reduce unanchored parts.
|
|
519
|
-
- `Render/Perform` - GPU-bound. Reduce draw calls, textures, particles.
|
|
520
|
-
- `Replication` - network overhead. Reduce remote calls and replicated property changes.
|
|
521
|
-
5. Click a bar to see details: script name, line number, time in microseconds.
|
|
522
|
-
6. Use the `microprofiler` dump (`Ctrl+F6` > `Dump`) to save a `.html` file for offline analysis.
|
|
523
|
-
|
|
524
|
-
### F9 Developer Console
|
|
525
|
-
|
|
526
|
-
- Press `F9` in-game or in Studio to open.
|
|
527
|
-
- **Log** tab: errors, warnings, print output.
|
|
528
|
-
- **Memory** tab: breakdown by category (Instances, PhysicsParts, Sounds, Scripts, Signals, etc.).
|
|
529
|
-
- **Stats** tab: FPS, ping, data send/receive rates.
|
|
530
|
-
- **Server Stats** (in-game): server heartbeat time, physics step time.
|
|
531
|
-
|
|
532
|
-
### Stats Service (Programmatic)
|
|
533
|
-
|
|
534
|
-
```lua
|
|
535
|
-
local Stats = game:GetService("Stats")
|
|
536
|
-
|
|
537
|
-
-- Total memory in MB
|
|
538
|
-
local totalMemory = Stats:GetTotalMemoryUsageMb()
|
|
539
|
-
|
|
540
|
-
-- Specific categories
|
|
541
|
-
local instanceMemory = Stats:GetMemoryUsageMbForTag(Enum.DeveloperMemoryTag.Instances)
|
|
542
|
-
local scriptMemory = Stats:GetMemoryUsageMbForTag(Enum.DeveloperMemoryTag.LuaHeap)
|
|
543
|
-
|
|
544
|
-
print(string.format("Total: %.1f MB | Instances: %.1f MB | Lua: %.1f MB",
|
|
545
|
-
totalMemory, instanceMemory, scriptMemory))
|
|
546
|
-
```
|
|
547
|
-
|
|
548
|
-
---
|
|
549
|
-
|
|
550
|
-
## 10. Best Practices
|
|
551
|
-
|
|
552
|
-
### Profile Before Optimizing
|
|
553
|
-
|
|
554
|
-
Never guess where the bottleneck is. Use the MicroProfiler and memory stats to find the actual hot path before changing code.
|
|
555
|
-
|
|
556
|
-
### Optimize Hot Paths First
|
|
557
|
-
|
|
558
|
-
Focus effort on code that runs every frame (Heartbeat, RenderStepped) or on every player action. Code that runs once at startup is rarely worth optimizing.
|
|
559
|
-
|
|
560
|
-
### Spatial Queries Over Brute Force
|
|
561
|
-
|
|
562
|
-
```lua
|
|
563
|
-
-- BAD: loop over every part in workspace
|
|
564
|
-
for _, part in workspace:GetDescendants() do
|
|
565
|
-
if (part.Position - origin).Magnitude < 50 then
|
|
566
|
-
-- ...
|
|
567
|
-
end
|
|
568
|
-
end
|
|
569
|
-
|
|
570
|
-
-- GOOD: spatial query
|
|
571
|
-
local params = OverlapParams.new()
|
|
572
|
-
params.FilterType = Enum.RaycastFilterType.Include
|
|
573
|
-
params.FilterDescendantsInstances = { workspace.Enemies }
|
|
574
|
-
|
|
575
|
-
local parts = workspace:GetPartBoundsInBox(
|
|
576
|
-
CFrame.new(origin),
|
|
577
|
-
Vector3.new(100, 100, 100), -- 50-stud radius box
|
|
578
|
-
params
|
|
579
|
-
)
|
|
580
|
-
```
|
|
581
|
-
|
|
582
|
-
### Object Pooling
|
|
583
|
-
|
|
584
|
-
Reuse instances instead of creating and destroying them repeatedly.
|
|
585
|
-
|
|
586
|
-
```lua
|
|
587
|
-
-- Object Pool Pattern
|
|
588
|
-
local ObjectPool = {}
|
|
589
|
-
ObjectPool.__index = ObjectPool
|
|
590
|
-
|
|
591
|
-
function ObjectPool.new(template: Instance, initialSize: number)
|
|
592
|
-
|
|
593
|
-
local self = setmetatable({}, ObjectPool)
|
|
594
|
-
self._template = template
|
|
595
|
-
self._available = table.create(initialSize)
|
|
596
|
-
self._active = {} :: { [Instance]: boolean }
|
|
597
|
-
|
|
598
|
-
-- Pre-populate
|
|
599
|
-
for i = 1, initialSize do
|
|
600
|
-
local clone = template:Clone()
|
|
601
|
-
clone.Parent = nil
|
|
602
|
-
table.insert(self._available, clone)
|
|
603
|
-
end
|
|
604
|
-
|
|
605
|
-
return self
|
|
606
|
-
end
|
|
607
|
-
|
|
608
|
-
function ObjectPool:Get(): Instance
|
|
609
|
-
local obj: Instance
|
|
610
|
-
if #self._available > 0 then
|
|
611
|
-
obj = table.remove(self._available)
|
|
612
|
-
else
|
|
613
|
-
obj = self._template:Clone()
|
|
614
|
-
end
|
|
615
|
-
|
|
616
|
-
self._active[obj] = true
|
|
617
|
-
return obj
|
|
618
|
-
end
|
|
619
|
-
|
|
620
|
-
function ObjectPool:Return(obj: Instance)
|
|
621
|
-
if not self._active[obj] then
|
|
622
|
-
return
|
|
623
|
-
end
|
|
624
|
-
|
|
625
|
-
self._active[obj] = nil
|
|
626
|
-
obj.Parent = nil
|
|
627
|
-
|
|
628
|
-
-- Reset state as needed (position, visibility, etc.)
|
|
629
|
-
if obj:IsA("BasePart") then
|
|
630
|
-
obj.CFrame = CFrame.new(0, -1000, 0) -- move off-screen
|
|
631
|
-
obj.Anchored = true
|
|
632
|
-
obj.Velocity = Vector3.zero
|
|
633
|
-
end
|
|
634
|
-
|
|
635
|
-
table.insert(self._available, obj)
|
|
636
|
-
end
|
|
637
|
-
|
|
638
|
-
function ObjectPool:ReturnAll()
|
|
639
|
-
for obj in self._active do
|
|
640
|
-
self:Return(obj)
|
|
641
|
-
end
|
|
642
|
-
end
|
|
643
|
-
|
|
644
|
-
function ObjectPool:Destroy()
|
|
645
|
-
for obj in self._active do
|
|
646
|
-
obj:Destroy()
|
|
647
|
-
end
|
|
648
|
-
for _, obj in self._available do
|
|
649
|
-
obj:Destroy()
|
|
650
|
-
end
|
|
651
|
-
table.clear(self._active)
|
|
652
|
-
table.clear(self._available)
|
|
653
|
-
end
|
|
654
|
-
|
|
655
|
-
return ObjectPool
|
|
656
|
-
```
|
|
657
|
-
|
|
658
|
-
Usage:
|
|
659
|
-
|
|
660
|
-
```lua
|
|
661
|
-
local ObjectPool = require(path.to.ObjectPool)
|
|
662
|
-
local bulletTemplate = ReplicatedStorage.Assets.Bullet
|
|
663
|
-
local bulletPool = ObjectPool.new(bulletTemplate, 50)
|
|
664
|
-
|
|
665
|
-
-- Spawn a bullet
|
|
666
|
-
local bullet = bulletPool:Get()
|
|
667
|
-
bullet.CFrame = firePoint
|
|
668
|
-
bullet.Parent = workspace
|
|
669
|
-
|
|
670
|
-
-- Return when done
|
|
671
|
-
bulletPool:Return(bullet)
|
|
672
|
-
```
|
|
673
|
-
|
|
674
|
-
### Lazy-Load Distant Content
|
|
675
|
-
|
|
676
|
-
Do not load all assets at game start. Use StreamingEnabled or manually load content as the player approaches.
|
|
677
|
-
|
|
678
|
-
```lua
|
|
679
|
-
local function onPlayerMoved(position: Vector3)
|
|
680
|
-
for _, zone in zones do
|
|
681
|
-
local distance = (zone.center - position).Magnitude
|
|
682
|
-
if distance < zone.loadRadius and not zone.loaded then
|
|
683
|
-
zone:Load()
|
|
684
|
-
elseif distance > zone.unloadRadius and zone.loaded then
|
|
685
|
-
zone:Unload()
|
|
686
|
-
end
|
|
687
|
-
end
|
|
688
|
-
end
|
|
689
|
-
```
|
|
690
|
-
|
|
691
|
-
---
|
|
692
|
-
|
|
693
|
-
## 11. Anti-Patterns
|
|
694
|
-
|
|
695
|
-
### Premature Optimization
|
|
696
|
-
|
|
697
|
-
Optimizing code that is not a bottleneck wastes development time and often makes code harder to read. Always profile first.
|
|
698
|
-
|
|
699
|
-
### Creating/Destroying Instances in Heartbeat
|
|
700
|
-
|
|
701
|
-
```lua
|
|
702
|
-
-- ANTI-PATTERN: creating parts every frame
|
|
703
|
-
RunService.Heartbeat:Connect(function()
|
|
704
|
-
local part = Instance.new("Part") -- allocation every frame
|
|
705
|
-
part.Parent = workspace
|
|
706
|
-
task.delay(1, function()
|
|
707
|
-
part:Destroy()
|
|
708
|
-
end)
|
|
709
|
-
end)
|
|
710
|
-
|
|
711
|
-
-- FIX: use an object pool (see section 10)
|
|
712
|
-
```
|
|
713
|
-
|
|
714
|
-
### Large Uncompressed Data Over Remotes
|
|
715
|
-
|
|
716
|
-
```lua
|
|
717
|
-
-- ANTI-PATTERN: sending entire inventory every update
|
|
718
|
-
remote:FireClient(player, fullInventoryTable) -- could be thousands of entries
|
|
719
|
-
|
|
720
|
-
-- FIX: send only changes
|
|
721
|
-
remote:FireClient(player, { added = { itemId }, removed = { oldItemId } })
|
|
722
|
-
```
|
|
723
|
-
|
|
724
|
-
### Not Testing on Mobile
|
|
725
|
-
|
|
726
|
-
A game that runs at 60 fps on a gaming PC may run at 10 fps on a phone. Always test on actual mobile hardware, not just the Studio emulator.
|
|
727
|
-
|
|
728
|
-
### Ignoring Memory Leaks
|
|
729
|
-
|
|
730
|
-
Common leak sources:
|
|
731
|
-
|
|
732
|
-
- Event connections that are never disconnected.
|
|
733
|
-
- Instances removed from the hierarchy but still referenced in a table.
|
|
734
|
-
- Closures capturing large upvalues that outlive their usefulness.
|
|
735
|
-
- Module-level tables that grow indefinitely without cleanup.
|
|
736
|
-
|
|
737
|
-
Detect leaks by monitoring `Stats:GetTotalMemoryUsageMb()` over time. If memory grows continuously during gameplay without stabilizing, there is a leak.
|
|
738
|
-
|
|
739
|
-
```lua
|
|
740
|
-
-- Simple memory monitor
|
|
741
|
-
local lastMemory = 0
|
|
742
|
-
task.spawn(function()
|
|
743
|
-
while true do
|
|
744
|
-
local current = game:GetService("Stats"):GetTotalMemoryUsageMb()
|
|
745
|
-
local delta = current - lastMemory
|
|
746
|
-
if delta > 10 then
|
|
747
|
-
warn(string.format("Memory spike: +%.1f MB (total: %.1f MB)", delta, current))
|
|
748
|
-
end
|
|
749
|
-
lastMemory = current
|
|
750
|
-
task.wait(10)
|
|
751
|
-
end
|
|
752
|
-
end)
|
|
753
|
-
```
|
|
1
|
+
---
|
|
2
|
+
name: roblox-runtime
|
|
3
|
+
description: >
|
|
4
|
+
StreamingEnabled, performance optimization, memory management, object pooling, mobile targets.
|
|
5
|
+
last_reviewed: 2026-05-22
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<!-- Source: brockmartin/roblox-game-skill (MIT) -->
|
|
9
|
+
|
|
10
|
+
# Roblox Runtime & Performance
|
|
11
|
+
|
|
12
|
+
## 1. Overview
|
|
13
|
+
|
|
14
|
+
Load this reference when:
|
|
15
|
+
|
|
16
|
+
- A game runs slowly or hitches during play (frame drops, lag spikes).
|
|
17
|
+
- Optimizing for mobile devices or low-end hardware.
|
|
18
|
+
- Conducting a performance audit before release or after adding major features.
|
|
19
|
+
- Players report high memory usage, disconnects, or long load times.
|
|
20
|
+
- Scaling a game to support more concurrent players.
|
|
21
|
+
|
|
22
|
+
Performance optimization is not a one-time task. It should be revisited after every significant content addition and tested across the full range of target devices.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Quick Reference
|
|
27
|
+
|
|
28
|
+
**Load Full Reference below only when you need specific optimization techniques or benchmarks.**
|
|
29
|
+
|
|
30
|
+
Key rules:
|
|
31
|
+
- Target: 60 FPS (16.6ms/frame). Server heartbeat budget: 30 FPS (33ms).
|
|
32
|
+
- Part count: <10k visible for mobile, <50k for desktop. Use StreamingEnabled.
|
|
33
|
+
- One Heartbeat connection that dispatches, not N separate connections.
|
|
34
|
+
- Disconnect ALL event connections when done. Use Trove. Leaks = silent frame drops.
|
|
35
|
+
- Instance.Destroying event for cleanup when instances are removed.
|
|
36
|
+
- Debris:AddItem() for timed cleanup (projectiles, effects).
|
|
37
|
+
- Network: minimize RemoteEvent payload size. Batch related calls. Use UnreliableRemoteEvent for non-critical updates (positions, cosmetics).
|
|
38
|
+
- Mobile: halve particle counts, reduce draw distance, simplify meshes.
|
|
39
|
+
- Memory: avoid reference cycles (A→B→A). Weak tables for caches.
|
|
40
|
+
- String concat in loops: use table.concat, not repeated `..`
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Full Reference
|
|
45
|
+
|
|
46
|
+
## 2. Performance Targets
|
|
47
|
+
|
|
48
|
+
| Metric | Desktop | Mobile |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| Frame Rate | 60 fps | 30 fps minimum |
|
|
51
|
+
| Memory Budget | ~1 GB | ~500 MB |
|
|
52
|
+
| Network | Minimize remote frequency | Same, with smaller payloads |
|
|
53
|
+
| Load Time | Under 10 seconds | Under 15 seconds |
|
|
54
|
+
|
|
55
|
+
**Key principles:**
|
|
56
|
+
|
|
57
|
+
- Always measure against the *lowest-spec target device*, not your development machine.
|
|
58
|
+
- Frame budget at 60 fps is ~16.6 ms per frame. At 30 fps it is ~33.3 ms.
|
|
59
|
+
- Network: keep RemoteEvent calls under 50 per second per client. Prefer batching.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 3. Part Count Optimization
|
|
64
|
+
|
|
65
|
+
### Limits
|
|
66
|
+
|
|
67
|
+
- **Per model:** aim for a maximum of ~500 parts.
|
|
68
|
+
- **Total scene:** keep the visible scene under 10,000 parts.
|
|
69
|
+
- Fewer parts means less physics simulation, less rendering overhead, and faster replication.
|
|
70
|
+
|
|
71
|
+
### MeshParts Over Unions
|
|
72
|
+
|
|
73
|
+
- `UnionOperation` recalculates collision geometry at runtime and is more expensive.
|
|
74
|
+
- Export unions as MeshParts in Studio (right-click > Export Selection) and re-import.
|
|
75
|
+
- MeshParts use a fixed collision fidelity that is cheaper to compute.
|
|
76
|
+
|
|
77
|
+
### StreamingEnabled
|
|
78
|
+
|
|
79
|
+
StreamingEnabled is **on by default** for new places. Only `BaseParts` and their descendants stream in/out. Other instances (Folders, ValueObjects, RemoteEvents, ModuleScripts) load during initial client load and never stream.
|
|
80
|
+
|
|
81
|
+
When instances stream out, they are **parented to nil** - not destroyed. Luau state persists if they stream back in. Removal signals fire, but local-only property changes may be lost.
|
|
82
|
+
|
|
83
|
+
#### Configuration
|
|
84
|
+
|
|
85
|
+
- `StreamingTargetRadius` - radius (studs) engine keeps loaded. Start at 256, tune.
|
|
86
|
+
- `StreamingMinRadius` - guaranteed radius. Set ~64 for nearby content.
|
|
87
|
+
- `StreamingPauseMode` - what happens during load (Default, Disabled, ClientPhysicsPause).
|
|
88
|
+
- `ModelStreamingMode` - per-model: `Atomic` (all descendants load together), `Persistent` (never streams out), `PersistentPerPlayer`, `Nonatomic`.
|
|
89
|
+
|
|
90
|
+
#### Critical Rules for AI-Generated Code
|
|
91
|
+
|
|
92
|
+
1. **Always use `WaitForChild()` on client** for any Workspace instance. Never use `workspace.MyPart` dot access in LocalScripts - the instance may not be loaded yet.
|
|
93
|
+
2. **Always include a timeout**: `WaitForChild("Name", 30)`. Without timeout, thread hangs forever if instance never streams in.
|
|
94
|
+
3. **Never use `math.huge` as timeout.** The instance may never stream in.
|
|
95
|
+
4. **Server has everything immediately.** WaitForChild is only needed on the client for Workspace instances.
|
|
96
|
+
5. **ReplicatedStorage/ReplicatedFirst never stream.** Always available on client.
|
|
97
|
+
6. **Handle nil returns from FindFirstChild** - instance may be streamed out.
|
|
98
|
+
7. **When instance streams out, Parent becomes nil.** Clean up connections on Parent change.
|
|
99
|
+
8. **BasePart descendants stream independently.** Only non-BasePart children are guaranteed to stream with parent.
|
|
100
|
+
9. **Use `ModelStreamingMode = Atomic`** when all parts must appear together.
|
|
101
|
+
10. **Use `Player:RequestStreamAroundAsync(location)`** to pre-fetch areas before teleporting.
|
|
102
|
+
|
|
103
|
+
#### CollectionService Pattern (Recommended by Roblox)
|
|
104
|
+
|
|
105
|
+
The official recommended pattern for streaming-aware code:
|
|
106
|
+
|
|
107
|
+
```luau
|
|
108
|
+
local CollectionService = game:GetService("CollectionService")
|
|
109
|
+
local tag = "Interactive"
|
|
110
|
+
|
|
111
|
+
local active: {[Instance]: any} = {}
|
|
112
|
+
|
|
113
|
+
CollectionService:GetInstanceAddedSignal(tag):Connect(function(instance)
|
|
114
|
+
active[instance] = true
|
|
115
|
+
-- Set up connections, UI, effects for this instance
|
|
116
|
+
end)
|
|
117
|
+
|
|
118
|
+
CollectionService:GetInstanceRemovedSignal(tag):Connect(function(instance)
|
|
119
|
+
active[instance] = nil
|
|
120
|
+
-- Cleanup: disconnect events, remove UI, stop effects
|
|
121
|
+
end)
|
|
122
|
+
|
|
123
|
+
-- Handle instances already present when script starts
|
|
124
|
+
for _, instance in CollectionService:GetTagged(tag) do
|
|
125
|
+
active[instance] = true
|
|
126
|
+
end
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
This handles stream-in and stream-out automatically. No WaitForChild needed.
|
|
130
|
+
|
|
131
|
+
#### Streamable Module (Sleitnick/RbxUtil)
|
|
132
|
+
|
|
133
|
+
For client-side observation of streaming instances with automatic cleanup:
|
|
134
|
+
|
|
135
|
+
```luau
|
|
136
|
+
local Streamable = require(ReplicatedStorage.Modules.Streamable)
|
|
137
|
+
|
|
138
|
+
local model = workspace:WaitForChild("MyModel")
|
|
139
|
+
|
|
140
|
+
local partStreamable = Streamable.new(model, "SomePart")
|
|
141
|
+
partStreamable:Observe(function(part, trove)
|
|
142
|
+
-- Called when part streams in
|
|
143
|
+
-- trove handles cleanup automatically when part streams out
|
|
144
|
+
trove:Add(function()
|
|
145
|
+
-- Cleanup code
|
|
146
|
+
end)
|
|
147
|
+
end)
|
|
148
|
+
|
|
149
|
+
-- Check existence directly
|
|
150
|
+
if partStreamable.Instance then
|
|
151
|
+
-- part is currently loaded
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
partStreamable:Destroy() -- clean up when done
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
#### Proactive Streaming
|
|
158
|
+
|
|
159
|
+
```luau
|
|
160
|
+
-- Server: pre-fetch area before teleporting player
|
|
161
|
+
Player:RequestStreamAroundAsync(targetLocation)
|
|
162
|
+
|
|
163
|
+
-- Keep an area permanently loaded for a player
|
|
164
|
+
Player:AddReplicationFocus(importantPart)
|
|
165
|
+
|
|
166
|
+
-- Remove when no longer needed
|
|
167
|
+
Player:RemoveReplicationFocus(importantPart)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Source: Roblox Instance Streaming docs, Sleitnick/RbxUtil Streamable (MIT)
|
|
171
|
+
|
|
172
|
+
### Anchoring
|
|
173
|
+
|
|
174
|
+
- **Anchor every static part.** Unanchored parts enter the physics solver even if they are not moving, consuming CPU every frame.
|
|
175
|
+
- Use `BasePart.Anchored = true` for terrain decorations, buildings, props, and anything that should not move.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## 4. Script Optimization
|
|
180
|
+
|
|
181
|
+
### Consolidated Heartbeat
|
|
182
|
+
|
|
183
|
+
Never scatter `RunService.Heartbeat:Connect(...)` across dozens of scripts. Consolidate into a single manager.
|
|
184
|
+
|
|
185
|
+
```lua
|
|
186
|
+
-- HeartbeatManager (single Script in ServerScriptService or a ModuleScript)
|
|
187
|
+
local RunService = game:GetService("RunService")
|
|
188
|
+
|
|
189
|
+
local HeartbeatManager = {}
|
|
190
|
+
HeartbeatManager._callbacks = {} :: { [string]: (dt: number) -> () }
|
|
191
|
+
|
|
192
|
+
function HeartbeatManager:Register(id: string, callback: (dt: number) -> ())
|
|
193
|
+
self._callbacks[id] = callback
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
function HeartbeatManager:Unregister(id: string)
|
|
197
|
+
self._callbacks[id] = nil
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
RunService.Heartbeat:Connect(function(dt: number)
|
|
201
|
+
for _, callback in self._callbacks do
|
|
202
|
+
callback(dt)
|
|
203
|
+
end
|
|
204
|
+
end)
|
|
205
|
+
|
|
206
|
+
return HeartbeatManager
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Usage from other modules:
|
|
210
|
+
|
|
211
|
+
```lua
|
|
212
|
+
local HeartbeatManager = require(path.to.HeartbeatManager)
|
|
213
|
+
|
|
214
|
+
HeartbeatManager:Register("EnemyAI", function(dt: number)
|
|
215
|
+
-- update all enemies
|
|
216
|
+
end)
|
|
217
|
+
|
|
218
|
+
-- When no longer needed:
|
|
219
|
+
HeartbeatManager:Unregister("EnemyAI")
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Table Pre-allocation
|
|
223
|
+
|
|
224
|
+
```lua
|
|
225
|
+
-- Pre-allocate a table with 100 slots
|
|
226
|
+
local results = table.create(100)
|
|
227
|
+
for i = 1, 100 do
|
|
228
|
+
results[i] = computeValue(i)
|
|
229
|
+
end
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### String Concatenation
|
|
233
|
+
|
|
234
|
+
```lua
|
|
235
|
+
-- BAD: creates a new string object every iteration
|
|
236
|
+
local result = ""
|
|
237
|
+
for i = 1, 1000 do
|
|
238
|
+
result = result .. tostring(i) .. ","
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
-- GOOD: build a table, join once
|
|
242
|
+
local parts = table.create(1000)
|
|
243
|
+
for i = 1, 1000 do
|
|
244
|
+
parts[i] = tostring(i)
|
|
245
|
+
end
|
|
246
|
+
local result = table.concat(parts, ",")
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## 5. Memory Management
|
|
252
|
+
|
|
253
|
+
### Disconnect Events
|
|
254
|
+
|
|
255
|
+
Every `:Connect()` call returns a `RBXScriptConnection`. Store it and disconnect when done.
|
|
256
|
+
|
|
257
|
+
```lua
|
|
258
|
+
-- Event Cleanup Pattern
|
|
259
|
+
local Cleaner = {}
|
|
260
|
+
Cleaner.__index = Cleaner
|
|
261
|
+
|
|
262
|
+
function Cleaner.new()
|
|
263
|
+
local self = setmetatable({}, Cleaner)
|
|
264
|
+
self._connections = {} :: { RBXScriptConnection }
|
|
265
|
+
self._instances = {} :: { Instance }
|
|
266
|
+
return self
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
function Cleaner:Add(connection: RBXScriptConnection)
|
|
270
|
+
table.insert(self._connections, connection)
|
|
271
|
+
return connection
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
function Cleaner:AddInstance(instance: Instance)
|
|
275
|
+
table.insert(self._instances, instance)
|
|
276
|
+
return instance
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
function Cleaner:Clean()
|
|
280
|
+
for _, conn in self._connections do
|
|
281
|
+
if conn.Connected then
|
|
282
|
+
conn:Disconnect()
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
table.clear(self._connections)
|
|
286
|
+
|
|
287
|
+
for _, inst in self._instances do
|
|
288
|
+
inst:Destroy()
|
|
289
|
+
end
|
|
290
|
+
table.clear(self._instances)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
return Cleaner
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Usage:
|
|
297
|
+
|
|
298
|
+
```lua
|
|
299
|
+
local Cleaner = require(path.to.Cleaner)
|
|
300
|
+
local cleaner = Cleaner.new()
|
|
301
|
+
|
|
302
|
+
cleaner:Add(workspace.ChildAdded:Connect(function(child)
|
|
303
|
+
print(child.Name, "added")
|
|
304
|
+
end))
|
|
305
|
+
|
|
306
|
+
cleaner:Add(Players.PlayerRemoving:Connect(function(player)
|
|
307
|
+
print(player.Name, "left")
|
|
308
|
+
end))
|
|
309
|
+
|
|
310
|
+
-- When this system shuts down or the player leaves:
|
|
311
|
+
cleaner:Clean()
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Destroy Instances Properly
|
|
315
|
+
|
|
316
|
+
- Always call `:Destroy()` rather than setting `Parent = nil`. `:Destroy()` locks the instance, disconnects all events on it, and marks it for garbage collection.
|
|
317
|
+
- Setting `Parent = nil` keeps the instance alive if anything still references it.
|
|
318
|
+
|
|
319
|
+
### Avoid Reference Cycles
|
|
320
|
+
|
|
321
|
+
```lua
|
|
322
|
+
-- BAD: mutual references prevent garbage collection
|
|
323
|
+
local a = {}
|
|
324
|
+
local b = {}
|
|
325
|
+
a.ref = b
|
|
326
|
+
b.ref = a
|
|
327
|
+
-- Neither a nor b can be collected until both references are broken
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Break references explicitly when done: `a.ref = nil; b.ref = nil`.
|
|
331
|
+
|
|
332
|
+
### Instance.Destroying
|
|
333
|
+
|
|
334
|
+
Use `Instance.Destroying` to run cleanup when an instance is about to be destroyed:
|
|
335
|
+
|
|
336
|
+
```lua
|
|
337
|
+
local part = Instance.new("Part")
|
|
338
|
+
part.Destroying:Connect(function()
|
|
339
|
+
-- clean up related data, disconnect connections, etc.
|
|
340
|
+
end)
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Debris Service
|
|
344
|
+
|
|
345
|
+
For timed cleanup of temporary instances (projectiles, effects):
|
|
346
|
+
|
|
347
|
+
```lua
|
|
348
|
+
local Debris = game:GetService("Debris")
|
|
349
|
+
local bullet = Instance.new("Part")
|
|
350
|
+
bullet.Parent = workspace
|
|
351
|
+
Debris:AddItem(bullet, 5) -- destroyed after 5 seconds
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## 6. Network Optimization
|
|
357
|
+
|
|
358
|
+
### Minimize RemoteEvent Data Size
|
|
359
|
+
|
|
360
|
+
- Send only what changed, not full state.
|
|
361
|
+
- Use numeric IDs instead of long string keys when possible.
|
|
362
|
+
- Avoid sending Instance references when a name or ID suffices.
|
|
363
|
+
|
|
364
|
+
### Batch Related Remotes
|
|
365
|
+
|
|
366
|
+
```lua
|
|
367
|
+
-- BAD: three separate remote calls
|
|
368
|
+
remoteHealth:FireClient(player, health)
|
|
369
|
+
remoteAmmo:FireClient(player, ammo)
|
|
370
|
+
remoteStamina:FireClient(player, stamina)
|
|
371
|
+
|
|
372
|
+
-- GOOD: one call with a table
|
|
373
|
+
remotePlayerState:FireClient(player, {
|
|
374
|
+
health = health,
|
|
375
|
+
ammo = ammo,
|
|
376
|
+
stamina = stamina,
|
|
377
|
+
})
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### UnreliableRemoteEvent
|
|
381
|
+
|
|
382
|
+
For high-frequency, non-critical data such as position or rotation updates, use `UnreliableRemoteEvent`. Dropped packets are acceptable because the next update will correct the state.
|
|
383
|
+
|
|
384
|
+
```lua
|
|
385
|
+
-- In ReplicatedStorage, create an UnreliableRemoteEvent named "PositionSync"
|
|
386
|
+
local posSync = ReplicatedStorage:WaitForChild("PositionSync")
|
|
387
|
+
|
|
388
|
+
-- Server: fire frequently without guaranteeing delivery
|
|
389
|
+
RunService.Heartbeat:Connect(function()
|
|
390
|
+
for _, player in Players:GetPlayers() do
|
|
391
|
+
posSync:FireClient(player, npcPositions)
|
|
392
|
+
end
|
|
393
|
+
end)
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### Compress Large Data
|
|
397
|
+
|
|
398
|
+
- Strip unnecessary keys before sending.
|
|
399
|
+
- Use short key names (`hp` instead of `hitPoints`).
|
|
400
|
+
- Consider delta compression: send only values that changed since the last update.
|
|
401
|
+
|
|
402
|
+
### Reduce Replication
|
|
403
|
+
|
|
404
|
+
- Set visual-only properties on the client (particle colors, UI tweens).
|
|
405
|
+
- Properties changed on the server replicate to all clients automatically, which consumes bandwidth.
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## 7. Rendering Optimization
|
|
410
|
+
|
|
411
|
+
### Level of Detail (LOD)
|
|
412
|
+
|
|
413
|
+
Create multiple versions of a model at different detail levels and swap based on distance:
|
|
414
|
+
|
|
415
|
+
```lua
|
|
416
|
+
local function setLOD(model: Model, playerPosition: Vector3)
|
|
417
|
+
local distance = (model:GetPivot().Position - playerPosition).Magnitude
|
|
418
|
+
if distance < 100 then
|
|
419
|
+
-- show high-detail version
|
|
420
|
+
elseif distance < 300 then
|
|
421
|
+
-- show medium-detail version
|
|
422
|
+
else
|
|
423
|
+
-- show low-detail version or hide
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Roblox also has built-in `MeshPart.RenderFidelity` (Automatic, Performance, Precise) which controls mesh LOD.
|
|
429
|
+
|
|
430
|
+
### Draw Distance Limits
|
|
431
|
+
|
|
432
|
+
- Use `BasePart.CastShadow = false` on distant or small parts.
|
|
433
|
+
- Disable unnecessary `SurfaceLight`, `PointLight`, `SpotLight` on distant objects.
|
|
434
|
+
- With StreamingEnabled, the engine handles draw distance automatically.
|
|
435
|
+
|
|
436
|
+
### Particle Count Budgets
|
|
437
|
+
|
|
438
|
+
| Property | Recommended Max |
|
|
439
|
+
|---|---|
|
|
440
|
+
| Particles per emitter (`Rate`) | ~200 |
|
|
441
|
+
| Total active emitters in view | ~20 |
|
|
442
|
+
| Beam segments (`Segments`) | 10-20 |
|
|
443
|
+
| Trail `MaxLength` | Keep short for mobile |
|
|
444
|
+
|
|
445
|
+
- Set `ParticleEmitter.Enabled = false` when off-screen or far away.
|
|
446
|
+
- Use fewer, larger particles instead of many small ones.
|
|
447
|
+
|
|
448
|
+
### Texture Resolution
|
|
449
|
+
|
|
450
|
+
| Use Case | Max Resolution |
|
|
451
|
+
|---|---|
|
|
452
|
+
| General props, walls, floors | 512x512 |
|
|
453
|
+
| Hero assets (player characters, key items) | 1024x1024 |
|
|
454
|
+
| UI icons, decals | 256x256 to 512x512 |
|
|
455
|
+
| Sky/environment | 1024x1024 |
|
|
456
|
+
|
|
457
|
+
- Use `Decal` over `Texture` when the surface only needs one face covered. Decals are simpler to render.
|
|
458
|
+
- Compress textures before uploading. Avoid PNG when JPEG quality is acceptable.
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
## 8. Mobile-Specific Optimization
|
|
463
|
+
|
|
464
|
+
### Part Counts
|
|
465
|
+
|
|
466
|
+
- Target 30-50% fewer parts than desktop. If the desktop budget is 10K parts, aim for 5-7K on mobile.
|
|
467
|
+
- Use `UserInputService:GetPlatform()` or screen size to detect mobile and reduce detail.
|
|
468
|
+
|
|
469
|
+
### Simplified Particle Effects
|
|
470
|
+
|
|
471
|
+
- Halve the `Rate` of particle emitters on mobile.
|
|
472
|
+
- Reduce `Lifetime` to keep fewer active particles.
|
|
473
|
+
- Disable non-essential emitters entirely.
|
|
474
|
+
|
|
475
|
+
### Touch-Optimized UI
|
|
476
|
+
|
|
477
|
+
- Minimum touch target size: **44x44 points** (following Apple HIG).
|
|
478
|
+
- Add padding between interactive elements.
|
|
479
|
+
- Use `GuiObject.Active = true` to ensure touch events register.
|
|
480
|
+
- Avoid hover-dependent UI (mobile has no hover state).
|
|
481
|
+
|
|
482
|
+
### Reduced Draw Distance
|
|
483
|
+
|
|
484
|
+
```lua
|
|
485
|
+
if UserInputService.TouchEnabled then
|
|
486
|
+
workspace.StreamingTargetRadius = 128 -- lower than desktop
|
|
487
|
+
workspace.StreamingMinRadius = 48
|
|
488
|
+
end
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
### Memory-Efficient Assets
|
|
492
|
+
|
|
493
|
+
- Use lower-resolution textures on mobile (256x256 where desktop uses 512x512).
|
|
494
|
+
- Reduce mesh polygon counts for mobile LOD models.
|
|
495
|
+
- Monitor memory with `Stats():GetTotalMemoryUsageMb()` and warn/act if approaching 500 MB.
|
|
496
|
+
|
|
497
|
+
### Test on Low-End Devices
|
|
498
|
+
|
|
499
|
+
- Test on devices with 2-3 GB RAM (older iPads, budget Android phones).
|
|
500
|
+
- Use the Roblox mobile emulator in Studio, but always verify on real hardware.
|
|
501
|
+
- Check for thermal throttling during extended play sessions.
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
505
|
+
## 9. Profiling Tools
|
|
506
|
+
|
|
507
|
+
### MicroProfiler (Ctrl+F6 in Studio)
|
|
508
|
+
|
|
509
|
+
The MicroProfiler displays a real-time flame graph of what the engine is doing each frame.
|
|
510
|
+
|
|
511
|
+
**How to read it:**
|
|
512
|
+
|
|
513
|
+
1. Press `Ctrl+F6` to open. Press `Ctrl+P` to pause and inspect a frame.
|
|
514
|
+
2. Each horizontal bar is a task. Width represents time spent.
|
|
515
|
+
3. Look for bars that are unusually wide - these are your hot frames.
|
|
516
|
+
4. Common labels to watch:
|
|
517
|
+
- `Heartbeat` - your Heartbeat scripts. If wide, your per-frame logic is too heavy.
|
|
518
|
+
- `Physics` - collision and simulation. Reduce unanchored parts.
|
|
519
|
+
- `Render/Perform` - GPU-bound. Reduce draw calls, textures, particles.
|
|
520
|
+
- `Replication` - network overhead. Reduce remote calls and replicated property changes.
|
|
521
|
+
5. Click a bar to see details: script name, line number, time in microseconds.
|
|
522
|
+
6. Use the `microprofiler` dump (`Ctrl+F6` > `Dump`) to save a `.html` file for offline analysis.
|
|
523
|
+
|
|
524
|
+
### F9 Developer Console
|
|
525
|
+
|
|
526
|
+
- Press `F9` in-game or in Studio to open.
|
|
527
|
+
- **Log** tab: errors, warnings, print output.
|
|
528
|
+
- **Memory** tab: breakdown by category (Instances, PhysicsParts, Sounds, Scripts, Signals, etc.).
|
|
529
|
+
- **Stats** tab: FPS, ping, data send/receive rates.
|
|
530
|
+
- **Server Stats** (in-game): server heartbeat time, physics step time.
|
|
531
|
+
|
|
532
|
+
### Stats Service (Programmatic)
|
|
533
|
+
|
|
534
|
+
```lua
|
|
535
|
+
local Stats = game:GetService("Stats")
|
|
536
|
+
|
|
537
|
+
-- Total memory in MB
|
|
538
|
+
local totalMemory = Stats:GetTotalMemoryUsageMb()
|
|
539
|
+
|
|
540
|
+
-- Specific categories
|
|
541
|
+
local instanceMemory = Stats:GetMemoryUsageMbForTag(Enum.DeveloperMemoryTag.Instances)
|
|
542
|
+
local scriptMemory = Stats:GetMemoryUsageMbForTag(Enum.DeveloperMemoryTag.LuaHeap)
|
|
543
|
+
|
|
544
|
+
print(string.format("Total: %.1f MB | Instances: %.1f MB | Lua: %.1f MB",
|
|
545
|
+
totalMemory, instanceMemory, scriptMemory))
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
---
|
|
549
|
+
|
|
550
|
+
## 10. Best Practices
|
|
551
|
+
|
|
552
|
+
### Profile Before Optimizing
|
|
553
|
+
|
|
554
|
+
Never guess where the bottleneck is. Use the MicroProfiler and memory stats to find the actual hot path before changing code.
|
|
555
|
+
|
|
556
|
+
### Optimize Hot Paths First
|
|
557
|
+
|
|
558
|
+
Focus effort on code that runs every frame (Heartbeat, RenderStepped) or on every player action. Code that runs once at startup is rarely worth optimizing.
|
|
559
|
+
|
|
560
|
+
### Spatial Queries Over Brute Force
|
|
561
|
+
|
|
562
|
+
```lua
|
|
563
|
+
-- BAD: loop over every part in workspace
|
|
564
|
+
for _, part in workspace:GetDescendants() do
|
|
565
|
+
if (part.Position - origin).Magnitude < 50 then
|
|
566
|
+
-- ...
|
|
567
|
+
end
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
-- GOOD: spatial query
|
|
571
|
+
local params = OverlapParams.new()
|
|
572
|
+
params.FilterType = Enum.RaycastFilterType.Include
|
|
573
|
+
params.FilterDescendantsInstances = { workspace.Enemies }
|
|
574
|
+
|
|
575
|
+
local parts = workspace:GetPartBoundsInBox(
|
|
576
|
+
CFrame.new(origin),
|
|
577
|
+
Vector3.new(100, 100, 100), -- 50-stud radius box
|
|
578
|
+
params
|
|
579
|
+
)
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
### Object Pooling
|
|
583
|
+
|
|
584
|
+
Reuse instances instead of creating and destroying them repeatedly.
|
|
585
|
+
|
|
586
|
+
```lua
|
|
587
|
+
-- Object Pool Pattern
|
|
588
|
+
local ObjectPool = {}
|
|
589
|
+
ObjectPool.__index = ObjectPool
|
|
590
|
+
|
|
591
|
+
function ObjectPool.new(template: Instance, initialSize: number)
|
|
592
|
+
|
|
593
|
+
local self = setmetatable({}, ObjectPool)
|
|
594
|
+
self._template = template
|
|
595
|
+
self._available = table.create(initialSize)
|
|
596
|
+
self._active = {} :: { [Instance]: boolean }
|
|
597
|
+
|
|
598
|
+
-- Pre-populate
|
|
599
|
+
for i = 1, initialSize do
|
|
600
|
+
local clone = template:Clone()
|
|
601
|
+
clone.Parent = nil
|
|
602
|
+
table.insert(self._available, clone)
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
return self
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
function ObjectPool:Get(): Instance
|
|
609
|
+
local obj: Instance
|
|
610
|
+
if #self._available > 0 then
|
|
611
|
+
obj = table.remove(self._available)
|
|
612
|
+
else
|
|
613
|
+
obj = self._template:Clone()
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
self._active[obj] = true
|
|
617
|
+
return obj
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
function ObjectPool:Return(obj: Instance)
|
|
621
|
+
if not self._active[obj] then
|
|
622
|
+
return
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
self._active[obj] = nil
|
|
626
|
+
obj.Parent = nil
|
|
627
|
+
|
|
628
|
+
-- Reset state as needed (position, visibility, etc.)
|
|
629
|
+
if obj:IsA("BasePart") then
|
|
630
|
+
obj.CFrame = CFrame.new(0, -1000, 0) -- move off-screen
|
|
631
|
+
obj.Anchored = true
|
|
632
|
+
obj.Velocity = Vector3.zero
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
table.insert(self._available, obj)
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
function ObjectPool:ReturnAll()
|
|
639
|
+
for obj in self._active do
|
|
640
|
+
self:Return(obj)
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
function ObjectPool:Destroy()
|
|
645
|
+
for obj in self._active do
|
|
646
|
+
obj:Destroy()
|
|
647
|
+
end
|
|
648
|
+
for _, obj in self._available do
|
|
649
|
+
obj:Destroy()
|
|
650
|
+
end
|
|
651
|
+
table.clear(self._active)
|
|
652
|
+
table.clear(self._available)
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
return ObjectPool
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
Usage:
|
|
659
|
+
|
|
660
|
+
```lua
|
|
661
|
+
local ObjectPool = require(path.to.ObjectPool)
|
|
662
|
+
local bulletTemplate = ReplicatedStorage.Assets.Bullet
|
|
663
|
+
local bulletPool = ObjectPool.new(bulletTemplate, 50)
|
|
664
|
+
|
|
665
|
+
-- Spawn a bullet
|
|
666
|
+
local bullet = bulletPool:Get()
|
|
667
|
+
bullet.CFrame = firePoint
|
|
668
|
+
bullet.Parent = workspace
|
|
669
|
+
|
|
670
|
+
-- Return when done
|
|
671
|
+
bulletPool:Return(bullet)
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
### Lazy-Load Distant Content
|
|
675
|
+
|
|
676
|
+
Do not load all assets at game start. Use StreamingEnabled or manually load content as the player approaches.
|
|
677
|
+
|
|
678
|
+
```lua
|
|
679
|
+
local function onPlayerMoved(position: Vector3)
|
|
680
|
+
for _, zone in zones do
|
|
681
|
+
local distance = (zone.center - position).Magnitude
|
|
682
|
+
if distance < zone.loadRadius and not zone.loaded then
|
|
683
|
+
zone:Load()
|
|
684
|
+
elseif distance > zone.unloadRadius and zone.loaded then
|
|
685
|
+
zone:Unload()
|
|
686
|
+
end
|
|
687
|
+
end
|
|
688
|
+
end
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
---
|
|
692
|
+
|
|
693
|
+
## 11. Anti-Patterns
|
|
694
|
+
|
|
695
|
+
### Premature Optimization
|
|
696
|
+
|
|
697
|
+
Optimizing code that is not a bottleneck wastes development time and often makes code harder to read. Always profile first.
|
|
698
|
+
|
|
699
|
+
### Creating/Destroying Instances in Heartbeat
|
|
700
|
+
|
|
701
|
+
```lua
|
|
702
|
+
-- ANTI-PATTERN: creating parts every frame
|
|
703
|
+
RunService.Heartbeat:Connect(function()
|
|
704
|
+
local part = Instance.new("Part") -- allocation every frame
|
|
705
|
+
part.Parent = workspace
|
|
706
|
+
task.delay(1, function()
|
|
707
|
+
part:Destroy()
|
|
708
|
+
end)
|
|
709
|
+
end)
|
|
710
|
+
|
|
711
|
+
-- FIX: use an object pool (see section 10)
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
### Large Uncompressed Data Over Remotes
|
|
715
|
+
|
|
716
|
+
```lua
|
|
717
|
+
-- ANTI-PATTERN: sending entire inventory every update
|
|
718
|
+
remote:FireClient(player, fullInventoryTable) -- could be thousands of entries
|
|
719
|
+
|
|
720
|
+
-- FIX: send only changes
|
|
721
|
+
remote:FireClient(player, { added = { itemId }, removed = { oldItemId } })
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
### Not Testing on Mobile
|
|
725
|
+
|
|
726
|
+
A game that runs at 60 fps on a gaming PC may run at 10 fps on a phone. Always test on actual mobile hardware, not just the Studio emulator.
|
|
727
|
+
|
|
728
|
+
### Ignoring Memory Leaks
|
|
729
|
+
|
|
730
|
+
Common leak sources:
|
|
731
|
+
|
|
732
|
+
- Event connections that are never disconnected.
|
|
733
|
+
- Instances removed from the hierarchy but still referenced in a table.
|
|
734
|
+
- Closures capturing large upvalues that outlive their usefulness.
|
|
735
|
+
- Module-level tables that grow indefinitely without cleanup.
|
|
736
|
+
|
|
737
|
+
Detect leaks by monitoring `Stats:GetTotalMemoryUsageMb()` over time. If memory grows continuously during gameplay without stabilizing, there is a leak.
|
|
738
|
+
|
|
739
|
+
```lua
|
|
740
|
+
-- Simple memory monitor
|
|
741
|
+
local lastMemory = 0
|
|
742
|
+
task.spawn(function()
|
|
743
|
+
while true do
|
|
744
|
+
local current = game:GetService("Stats"):GetTotalMemoryUsageMb()
|
|
745
|
+
local delta = current - lastMemory
|
|
746
|
+
if delta > 10 then
|
|
747
|
+
warn(string.format("Memory spike: +%.1f MB (total: %.1f MB)", delta, current))
|
|
748
|
+
end
|
|
749
|
+
lastMemory = current
|
|
750
|
+
task.wait(10)
|
|
751
|
+
end
|
|
752
|
+
end)
|
|
753
|
+
```
|