返回博客
Tutorial1 分钟阅读

FiveM 赛车系统:赛道、排行榜与投注

为您的FiveM服务器添加街头赛车。赛道构建器、检查点、赌注、排行榜以及适合严肃改装社区的最佳赛车脚本。

Agency Scripts

Agency Scripts 创始人兼首席开发者

设计竞赛创建系统

引人入胜的街头赛车系统始于赋予玩家创建和管理自定义赛道的能力。最佳赛车脚本允许组织者通过驾驶路线动态放置检查点,而非仅依赖预设赛道。每场比赛存储为包含唯一标识符、创建者玩家 ID、名称、有序检查点坐标及朝向列表,以及距离和预计圈速等元数据的数据结构。创建过程直观:玩家进入赛道编辑模式,驾驶至每个检查点位置,按键确认放置,最后返回起点完成。保存完成的赛道至数据库,确保跨服务器重启持久并可共享。实现审核系统,管理员可批准玩家创建的赛道后公开,防止通过不可能或不公平的赛道设计恶意破坏。

检查点系统和竞赛逻辑

检查点是验证比赛进度和防止抄近路的核心机制。每个检查点应定义为具有可配置半径的3D区域,通常根据道路宽度在8到15米之间。使用GTA原生标记绘制函数直观显示检查点,当前检查点用亮色高亮,下一个检查点显示为透明预览。通过索引检查点列表跟踪每个赛车手的进度,仅当他们按顺序进入正确的下一个检查点区域时才推进。通过跟踪检查点间时间实施反作弊措施,标记或取消资格那些完成赛段速度不可能的赛车手,这表明瞬移或速度作弊。

-- Client-side checkpoint detection
local currentCheckpoint = 1
local raceCheckpoints = {} -- populated when race starts

CreateThread(function()
    while isRacing do
        local playerCoords = GetEntityCoords(PlayerPedId())
        local checkpoint = raceCheckpoints[currentCheckpoint]

        if checkpoint then
            local dist = #(playerCoords - checkpoint.coords)

            -- Draw current checkpoint marker
            DrawMarker(1, checkpoint.coords.x, checkpoint.coords.y,
                checkpoint.coords.z - 1.0, 0, 0, 0, 0, 0, 0,
                checkpoint.radius * 2, checkpoint.radius * 2, 2.0,
                45, 212, 191, 120, false, true, 2, false, nil, nil, false)

            if dist < checkpoint.radius then
                PlaySoundFrontend(-1, 'CHECKPOINT_NORMAL', 'HUD_MINI_GAME_SOUNDSET', true)
                currentCheckpoint = currentCheckpoint + 1

                if currentCheckpoint > #raceCheckpoints then
                    -- Race finished
                    local finishTime = GetGameTimer() - raceStartTime
                    TriggerServerEvent('racing:finished', raceId, finishTime)
                    isRacing = false
                else
                    -- Notify server of checkpoint progress
                    TriggerServerEvent('racing:checkpoint', raceId, currentCheckpoint)
                end
            end
        end
        Wait(50)
    end
end)

排行榜和排名系统

排行榜激发竞争,促使玩家不断回归提升成绩。将比赛结果存储在数据库表中,记录玩家标识、比赛ID、完成时间(毫秒)、使用的车辆模型、日期及是否为个人最佳。通过NUI界面展示排行榜,显示每条赛道的最佳成绩,并可按车辆类别筛选,方便玩家在跑车、超级车、肌肉车或紧凑型等类别内公平比较。实现定期重置的赛季排名系统,保持竞争新鲜感,同时维护历史最高记录榜以供炫耀。基于多人比赛中的一对一表现计算ELO风格的赛车评级,给予技术高超的赛车手可见排名,击败排名更高对手时排名上升,输给排名较低对手时下降。在比赛UI显示该评级,并用于随机比赛的可选匹配。

投注与奖品系统

投注系统为比赛增添财务赌注,将赛车场景与服务器更广泛的经济体系整合。允许赛事组织者设定所有参与者必须支付的报名费,总奖金池根据可配置的分配比例(如第一名70%、第二名20%、第三名10%)分配给前三名。实现观众投注系统,非赛车玩家可下注支持的车手。所有财务交易仅用服务器端逻辑防止作弊。对所有投注收取5-10%的抽水,作为经济资金回收。大型赛事允许管理员从服务器资金池添加额外奖金。跟踪每位玩家的投注统计,检测并防止操纵比赛行为,如车手在大量投注反对时持续输掉比赛。

-- Server-side betting handler
local raceBets = {}

