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,1325 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: roblox-animation-vfx
|
|
3
|
+
description: >
|
|
4
|
+
Animations, particles, tweens, ContentProvider, visual effects.
|
|
5
|
+
last_reviewed: 2026-05-25
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<!-- Source: brockmartin/roblox-game-skill (MIT) -->
|
|
9
|
+
|
|
10
|
+
# Animation & VFX Reference
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
Load this reference when working on:
|
|
15
|
+
|
|
16
|
+
- Character or NPC animations (idle, walk, attack, emotes)
|
|
17
|
+
- Particle effects (fire, smoke, sparkles, magic, weather)
|
|
18
|
+
- Beams and trails (lasers, sword swings, magic projectiles)
|
|
19
|
+
- TweenService-driven visual feedback (hit flashes, pulses, transitions)
|
|
20
|
+
- Lighting and post-processing (mood, atmosphere, glow)
|
|
21
|
+
- Sound design and positional audio
|
|
22
|
+
- Camera effects (shake, zoom, cutscenes)
|
|
23
|
+
- General visual polish and juice
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Quick Reference
|
|
28
|
+
|
|
29
|
+
**Load Full Reference below only when you need specific property values, recipes, or implementation details.**
|
|
30
|
+
|
|
31
|
+
Key rules:
|
|
32
|
+
- Animations need uploaded AnimationIds (rbxassetid://). Never invent IDs.
|
|
33
|
+
- Priority order: Core < Idle < Movement < Action. Higher priority overrides lower on same track.
|
|
34
|
+
- Always use `Animator` (on Humanoid/AnimationController), not deprecated `Humanoid:LoadAnimation()`
|
|
35
|
+
- MarkerReachedSignal for syncing sounds/VFX to animation frames
|
|
36
|
+
- ParticleEmitter: Rate=0 + Emit(count) for burst effects. Enabled=false to stop new particles.
|
|
37
|
+
- Beams need Attachment0 + Attachment1. Trails need one Attachment.
|
|
38
|
+
- Highlight: parent to target or set Adornee. Max 255 per client. AlwaysOnTop to see through geometry.
|
|
39
|
+
- TweenService: create TweenInfo once, reuse. Chain with Completed event, don't nest.
|
|
40
|
+
- Post-processing: keep subtle. Bloom + ColorCorrection + DepthOfField cover most moods.
|
|
41
|
+
- Clean up: Destroy() particles/beams when done. Use Trove for lifecycle.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Full Reference
|
|
46
|
+
|
|
47
|
+
## Character Animation
|
|
48
|
+
|
|
49
|
+
### Animator Service on Humanoid
|
|
50
|
+
|
|
51
|
+
Every `Humanoid` has (or should have) an `Animator` child. The `Animator` is the engine that plays, blends, and prioritizes animation tracks on a character rig.
|
|
52
|
+
|
|
53
|
+
```luau
|
|
54
|
+
local character = player.Character or player.CharacterAdded:Wait()
|
|
55
|
+
local humanoid = character:WaitForChild("Humanoid")
|
|
56
|
+
local animator = humanoid:FindFirstChildOfClass("Animator")
|
|
57
|
+
or humanoid:WaitForChild("Animator")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Loading and Playing Animations
|
|
61
|
+
|
|
62
|
+
```luau
|
|
63
|
+
-- 1. Create an Animation instance with the asset ID
|
|
64
|
+
local slashAnim = Instance.new("Animation")
|
|
65
|
+
slashAnim.AnimationId = "rbxassetid://123456789"
|
|
66
|
+
|
|
67
|
+
-- 2. Load it through the Animator (returns an AnimationTrack)
|
|
68
|
+
local slashTrack = animator:LoadAnimation(slashAnim)
|
|
69
|
+
|
|
70
|
+
-- 3. Play / Stop
|
|
71
|
+
slashTrack:Play()
|
|
72
|
+
-- Optional fade time and weight
|
|
73
|
+
slashTrack:Play(0.2) -- 0.2s fade-in
|
|
74
|
+
slashTrack:Stop(0.3) -- 0.3s fade-out
|
|
75
|
+
|
|
76
|
+
-- 4. Adjust speed at runtime
|
|
77
|
+
slashTrack:AdjustSpeed(1.5) -- 1.5x playback
|
|
78
|
+
slashTrack:AdjustWeight(0.8) -- 80% blend weight
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Animation Priorities
|
|
82
|
+
|
|
83
|
+
Priorities determine which animation wins when multiple tracks affect the same joints. Higher priority overrides lower.
|
|
84
|
+
|
|
85
|
+
| Priority | Use Case |
|
|
86
|
+
| ------------------------------------- | ---------------------------------- |
|
|
87
|
+
| `Enum.AnimationPriority.Idle` | Breathing, idle sway |
|
|
88
|
+
| `Enum.AnimationPriority.Movement` | Walk, run, jump, fall |
|
|
89
|
+
| `Enum.AnimationPriority.Action` | Attack, interact, emote |
|
|
90
|
+
| `Enum.AnimationPriority.Action2` | Higher-priority actions |
|
|
91
|
+
| `Enum.AnimationPriority.Action3` | Even higher-priority actions |
|
|
92
|
+
| `Enum.AnimationPriority.Action4` | Highest action tier |
|
|
93
|
+
| `Enum.AnimationPriority.Core` | Internal Roblox (avoid overriding) |
|
|
94
|
+
|
|
95
|
+
```luau
|
|
96
|
+
slashTrack.Priority = Enum.AnimationPriority.Action
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### MarkerReachedSignal
|
|
100
|
+
|
|
101
|
+
Animation events let you fire logic at exact frames inside an animation (set markers in the Animation Editor).
|
|
102
|
+
|
|
103
|
+
```luau
|
|
104
|
+
slashTrack:GetMarkerReachedSignal("HitFrame"):Connect(function(paramValue: string)
|
|
105
|
+
-- Spawn hitbox, play sound, emit particles, etc.
|
|
106
|
+
print("Hit frame reached!", paramValue)
|
|
107
|
+
end)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Blending Between Animations
|
|
111
|
+
|
|
112
|
+
Roblox automatically blends overlapping tracks based on priority and weight. To cross-fade manually:
|
|
113
|
+
|
|
114
|
+
```luau
|
|
115
|
+
local walkTrack = animator:LoadAnimation(walkAnim)
|
|
116
|
+
local runTrack = animator:LoadAnimation(runAnim)
|
|
117
|
+
|
|
118
|
+
walkTrack:Play(0.2)
|
|
119
|
+
|
|
120
|
+
-- Later, cross-fade to run
|
|
121
|
+
walkTrack:Stop(0.3)
|
|
122
|
+
runTrack:Play(0.3)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
For partial-body layering (e.g., upper body attack while legs run), set different priorities and ensure the lower-priority animation only drives lower body joints.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## AnimationController
|
|
130
|
+
|
|
131
|
+
Use `AnimationController` for anything that is NOT a `Humanoid` -- props, doors, creatures with custom rigs, cutscene actors, etc.
|
|
132
|
+
|
|
133
|
+
```luau
|
|
134
|
+
local model = workspace.DragonNPC
|
|
135
|
+
local animController = Instance.new("AnimationController")
|
|
136
|
+
animController.Parent = model
|
|
137
|
+
|
|
138
|
+
local flyAnim = Instance.new("Animation")
|
|
139
|
+
flyAnim.AnimationId = "rbxassetid://987654321"
|
|
140
|
+
|
|
141
|
+
local flyTrack = animController:LoadAnimation(flyAnim)
|
|
142
|
+
flyTrack.Looped = true
|
|
143
|
+
flyTrack:Play()
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Custom Rigs
|
|
147
|
+
|
|
148
|
+
- The model needs `Motor6D` joints connecting its parts, just like a character rig.
|
|
149
|
+
- Root part should be the `PrimaryPart` of the model.
|
|
150
|
+
- Animations are authored in the Animation Editor against this rig, then exported.
|
|
151
|
+
|
|
152
|
+
### Attaching to Models
|
|
153
|
+
|
|
154
|
+
```luau
|
|
155
|
+
-- Typical setup for an animated NPC without Humanoid
|
|
156
|
+
local npc = Instance.new("Model")
|
|
157
|
+
npc.Name = "CrystalGolem"
|
|
158
|
+
|
|
159
|
+
local rootPart = Instance.new("Part")
|
|
160
|
+
rootPart.Name = "HumanoidRootPart" -- convention for animation rigs
|
|
161
|
+
rootPart.Anchored = false
|
|
162
|
+
rootPart.Parent = npc
|
|
163
|
+
npc.PrimaryPart = rootPart
|
|
164
|
+
|
|
165
|
+
local animController = Instance.new("AnimationController")
|
|
166
|
+
animController.Parent = npc
|
|
167
|
+
|
|
168
|
+
-- Add Motor6D joints, mesh parts, then load animations
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Particle Effects
|
|
174
|
+
|
|
175
|
+
### ParticleEmitter Core Properties
|
|
176
|
+
|
|
177
|
+
| Property | Type | Description |
|
|
178
|
+
| --------------- | ----------------- | -------------------------------------------------- |
|
|
179
|
+
| `Rate` | `number` | Particles emitted per second (0 = manual Emit()) |
|
|
180
|
+
| `Lifetime` | `NumberRange` | How long each particle lives (seconds) |
|
|
181
|
+
| `Speed` | `NumberRange` | Initial velocity (studs/second) |
|
|
182
|
+
| `SpreadAngle` | `Vector2` | Cone spread in X and Y (degrees) |
|
|
183
|
+
| `Size` | `NumberSequence` | Size over particle lifetime |
|
|
184
|
+
| `Color` | `ColorSequence` | Color over particle lifetime |
|
|
185
|
+
| `Transparency` | `NumberSequence` | Transparency over particle lifetime |
|
|
186
|
+
| `Texture` | `string` | Decal/image asset ID for particle appearance |
|
|
187
|
+
| `RotSpeed` | `NumberRange` | Rotation speed (degrees/second) |
|
|
188
|
+
| `Acceleration` | `Vector3` | Constant force (gravity = `Vector3.new(0,-10,0)`) |
|
|
189
|
+
| `Drag` | `number` | Air resistance (0 = none, higher = more drag) |
|
|
190
|
+
| `LightEmission` | `number` | 0-1, additive blending (1 = fully additive/glowy) |
|
|
191
|
+
| `LightInfluence`| `number` | 0-1, how much scene lighting affects particles |
|
|
192
|
+
| `ZOffset` | `number` | Render order offset toward/away from camera |
|
|
193
|
+
| `Orientation` | `Enum.ParticleOrientation` | FacingCamera, VelocityParallel, etc. |
|
|
194
|
+
|
|
195
|
+
### NumberSequence and ColorSequence
|
|
196
|
+
|
|
197
|
+
```luau
|
|
198
|
+
-- Size: start at 1, peak at 2 at midlife, shrink to 0
|
|
199
|
+
local sizeSeq = NumberSequence.new({
|
|
200
|
+
NumberSequenceKeypoint.new(0, 1),
|
|
201
|
+
NumberSequenceKeypoint.new(0.5, 2),
|
|
202
|
+
NumberSequenceKeypoint.new(1, 0),
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
-- Color: orange to red
|
|
206
|
+
local colorSeq = ColorSequence.new({
|
|
207
|
+
ColorSequenceKeypoint.new(0, Color3.fromRGB(255, 170, 0)),
|
|
208
|
+
ColorSequenceKeypoint.new(1, Color3.fromRGB(200, 30, 0)),
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
-- Transparency: fade in then fade out
|
|
212
|
+
local transSeq = NumberSequence.new({
|
|
213
|
+
NumberSequenceKeypoint.new(0, 1),
|
|
214
|
+
NumberSequenceKeypoint.new(0.1, 0),
|
|
215
|
+
NumberSequenceKeypoint.new(0.8, 0),
|
|
216
|
+
NumberSequenceKeypoint.new(1, 1),
|
|
217
|
+
})
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Common Effect Recipes
|
|
221
|
+
|
|
222
|
+
#### Fire
|
|
223
|
+
|
|
224
|
+
```luau
|
|
225
|
+
local fire = Instance.new("ParticleEmitter")
|
|
226
|
+
fire.Rate = 80
|
|
227
|
+
fire.Lifetime = NumberRange.new(0.4, 0.8)
|
|
228
|
+
fire.Speed = NumberRange.new(3, 6)
|
|
229
|
+
fire.SpreadAngle = Vector2.new(15, 15)
|
|
230
|
+
fire.Size = NumberSequence.new({
|
|
231
|
+
NumberSequenceKeypoint.new(0, 0.5),
|
|
232
|
+
NumberSequenceKeypoint.new(0.3, 1.5),
|
|
233
|
+
NumberSequenceKeypoint.new(1, 0),
|
|
234
|
+
})
|
|
235
|
+
fire.Color = ColorSequence.new({
|
|
236
|
+
ColorSequenceKeypoint.new(0, Color3.fromRGB(255, 220, 50)),
|
|
237
|
+
ColorSequenceKeypoint.new(0.4, Color3.fromRGB(255, 100, 0)),
|
|
238
|
+
ColorSequenceKeypoint.new(1, Color3.fromRGB(100, 20, 0)),
|
|
239
|
+
})
|
|
240
|
+
fire.Transparency = NumberSequence.new({
|
|
241
|
+
NumberSequenceKeypoint.new(0, 0.3),
|
|
242
|
+
NumberSequenceKeypoint.new(1, 1),
|
|
243
|
+
})
|
|
244
|
+
fire.LightEmission = 1
|
|
245
|
+
fire.Acceleration = Vector3.new(0, 4, 0)
|
|
246
|
+
fire.Texture = "rbxasset://textures/particles/fire_main.dds"
|
|
247
|
+
fire.Parent = somePart
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
#### Smoke
|
|
251
|
+
|
|
252
|
+
```luau
|
|
253
|
+
local smoke = Instance.new("ParticleEmitter")
|
|
254
|
+
smoke.Rate = 30
|
|
255
|
+
smoke.Lifetime = NumberRange.new(2, 4)
|
|
256
|
+
smoke.Speed = NumberRange.new(1, 3)
|
|
257
|
+
smoke.SpreadAngle = Vector2.new(30, 30)
|
|
258
|
+
smoke.Size = NumberSequence.new({
|
|
259
|
+
NumberSequenceKeypoint.new(0, 1),
|
|
260
|
+
NumberSequenceKeypoint.new(1, 5),
|
|
261
|
+
})
|
|
262
|
+
smoke.Color = ColorSequence.new(Color3.fromRGB(120, 120, 120))
|
|
263
|
+
smoke.Transparency = NumberSequence.new({
|
|
264
|
+
NumberSequenceKeypoint.new(0, 0.5),
|
|
265
|
+
NumberSequenceKeypoint.new(1, 1),
|
|
266
|
+
})
|
|
267
|
+
smoke.RotSpeed = NumberRange.new(-30, 30)
|
|
268
|
+
smoke.Acceleration = Vector3.new(0, 2, 0)
|
|
269
|
+
smoke.LightInfluence = 1
|
|
270
|
+
smoke.Parent = somePart
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
#### Sparkles / Magic Particles
|
|
274
|
+
|
|
275
|
+
```luau
|
|
276
|
+
local sparkle = Instance.new("ParticleEmitter")
|
|
277
|
+
sparkle.Rate = 40
|
|
278
|
+
sparkle.Lifetime = NumberRange.new(0.5, 1.2)
|
|
279
|
+
sparkle.Speed = NumberRange.new(2, 5)
|
|
280
|
+
sparkle.SpreadAngle = Vector2.new(180, 180)
|
|
281
|
+
sparkle.Size = NumberSequence.new({
|
|
282
|
+
NumberSequenceKeypoint.new(0, 0.3),
|
|
283
|
+
NumberSequenceKeypoint.new(0.5, 0.6),
|
|
284
|
+
NumberSequenceKeypoint.new(1, 0),
|
|
285
|
+
})
|
|
286
|
+
sparkle.Color = ColorSequence.new({
|
|
287
|
+
ColorSequenceKeypoint.new(0, Color3.fromRGB(200, 200, 255)),
|
|
288
|
+
ColorSequenceKeypoint.new(1, Color3.fromRGB(100, 100, 255)),
|
|
289
|
+
})
|
|
290
|
+
sparkle.LightEmission = 1
|
|
291
|
+
sparkle.Texture = "rbxasset://textures/particles/sparkles_main.dds"
|
|
292
|
+
sparkle.Parent = somePart
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
#### Rain
|
|
296
|
+
|
|
297
|
+
```luau
|
|
298
|
+
local rain = Instance.new("ParticleEmitter")
|
|
299
|
+
rain.Rate = 300
|
|
300
|
+
rain.Lifetime = NumberRange.new(0.8, 1.2)
|
|
301
|
+
rain.Speed = NumberRange.new(40, 60)
|
|
302
|
+
rain.SpreadAngle = Vector2.new(5, 5)
|
|
303
|
+
rain.Size = NumberSequence.new(0.05)
|
|
304
|
+
rain.Color = ColorSequence.new(Color3.fromRGB(180, 200, 220))
|
|
305
|
+
rain.Transparency = NumberSequence.new(0.4)
|
|
306
|
+
rain.Acceleration = Vector3.new(0, -80, 0)
|
|
307
|
+
rain.Drag = 0
|
|
308
|
+
rain.Orientation = Enum.ParticleOrientation.VelocityParallel
|
|
309
|
+
rain.Parent = largeCoverPart -- position above the play area
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
#### Snow
|
|
313
|
+
|
|
314
|
+
```luau
|
|
315
|
+
local snow = Instance.new("ParticleEmitter")
|
|
316
|
+
snow.Rate = 100
|
|
317
|
+
snow.Lifetime = NumberRange.new(4, 7)
|
|
318
|
+
snow.Speed = NumberRange.new(1, 3)
|
|
319
|
+
snow.SpreadAngle = Vector2.new(60, 60)
|
|
320
|
+
snow.Size = NumberSequence.new({
|
|
321
|
+
NumberSequenceKeypoint.new(0, 0.1),
|
|
322
|
+
NumberSequenceKeypoint.new(1, 0.15),
|
|
323
|
+
})
|
|
324
|
+
snow.Color = ColorSequence.new(Color3.new(1, 1, 1))
|
|
325
|
+
snow.Transparency = NumberSequence.new({
|
|
326
|
+
NumberSequenceKeypoint.new(0, 0),
|
|
327
|
+
NumberSequenceKeypoint.new(0.8, 0),
|
|
328
|
+
NumberSequenceKeypoint.new(1, 1),
|
|
329
|
+
})
|
|
330
|
+
snow.Acceleration = Vector3.new(0, -2, 0)
|
|
331
|
+
snow.RotSpeed = NumberRange.new(-60, 60)
|
|
332
|
+
snow.Drag = 3
|
|
333
|
+
snow.Parent = largeCoverPart
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
#### Magic Aura (orbiting particles)
|
|
337
|
+
|
|
338
|
+
```luau
|
|
339
|
+
local aura = Instance.new("ParticleEmitter")
|
|
340
|
+
aura.Rate = 25
|
|
341
|
+
aura.Lifetime = NumberRange.new(1, 2)
|
|
342
|
+
aura.Speed = NumberRange.new(0.5, 1.5)
|
|
343
|
+
aura.SpreadAngle = Vector2.new(180, 180)
|
|
344
|
+
aura.Size = NumberSequence.new({
|
|
345
|
+
NumberSequenceKeypoint.new(0, 0),
|
|
346
|
+
NumberSequenceKeypoint.new(0.3, 0.8),
|
|
347
|
+
NumberSequenceKeypoint.new(1, 0),
|
|
348
|
+
})
|
|
349
|
+
aura.Color = ColorSequence.new({
|
|
350
|
+
ColorSequenceKeypoint.new(0, Color3.fromRGB(100, 0, 255)),
|
|
351
|
+
ColorSequenceKeypoint.new(1, Color3.fromRGB(200, 50, 255)),
|
|
352
|
+
})
|
|
353
|
+
aura.Transparency = NumberSequence.new({
|
|
354
|
+
NumberSequenceKeypoint.new(0, 1),
|
|
355
|
+
NumberSequenceKeypoint.new(0.2, 0.2),
|
|
356
|
+
NumberSequenceKeypoint.new(1, 1),
|
|
357
|
+
})
|
|
358
|
+
aura.LightEmission = 1
|
|
359
|
+
aura.RotSpeed = NumberRange.new(-90, 90)
|
|
360
|
+
aura.Drag = 5
|
|
361
|
+
aura.Parent = character.HumanoidRootPart
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## Beam and Trail
|
|
367
|
+
|
|
368
|
+
### Beam
|
|
369
|
+
|
|
370
|
+
A `Beam` renders a textured ribbon between two `Attachment` instances. Perfect for lasers, lightning, tethers, and energy connections.
|
|
371
|
+
|
|
372
|
+
```luau
|
|
373
|
+
-- Setup: two parts with attachments
|
|
374
|
+
local att0 = Instance.new("Attachment")
|
|
375
|
+
att0.Parent = partA
|
|
376
|
+
|
|
377
|
+
local att1 = Instance.new("Attachment")
|
|
378
|
+
att1.Parent = partB
|
|
379
|
+
|
|
380
|
+
local beam = Instance.new("Beam")
|
|
381
|
+
beam.Attachment0 = att0
|
|
382
|
+
beam.Attachment1 = att1
|
|
383
|
+
beam.Width0 = 0.5
|
|
384
|
+
beam.Width1 = 0.5
|
|
385
|
+
beam.Color = ColorSequence.new(Color3.fromRGB(0, 150, 255))
|
|
386
|
+
beam.Transparency = NumberSequence.new({
|
|
387
|
+
NumberSequenceKeypoint.new(0, 0),
|
|
388
|
+
NumberSequenceKeypoint.new(1, 0.5),
|
|
389
|
+
})
|
|
390
|
+
beam.LightEmission = 1
|
|
391
|
+
beam.FaceCamera = true
|
|
392
|
+
beam.Segments = 20 -- more segments = smoother curves
|
|
393
|
+
beam.CurveSize0 = 2 -- bend near Attachment0
|
|
394
|
+
beam.CurveSize1 = -2 -- bend near Attachment1
|
|
395
|
+
beam.TextureLength = 1
|
|
396
|
+
beam.TextureSpeed = 1 -- scrolling texture
|
|
397
|
+
beam.Texture = "rbxassetid://123456789"
|
|
398
|
+
beam.Parent = partA
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Key Beam Properties
|
|
402
|
+
|
|
403
|
+
| Property | Description |
|
|
404
|
+
| ---------------- | -------------------------------------------------------- |
|
|
405
|
+
| `Attachment0/1` | Start and end points |
|
|
406
|
+
| `Width0/1` | Width at each attachment (studs) |
|
|
407
|
+
| `Color` | `ColorSequence` along the beam length |
|
|
408
|
+
| `Transparency` | `NumberSequence` along the beam length |
|
|
409
|
+
| `CurveSize0/1` | Bezier curve magnitude at each end |
|
|
410
|
+
| `Segments` | Number of straight segments (more = smoother curves) |
|
|
411
|
+
| `FaceCamera` | Always faces the camera for billboard effect |
|
|
412
|
+
| `TextureSpeed` | Scrolls the texture along the beam |
|
|
413
|
+
| `LightEmission` | Additive blending for glow |
|
|
414
|
+
|
|
415
|
+
### Trail
|
|
416
|
+
|
|
417
|
+
A `Trail` renders a ribbon behind a moving part. Requires two `Attachment` instances on the same part (defining the trail's width axis).
|
|
418
|
+
|
|
419
|
+
```luau
|
|
420
|
+
local part = workspace.Sword.Blade
|
|
421
|
+
|
|
422
|
+
local att0 = Instance.new("Attachment")
|
|
423
|
+
att0.Position = Vector3.new(0, 0, -2) -- base of blade
|
|
424
|
+
att0.Parent = part
|
|
425
|
+
|
|
426
|
+
local att1 = Instance.new("Attachment")
|
|
427
|
+
att1.Position = Vector3.new(0, 0, 2) -- tip of blade
|
|
428
|
+
att1.Parent = part
|
|
429
|
+
|
|
430
|
+
local trail = Instance.new("Trail")
|
|
431
|
+
trail.Attachment0 = att0
|
|
432
|
+
trail.Attachment1 = att1
|
|
433
|
+
trail.Lifetime = 0.3 -- how long segments persist
|
|
434
|
+
trail.MinLength = 0.05 -- minimum distance before new segment
|
|
435
|
+
trail.FaceCamera = true
|
|
436
|
+
trail.Color = ColorSequence.new({
|
|
437
|
+
ColorSequenceKeypoint.new(0, Color3.fromRGB(255, 255, 255)),
|
|
438
|
+
ColorSequenceKeypoint.new(1, Color3.fromRGB(100, 100, 255)),
|
|
439
|
+
})
|
|
440
|
+
trail.Transparency = NumberSequence.new({
|
|
441
|
+
NumberSequenceKeypoint.new(0, 0),
|
|
442
|
+
NumberSequenceKeypoint.new(1, 1),
|
|
443
|
+
})
|
|
444
|
+
trail.LightEmission = 0.8
|
|
445
|
+
trail.WidthScale = NumberSequence.new(1)
|
|
446
|
+
trail.Parent = part
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### Common Uses
|
|
450
|
+
|
|
451
|
+
- **Laser beams**: `Beam` between gun barrel attachment and hit-point attachment.
|
|
452
|
+
- **Sword trails**: `Trail` on blade with short `Lifetime` (0.2-0.4s).
|
|
453
|
+
- **Magic effects**: `Beam` with high `CurveSize` values and scrolling texture for arcane tethers.
|
|
454
|
+
- **Lightning**: `Beam` with many `Segments`, rapidly randomizing `CurveSize0/1` each frame.
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
### Parent Destruction Behavior
|
|
459
|
+
|
|
460
|
+
When a `Part` or `Model` containing effects is destroyed (`:Destroy()`, player leave, workspace clear), all child `ParticleEmitter`, `Trail`, `Beam`, and `Attachment` are destroyed instantly. Active particles vanish mid-flight, trails cut off, beams disappear.
|
|
461
|
+
|
|
462
|
+
**Solution - Debris + temporary holder:** If you need an effect to finish gracefully after its parent is gone, reparent it to a temporary part and let `Debris` clean up.
|
|
463
|
+
|
|
464
|
+
```luau
|
|
465
|
+
local Debris = game:GetService("Debris")
|
|
466
|
+
|
|
467
|
+
local function destroyWithGrace(effect: Instance, parent: Instance, gracePeriod: number)
|
|
468
|
+
-- Create a temporary invisible holder
|
|
469
|
+
local holder = Instance.new("Part")
|
|
470
|
+
holder.Anchored = true
|
|
471
|
+
holder.CanCollide = false
|
|
472
|
+
holder.Transparency = 1
|
|
473
|
+
holder.Size = Vector3.one
|
|
474
|
+
holder.Parent = workspace
|
|
475
|
+
|
|
476
|
+
-- Reparent the effect so it survives the original parent
|
|
477
|
+
effect.Parent = holder
|
|
478
|
+
|
|
479
|
+
-- Destroy the original parent (effect is now safe)
|
|
480
|
+
parent:Destroy()
|
|
481
|
+
|
|
482
|
+
-- Debris cleans up the holder + effect after the grace period
|
|
483
|
+
Debris:AddItem(holder, gracePeriod)
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
-- Example: Trail with 1s lifetime - give it 1.1s to fade out cleanly
|
|
487
|
+
local trail = -- ... setup trail on a sword part ...
|
|
488
|
+
destroyWithGrace(trail, swordPart, 1.1)
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
For `Trail` specifically, set `Debris:AddItem(holder, trail.Lifetime + 0.1)` so the trail's existing segments finish rendering before cleanup.
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
## Highlight
|
|
496
|
+
|
|
497
|
+
A `Highlight` instance draws a colored outline around a `BasePart` or `Model` to call attention to it. Every highlight has two layers: an **outline** (silhouette edge) and an **interior** (overlay fill), each independently customizable.
|
|
498
|
+
|
|
499
|
+
### Basic Usage
|
|
500
|
+
|
|
501
|
+
```luau
|
|
502
|
+
local highlight = Instance.new("Highlight")
|
|
503
|
+
highlight.Adornee = targetPart
|
|
504
|
+
highlight.FillColor = Color3.fromRGB(255, 50, 50)
|
|
505
|
+
highlight.FillTransparency = 0.3
|
|
506
|
+
highlight.OutlineColor = Color3.new(1, 1, 1)
|
|
507
|
+
highlight.Parent = targetPart
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### Properties
|
|
511
|
+
|
|
512
|
+
| Property | Type | Default | Description |
|
|
513
|
+
|----------|------|---------|-------------|
|
|
514
|
+
| `Adornee` | Instance | - | The `BasePart` or `Model` to highlight |
|
|
515
|
+
| `DepthMode` | Enum.HighlightDepthMode | `AlwaysOnTop` | `AlwaysOnTop` = visible through objects, `Occluded` = hidden by obstructions |
|
|
516
|
+
| `Enabled` | boolean | `true` | Toggle visibility |
|
|
517
|
+
| `FillColor` | Color3 | `[255, 200, 50]` | Interior overlay color |
|
|
518
|
+
| `FillTransparency` | number | `0.5` | 0 = opaque, 1 = invisible |
|
|
519
|
+
| `OutlineColor` | Color3 | `[255, 255, 255]` | Edge outline color |
|
|
520
|
+
| `OutlineTransparency` | number | `0` | 0 = opaque, 1 = invisible |
|
|
521
|
+
|
|
522
|
+
### Common Pattern - Team Highlight
|
|
523
|
+
|
|
524
|
+
```luau
|
|
525
|
+
local function addTeamHighlight(character: Model, teamColor: Color3)
|
|
526
|
+
local hl = Instance.new("Highlight")
|
|
527
|
+
hl.Adornee = character
|
|
528
|
+
hl.FillColor = teamColor
|
|
529
|
+
hl.FillTransparency = 0.6
|
|
530
|
+
hl.OutlineColor = teamColor
|
|
531
|
+
hl.DepthMode = Enum.HighlightDepthMode.AlwaysOnTop
|
|
532
|
+
hl.Parent = character
|
|
533
|
+
end
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### Limitations
|
|
537
|
+
|
|
538
|
+
- **Max 255 simultaneous** Highlight instances per client. Excess instances are silently ignored.
|
|
539
|
+
- Disabled highlights still count toward the 255 limit - `:Destroy()` instead of `Enabled = false` if permanently unused.
|
|
540
|
+
- The `Highlight` itself is **not** destroyed when its `Adornee` is destroyed. Clean up manually.
|
|
541
|
+
|
|
542
|
+
### Cleanup on Adornee Destroyed
|
|
543
|
+
|
|
544
|
+
```luau
|
|
545
|
+
local function attachHighlight(adornee: Instance): Highlight
|
|
546
|
+
local hl = Instance.new("Highlight")
|
|
547
|
+
hl.Adornee = adornee
|
|
548
|
+
hl.Parent = adornee
|
|
549
|
+
|
|
550
|
+
adornee.AncestryChanged:Connect(function()
|
|
551
|
+
if not adornee:IsDescendantOf(game) then
|
|
552
|
+
hl:Destroy()
|
|
553
|
+
end
|
|
554
|
+
end)
|
|
555
|
+
return hl
|
|
556
|
+
end
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
---
|
|
560
|
+
|
|
561
|
+
## TweenService for VFX
|
|
562
|
+
|
|
563
|
+
`TweenService` interpolates any numeric or color property over time. It is the backbone of procedural visual feedback.
|
|
564
|
+
|
|
565
|
+
### TweenInfo
|
|
566
|
+
|
|
567
|
+
```luau
|
|
568
|
+
local TweenService = game:GetService("TweenService")
|
|
569
|
+
|
|
570
|
+
local info = TweenInfo.new(
|
|
571
|
+
0.5, -- Duration (seconds)
|
|
572
|
+
Enum.EasingStyle.Quad, -- Easing style
|
|
573
|
+
Enum.EasingDirection.Out, -- Easing direction
|
|
574
|
+
0, -- RepeatCount (0 = no repeat, -1 = infinite)
|
|
575
|
+
false, -- Reverses (plays backward after forward)
|
|
576
|
+
0 -- DelayTime (seconds before starting)
|
|
577
|
+
)
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
### Common Easing Styles
|
|
581
|
+
|
|
582
|
+
| Style | Feel |
|
|
583
|
+
| ----------- | --------------------------------------- |
|
|
584
|
+
| `Linear` | Constant speed, mechanical |
|
|
585
|
+
| `Quad` | Gentle acceleration/deceleration |
|
|
586
|
+
| `Cubic` | Stronger ease |
|
|
587
|
+
| `Quart` | Even stronger |
|
|
588
|
+
| `Sine` | Smooth, organic |
|
|
589
|
+
| `Back` | Overshoots then settles |
|
|
590
|
+
| `Bounce` | Bounces at the end |
|
|
591
|
+
| `Elastic` | Springy overshoot |
|
|
592
|
+
| `Exponential` | Very sharp acceleration |
|
|
593
|
+
|
|
594
|
+
### Tweening Part Properties
|
|
595
|
+
|
|
596
|
+
```luau
|
|
597
|
+
-- Flash on hit: turn white then revert
|
|
598
|
+
local function flashPart(part: BasePart, originalColor: Color3)
|
|
599
|
+
part.Color = Color3.new(1, 1, 1) -- instant white
|
|
600
|
+
local tweenBack = TweenService:Create(part, TweenInfo.new(0.3, Enum.EasingStyle.Quad), {
|
|
601
|
+
Color = originalColor,
|
|
602
|
+
})
|
|
603
|
+
tweenBack:Play()
|
|
604
|
+
end
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
```luau
|
|
608
|
+
-- Pulse effect: scale up then back
|
|
609
|
+
local function pulse(part: BasePart)
|
|
610
|
+
local originalSize = part.Size
|
|
611
|
+
local tweenGrow = TweenService:Create(part, TweenInfo.new(0.15, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {
|
|
612
|
+
Size = originalSize * 1.3,
|
|
613
|
+
})
|
|
614
|
+
local tweenShrink = TweenService:Create(part, TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.In), {
|
|
615
|
+
Size = originalSize,
|
|
616
|
+
})
|
|
617
|
+
tweenGrow:Play()
|
|
618
|
+
tweenGrow.Completed:Connect(function()
|
|
619
|
+
tweenShrink:Play()
|
|
620
|
+
end)
|
|
621
|
+
end
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
```luau
|
|
625
|
+
-- Grow and fade out (explosion ring)
|
|
626
|
+
local function expandAndFade(part: BasePart)
|
|
627
|
+
local tween = TweenService:Create(part, TweenInfo.new(0.6, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {
|
|
628
|
+
Size = part.Size * 5,
|
|
629
|
+
Transparency = 1,
|
|
630
|
+
})
|
|
631
|
+
tween:Play()
|
|
632
|
+
tween.Completed:Connect(function()
|
|
633
|
+
part:Destroy()
|
|
634
|
+
end)
|
|
635
|
+
end
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
```luau
|
|
639
|
+
-- Color transition (damage indicator)
|
|
640
|
+
local function colorTransition(part: BasePart, targetColor: Color3, duration: number)
|
|
641
|
+
local tween = TweenService:Create(part, TweenInfo.new(duration, Enum.EasingStyle.Sine), {
|
|
642
|
+
Color = targetColor,
|
|
643
|
+
})
|
|
644
|
+
tween:Play()
|
|
645
|
+
end
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
### Chaining Tweens
|
|
649
|
+
|
|
650
|
+
Use the `Completed` event to sequence tweens without coroutines:
|
|
651
|
+
|
|
652
|
+
```luau
|
|
653
|
+
local function chainedEffect(part: BasePart)
|
|
654
|
+
local step1 = TweenService:Create(part, TweenInfo.new(0.2), { Transparency = 0.5 })
|
|
655
|
+
local step2 = TweenService:Create(part, TweenInfo.new(0.3), { Size = part.Size * 2 })
|
|
656
|
+
local step3 = TweenService:Create(part, TweenInfo.new(0.2), { Transparency = 1 })
|
|
657
|
+
|
|
658
|
+
step1:Play()
|
|
659
|
+
step1.Completed:Connect(function()
|
|
660
|
+
step2:Play()
|
|
661
|
+
end)
|
|
662
|
+
step2.Completed:Connect(function()
|
|
663
|
+
step3:Play()
|
|
664
|
+
end)
|
|
665
|
+
step3.Completed:Connect(function()
|
|
666
|
+
part:Destroy()
|
|
667
|
+
end)
|
|
668
|
+
end
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
---
|
|
672
|
+
|
|
673
|
+
## Lighting Effects
|
|
674
|
+
|
|
675
|
+
### Dynamic Lights
|
|
676
|
+
|
|
677
|
+
```luau
|
|
678
|
+
-- PointLight: omnidirectional, good for torches, explosions
|
|
679
|
+
local pointLight = Instance.new("PointLight")
|
|
680
|
+
pointLight.Brightness = 2
|
|
681
|
+
pointLight.Color = Color3.fromRGB(255, 180, 50)
|
|
682
|
+
pointLight.Range = 20
|
|
683
|
+
pointLight.Shadows = true
|
|
684
|
+
pointLight.Parent = torchPart
|
|
685
|
+
|
|
686
|
+
-- SpotLight: directional cone, good for flashlights, spotlights
|
|
687
|
+
local spotLight = Instance.new("SpotLight")
|
|
688
|
+
spotLight.Brightness = 3
|
|
689
|
+
spotLight.Color = Color3.new(1, 1, 1)
|
|
690
|
+
spotLight.Range = 40
|
|
691
|
+
spotLight.Angle = 30 -- cone half-angle in degrees
|
|
692
|
+
spotLight.Face = Enum.NormalId.Front
|
|
693
|
+
spotLight.Parent = flashlightPart
|
|
694
|
+
|
|
695
|
+
-- SurfaceLight: emits from a surface, good for screens, signs
|
|
696
|
+
local surfLight = Instance.new("SurfaceLight")
|
|
697
|
+
surfLight.Brightness = 1
|
|
698
|
+
surfLight.Color = Color3.fromRGB(0, 200, 255)
|
|
699
|
+
surfLight.Range = 10
|
|
700
|
+
surfLight.Face = Enum.NormalId.Front
|
|
701
|
+
surfLight.Parent = screenPart
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
### Post-Processing Effects
|
|
705
|
+
|
|
706
|
+
All post-processing objects go in `Lighting` or `Camera`.
|
|
707
|
+
|
|
708
|
+
#### Atmosphere
|
|
709
|
+
|
|
710
|
+
```luau
|
|
711
|
+
local atmo = Instance.new("Atmosphere")
|
|
712
|
+
atmo.Density = 0.3 -- 0-1, how thick the atmosphere is
|
|
713
|
+
atmo.Offset = 0.25 -- shifts haze up/down
|
|
714
|
+
atmo.Color = Color3.fromRGB(200, 210, 230) -- scatter color
|
|
715
|
+
atmo.Decay = Color3.fromRGB(120, 140, 170) -- far-away color
|
|
716
|
+
atmo.Glare = 0.2 -- sun glare intensity
|
|
717
|
+
atmo.Haze = 1.5 -- haze amount
|
|
718
|
+
atmo.Parent = game.Lighting
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
#### ColorCorrection
|
|
722
|
+
|
|
723
|
+
```luau
|
|
724
|
+
local cc = Instance.new("ColorCorrectionEffect")
|
|
725
|
+
cc.Brightness = 0.05 -- -1 to 1
|
|
726
|
+
cc.Contrast = 0.1 -- -1 to 1
|
|
727
|
+
cc.Saturation = 0.15 -- -1 to 1
|
|
728
|
+
cc.TintColor = Color3.new(1, 0.95, 0.9) -- warm tint
|
|
729
|
+
cc.Parent = game.Lighting
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
#### Bloom
|
|
733
|
+
|
|
734
|
+
```luau
|
|
735
|
+
local bloom = Instance.new("BloomEffect")
|
|
736
|
+
bloom.Intensity = 0.8 -- glow strength
|
|
737
|
+
bloom.Size = 24 -- glow spread (pixels)
|
|
738
|
+
bloom.Threshold = 1.2 -- brightness threshold to bloom
|
|
739
|
+
bloom.Parent = game.Lighting
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
#### DepthOfField
|
|
743
|
+
|
|
744
|
+
```luau
|
|
745
|
+
local dof = Instance.new("DepthOfFieldEffect")
|
|
746
|
+
dof.FarIntensity = 0.3 -- blur intensity far from focus
|
|
747
|
+
dof.FocusDistance = 30 -- distance in studs to focus point
|
|
748
|
+
dof.InFocusRadius = 20 -- radius of sharp area
|
|
749
|
+
dof.NearIntensity = 0.2 -- blur intensity near camera
|
|
750
|
+
dof.Parent = game.Lighting
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
#### SunRays
|
|
754
|
+
|
|
755
|
+
```luau
|
|
756
|
+
local sunRays = Instance.new("SunRaysEffect")
|
|
757
|
+
sunRays.Intensity = 0.15 -- ray visibility
|
|
758
|
+
sunRays.Spread = 0.8 -- how far rays extend
|
|
759
|
+
sunRays.Parent = game.Lighting
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
### Setting Mood with Global Lighting
|
|
763
|
+
|
|
764
|
+
```luau
|
|
765
|
+
local lighting = game.Lighting
|
|
766
|
+
|
|
767
|
+
-- Daytime bright
|
|
768
|
+
lighting.ClockTime = 14
|
|
769
|
+
lighting.Brightness = 2
|
|
770
|
+
lighting.Ambient = Color3.fromRGB(140, 140, 140)
|
|
771
|
+
lighting.OutdoorAmbient = Color3.fromRGB(130, 130, 130)
|
|
772
|
+
|
|
773
|
+
-- Nighttime spooky
|
|
774
|
+
lighting.ClockTime = 0
|
|
775
|
+
lighting.Brightness = 0.5
|
|
776
|
+
lighting.Ambient = Color3.fromRGB(20, 20, 40)
|
|
777
|
+
lighting.OutdoorAmbient = Color3.fromRGB(10, 10, 30)
|
|
778
|
+
lighting.FogEnd = 200
|
|
779
|
+
lighting.FogColor = Color3.fromRGB(15, 15, 30)
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
---
|
|
783
|
+
|
|
784
|
+
## Sound + Animation Sync
|
|
785
|
+
|
|
786
|
+
For general sound (SoundService, positional audio, SoundGroups), use mcp-roblox-docs. This section covers only the animation-specific pattern.
|
|
787
|
+
|
|
788
|
+
### Triggering Sounds with Animation Events
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
```luau
|
|
792
|
+
-- Sync a footstep sound to walk animation markers
|
|
793
|
+
walkTrack:GetMarkerReachedSignal("Footstep"):Connect(function()
|
|
794
|
+
local footstep = Instance.new("Sound")
|
|
795
|
+
footstep.SoundId = "rbxassetid://112233445"
|
|
796
|
+
footstep.Volume = 0.5
|
|
797
|
+
footstep.PlaybackSpeed = 0.9 + math.random() * 0.2 -- slight variation
|
|
798
|
+
footstep.Parent = character.HumanoidRootPart
|
|
799
|
+
footstep:Play()
|
|
800
|
+
footstep.Ended:Connect(function()
|
|
801
|
+
footstep:Destroy()
|
|
802
|
+
end)
|
|
803
|
+
end)
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
---
|
|
807
|
+
|
|
808
|
+
## Camera Effects
|
|
809
|
+
|
|
810
|
+
### Camera Shake
|
|
811
|
+
|
|
812
|
+
```luau
|
|
813
|
+
local camera = workspace.CurrentCamera
|
|
814
|
+
local RunService = game:GetService("RunService")
|
|
815
|
+
|
|
816
|
+
local function shakeCamera(intensity: number, duration: number)
|
|
817
|
+
local elapsed = 0
|
|
818
|
+
local originalCFrame = camera.CFrame
|
|
819
|
+
local connection: RBXScriptConnection
|
|
820
|
+
|
|
821
|
+
connection = RunService.RenderStepped:Connect(function(dt: number)
|
|
822
|
+
elapsed += dt
|
|
823
|
+
if elapsed >= duration then
|
|
824
|
+
connection:Disconnect()
|
|
825
|
+
-- Camera returns to normal since CameraType is usually
|
|
826
|
+
-- "Custom" (player-controlled)
|
|
827
|
+
return
|
|
828
|
+
end
|
|
829
|
+
|
|
830
|
+
local progress = 1 - (elapsed / duration) -- decay over time
|
|
831
|
+
local shakeX = (math.random() - 0.5) * 2 * intensity * progress
|
|
832
|
+
local shakeY = (math.random() - 0.5) * 2 * intensity * progress
|
|
833
|
+
local shakeZ = (math.random() - 0.5) * 2 * intensity * progress
|
|
834
|
+
|
|
835
|
+
camera.CFrame = camera.CFrame * CFrame.new(shakeX, shakeY, shakeZ)
|
|
836
|
+
end)
|
|
837
|
+
end
|
|
838
|
+
|
|
839
|
+
-- Usage: moderate shake for 0.3 seconds
|
|
840
|
+
shakeCamera(0.5, 0.3)
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
### Zoom Effect
|
|
844
|
+
|
|
845
|
+
```luau
|
|
846
|
+
local TweenService = game:GetService("TweenService")
|
|
847
|
+
local camera = workspace.CurrentCamera
|
|
848
|
+
|
|
849
|
+
local function zoomCamera(targetFOV: number, duration: number)
|
|
850
|
+
local tween = TweenService:Create(camera, TweenInfo.new(duration, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {
|
|
851
|
+
FieldOfView = targetFOV,
|
|
852
|
+
})
|
|
853
|
+
tween:Play()
|
|
854
|
+
return tween
|
|
855
|
+
end
|
|
856
|
+
|
|
857
|
+
-- Zoom in for dramatic moment
|
|
858
|
+
local zoomIn = zoomCamera(40, 0.5)
|
|
859
|
+
zoomIn.Completed:Connect(function()
|
|
860
|
+
task.wait(1)
|
|
861
|
+
zoomCamera(70, 0.8) -- zoom back to normal
|
|
862
|
+
end)
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
### Focus / CFrame Lerp
|
|
866
|
+
|
|
867
|
+
```luau
|
|
868
|
+
local function focusOnPoint(targetCFrame: CFrame, duration: number)
|
|
869
|
+
camera.CameraType = Enum.CameraType.Scriptable
|
|
870
|
+
|
|
871
|
+
local startCFrame = camera.CFrame
|
|
872
|
+
local elapsed = 0
|
|
873
|
+
local connection: RBXScriptConnection
|
|
874
|
+
|
|
875
|
+
connection = RunService.RenderStepped:Connect(function(dt: number)
|
|
876
|
+
elapsed += dt
|
|
877
|
+
local alpha = math.clamp(elapsed / duration, 0, 1)
|
|
878
|
+
-- Smooth step for natural feel
|
|
879
|
+
local smoothAlpha = alpha * alpha * (3 - 2 * alpha)
|
|
880
|
+
camera.CFrame = startCFrame:Lerp(targetCFrame, smoothAlpha)
|
|
881
|
+
|
|
882
|
+
if alpha >= 1 then
|
|
883
|
+
connection:Disconnect()
|
|
884
|
+
end
|
|
885
|
+
end)
|
|
886
|
+
end
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
### Cutscene Waypoint System
|
|
890
|
+
|
|
891
|
+
```luau
|
|
892
|
+
type CutsceneWaypoint = {
|
|
893
|
+
cframe: CFrame,
|
|
894
|
+
duration: number,
|
|
895
|
+
easingStyle: Enum.EasingStyle?,
|
|
896
|
+
holdTime: number?, -- pause at this point before moving on
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
local function playCutscene(waypoints: { CutsceneWaypoint })
|
|
900
|
+
camera.CameraType = Enum.CameraType.Scriptable
|
|
901
|
+
|
|
902
|
+
for i, waypoint in waypoints do
|
|
903
|
+
local style = waypoint.easingStyle or Enum.EasingStyle.Quad
|
|
904
|
+
local info = TweenInfo.new(waypoint.duration, style, Enum.EasingDirection.InOut)
|
|
905
|
+
|
|
906
|
+
local tween = TweenService:Create(camera, info, {
|
|
907
|
+
CFrame = waypoint.cframe,
|
|
908
|
+
})
|
|
909
|
+
tween:Play()
|
|
910
|
+
tween.Completed:Wait()
|
|
911
|
+
|
|
912
|
+
if waypoint.holdTime and waypoint.holdTime > 0 then
|
|
913
|
+
task.wait(waypoint.holdTime)
|
|
914
|
+
end
|
|
915
|
+
end
|
|
916
|
+
|
|
917
|
+
-- Return camera control to player
|
|
918
|
+
camera.CameraType = Enum.CameraType.Custom
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
-- Usage
|
|
922
|
+
playCutscene({
|
|
923
|
+
{ cframe = CFrame.new(0, 50, 0) * CFrame.Angles(math.rad(-90), 0, 0), duration = 2, holdTime = 1 },
|
|
924
|
+
{ cframe = CFrame.new(20, 10, 20) * CFrame.lookAt(Vector3.new(20, 10, 20), Vector3.zero), duration = 3 },
|
|
925
|
+
{ cframe = camera.CFrame, duration = 1.5, easingStyle = Enum.EasingStyle.Sine },
|
|
926
|
+
})
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
---
|
|
930
|
+
|
|
931
|
+
## Best Practices
|
|
932
|
+
|
|
933
|
+
### Performance Budgets
|
|
934
|
+
|
|
935
|
+
- **Max ~200 particles per emitter** -- more than this and you risk frame drops, especially on mobile.
|
|
936
|
+
- **Limit total active emitters** -- aim for fewer than 20-30 active emitters visible at once in any scene.
|
|
937
|
+
- **Particle texture size** -- keep textures small (64x64 or 128x128 PNG). Avoid large or high-res particle textures.
|
|
938
|
+
- **Beams** -- keep `Segments` reasonable (10-30). Very high segment counts cost draw calls.
|
|
939
|
+
- **Tweens** -- hundreds of simultaneous tweens are fine; thousands may cause issues. Cancel/destroy tweens when no longer needed.
|
|
940
|
+
- **Sounds** -- limit simultaneous playing sounds to ~20-30. Destroy one-shot sounds after `Ended`.
|
|
941
|
+
|
|
942
|
+
### Disable Effects on Low-End Devices
|
|
943
|
+
|
|
944
|
+
```luau
|
|
945
|
+
local function getQualityLevel(): string
|
|
946
|
+
local quality = UserSettings().GameSettings.SavedQualityLevel
|
|
947
|
+
-- quality is Enum.SavedQualitySetting or an int 1-10
|
|
948
|
+
if quality == Enum.SavedQualitySetting.Automatic then
|
|
949
|
+
-- Use a heuristic: check current graphics quality
|
|
950
|
+
local level = settings().Rendering.QualityLevel
|
|
951
|
+
if level <= 3 then return "low"
|
|
952
|
+
elseif level <= 6 then return "medium"
|
|
953
|
+
else return "high" end
|
|
954
|
+
end
|
|
955
|
+
local numQuality = quality.Value
|
|
956
|
+
if numQuality <= 3 then return "low"
|
|
957
|
+
elseif numQuality <= 6 then return "medium"
|
|
958
|
+
else return "high" end
|
|
959
|
+
end
|
|
960
|
+
|
|
961
|
+
local function applyQualitySettings()
|
|
962
|
+
local level = getQualityLevel()
|
|
963
|
+
if level == "low" then
|
|
964
|
+
-- Disable post-processing
|
|
965
|
+
for _, effect in game.Lighting:GetChildren() do
|
|
966
|
+
if effect:IsA("PostEffect") then
|
|
967
|
+
effect.Enabled = false
|
|
968
|
+
end
|
|
969
|
+
end
|
|
970
|
+
-- Reduce particle rates
|
|
971
|
+
-- Disable shadows on dynamic lights
|
|
972
|
+
end
|
|
973
|
+
end
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
### Pool VFX Objects
|
|
977
|
+
|
|
978
|
+
Avoid creating and destroying particles every frame. Pre-create a pool and enable/disable or reposition them.
|
|
979
|
+
|
|
980
|
+
```luau
|
|
981
|
+
local VFXPool = {}
|
|
982
|
+
VFXPool.__index = VFXPool
|
|
983
|
+
|
|
984
|
+
function VFXPool.new(template: Instance, poolSize: number): typeof(setmetatable({}, VFXPool))
|
|
985
|
+
local self = setmetatable({}, VFXPool)
|
|
986
|
+
self._pool = table.create(poolSize)
|
|
987
|
+
self._available = table.create(poolSize)
|
|
988
|
+
|
|
989
|
+
for i = 1, poolSize do
|
|
990
|
+
local clone = template:Clone()
|
|
991
|
+
clone.Parent = workspace.VFXFolder
|
|
992
|
+
-- Disable all emitters
|
|
993
|
+
for _, emitter in clone:GetDescendants() do
|
|
994
|
+
if emitter:IsA("ParticleEmitter") then
|
|
995
|
+
emitter.Enabled = false
|
|
996
|
+
end
|
|
997
|
+
end
|
|
998
|
+
self._pool[i] = clone
|
|
999
|
+
self._available[i] = clone
|
|
1000
|
+
end
|
|
1001
|
+
|
|
1002
|
+
return self
|
|
1003
|
+
end
|
|
1004
|
+
|
|
1005
|
+
function VFXPool:get(): Instance?
|
|
1006
|
+
local obj = table.remove(self._available)
|
|
1007
|
+
return obj
|
|
1008
|
+
end
|
|
1009
|
+
|
|
1010
|
+
function VFXPool:release(obj: Instance)
|
|
1011
|
+
-- Reset position, disable emitters
|
|
1012
|
+
for _, emitter in obj:GetDescendants() do
|
|
1013
|
+
if emitter:IsA("ParticleEmitter") then
|
|
1014
|
+
emitter.Enabled = false
|
|
1015
|
+
end
|
|
1016
|
+
end
|
|
1017
|
+
table.insert(self._available, obj)
|
|
1018
|
+
end
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
### Sync Sound with Visuals
|
|
1022
|
+
|
|
1023
|
+
- Use `MarkerReachedSignal` to trigger sounds at exact animation frames.
|
|
1024
|
+
- Play impact sounds at the moment of collision, not when the swing starts.
|
|
1025
|
+
- Match `PlaybackSpeed` to animation speed adjustments.
|
|
1026
|
+
- Use `task.delay` or tween `Completed` events for sequenced audio.
|
|
1027
|
+
|
|
1028
|
+
---
|
|
1029
|
+
|
|
1030
|
+
## Anti-Patterns
|
|
1031
|
+
|
|
1032
|
+
### Unlimited Particles
|
|
1033
|
+
|
|
1034
|
+
```luau
|
|
1035
|
+
-- BAD: unbounded particle creation with no cleanup
|
|
1036
|
+
RunService.Heartbeat:Connect(function()
|
|
1037
|
+
local emitter = Instance.new("ParticleEmitter")
|
|
1038
|
+
emitter.Rate = 500 -- extremely high rate
|
|
1039
|
+
emitter.Parent = somePart
|
|
1040
|
+
-- never destroyed, accumulates forever
|
|
1041
|
+
end)
|
|
1042
|
+
|
|
1043
|
+
-- GOOD: reuse a single emitter, burst when needed
|
|
1044
|
+
local emitter = Instance.new("ParticleEmitter")
|
|
1045
|
+
emitter.Rate = 0 -- manual emission only
|
|
1046
|
+
emitter.Parent = somePart
|
|
1047
|
+
|
|
1048
|
+
local function burstParticles(count: number)
|
|
1049
|
+
emitter:Emit(count)
|
|
1050
|
+
end
|
|
1051
|
+
```
|
|
1052
|
+
|
|
1053
|
+
### Unoptimized Particle Textures
|
|
1054
|
+
|
|
1055
|
+
```luau
|
|
1056
|
+
-- BAD: 1024x1024 high-res texture for tiny particles
|
|
1057
|
+
emitter.Texture = "rbxassetid://huge_4k_texture"
|
|
1058
|
+
|
|
1059
|
+
-- GOOD: 64x64 or 128x128 simple shape on transparent background
|
|
1060
|
+
emitter.Texture = "rbxassetid://small_optimized_circle"
|
|
1061
|
+
-- Use LightEmission = 1 with simple shapes for clean glow effects
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
### Synchronous Animation Loading Blocking Gameplay
|
|
1065
|
+
|
|
1066
|
+
```luau
|
|
1067
|
+
-- BAD: loading animations in a hot path synchronously
|
|
1068
|
+
local function onAttack()
|
|
1069
|
+
local anim = Instance.new("Animation")
|
|
1070
|
+
anim.AnimationId = "rbxassetid://123456789"
|
|
1071
|
+
local track = animator:LoadAnimation(anim) -- may yield on first load
|
|
1072
|
+
track:Play()
|
|
1073
|
+
end
|
|
1074
|
+
|
|
1075
|
+
-- GOOD: preload animations at character spawn
|
|
1076
|
+
local attackAnim = Instance.new("Animation")
|
|
1077
|
+
attackAnim.AnimationId = "rbxassetid://123456789"
|
|
1078
|
+
local attackTrack: AnimationTrack -- forward declare
|
|
1079
|
+
|
|
1080
|
+
local function onCharacterAdded(character: Model)
|
|
1081
|
+
local animator = character:WaitForChild("Humanoid"):WaitForChild("Animator")
|
|
1082
|
+
attackTrack = animator:LoadAnimation(attackAnim)
|
|
1083
|
+
attackTrack.Priority = Enum.AnimationPriority.Action
|
|
1084
|
+
end
|
|
1085
|
+
|
|
1086
|
+
local function onAttack()
|
|
1087
|
+
if attackTrack then
|
|
1088
|
+
attackTrack:Play()
|
|
1089
|
+
end
|
|
1090
|
+
end
|
|
1091
|
+
```
|
|
1092
|
+
|
|
1093
|
+
### Other Anti-Patterns to Avoid
|
|
1094
|
+
|
|
1095
|
+
- **Tweening properties every frame via RenderStepped instead of using TweenService** -- TweenService is optimized internally and handles cleanup.
|
|
1096
|
+
- **Not disconnecting camera shake connections** -- leads to permanent jitter.
|
|
1097
|
+
- **Setting `Camera.CameraType` to `Scriptable` and forgetting to restore it** -- player loses control.
|
|
1098
|
+
- **Not cleaning up Highlight after its Adornee is destroyed** -- orphaned Highlights waste one of the 255 slots. Use `AncestryChanged` to auto-destroy.
|
|
1099
|
+
- **Destroying a part while effects are active** -- particle bursts, trails, and beams vanish instantly. Reparent to a temporary holder + Debris if you need graceful cleanup.
|
|
1100
|
+
- **Playing sounds without ever destroying them** -- memory leak. Always clean up one-shot sounds via `Ended`.
|
|
1101
|
+
- **Creating hundreds of PointLights with shadows enabled** -- massive performance hit. Use `Shadows = false` for most dynamic lights.
|
|
1102
|
+
|
|
1103
|
+
---
|
|
1104
|
+
|
|
1105
|
+
## Complete Hit Effect System
|
|
1106
|
+
|
|
1107
|
+
A production-ready system combining white flash, particle burst, sound stinger, and camera shake. Designed for client-side use (LocalScript or module required from a LocalScript).
|
|
1108
|
+
|
|
1109
|
+
```luau
|
|
1110
|
+
--[[
|
|
1111
|
+
HitEffectSystem
|
|
1112
|
+
Combines visual and audio feedback for combat hit registration.
|
|
1113
|
+
Run on the CLIENT only (camera shake and local VFX).
|
|
1114
|
+
]]
|
|
1115
|
+
|
|
1116
|
+
local TweenService = game:GetService("TweenService")
|
|
1117
|
+
local RunService = game:GetService("RunService")
|
|
1118
|
+
|
|
1119
|
+
local HitEffectSystem = {}
|
|
1120
|
+
|
|
1121
|
+
-- Configuration
|
|
1122
|
+
local DEFAULT_CONFIG = {
|
|
1123
|
+
-- Flash
|
|
1124
|
+
flashColor = Color3.new(1, 1, 1),
|
|
1125
|
+
flashDuration = 0.15,
|
|
1126
|
+
flashRevertDuration = 0.25,
|
|
1127
|
+
|
|
1128
|
+
-- Particles
|
|
1129
|
+
particleBurstCount = 20,
|
|
1130
|
+
particleLifetime = NumberRange.new(0.2, 0.5),
|
|
1131
|
+
particleSpeed = NumberRange.new(8, 15),
|
|
1132
|
+
particleSize = NumberSequence.new({
|
|
1133
|
+
NumberSequenceKeypoint.new(0, 0.5),
|
|
1134
|
+
NumberSequenceKeypoint.new(1, 0),
|
|
1135
|
+
}),
|
|
1136
|
+
particleColor = ColorSequence.new({
|
|
1137
|
+
ColorSequenceKeypoint.new(0, Color3.new(1, 1, 1)),
|
|
1138
|
+
ColorSequenceKeypoint.new(1, Color3.fromRGB(255, 200, 50)),
|
|
1139
|
+
}),
|
|
1140
|
+
particleTransparency = NumberSequence.new({
|
|
1141
|
+
NumberSequenceKeypoint.new(0, 0),
|
|
1142
|
+
NumberSequenceKeypoint.new(0.7, 0.3),
|
|
1143
|
+
NumberSequenceKeypoint.new(1, 1),
|
|
1144
|
+
}),
|
|
1145
|
+
|
|
1146
|
+
-- Sound
|
|
1147
|
+
hitSoundId = "rbxassetid://123456789", -- replace with your asset
|
|
1148
|
+
hitSoundVolume = 0.8,
|
|
1149
|
+
hitSoundPitchVariation = 0.15, -- random pitch +/- this amount
|
|
1150
|
+
|
|
1151
|
+
-- Camera shake
|
|
1152
|
+
shakeIntensity = 0.4,
|
|
1153
|
+
shakeDuration = 0.2,
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
-- Pre-create a reusable particle emitter template
|
|
1157
|
+
local function createHitEmitterTemplate(config: typeof(DEFAULT_CONFIG)): ParticleEmitter
|
|
1158
|
+
local emitter = Instance.new("ParticleEmitter")
|
|
1159
|
+
emitter.Name = "HitBurst"
|
|
1160
|
+
emitter.Rate = 0 -- manual emission only
|
|
1161
|
+
emitter.Lifetime = config.particleLifetime
|
|
1162
|
+
emitter.Speed = config.particleSpeed
|
|
1163
|
+
emitter.SpreadAngle = Vector2.new(180, 180) -- omnidirectional burst
|
|
1164
|
+
emitter.Size = config.particleSize
|
|
1165
|
+
emitter.Color = config.particleColor
|
|
1166
|
+
emitter.Transparency = config.particleTransparency
|
|
1167
|
+
emitter.LightEmission = 1
|
|
1168
|
+
emitter.Drag = 5
|
|
1169
|
+
emitter.RotSpeed = NumberRange.new(-180, 180)
|
|
1170
|
+
return emitter
|
|
1171
|
+
end
|
|
1172
|
+
|
|
1173
|
+
-- Emitter cache: one emitter per part (lazily created)
|
|
1174
|
+
local emitterCache: { [BasePart]: ParticleEmitter } = {}
|
|
1175
|
+
local emitterTemplate: ParticleEmitter? = nil
|
|
1176
|
+
|
|
1177
|
+
local function getOrCreateEmitter(part: BasePart, config: typeof(DEFAULT_CONFIG)): ParticleEmitter
|
|
1178
|
+
if emitterCache[part] then
|
|
1179
|
+
return emitterCache[part]
|
|
1180
|
+
end
|
|
1181
|
+
|
|
1182
|
+
if not emitterTemplate then
|
|
1183
|
+
emitterTemplate = createHitEmitterTemplate(config)
|
|
1184
|
+
end
|
|
1185
|
+
|
|
1186
|
+
local emitter = emitterTemplate:Clone()
|
|
1187
|
+
emitter.Parent = part
|
|
1188
|
+
emitterCache[part] = emitter
|
|
1189
|
+
|
|
1190
|
+
-- Clean up if part is destroyed
|
|
1191
|
+
part.Destroying:Connect(function()
|
|
1192
|
+
emitterCache[part] = nil
|
|
1193
|
+
end)
|
|
1194
|
+
|
|
1195
|
+
return emitter
|
|
1196
|
+
end
|
|
1197
|
+
|
|
1198
|
+
--[[
|
|
1199
|
+
Flash the target part white and tween back to original color.
|
|
1200
|
+
]]
|
|
1201
|
+
local function flashPart(part: BasePart, config: typeof(DEFAULT_CONFIG))
|
|
1202
|
+
local originalColor = part.Color
|
|
1203
|
+
part.Color = config.flashColor
|
|
1204
|
+
|
|
1205
|
+
local tweenBack = TweenService:Create(
|
|
1206
|
+
part,
|
|
1207
|
+
TweenInfo.new(config.flashRevertDuration, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
|
|
1208
|
+
{ Color = originalColor }
|
|
1209
|
+
)
|
|
1210
|
+
tweenBack:Play()
|
|
1211
|
+
end
|
|
1212
|
+
|
|
1213
|
+
--[[
|
|
1214
|
+
Emit a burst of particles from the hit location.
|
|
1215
|
+
]]
|
|
1216
|
+
local function emitParticleBurst(part: BasePart, config: typeof(DEFAULT_CONFIG))
|
|
1217
|
+
local emitter = getOrCreateEmitter(part, config)
|
|
1218
|
+
emitter:Emit(config.particleBurstCount)
|
|
1219
|
+
end
|
|
1220
|
+
|
|
1221
|
+
--[[
|
|
1222
|
+
Play a one-shot hit sound with slight pitch variation.
|
|
1223
|
+
]]
|
|
1224
|
+
local function playHitSound(part: BasePart, config: typeof(DEFAULT_CONFIG))
|
|
1225
|
+
local sound = Instance.new("Sound")
|
|
1226
|
+
sound.SoundId = config.hitSoundId
|
|
1227
|
+
sound.Volume = config.hitSoundVolume
|
|
1228
|
+
sound.PlaybackSpeed = 1 + (math.random() - 0.5) * 2 * config.hitSoundPitchVariation
|
|
1229
|
+
sound.RollOffMaxDistance = 80
|
|
1230
|
+
sound.RollOffMinDistance = 5
|
|
1231
|
+
sound.Parent = part
|
|
1232
|
+
sound:Play()
|
|
1233
|
+
|
|
1234
|
+
sound.Ended:Connect(function()
|
|
1235
|
+
sound:Destroy()
|
|
1236
|
+
end)
|
|
1237
|
+
end
|
|
1238
|
+
|
|
1239
|
+
--[[
|
|
1240
|
+
Apply a short screen shake that decays over time.
|
|
1241
|
+
]]
|
|
1242
|
+
local function shakeCamera(config: typeof(DEFAULT_CONFIG))
|
|
1243
|
+
local camera = workspace.CurrentCamera
|
|
1244
|
+
if not camera then return end
|
|
1245
|
+
|
|
1246
|
+
local elapsed = 0
|
|
1247
|
+
local intensity = config.shakeIntensity
|
|
1248
|
+
local duration = config.shakeDuration
|
|
1249
|
+
local connection: RBXScriptConnection
|
|
1250
|
+
|
|
1251
|
+
connection = RunService.RenderStepped:Connect(function(dt: number)
|
|
1252
|
+
elapsed += dt
|
|
1253
|
+
if elapsed >= duration then
|
|
1254
|
+
connection:Disconnect()
|
|
1255
|
+
return
|
|
1256
|
+
end
|
|
1257
|
+
|
|
1258
|
+
local decay = 1 - (elapsed / duration)
|
|
1259
|
+
local offsetX = (math.random() - 0.5) * 2 * intensity * decay
|
|
1260
|
+
local offsetY = (math.random() - 0.5) * 2 * intensity * decay
|
|
1261
|
+
|
|
1262
|
+
camera.CFrame = camera.CFrame * CFrame.new(offsetX, offsetY, 0)
|
|
1263
|
+
end)
|
|
1264
|
+
end
|
|
1265
|
+
|
|
1266
|
+
--[[
|
|
1267
|
+
Main entry point: trigger the full hit effect on a target part.
|
|
1268
|
+
|
|
1269
|
+
@param targetPart The BasePart that was hit (e.g., a character limb or NPC body).
|
|
1270
|
+
@param overrides Optional table to override any DEFAULT_CONFIG values.
|
|
1271
|
+
]]
|
|
1272
|
+
function HitEffectSystem.play(targetPart: BasePart, overrides: { [string]: any }?)
|
|
1273
|
+
-- Merge config with overrides
|
|
1274
|
+
local config = table.clone(DEFAULT_CONFIG)
|
|
1275
|
+
if overrides then
|
|
1276
|
+
for key, value in overrides do
|
|
1277
|
+
(config :: any)[key] = value
|
|
1278
|
+
end
|
|
1279
|
+
end
|
|
1280
|
+
|
|
1281
|
+
-- Fire all effects simultaneously
|
|
1282
|
+
flashPart(targetPart, config)
|
|
1283
|
+
emitParticleBurst(targetPart, config)
|
|
1284
|
+
playHitSound(targetPart, config)
|
|
1285
|
+
shakeCamera(config)
|
|
1286
|
+
end
|
|
1287
|
+
|
|
1288
|
+
--[[
|
|
1289
|
+
Clean up cached emitters (call when resetting scene or on player leave).
|
|
1290
|
+
]]
|
|
1291
|
+
function HitEffectSystem.cleanup()
|
|
1292
|
+
for part, emitter in emitterCache do
|
|
1293
|
+
emitter:Destroy()
|
|
1294
|
+
end
|
|
1295
|
+
table.clear(emitterCache)
|
|
1296
|
+
end
|
|
1297
|
+
|
|
1298
|
+
return HitEffectSystem
|
|
1299
|
+
|
|
1300
|
+
--[[
|
|
1301
|
+
USAGE EXAMPLE (in a LocalScript):
|
|
1302
|
+
|
|
1303
|
+
local HitEffectSystem = require(script.Parent.HitEffectSystem)
|
|
1304
|
+
|
|
1305
|
+
-- When a hit is confirmed (e.g., via RemoteEvent from server):
|
|
1306
|
+
hitRemote.OnClientEvent:Connect(function(targetPart: BasePart)
|
|
1307
|
+
HitEffectSystem.play(targetPart)
|
|
1308
|
+
end)
|
|
1309
|
+
|
|
1310
|
+
-- Custom overrides for a critical hit:
|
|
1311
|
+
hitRemote.OnClientEvent:Connect(function(targetPart: BasePart, isCritical: boolean)
|
|
1312
|
+
if isCritical then
|
|
1313
|
+
HitEffectSystem.play(targetPart, {
|
|
1314
|
+
flashColor = Color3.fromRGB(255, 50, 50),
|
|
1315
|
+
particleBurstCount = 40,
|
|
1316
|
+
shakeIntensity = 0.8,
|
|
1317
|
+
shakeDuration = 0.4,
|
|
1318
|
+
hitSoundId = "rbxassetid://critical_hit_sound_id",
|
|
1319
|
+
})
|
|
1320
|
+
else
|
|
1321
|
+
HitEffectSystem.play(targetPart)
|
|
1322
|
+
end
|
|
1323
|
+
end)
|
|
1324
|
+
]]
|
|
1325
|
+
```
|