返回博客
Tutorial3 分钟阅读

FiveM音乐与广播播放器脚本:xSound及更多

正确方式在游戏内播放音乐。xSound、收音机播放器应用、便携音箱道具及最佳FiveM音频脚本,适用于派对、酒吧和车辆。

Agency Scripts

Agency Scripts 创始人兼首席开发者

为什么自定义无线电在角色扮演中重要

音乐和广播是赋予FiveM角色扮演服务器生命的关键氛围元素。虽然GTA V自带广播电台,但自定义广播系统允许服务器拥有者创建符合社区身份的主题电台,直播DJ节目,播放社区策划的播放列表,甚至广播角色内新闻简报。一个构建良好的音乐系统不仅仅是简单的音频播放。它为同车玩家提供同步聆听体验,支持来自便携音箱和俱乐部扬声器的空间音频,玩家可通过手机应用管理个人播放列表,并支持夜店角色扮演的DJ台集成。技术基础依赖于NUI音频功能,使您能够在FiveM客户端内充分利用基于浏览器的音频API。

音频架构与NUI集成

FiveM 的 NUI 层运行在游戏客户端内嵌的基于 Chromium 的浏览器中,这意味着你可以使用 Web Audio API、HTML5 音频元素,甚至 MediaSource API 进行自适应流媒体。架构分为三部分:Lua 客户端脚本处理游戏世界交互,如进入车辆或接近音箱;NUI HTML 和 JavaScript 层管理实际音频播放和可视化;服务器协调多个客户端的同步播放状态。当玩家进入车辆时,客户端脚本发送 NUI 消息启动当前电台播放。NUI 层创建音频元素,设置源 URL 并开始播放。音量控制完全由 NUI 层使用 Web Audio API 的 GainNode 实现平滑音量过渡:

-- Client-side: Vehicle radio controller
local currentStation = nil
local radioActive = false

CreateThread(function()
    while true do
        Wait(500)
        local ped = PlayerPedId()

        if IsPedInAnyVehicle(ped, false) then
            local vehicle = GetVehiclePedIsIn(ped, false)

            if not radioActive then
                radioActive = true
                -- Disable default GTA radio
                SetVehicleRadioEnabled(vehicle, false)

                -- Load saved station preference
                local savedStation = LocalPlayer.state.radioStation or 'station_1'
                SetStation(savedStation)
            end
        elseif radioActive then
            radioActive = false
            SendNUIMessage({action = 'stopRadio'})
            currentStation = nil
        end
    end
end)

function SetStation(stationId)
    local station = Config.Stations[stationId]
    if not station then return end

    currentStation = stationId
    SendNUIMessage({
        action = 'playStation',
        url = station.url,
        name = station.name,
        volume = LocalPlayer.state.radioVolume or 0.5,
    })
end

NUI音频播放器设置

NUI端需要一个强大的音频播放器,能够处理流连接、缓冲、错误恢复和音量管理。使用Web Audio API而非简单的HTML5音频元素,因为它让你控制音频路由,允许你从频率数据创建可视化,并支持空间音频处理。创建一个带有GainNode用于音量控制和AnalyserNode用于可视化数据的AudioContext:

// NUI JavaScript: Audio engine
class RadioPlayer {
    constructor() {
        this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
        this.gainNode = this.audioContext.createGain();
        this.analyser = this.audioContext.createAnalyser();
        this.analyser.fftSize = 256;

        this.gainNode.connect(this.analyser);
        this.analyser.connect(this.audioContext.destination);

        this.audio = new Audio();
        this.audio.crossOrigin = 'anonymous';
        this.source = null;
        this.isPlaying = false;
    }

    play(url, volume) {
        this.stop();
        this.audio = new Audio(url);
        this.audio.crossOrigin = 'anonymous';
        this.audio.volume = 1.0;

        this.source = this.audioContext.createMediaElementSource(this.audio);
        this.source.connect(this.gainNode);
        this.gainNode.gain.value = volume;

        this.audio.play().then(() => {
            this.isPlaying = true;
        }).catch(err => {
            console.error('Radio playback failed:', err);
            setTimeout(() => this.play(url, volume), 3000);
        });
    }

    stop() {
        if (this.audio) {
            this.audio.pause();
            this.audio.src = '';
            this.isPlaying = false;
        }
    }

    setVolume(vol) {
        this.gainNode.gain.linearRampToValueAtTime(
            vol, this.audioContext.currentTime + 0.1
        );
    }

    getFrequencyData() {
        const data = new Uint8Array(this.analyser.frequencyBinCount);
        this.analyser.getByteFrequencyData(data);
        return data;
    }
}

const radio = new RadioPlayer();

车站配置与流媒体

定义你的电台,包含流媒体 URL、元数据和类别标签。大多数自定义 FiveM 电台系统使用 MP3 或 AAC 格式的互联网电台流,因为它们广泛支持且易于设置。你可以自行托管 Icecast 或 Shoutcast 服务器以完全控制,或使用公共电台流 URL。每个电台存储流 URL、显示名称、类别标签,及可选的 UI 标志图片。考虑通过代理服务支持 YouTube 和 SoundCloud URL,但需注意服务条款和版权限制。为实现直播 DJ 功能,集成提供 RTMP 转 HTTP 流转换的服务,使 DJ 可使用 OBS 或类似软件广播:

