Назад к блогу
Tutorial7 мин чтения

Скрипт торгового автомата для FiveM: еда, напитки и другое

Добавьте работающие торговые автоматы на ваш сервер FiveM. Закуски, напитки, кастомные предметы, размещение пропов и лучшие скрипты торговых автоматов для установки за считанные минуты.

Agency Scripts

Основатель и ведущий разработчик Agency Scripts

Почему торговые автоматы добавляют глубину вашему серверу

Торговые автоматы и автоматизированные системы магазинов играют ключевую роль в любой ролевой экономике. Они обеспечивают круглосуточный доступ к необходимым товарам, таким как еда, вода, сигареты и базовые припасы, без необходимости присутствия продавца. Помимо удобства, они создают естественные места сбора игроков, что приводит к спонтанным ролевым встречам на заправках, в больницах и офисных зданиях. Хорошо реализованная система торговых автоматов , это не просто меню покупки. Она включает реалистичные анимации покупки, управление запасами, создающее дефицит, динамическое ценообразование, реагирующее на спрос и предложение, разные типы автоматов с разными категориями товаров, механики ограблений для криминальной ролевой игры и работу по пополнению запасов, дающую игрокам значимую занятость. Этот учебник охватывает создание полной системы торговых автоматов и магазинов от обнаружения объектов до интеграции инвентаря и управления экономикой.

Обнаружение и настройка объектов торговых автоматов

Мир GTA V наполнен объектами торговых автоматов, которые вы можете превратить в интерактивные объекты. Ключевые модели объектов включают prop_vend_soda_01 и prop_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)

Механика ограблений для криминальной ролевой игры

Торговые автоматы , естественная цель для ограблений криминальными персонажами. Реализуйте механику отмычек или монтировки, позволяющую преступникам взламывать автоматы и красть деньги внутри. Ограбление должно занимать время, требовать мини-игру, например, пазл с отмычками, создавать шум, который предупреждает nearby игроков и может вызвать вызов полиции, а также приносить переменную награду в зависимости от количества покупок с момента последнего пополнения или ограбления. Отслеживайте накопление денег в каждом автомате на основе реальных покупок игроков, чтобы награда была реалистичной и связанной с настоящей экономической активностью. После успешного ограбления автомат переходит в повреждённое состояние, в котором не может принимать покупки до ремонта механиком или автоматического восстановления после тайм-аута. Это создаёт последствия для криминальной деятельности, влияющие на сообщество, и генерирует работу для механиков, что дополнительно обогащает экономическую экосистему.

Магазины, принадлежащие игрокам, и индивидуальные витрины

Расширьте систему торговых автоматов в полноценную POS-систему для бизнеса, принадлежащего игрокам. Владельцы бизнеса могут размещать кастомные прилавки на своих объектах, настраивать продаваемые товары, устанавливать собственные цены и управлять запасами через управляющий 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 за поддержкой, обновлениями и анонсами.