返回博客
Tutorial3 分钟阅读

FiveM 自动售货机脚本:食品、饮料及更多

为您的FiveM服务器添加工作自动售货机。零食、饮料、自定义物品、道具摆放以及几分钟内安装的顶级售货机脚本。

Agency Scripts

Agency Scripts 创始人兼首席开发者

为什么自动售货机为你的服务器增添深度

自动售货机和自动商店系统在任何角色扮演经济中都扮演着关键角色。它们提供全天候访问必需品,如食物、水、香烟和基本用品,无需店员在线。除了便利性,它们还创造了玩家聚集的自然场所,促成加油站、医院和办公楼的自发角色扮演互动。一个良好实现的自动售货机系统远不止简单的购买菜单。它包括真实的购买动画、库存管理以制造稀缺性、根据供需动态调整价格、不同类型机器销售不同类别产品、用于犯罪角色扮演的抢劫机制,以及为玩家提供有意义工作的补货任务。本教程涵盖从道具检测到库存集成和经济管理的完整自动售货机和商店系统构建。

检测和配置自动售货机道具

GTA V的世界充满了自动售货机道具,您可以将它们变成交互式物体。关键道具模型包括 prop_vend_soda_01prop_vend_soda_02 用于汽水机, prop_vend_water_01 用于饮水机, prop_vend_coffe_01 用于咖啡机,以及 prop_vend_snak_01 用于零食机。与其在地图上的每个机器位置手动放置交互点,不如使用基于半径的检测系统扫描玩家附近的这些道具模型。当玩家接近任何匹配的道具且在交互范围内时,系统激活提示或目标区域。这种方法自动适用于整个地图上这些道具的每个实例,无需手动配置,甚至能识别包含这些模型的自定义MLO内部。为每种道具类型定义不同的产品目录,因此汽水机只卖饮料,零食机卖食品,咖啡机卖咖啡品种。

-- shared/config.lua
Config = {}

Config.InteractDistance = 1.5
Config.UseAnimation     = true
Config.AnimDuration     = 3000 -- ms

Config.Machines = {
    soda = {
        models = {
            `prop_vend_soda_01`,
            `prop_vend_soda_02`,
        },
        label = 'Soda Machine',
        items = {
            {name = 'cola',       label = 'Cola',        price = 5,  stock = 20},
            {name = 'sprunk',     label = 'Sprunk',      price = 5,  stock = 20},
            {name = 'ecola',      label = 'E-Cola',      price = 6,  stock = 15},
            {name = 'energydrink',label = 'Energy Drink', price = 8,  stock = 10},
        },
    },
    snack = {
        models = {`prop_vend_snak_01`},
        label = 'Snack Machine',
        items = {
            {name = 'chips',      label = 'Chips',       price = 4,  stock = 25},
            {name = 'candy',      label = 'Candy Bar',   price = 3,  stock = 30},
            {name = 'sandwich',   label = 'Sandwich',    price = 7,  stock = 15},
            {name = 'granola',    label = 'Granola Bar',  price = 5,  stock = 20},
        },
    },
    coffee = {
        models = {`prop_vend_coffe_01`},
        label = 'Coffee Machine',
        items = {
            {name = 'coffee',     label = 'Black Coffee', price = 4,  stock = 30},
            {name = 'latte',      label = 'Latte',       price = 6,  stock = 20},
            {name = 'espresso',   label = 'Espresso',    price = 5,  stock = 25},
        },
    },
    water = {
        models = {`prop_vend_water_01`},
        label = 'Water Cooler',
        items = {
            {name = 'water',      label = 'Water Bottle', price = 3,  stock = 40},
        },
    },
}

客户端交互与动画系统

当玩家接近自动售货机并按下交互键时,依次发生多项操作以营造沉浸式购买体验。首先限制玩家移动,防止动画中途走开。然后玩家角色转向面对售货机,使用 TaskTurnPedToFaceEntity. 购买时会播放一个动画,使用的是 MINI@SPRUNK 用于苏打机的动画字典或 ANIM@AM_HOLD_UP@MALE 用于一般交互。机器本身使用 PlaySoundFromEntity 模拟机械发放声音。动画完成后,客户端向服务器发送购买请求,服务器验证交易并将物品添加到玩家库存。如果机器有投币口交互,使用动画期间在玩家手中添加小道具附件 AttachEntityToEntity 用于额外视觉细节。整个序列应约3秒,足够真实但不致令玩家感到沮丧。

-- client/interaction.lua
local isBuying = false

function PurchaseFromMachine(machineEntity, machineType, itemIndex)
    if isBuying then return end
    isBuying = true

    local ped = PlayerPedId()
    local machineCoords = GetEntityCoords(machineEntity)

    -- Face the machine
    TaskTurnPedToFaceEntity(ped, machineEntity, 1000)
    Wait(1000)

    -- Play animation
    if Config.UseAnimation then
        RequestAnimDict('mini@sprunk')
        while not HasAnimDictLoaded('mini@sprunk') do Wait(10) end

        TaskPlayAnim(ped, 'mini@sprunk', 'plyr_buy_drink_pt1',
            8.0, -8.0, 2000, 0, 0, false, false, false)
        Wait(1500)

        TaskPlayAnim(ped, 'mini@sprunk', 'plyr_buy_drink_pt2',
            8.0, -8.0, 1500, 0, 0, false, false, false)

        -- Machine sound effect
        PlaySoundFromEntity(-1, 'vending_machine_purchase',
            machineEntity, 'dlc_vw_table_games', false, 0)
        Wait(1500)
    end

    -- Request purchase from server
    TriggerServerEvent('vendingmachine:purchase', machineType, itemIndex)

    ClearPedTasks(ped)
    isBuying = false