Config.Stations = {
    ['station_1'] = {
        name = 'Agency Radio',
        genre = 'Hip Hop',
        url = 'https://stream.example.com/hiphop',
        logo = 'station_hiphop.webp',
        description = 'The freshest beats in Los Santos',
    },
    ['station_2'] = {
        name = 'LS Rock Radio',
        genre = 'Rock',
        url = 'https://stream.example.com/rock',
        logo = 'station_rock.webp',
        description = 'Classic and modern rock anthems',
    },
    ['station_3'] = {
        name = 'Vinewood Lounge',
        genre = 'Jazz / Lofi',
        url = 'https://stream.example.com/lofi',
        logo = 'station_lofi.webp',
        description = 'Smooth vibes for the night drive',
    },
    ['station_live'] = {
        name = 'Live DJ',
        genre = 'Live',
        url = 'https://stream.example.com/live',
        logo = 'station_live.webp',
        description = 'Live DJ sets from the community',
        isLive = true,
    },
}

同步车辆收音机

当多名玩家乘坐同一辆车时,他们应听同一电台。这需要通过状态袋或网络事件进行服务器端协调。司机更换电台时,服务器向所有乘客广播新电台。使用 FiveM 的状态袋系统实现高效同步,因为它自动处理网络复制,无需手动事件管理。在车辆实体的状态袋中设置电台,任何进入车辆的玩家都会自动接收当前电台。处理边缘情况,如乘客进入已有电台的车辆、司机离开但乘客仍在、以及如果允许非司机控制时乘客的冲突电台更改:

-- Server-side: Sync radio state via state bags
RegisterNetEvent('radio:server:setStation', function(vehicleNet, stationId)
    local src = source
    local vehicle = NetworkGetEntityFromNetworkId(vehicleNet)
    if not DoesEntityExist(vehicle) then return end

    -- Verify player is the driver
    local ped = GetPlayerPed(src)
    if GetPedInVehicleSeat(vehicle, -1) ~= ped then return end

    -- Set station on vehicle state bag
    Entity(vehicle).state:set('radioStation', stationId, true)
end)

-- Client-side: Listen for state bag changes
AddStateBagChangeHandler('radioStation', nil, function(bagName, key, value)
    if not value then return end
    local entity = GetEntityFromStateBagName(bagName)
    if not DoesEntityExist(entity) then return end

    local ped = PlayerPedId()
    if not IsPedInVehicle(ped, entity, false) then return end

    SetStation(value)
end)

空间音频和Boombox系统

空间音频增加了平面音频播放无法比拟的沉浸感。Boombox、俱乐部音箱和汽车音响应发出随距离衰减并根据听者相对位置声像变化的声音。FiveM提供原生音频功能,如 PlaySoundFromCoord 用于简单的空间声音,但对于流式音频,您需要在NUI层实现基于距离的音量缩放。每帧计算玩家与音频源之间的距离,然后将该距离映射到音量曲线。使用反平方衰减实现逼真的声音衰减,具有可配置的最小和最大听觉距离。对于玩家可以放置在世界中的boombox,创建一个可部署的物品,在玩家位置生成一个道具并将其注册为音频源。范围内的其他玩家应自动听到音乐,音量根据他们与boombox道具的距离进行缩放。

玩家播放列表和电话集成

通过与服务器电话资源集成的播放列表系统,让玩家个人控制他们的音乐体验。玩家应能通过添加单曲 URL 创建自定义播放列表,拖放重新排序歌曲,设置播放列表为随机或重复,并与其他玩家分享播放列表。播放列表存储在与玩家标识符关联的数据库中,以便跨会话保持。电话应用界面应显示当前播放曲目及艺术家和标题元数据,播放控制包括播放、暂停、跳过和上一曲,音量滑块,以及保存的播放列表列表。当玩家在车辆中时,电话播放器应自动通过车辆收音机系统播放音频,使乘客能听到音乐。步行时装备耳机作为物品,音乐私密播放,其他玩家可见一个小耳机图标,表示角色正在听音乐但其他人听不到。

可视化工具和界面设计

音乐播放器UI应生动响应播放音频。使用Web Audio API的AnalyserNode实时提取频率数据并渲染音频可视化。条形可视化、波形显示和圆形频谱分析器均适合不同UI美学。使用canvas元素渲染以提升性能,更新频率为30或60帧每秒,视可视化复杂度而定。收音机UI本身应简洁不干扰,作为屏幕角落的小部件显示当前电台名、曲目标题(如流媒体元数据提供)和基本播放控件。玩家打开完整收音机菜单切换电台时,显示电台网格或列表,含标志、类型和听众数。为拥有众多电台的服务器提供搜索栏和收藏系统,方便玩家快速访问喜爱电台,无需滚动整个列表。

性能考虑因素

音频流消耗带宽和客户端资源,请谨慎优化实现。限制每个客户端的同时音频流数量以防止内存泄漏和CPU峰值。当玩家不在车辆内且附近无音箱时,应完全销毁音频上下文,而非仅暂停。对于来自多个音箱的空间音频,仅为听力范围内的音源创建音频流,并在音源移出范围时清理流。对环境背景音乐使用低比特率流,主要聆听体验保留高质量流。服务器端避免在事件中直接发送流URL,以防被拦截和滥用。改用回调系统,客户端请求特定电台ID的流URL,服务器返回带时限签名的URL。监控客户端报告的播放错误,自动重连因网络问题断开的流,采用指数退避以防止频繁请求负载过重的流服务器。

准备好开始了吗?

在我们的商店获取脚本,或加入 Discord 获取支持、更新以及新功能预告。