简介:面向FiveM游戏服务器管理员的加载屏幕定制项目,通过CSS技术实现动态过渡效果,用于替换默认启动界面,提升玩家等待时的视觉体验并强化服务器品牌识别。压缩包共21个文件、大小2.75MB,核心包括HTML页面结构、两套CSS样式、JavaScript交互脚本、两段Lua资源加载配置,以及多款Orbitron字体、背景图与ogg音乐文件,涵盖视觉、听觉与资源加载逻辑,便于从各个层面定制加载界面。已有154人学习,适合具备基础HTML/CSS知识、希望快速部署个性化启动画面的管理员选用。压缩包内附README说明与完整源文件,可自行修改动画节奏、配色、背景音乐和服务器名称,无需外部依赖即可集成进FiveM服务器;加载屏幕本身由CSS驱动动态样式,结合字体与图片资源,能形成独特的过场氛围,让等待过程成为展示服务器社区特色的窗口。
1. Flexy-Loading-Screen-FiveM:把等待进服务器的 30 秒变成服务器的第一张名片
玩家从点击进入 FiveM 服务器到真正上线,往往要经历几十秒的加载窗口。默认加载页只有一个干巴巴的进度条,而 Flexy-Loading-Screen-FiveM 这类方案,是把这段等待做成服务器的第一张名片:背景视频、平滑进度条、轮播提示语、服务器名和主题色全部由一份配置控制。它解决的是社区服务器的真实痛点——服主想要一套看起来专业的加载屏,又不希望每次改个文案都要翻 HTML、改 CSS。这篇内容适合两类人:一类是刚接手服务器、想换掉默认加载页的服主;另一类是写 NUI 的开发者,想确认加载屏的消息协议、CEF 边界和常见翻车点。
2. 加载屏是怎么被驱动起来的:loadingProgress 消息协议、fxmanifest 与最小骨架
2.1 loadingProgress 之外:FiveM 发给加载屏的 4 类消息
加载屏本质上是一个在 CEF(Chromium Embedded Framework)里打开的本地 HTML 页面。游戏在加载的不同阶段,会把 JSON 数据投递到页面,页面只需要在window上监听message事件。所有消息都挂在event.data上,判断type字段决定怎么处理。
| type | 数据字段 | 出现时机 | 常见用途 |
|---|---|---|---|
| loadingProgress | progress(0~100)、stage(阶段文案) | 加载过程中频繁触发 | 驱动进度条、阶段文字 |
| onConnectingMessage | message | 正在连接服务器 | 显示“正在连接”状态 |
| onServerMessage | message | 服务器下发连接消息 | 显示欢迎语、公告 |
| onLogLine | message | 游戏日志输出 | 仅调试用 |
loadingProgress是最核心的消息。progress是 0 到 100 的数字,stage是英文阶段字符串,常见值有Loading game、Loading map这类。注意stage不是协议保证的稳定值,客户端版本变化后内容可能带序号或附加说明,所以前端做展示时最好有兜底逻辑。onLogLine消息非常密集,一条条日志往外吐,别用它驱动任何 UI,否则进度条会被高频消息抖成筛子。onConnectingMessage和onServerMessage只在连接阶段出现,适合做“正在连接服务器”之类的过渡文案。
2.2 fxmanifest 里必须写对的 3 个指令:files、loadscreen 与 manual shutdown
加载屏资源和其他资源一样放在resources目录下,关键配置都在fxmanifest.lua里。三个指令必须写对:files声明所有页面引用到的文件,漏一个就 404;loadscreen指定 HTML 入口;loadscreen_manual_shutdown决定加载完成后由谁收起页面。
-- fxmanifest.lua fx_version 'cerulean' game 'gta5' description 'Flexy Loading Screen' version '1.2.0' files { 'index.html', 'config.js', 'css/style.css', 'js/app.js', 'assets/bg.mp4', 'assets/poster.jpg' } loadscreen 'index.html' loadscreen_manual_shutdown 'yes'这份 manifest 的重点:game 'gta5'是必填项;files里的路径是相对资源目录的,HTML 里能通过相对路径打开的文件,在 CEF 里同样必须在files中列出,否则会加载失败,这是新手最容易漏的一步。loadscreen_manual_shutdown 'yes'表示加载完成后不自动收起加载屏,改由前端代码控制,后面避坑章节会细说。部署时把整个文件夹放进服务器的resources目录,在server.cfg里写一行ensure flexy-loading-screen,注意目录名就是资源名,两处必须一致。
2.3 跑通一个最小加载屏:manifest + HTML + CSS + 30 行 JS
先抛开花哨效果,验证协议链路。一个最小结构只需要四个文件。
flexy-loading-screen/ ├── fxmanifest.lua ├── index.html ├── css/style.css └── js/app.js<!-- index.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="css/style.css"> <title>Loading</title> </head> <body> <main> <h1 id="serverName">My Server</h1> <div class="bar"> <div id="barInner"></div> </div> <p id="stage">正在连接...</p> </main> <script src="js/app.js"></script> </body> </html>/* css/style.css */ body { margin: 0; background: #0b0e12; color: #fff; font-family: system-ui, sans-serif; } main { display: grid; place-items: center; height: 100vh; text-align: center; } .bar { width: 320px; height: 6px; background: rgba(255, 255, 255, 0.15); border-radius: 3px; overflow: hidden; } #barInner { width: 0%; height: 100%; background: #ffc107; transition: width 0.15s linear; } #stage { font-size: 14px; opacity: 0.7; }// js/app.js —— 最小实现 window.addEventListener('message', function (event) { var data = event.data || {}; if (data.type !== 'loadingProgress') return; var value = Math.max(0, Math.min(100, Number(data.progress) || 0)); document.getElementById('barInner').style.width = value + '%'; document.getElementById('stage').textContent = data.stage || '加载中...'; }); // 游戏加载完成时会调用这个全局函数,也可以由页面自己调用 window.cancelLoading = function () { document.body.style.opacity = '0'; setTimeout(function () { document.body.style.display = 'none'; }, 400); };这段 JS 的要点:message监听是加载屏的入口,只认loadingProgress,过滤掉onLogLine等高频消息;Math.max(0, Math.min(100, ...))把进度值限制在合法区间,防止脏数据把进度条顶出容器。cancelLoading是加载屏页面暴露给游戏的总开关,游戏判定完成时调用它收起页面;如果你开了手动关闭,也可以由页面自己调用。transition: width 0.15s linear保证进度条跟随消息平滑推进,不至于一顿一顿。到这里资源已经可以在服务器上跑起来,你会看到进度条跟着loadingProgress走,阶段文字显示英文原文——这就说明协议链路通了。
提示:页面里的脚本放在
<body>末尾,避免 DOM 还没构建完就执行getElementById拿不到节点。
3. 把加载屏做得“Flexy”:config.js 单配置、rAF 平滑进度条与多背景轮换
3.1 一个 config.js 管全部:FlexyConfig 字段与主题变量
“Flexy”风格的核心卖点是配置驱动。所有可变项收敛到window.FlexyConfig一个对象里,HTML 和业务 JS 不硬编码任何文案、颜色、素材路径。服主换皮肤只改这一个文件。
// config.js window.FlexyConfig = { serverName: 'CITY ROLEPLAY', mottos: [ '欢迎回来,市民。', '主干道有警察查酒驾,建议绕行。', '今日房产价格上调 5%。' ], mottoInterval: 4000, stageTexts: { 'Loading game': '正在加载游戏', 'Loading map': '正在载入地图' }, progress: { smoothFactor: 0.1, fallbackStart: 10000, fallbackMax: 98 }, background: { type: 'video', list: ['assets/bg1.mp4', 'assets/bg2.mp4'], poster: 'assets/poster.jpg', fallback: 'assets/fallback.jpg' }, theme: { progress: '#ffc107', progressBg: 'rgba(255, 255, 255, 0.15)' }, fadeOutMs: 800 };启动时把配置应用进页面:
// app.js 顶部 (function () { 'use strict'; var cfg = window.FlexyConfig || {}; var root = document.documentElement.style; if (cfg.theme) { root.setProperty('--progress-color', cfg.theme.progress || '#ffc107'); root.setProperty('--progress-bg', cfg.theme.progressBg || 'rgba(255, 255, 255, 0.15)'); } document.getElementById('serverName').textContent = cfg.serverName || ''; })();CSS 里用变量承接颜色:
:root { --progress-color: #ffc107; --progress-bg: rgba(255, 255, 255, 0.15); } .bar { background: var(--progress-bg); } #barInner { background: var(--progress-color); }这套做法的好处是主题和逻辑彻底解耦。换配色不用碰 JS,换文案不用碰 HTML。字段设计上有几个值得注意的点:mottos是数组,页面轮播取用;stageTexts的 key 必须和游戏下发的 stage 字符串完全一致,否则匹配不上;progress.fallbackMax建议低于 100,避免假进度顶到满值干扰完成判断。
| 配置项 | 类型 | 作用 | 注意点 |
|---|---|---|---|
| serverName | string | 页面主标题 | 别写太长,窄屏会折行 |
| mottos | string[] | 轮播提示语 | 文案里注意引号转义 |
| stageTexts | object | 英文 stage 映射中文 | key 要与下发值完全一致 |
| smoothFactor | number | 进度条追赶速度 | 0.05~0.2 手感较好 |
| fallbackStart | number | 假进度启动延时 | 不小于 8000ms |
| fallbackMax | number | 假进度上限 | 设 98,别设 100 |
| background.type | string | video/image/gradient | 要和渲染逻辑匹配 |
| theme | object | CSS 变量值 | 变量名与 CSS 一致 |
3.2 平滑进度条:rAF 追赶、防回退与假进度兜底
游戏下发的progress是离散的、跳跃的,直接赋值给宽度会看到进度条一卡一卡。常见做法是用requestAnimationFrame做指数追赶:真实进度作为目标值,显示进度每帧向目标靠拢一段距离。
// 进度条平滑核心 var state = { raw: 0, show: 0, raf: 0 }; var cfg = window.FlexyConfig || {}; window.addEventListener('message', function (event) { var data = event.data || {}; if (data.type !== 'loadingProgress') return; if (typeof data.progress === 'number') { // 防回退:进度只进不退 state.raw = Math.max(state.raw, data.progress); schedule(); } if (data.stage && cfg.stageTexts) { document.getElementById('stage').textContent = cfg.stageTexts[data.stage] || data.stage; } }); function schedule() { if (state.raf) return; state.raf = requestAnimationFrame(tick); } function tick() { var diff = state.raw - state.show; var factor = (cfg.progress && cfg.progress.smoothFactor) || 0.1; state.show += diff * factor; if (Math.abs(diff) < 0.5) state.show = state.raw; document.getElementById('barInner').style.width = state.show.toFixed(2) + '%'; state.raf = 0; if (Math.abs(diff) >= 0.5) schedule(); } // 兜底:长时间没收消息时缓慢补进度,避免玩家以为卡死 var fallbackTimer = setTimeout(function () { var timer = setInterval(function () { if (state.raw >= 100) return clearInterval(timer); state.raw = Math.min( (cfg.progress && cfg.progress.fallbackMax) || 98, state.raw + 2 ); schedule(); }, 300); }, (cfg.progress && cfg.progress.fallbackStart) || 10000);Math.max(state.raw, data.progress)是关键:某些阶段游戏可能先发高值再小幅回退,直接赋值会导致进度条倒着走,给玩家“出 bug”的感觉。schedule用state.raf做锁,保证同一时间只挂一个动画帧循环。假进度是兜底不是造假,上限压在 98,给玩家“还在动”的反馈;真正到 100 必须由真实消息触发,否则会出现进度满但还没进游戏的状态。
参数调整建议:smoothFactor越小动画越绵,但会显得进度条反应慢;fallbackStart我一般设 10000ms 以上,正常加载过程中有些阶段本来就不发消息,设太短会把正常停顿误判成卡死。这些参数没玄学,都要在真实服务器上观察一轮再定。
3.3 背景 video/image/gradient 三选一与自动降级
多背景轮换是这类模板最常见的卖点。background.list传数组就随机选一个,type决定渲染方式。降级顺序是 video → image → gradient,任何一个环节失败都不能让页面白屏。
function setupBackground(bg) { if (!bg) return; var body = document.body; if (bg.type === 'video' && bg.list && bg.list.length) { var video = document.createElement('video'); video.autoplay = true; video.muted = true; video.loop = true; video.poster = bg.poster || ''; video.src = bg.list[Math.floor(Math.random() * bg.list.length)]; video.addEventListener('error', function () { applyImage(bg.fallback); }); video.addEventListener('canplay', function () { video.play().catch(function () { applyImage(bg.fallback); }); }); body.appendChild(video); return; } if (bg.type === 'image') { applyImage((bg.list && bg.list[0]) || bg.fallback); return; } body.classList.add('gradient-bg'); } function applyImage(src) { var img = new Image(); img.onload = function () { document.body.style.backgroundImage = 'url(' + src + ')'; document.body.style.backgroundSize = 'cover'; document.body.style.backgroundPosition = 'center'; }; img.src = src; }视频必须muted才能自动播放,这是 CEF 自动播放策略决定的。poster是视频首帧加载出来前的占位图,没有它会出现一段黑背景。error事件兜住文件损坏、路径写错的情况;canplay后play()返回的 Promise 拒绝也要处理,比如解码性能不够时视频播不起来,此时直接降级到静态图。生产环境建议视频统一用 MP4 加 H.264 编码,码率控制在 2~3Mbps,1080p 足够,别上 4K。降级函数里要先移除已经插入的 video 节点,否则黑屏底下还挂着一个解码中的视频在消耗 CPU。
3.4 轮播提示语与 stage 文案本地化
提示语轮播用setInterval实现,淡入淡出用 CSSopacity过渡,不要用display控制,display没有过渡效果。stage 文案除了精确匹配,还要做前缀匹配,因为游戏某些版本会下发带序号或附加说明的 stage 字符串。
var mottoEl = document.getElementById('motto'); var mottos = cfg.mottos || ['Loading...']; var index = 0; setInterval(function () { index = (index + 1) % mottos.length; mottoEl.textContent = mottos[index]; mottoEl.classList.add('fade'); setTimeout(function () { mottoEl.classList.remove('fade'); }, 300); }, cfg.mottoInterval || 4000); function translateStage(stage) { if (!stage || !cfg.stageTexts) return stage; if (cfg.stageTexts[stage]) return cfg.stageTexts[stage]; var keys = Object.keys(cfg.stageTexts); for (var i = 0; i < keys.length; i++) { if (stage.indexOf(keys[i]) === 0) return cfg.stageTexts[keys[i]]; } return stage; }indexOf(keys[i]) === 0是前缀匹配,兜住Loading map (45%)这类带额外信息的字符串。不建议把 stage 全量翻译,因为收集不到完整清单,保留原文兜底反而更稳妥。轮播的fade类在 CSS 里配合transition: opacity 0.3s实现淡入淡出,300ms 后移除类名,避免动画叠加。
4. 加载屏避坑:5 个吞掉玩家耐心的常见问题与排查顺序
4.1 改了代码玩家看到的还是旧页面:CEF 缓存怎么破
现象:服主更新了 HTML 或配置,自己重进服务器,加载屏还是老样子。
原因:加载屏资源在玩家本地 FiveM 缓存里有旧副本,CEF 按 URL 做缓存,文件名没变就命中旧文件。很多服主直接在资源目录覆盖文件,内容变了但地址没变,于是本地缓存一直不失效。
解决:改完 UI 后习惯性改文件名,app.js改成app.v2.js并同步更新index.html引用和 manifest 的files列表;或者给引用的资源加查询参数,比如style.css?v=3,查询参数不需要在files里单独声明,最省事。已经分发出去的旧缓存,需要玩家清一次本地缓存才能恢复,所以版本号习惯最好从一开始就养成。
4.2 进度条卡在 90% 不动:字段名与假进度兜底
现象:进度条走到 90% 停住,长时间没变化,玩家开始怀疑服务器崩了。
原因:多数情况不是进度条坏了,而是消息字段判断写错。比如把data.progress写成data.percent,进度条从头到尾就是 0;另一种是某些阶段游戏确实不发消息,比如地图资源下载阶段,最后 10% 可能要等十几秒。
解决:先在app.js里临时加一句console.log(event.data),用 F8 控制台看原始数据结构,确认字段名。然后把 3.2 的假进度兜底上限调到 95 而不是 98,留 5% 的余量给真实完成事件。有一点必须注意:开了loadscreen_manual_shutdown 'yes'时,真实进度不到 100 千万别手动触发cancelLoading,会导致玩家还没进世界就被切换画面。
4.3 加载完成但不进游戏:loadscreen_manual_shutdown 的连锁反应
现象:进度条走满、画面淡出,但屏幕一直黑着,进不去游戏。
原因:开了loadscreen_manual_shutdown 'yes',游戏等页面自己结束,但页面的cancelLoading没有被调用。常见情况是window.cancelLoading没定义,或者进度到 100 后 setTimeout 的调用路径被某个报错中断。
解决:检查两件事:全局window.cancelLoading是否定义;进度满 100 后是否走到了调用它的分支。建议把“进度满”和“结束加载”拆成两个阶段:进度满只播完成动画,动画结束后再调用cancelLoading()。如果不想要这个复杂度,直接把 manifest 里的loadscreen_manual_shutdown 'yes'删掉,让游戏自动收起加载屏,省掉这一层心智负担。
4.4 背景音乐和视频不听话:自动播放策略怎么处理
现象:视频第一次打开不播,音乐不出声;玩家点击页面任意位置后突然正常。
原因:CEF 的自动播放策略要求用户手势。视频加muted属性可以绕过,但音乐一旦静音就没有意义了。
解决:视频用muted加autoplay组合,可以自动播。音乐推荐做一个“点击进入”遮罩:页面先显示一个按钮,点击后同时触发play()和unmute,这个交互顺便作为加载完成的确认入口。
var gate = document.getElementById('soundGate'); if (cfg.music && cfg.music.src) { var audio = new Audio(cfg.music.src); audio.loop = true; audio.volume = (cfg.music && cfg.music.volume) || 0.5; gate.addEventListener('click', function () { audio.play(); gate.style.display = 'none'; }); }参数说明:volume取值 0 到 1,别默认设 1,玩家戴耳机直接会被炸到;音乐文件优先 ogg 或 mp3,体积控制在 5MB 以内,加载屏本身就在下载资源,再拖一个 20MB 的音乐文件纯属给玩家添堵。
4.5 Linux 服务器上的素材 404:路径大小写三处核对
现象:本地窗口调试一切正常,部署到服务器后视频、图片、字体全部加载失败。
原因:服务器是 Linux,路径区分大小写。HTML 里写的assets/Bg.mp4实际文件是assets/bg.mp4,Windows 本地不区分所以能打开,Linux 上 CEF 直接 404。
解决:约定所有资源路径统一小写加连字符,manifest 的files列表、HTML/CSS/JS 里的引用、磁盘上的实际文件名,三处保持一致。上线前在服务器上执行find resources/flexy-loading-screen -type f看真实文件名,再和引用做一次核对,这个习惯能省掉大量莫名其妙的 404。
5. 再进一步:本地调试工作流、低配兼容与上线检查清单
5.1 本地先调好再上服:调试三件套
加载屏是纯前端,没必要每次改样式都重启游戏。我习惯在项目目录起一个本地静态服务器:
cd flexy-loading-screen python3 -m http.server 8080浏览器打开http://localhost:8080,F12 直接调整样式。然后在控制台跑一段模拟消息验证协议链路:
let n = 0; const sim = setInterval(() => { n = Math.min(100, n + 10); window.postMessage({ type: 'loadingProgress', progress: n, stage: 'Loading map' }, '*'); if (n >= 100) clearInterval(sim); }, 400);这段代码在浏览器环境里模拟游戏下发的loadingProgress,进度条动起来、阶段文案换掉、假进度兜底触发,基本就能确认 JS 链路通畅。之后才进游戏验证cancelLoading的调用时机。
5.2 低配玩家的体验:prefers-reduced-motion 与素材分级
加载屏动效越重,低配机器加载越慢,越慢越需要加载屏,这是个恶性循环。用媒体查询把重动画直接降级:
@media (prefers-reduced-motion: reduce) { * { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; } }JS 里同步判断,视频背景换成静态图:
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { setupBackground({ type: 'image', list: ['assets/fallback.jpg'] }); } else { setupBackground(cfg.background); }素材分级也要做:同屏玩家多的社区服,准备 720p 和 1080p 两档视频,按客户端屏幕宽度选择。这一步没法让低配玩家获得高端体验,但能让他们少骂两句。
5.3 上线检查清单
我每次给服务器换加载屏前都会过一遍这个清单:
| 检查项 | 通过标准 |
|---|---|
| manifest 与实际文件一致 | 服务器端 find 核对引用,无 404 |
| 进度条协议 | 进服看到 loadingProgress 正常驱动 |
| cancelLoading 时机 | 所有玩家能正常进游戏,不黑屏 |
| 视频与音乐降级 | 素材损坏时降级到静态图,不白屏 |
| 缓存版本号 | 资源带版本参数,可强制刷新分发 |
第一次做加载屏时我被“进度满但黑屏”坑过一次,排查半天发现是loadscreen_manual_shutdown 'yes'开着但cancelLoading没被调用。后来把这份清单固定下来,每次上线前照做,再没在这个方向上翻过车。希望帮到你。
本文还有配套的精品资源,点击获取