FiveM CreateThread и Wait(0) объяснены: почему ваш скрипт стоит ms

Как работают CreateThread, Wait и SetTimeout в FiveM Lua, почему Wait(0) в каждом цикле показывается в resmon и почему цикл while без Wait зависает игру.

Вы пишете цикл, чтобы что-то проверить, а игра перестаёт отвечать, или resmon показывает ваш ресурс на несколько ms, пока вы стоите:

lua
while true do
    -- no Wait here: the game never gets control back
    local ped = PlayerPedId()
end

В этой статье объясняется, как работают потоки и Wait в FiveM Lua, что действительно стоит Wait(0) и как писать циклы, которые остаются дешёвыми.

Как работают потоки в FiveM

Lua в FiveM кооперативна. Ваш скрипт не выполняется параллельно с игрой: он выполняется, когда его вызывает runtime, и должен вернуть управление. Две вещи делают это возможным:

  • Обработчики событий и обратные вызовы выполняются один раз от начала до конца, затем возвращаются.
  • Потоки, созданные с помощью CreateThread, — это маленькие сопрограммы, которые планировщик возобновляет снова и снова. Поток возвращает управление, когда вызывает Wait.
lua
CreateThread(function()
    while true do
        Wait(1000)
        print('one second later')
    end
end)

CreateThread и Citizen.CreateThread — это одна и та же функция, а также Wait и Citizen.Wait. Используйте то, что вам нравится, но будьте последовательны в одном скрипте.

Цикл while без Wait зависает игру

Если поток никогда не вызывает Wait, планировщик никогда не получает управление обратно. На клиенте игра перестаёт рисовать кадры до завершения цикла, а на сервере все остальные скрипты ждут вашего.

lua
CreateThread(function()
    while true do
        -- missing Wait: this blocks the whole client
    end
end)

То же самое происходит с циклом repeat … until или длинным циклом for, который выполняет тяжёлую работу. Каждый цикл, который может работать какое-то время, нуждается в Wait внутри него. Если скрипт зависает игру при запуске, сначала ищите такой цикл.

Что стоит Wait(0)

Wait(0) означает: пауза до следующего кадра (клиент) или следующего тика (сервер), затем продолжить. Цикл выполняется снова на каждом кадре, поэтому при 60 fps его тело платится 60 раз в секунду.

lua
CreateThread(function()
    while true do
        Wait(0)
        local coords = GetEntityCoords(PlayerPedId())
        local dist = #(coords - vector3(215.0, -810.0, 30.7))
        if dist < 3.0 then
            -- draw a marker, show text...
        end
    end
end)

Один такой цикл стоит мало. Десять из них, в тридцати ресурсах, складываются на несколько ms клиентского времени, что именно resmon показывает высокой ms. Стоимость — не сам Wait(0), а всё, что вы повторяете каждый кадр.

Wait только так коротко, как вам нужно

Большинство проверок не нужно выполнять каждый кадр. Спросите, для чего нужен цикл:

Что делает цикл Разумное ожидание
Нарисовать маркер или текст, обработать нажатие клавиши 0 (только пока игрок близко)
Проверить расстояние, чтобы решить, близко ли что-то 250 to 1000
Опросить состояние, которое редко меняется (работа, автомобиль) 1000 или больше, или используйте событие

Обычный трюк — две скорости: медленный цикл, который проверяет расстояние, и быстрый только когда игрок находится внутри области.

lua
local zone = vector3(215.0, -810.0, 30.7)

CreateThread(function()
    while true do
        local sleep = 1000
        local dist = #(GetEntityCoords(PlayerPedId()) - zone)

        if dist < 10.0 then
            sleep = 0
            DrawMarker(1, zone.x, zone.y, zone.z - 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
                1.5, 1.5, 0.5, 255, 255, 255, 120, false, false, 2, false, nil, nil, false)
        end

        Wait(sleep)
    end
end)

Далеко от зоны поток пробуждается один раз в секунду. Рядом с ней он рисует каждый кадр. Resmon падает почти до нуля для большинства игроков.

Совет: перед тем как обратиться к более быстрому циклу, спросите, может ли событие или state bag сказать вам, когда значение меняется. Вообще никакого цикла — дешевле, чем любой Wait.

SetTimeout: запустить что-то один раз, позже

Если вам нужно сделать что-то только один раз после задержки, вам не нужен поток с Wait:

lua
SetTimeout(5000, function()
    print('five seconds later')
end)

SetTimeout(ms, callback) запускает обратный вызов один раз после задержки и ничего не блокирует. Он также существует на сервере. Для повторяющейся задачи поток с Wait — правильный инструмент, так как SetTimeout не повторяется сам по себе.

Ожидание внутри обработчиков событий

Обработчик событий также может вызывать Wait, потому что FiveM запускает его в своей собственной сопрограмме. Будьте осторожны на сервере: после Wait глобальный source больше не указывает на игрока, который вызвал событие. Сохраните его в первую очередь (см. события клиента и сервера):

lua
RegisterNetEvent('my_script:server:buy', function()
    local src = source
    Wait(500)
    print(('player %s bought something'):format(src))
end)

Остановка потока

Поток заканчивается, когда его функция возвращается. Чтобы остановить цикл снаружи, используйте флаг, который проверяет цикл:

lua
local running = true

CreateThread(function()
    while running do
        Wait(500)
        -- work
    end
end)

AddEventHandler('onResourceStop', function(resource)
    if resource == GetCurrentResourceName() then
        running = false
    end
end)

Все потоки ресурса автоматически останавливаются, когда ресурс останавливается, поэтому вам это нужно только для остановки цикла, пока ресурс продолжает работать.

Чеклист

Симптом Исправление
Игра зависает, когда скрипт запускается Цикл while true do не имеет Wait: добавьте один
Ресурс показывает постоянный ms в resmon во время простоя Замените Wait(0) проверкой расстояния и более длительным ожиданием
Нужно запустить что-то один раз после задержки SetTimeout(ms, fn) вместо потока
source неправильный после Wait на сервере Сохраните local src = source перед Wait
Опрос значения, которое редко меняется Используйте событие или state bag вместо цикла
Citizen.Wait или Wait? Одна и та же функция, выберите один стиль

Короткие ответы

Что делает Wait(0) в FiveM?

Он приостанавливает текущий поток до следующего кадра на клиенте или следующего тика на сервере. Затем цикл выполняется снова на каждом кадре, поэтому всё внутри платится 60 или более раз в секунду.

Отличается ли Citizen.Wait от Wait?

Нет. Wait и Citizen.Wait — это одна и та же функция, а также CreateThread и Citizen.CreateThread. Короткие названия — просто псевдонимы.

Wait останавливает всю игру?

Нет. Он только приостанавливает поток, который его вызывает, а другие потоки и игра продолжают работать. Цикл, который никогда не вызывает Wait, — вот что блокирует всё.

Скрипты без этой проблемы

Advanced BoostingУгон машин на заказ через планшет: контракты от класса D до S+, команды и живая очередь.Смотреть скрипт →CCTV Security CamerasУстанавливаемые камеры, планшет с мультиэкраном в реальном времени и распечатанные фото-улики.Смотреть скрипт →Mic PhoneСкладной телефон, который раскладывается в планшет и работает и на настоящем телефоне игрока.Смотреть скрипт →

Читайте также