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
    Animator
    (on Humanoid/AnimationController), not deprecated
    Humanoid: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。同一轨道上,高优先级会覆盖低优先级动画。
  • 始终使用
    Animator
    (挂载在Humanoid/AnimationController上),而非已弃用的
    Humanoid: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
Humanoid
has (or should have) an
Animator
child. The
Animator
is the engine that plays, blends, and prioritizes animation tracks on a character rig.
luau
local character = player.Character or player.CharacterAdded:Wait()
local humanoid = character:WaitForChild("Humanoid")
local animator = humanoid:FindFirstChildOfClass("Animator")
    or humanoid:WaitForChild("Animator")
每个
Humanoid
都有(或应该有)一个
Animator
子对象。
Animator
是在角色骨架上播放、混合并管理动画轨道优先级的核心引擎。
luau
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 weight
luau
-- 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.
PriorityUse Case
Enum.AnimationPriority.Idle
Breathing, idle sway
Enum.AnimationPriority.Movement
Walk, run, jump, fall
Enum.AnimationPriority.Action
Attack, interact, emote
Enum.AnimationPriority.Action2
Higher-priority actions
Enum.AnimationPriority.Action3
Even higher-priority actions
Enum.AnimationPriority.Action4
Highest action tier
Enum.AnimationPriority.Core
Internal Roblox (avoid overriding)
luau
slashTrack.Priority = Enum.AnimationPriority.Action
当多个轨道影响同一关节时,优先级决定哪个动画生效。高优先级会覆盖低优先级。
优先级使用场景
Enum.AnimationPriority.Idle
呼吸、 idle晃动
Enum.AnimationPriority.Movement
行走、奔跑、跳跃、坠落
Enum.AnimationPriority.Action
攻击、交互、表情动作
Enum.AnimationPriority.Action2
更高优先级的动作
Enum.AnimationPriority.Action3
优先级更高的动作
Enum.AnimationPriority.Action4
最高优先级的动作
Enum.AnimationPriority.Core
Roblox内部动画(避免覆盖)
luau
slashTrack.Priority = Enum.AnimationPriority.Action

MarkerReachedSignal

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
AnimationController
for anything that is NOT a
Humanoid
-- props, doors, creatures with custom rigs, cutscene actors, etc.
luau
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()
对于非
Humanoid
对象(道具、门、自定义骨架的生物、过场动画角色等),使用
AnimationController
luau
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
    Motor6D
    joints connecting its parts, just like a character rig.
  • Root part should be the
    PrimaryPart
    of the model.
  • 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 animations

luau
-- 无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核心属性

