Live player blips for police and jobs on OneSync
Every police department on a FiveM server wants to see its officers on the map. The first version of that script is always the same: loop over active players, create a blip for each colleague's ped, done. It works in testing with two people standing next to each other and falls apart in production, where officers across the map simply do not appear. The reason is OneSync's scoping, and the fix is to let the server feed positions. This guide builds a job-tracking blip system that works at any distance, stays cheap and does not leak positions to people who should not see them.
Short answer: On OneSync a client only knows about players inside its scope, roughly 424 metres by default, so AddBlipForEntity on a distant colleague's ped fails. Collect positions on the server with GetPlayerPed and GetEntityCoords, send them every one to two seconds only to authorised players, and on the client use an entity blip when the colleague is in scope and a coordinate blip updated with SetBlipCoords when not. Remove blips for players who left or changed job.

#Why client-only player blips break
With OneSync, the server decides which entities each client knows about. A player's client receives other players and their peds only when they are within its scope, which by default is roughly 424 metres. Outside it, GetPlayerFromServerId(id) returns -1, NetworkIsPlayerActive is false and there is no ped handle to attach a blip to. A client-only script therefore shows colleagues nearby and silently drops everyone else.
Old tutorials written for non-OneSync servers with 32 slots do not mention this, because without OneSync every client knew about every player. On any modern server with OneSync enabled, the server has to supply positions.
#The design
- The server keeps a list of which players are on which tracked job, fed by your framework's job events.
- Every one to two seconds, the server reads each tracked player's ped position, heading and vehicle state.
- It sends each authorised player one compact list of their colleagues.
- The client keeps one blip per colleague and updates it, swapping between entity and coordinate blips as the colleague enters or leaves scope.
- Blips for colleagues no longer in the list are removed.
This keeps the server as the source of truth, keeps bandwidth tiny and means a cheater cannot see anything their job would not show them anyway.
It also separates two concerns that client-only scripts mix up. Who may see whom is a permission question and belongs on the server, next to your job and duty data. How a colleague looks on the map is a presentation question and belongs on the client, where sprites, colours and categories can change without touching the server loop. Keeping them apart means a department can restyle its blips, or a new division can get its own colour, without anyone reviewing the access logic again.
#Server side
local TRACKED = { police = true, ambulance = true }
local members = {}
local function setMember(src, job, callsign)
if TRACKED[job] then
members[src] = { job = job, callsign = callsign or GetPlayerName(src) }
else
members[src] = nil
end
end
AddEventHandler('playerDropped', function()
members[source] = nil
end)
CreateThread(function()
while true do
local byJob = {}
for src, info in pairs(members) do
local ped = GetPlayerPed(src)
if ped ~= 0 and DoesEntityExist(ped) then
local coords = GetEntityCoords(ped)
local list = byJob[info.job] or {}
list[#list + 1] = {
id = tonumber(src),
x = coords.x, y = coords.y, z = coords.z,
h = math.floor(GetEntityHeading(ped)),
v = GetVehiclePedIsIn(ped, false) ~= 0,
n = info.callsign,
b = GetPlayerRoutingBucket(src),
}
byJob[info.job] = list
end
end
for src, info in pairs(members) do
local bucket = GetPlayerRoutingBucket(src)
local visible = {}
for _, entry in ipairs(byJob[info.job] or {}) do
if entry.id ~= tonumber(src) and entry.b == bucket then
visible[#visible + 1] = entry
end
end
TriggerClientEvent('jobblips:update', src, visible)
end
Wait(1500)
end
end)Wire setMember to your framework: the job-set and player-loaded events on ESX or QBCore, or your own duty toggle. Tracking on-duty rather than job membership is usually what departments want, since off-duty officers should not appear.
The payload is small. Thirty officers each receiving twenty-nine entries of a few numbers every 1.5 seconds is a few kilobytes per second in total, which is negligible next to normal entity sync.
#Client side
local blips = {}
local function styleBlip(blip, entry)
SetBlipSprite(blip, entry.v and 56 or 1)
SetBlipColour(blip, 3)
SetBlipScale(blip, 0.85)
SetBlipCategory(blip, 7)
SetBlipPriority(blip, 10)
ShowHeadingIndicatorOnBlip(blip, true)
SetBlipAsShortRange(blip, false)
BeginTextCommandSetBlipName('STRING')
AddTextComponentSubstringPlayerName(entry.n)
EndTextCommandSetBlipName(blip)
end
local function removeBlip(id)
local b = blips[id]
if b and DoesBlipExist(b.handle) then RemoveBlip(b.handle) end
blips[id] = nil
end
RegisterNetEvent('jobblips:update', function(list)
local seen = {}
for _, entry in ipairs(list) do
seen[entry.id] = true
local player = GetPlayerFromServerId(entry.id)
local ped = player ~= -1 and GetPlayerPed(player) or 0
local wantEntity = ped ~= 0 and DoesEntityExist(ped)
local current = blips[entry.id]
if current and current.entity ~= wantEntity then
removeBlip(entry.id)
current = nil
end
if not current then
local handle = wantEntity and AddBlipForEntity(ped)
or AddBlipForCoord(entry.x, entry.y, entry.z)
current = { handle = handle, entity = wantEntity }
blips[entry.id] = current
end
if not current.entity then
SetBlipCoords(current.handle, entry.x, entry.y, entry.z)
SetBlipRotation(current.handle, entry.h)
end
styleBlip(current.handle, entry)
end
for id in pairs(blips) do
if not seen[id] then removeBlip(id) end
end
end)
AddEventHandler('onResourceStop', function(name)
if name ~= GetCurrentResourceName() then return end
for id in pairs(blips) do removeBlip(id) end
end)Entity blips follow the ped every frame, so nearby colleagues move smoothly. Coordinate blips jump once per update, which is fine for someone half a map away. The heading indicator follows the entity automatically for entity blips; for coordinate blips, the rotation comes from the server heading.
Sprite 56 is the patrol-car icon and 1 is the plain dot, which gives dispatchers an instant read of who is driving. Colour 3 is blue; use 1 for red or 2 for green for EMS. Category 7 lists colleagues under "Other Players" in the legend with their distance, which is surprisingly useful for backup calls. More on categories in blip categories and the legend.
#Hooking it to ESX and QBCore
The server needs to know who is on a tracked job and on duty. Both big frameworks fire server-side events you can listen to instead of polling player data. Check the event names against your framework version, since forks rename them.
AddEventHandler('esx:setJob', function(src, job)
if job.onDuty == false then
setMember(src, nil)
else
setMember(src, job.name)
end
end)
AddEventHandler('esx:playerLoaded', function(src, xPlayer)
setMember(src, xPlayer.job.name)
end)AddEventHandler('QBCore:Server:OnJobUpdate', function(src, job)
setMember(src, job.onduty and job.name or nil)
end)
AddEventHandler('QBCore:Server:SetDuty', function(src, onDuty)
local player = exports['qb-core']:GetCoreObject().Functions.GetPlayer(src)
if not player then return end
local job = player.PlayerData.job
setMember(src, onDuty and job.name or nil, player.PlayerData.metadata.callsign)
end)Also populate the list when the resource starts, by looping over the players already online, so a restart of the blip resource does not leave everyone invisible until they change job.
#Scaling to large servers
The loop above does work proportional to tracked players times recipients. At 300 players with 60 officers and 30 medics that is still only a few thousand small table entries every 1.5 seconds, well within what a server thread handles without a measurable tick. The things that actually cause problems at scale are different:
- Sending the list to every player and letting the client filter. That multiplies traffic by the player count and leaks positions.
- Serialising large per-player objects (full player data, inventories) instead of a few numbers.
- Updating every 100 ms because the blips looked jumpy. Jumpiness only affects out-of-scope blips, which are far away and do not need precision.
- Recreating blips on every update instead of moving them. Creation is more expensive and makes blips flicker in the legend.
If you need finer updates for a pursuit, raise the rate only for the units involved and only while the pursuit runs. Dispatch-heavy servers sometimes move this whole system into their MDT resource so that one resource owns units, calls and blips together.
#Access control and privacy
- Decide on the server who receives what. Never send the full list to every client and filter on the client; a modified client ignores your filter.
- Remove players from
membersthe moment they go off duty or change job, not only on disconnect. - Keep routing buckets separate so players in an apartment instance or a different game mode are not shown.
- For gang or criminal factions, consider showing members only within a radius or only while a territory event is running; permanent enemy-proof tracking changes gameplay a lot.
- Do not expose other jobs' positions through the same event name with a job parameter the client can choose.
#Useful extras
| Feature | How |
|---|---|
| Panic button | Flash the officer's blip with SetBlipFlashes and SetBlipFlashTimer, and set its colour to red |
| Unit colours by division | Send a division field and map it to blip colours |
| Hide while in the pause map | Nothing needed; blips render in both views |
| Show only on the pause map | SetBlipDisplay(blip, 3) for dispatch-only views |
| Route to a colleague | SetBlipRoute(handle, true); entity blips keep the route updated as they move |
Routing to a unit is covered in more depth in GPS routes and waypoints.
#Troubleshooting
| Symptom | Cause |
|---|---|
| Colleagues only show when close | Client-only script using entity blips, no server feed |
| Blips duplicate over time | Blip handle not removed when swapping entity and coordinate modes |
| Blip stuck at an old position | Player dropped or went off duty but was not removed from the list |
| Blip shows your own position twice | Server did not exclude the recipient from the list |
| Players in apartments visible on the street | Routing bucket not compared |
Questions
Why can't I see police blips across the map?
How often should player blips update?
Is it safe to send all positions to every client?
Can I use state bags for player positions?
Which sprite should police blips use?
Should players in other routing buckets appear?
GetPlayerRoutingBucket for the sender and recipient on the server and skip entries from a different bucket, so apartment instances and separate game modes stay hidden.Ready to pick a map?
Twelve themes on three base map styles, $8 each, instant download.
Relevant to what you just read