RegisterNetEvent('racing:placeBet')
AddEventHandler('racing:placeBet', function(raceId, targetRacerId, amount)
    local src = source
    if not activeRaces[raceId] then return end
    if activeRaces[raceId].state ~= 'lobby' then
        return notify(src, 'Betting is closed once the race starts.', 'error')
    end

    -- Validate the target racer is in this race
    local racerFound = false
    for _, racer in ipairs(activeRaces[raceId].participants) do
        if racer.src == targetRacerId then racerFound = true break end
    end
    if not racerFound then return end

    -- Check and deduct funds
    local playerMoney = GetPlayerMoney(src)
    if playerMoney < amount or amount < Config.MinBet then
        return notify(src, 'Insufficient funds or below minimum bet.', 'error')
    end

    RemoveMoney(src, amount)

    if not raceBets[raceId] then raceBets[raceId] = {} end
    table.insert(raceBets[raceId], {
        bettor = src,
        target = targetRacerId,
        amount = amount,
    })

    notify(src, ('Bet $%s on %s'):format(amount, GetPlayerName(targetRacerId)), 'success')
end)

使用 NUI 构建比赛 UI

赛车UI需要传达关键实时信息,同时不使屏幕杂乱或分散驾驶员注意力。设计一个简洁的HUD,显示当前赛车位置、圈数(如适用)、与个人最好成绩或赛道记录的分段计时,以及突出显示即将到达检查点方向的迷你地图叠加。使用符合比赛主题的速度计,根据玩家偏好显示mph或km/h。赛前大厅屏幕应显示所有注册参与者及其车辆、赛道名称和距离、奖金池分配及倒计时。使用HTML、CSS和JavaScript通过NUI系统构建UI,并通过客户端Lua向NUI框架传递比赛状态更新, SendNUIMessage. 通过将位置更新限制为每秒 4-5 次而非每帧,保持 UI 响应速度,这显著减少了 NUI 开销,对显示信息无可见影响。考虑添加赛后结果屏幕,显示最高速度、平均速度和每个检查点段的时间对比等详细统计。

比赛类别和车辆等级

将比赛组织成不同类别,确保公平竞争和多样性。根据游戏原生操控数据中的最高速度、加速和操控评级定义车辆类别。为特定比赛设置类别限制,例如Dominator车辆不能参加仅限紧凑型车的比赛,反之亦然。除车辆类别外,提供不同比赛形式:从A点到B点的城市直线冲刺、多圈环形赛道、直线加速赛(反应时间起跑)和以角度、速度及接近剪辑点评分的漂移比赛。每种形式需要不同的计分和完成检测逻辑。漂移评分特别需要自定义算法,评估车辆相对于速度向量的滑移角度、漂移时保持的速度及连续漂移无失控的加成。允许比赛组织者切换追赶机制、穿越其他赛车防止恶意碰撞的幽灵模式,以及天气或时间设置以营造氛围。

警察集成与非法赛车

街头赛车应是一项非法活动,能够与执法部门产生有机互动。当比赛开始时,在可配置的延迟后,生成来自该区域NPC的噪音投诉,这些投诉会出现在警察调度系统中,给赛车手一个领先的起步时间,但确保较长的比赛会吸引注意力。响应的警察可以尝试通过部署钉带、设置路障或追捕单个赛车手来阻止比赛。如果赛车手被警察抓获,他们将面临罚款、车辆扣押,甚至可能入狱,增加真实的后果,使赛车场景感觉既危险又刺激。为赛车专门实现一个通缉等级系统,随着重复违规而升级,使长期街头赛车手吸引更激进的警察反应。允许警察使用已知赛车地点的监控摄像头对组织者进行取证,创建一个调查层面,奖励耐心的警察工作,能够抓获整个赛车团队而非单个司机。

赛车性能优化

赛车系统要求流畅的性能,因为即使是轻微的延迟也会破坏竞技体验。通过在客户端处理检查点检测和位置跟踪,减少比赛进行时服务器与客户端的通信,仅在达到检查点或比赛结束时向服务器发送更新。使用实体状态包进行位置广播,而不是自定义网络事件,因为状态包针对频繁更新进行了优化,并更有效地处理带宽。对于检查点渲染,仅绘制当前和下一个检查点的标记,而不是同时绘制所有检查点,并使用LOD(细节层次)逻辑在较远距离减少标记复杂度。通过批量操作获取排行榜和比赛列表,避免单独查询,并在客户端缓存结果,设置短TTL以减少高峰时段数据库负载。使用最大预期同时比赛人数(通常每场8-16人)测试系统,确保位置同步和检查点验证在不掉帧或不同步的情况下正确扩展。

准备好开始了吗?

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