PropertyTypeDescription
Rate
number
Particles emitted per second (0 = manual Emit())
Lifetime
NumberRange
How long each particle lives (seconds)
Speed
NumberRange
Initial velocity (studs/second)
SpreadAngle
Vector2
Cone spread in X and Y (degrees)
Size
NumberSequence
Size over particle lifetime
Color
ColorSequence
Color over particle lifetime
Transparency
NumberSequence
Transparency over particle lifetime
Texture
string
Decal/image asset ID for particle appearance
RotSpeed
NumberRange
Rotation speed (degrees/second)
Acceleration
Vector3
Constant force (gravity =
Vector3.new(0,-10,0)
)
Drag
number
Air resistance (0 = none, higher = more drag)
LightEmission
number
0-1, additive blending (1 = fully additive/glowy)
LightInfluence
number
0-1, how much scene lighting affects particles
ZOffset
number
Render order offset toward/away from camera
Orientation
Enum.ParticleOrientation
FacingCamera, VelocityParallel, etc.
属性名称类型描述
Rate
number
每秒发射的粒子数(0 = 手动调用Emit())
Lifetime
NumberRange
每个粒子的生命周期(秒)
Speed
NumberRange
初始速度( studs/秒)
SpreadAngle
Vector2
X和Y轴的圆锥扩散角度(度)
Size
NumberSequence
粒子生命周期内的尺寸变化
Color
ColorSequence
粒子生命周期内的颜色变化
Transparency
NumberSequence
粒子生命周期内的透明度变化
Texture
string
粒子外观的贴图/图像资源ID
RotSpeed
NumberRange
旋转速度(度/秒)
Acceleration
Vector3
恒定作用力(重力为
Vector3.new(0,-10,0)
Drag
number
空气阻力(0 = 无阻力,值越大阻力越强)
LightEmission
number
0-1, additive blending(1 = 完全叠加/发光)
LightInfluence
number
0-1,场景光照对粒子的影响程度
ZOffset
number
渲染顺序偏移(靠近/远离相机)
Orientation
Enum.ParticleOrientation
朝向相机、与速度平行等模式

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 = somePart
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 = somePart

Smoke

烟雾

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 = somePart
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 = somePart

Sparkles / 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 = somePart
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 = somePart

Rain

下雨

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 area
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 -- 放置在游戏区域上方

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 = largeCoverPart
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 = largeCoverPart

Magic 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.HumanoidRootPart

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.HumanoidRootPart

Beam and Trail

光束与轨迹

Beam

光束

A
Beam
renders a textured ribbon between two
Attachment
instances. Perfect for lasers, lightning, tethers, and energy connections.
luau
-- 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 = partA
Beam
在两个
Attachment
实例之间渲染带纹理的带状效果,非常适合激光、闪电、绳索和能量连接效果。
luau
-- 设置:两个带有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 = partA

Key Beam Properties

光束核心属性

PropertyDescription
Attachment0/1
Start and end points
Width0/1
Width at each attachment (studs)
Color
ColorSequence
along the beam length
Transparency
NumberSequence
along the beam length
CurveSize0/1
Bezier curve magnitude at each end
Segments
Number of straight segments (more = smoother curves)
FaceCamera
Always faces the camera for billboard effect
TextureSpeed
Scrolls the texture along the beam
LightEmission
Additive blending for glow
属性名称描述
Attachment0/1
起点和终点
Width0/1
每个Attachment处的宽度(studs)
Color
光束长度方向的
ColorSequence
变化
Transparency
光束长度方向的
NumberSequence
透明度变化
CurveSize0/1
两端的贝塞尔曲线幅度
Segments
直线分段数(越多曲线越平滑)
FaceCamera
始终朝向相机,实现公告板效果
TextureSpeed
沿光束滚动纹理
LightEmission
叠加混合实现发光效果

Trail

轨迹

A
Trail
renders a ribbon behind a moving part. Requires two
Attachment
instances on the same part (defining the trail's width axis).
luau
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 = part
Trail
在移动部件后方渲染带状效果,需要同一部件上的两个
Attachment
实例(定义轨迹的宽度轴)。
luau
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 = part

Common Uses

常见用途

  • Laser beams:
    Beam
    between gun barrel attachment and hit-point attachment.
  • Sword trails:
    Trail
    on blade with short
    Lifetime
    (0.2-0.4s).
  • Magic effects:
    Beam
    with high
    CurveSize
    values and scrolling texture for arcane tethers.
  • Lightning:
    Beam
    with many
    Segments
    , rapidly randomizing
    CurveSize0/1
    each frame.

  • 激光束
    Beam
    连接枪口Attachment和命中点Attachment。
  • 挥剑轨迹:在剑刃上添加
    Trail
    ,设置较短的
    Lifetime
    (0.2-0.4秒)。
  • 魔法效果
    Beam
    设置高
    CurveSize
    值和滚动纹理,实现神秘能量绳索效果。
  • 闪电
    Beam
    设置大量
    Segments
    ,每帧随机改变
    CurveSize0/1

Parent Destruction Behavior

父对象销毁行为

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.
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.
luau
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
Trail
specifically, set
Debris:AddItem(holder, trail.Lifetime + 0.1)
so the trail's existing segments finish rendering before cleanup.

当包含效果的
Part
Model
被销毁(
:Destroy()
、玩家离开、工作区清空)时,所有子对象
ParticleEmitter
Trail
Beam
Attachment
会立即被销毁。活跃粒子会中途消失,轨迹被切断,光束消失。
解决方案 - Debris + 临时容器:如果需要效果在父对象销毁后优雅结束,可将其重新挂载到临时部件,然后用
Debris
清理。
luau
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)
对于
Trail
,设置
Debris:AddItem(holder, trail.Lifetime + 0.1)
,确保轨迹的现有片段在清理前完成渲染。

Highlight

Highlight

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.
Highlight
实例在
BasePart
Model
周围绘制彩色轮廓,用于突出显示对象。每个Highlight有两层:轮廓(边缘剪影)和内部填充(覆盖层),两者可独立自定义。

Basic 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 = targetPart
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 = targetPart

Properties

属性

PropertyTypeDefaultDescription
Adornee
Instance-The
BasePart
or
Model
to highlight
DepthMode
Enum.HighlightDepthMode
AlwaysOnTop
AlwaysOnTop
= visible through objects,
Occluded
= hidden by obstructions
Enabled
boolean
true
Toggle visibility
FillColor
Color3
[255, 200, 50]
Interior overlay color
FillTransparency
number
0.5
0 = opaque, 1 = invisible
OutlineColor
Color3
[255, 255, 255]
Edge outline color
OutlineTransparency
number
0
0 = opaque, 1 = invisible
属性名称类型默认值描述
Adornee
Instance-要高亮的
BasePart
Model
DepthMode
Enum.HighlightDepthMode
AlwaysOnTop
AlwaysOnTop
= 穿透物体可见,
Occluded
= 被遮挡时隐藏
Enabled
boolean
true
切换可见性
FillColor
Color3
[255, 200, 50]
内部填充颜色
FillTransparency
number
0.5
0 = 不透明,1 = 完全透明
OutlineColor
Color3
[255, 255, 255]
边缘轮廓颜色
OutlineTransparency
number
0
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
end
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
end

Limitations

限制

  • Max 255 simultaneous Highlight instances per client. Excess instances are silently ignored.
  • Disabled highlights still count toward the 255 limit -
    :Destroy()
    instead of
    Enabled = false
    if permanently unused.
  • The
    Highlight
    itself is not destroyed when its
    Adornee
    is destroyed. Clean up manually.
  • 每个客户端最多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
end

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
end

TweenService for VFX

用于VFX的TweenService

TweenService
interpolates any numeric or color property over time. It is the backbone of procedural visual feedback.
TweenService
可在一段时间内插值任何数值或颜色属性,是程序化视觉反馈的核心。

TweenInfo

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

常见缓动风格

StyleFeel
Linear
Constant speed, mechanical
Quad
Gentle acceleration/deceleration
Cubic
Stronger ease
Quart
Even stronger
Sine
Smooth, organic
Back
Overshoots then settles
Bounce
Bounces at the end
Elastic
Springy overshoot
Exponential
Very sharp acceleration
风格效果感受
Linear
匀速,机械感强
Quad
平缓加速/减速
Cubic
更强的缓动效果
Quart
缓动效果更明显
Sine
平滑,自然感强
Back
先超出目标再回弹
Bounce
结束时弹跳效果
Elastic
弹性回弹效果
Exponential
急剧加速

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()
end
luau
-- 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)
end
luau
-- 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)
end
luau
-- 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()
end
luau
-- 击中闪光:瞬间变白然后恢复原颜色
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()
end
luau
-- 脉冲效果:放大然后缩小
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)
end
luau
-- 扩大并淡出(爆炸环)
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)
end
luau
-- 颜色过渡(伤害指示器)
local function colorTransition(part: BasePart, targetColor: Color3, duration: number)
    local tween = TweenService:Create(part, TweenInfo.new(duration, Enum.EasingStyle.Sine), {
        Color = targetColor,
    })
    tween:Play()
