Skip to content

第 16 章:音效、特效与场景渲染

把玩法判定与音效、特效、光照和后处理表现分离,并管理复用生命周期。

你会学到什么

  • 什么时候用 SoundService,什么时候用 SoundUnit
  • 如何用 EffectUnit 播放一次性特效,并避免特效残留。
  • 如何把命中反馈拆成“判定”和“表现”。
  • 如何用 LightingServiceSky 和后处理做场景氛围。
  • 如何避免表现层代码污染玩法权威逻辑。

表现层不是玩法判定

音效、特效、相机、光照都属于表现层。它们让玩家“看见”和“听见”结果,但不应该替代服务端玩法判定。

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 不替代本教程的生命周期清理。
SetColorColor(...)不传十六进制字符串。

特效颜色、缩放和绑定

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 创建失败。")
end

game:CreateUnitWorld: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.AmbientColorLightingService.Color 使用 Color(...),RGBA 范围 0~255。

错误:Sky 或后处理没有挂到 LightingService

SkyBloomEffectColorGradingEffect 等需要 Parent = LightingService 才能生效。

错误:表现逻辑决定玩法结果

声音和特效可以由 client 播放,但得分、扣血、奖励发放等结果必须由 server 判定。

练习任务

  1. 写一个 PlayHitFeedback(position),同时播放 3D 音效和一次性特效。
  2. 给特效设置颜色、缩放和播放速率。
  3. LightingService 切换一个夜晚氛围。
  4. 创建一个 Sky 并挂到 LightingService 下。
  5. 思考:哪些表现只给本地玩家看,哪些需要所有玩家看到?

本章验收标准

  • [ ] 我知道 SoundServiceSoundUnit 的适用场景。
  • [ ] 我能用 Play2D / Play3D 播放短音效。
  • [ ] 我能定位编辑器中已配置的 EffectUnit,并用公开方法控制时长和可见性。
  • [ ] 我知道 LightingService 使用 Color(...)Vector3(...)
  • [ ] 我知道 Sky 和后处理要挂到 LightingService 下。
  • [ ] 我能把玩法判定和表现反馈拆开。

本章产物

  • 一个 PlayHitFeedback(position) 封装,至少包含 3D 音效和一次性特效。
  • 一张表现层截图或运行效果记录,确认特效、音效、光照或天空实际可见/可听。
  • 一份表现归属说明,标出哪些反馈只给本地玩家,哪些需要所有玩家看到。

本章 API 对照

下一章预告

表现层学完后,下一章进入资产加载:如何优先用 preset 创建完整模型,再进阶到手动创建单个 Unit。