end

服务器端库存管理和经济系统

库存管理将自动售货机从无限物品分发器转变为动态经济元素。每个机器位置独立跟踪其库存。当机器某种物品售罄时,玩家在购买菜单中会看到该物品标记为售罄。库存水平在数据库中跨服务器重启持续保存。补货工作允许玩家作为送货司机,从仓库取货并将物资运送到城市中空的机器。这形成了完整的经济循环:供应商补货,机器向消费者销售,收入流回机器所有者,送货司机获得工资。动态定价增加了另一层,当库存减少时,热门物品价格逐渐上涨,补货时价格重置。这自然平衡了消费模式,并在机器某种受欢迎物品库存不足时产生紧迫感。

-- server/stock.lua
local machineStock = {}

function GetMachineKey(machineType, coords)
    return machineType .. ':' ..
        math.floor(coords.x) .. ':' ..
        math.floor(coords.y) .. ':' ..
        math.floor(coords.z)
end

function GetStock(machineKey, itemIndex)
    if not machineStock[machineKey] then
        -- Load from database or initialize defaults
        machineStock[machineKey] = {}
    end
    return machineStock[machineKey][itemIndex] or
        Config.Machines[machineKey:match('^(%w+):')].items[itemIndex].stock
end

RegisterNetEvent('vendingmachine:purchase', function(machineType, itemIndex)
    local src = source
    local config = Config.Machines[machineType]
    if not config or not config.items[itemIndex] then return end

    local item = config.items[itemIndex]
    local machineKey = machineType -- simplified; use coords in production

    -- Check stock
    local stock = GetStock(machineKey, itemIndex)
    if stock <= 0 then
        TriggerClientEvent('ox_lib:notify', src, {
            title = 'Sold Out',
            description = item.label .. ' is out of stock',
            type = 'error'
        })
        return
    end

    -- Calculate dynamic price
    local basePrice = item.price
    local stockRatio = stock / item.stock
    local dynamicPrice = math.ceil(basePrice * (1 + (1 - stockRatio) * 0.5))

    -- Deduct money
    local paid = exports['framework']:RemoveMoney(src, dynamicPrice, 'cash')
    if not paid then
        TriggerClientEvent('ox_lib:notify', src, {
            title = 'No Cash',
            description = 'You need $' .. dynamicPrice,
            type = 'error'
        })
        return
    end

    -- Add item to inventory
    local added = exports['ox_inventory']:AddItem(src, item.name, 1)
    if not added then
        exports['framework']:AddMoney(src, dynamicPrice, 'cash')
        return
    end

    -- Decrease stock
    machineStock[machineKey] = machineStock[machineKey] or {}
    machineStock[machineKey][itemIndex] = stock - 1

    TriggerClientEvent('ox_lib:notify', src, {
        title = 'Purchased',
        description = item.label .. ' - $' .. dynamicPrice,
        type = 'success'
    })
end)

犯罪角色扮演的抢劫机制

自动售货机是犯罪角色的自然抢劫目标。实现开锁或撬棍机制,让罪犯破坏机器偷取现金。抢劫需耗时,要求类似开锁谜题的小游戏,产生噪音警告附近玩家并可能触发警察派遣,奖励基于机器自上次补货或抢劫以来处理的购买次数而变化。根据实际玩家购买跟踪每台机器的现金积累,使抢劫奖励真实且与经济活动相关。抢劫成功后,机器进入损坏状态,无法处理购买,直到机械师修理或冷却期后自动恢复。此机制为犯罪活动带来后果,影响更广泛社区,并为机械师创造维修工作,进一步丰富经济生态。

玩家拥有的商店和自定义店面

将自动售货机框架扩展为玩家拥有企业的完整销售点系统。业主可在其地产放置定制店铺柜台,配置销售物品、设定价格,并通过管理 NUI 管理库存。店铺系统使用与自动售货机相同的购买和库存机制,但增加业主层,包含收入跟踪、费用管理、员工权限和利润报告。打卡上班的员工可访问收银机手动处理销售,支持角色扮演互动;无人时自动系统处理销售。自动销售收入进入企业银行账户,扣除可配置的税率,税收流入服务器政府财政。这构建了完整零售经济,玩家可制造或采购商品,补货店铺,价格与其他企业竞争,并从投资中获得被动收入。

性能优化与目标集成

每帧扫描道具模型会严重影响服务器性能。应采用分层检测方法。每 2 秒在 15 米半径内进行广泛扫描,寻找附近的自动售货机道具。缓存发现的机器位置,后续帧仅对缓存进行精确距离检查。当玩家距离任何缓存机器超过 20 米时,清除缓存并触发新扫描。对于使用 ox_target 或 qb-target 等目标系统的服务器,应在发现的道具上注册目标区域,而非使用接近提示。目标区域性能更佳,仅在玩家瞄准对象时激活,完全消除持续距离检查需求。目标方法还提供更清晰的 UI 体验,带有上下文相关的交互选项,直接显示在机器模型上,而非浮动文本提示。

-- client/target.lua (ox_target integration)
local registeredMachines = {}

CreateThread(function()
    for machineType, config in pairs(Config.Machines) do
        for _, model in ipairs(config.models) do
            exports.ox_target:addModel(model, {
                {
                    name     = 'vending_' .. machineType,
                    icon     = 'fas fa-shopping-cart',
                    label    = 'Use ' .. config.label,
                    onSelect = function(data)
                        OpenMachineMenu(machineType, data.entity)
                    end,
                    distance = Config.InteractDistance,
                },
            })
        end
    end
end)

准备好开始了吗?

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