Last active
December 11, 2024 14:41
Revisions
-
nnsdev renamed this gist
Mar 21, 2021 . 1 changed file with 0 additions and 0 deletions.There are no files selected for viewing
File renamed without changes. -
nnsdev revised this gist
Mar 21, 2021 . 5 changed files with 270 additions and 0 deletions.There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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) This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,4 @@ Config = {} Config.DrawDistance = 10 Config.Draw3DDistance = 5 This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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 This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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) This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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 -
nnsdev created this gist
Mar 21, 2021 .There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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  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:  Inside render distance:  Inside a marker:  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:  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:  Inside a marker:  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  ### 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.   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.