end

Chaining Tweens

链式补间

Use the
Completed
event to sequence tweens without coroutines:
luau
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

使用
Completed
事件序列补间,无需协程:
luau
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

Lighting 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 = screenPart
luau
-- 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 = screenPart

Post-Processing Effects

后期处理效果

All post-processing objects go in
Lighting
or
Camera
.
所有后期处理对象需放在
Lighting
Camera
中。

Atmosphere

大气效果

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.Lighting
luau
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.Lighting

ColorCorrection

颜色校正

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.Lighting
luau
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.Lighting

Bloom

光晕

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.Lighting
luau
local bloom = Instance.new("BloomEffect")
bloom.Intensity = 0.8        -- 发光强度
bloom.Size = 24              -- 发光扩散范围(像素)
bloom.Threshold = 1.2        -- 触发发光的亮度阈值
bloom.Parent = game.Lighting

DepthOfField

景深

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.Lighting
luau
local dof = Instance.new("DepthOfFieldEffect")
dof.FarIntensity = 0.3       -- 远离焦点区域的模糊强度
dof.FocusDistance = 30        -- 焦点距离(studs)
dof.InFocusRadius = 20       -- 清晰区域半径
dof.NearIntensity = 0.2      -- 靠近相机区域的模糊强度
dof.Parent = game.Lighting

SunRays

太阳射线

luau
local sunRays = Instance.new("SunRaysEffect")
sunRays.Intensity = 0.15     -- ray visibility
sunRays.Spread = 0.8         -- how far rays extend
sunRays.Parent = game.Lighting
luau
local sunRays = Instance.new("SunRaysEffect")
sunRays.Intensity = 0.15     -- 射线可见度
sunRays.Spread = 0.8         -- 射线延伸距离
sunRays.Parent = game.Lighting

Setting 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)
end
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)
        -- 平滑步进实现自然效果
        local smoothAlpha = alpha * alpha * (3 - 2 * alpha)
        camera.CFrame = startCFrame:Lerp(targetCFrame, smoothAlpha)

        if alpha >= 1 then
            connection:Disconnect()
        end
    end)
end

Cutscene 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
    Segments
    reasonable (10-30). Very high segment counts cost draw calls.
  • 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)。避免使用大尺寸或高分辨率粒子纹理。
  • 光束 -- 保持
    Segments
    合理(10-30)。过高的分段数会增加绘制调用。
  • 补间 -- 数百个同时运行的补间没问题;数千个可能会出问题。不再需要时取消/销毁补间。
  • 音效 -- 限制同时播放的音效数量在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
end
luau
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
end

Pool 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)
end

Sync Sound with Visuals

音效与视觉同步

  • Use
    MarkerReachedSignal
    to trigger sounds at exact animation frames.
  • Play impact sounds at the moment of collision, not when the swing starts.
  • Match
    PlaybackSpeed
    to animation speed adjustments.
  • Use
    task.delay
    or tween
    Completed
    events for sequenced audio.

  • 使用
    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)
end
luau
-- 错误:无限制创建粒子且不清理
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)
end

Unoptimized 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 effects
luau
-- 错误:用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
end
luau
-- 错误:在热点路径同步加载动画
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
end

Other 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
    Camera.CameraType
    to
    Scriptable
    and forgetting to restore it
    -- player loses control.
  • Not cleaning up Highlight after its Adornee is destroyed -- orphaned Highlights waste one of the 255 slots. Use
    AncestryChanged
    to auto-destroy.
  • 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
    Shadows = false
    for most dynamic lights.

  • 通过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)
]]