roblox-animation-vfx
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinese<!-- Source: brockmartin/roblox-game-skill (MIT) -->
<!-- 来源:brockmartin/roblox-game-skill(MIT协议) -->
Animation & VFX Reference
动画与VFX参考文档
Overview
概述
Load this reference when working on:
- Character or NPC animations (idle, walk, attack, emotes)
- Particle effects (fire, smoke, sparkles, magic, weather)
- Beams and trails (lasers, sword swings, magic projectiles)
- TweenService-driven visual feedback (hit flashes, pulses, transitions)
- Lighting and post-processing (mood, atmosphere, glow)
- Sound design and positional audio
- Camera effects (shake, zoom, cutscenes)
- General visual polish and juice
在处理以下内容时可参考本文档:
- 角色或NPC动画( idle、行走、攻击、表情动作)
- 粒子效果(火焰、烟雾、火花、魔法、天气)
- 光束与轨迹(激光、挥剑、魔法投射物)
- TweenService驱动的视觉反馈(击中闪光、脉冲、过渡效果)
- 光照与后期处理(氛围、环境、光晕)
- 音效设计与位置音频(positional audio)
- 相机效果(震动、缩放、过场动画)
- 通用视觉优化与细节打磨
Quick Reference
快速参考
Load Full Reference below only when you need specific property values, recipes, or implementation details.
Key rules:
- Animations need uploaded AnimationIds (rbxassetid://). Never invent IDs.
- Priority order: Core < Idle < Movement < Action. Higher priority overrides lower on same track.
- Always use (on Humanoid/AnimationController), not deprecated
AnimatorHumanoid:LoadAnimation() - MarkerReachedSignal for syncing sounds/VFX to animation frames
- ParticleEmitter: Rate=0 + Emit(count) for burst effects. Enabled=false to stop new particles.
- Beams need Attachment0 + Attachment1. Trails need one Attachment.
- Highlight: parent to target or set Adornee. Max 255 per client. AlwaysOnTop to see through geometry.
- TweenService: create TweenInfo once, reuse. Chain with Completed event, don't nest.
- Post-processing: keep subtle. Bloom + ColorCorrection + DepthOfField cover most moods.
- Clean up: Destroy() particles/beams when done. Use Trove for lifecycle.
仅在需要特定属性值、实现方案或细节时,再查看下方的完整参考内容。
核心规则:
- 动画需要已上传的AnimationIds(格式为rbxassetid://)。切勿自行编造ID。
- 优先级顺序:Core < Idle < Movement < Action。同一轨道上,高优先级会覆盖低优先级动画。
- 始终使用(挂载在Humanoid/AnimationController上),而非已弃用的
AnimatorHumanoid:LoadAnimation() - 使用MarkerReachedSignal将音效/VFX与动画帧同步
- 对于爆发式效果,将ParticleEmitter的Rate设为0 + 调用Emit(count)。设置Enabled=false可停止生成新粒子。
- 光束需要Attachment0 + Attachment1。轨迹只需要一个Attachment。
- Highlight:挂载到目标对象或设置Adornee。每个客户端最多支持255个。开启AlwaysOnTop可穿透几何体显示。
- TweenService:创建一次TweenInfo后重复使用。通过Completed事件链式调用,不要嵌套。
- 后期处理:保持效果柔和。Bloom + ColorCorrection + DepthOfField可覆盖大多数氛围需求。
- 清理:使用完毕后调用Destroy()销毁粒子/光束。使用Trove管理生命周期。
Full Reference
完整参考
Character Animation
角色动画
Animator Service on Humanoid
Humanoid上的Animator服务
Every has (or should have) an child. The is the engine that plays, blends, and prioritizes animation tracks on a character rig.
HumanoidAnimatorAnimatorluau
local character = player.Character or player.CharacterAdded:Wait()
local humanoid = character:WaitForChild("Humanoid")
local animator = humanoid:FindFirstChildOfClass("Animator")
or humanoid:WaitForChild("Animator")每个都有(或应该有)一个子对象。是在角色骨架上播放、混合并管理动画轨道优先级的核心引擎。
HumanoidAnimatorAnimatorluau
local character = player.Character or player.CharacterAdded:Wait()
local humanoid = character:WaitForChild("Humanoid")
local animator = humanoid:FindFirstChildOfClass("Animator")
or humanoid:WaitForChild("Animator")Loading and Playing Animations
加载与播放动画
luau
-- 1. Create an Animation instance with the asset ID
local slashAnim = Instance.new("Animation")
slashAnim.AnimationId = "rbxassetid://123456789"
-- 2. Load it through the Animator (returns an AnimationTrack)
local slashTrack = animator:LoadAnimation(slashAnim)
-- 3. Play / Stop
slashTrack:Play()
-- Optional fade time and weight
slashTrack:Play(0.2) -- 0.2s fade-in
slashTrack:Stop(0.3) -- 0.3s fade-out
-- 4. Adjust speed at runtime
slashTrack:AdjustSpeed(1.5) -- 1.5x playback
slashTrack:AdjustWeight(0.8) -- 80% blend weightluau
-- 1. 使用资源ID创建Animation实例
local slashAnim = Instance.new("Animation")
slashAnim.AnimationId = "rbxassetid://123456789"
-- 2. 通过Animator加载动画(返回AnimationTrack)
local slashTrack = animator:LoadAnimation(slashAnim)
-- 3. 播放 / 停止
slashTrack:Play()
-- 可选:设置淡入时间与权重
slashTrack:Play(0.2) -- 0.2秒淡入
slashTrack:Stop(0.3) -- 0.3秒淡出
-- 4. 运行时调整速度
slashTrack:AdjustSpeed(1.5) -- 1.5倍播放速度
slashTrack:AdjustWeight(0.8) -- 80%混合权重Animation Priorities
动画优先级
Priorities determine which animation wins when multiple tracks affect the same joints. Higher priority overrides lower.
| Priority | Use Case |
|---|---|
| Breathing, idle sway |
| Walk, run, jump, fall |
| Attack, interact, emote |
| Higher-priority actions |
| Even higher-priority actions |
| Highest action tier |
| Internal Roblox (avoid overriding) |
luau
slashTrack.Priority = Enum.AnimationPriority.Action当多个轨道影响同一关节时,优先级决定哪个动画生效。高优先级会覆盖低优先级。
| 优先级 | 使用场景 |
|---|---|
| 呼吸、 idle晃动 |
| 行走、奔跑、跳跃、坠落 |
| 攻击、交互、表情动作 |
| 更高优先级的动作 |
| 优先级更高的动作 |
| 最高优先级的动作 |
| Roblox内部动画(避免覆盖) |
luau
slashTrack.Priority = Enum.AnimationPriority.ActionMarkerReachedSignal
MarkerReachedSignal
Animation events let you fire logic at exact frames inside an animation (set markers in the Animation Editor).
luau
slashTrack:GetMarkerReachedSignal("HitFrame"):Connect(function(paramValue: string)
-- Spawn hitbox, play sound, emit particles, etc.
print("Hit frame reached!", paramValue)
end)动画事件可让你在动画的特定帧触发逻辑(在动画编辑器中设置标记)。
luau
slashTrack:GetMarkerReachedSignal("HitFrame"):Connect(function(paramValue: string)
-- 生成碰撞盒、播放音效、发射粒子等
print("击中帧触发!", paramValue)
end)Blending Between Animations
动画间混合
Roblox automatically blends overlapping tracks based on priority and weight. To cross-fade manually:
luau
local walkTrack = animator:LoadAnimation(walkAnim)
local runTrack = animator:LoadAnimation(runAnim)
walkTrack:Play(0.2)
-- Later, cross-fade to run
walkTrack:Stop(0.3)
runTrack:Play(0.3)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.
Roblox会根据优先级和权重自动混合重叠的轨道。如需手动交叉淡入淡出:
luau
local walkTrack = animator:LoadAnimation(walkAnim)
local runTrack = animator:LoadAnimation(runAnim)
walkTrack:Play(0.2)
-- 之后,交叉淡入到奔跑动画
walkTrack:Stop(0.3)
runTrack:Play(0.3)对于局部身体分层(比如腿部奔跑时上半身攻击),设置不同优先级,并确保低优先级动画仅驱动下半身关节。
AnimationController
AnimationController
Use for anything that is NOT a -- props, doors, creatures with custom rigs, cutscene actors, etc.
AnimationControllerHumanoidluau
local model = workspace.DragonNPC
local animController = Instance.new("AnimationController")
animController.Parent = model
local flyAnim = Instance.new("Animation")
flyAnim.AnimationId = "rbxassetid://987654321"
local flyTrack = animController:LoadAnimation(flyAnim)
flyTrack.Looped = true
flyTrack:Play()对于非对象(道具、门、自定义骨架的生物、过场动画角色等),使用。
HumanoidAnimationControllerluau
local model = workspace.DragonNPC
local animController = Instance.new("AnimationController")
animController.Parent = model
local flyAnim = Instance.new("Animation")
flyAnim.AnimationId = "rbxassetid://987654321"
local flyTrack = animController:LoadAnimation(flyAnim)
flyTrack.Looped = true
flyTrack:Play()Custom Rigs
自定义骨架
- The model needs joints connecting its parts, just like a character rig.
Motor6D - Root part should be the of the model.
PrimaryPart - Animations are authored in the Animation Editor against this rig, then exported.
- 模型需要关节连接各个部件,就像角色骨架一样。
Motor6D - 根部件应设为模型的。
PrimaryPart - 在动画编辑器中针对该骨架制作动画,然后导出。
Attaching to Models
挂载到模型
luau
-- Typical setup for an animated NPC without Humanoid
local npc = Instance.new("Model")
npc.Name = "CrystalGolem"
local rootPart = Instance.new("Part")
rootPart.Name = "HumanoidRootPart" -- convention for animation rigs
rootPart.Anchored = false
rootPart.Parent = npc
npc.PrimaryPart = rootPart
local animController = Instance.new("AnimationController")
animController.Parent = npc
-- Add Motor6D joints, mesh parts, then load animationsluau
-- 无Humanoid的动画NPC典型设置
local npc = Instance.new("Model")
npc.Name = "CrystalGolem"
local rootPart = Instance.new("Part")
rootPart.Name = "HumanoidRootPart" -- 动画骨架的命名惯例
rootPart.Anchored = false
rootPart.Parent = npc
npc.PrimaryPart = rootPart
local animController = Instance.new("AnimationController")
animController.Parent = npc
-- 添加Motor6D关节、网格部件,然后加载动画Particle Effects
粒子效果
ParticleEmitter Core Properties
ParticleEmitter核心属性
| Property | Type | Description |
|---|---|---|
| | Particles emitted per second (0 = manual Emit()) |
| | How long each particle lives (seconds) |
| | Initial velocity (studs/second) |
| | Cone spread in X and Y (degrees) |
| | Size over particle lifetime |
| | Color over particle lifetime |
| | Transparency over particle lifetime |
| | Decal/image asset ID for particle appearance |
| | Rotation speed (degrees/second) |
| | Constant force (gravity = |
| | Air resistance (0 = none, higher = more drag) |
| | 0-1, additive blending (1 = fully additive/glowy) |
| | 0-1, how much scene lighting affects particles |
| | Render order offset toward/away from camera |
| | FacingCamera, VelocityParallel, etc. |
| 属性名称 | 类型 | 描述 |
|---|---|---|
| | 每秒发射的粒子数(0 = 手动调用Emit()) |
| | 每个粒子的生命周期(秒) |
| | 初始速度( studs/秒) |
| | X和Y轴的圆锥扩散角度(度) |
| | 粒子生命周期内的尺寸变化 |
| | 粒子生命周期内的颜色变化 |
| | 粒子生命周期内的透明度变化 |
| | 粒子外观的贴图/图像资源ID |
| | 旋转速度(度/秒) |
| | 恒定作用力(重力为 |
| | 空气阻力(0 = 无阻力,值越大阻力越强) |
| | 0-1, additive blending(1 = 完全叠加/发光) |
| | 0-1,场景光照对粒子的影响程度 |
| | 渲染顺序偏移(靠近/远离相机) |
| | 朝向相机、与速度平行等模式 |
NumberSequence and ColorSequence
NumberSequence与ColorSequence
luau
-- Size: start at 1, peak at 2 at midlife, shrink to 0
local sizeSeq = NumberSequence.new({
NumberSequenceKeypoint.new(0, 1),
NumberSequenceKeypoint.new(0.5, 2),
NumberSequenceKeypoint.new(1, 0),
})
-- Color: orange to red
local colorSeq = ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.fromRGB(255, 170, 0)),
ColorSequenceKeypoint.new(1, Color3.fromRGB(200, 30, 0)),
})
-- Transparency: fade in then fade out
local transSeq = NumberSequence.new({
NumberSequenceKeypoint.new(0, 1),
NumberSequenceKeypoint.new(0.1, 0),
NumberSequenceKeypoint.new(0.8, 0),
NumberSequenceKeypoint.new(1, 1),
})luau
-- 尺寸:初始为1,生命周期中期达到峰值2,最后缩小到0
local sizeSeq = NumberSequence.new({
NumberSequenceKeypoint.new(0, 1),
NumberSequenceKeypoint.new(0.5, 2),
NumberSequenceKeypoint.new(1, 0),
})
-- 颜色:从橙色过渡到红色
local colorSeq = ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.fromRGB(255, 170, 0)),
ColorSequenceKeypoint.new(1, Color3.fromRGB(200, 30, 0)),
})
-- 透明度:先淡入再淡出
local transSeq = NumberSequence.new({
NumberSequenceKeypoint.new(0, 1),
NumberSequenceKeypoint.new(0.1, 0),
NumberSequenceKeypoint.new(0.8, 0),
NumberSequenceKeypoint.new(1, 1),
})Common Effect Recipes
常见效果实现方案
Fire
火焰
luau
local fire = Instance.new("ParticleEmitter")
fire.Rate = 80
fire.Lifetime = NumberRange.new(0.4, 0.8)
fire.Speed = NumberRange.new(3, 6)
fire.SpreadAngle = Vector2.new(15, 15)
fire.Size = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0.5),
NumberSequenceKeypoint.new(0.3, 1.5),
NumberSequenceKeypoint.new(1, 0),
})
fire.Color = ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.fromRGB(255, 220, 50)),
ColorSequenceKeypoint.new(0.4, Color3.fromRGB(255, 100, 0)),
ColorSequenceKeypoint.new(1, Color3.fromRGB(100, 20, 0)),
})
fire.Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0.3),
NumberSequenceKeypoint.new(1, 1),
})
fire.LightEmission = 1
fire.Acceleration = Vector3.new(0, 4, 0)
fire.Texture = "rbxasset://textures/particles/fire_main.dds"
fire.Parent = somePartluau
local fire = Instance.new("ParticleEmitter")
fire.Rate = 80
fire.Lifetime = NumberRange.new(0.4, 0.8)
fire.Speed = NumberRange.new(3, 6)
fire.SpreadAngle = Vector2.new(15, 15)
fire.Size = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0.5),
NumberSequenceKeypoint.new(0.3, 1.5),
NumberSequenceKeypoint.new(1, 0),
})
fire.Color = ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.fromRGB(255, 220, 50)),
ColorSequenceKeypoint.new(0.4, Color3.fromRGB(255, 100, 0)),
ColorSequenceKeypoint.new(1, Color3.fromRGB(100, 20, 0)),
})
fire.Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0.3),
NumberSequenceKeypoint.new(1, 1),
})
fire.LightEmission = 1
fire.Acceleration = Vector3.new(0, 4, 0)
fire.Texture = "rbxasset://textures/particles/fire_main.dds"
fire.Parent = somePartSmoke
烟雾
luau
local smoke = Instance.new("ParticleEmitter")
smoke.Rate = 30
smoke.Lifetime = NumberRange.new(2, 4)
smoke.Speed = NumberRange.new(1, 3)
smoke.SpreadAngle = Vector2.new(30, 30)
smoke.Size = NumberSequence.new({
NumberSequenceKeypoint.new(0, 1),
NumberSequenceKeypoint.new(1, 5),
})
smoke.Color = ColorSequence.new(Color3.fromRGB(120, 120, 120))
smoke.Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0.5),
NumberSequenceKeypoint.new(1, 1),
})
smoke.RotSpeed = NumberRange.new(-30, 30)
smoke.Acceleration = Vector3.new(0, 2, 0)
smoke.LightInfluence = 1
smoke.Parent = somePartluau
local smoke = Instance.new("ParticleEmitter")
smoke.Rate = 30
smoke.Lifetime = NumberRange.new(2, 4)
smoke.Speed = NumberRange.new(1, 3)
smoke.SpreadAngle = Vector2.new(30, 30)
smoke.Size = NumberSequence.new({
NumberSequenceKeypoint.new(0, 1),
NumberSequenceKeypoint.new(1, 5),
})
smoke.Color = ColorSequence.new(Color3.fromRGB(120, 120, 120))
smoke.Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0.5),
NumberSequenceKeypoint.new(1, 1),
})
smoke.RotSpeed = NumberRange.new(-30, 30)
smoke.Acceleration = Vector3.new(0, 2, 0)
smoke.LightInfluence = 1
smoke.Parent = somePartSparkles / Magic Particles
火花 / 魔法粒子
luau
local sparkle = Instance.new("ParticleEmitter")
sparkle.Rate = 40
sparkle.Lifetime = NumberRange.new(0.5, 1.2)
sparkle.Speed = NumberRange.new(2, 5)
sparkle.SpreadAngle = Vector2.new(180, 180)
sparkle.Size = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0.3),
NumberSequenceKeypoint.new(0.5, 0.6),
NumberSequenceKeypoint.new(1, 0),
})
sparkle.Color = ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.fromRGB(200, 200, 255)),
ColorSequenceKeypoint.new(1, Color3.fromRGB(100, 100, 255)),
})
sparkle.LightEmission = 1
sparkle.Texture = "rbxasset://textures/particles/sparkles_main.dds"
sparkle.Parent = somePartluau
local sparkle = Instance.new("ParticleEmitter")
sparkle.Rate = 40
sparkle.Lifetime = NumberRange.new(0.5, 1.2)
sparkle.Speed = NumberRange.new(2, 5)
sparkle.SpreadAngle = Vector2.new(180, 180)
sparkle.Size = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0.3),
NumberSequenceKeypoint.new(0.5, 0.6),
NumberSequenceKeypoint.new(1, 0),
})
sparkle.Color = ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.fromRGB(200, 200, 255)),
ColorSequenceKeypoint.new(1, Color3.fromRGB(100, 100, 255)),
})
sparkle.LightEmission = 1
sparkle.Texture = "rbxasset://textures/particles/sparkles_main.dds"
sparkle.Parent = somePartRain
下雨
luau
local rain = Instance.new("ParticleEmitter")
rain.Rate = 300
rain.Lifetime = NumberRange.new(0.8, 1.2)
rain.Speed = NumberRange.new(40, 60)
rain.SpreadAngle = Vector2.new(5, 5)
rain.Size = NumberSequence.new(0.05)
rain.Color = ColorSequence.new(Color3.fromRGB(180, 200, 220))
rain.Transparency = NumberSequence.new(0.4)
rain.Acceleration = Vector3.new(0, -80, 0)
rain.Drag = 0
rain.Orientation = Enum.ParticleOrientation.VelocityParallel
rain.Parent = largeCoverPart -- position above the play arealuau
local rain = Instance.new("ParticleEmitter")
rain.Rate = 300
rain.Lifetime = NumberRange.new(0.8, 1.2)
rain.Speed = NumberRange.new(40, 60)
rain.SpreadAngle = Vector2.new(5, 5)
rain.Size = NumberSequence.new(0.05)
rain.Color = ColorSequence.new(Color3.fromRGB(180, 200, 220))
rain.Transparency = NumberSequence.new(0.4)
rain.Acceleration = Vector3.new(0, -80, 0)
rain.Drag = 0
rain.Orientation = Enum.ParticleOrientation.VelocityParallel
rain.Parent = largeCoverPart -- 放置在游戏区域上方Snow
下雪
luau
local snow = Instance.new("ParticleEmitter")
snow.Rate = 100
snow.Lifetime = NumberRange.new(4, 7)
snow.Speed = NumberRange.new(1, 3)
snow.SpreadAngle = Vector2.new(60, 60)
snow.Size = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0.1),
NumberSequenceKeypoint.new(1, 0.15),
})
snow.Color = ColorSequence.new(Color3.new(1, 1, 1))
snow.Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0),
NumberSequenceKeypoint.new(0.8, 0),
NumberSequenceKeypoint.new(1, 1),
})
snow.Acceleration = Vector3.new(0, -2, 0)
snow.RotSpeed = NumberRange.new(-60, 60)
snow.Drag = 3
snow.Parent = largeCoverPartluau
local snow = Instance.new("ParticleEmitter")
snow.Rate = 100
snow.Lifetime = NumberRange.new(4, 7)
snow.Speed = NumberRange.new(1, 3)
snow.SpreadAngle = Vector2.new(60, 60)
snow.Size = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0.1),
NumberSequenceKeypoint.new(1, 0.15),
})
snow.Color = ColorSequence.new(Color3.new(1, 1, 1))
snow.Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0),
NumberSequenceKeypoint.new(0.8, 0),
NumberSequenceKeypoint.new(1, 1),
})
snow.Acceleration = Vector3.new(0, -2, 0)
snow.RotSpeed = NumberRange.new(-60, 60)
snow.Drag = 3
snow.Parent = largeCoverPartMagic Aura (orbiting particles)
魔法光环(环绕粒子)
luau
local aura = Instance.new("ParticleEmitter")
aura.Rate = 25
aura.Lifetime = NumberRange.new(1, 2)
aura.Speed = NumberRange.new(0.5, 1.5)
aura.SpreadAngle = Vector2.new(180, 180)
aura.Size = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0),
NumberSequenceKeypoint.new(0.3, 0.8),
NumberSequenceKeypoint.new(1, 0),
})
aura.Color = ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.fromRGB(100, 0, 255)),
ColorSequenceKeypoint.new(1, Color3.fromRGB(200, 50, 255)),
})
aura.Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 1),
NumberSequenceKeypoint.new(0.2, 0.2),
NumberSequenceKeypoint.new(1, 1),
})
aura.LightEmission = 1
aura.RotSpeed = NumberRange.new(-90, 90)
aura.Drag = 5
aura.Parent = character.HumanoidRootPartluau
local aura = Instance.new("ParticleEmitter")
aura.Rate = 25
aura.Lifetime = NumberRange.new(1, 2)
aura.Speed = NumberRange.new(0.5, 1.5)
aura.SpreadAngle = Vector2.new(180, 180)
aura.Size = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0),
NumberSequenceKeypoint.new(0.3, 0.8),
NumberSequenceKeypoint.new(1, 0),
})
aura.Color = ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.fromRGB(100, 0, 255)),
ColorSequenceKeypoint.new(1, Color3.fromRGB(200, 50, 255)),
})
aura.Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 1),
NumberSequenceKeypoint.new(0.2, 0.2),
NumberSequenceKeypoint.new(1, 1),
})
aura.LightEmission = 1
aura.RotSpeed = NumberRange.new(-90, 90)
aura.Drag = 5
aura.Parent = character.HumanoidRootPartBeam and Trail
光束与轨迹
Beam
光束
A renders a textured ribbon between two instances. Perfect for lasers, lightning, tethers, and energy connections.
BeamAttachmentluau
-- Setup: two parts with attachments
local att0 = Instance.new("Attachment")
att0.Parent = partA
local att1 = Instance.new("Attachment")
att1.Parent = partB
local beam = Instance.new("Beam")
beam.Attachment0 = att0
beam.Attachment1 = att1
beam.Width0 = 0.5
beam.Width1 = 0.5
beam.Color = ColorSequence.new(Color3.fromRGB(0, 150, 255))
beam.Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0),
NumberSequenceKeypoint.new(1, 0.5),
})
beam.LightEmission = 1
beam.FaceCamera = true
beam.Segments = 20 -- more segments = smoother curves
beam.CurveSize0 = 2 -- bend near Attachment0
beam.CurveSize1 = -2 -- bend near Attachment1
beam.TextureLength = 1
beam.TextureSpeed = 1 -- scrolling texture
beam.Texture = "rbxassetid://123456789"
beam.Parent = partABeamAttachmentluau
-- 设置:两个带有Attachment的部件
local att0 = Instance.new("Attachment")
att0.Parent = partA
local att1 = Instance.new("Attachment")
att1.Parent = partB
local beam = Instance.new("Beam")
beam.Attachment0 = att0
beam.Attachment1 = att1
beam.Width0 = 0.5
beam.Width1 = 0.5
beam.Color = ColorSequence.new(Color3.fromRGB(0, 150, 255))
beam.Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0),
NumberSequenceKeypoint.new(1, 0.5),
})
beam.LightEmission = 1
beam.FaceCamera = true
beam.Segments = 20 -- 分段数越多,曲线越平滑
beam.CurveSize0 = 2 -- 在Attachment0附近弯曲
beam.CurveSize1 = -2 -- 在Attachment1附近弯曲
beam.TextureLength = 1
beam.TextureSpeed = 1 -- 纹理滚动速度
beam.Texture = "rbxassetid://123456789"
beam.Parent = partAKey Beam Properties
光束核心属性
| Property | Description |
|---|---|
| Start and end points |
| Width at each attachment (studs) |
| |
| |
| Bezier curve magnitude at each end |
| Number of straight segments (more = smoother curves) |
| Always faces the camera for billboard effect |
| Scrolls the texture along the beam |
| Additive blending for glow |
| 属性名称 | 描述 |
|---|---|
| 起点和终点 |
| 每个Attachment处的宽度(studs) |
| 光束长度方向的 |
| 光束长度方向的 |
| 两端的贝塞尔曲线幅度 |
| 直线分段数(越多曲线越平滑) |
| 始终朝向相机,实现公告板效果 |
| 沿光束滚动纹理 |
| 叠加混合实现发光效果 |
Trail
轨迹
A renders a ribbon behind a moving part. Requires two instances on the same part (defining the trail's width axis).
TrailAttachmentluau
local part = workspace.Sword.Blade
local att0 = Instance.new("Attachment")
att0.Position = Vector3.new(0, 0, -2) -- base of blade
att0.Parent = part
local att1 = Instance.new("Attachment")
att1.Position = Vector3.new(0, 0, 2) -- tip of blade
att1.Parent = part
local trail = Instance.new("Trail")
trail.Attachment0 = att0
trail.Attachment1 = att1
trail.Lifetime = 0.3 -- how long segments persist
trail.MinLength = 0.05 -- minimum distance before new segment
trail.FaceCamera = true
trail.Color = ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.fromRGB(255, 255, 255)),
ColorSequenceKeypoint.new(1, Color3.fromRGB(100, 100, 255)),
})
trail.Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0),
NumberSequenceKeypoint.new(1, 1),
})
trail.LightEmission = 0.8
trail.WidthScale = NumberSequence.new(1)
trail.Parent = partTrailAttachmentluau
local part = workspace.Sword.Blade
local att0 = Instance.new("Attachment")
att0.Position = Vector3.new(0, 0, -2) -- 剑柄位置
att0.Parent = part
local att1 = Instance.new("Attachment")
att1.Position = Vector3.new(0, 0, 2) -- 剑尖位置
att1.Parent = part
local trail = Instance.new("Trail")
trail.Attachment0 = att0
trail.Attachment1 = att1
trail.Lifetime = 0.3 -- 轨迹片段的持续时间
trail.MinLength = 0.05 -- 生成新片段的最小移动距离
trail.FaceCamera = true
trail.Color = ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.fromRGB(255, 255, 255)),
ColorSequenceKeypoint.new(1, Color3.fromRGB(100, 100, 255)),
})
trail.Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0),
NumberSequenceKeypoint.new(1, 1),
})
trail.LightEmission = 0.8
trail.WidthScale = NumberSequence.new(1)
trail.Parent = partCommon Uses
常见用途
- Laser beams: between gun barrel attachment and hit-point attachment.
Beam - Sword trails: on blade with short
Trail(0.2-0.4s).Lifetime - Magic effects: with high
Beamvalues and scrolling texture for arcane tethers.CurveSize - Lightning: with many
Beam, rapidly randomizingSegmentseach frame.CurveSize0/1
- 激光束:连接枪口Attachment和命中点Attachment。
Beam - 挥剑轨迹:在剑刃上添加,设置较短的
Trail(0.2-0.4秒)。Lifetime - 魔法效果:设置高
Beam值和滚动纹理,实现神秘能量绳索效果。CurveSize - 闪电:设置大量
Beam,每帧随机改变Segments。CurveSize0/1
Parent Destruction Behavior
父对象销毁行为
When a or containing effects is destroyed (, player leave, workspace clear), all child , , , and are destroyed instantly. Active particles vanish mid-flight, trails cut off, beams disappear.
PartModel:Destroy()ParticleEmitterTrailBeamAttachmentSolution - Debris + temporary holder: If you need an effect to finish gracefully after its parent is gone, reparent it to a temporary part and let clean up.
Debrisluau
local Debris = game:GetService("Debris")
local function destroyWithGrace(effect: Instance, parent: Instance, gracePeriod: number)
-- Create a temporary invisible holder
local holder = Instance.new("Part")
holder.Anchored = true
holder.CanCollide = false
holder.Transparency = 1
holder.Size = Vector3.one
holder.Parent = workspace
-- Reparent the effect so it survives the original parent
effect.Parent = holder
-- Destroy the original parent (effect is now safe)
parent:Destroy()
-- Debris cleans up the holder + effect after the grace period
Debris:AddItem(holder, gracePeriod)
end
-- Example: Trail with 1s lifetime - give it 1.1s to fade out cleanly
local trail = -- ... setup trail on a sword part ...
destroyWithGrace(trail, swordPart, 1.1)For specifically, set so the trail's existing segments finish rendering before cleanup.
TrailDebris:AddItem(holder, trail.Lifetime + 0.1)当包含效果的或被销毁(、玩家离开、工作区清空)时,所有子对象、、和会立即被销毁。活跃粒子会中途消失,轨迹被切断,光束消失。
PartModel:Destroy()ParticleEmitterTrailBeamAttachment解决方案 - Debris + 临时容器:如果需要效果在父对象销毁后优雅结束,可将其重新挂载到临时部件,然后用清理。
Debrisluau
local Debris = game:GetService("Debris")
local function destroyWithGrace(effect: Instance, parent: Instance, gracePeriod: number)
-- 创建临时不可见容器
local holder = Instance.new("Part")
holder.Anchored = true
holder.CanCollide = false
holder.Transparency = 1
holder.Size = Vector3.one
holder.Parent = workspace
-- 将效果重新挂载,使其在原父对象销毁后存活
effect.Parent = holder
-- 销毁原父对象(效果现在安全了)
parent:Destroy()
-- 等待宽限期后,Debris清理容器和效果
Debris:AddItem(holder, gracePeriod)
end
-- 示例:轨迹生命周期为1秒 - 给1.1秒让它完全淡出
local trail = -- ... 在剑部件上设置轨迹 ...
destroyWithGrace(trail, swordPart, 1.1)对于,设置,确保轨迹的现有片段在清理前完成渲染。
TrailDebris:AddItem(holder, trail.Lifetime + 0.1)Highlight
Highlight
A instance draws a colored outline around a or to call attention to it. Every highlight has two layers: an outline (silhouette edge) and an interior (overlay fill), each independently customizable.
HighlightBasePartModelHighlightBasePartModelBasic Usage
基础用法
luau
local highlight = Instance.new("Highlight")
highlight.Adornee = targetPart
highlight.FillColor = Color3.fromRGB(255, 50, 50)
highlight.FillTransparency = 0.3
highlight.OutlineColor = Color3.new(1, 1, 1)
highlight.Parent = targetPartluau
local highlight = Instance.new("Highlight")
highlight.Adornee = targetPart
highlight.FillColor = Color3.fromRGB(255, 50, 50)
highlight.FillTransparency = 0.3
highlight.OutlineColor = Color3.new(1, 1, 1)
highlight.Parent = targetPartProperties
属性
| Property | Type | Default | Description |
|---|---|---|---|
| Instance | - | The |
| Enum.HighlightDepthMode | | |
| boolean | | Toggle visibility |
| Color3 | | Interior overlay color |
| number | | 0 = opaque, 1 = invisible |
| Color3 | | Edge outline color |
| number | | 0 = opaque, 1 = invisible |
| 属性名称 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| Instance | - | 要高亮的 |
| Enum.HighlightDepthMode | | |
| boolean | | 切换可见性 |
| Color3 | | 内部填充颜色 |
| number | | 0 = 不透明,1 = 完全透明 |
| Color3 | | 边缘轮廓颜色 |
| number | | 0 = 不透明,1 = 完全透明 |
Common Pattern - Team Highlight
常见模式 - 队伍高亮
luau
local function addTeamHighlight(character: Model, teamColor: Color3)
local hl = Instance.new("Highlight")
hl.Adornee = character
hl.FillColor = teamColor
hl.FillTransparency = 0.6
hl.OutlineColor = teamColor
hl.DepthMode = Enum.HighlightDepthMode.AlwaysOnTop
hl.Parent = character
endluau
local function addTeamHighlight(character: Model, teamColor: Color3)
local hl = Instance.new("Highlight")
hl.Adornee = character
hl.FillColor = teamColor
hl.FillTransparency = 0.6
hl.OutlineColor = teamColor
hl.DepthMode = Enum.HighlightDepthMode.AlwaysOnTop
hl.Parent = character
endLimitations
限制
- Max 255 simultaneous Highlight instances per client. Excess instances are silently ignored.
- Disabled highlights still count toward the 255 limit - instead of
:Destroy()if permanently unused.Enabled = false - The itself is not destroyed when its
Highlightis destroyed. Clean up manually.Adornee
- 每个客户端最多255个同时存在的Highlight实例。超出的实例会被静默忽略。
- 禁用的Highlight仍会占用255个名额 - 若永久不再使用,调用而非设置
:Destroy()。Enabled = false - 当被销毁时,
Adornee本身不会被销毁。需手动清理。Highlight
Cleanup on Adornee Destroyed
Adornee销毁时自动清理
luau
local function attachHighlight(adornee: Instance): Highlight
local hl = Instance.new("Highlight")
hl.Adornee = adornee
hl.Parent = adornee
adornee.AncestryChanged:Connect(function()
if not adornee:IsDescendantOf(game) then
hl:Destroy()
end
end)
return hl
endluau
local function attachHighlight(adornee: Instance): Highlight
local hl = Instance.new("Highlight")
hl.Adornee = adornee
hl.Parent = adornee
adornee.AncestryChanged:Connect(function()
if not adornee:IsDescendantOf(game) then
hl:Destroy()
end
end)
return hl
endTweenService for VFX
用于VFX的TweenService
TweenServiceTweenServiceTweenInfo
TweenInfo
luau
local TweenService = game:GetService("TweenService")
local info = TweenInfo.new(
0.5, -- Duration (seconds)
Enum.EasingStyle.Quad, -- Easing style
Enum.EasingDirection.Out, -- Easing direction
0, -- RepeatCount (0 = no repeat, -1 = infinite)
false, -- Reverses (plays backward after forward)
0 -- DelayTime (seconds before starting)
)luau
local TweenService = game:GetService("TweenService")
local info = TweenInfo.new(
0.5, -- 持续时间(秒)
Enum.EasingStyle.Quad, -- 缓动风格
Enum.EasingDirection.Out, -- 缓动方向
0, -- 重复次数(0 = 不重复,-1 = 无限重复)
false, -- 是否反向播放(正向播放后反向)
0 -- 延迟时间(秒)
)Common Easing Styles
常见缓动风格
| Style | Feel |
|---|---|
| Constant speed, mechanical |
| Gentle acceleration/deceleration |
| Stronger ease |
| Even stronger |
| Smooth, organic |
| Overshoots then settles |
| Bounces at the end |
| Springy overshoot |
| Very sharp acceleration |
| 风格 | 效果感受 |
|---|---|
| 匀速,机械感强 |
| 平缓加速/减速 |
| 更强的缓动效果 |
| 缓动效果更明显 |
| 平滑,自然感强 |
| 先超出目标再回弹 |
| 结束时弹跳效果 |
| 弹性回弹效果 |
| 急剧加速 |
Tweening Part Properties
部件属性补间
luau
-- Flash on hit: turn white then revert
local function flashPart(part: BasePart, originalColor: Color3)
part.Color = Color3.new(1, 1, 1) -- instant white
local tweenBack = TweenService:Create(part, TweenInfo.new(0.3, Enum.EasingStyle.Quad), {
Color = originalColor,
})
tweenBack:Play()
endluau
-- Pulse effect: scale up then back
local function pulse(part: BasePart)
local originalSize = part.Size
local tweenGrow = TweenService:Create(part, TweenInfo.new(0.15, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {
Size = originalSize * 1.3,
})
local tweenShrink = TweenService:Create(part, TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.In), {
Size = originalSize,
})
tweenGrow:Play()
tweenGrow.Completed:Connect(function()
tweenShrink:Play()
end)
endluau
-- Grow and fade out (explosion ring)
local function expandAndFade(part: BasePart)
local tween = TweenService:Create(part, TweenInfo.new(0.6, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {
Size = part.Size * 5,
Transparency = 1,
})
tween:Play()
tween.Completed:Connect(function()
part:Destroy()
end)
endluau
-- Color transition (damage indicator)
local function colorTransition(part: BasePart, targetColor: Color3, duration: number)
local tween = TweenService:Create(part, TweenInfo.new(duration, Enum.EasingStyle.Sine), {
Color = targetColor,
})
tween:Play()
endluau
-- 击中闪光:瞬间变白然后恢复原颜色
local function flashPart(part: BasePart, originalColor: Color3)
part.Color = Color3.new(1, 1, 1) -- 瞬间变白
local tweenBack = TweenService:Create(part, TweenInfo.new(0.3, Enum.EasingStyle.Quad), {
Color = originalColor,
})
tweenBack:Play()
endluau
-- 脉冲效果:放大然后缩小
local function pulse(part: BasePart)
local originalSize = part.Size
local tweenGrow = TweenService:Create(part, TweenInfo.new(0.15, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {
Size = originalSize * 1.3,
})
local tweenShrink = TweenService:Create(part, TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.In), {
Size = originalSize,
})
tweenGrow:Play()
tweenGrow.Completed:Connect(function()
tweenShrink:Play()
end)
endluau
-- 扩大并淡出(爆炸环)
local function expandAndFade(part: BasePart)
local tween = TweenService:Create(part, TweenInfo.new(0.6, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {
Size = part.Size * 5,
Transparency = 1,
})
tween:Play()
tween.Completed:Connect(function()
part:Destroy()
end)
endluau
-- 颜色过渡(伤害指示器)
local function colorTransition(part: BasePart, targetColor: Color3, duration: number)
local tween = TweenService:Create(part, TweenInfo.new(duration, Enum.EasingStyle.Sine), {
Color = targetColor,
})
tween:Play()
endChaining Tweens
链式补间
Use the event to sequence tweens without coroutines:
Completedluau
local function chainedEffect(part: BasePart)
local step1 = TweenService:Create(part, TweenInfo.new(0.2), { Transparency = 0.5 })
local step2 = TweenService:Create(part, TweenInfo.new(0.3), { Size = part.Size * 2 })
local step3 = TweenService:Create(part, TweenInfo.new(0.2), { Transparency = 1 })
step1:Play()
step1.Completed:Connect(function()
step2:Play()
end)
step2.Completed:Connect(function()
step3:Play()
end)
step3.Completed:Connect(function()
part:Destroy()
end)
end使用事件序列补间,无需协程:
Completedluau
local function chainedEffect(part: BasePart)
local step1 = TweenService:Create(part, TweenInfo.new(0.2), { Transparency = 0.5 })
local step2 = TweenService:Create(part, TweenInfo.new(0.3), { Size = part.Size * 2 })
local step3 = TweenService:Create(part, TweenInfo.new(0.2), { Transparency = 1 })
step1:Play()
step1.Completed:Connect(function()
step2:Play()
end)
step2.Completed:Connect(function()
step3:Play()
end)
step3.Completed:Connect(function()
part:Destroy()
end)
endLighting Effects
光照效果
Dynamic Lights
动态光源
luau
-- PointLight: omnidirectional, good for torches, explosions
local pointLight = Instance.new("PointLight")
pointLight.Brightness = 2
pointLight.Color = Color3.fromRGB(255, 180, 50)
pointLight.Range = 20
pointLight.Shadows = true
pointLight.Parent = torchPart
-- SpotLight: directional cone, good for flashlights, spotlights
local spotLight = Instance.new("SpotLight")
spotLight.Brightness = 3
spotLight.Color = Color3.new(1, 1, 1)
spotLight.Range = 40
spotLight.Angle = 30 -- cone half-angle in degrees
spotLight.Face = Enum.NormalId.Front
spotLight.Parent = flashlightPart
-- SurfaceLight: emits from a surface, good for screens, signs
local surfLight = Instance.new("SurfaceLight")
surfLight.Brightness = 1
surfLight.Color = Color3.fromRGB(0, 200, 255)
surfLight.Range = 10
surfLight.Face = Enum.NormalId.Front
surfLight.Parent = screenPartluau
-- PointLight:全向光源,适合火把、爆炸效果
local pointLight = Instance.new("PointLight")
pointLight.Brightness = 2
pointLight.Color = Color3.fromRGB(255, 180, 50)
pointLight.Range = 20
pointLight.Shadows = true
pointLight.Parent = torchPart
-- SpotLight:定向锥形光源,适合手电筒、聚光灯
local spotLight = Instance.new("SpotLight")
spotLight.Brightness = 3
spotLight.Color = Color3.new(1, 1, 1)
spotLight.Range = 40
spotLight.Angle = 30 -- 锥形半角(度)
spotLight.Face = Enum.NormalId.Front
spotLight.Parent = flashlightPart
-- SurfaceLight:从表面发光,适合屏幕、标牌
local surfLight = Instance.new("SurfaceLight")
surfLight.Brightness = 1
surfLight.Color = Color3.fromRGB(0, 200, 255)
surfLight.Range = 10
surfLight.Face = Enum.NormalId.Front
surfLight.Parent = screenPartPost-Processing Effects
后期处理效果
All post-processing objects go in or .
LightingCamera所有后期处理对象需放在或中。
LightingCameraAtmosphere
大气效果
luau
local atmo = Instance.new("Atmosphere")
atmo.Density = 0.3 -- 0-1, how thick the atmosphere is
atmo.Offset = 0.25 -- shifts haze up/down
atmo.Color = Color3.fromRGB(200, 210, 230) -- scatter color
atmo.Decay = Color3.fromRGB(120, 140, 170) -- far-away color
atmo.Glare = 0.2 -- sun glare intensity
atmo.Haze = 1.5 -- haze amount
atmo.Parent = game.Lightingluau
local atmo = Instance.new("Atmosphere")
atmo.Density = 0.3 -- 0-1,大气厚度
atmo.Offset = 0.25 -- 上下移动雾的位置
atmo.Color = Color3.fromRGB(200, 210, 230) -- 散射颜色
atmo.Decay = Color3.fromRGB(120, 140, 170) -- 远处颜色
atmo.Glare = 0.2 -- 太阳眩光强度
atmo.Haze = 1.5 -- 雾量
atmo.Parent = game.LightingColorCorrection
颜色校正
luau
local cc = Instance.new("ColorCorrectionEffect")
cc.Brightness = 0.05 -- -1 to 1
cc.Contrast = 0.1 -- -1 to 1
cc.Saturation = 0.15 -- -1 to 1
cc.TintColor = Color3.new(1, 0.95, 0.9) -- warm tint
cc.Parent = game.Lightingluau
local cc = Instance.new("ColorCorrectionEffect")
cc.Brightness = 0.05 -- -1到1
cc.Contrast = 0.1 -- -1到1
cc.Saturation = 0.15 -- -1到1
cc.TintColor = Color3.new(1, 0.95, 0.9) -- 暖色调
cc.Parent = game.LightingBloom
光晕
luau
local bloom = Instance.new("BloomEffect")
bloom.Intensity = 0.8 -- glow strength
bloom.Size = 24 -- glow spread (pixels)
bloom.Threshold = 1.2 -- brightness threshold to bloom
bloom.Parent = game.Lightingluau
local bloom = Instance.new("BloomEffect")
bloom.Intensity = 0.8 -- 发光强度
bloom.Size = 24 -- 发光扩散范围(像素)
bloom.Threshold = 1.2 -- 触发发光的亮度阈值
bloom.Parent = game.LightingDepthOfField
景深
luau
local dof = Instance.new("DepthOfFieldEffect")
dof.FarIntensity = 0.3 -- blur intensity far from focus
dof.FocusDistance = 30 -- distance in studs to focus point
dof.InFocusRadius = 20 -- radius of sharp area
dof.NearIntensity = 0.2 -- blur intensity near camera
dof.Parent = game.Lightingluau
local dof = Instance.new("DepthOfFieldEffect")
dof.FarIntensity = 0.3 -- 远离焦点区域的模糊强度
dof.FocusDistance = 30 -- 焦点距离(studs)
dof.InFocusRadius = 20 -- 清晰区域半径
dof.NearIntensity = 0.2 -- 靠近相机区域的模糊强度
dof.Parent = game.LightingSunRays
太阳射线
luau
local sunRays = Instance.new("SunRaysEffect")
sunRays.Intensity = 0.15 -- ray visibility
sunRays.Spread = 0.8 -- how far rays extend
sunRays.Parent = game.Lightingluau
local sunRays = Instance.new("SunRaysEffect")
sunRays.Intensity = 0.15 -- 射线可见度
sunRays.Spread = 0.8 -- 射线延伸距离
sunRays.Parent = game.LightingSetting Mood with Global Lighting
全局光照营造氛围
luau
local lighting = game.Lighting
-- Daytime bright
lighting.ClockTime = 14
lighting.Brightness = 2
lighting.Ambient = Color3.fromRGB(140, 140, 140)
lighting.OutdoorAmbient = Color3.fromRGB(130, 130, 130)
-- Nighttime spooky
lighting.ClockTime = 0
lighting.Brightness = 0.5
lighting.Ambient = Color3.fromRGB(20, 20, 40)
lighting.OutdoorAmbient = Color3.fromRGB(10, 10, 30)
lighting.FogEnd = 200
lighting.FogColor = Color3.fromRGB(15, 15, 30)luau
local lighting = game.Lighting
-- 白天明亮效果
lighting.ClockTime = 14
lighting.Brightness = 2
lighting.Ambient = Color3.fromRGB(140, 140, 140)
lighting.OutdoorAmbient = Color3.fromRGB(130, 130, 130)
-- 夜晚恐怖效果
lighting.ClockTime = 0
lighting.Brightness = 0.5
lighting.Ambient = Color3.fromRGB(20, 20, 40)
lighting.OutdoorAmbient = Color3.fromRGB(10, 10, 30)
lighting.FogEnd = 200
lighting.FogColor = Color3.fromRGB(15, 15, 30)Sound + Animation Sync
音效与动画同步
For general sound (SoundService, positional audio, SoundGroups), use mcp-roblox-docs. This section covers only the animation-specific pattern.
通用音效(SoundService、位置音频、SoundGroups)请参考mcp-roblox-docs。本节仅介绍动画相关的同步模式。
Triggering Sounds with Animation Events
用动画事件触发音效
luau
-- Sync a footstep sound to walk animation markers
walkTrack:GetMarkerReachedSignal("Footstep"):Connect(function()
local footstep = Instance.new("Sound")
footstep.SoundId = "rbxassetid://112233445"
footstep.Volume = 0.5
footstep.PlaybackSpeed = 0.9 + math.random() * 0.2 -- slight variation
footstep.Parent = character.HumanoidRootPart
footstep:Play()
footstep.Ended:Connect(function()
footstep:Destroy()
end)
end)luau
-- 将脚步声与行走动画标记同步
walkTrack:GetMarkerReachedSignal("Footstep"):Connect(function()
local footstep = Instance.new("Sound")
footstep.SoundId = "rbxassetid://112233445"
footstep.Volume = 0.5
footstep.PlaybackSpeed = 0.9 + math.random() * 0.2 -- 轻微随机变化
footstep.Parent = character.HumanoidRootPart
footstep:Play()
footstep.Ended:Connect(function()
footstep:Destroy()
end)
end)Camera Effects
相机效果
Camera Shake
相机震动
luau
local camera = workspace.CurrentCamera
local RunService = game:GetService("RunService")
local function shakeCamera(intensity: number, duration: number)
local elapsed = 0
local originalCFrame = camera.CFrame
local connection: RBXScriptConnection
connection = RunService.RenderStepped:Connect(function(dt: number)
elapsed += dt
if elapsed >= duration then
connection:Disconnect()
-- Camera returns to normal since CameraType is usually
-- "Custom" (player-controlled)
return
end
local progress = 1 - (elapsed / duration) -- decay over time
local shakeX = (math.random() - 0.5) * 2 * intensity * progress
local shakeY = (math.random() - 0.5) * 2 * intensity * progress
local shakeZ = (math.random() - 0.5) * 2 * intensity * progress
camera.CFrame = camera.CFrame * CFrame.new(shakeX, shakeY, shakeZ)
end)
end
-- Usage: moderate shake for 0.3 seconds
shakeCamera(0.5, 0.3)luau
local camera = workspace.CurrentCamera
local RunService = game:GetService("RunService")
local function shakeCamera(intensity: number, duration: number)
local elapsed = 0
local originalCFrame = camera.CFrame
local connection: RBXScriptConnection
connection = RunService.RenderStepped:Connect(function(dt: number)
elapsed += dt
if elapsed >= duration then
connection:Disconnect()
-- 由于CameraType通常为"Custom"(玩家控制),相机会自动恢复正常
return
end
local progress = 1 - (elapsed / duration) -- 随时间衰减
local shakeX = (math.random() - 0.5) * 2 * intensity * progress
local shakeY = (math.random() - 0.5) * 2 * intensity * progress
local shakeZ = (math.random() - 0.5) * 2 * intensity * progress
camera.CFrame = camera.CFrame * CFrame.new(shakeX, shakeY, shakeZ)
end)
end
-- 使用:中等强度震动0.3秒
shakeCamera(0.5, 0.3)Zoom Effect
缩放效果
luau
local TweenService = game:GetService("TweenService")
local camera = workspace.CurrentCamera
local function zoomCamera(targetFOV: number, duration: number)
local tween = TweenService:Create(camera, TweenInfo.new(duration, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {
FieldOfView = targetFOV,
})
tween:Play()
return tween
end
-- Zoom in for dramatic moment
local zoomIn = zoomCamera(40, 0.5)
zoomIn.Completed:Connect(function()
task.wait(1)
zoomCamera(70, 0.8) -- zoom back to normal
end)luau
local TweenService = game:GetService("TweenService")
local camera = workspace.CurrentCamera
local function zoomCamera(targetFOV: number, duration: number)
local tween = TweenService:Create(camera, TweenInfo.new(duration, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {
FieldOfView = targetFOV,
})
tween:Play()
return tween
end
-- 放大营造戏剧性时刻
local zoomIn = zoomCamera(40, 0.5)
zoomIn.Completed:Connect(function()
task.wait(1)
zoomCamera(70, 0.8) -- 恢复正常缩放
end)Focus / CFrame Lerp
聚焦 / CFrame插值
luau
local function focusOnPoint(targetCFrame: CFrame, duration: number)
camera.CameraType = Enum.CameraType.Scriptable
local startCFrame = camera.CFrame
local elapsed = 0
local connection: RBXScriptConnection
connection = RunService.RenderStepped:Connect(function(dt: number)
elapsed += dt
local alpha = math.clamp(elapsed / duration, 0, 1)
-- Smooth step for natural feel
local smoothAlpha = alpha * alpha * (3 - 2 * alpha)
camera.CFrame = startCFrame:Lerp(targetCFrame, smoothAlpha)
if alpha >= 1 then
connection:Disconnect()
end
end)
endluau
local function focusOnPoint(targetCFrame: CFrame, duration: number)
camera.CameraType = Enum.CameraType.Scriptable
local startCFrame = camera.CFrame
local elapsed = 0
local connection: RBXScriptConnection
connection = RunService.RenderStepped:Connect(function(dt: number)
elapsed += dt
local alpha = math.clamp(elapsed / duration, 0, 1)
-- 平滑步进实现自然效果
local smoothAlpha = alpha * alpha * (3 - 2 * alpha)
camera.CFrame = startCFrame:Lerp(targetCFrame, smoothAlpha)
if alpha >= 1 then
connection:Disconnect()
end
end)
endCutscene Waypoint System
过场动画路径点系统
luau
type CutsceneWaypoint = {
cframe: CFrame,
duration: number,
easingStyle: Enum.EasingStyle?,
holdTime: number?, -- pause at this point before moving on
}
local function playCutscene(waypoints: { CutsceneWaypoint })
camera.CameraType = Enum.CameraType.Scriptable
for i, waypoint in waypoints do
local style = waypoint.easingStyle or Enum.EasingStyle.Quad
local info = TweenInfo.new(waypoint.duration, style, Enum.EasingDirection.InOut)
local tween = TweenService:Create(camera, info, {
CFrame = waypoint.cframe,
})
tween:Play()
tween.Completed:Wait()
if waypoint.holdTime and waypoint.holdTime > 0 then
task.wait(waypoint.holdTime)
end
end
-- Return camera control to player
camera.CameraType = Enum.CameraType.Custom
end
-- Usage
playCutscene({
{ cframe = CFrame.new(0, 50, 0) * CFrame.Angles(math.rad(-90), 0, 0), duration = 2, holdTime = 1 },
{ cframe = CFrame.new(20, 10, 20) * CFrame.lookAt(Vector3.new(20, 10, 20), Vector3.zero), duration = 3 },
{ cframe = camera.CFrame, duration = 1.5, easingStyle = Enum.EasingStyle.Sine },
})luau
type CutsceneWaypoint = {
cframe: CFrame,
duration: number,
easingStyle: Enum.EasingStyle?,
holdTime: number?, -- 在该点暂停后再继续
}
local function playCutscene(waypoints: { CutsceneWaypoint })
camera.CameraType = Enum.CameraType.Scriptable
for i, waypoint in waypoints do
local style = waypoint.easingStyle or Enum.EasingStyle.Quad
local info = TweenInfo.new(waypoint.duration, style, Enum.EasingDirection.InOut)
local tween = TweenService:Create(camera, info, {
CFrame = waypoint.cframe,
})
tween:Play()
tween.Completed:Wait()
if waypoint.holdTime and waypoint.holdTime > 0 then
task.wait(waypoint.holdTime)
end
end
-- 将相机控制权交还给玩家
camera.CameraType = Enum.CameraType.Custom
end
-- 使用示例
playCutscene({
{ cframe = CFrame.new(0, 50, 0) * CFrame.Angles(math.rad(-90), 0, 0), duration = 2, holdTime = 1 },
{ cframe = CFrame.new(20, 10, 20) * CFrame.lookAt(Vector3.new(20, 10, 20), Vector3.zero), duration = 3 },
{ cframe = camera.CFrame, duration = 1.5, easingStyle = Enum.EasingStyle.Sine },
})Best Practices
最佳实践
Performance Budgets
性能预算
- Max ~200 particles per emitter -- more than this and you risk frame drops, especially on mobile.
- Limit total active emitters -- aim for fewer than 20-30 active emitters visible at once in any scene.
- Particle texture size -- keep textures small (64x64 or 128x128 PNG). Avoid large or high-res particle textures.
- Beams -- keep reasonable (10-30). Very high segment counts cost draw calls.
Segments - Tweens -- hundreds of simultaneous tweens are fine; thousands may cause issues. Cancel/destroy tweens when no longer needed.
- Sounds -- limit simultaneous playing sounds to ~20-30. Destroy one-shot sounds after .
Ended
- 每个发射器最多约200个粒子 -- 超过这个数量可能导致掉帧,尤其是在移动设备上。
- 限制活跃发射器总数 -- 任何场景中同时可见的活跃发射器应控制在20-30个以内。
- 粒子纹理尺寸 -- 保持纹理较小(64x64或128x128 PNG)。避免使用大尺寸或高分辨率粒子纹理。
- 光束 -- 保持合理(10-30)。过高的分段数会增加绘制调用。
Segments - 补间 -- 数百个同时运行的补间没问题;数千个可能会出问题。不再需要时取消/销毁补间。
- 音效 -- 限制同时播放的音效数量在20-30个以内。单次播放的音效在后销毁。
Ended
Disable Effects on Low-End Devices
低端设备禁用效果
luau
local function getQualityLevel(): string
local quality = UserSettings().GameSettings.SavedQualityLevel
-- quality is Enum.SavedQualitySetting or an int 1-10
if quality == Enum.SavedQualitySetting.Automatic then
-- Use a heuristic: check current graphics quality
local level = settings().Rendering.QualityLevel
if level <= 3 then return "low"
elseif level <= 6 then return "medium"
else return "high" end
end
local numQuality = quality.Value
if numQuality <= 3 then return "low"
elseif numQuality <= 6 then return "medium"
else return "high" end
end
local function applyQualitySettings()
local level = getQualityLevel()
if level == "low" then
-- Disable post-processing
for _, effect in game.Lighting:GetChildren() do
if effect:IsA("PostEffect") then
effect.Enabled = false
end
end
-- Reduce particle rates
-- Disable shadows on dynamic lights
end
endluau
local function getQualityLevel(): string
local quality = UserSettings().GameSettings.SavedQualityLevel
-- quality是Enum.SavedQualitySetting或1-10的整数
if quality == Enum.SavedQualitySetting.Automatic then
-- 使用启发式方法:检查当前图形质量
local level = settings().Rendering.QualityLevel
if level <= 3 then return "low"
elseif level <= 6 then return "medium"
else return "high" end
end
local numQuality = quality.Value
if numQuality <= 3 then return "low"
elseif numQuality <= 6 then return "medium"
else return "high" end
end
local function applyQualitySettings()
local level = getQualityLevel()
if level == "low" then
-- 禁用后期处理
for _, effect in game.Lighting:GetChildren() do
if effect:IsA("PostEffect") then
effect.Enabled = false
end
end
-- 降低粒子发射率
-- 禁用动态光源的阴影
end
endPool VFX Objects
对象池化VFX
Avoid creating and destroying particles every frame. Pre-create a pool and enable/disable or reposition them.
luau
local VFXPool = {}
VFXPool.__index = VFXPool
function VFXPool.new(template: Instance, poolSize: number): typeof(setmetatable({}, VFXPool))
local self = setmetatable({}, VFXPool)
self._pool = table.create(poolSize)
self._available = table.create(poolSize)
for i = 1, poolSize do
local clone = template:Clone()
clone.Parent = workspace.VFXFolder
-- Disable all emitters
for _, emitter in clone:GetDescendants() do
if emitter:IsA("ParticleEmitter") then
emitter.Enabled = false
end
end
self._pool[i] = clone
self._available[i] = clone
end
return self
end
function VFXPool:get(): Instance?
local obj = table.remove(self._available)
return obj
end
function VFXPool:release(obj: Instance)
-- Reset position, disable emitters
for _, emitter in obj:GetDescendants() do
if emitter:IsA("ParticleEmitter") then
emitter.Enabled = false
end
end
table.insert(self._available, obj)
end避免每帧创建和销毁粒子。预先创建对象池,通过启用/禁用或重新定位来复用。
luau
local VFXPool = {}
VFXPool.__index = VFXPool
function VFXPool.new(template: Instance, poolSize: number): typeof(setmetatable({}, VFXPool))
local self = setmetatable({}, VFXPool)
self._pool = table.create(poolSize)
self._available = table.create(poolSize)
for i = 1, poolSize do
local clone = template:Clone()
clone.Parent = workspace.VFXFolder
-- 禁用所有发射器
for _, emitter in clone:GetDescendants() do
if emitter:IsA("ParticleEmitter") then
emitter.Enabled = false
end
end
self._pool[i] = clone
self._available[i] = clone
end
return self
end
function VFXPool:get(): Instance?
local obj = table.remove(self._available)
return obj
end
function VFXPool:release(obj: Instance)
-- 重置位置,禁用发射器
for _, emitter in obj:GetDescendants() do
if emitter:IsA("ParticleEmitter") then
emitter.Enabled = false
end
end
table.insert(self._available, obj)
endSync Sound with Visuals
音效与视觉同步
- Use to trigger sounds at exact animation frames.
MarkerReachedSignal - Play impact sounds at the moment of collision, not when the swing starts.
- Match to animation speed adjustments.
PlaybackSpeed - Use or tween
task.delayevents for sequenced audio.Completed
- 使用在精确的动画帧触发音效。
MarkerReachedSignal - 在碰撞瞬间播放冲击音效,而非挥击开始时。
- 调整以匹配动画速度变化。
PlaybackSpeed - 使用或补间
task.delay事件实现序列音频。Completed
Anti-Patterns
反模式
Unlimited Particles
无限粒子
luau
-- BAD: unbounded particle creation with no cleanup
RunService.Heartbeat:Connect(function()
local emitter = Instance.new("ParticleEmitter")
emitter.Rate = 500 -- extremely high rate
emitter.Parent = somePart
-- never destroyed, accumulates forever
end)
-- GOOD: reuse a single emitter, burst when needed
local emitter = Instance.new("ParticleEmitter")
emitter.Rate = 0 -- manual emission only
emitter.Parent = somePart
local function burstParticles(count: number)
emitter:Emit(count)
endluau
-- 错误:无限制创建粒子且不清理
RunService.Heartbeat:Connect(function()
local emitter = Instance.new("ParticleEmitter")
emitter.Rate = 500 -- 极高的发射率
emitter.Parent = somePart
-- 从未销毁,持续累积
end)
-- 正确:复用单个发射器,按需爆发
local emitter = Instance.new("ParticleEmitter")
emitter.Rate = 0 -- 仅手动发射
emitter.Parent = somePart
local function burstParticles(count: number)
emitter:Emit(count)
endUnoptimized Particle Textures
未优化的粒子纹理
luau
-- BAD: 1024x1024 high-res texture for tiny particles
emitter.Texture = "rbxassetid://huge_4k_texture"
-- GOOD: 64x64 or 128x128 simple shape on transparent background
emitter.Texture = "rbxassetid://small_optimized_circle"
-- Use LightEmission = 1 with simple shapes for clean glow effectsluau
-- 错误:用1024x1024高分辨率纹理做微小粒子
emitter.Texture = "rbxassetid://huge_4k_texture"
-- 正确:用64x64或128x128透明背景的简单形状
emitter.Texture = "rbxassetid://small_optimized_circle"
-- 设置LightEmission = 1,配合简单形状实现干净的发光效果Synchronous Animation Loading Blocking Gameplay
同步加载动画阻塞游戏运行
luau
-- BAD: loading animations in a hot path synchronously
local function onAttack()
local anim = Instance.new("Animation")
anim.AnimationId = "rbxassetid://123456789"
local track = animator:LoadAnimation(anim) -- may yield on first load
track:Play()
end
-- GOOD: preload animations at character spawn
local attackAnim = Instance.new("Animation")
attackAnim.AnimationId = "rbxassetid://123456789"
local attackTrack: AnimationTrack -- forward declare
local function onCharacterAdded(character: Model)
local animator = character:WaitForChild("Humanoid"):WaitForChild("Animator")
attackTrack = animator:LoadAnimation(attackAnim)
attackTrack.Priority = Enum.AnimationPriority.Action
end
local function onAttack()
if attackTrack then
attackTrack:Play()
end
endluau
-- 错误:在热点路径同步加载动画
local function onAttack()
local anim = Instance.new("Animation")
anim.AnimationId = "rbxassetid://123456789"
local track = animator:LoadAnimation(anim) -- 首次加载可能阻塞
track:Play()
end
-- 正确:在角色生成时预加载动画
local attackAnim = Instance.new("Animation")
attackAnim.AnimationId = "rbxassetid://123456789"
local attackTrack: AnimationTrack -- 提前声明
local function onCharacterAdded(character: Model)
local animator = character:WaitForChild("Humanoid"):WaitForChild("Animator")
attackTrack = animator:LoadAnimation(attackAnim)
attackTrack.Priority = Enum.AnimationPriority.Action
end
local function onAttack()
if attackTrack then
attackTrack:Play()
end
endOther Anti-Patterns to Avoid
其他需避免的反模式
- Tweening properties every frame via RenderStepped instead of using TweenService -- TweenService is optimized internally and handles cleanup.
- Not disconnecting camera shake connections -- leads to permanent jitter.
- Setting to
Camera.CameraTypeand forgetting to restore it -- player loses control.Scriptable - Not cleaning up Highlight after its Adornee is destroyed -- orphaned Highlights waste one of the 255 slots. Use to auto-destroy.
AncestryChanged - 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.
- Playing sounds without ever destroying them -- memory leak. Always clean up one-shot sounds via .
Ended - Creating hundreds of PointLights with shadows enabled -- massive performance hit. Use for most dynamic lights.
Shadows = false
- 通过RenderStepped每帧补间属性而非使用TweenService -- TweenService内部已优化,且处理清理逻辑。
- 未断开相机震动连接 -- 导致永久抖动。
- 将设为
Camera.CameraType后忘记恢复 -- 玩家失去控制权。Scriptable - Adornee销毁后未清理Highlight -- 孤立的Highlight会占用255个名额之一。使用自动销毁。
AncestryChanged - 部件销毁时效果仍在运行 -- 粒子爆发、轨迹和光束会瞬间消失。如需优雅清理,将效果重新挂载到临时容器 + 使用Debris。
- 播放音效后从未销毁 -- 内存泄漏。单次播放的音效务必通过事件清理。
Ended - 创建数百个启用阴影的PointLight -- 性能大幅下降。大多数动态光源应设置。
Shadows = false
Complete Hit Effect System
完整击中效果系统
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).
luau
--[[
HitEffectSystem
Combines visual and audio feedback for combat hit registration.
Run on the CLIENT only (camera shake and local VFX).
]]
local TweenService = game:GetService("TweenService")
local RunService = game:GetService("RunService")
local HitEffectSystem = {}
-- Configuration
local DEFAULT_CONFIG = {
-- Flash
flashColor = Color3.new(1, 1, 1),
flashDuration = 0.15,
flashRevertDuration = 0.25,
-- Particles
particleBurstCount = 20,
particleLifetime = NumberRange.new(0.2, 0.5),
particleSpeed = NumberRange.new(8, 15),
particleSize = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0.5),
NumberSequenceKeypoint.new(1, 0),
}),
particleColor = ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.new(1, 1, 1)),
ColorSequenceKeypoint.new(1, Color3.fromRGB(255, 200, 50)),
}),
particleTransparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0),
NumberSequenceKeypoint.new(0.7, 0.3),
NumberSequenceKeypoint.new(1, 1),
}),
-- Sound
hitSoundId = "rbxassetid://123456789", -- replace with your asset
hitSoundVolume = 0.8,
hitSoundPitchVariation = 0.15, -- random pitch +/- this amount
-- Camera shake
shakeIntensity = 0.4,
shakeDuration = 0.2,
}
-- Pre-create a reusable particle emitter template
local function createHitEmitterTemplate(config: typeof(DEFAULT_CONFIG)): ParticleEmitter
local emitter = Instance.new("ParticleEmitter")
emitter.Name = "HitBurst"
emitter.Rate = 0 -- manual emission only
emitter.Lifetime = config.particleLifetime
emitter.Speed = config.particleSpeed
emitter.SpreadAngle = Vector2.new(180, 180) -- omnidirectional burst
emitter.Size = config.particleSize
emitter.Color = config.particleColor
emitter.Transparency = config.particleTransparency
emitter.LightEmission = 1
emitter.Drag = 5
emitter.RotSpeed = NumberRange.new(-180, 180)
return emitter
end
-- Emitter cache: one emitter per part (lazily created)
local emitterCache: { [BasePart]: ParticleEmitter } = {}
local emitterTemplate: ParticleEmitter? = nil
local function getOrCreateEmitter(part: BasePart, config: typeof(DEFAULT_CONFIG)): ParticleEmitter
if emitterCache[part] then
return emitterCache[part]
end
if not emitterTemplate then
emitterTemplate = createHitEmitterTemplate(config)
end
local emitter = emitterTemplate:Clone()
emitter.Parent = part
emitterCache[part] = emitter
-- Clean up if part is destroyed
part.Destroying:Connect(function()
emitterCache[part] = nil
end)
return emitter
end
--[[
Flash the target part white and tween back to original color.
]]
local function flashPart(part: BasePart, config: typeof(DEFAULT_CONFIG))
local originalColor = part.Color
part.Color = config.flashColor
local tweenBack = TweenService:Create(
part,
TweenInfo.new(config.flashRevertDuration, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
{ Color = originalColor }
)
tweenBack:Play()
end
--[[
Emit a burst of particles from the hit location.
]]
local function emitParticleBurst(part: BasePart, config: typeof(DEFAULT_CONFIG))
local emitter = getOrCreateEmitter(part, config)
emitter:Emit(config.particleBurstCount)
end
--[[
Play a one-shot hit sound with slight pitch variation.
]]
local function playHitSound(part: BasePart, config: typeof(DEFAULT_CONFIG))
local sound = Instance.new("Sound")
sound.SoundId = config.hitSoundId
sound.Volume = config.hitSoundVolume
sound.PlaybackSpeed = 1 + (math.random() - 0.5) * 2 * config.hitSoundPitchVariation
sound.RollOffMaxDistance = 80
sound.RollOffMinDistance = 5
sound.Parent = part
sound:Play()
sound.Ended:Connect(function()
sound:Destroy()
end)
end
--[[
Apply a short screen shake that decays over time.
]]
local function shakeCamera(config: typeof(DEFAULT_CONFIG))
local camera = workspace.CurrentCamera
if not camera then return end
local elapsed = 0
local intensity = config.shakeIntensity
local duration = config.shakeDuration
local connection: RBXScriptConnection
connection = RunService.RenderStepped:Connect(function(dt: number)
elapsed += dt
if elapsed >= duration then
connection:Disconnect()
return
end
local decay = 1 - (elapsed / duration)
local offsetX = (math.random() - 0.5) * 2 * intensity * decay
local offsetY = (math.random() - 0.5) * 2 * intensity * decay
camera.CFrame = camera.CFrame * CFrame.new(offsetX, offsetY, 0)
end)
end
--[[
Main entry point: trigger the full hit effect on a target part.
@param targetPart The BasePart that was hit (e.g., a character limb or NPC body).
@param overrides Optional table to override any DEFAULT_CONFIG values.
]]
function HitEffectSystem.play(targetPart: BasePart, overrides: { [string]: any }?)
-- Merge config with overrides
local config = table.clone(DEFAULT_CONFIG)
if overrides then
for key, value in overrides do
(config :: any)[key] = value
end
end
-- Fire all effects simultaneously
flashPart(targetPart, config)
emitParticleBurst(targetPart, config)
playHitSound(targetPart, config)
shakeCamera(config)
end
--[[
Clean up cached emitters (call when resetting scene or on player leave).
]]
function HitEffectSystem.cleanup()
for part, emitter in emitterCache do
emitter:Destroy()
end
table.clear(emitterCache)
end
return HitEffectSystem
--[[
USAGE EXAMPLE (in a LocalScript):
local HitEffectSystem = require(script.Parent.HitEffectSystem)
-- When a hit is confirmed (e.g., via RemoteEvent from server):
hitRemote.OnClientEvent:Connect(function(targetPart: BasePart)
HitEffectSystem.play(targetPart)
end)
-- Custom overrides for a critical hit:
hitRemote.OnClientEvent:Connect(function(targetPart: BasePart, isCritical: boolean)
if isCritical then
HitEffectSystem.play(targetPart, {
flashColor = Color3.fromRGB(255, 50, 50),
particleBurstCount = 40,
shakeIntensity = 0.8,
shakeDuration = 0.4,
hitSoundId = "rbxassetid://critical_hit_sound_id",
})
else
HitEffectSystem.play(targetPart)
end
end)
]]一个结合白色闪光、粒子爆发、音效提示和相机震动的生产级系统。设计用于客户端(需在LocalScript或从LocalScript调用的模块中运行)。
luau
--[[
HitEffectSystem
结合视觉和音频反馈实现战斗击中判定。
仅在客户端运行(相机震动和本地VFX)。
]]
local TweenService = game:GetService("TweenService")
local RunService = game:GetService("RunService")
local HitEffectSystem = {}
-- 配置
local DEFAULT_CONFIG = {
-- 闪光
flashColor = Color3.new(1, 1, 1),
flashDuration = 0.15,
flashRevertDuration = 0.25,
-- 粒子
particleBurstCount = 20,
particleLifetime = NumberRange.new(0.2, 0.5),
particleSpeed = NumberRange.new(8, 15),
particleSize = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0.5),
NumberSequenceKeypoint.new(1, 0),
}),
particleColor = ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.new(1, 1, 1)),
ColorSequenceKeypoint.new(1, Color3.fromRGB(255, 200, 50)),
}),
particleTransparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0),
NumberSequenceKeypoint.new(0.7, 0.3),
NumberSequenceKeypoint.new(1, 1),
}),
-- 音效
hitSoundId = "rbxassetid://123456789", -- 替换为你的资源ID
hitSoundVolume = 0.8,
hitSoundPitchVariation = 0.15, -- 随机音高变化范围 +/- 该值
-- 相机震动
shakeIntensity = 0.4,
shakeDuration = 0.2,
}
-- 预创建可复用的粒子发射器模板
local function createHitEmitterTemplate(config: typeof(DEFAULT_CONFIG)): ParticleEmitter
local emitter = Instance.new("ParticleEmitter")
emitter.Name = "HitBurst"
emitter.Rate = 0 -- 仅手动发射
emitter.Lifetime = config.particleLifetime
emitter.Speed = config.particleSpeed
emitter.SpreadAngle = Vector2.new(180, 180) -- 全向爆发
emitter.Size = config.particleSize
emitter.Color = config.particleColor
emitter.Transparency = config.particleTransparency
emitter.LightEmission = 1
emitter.Drag = 5
emitter.RotSpeed = NumberRange.new(-180, 180)
return emitter
end
-- 发射器缓存:每个部件对应一个发射器(懒加载)
local emitterCache: { [BasePart]: ParticleEmitter } = {}
local emitterTemplate: ParticleEmitter? = nil
local function getOrCreateEmitter(part: BasePart, config: typeof(DEFAULT_CONFIG)): ParticleEmitter
if emitterCache[part] then
return emitterCache[part]
end
if not emitterTemplate then
emitterTemplate = createHitEmitterTemplate(config)
end
local emitter = emitterTemplate:Clone()
emitter.Parent = part
emitterCache[part] = emitter
-- 部件销毁时清理缓存
part.Destroying:Connect(function()
emitterCache[part] = nil
end)
return emitter
end
--[[
将目标部件闪白并补间恢复原颜色。
]]
local function flashPart(part: BasePart, config: typeof(DEFAULT_CONFIG))
local originalColor = part.Color
part.Color = config.flashColor
local tweenBack = TweenService:Create(
part,
TweenInfo.new(config.flashRevertDuration, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
{ Color = originalColor }
)
tweenBack:Play()
end
--[[
在击中位置发射粒子爆发。
]]
local function emitParticleBurst(part: BasePart, config: typeof(DEFAULT_CONFIG))
local emitter = getOrCreateEmitter(part, config)
emitter:Emit(config.particleBurstCount)
end
--[[
播放带轻微音高变化的单次击中音效。
]]
local function playHitSound(part: BasePart, config: typeof(DEFAULT_CONFIG))
local sound = Instance.new("Sound")
sound.SoundId = config.hitSoundId
sound.Volume = config.hitSoundVolume
sound.PlaybackSpeed = 1 + (math.random() - 0.5) * 2 * config.hitSoundPitchVariation
sound.RollOffMaxDistance = 80
sound.RollOffMinDistance = 5
sound.Parent = part
sound:Play()
sound.Ended:Connect(function()
sound:Destroy()
end)
end
--[[
应用随时间衰减的短时间屏幕震动。
]]
local function shakeCamera(config: typeof(DEFAULT_CONFIG))
local camera = workspace.CurrentCamera
if not camera then return end
local elapsed = 0
local intensity = config.shakeIntensity
local duration = config.shakeDuration
local connection: RBXScriptConnection
connection = RunService.RenderStepped:Connect(function(dt: number)
elapsed += dt
if elapsed >= duration then
connection:Disconnect()
return
end
local decay = 1 - (elapsed / duration)
local offsetX = (math.random() - 0.5) * 2 * intensity * decay
local offsetY = (math.random() - 0.5) * 2 * intensity * decay
camera.CFrame = camera.CFrame * CFrame.new(offsetX, offsetY, 0)
end)
end
--[[
主入口:在目标部件上触发完整的击中效果。
@param targetPart 被击中的BasePart(例如角色肢体或NPC身体)。
@param overrides 可选,覆盖DEFAULT_CONFIG的任意值。
]]
function HitEffectSystem.play(targetPart: BasePart, overrides: { [string]: any }?)
-- 合并配置与覆盖项
local config = table.clone(DEFAULT_CONFIG)
if overrides then
for key, value in overrides do
(config :: any)[key] = value
end
end
-- 同时触发所有效果
flashPart(targetPart, config)
emitParticleBurst(targetPart, config)
playHitSound(targetPart, config)
shakeCamera(config)
end
--[[
清理缓存的发射器(场景重置或玩家离开时调用)。
]]
function HitEffectSystem.cleanup()
for part, emitter in emitterCache do
emitter:Destroy()
end
table.clear(emitterCache)
end
return HitEffectSystem
--[[
使用示例(在LocalScript中):
local HitEffectSystem = require(script.Parent.HitEffectSystem)
-- 击中确认时(例如通过服务器发送的RemoteEvent):
hitRemote.OnClientEvent:Connect(function(targetPart: BasePart)
HitEffectSystem.play(targetPart)
end)
-- 暴击时自定义覆盖项:
hitRemote.OnClientEvent:Connect(function(targetPart: BasePart, isCritical: boolean)
if isCritical then
HitEffectSystem.play(targetPart, {
flashColor = Color3.fromRGB(255, 50, 50),
particleBurstCount = 40,
shakeIntensity = 0.8,
shakeDuration = 0.4,
hitSoundId = "rbxassetid://critical_hit_sound_id",
})
else
HitEffectSystem.play(targetPart)
end
end)
]]