Wróć do bloga
Tutorial7 min czytania

Skrypt FiveM na automaty: Jedzenie, napoje i więcej

Dodaj działające automaty z przekąskami do swojego serwera FiveM. Przekąski, napoje, customowe przedmioty, stawianie propów i najlepsze skrypty automatów do zainstalowania w kilka minut.

Agency Scripts

Założyciel i główny deweloper w Agency Scripts

Dlaczego automaty vendingowe dodają głębi Twojemu serwerowi

Automaty z przekąskami i zautomatyzowane systemy sklepowe odgrywają kluczową rolę w każdej gospodarce roleplay. Zapewniają całodobowy dostęp do niezbędnych przedmiotów, takich jak jedzenie, woda, papierosy i podstawowe zapasy, bez konieczności obecności sprzedawcy na serwerze. Poza wygodą tworzą naturalne punkty spotkań graczy, prowadząc do spontanicznych akcji roleplay na stacjach benzynowych, w szpitalach i biurowcach. Dobrze wdrożony system automatów wykracza daleko poza proste menu zakupu. Obejmuje realistyczne animacje kupowania, zarządzanie zapasami tworzące deficyt towarów, dynamiczne ceny reagujące na podaż i popyt, różne typy automatów sprzedające odmienne kategorie produktów, mechanikę napadów dla przestępczego RP oraz zlecenia uzupełniania towaru dające graczom sensowne zatrudnienie. W tym poradniku omawiamy budowę kompletnego systemu automatów i sklepów: od wykrywania propów, przez integrację z ekwipunkiem, po zarządzanie ekonomią.

Wykrywanie i konfigurowanie propów automatów

Świat GTA V jest pełen propów automatów z przekąskami i napojami, które możesz przekształcić w interaktywne obiekty. Kluczowe modele propów obejmują prop_vend_soda_01 i prop_vend_soda_02 do automatów z napojami, prop_vend_water_01 do dystrybutorów wody, prop_vend_coffe_01 dla ekspresów do kawy, a prop_vend_snak_01 do automatów z przekąskami. Zamiast ręcznie umieszczać punkty interakcji przy każdym automacie na mapie, użyj systemu wykrywania opartego na promieniu, który skanuje otoczenie gracza pod kątem tych modeli propów. Gdy gracz zbliży się do pasującego propa w zasięgu interakcji, system aktywuje prompt lub strefę target. Takie podejście działa automatycznie z każdą instancją tych propów na całej mapie bez ręcznej konfiguracji, a nawet wykrywa niestandardowe wnętrza MLO zawierające te modele. Zdefiniuj różne katalogi produktów dla każdego typu propa, aby automaty z napojami sprzedawały tylko napoje, automaty z przekąskami jedzenie, a ekspresy do kawy warianty kawy.

-- 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},
        },
    },
}

System interakcji i animacji po stronie klienta

Gdy gracz podchodzi do automatu z przekąskami i naciska klawisz interakcji, po kolei następuje kilka zdarzeń tworzących realistyczne wrażenia z zakupu. Najpierw ruch gracza zostaje zablokowany, aby uniemożliwić odejście w trakcie animacji. Następnie postać gracza obraca się przodem do automatu za pomocą TaskTurnPedToFaceEntity. Odtwarzana jest animacja zakupu przy użyciu MINI@SPRUNK słownik animacji dla automatów z napojami lub ANIM@AM_HOLD_UP@MALE do ogólnych interakcji. Sama maszyna odtwarza efekt dźwiękowy przy użyciu PlaySoundFromEntity aby zasymulować mechaniczny dźwięk wydawania towaru. Po zakończeniu animacji klient wysyła żądanie zakupu do serwera, który weryfikuje transakcję i dodaje przedmiot do ekwipunku gracza. Jeśli automat posiada interakcję z wrzucaniem monet, dodaj mały prop przyczepiany do dłoni gracza podczas animacji za pomocą AttachEntityToEntity dla dodatkowego realizmu wizualnego. Cała sekwencja powinna trwać około 3 sekund: wystarczająco długo, by wyglądać realistycznie, ale wystarczająco krótko, by nie frustrować graczy.

-- 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

Zarządzanie stanem magazynowym i gospodarką po stronie serwera

