Best Practices and Performance Tips for Particle FX in FiveM
Best Practices and Performance Tips for Particle FX
Unoptimized particle effects can cause severe client lag — here are guidelines for using PTFX effectively.
Golden Rule No. 1: Always clean.
-- ❌ BAD: No cleanup looped effects
local function StartEffect(ped)
UseParticleFxAssetNextCall("core")
StartParticleFxLoopedOnEntityBone("ent_anim_fire", ped, ...)
-- If you don't pick up the handle, there's no way to stop!
end
-- ✅ GOOD: Always keep the handle clean.
local effectHandles = {}
local function StartEffect(ped, playerId)
UseParticleFxAssetNextCall("core")
effectHandles[playerId] = StartParticleFxLoopedOnEntityBone(
"ent_anim_fire", ped, ...
)
end
local function StopEffect(playerId)
if effectHandles[playerId] then
StopParticleFxLooped(effectHandles[playerId], false)
effectHandles[playerId] = nil
end
end
AddEventHandler("onResourceStop", function(name)
if name == GetCurrentResourceName() then
for _, handle in pairs(effectHandles) do
StopParticleFxLooped(handle, false)
end
effectHandles = {}
end
end)
Rule 2: Limit the Number of Concurrent Effects
local MAX_CONCURRENT_EFFECTS = 20
local activeEffectCount = 0
local function CanStartEffect()
return activeEffectCount < MAX_CONCURRENT_EFFECTS
end
local function TrackEffect(handle, onStop)
activeEffectCount = activeEffectCount + 1
return {
handle = handle,
stop = function()
if handle then
StopParticleFxLooped(handle, false)
activeEffectCount = activeEffectCount - 1
if onStop then onStop() end
end
end
}
end
Rule 3: Distance Culling
Do not play this effect for players who are not in line of sight:
local function IsInRange(coords, maxDistance)
local playerCoords = GetEntityCoords(PlayerPedId())
return #(playerCoords - coords) <= maxDistance
end
RegisterNetEvent("server:playEffect")
AddEventHandler("server:playEffect", function(data)
local coords = vector3(data.x, data.y, data.z)
-- Skip if more than 100 units away
if not IsInRange(coords, 100.0) then
return
end
-- play effect
Citizen.CreateThread(function()
PlayEffect(data)
end)
end)
Rule 4: Asset Pool Management
local assetPool = {
loaded = {},
refCount = {}
}
local function AcquireAsset(assetName)
assetPool.refCount[assetName] = (assetPool.refCount[assetName] or 0) + 1
if not assetPool.loaded[assetName] then
RequestNamedPtfxAsset(assetName)
while not HasNamedPtfxAssetLoaded(assetName) do
Citizen.Wait(0)
end
assetPool.loaded[assetName] = true
end
end
local function ReleaseAsset(assetName)
if assetPool.refCount[assetName] then
assetPool.refCount[assetName] = assetPool.refCount[assetName] - 1
-- Delete from memory when no one is using it.
if assetPool.refCount[assetName] <= 0 then
RemoveNamedPtfxAsset(assetName)
assetPool.loaded[assetName] = nil
assetPool.refCount[assetName] = nil
end
end
end
Rule 5: Timeout for Loading
-- Don't let the infinite loop freeze the server!
local function SafeLoadAsset(assetName, timeoutMs)
timeoutMs = timeoutMs or 3000
if HasNamedPtfxAssetLoaded(assetName) then
return true
end
RequestNamedPtfxAsset(assetName)
local start = GetGameTimer()
while not HasNamedPtfxAssetLoaded(assetName) do
if GetGameTimer() - start > timeoutMs then
print("[WARNING] PTFX timeout: " .. assetName)
return false
end
Citizen.Wait(10)
end
return true
end
Common problems and solutions
| Problem | Cause | Solution |
|---|---|---|
| Effect not shown | Not used. UseParticleFxAssetNextCall | Always called before start |
| Effect disappears after resource restart | Cannot cleanup in onResourceStop | Add cleanup handler |
| Heavy client lag | Too many effects | Limited concurrent effects |
| Effect hangs in the air | entity deleted but effect still exists | check entity validity |
| Memory leak | No RemoveNamedPtfxAsset | Use ref counting |
Check Entity before playing Effect
local function PlayEffectOnEntity(entity, effectName, dictName, bone)
-- Check that the entity still exists and is valid.
if not DoesEntityExist(entity) then return end
if not IsEntityVisible(entity) then return end -- optional: skip if not visible
local boneIndex = GetEntityBoneIndexByName(entity, bone)
if boneIndex == -1 then return end -- bone does not exist
if not SafeLoadAsset(dictName) then return end
UseParticleFxAssetNextCall(dictName)
return StartParticleFxLoopedOnEntityBone(
effectName, entity,
0, 0, 0, 0, 0, 0,
boneIndex, 1.0,
false, false, false
)
end
Performance Checklist
- Keep handle every looped effect
- StopParticleFxLooped every handle before resource stops
- Put timeout in loading loop.
- Use distance culling for networked effects.
- Limit MAX_CONCURRENT_EFFECTS
- RemoveNamedPtfxAsset after use (for one-time assets)
- Check DoesEntityExist before attach effects
Summary
Good particle FX isn't just about "looking good" — it also has to be clean, efficient, and self-managing. Use this checklist every time you add a new effect to your script.
Related Articles
วิธีจัดการ Version และ Update Script ในเซิร์ฟเวอร์ FiveM อย่างมืออาชีพ
อัพเดท script บน production โดยไม่ให้เซิร์ฟเวอร์ down — เรียนรู้ระบบจัดการ version, Git workflow, และกลยุทธ์ deploy ที่ลดความเสี่ยง
หลักการ Clean Code สำหรับ FiveM Script Developer
โค้ดที่ทำงานได้กับโค้ดที่ดีไม่ใช่สิ่งเดียวกัน — เรียนรู้หลักการ clean code ที่ทำให้ FiveM script ของคุณอ่านง่าย, แก้ง่าย และขยายได้
วิธีทดสอบ FiveM Script ก่อน Deploy ขึ้น Production Server
อย่า deploy script ที่ยังไม่ผ่านการทดสอบลงบน production — เรียนรู้วิธีสร้าง testing workflow สำหรับ FiveM ที่ลด downtime และป้องกัน bug จากผู้เล่นจริง