Skip to content

Instantly share code, notes, and snippets.

@nnsdev
Last active December 11, 2024 14:41

Revisions

  1. nnsdev renamed this gist Mar 21, 2021. 1 changed file with 0 additions and 0 deletions.
  2. nnsdev revised this gist Mar 21, 2021. 5 changed files with 270 additions and 0 deletions.
    203 changes: 203 additions & 0 deletions client.lua
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,203 @@
    local markers = {}
    local CurrentMarker = nil
    local blips = {}
    local drawnMarkers = {}

    local HasAlreadyEnteredMarker

    RegisterNetEvent('disc-base:registerMarker')
    AddEventHandler('disc-base:registerMarker', function(marker)
    if marker.coords == nil then
    print('Needs Coords for marker')
    return
    end
    if marker.shouldDraw == nil then
    marker.shouldDraw = function()
    return true
    end
    end

    if marker.command then
    RegisterCommand(marker.command.key, function(src, args, raw)
    local command = marker.command.key
    if args and marker.command.args then
    command = command .. ' ' .. marker.command.args
    end
    if raw == command then
    TriggerEvent('disc-base:triggerCurrentMarkerAction')
    end
    end)
    end

    local zone = GetZoneAtCoords(marker.coords.x, marker.coords.y, marker.coords.z)

    if markers[zone] == nil then
    markers[zone] = {}
    end

    markers[zone][marker.name] = marker
    end)

    RegisterNetEvent('disc-base:removeMarker')
    AddEventHandler('disc-base:removeMarker', function(name)
    for k, v in pairs(markers) do
    markers[k][name] = nil
    end
    end)

    CreateThread(function ()
    while true do
    Citizen.Wait(500)
    local playerPed = PlayerPedId()
    local coords = GetEntityCoords(playerPed)
    local isInMarker = false
    local lastMarker = nil
    drawnMarkers = {}
    local zone = GetZoneAtCoords(coords.x, coords.y, coords.z)
    if markers[zone] ~= nil then
    for k, v in pairs(markers[zone]) do
    local distance = #(coords - v.coords)
    if distance < Config.DrawDistance and v.shouldDraw() then
    v.distance = distance
    table.insert(drawnMarkers, v)
    end
    end
    end
    end
    end)

    Citizen.CreateThread(function()
    while true do
    local isInMarker = false
    local lastMarker = nil
    if #drawnMarkers > 0 then
    for k, v in pairs(drawnMarkers) do
    if v.show3D then
    if v.distance < Config.Draw3DDistance then
    DrawText3D(v.coords, v.msg, 0.5)
    end
    elseif v.type ~= -1 then
    DrawMarker(v.type, v.coords, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, v.size.x, v.size.y, v.size.z, v.colour.r, v.colour.g, v.colour.b, 100, getOrElse(v.bob, false), true, 2, getOrElse(v.rotate, true), false, false, false)
    end
    if v.distance < v.size.x then
    isInMarker = true
    lastMarker = v
    end
    end
    end

    if isInMarker and not HasAlreadyEnteredMarker then
    HasAlreadyEnteredMarker = true
    TriggerEvent('disc-base:hasEnteredMarker', lastMarker)
    end
    if not hasExited and not isInMarker and HasAlreadyEnteredMarker then
    HasAlreadyEnteredMarker = false
    TriggerEvent('disc-base:hasExitedMarker')
    end

    if CurrentMarker and CurrentMarker.shouldDraw() then
    if not CurrentMarker.show3D and CurrentMarker.msg then
    ShowHelpNotification(CurrentMarker.msg)
    end

    if IsControlJustReleased(0, 38) then
    if CurrentMarker.action ~= nil then
    CurrentMarker.action(CurrentMarker)
    end
    end
    end

    if #drawnMarkers > 0 then
    Citizen.Wait(1)
    else
    Citizen.Wait(500)
    end
    end
    end)

    AddEventHandler('disc-base:hasExitedMarker', function()
    CurrentMarker = nil
    end)

    AddEventHandler('disc-base:hasEnteredMarker', function(marker)
    if marker.show3D then
    PlaySound(GetSoundId(), "SELECT", "HUD_FRONTEND_DEFAULT_SOUNDSET", 0, 0, 1)
    end
    CurrentMarker = marker
    if not CurrentMarker.show3D and CurrentMarker.msg then
    AddTextEntry('esxHelpNotification', CurrentMarker.msg)
    end
    end)

    RegisterNetEvent('disc-base:triggerCurrentMarkerAction')
    AddEventHandler('disc-base:triggerCurrentMarkerAction', function()
    if CurrentMarker and CurrentMarker.action ~= nil then
    CurrentMarker.action(CurrentMarker)
    end
    end)

    RegisterNetEvent('disc-base:registerBlip')
    AddEventHandler('disc-base:registerBlip', function(blip)

    if blip.coords == nil then
    print("Coords needed for Blip")
    return
    end

    local _blip = AddBlipForCoord(blip.coords)
    SetBlipSprite(_blip, getOrElse(blip.sprite, 1))
    SetBlipAsShortRange(_blip, true)
    SetBlipDisplay(_blip, getOrElse(blip.display, 4))

    if blip.scale then
    SetBlipScale(_blip, getOrElse(blip.scale, 0.5))
    end
    SetBlipColour(_blip, getOrElse(blip.colour, 1))
    BeginTextCommandSetBlipName("STRING")
    AddTextComponentString(getOrElse(blip.name, "Blip Missing Name"))
    EndTextCommandSetBlipName(_blip)
    blips[getOrElse(blip.id, #blips + 1)] = {
    _blip = _blip,
    blip = blip
    }
    end)

    RegisterNetEvent('disc-base:updateBlip')
    AddEventHandler('disc-base:updateBlip', function(blip, debug)
    if blip.id == nil or blips[blip.id] == nil then
    return
    end
    local _blip = blips[blip.id]._blip

    if blip.coords then

    if _blip and GetBlipCoords(_blip) ~= blip.coords then
    RemoveBlip(_blip)
    local tempBlip = blips[blip.id].blip
    blips[blip.id] = nil
    tempBlip.coords = blip.coords
    tempBlip.display = blip.display
    TriggerEvent('disc-base:registerBlip', tempBlip)
    return
    end

    end

    if blip.sprite then
    SetBlipSprite(_blip, blip.sprite)
    end
    if blip.display then
    SetBlipDisplay(_blip, blip.display)
    end
    if blip.scale then
    SetBlipScale(_blip, getOrElse(blip.scale, 0.5))
    end
    if blip.colour then
    SetBlipScale(_blip, blip.colour)
    end
    if blip.name then
    BeginTextCommandSetBlipName("STRING")
    AddTextComponentString(blip.name)
    EndTextCommandSetBlipName(_blip)
    end
    end)
    4 changes: 4 additions & 0 deletions config.lua
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,4 @@
    Config = {}

    Config.DrawDistance = 10
    Config.Draw3DDistance = 5
    2 changes: 2 additions & 0 deletions improve_fivem_resources.md
    Original file line number Diff line number Diff line change
    @@ -3,6 +3,8 @@

    This is an example of a resource called "disc-base", I have only taken parts of it and removed the mentions of ESX, because I am running this on an empty server. The part we are looking at here especially is the marker drawing. This is also an outdated version of this resource before they applied performance improvements, but we are also going to go deeper than their changes.

    Base of this tutorial: https://github.com/DiscworldZA/gta-resources/blob/54a2aaf7080286c8d49bbd7b1978b6cc430ec755/disc-base/client/markers.lua (client.lua here)


    # Assessment

    16 changes: 16 additions & 0 deletions spamcreation.lua
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,16 @@
    RegisterCommand('spammarkers', function ()
    local playerPos = GetEntityCoords(PlayerPedId())
    for i = 0, 500, 1 do
    TriggerEvent('disc-base:registerMarker', {
    coords = vector3(playerPos.x + i, playerPos.y + i, playerPos.z),
    action = function ()
    print('trigger action')
    end,
    name = ('marker_%s'):format(i),
    type = 1,
    colour = { r = 55, b = 255, g = 55 },
    size = vector3(0.5, 0.5, 1.0),
    msg = 'test message'
    })
    end
    end, false)
    45 changes: 45 additions & 0 deletions utils.lua
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,45 @@
    function getOrElse(value, default)
    if value ~= nil then
    return value
    else
    return default
    end
    end

    ShowHelpNotification = function(msg, thisFrame, beep, duration)
    if thisFrame then
    DisplayHelpTextThisFrame('esxHelpNotification', false)
    else
    if beep == nil then beep = true end
    BeginTextCommandDisplayHelp('esxHelpNotification')
    EndTextCommandDisplayHelp(0, false, beep, duration or -1)
    end
    end

    DrawText3D = function(coords, text, size, font)
    coords = vector3(coords.x, coords.y, coords.z)

    local camCoords = GetGameplayCamCoords()
    local distance = #(coords - camCoords)

    if not size then size = 1 end
    if not font then font = 0 end

    local scale = (size / distance) * 2
    local fov = (1 / GetGameplayCamFov()) * 100
    scale = scale * fov

    SetTextScale(0.0 * scale, 0.55 * scale)
    SetTextFont(font)
    SetTextColour(255, 255, 255, 255)
    SetTextDropshadow(0, 0, 0, 0, 255)
    SetTextDropShadow()
    SetTextOutline()
    SetTextCentre(true)

    SetDrawOrigin(coords, 0)
    BeginTextCommandDisplayText('STRING')
    AddTextComponentSubstringPlayerName(text)
    EndTextCommandDisplayText(0.0, 0.0)
    ClearDrawOrigin()
    end
  3. nnsdev created this gist Mar 21, 2021.
    356 changes: 356 additions & 0 deletions improve_fivem_resources.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,356 @@
    # How to fix resource performance
    ## Showing by example of "disc-base" exempts

    This is an example of a resource called "disc-base", I have only taken parts of it and removed the mentions of ESX, because I am running this on an empty server. The part we are looking at here especially is the marker drawing. This is also an outdated version of this resource before they applied performance improvements, but we are also going to go deeper than their changes.


    # Assessment

    ![Resmon of disc-base before modifications](https://iam.malding.dev/i/b8123762ec.jpg)

    As of right now, our `client.lua` runs a thread on every game frame checking for marker distance, and looks if any marker is near our maximum render distance set in the config file (30 units).

    *Wihout* having any markers created, the resource already runs at 0.02-0.03ms, but we are not even doing distance checks yet.

    I have created a simple command that just spam creates 500 markers with an offset on each, let's look at the performance now:

    Outside render distance:
    ![Outside render distance](https://iam.malding.dev/i/e72141e5b4.jpg)

    Inside render distance:
    ![Inside render distance](https://iam.malding.dev/i/cc2cad8e60.jpg)

    Inside a marker:
    ![Inside a marker](https://iam.malding.dev/i/80d18aa4ad.png)

    Performance is terrible now, and even without rendering *any* we are above 0.4ms, which is unacceptable.

    Let's look at some code now.

    ```lua
    Citizen.CreateThread(function()
    while true do
    Citizen.Wait(0)
    local playerPed = PlayerPedId()
    local coords = GetEntityCoords(playerPed)
    local isInMarker = false
    local lastMarker = nil
    for k, v in pairs(markers) do
    local distance = GetDistanceBetweenCoords(coords.x, coords.y, coords.z, v.coords.x, v.coords.y, v.coords.z, true)
    if distance < Config.DrawDistance and v.shouldDraw() then
    if v.show3D then
    if distance < Config.Draw3DDistance then
    DrawText3D(v.coords, v.msg, 0.5)
    end
    elseif v.type ~= -1 then
    DrawMarker(v.type, v.coords, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, v.size.x, v.size.y, v.size.z, v.colour.r, v.colour.g, v.colour.b, 100, getOrElse(v.bob, false), true, 2, getOrElse(v.rotate, true), false, false, false)
    end
    end
    if distance < v.size.x and v.shouldDraw() then
    if v.enableE then
    EnableControlAction(0, 38)
    end
    isInMarker = true
    lastMarker = v
    end
    end

    if isInMarker and not HasAlreadyEnteredMarker then
    HasAlreadyEnteredMarker = true
    TriggerEvent('disc-base:hasEnteredMarker', lastMarker)
    end
    if not hasExited and not isInMarker and HasAlreadyEnteredMarker then
    HasAlreadyEnteredMarker = false
    TriggerEvent('disc-base:hasExitedMarker')
    end
    end
    end)
    ```
    Here we can see the "main thread" that works coordinate checks. It loops every thread, and triggers events if you enter or exit ont he first time. So how can we improve this?

    ### Step 1: Remove the native for coord distance calculation

    Natives are *slow*. FiveM has utilities to make checks like those much faster.

    `local distance = GetDistanceBetweenCoords(coords.x, coords.y, coords.z, v.coords.x, v.coords.y, v.coords.z, true)`

    gets changed to:

    `local distance = #(coords - v.coords)`

    Outside render distance we now have the following:
    ![Improvements outside render distance](https://iam.malding.dev/i/766ff75edd.png)

    0.28ms! Wow, we have already shaved of 0.15ms, with a change of one line. Use #(a-b) for distance checks, instead of any natives.

    ### Step 2: Splitting up loops:

    This doesn't make us happy enough though. Let's split up the threads into two.

    ```lua
    local drawnMarkers = {]
    CreateThread(function ()
    while true do
    Citizen.Wait(500)
    local playerPed = PlayerPedId()
    local coords = GetEntityCoords(playerPed)
    local isInMarker = false
    local lastMarker = nil
    drawnMarkers = {}
    for k, v in pairs(markers) do
    local distance = #(coords - v.coords)
    if distance < Config.DrawDistance and v.shouldDraw() then
    table.insert(drawnMarkers, v)
    end
    end
    end
    end)

    Citizen.CreateThread(function()
    while true do
    Citizen.Wait(0)
    local playerPed = PlayerPedId()
    local coords = GetEntityCoords(playerPed)
    local isInMarker = false
    local lastMarker = nil
    for k, v in pairs(drawnMarkers) do
    local distance = #(coords - v.coords)
    if distance < Config.DrawDistance then
    if v.show3D then
    if distance < Config.Draw3DDistance then
    DrawText3D(v.coords, v.msg, 0.5)
    end
    elseif v.type ~= -1 then
    DrawMarker(v.type, v.coords, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, v.size.x, v.size.y, v.size.z, v.colour.r, v.colour.g, v.colour.b, 100, getOrElse(v.bob, false), true, 2, getOrElse(v.rotate, true), false, false, false)
    end
    end
    if distance < v.size.x and v.shouldDraw() then
    if v.enableE then
    EnableControlAction(0, 38)
    end
    isInMarker = true
    lastMarker = v
    end
    end

    if isInMarker and not HasAlreadyEnteredMarker then
    HasAlreadyEnteredMarker = true
    TriggerEvent('disc-base:hasEnteredMarker', lastMarker)
    end
    if not hasExited and not isInMarker and HasAlreadyEnteredMarker then
    HasAlreadyEnteredMarker = false
    TriggerEvent('disc-base:hasExitedMarker')
    end
    end
    end)
    ```

    Now we have split this into two threads, you will notice the main thread we looked at before has basically not changed. The only thing we changed is `markers` -> `drawnMarkers` when we start the for loop.

    We add a new variable called `drawnMarkers` including all markers that are currently in draw range.

    In addition to this, we have added a completely new thread that runs every 500 frames, and repopulates the `drawnMarker` table with the now new entries.

    Let's now check the performance:

    Outside draw range:
    ![Outside draw range](https://iam.malding.dev/i/b56b7078e9.png)

    Inside a marker:
    ![Inside a marker](https://iam.malding.dev/i/6adf01eea3.jpg)

    Now we are starting to see massive performance improvements. Outside render range we are down to 0.06ms, inside a marker directly we are at 0.20ms

    Now why is the performance so terrible inside a marker? Let's look at ESX' help notification function.

    ```lua
    ShowHelpNotification = function(msg, thisFrame, beep, duration)
    AddTextEntry('esxHelpNotification', msg)

    if thisFrame then
    DisplayHelpTextThisFrame('esxHelpNotification', false)
    else
    if beep == nil then beep = true end
    BeginTextCommandDisplayHelp('esxHelpNotification')
    EndTextCommandDisplayHelp(0, false, beep, duration or -1)
    end
    ```
    This displays a help text inside which is just "text example" in our markers. What this also does is add a text entry *every frame* together with displaying the help text. A simple improvement would be deleting the line of `AddTextEntry('esxHelpNotification', msg)` and adding the following to the end of the `disc-base:hasEnteredMarker` event:
    ```lua
    if not CurrentMarker.show3D and CurrentMarker.msg then
    AddTextEntry('esxHelpNotification', CurrentMarker.msg)
    end
    ```

    This saves about 0.02ms once again, we want to use **the least amount of native calls possible**.

    ### Digression: Render distances

    As of the config file I have here, the default render distance is 30 units. That is *way too much*. Reducing this distance to 10 or 15 units cuts down resource time drastically especially when there is a lot of markers in small range. Reducing our example to 10 units brings it to around 0.06ms

    ![Performance improvements](https://iam.malding.dev/i/dca06e47d2.jpg)


    ### Going the extra mile

    Right now we are just spawning markers close to ourselves. If we were to run, let's say, an amazing RP server that has like 400 markers all across the map somewhere, 0.06ms is still too much. How would we tackle this?

    My suggestion would be the following:
    1. When registering markers, get the zone this marker is in, and add it to a sub-table of our markers table
    2. Only loop markers of your current zone
    3. Profit

    Adjust the `disc-base:registerMarker` event at the end to the following:
    ```lua
    local zone = GetZoneAtCoords(marker.coords.x, marker.coords.y, marker.coords.z)

    if markers[zone] == nil then
    markers[zone] = {}
    end

    markers[zone][marker.name] = marker
    ```

    Our 500ms thread that initially loops over markers now gets adjusted like this:
    ```lua
    drawnMarkers = {}
    local zone = GetZoneAtCoords(coords.x, coords.y, coords.z)
    if markers[zone] ~= nil then
    for k, v in pairs(markers[zone]) do
    local distance = #(coords - v.coords)
    if distance < Config.DrawDistance and v.shouldDraw() then
    table.insert(drawnMarkers, v)
    end
    end
    end
    ```

    To unregister a marker correctly, also make the following change:
    ```lua
    RegisterNetEvent('disc-base:removeMarker')
    AddEventHandler('disc-base:removeMarker', function(name)
    for k, v in pairs(markers) do
    markers[k][name] = nil
    end
    end)
    ```

    Now, we will not directly notice a performance improvement, but if we adjust our thread that runs every frame to this, we will see drastic improvements again:

    ```lua
    Citizen.CreateThread(function()
    while true do
    local isInMarker = false
    local lastMarker = nil
    if #drawnMarkers > 0 then
    local playerPed = PlayerPedId()
    local coords = GetEntityCoords(playerPed)
    for k, v in pairs(drawnMarkers) do
    local distance = #(coords - v.coords)
    if distance < Config.DrawDistance then
    if v.show3D then
    if distance < Config.Draw3DDistance then
    DrawText3D(v.coords, v.msg, 0.5)
    end
    elseif v.type ~= -1 then
    DrawMarker(v.type, v.coords, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, v.size.x, v.size.y, v.size.z, v.colour.r, v.colour.g, v.colour.b, 100, getOrElse(v.bob, false), true, 2, getOrElse(v.rotate, true), false, false, false)
    end
    end
    if distance < v.size.x and v.shouldDraw() then
    if v.enableE then
    EnableControlAction(0, 38)
    end
    isInMarker = true
    lastMarker = v
    end
    end
    end

    if isInMarker and not HasAlreadyEnteredMarker then
    HasAlreadyEnteredMarker = true
    TriggerEvent('disc-base:hasEnteredMarker', lastMarker)
    end
    if not hasExited and not isInMarker and HasAlreadyEnteredMarker then
    HasAlreadyEnteredMarker = false
    TriggerEvent('disc-base:hasExitedMarker')
    end

    if CurrentMarker and CurrentMarker.shouldDraw() then
    if not CurrentMarker.show3D and CurrentMarker.msg then
    ShowHelpNotification(CurrentMarker.msg)
    end

    if IsControlJustReleased(0, 38) then
    if CurrentMarker.action ~= nil then
    CurrentMarker.action(CurrentMarker)
    end
    end
    end

    if #drawnMarkers > 0 then
    Citizen.Wait(1)
    else
    Citizen.Wait(500)
    end
    end
    end`
    ```

    We move the other thread into the same thread that checks for the current marker. It does not need to be its own thread, it does just fine in here. However I would recommend some refactor so it just runs a function in here that does the same as this.

    Also, we only run natives like grabbing the ped or the coordinates, or *any* distance checks if there is at least one element in our `drawnMarkers` table. Also, at the end we run a 1 frame wait if that table has more than one element, if not we run a 500 frame wait. This drops the resource time to **0.01ms** if there are no markers drawn.

    ![0.01ms Resmon](https://iam.malding.dev/i/e5079914b1.png)

    ![Rendering 11 markers](https://iam.malding.dev/i/f8426b0539.jpg)

    Now, our performance is still about 0.07ms looking at about 11 markers, but that is acceptable. But we can do more.

    ### We want more FPS

    If we add another key on the markers we put in the `drawnMarker` check, we can save up some more resource:

    ```lua
    for k, v in pairs(markers[zone]) do
    local distance = #(coords - v.coords)
    if distance < Config.DrawDistance and v.shouldDraw() then
    v.distance = distance
    table.insert(drawnMarkers, v)
    end
    end
    ```

    Running this, we can shorten the main thread loop to this:
    ```lua
    while true do
    local isInMarker = false
    local lastMarker = nil
    if #drawnMarkers > 0 then
    for k, v in pairs(drawnMarkers) do
    if v.show3D then
    if v.distance < Config.Draw3DDistance then
    DrawText3D(v.coords, v.msg, 0.5)
    end
    elseif v.type ~= -1 then
    DrawMarker(v.type, v.coords, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, v.size.x, v.size.y, v.size.z, v.colour.r, v.colour.g, v.colour.b, 100, getOrElse(v.bob, false), true, 2, getOrElse(v.rotate, true), false, false, false)
    end
    if v.distance < v.size.x then
    isInMarker = true
    lastMarker = v
    end
    end
    end
    ...
    ```

    Now, we are dropping as far down as 0.05 when rendering 11 markers, which is fantastic. Other than some adjustments to the waits (i.e. dropping it to 1000 from 500, which is certainly viable) we have achieved massive performance improvements in just a short amount of time.

    # Takeaways

    - Check if you can split up your threads into multiple threads with longer waits. Filtering out most non-viable markers every 500ms instead of on tick, while keeping the main tick loop is drastic.
    - Use the least amount of natives possible. Save where you can.
    - Do not repeat yourself, some natives do not need to run every frame!
    - See where you can condense threads together if they have the same wait times. You don't need 2 threads that each run every tick, you can just make it one.
    - Something that disc-base already applies, work with events! I personally do this a lot in my private projects, an example would be:
    - Instead of checking every x ms if the player is in a vehicle, use the `baseevents` resource that comes with the cfx-server-data and their `enteredVehicle` and `exitedVehicle` events.
    - **Refactor your code**, generalize it. A lot of code can be reused in so many other parts of your code.