Zarządzanie stanem magazynowym przekształca automaty z nielimitowanych dystrybutorów przedmiotów w dynamiczne elementy gospodarki. Każda lokalizacja automatu śledzi swój stan magazynowy niezależnie. Gdy w automacie zabraknie danego przedmiotu, gracze widzą go w menu zakupu jako wyprzedany. Stany magazynowe są zapisywane w bazie danych i zachowywane po restartach serwera. Praca zaopatrzeniowca pozwala graczom pracować jako dostawcy, którzy odbierają towary z magazynu i rozwożą je do pustych automatów w całym mieście. Tworzy to pełny obieg gospodarczy: dostawcy uzupełniają automaty, automaty sprzedają klientom, przychód trafia do właściciela automatu, a kierowca dostawczy otrzymuje wynagrodzenie. Dynamiczne ceny dodają kolejny poziom, popularne przedmioty stopniowo drożeją w miarę spadku zapasów, a ceny resetują się po uzupełnieniu automatu. W naturalny sposób równoważy to wzorce konsumpcji i wywołuje poczucie pilności, gdy w automacie kończy się pożądany towar.

-- 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)

Mechaniki napadów dla przestępczego roleplayu

Automaty są naturalnym celem napadów dla postaci ze świata przestępczego. Wdróż mechanikę wytrychu lub łomu, która pozwala przestępcom włamywać się do maszyn i kraść znajdującą się wewnątrz gotówkę. Napad powinien zajmować czas, wymagać minigry, np. zagadki z wytrychem, generować hałas alarmujący pobliskich graczy i potencjalnie wywoływać zgłoszenie do policji, a także dawać zmienną nagrodę zależną od liczby transakcji zrealizowanych przez automat od ostatniego uzupełnienia lub napadu. Śledź gromadzenie gotówki w każdym automacie na podstawie faktycznych zakupów graczy, aby łup z napadu był realistyczny i powiązany z realną aktywnością ekonomiczną. Po udanym napadzie automat przechodzi w stan uszkodzenia, w którym nie może realizować zakupów, dopóki nie zostanie naprawiony przez mechanika lub automatycznie po upływie czasu odnowienia. Tworzy to konsekwencje działalności przestępczej wpływające na całą społeczność oraz generuje zlecenia naprawy dla mechaników, dodatkowo wzbogacając ekosystem ekonomiczny.

Sklepy graczy i niestandardowe witryny

Rozbuduj framework automatów z przekąskami w pełny system kasowy dla firm należących do graczy. Właściciele firm mogą stawiać własne lady sklepowe na swoich posesjach, konfigurować sprzedawane przedmioty, ustalać własne ceny i zarządzać ekwipunkiem za pomocą interfejsu NUI. System sklepu korzysta z tej samej bazowej mechaniki zakupu i magazynu co automaty, ale dodaje warstwę właściciela ze śledzeniem przychodów, zarządzaniem wydatkami, uprawnieniami pracowników i raportami zysków. Pracownicy na służbie mogą korzystać z kasy, aby ręcznie realizować sprzedaż na potrzeby interakcji roleplay, podczas gdy zautomatyzowany system obsługuje sprzedaż pod nieobecność pracowników. Przychody z automatycznej sprzedaży trafiają na konto bankowe firmy po potrąceniu konfigurowalnego podatku procentowego, który zasila skarbiec rządu serwera. Tworzy to kompletną gospodarkę detaliczną, w której gracze wytwarzają lub pozyskują towary, zaopatrują swoje sklepy, konkurują cenowo z innymi firmami i generują dochód pasywny ze swojej inwestycji.

Optymalizacja wydajności i integracja z targetem

Skanowanie modeli propów w każdej klatce zniszczyłoby wydajność serwera. Zamiast tego zastosuj wielopoziomowe podejście do wykrywania. Uruchamiaj szerokie skanowanie co 2 sekundy w promieniu 15 metrów, aby znaleźć pobliskie automaty. Zapisuj lokalizacje wykrytych maszyn w pamięci podręcznej i w kolejnych klatkach wykonuj tylko precyzyjne sprawdzanie odległości względem cache. Gdy gracz oddali się na ponad 20 metrów od dowolnego automatu w pamięci podręcznej, wyczyść cache i uruchom ponowne skanowanie. W przypadku serwerów korzystających z systemów target, takich jak ox_target lub qb-target, rejestruj strefy target na wykrytych propach zamiast używać komunikatów zbliżeniowych. Strefy target są bardziej wydajne, ponieważ aktywują się tylko wtedy, gdy gracz celuje w obiekt, co całkowicie eliminuje potrzebę ciągłego sprawdzania odległości. Podejście oparte na target zapewnia również bardziej przejrzysty interfejs użytkownika z kontekstowymi opcjami interakcji, które pojawiają się bezpośrednio na modelu automatu, a nie jako unoszące się komunikaty tekstowe.

-- 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)

Pasujący skrypt

Gotowe na twój serwer, aktualizacje w zestawie.

Gotowy, aby zacząć?

Kup skrypty w naszym sklepie lub dołącz do Discorda, aby uzyskać wsparcie, aktualizacje i sprawdzić nadchodzące nowości.