主题
第 16 章:音效、特效与场景渲染
把玩法判定与音效、特效、光照和后处理表现分离,并管理复用生命周期。
你会学到什么
- 什么时候用
SoundService,什么时候用SoundUnit。 - 如何用
EffectUnit播放一次性特效,并避免特效残留。 - 如何把命中反馈拆成“判定”和“表现”。
- 如何用
LightingService、Sky和后处理做场景氛围。 - 如何避免表现层代码污染玩法权威逻辑。
表现层不是玩法判定
音效、特效、相机、光照都属于表现层。它们让玩家“看见”和“听见”结果,但不应该替代服务端玩法判定。
text
server 判定命中 / 得分 / 受伤
→ server 更新权威状态
→ server 或 RemoteEvent 通知需要看到表现的客户端
→ client / server 播放音效、特效、相机震动、UI 反馈如果所有玩家都必须看到同一个世界特效,可以由 server 创建。如果只有本地玩家需要看到,例如命中准星提示、受击红屏、UI 音效,优先放在 client。
SoundService:短音效和全局音量
SoundService 适合播放短促的 2D/3D 音效,例如点击声、命中声、爆炸声,也适合管理音组音量。
Play2D:不带空间位置的声音
lua
local SoundService = game:GetService("SoundService")
if not SoundService then return end
-- Play2D(soundId, volume, speed, playerOrCampRoleId, campRoleId, duration)
-- false 表示不限定具体玩家或阵营;duration > 0 时便于自动结束。
SoundService:Play2D("official://audio/10772", 80, 1, false, false, 2)2D 音效适合 UI 点击、倒计时提示、任务完成提示。它不随世界距离衰减。
Play3D:在世界位置播放声音
lua
local SoundService = game:GetService("SoundService")
if not SoundService then return end
local hitPos = Vector3(0, 5, 0)
-- Play3D(soundId, position, duration, volume, speed, player)
-- false 表示不限定具体玩家。
SoundService:Play3D("official://audio/10772", hitPos, 2, 90, 1, false)3D 音效适合爆炸、机关、环境物体等“声音从某个位置传来”的场景。
音组音量
lua
local SoundService = game:GetService("SoundService")
if not SoundService then return end
SoundService:SetGroupVolume("BGM", 50)
SoundService:SetGroupVolume("SFX", 85)
local sfxVolume = SoundService:GetGroupVolume("SFX")
print("[SE Lua Guide] 当前 SFX 音量:", sfxVolume)
SoundService.GroupVolumeChanged:Connect(function(name, volume)
print("[SE Lua Guide] 音组音量变化:", name, volume)
end)GroupVolumeChanged 是本地事件,适合更新设置界面上的音量显示。
SoundUnit:持续播放和可控声音实例
如果声音需要循环、暂停、停止、改变衰减距离,使用 SoundUnit 会比一次性 Play2D/Play3D 更清楚。
lua
local World = game:GetService("World")
if not World then return end
local bgm = World:CreateUnit("SoundUnit", {
SoundId = "official://audio/10772",
Looped = true,
Volume = 45,
})
if not bgm or not bgm:IsA("SoundUnit") then
print("[SE Lua Guide][WARN] SoundUnit 创建失败。")
return
end
bgm:Play()
-- 需要切换关卡、暂停游戏或关闭音乐时:
-- bgm:Stop()
-- bgm:Destroy()常见选择:
| 需求 | 推荐 |
|---|---|
| UI 点击声 | SoundService:Play2D |
| 命中点爆炸声 | SoundService:Play3D |
| 背景音乐 | SoundUnit |
| 持续环境声 | SoundUnit |
| 玩家设置音量 | SoundService:SetGroupVolume |
EffectUnit:一次性视觉特效
EffectUnit 用来播放视觉特效。本项目的教程作者面门禁不允许在 Lua 示例中选择特效资源,因此先在编辑器中配置一个 EffectUnit 并命名;Lua 通过公开对象树定位它,再控制位置、时长和可见性。不要从更宽松的原始 Meta 猜测或复制被教程门禁隐藏的资源字段。
lua
local World = game:GetService("World")
local Task = game:GetService("Task")
if not World or not Task then return end
local effect = World:FindFirstChild("HitEffect", true)
if effect and effect:IsA("EffectUnit") then
local duration = 1.2
effect:SetPosition(Vector3(0, 5, 0))
effect:SetDuration(duration)
effect:SetVisible(true)
Task:Delay(duration, function()
if effect and effect.Parent then
effect:SetVisible(false)
end
end)
end关键规则:
| 规则 | 原因 |
|---|---|
| 特效资源在编辑器配置 | 教程作者面门禁不开放 Lua 侧资源选择,不猜测隐藏属性。 |
SetDuration(duration) 设置时长 | 使用 EffectUnit 已公开的方法,不直接写未公开资源字段。 |
当前版本没有独立 Play() | 使用 SetVisible(true/false) 控制可见性。 |
| 到时显式隐藏 | SetDuration 不替代本教程的生命周期清理。 |
SetColor 传 Color(...) | 不传十六进制字符串。 |
特效颜色、缩放和绑定
lua
local World = game:GetService("World")
local Task = game:GetService("Task")
if not World or not Task then return end
local effect = World:FindFirstChild("HitEffect", true)
if effect and effect:IsA("EffectUnit") then
local duration = 2
effect:SetPosition(Vector3(0, 4, 0))
effect:SetDuration(duration)
effect:SetScale(Vector3(1.5, 1.5, 1.5))
effect:SetColor(Color(255, 80, 40, 255), 100, 0.6)
effect:SetRate(1.2)
effect:SetVisible(true)
Task:Delay(duration, function()
if effect and effect.Parent then
effect:SetVisible(false)
end
end)
end需要让特效跟随某个单位时,使用公开的 SetBindData(otherUnit, socket, offset, bindType)。其中:
otherUnit必须是SpaceUnit。socket是具体模型资产提供的挂点名;当前 Meta 没有承诺所有模型都存在名为Root的通用挂点。offset是位置偏移。bindType使用Enums.EffectBindType.POS / POS_ROT / POS_SCALE / ALL。
只有从目标模型的公开资源说明或编辑器配置中确认挂点名后,才能填写并实测这次绑定。绑定特效适合角色光环、技能蓄力、拾取物发光;一次性命中特效通常只需要 SetPosition,不应为了示例猜一个挂点名。
命中反馈封装
下面的函数只负责表现,不负责判定。判定命中、加分、扣血仍应在服务端玩法逻辑中完成。
运行端:server 或 client,取决于这段表现要给谁看。
lua
local SoundService = game:GetService("SoundService")
local World = game:GetService("World")
local Task = game:GetService("Task")
if not SoundService or not World or not Task then return end
local hitEffect = World:FindFirstChild("HitEffect", true)
local effectGeneration = 0
local function PlayHitFeedback(position)
SoundService:Play3D("official://audio/10772", position, 2, 90, 1, false)
if hitEffect and hitEffect:IsA("EffectUnit") then
local duration = 1.2
effectGeneration = effectGeneration + 1
local generation = effectGeneration
hitEffect:SetVisible(false)
hitEffect:SetPosition(position)
hitEffect:SetDuration(duration)
hitEffect:SetVisible(true)
Task:Delay(duration, function()
-- 连续命中时,旧定时器不能关闭较新的特效播放。
if generation == effectGeneration and hitEffect and hitEffect.Parent then
hitEffect:SetVisible(false)
end
end)
end
print("[SE Lua Guide] 命中表现已播放")
end如果只有命中者本人需要看到更强反馈,可以由 server 发送 RemoteEvent 给该玩家,在 client 本地播放增强版音效、特效、相机震动或 UI 动画。
LightingService:全局光照
LightingService 控制场景环境光和主光。它的颜色字段使用 Color(RGBA 0~255),不是 Color3。
lua
local LightingService = game:GetService("LightingService")
if not LightingService then return end
LightingService.AmbientColor = Color(40, 50, 80, 255)
LightingService.AmbientIntensity = 0.8
LightingService.Color = Color(180, 210, 255, 255)
LightingService.Intensity = 1.5
-- Orientation 是 Vector3,表示角度制欧拉角,不是 CFrame。
LightingService.Orientation = Vector3(45, -30, 0)
LightingService.ShadowFade = 0.6常见氛围:
| 氛围 | 调整方向 |
|---|---|
| 白天 | 主光强,环境光偏亮。 |
| 夜晚 | 主光弱,环境光偏蓝紫。 |
| 危险区域 | 环境光偏红,后处理略提高对比度。 |
| 梦幻区域 | Bloom 强一点,天空模板更鲜艳。 |
Sky:天空必须挂到 LightingService 下
Sky 是可定制天空单元。新项目优先用 Sky,通过 Enums.SkyTemplate.* 切换模板。它必须挂到 LightingService 下才生效;移除或销毁 Sky 后会恢复默认天空。切换关卡氛围时,要清楚当前是谁创建了天空,避免多个系统互相覆盖。
lua
local LightingService = game:GetService("LightingService")
if not LightingService then return end
local sky = game:CreateUnit("Sky", {
Parent = LightingService,
Template = Enums.SkyTemplate.Night,
Orientation = Vector3(0, 25, 0),
YOffset = 0,
})
if sky and sky:IsA("Sky") then
print("[SE Lua Guide] 夜晚天空已设置:", sky.Template)
else
print("[SE Lua Guide][WARN] Sky 创建失败。")
endgame:CreateUnit 与 World:CreateUnit 的父节点语义不同:前者不会自动设置 Parent,未显式传入时对象保持“游离”(Detached);后者会把新对象放到 World 下。
后处理:挂到 LightingService 下才生效
Bloom、ColorGrading、DepthOfField、AmbientOcclusion 等后处理都继承自 BasePostEffect,核心开关是 Enabled。通常通过 game:CreateUnit 创建,并挂到 LightingService 下。
lua
local LightingService = game:GetService("LightingService")
if not LightingService then return end
local bloom = game:CreateUnit("BloomEffect", {
Parent = LightingService,
Enabled = true,
Intensity = 0.35,
Threshold = 1.4,
Size = 0.8,
})
local grading = game:CreateUnit("ColorGradingEffect", {
Parent = LightingService,
Enabled = true,
Saturation = 0.9,
Contrast = 1.08,
Temperature = -0.1,
})
if not bloom or not grading then
print("[SE Lua Guide][WARN] 后处理创建不完整,请检查当前渲染配置。")
end后处理会影响整体画面,属于高影响表现。建议先做轻量调整,再在目标设备上检查性能和可读性。
常见错误
错误:只设置 Duration,不负责隐藏
本章复用的是编辑器中预配置的 EffectUnit,不应把它自动销毁。SetDuration 只设置特效时长;教程仍使用 Task:Delay 在到时后显式 SetVisible(false),保证实例回到可复用状态。
错误:连续播放时让旧定时器关闭新特效
同一个预配置特效可能在旧 Delay 到期前再次播放。保存 generation,并只允许最新一轮定时器隐藏实例;否则第二次播放会被第一次的清理提前截断。
错误:把 Color3 传给 LightingService
LightingService.AmbientColor 和 LightingService.Color 使用 Color(...),RGBA 范围 0~255。
错误:Sky 或后处理没有挂到 LightingService
Sky、BloomEffect、ColorGradingEffect 等需要 Parent = LightingService 才能生效。
错误:表现逻辑决定玩法结果
声音和特效可以由 client 播放,但得分、扣血、奖励发放等结果必须由 server 判定。
练习任务
- 写一个
PlayHitFeedback(position),同时播放 3D 音效和一次性特效。 - 给特效设置颜色、缩放和播放速率。
- 用
LightingService切换一个夜晚氛围。 - 创建一个
Sky并挂到LightingService下。 - 思考:哪些表现只给本地玩家看,哪些需要所有玩家看到?
本章验收标准
- [ ] 我知道
SoundService和SoundUnit的适用场景。 - [ ] 我能用
Play2D/Play3D播放短音效。 - [ ] 我能定位编辑器中已配置的
EffectUnit,并用公开方法控制时长和可见性。 - [ ] 我知道
LightingService使用Color(...)和Vector3(...)。 - [ ] 我知道
Sky和后处理要挂到LightingService下。 - [ ] 我能把玩法判定和表现反馈拆开。
本章产物
- 一个
PlayHitFeedback(position)封装,至少包含 3D 音效和一次性特效。 - 一张表现层截图或运行效果记录,确认特效、音效、光照或天空实际可见/可听。
- 一份表现归属说明,标出哪些反馈只给本地玩家,哪些需要所有玩家看到。
本章 API 对照
下一章预告
表现层学完后,下一章进入资产加载:如何优先用 preset 创建完整模型,再进阶到手动创建单个 Unit。
