Carte GTA V interactive pour le NUI FiveM avec Leaflet
Une carte GTA V zoomable dans un NUI FiveM avec Leaflet : CRS des coordonnées du jeu, blips, position du joueur, routes, zones et waypoint au clic.
Par Termi
- NUI
- Leaflet
- Lua
- Maps
Sur cette page
- Ce que vous allez construire
- La resource : fxmanifest et fichiers
- Leaflet depuis un CDN, ou embarqué
- Les coordonnées du jeu sur une carte Leaflet
- Ajouter les tuiles GTA V
- Marqueurs et blips à partir des coordonnées du jeu
- Afficher la position du joueur
- Dessiner une route et une zone
- Cliquer sur la carte, obtenir des coordonnées GTA en Lua
- La garder rapide
- Questions fréquentes
- Comment convertir les coordonnées de GTA V pour une carte Leaflet ?
- Pourquoi ma carte Leaflet est-elle grise dans un NUI FiveM ?
- Pourquoi ma page NUI reçoit-elle un 404 pour ses propres fichiers ?
- Où trouver des tuiles de la carte GTA V pour Leaflet ?
- Héberger vos propres tuiles sur FiveDock
- Zipper et envoyer
- Faire pointer Leaflet vers vos tuiles
La plupart des cartes en NUI FiveM démarrent pareil : quelqu’un fait une capture de la carte du menu pause, la colle dans un <img> et passe une soirée à se battre avec des décalages de pixels. Leaflet sait déjà afficher une énorme image sous forme de tuiles zoomables, dessiner des marqueurs, des lignes et des cercles, et vous dire où l’utilisateur a cliqué. Il lui manque seulement un système de coordonnées qui colle à GTA V.
Ce guide construit de zéro une carte NUI pour une resource FiveM : la mise en place de Leaflet, le CRS qui convertit les coordonnées du jeu, des blips, la position du joueur en direct depuis Lua, une route, une zone, et un clic sur la carte qui place un waypoint. Les tuiles GTA V sont hébergées sur le CDN de FiveDock et gratuites, vous obtenez donc une carte qui fonctionne sans rien héberger vous-même.
Ce que vous allez construire
Une resource avec une page NUI qui ouvre une grande carte GTA V quand vous tapez /map. La carte :
Tout utilise Leaflet 1.9, qui tourne sans problème dans le navigateur NUI.
La resource : fxmanifest et fichiers
Une resource NUI est une resource ordinaire avec une ui_page. Chaque fichier que la page charge depuis la resource doit aussi figurer sous files, sinon le navigateur NUI reçoit un 404.
fx_version 'cerulean'
game 'gta5'
client_script 'client.lua'
ui_page 'html/index.html'
files {
'html/index.html',
'html/style.css',
'html/app.js',
}my-map/
├── fxmanifest.lua
├── client.lua
└── html/
├── index.html
├── style.css
└── app.jsLeaflet depuis un CDN, ou embarqué
Le plus rapide est de charger Leaflet depuis un CDN. Le navigateur NUI charge les scripts distants et les met en cache comme n’importe quel navigateur. Fixez la version et gardez les hashes d’intégrité, pour qu’un fichier modifié sur le CDN ne s’exécute jamais :
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<link
rel="stylesheet"
href="https://unpkg.com/[email protected]/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div id="map"></div>
<script
src="https://unpkg.com/[email protected]/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""
></script>
<script src="app.js"></script>
</body>
</html>html,
body {
margin: 0;
height: 100%;
background: transparent;
}
#map {
position: absolute;
inset: 5vh 5vw;
display: none;
border-radius: 12px;
/* the sea colour of the tiles, behind the edges and tiles still loading */
background: #0fa8d2;
}
#map.open {
display: block;
}Si vous préférez ne pas dépendre d’un CDN tiers, embarquez-le : téléchargez leaflet.js et leaflet.css dans html/vendor/, faites pointer les deux balises dessus et ajoutez 'html/vendor/*' à files. Si votre NUI est une appli Vite ou React, faites npm install leaflet et importez-le ; le reste de ce guide ne change pas.
Les épingles de marqueur par défaut de Leaflet sont des images placées à côté de son CSS, et dans un NUI elles doivent aussi figurer dans files. Ce guide utilise des circle markers, qui n’ont besoin d’aucune image.
Les coordonnées du jeu sur une carte Leaflet
Leaflet raisonne en latitude et longitude sur une Terre ronde. GTA V est plat et mesuré en mètres, avec x qui croît vers l’est et y vers le nord. L.CRS.Simple est le système de coordonnées plat de Leaflet. Nous gardons sa projection, L.Projection.LonLat, qui prend la longitude comme x et la latitude comme y, et nous y mettons une transformation qui aligne les coordonnées du jeu sur les tuiles de la carte :
// app.js, part 1: the coordinate system
const GTA_CRS = L.extend({}, L.CRS.Simple, {
projection: L.Projection.LonLat,
scale: (zoom) => Math.pow(2, zoom),
zoom: (scale) => Math.log(scale) / Math.LN2,
transformation: new L.Transformation(0.02072, 117.3, -0.0205, 172.8),
infinite: true,
});
// A GTA world position (x, y) is L.latLng(y, x) on this map.
function toLatLng(x, y) {
return L.latLng(y, x);
}
// ...and back: lng is the game x, lat is the game y.
function toGame(latlng) {
return { x: latlng.lng, y: latlng.lat };
}Ce que signifient ces nombres : au zoom 0, toute la carte tient dans la première tuile, 256 × 256 pixels, et une position du jeu (x, y) tombe sur le pixel (0.02072 · x + 117.3, −0.0205 · y + 172.8). Chaque niveau de zoom double cela (scale vaut 2 puissance le zoom), donc au zoom 5 la carte fait 8 192 pixels de large. Le facteur y est négatif parce que le y du jeu pointe vers le nord alors que le y des pixels pointe vers le bas. Les tuiles FiveDock sont découpées exactement pour cette calibration.
Une règle à retenir
Leaflet prend [lat, lng], donc une position du jeu entre sous la forme [y, x]. Appelez toLatLng(x, y) partout et vous ne les inverserez jamais par accident.
Ajouter les tuiles GTA V
Créez la carte avec le CRS et ajoutez la couche de tuiles. FiveDock héberge un jeu de tuiles GTA V que tout le monde peut utiliser, aux niveaux de zoom 0 à 5 :
// app.js, part 2: the map and the tiles
const TILE_SIZE = 256;
const MAP_SIZE = 8192;
const NATIVE_ZOOM = 5;
const map = L.map('map', {
crs: GTA_CRS,
minZoom: 2,
maxZoom: 6,
zoomControl: false,
preferCanvas: true,
});
// The tiles hold one MAP_SIZE × MAP_SIZE image at zoom 5, from their top-left corner.
const worldBounds = L.latLngBounds(
map.unproject([0, MAP_SIZE], NATIVE_ZOOM),
map.unproject([MAP_SIZE, 0], NATIVE_ZOOM),
);
L.tileLayer('https://cdn.fivedock.dev/o/fivedock/maps/styleatlas/{z}/{x}/{y}.jpg', {
tileSize: TILE_SIZE,
minZoom: 0,
maxZoom: 6,
maxNativeZoom: NATIVE_ZOOM,
bounds: worldBounds,
attribution: 'Map © Rockstar Games',
}).addTo(map);
map.setMaxBounds(worldBounds.pad(0.1));
map.setView(toLatLng(0, 0), 3);Deux options font le gros du travail. bounds compte avec ce CRS : infinite: true signifie que Leaflet ignore où le monde s’arrête, et sans limites il demanderait des tuiles très loin hors de la carte, qui ne peuvent revenir qu’en 404. maxNativeZoom laisse les joueurs zoomer d’un niveau au-delà du dernier niveau de tuiles : Leaflet agrandit les tuiles du zoom 5 au lieu de demander des tuiles qui n’existent pas.
Marqueurs et blips à partir des coordonnées du jeu
Tout ce dont vous connaissez les coordonnées du jeu peut aller sur la carte. Avec preferCanvas, les circle markers sont dessinés sur un seul canvas, si bien que même des centaines restent fluides :
// app.js, part 3: blips
const blips = [
{ label: 'Mission Row PD', x: 441.0, y: -982.0, color: '#3b82f6' },
{ label: 'Pillbox Hill Medical', x: 298.6, y: -584.5, color: '#ef4444' },
{ label: 'LSIA', x: -1037.0, y: -2737.0, color: '#f59e0b' },
];
for (const blip of blips) {
L.circleMarker(toLatLng(blip.x, blip.y), {
radius: 6,
color: '#ffffff',
weight: 2,
fillColor: blip.color,
fillOpacity: 1,
})
.bindTooltip(blip.label)
.addTo(map);
}Les coordonnées sont approximatives ; placez-vous quelque part en jeu et affichez GetEntityCoords(PlayerPedId()) pour obtenir les exactes. Si vous préférez des icônes, L.marker avec un L.icon qui pointe vers une image de votre dossier html/ fonctionne pareil, tant que l’image figure dans files.
Afficher la position du joueur
La page NUI ne peut pas lire l’état du jeu, c’est donc au Lua client de l’envoyer. SendNUIMessage remet une table à la page sous forme d’événement message. Ouvrez la carte avec une commande et, tant qu’elle est ouverte, envoyez la position quatre fois par seconde :
local isOpen = false
RegisterCommand('map', function()
isOpen = true
SetNuiFocus(true, true)
SendNUIMessage({ action = 'open' })
end, false)
CreateThread(function()
while true do
if isOpen then
local coords = GetEntityCoords(PlayerPedId())
SendNUIMessage({ action = 'player', x = coords.x, y = coords.y })
Wait(250)
else
Wait(1000)
end
end
end)// app.js, part 4: messages from Lua
const mapElement = document.getElementById('map');
let player = null;
let followPlayer = true;
window.addEventListener('message', (event) => {
const data = event.data;
if (data.action === 'open') {
mapElement.classList.add('open');
map.invalidateSize();
followPlayer = true;
}
if (data.action === 'close') {
mapElement.classList.remove('open');
}
if (data.action === 'player') {
const position = toLatLng(data.x, data.y);
if (player) {
player.setLatLng(position);
} else {
player = L.circleMarker(position, {
radius: 7,
color: '#ffffff',
weight: 2,
fillColor: '#22c55e',
fillOpacity: 1,
}).addTo(map);
}
if (followPlayer) map.panTo(position, { animate: false });
}
});
// stop following once the player drags the map around
map.on('dragstart', () => {
followPlayer = false;
});Notez l’appel à map.invalidateSize(). Leaflet mesure son conteneur à la création de la carte, et le nôtre démarre masqué. Sans cet appel, vous obtenez une carte grise avec une seule tuile dans un coin.
Dessiner une route et une zone
Les lignes et les cercles prennent les mêmes coordonnées converties. Une route est une polyline passant par des positions du jeu ; une zone est un cercle dont le rayon est en unités du jeu :
// app.js, part 5: a route and a zone
// [x, y] in game coordinates: Mission Row PD to Pillbox Hill Medical
const route = [
[441.0, -982.0],
[402.0, -860.0],
[298.6, -584.5],
];
L.polyline(
route.map(([x, y]) => toLatLng(x, y)),
{ color: '#22c55e', weight: 4, opacity: 0.9 },
).addTo(map);
// radius in game units, which are metres
L.circle(toLatLng(441.0, -982.0), {
radius: 80,
color: '#3b82f6',
fillOpacity: 0.15,
}).addTo(map);Quand le CRS n’est pas la Terre, L.circle mesure son rayon dans les unités du CRS lui-même, donc radius: 80 fait 80 mètres dans le jeu. Une vraie route, issue de vos propres waypoints ou d’un script GPS, n’est qu’une liste de points plus longue.
Cliquer sur la carte, obtenir des coordonnées GTA en Lua
Un clic donne un LatLng, et avec notre CRS son lng est le x du jeu et son lat le y. Envoyez-le à un callback NUI :
// app.js, part 6: clicks and closing, back to Lua
function post(name, body) {
return fetch(`https://${GetParentResourceName()}/${name}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json; charset=UTF-8' },
body: JSON.stringify(body),
});
}
map.on('click', (event) => {
post('mapClick', toGame(event.latlng));
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') post('close', {});
});RegisterNUICallback('mapClick', function(data, cb)
SetNewWaypoint(data.x + 0.0, data.y + 0.0)
cb('ok')
end)
RegisterNUICallback('close', function(_, cb)
isOpen = false
SetNuiFocus(false, false)
SendNUIMessage({ action = 'close' })
cb('ok')
end)GetParentResourceName() est disponible dans toute page NUI et renvoie le nom de votre resource. Le + 0.0 garantit que les valeurs arrivent à la native sous forme de floats. Le même schéma sert pour tout ce que vous voulez choisir sur une carte : un point de spawn, un lieu de livraison, une scène de crime pour votre MDT.
La garder rapide
Questions fréquentes
Comment convertir les coordonnées de GTA V pour une carte Leaflet ?
Utilisez L.CRS.Simple avec sa projection L.Projection.LonLat et votre propre L.Transformation calibrée sur les tuiles, et passez chaque position du jeu sous la forme [y, x], car Leaflet prend la latitude en premier. Avec les tuiles FiveDock, une position du jeu (x, y) tombe au zoom 0 sur le pixel (0.02072 · x + 117.3, −0.0205 · y + 172.8), et chaque niveau de zoom double cela. Un petit helper toLatLng(x, y) vous évite de les inverser.
Pourquoi ma carte Leaflet est-elle grise dans un NUI FiveM ?
Leaflet mesure son conteneur à la création de la carte, et une carte NUI démarre en général masquée : elle se dimensionne donc à rien et affiche une zone grise avec une tuile dans un coin. Appelez map.invalidateSize() juste après avoir affiché la carte. Si la console se remplit de 404 pour des tuiles, définissez bounds sur la couche de tuiles pour que Leaflet cesse de demander des tuiles hors de la carte.
Pourquoi ma page NUI reçoit-elle un 404 pour ses propres fichiers ?
Chaque fichier que la page NUI charge depuis la resource, scripts, feuilles de style et images confondus, doit figurer sous files dans fxmanifest.lua, à côté de ui_page. Un fichier non listé n’est jamais envoyé au client, donc le navigateur NUI reçoit un 404. Les images de marqueurs par défaut de Leaflet comptent aussi, c’est pourquoi ce guide utilise des circle markers.
Où trouver des tuiles de la carte GTA V pour Leaflet ?
FiveDock héberge un jeu de tuiles GTA V aux niveaux de zoom 0 à 5 que chacun peut utiliser dans un NUI FiveM, sans rien héberger vous-même. Pour votre propre style, comme un rendu satellite ou une carte avec des routes personnalisées, découpez des tuiles {z}/{x}/{y} avec un outil comme gdal2tiles.py -p raster --xyz et mettez-les sur un CDN.
Héberger vos propres tuiles sur FiveDock
Les tuiles partagées couvrent le jeu de base. Si vous avez votre propre style (un rendu satellite, un thème sombre, une carte avec vos routes et vos MLO personnalisés), hébergez-le de la même façon. Leaflet veut un dossier par niveau de zoom, un dossier par colonne à l’intérieur et une image par tuile : {z}/{x}/{y}.png (ou .jpg, comme la carte de base). Tout outil qui écrit des tuiles XYZ convient, par exemple gdal2tiles.py -p raster --xyz. Gardez le cadrage de la carte de base (la même zone du monde, une image de 8 192 × 8 192 pixels au zoom 5, qui commence dans le coin supérieur gauche des tuiles) et la calibration ci-dessus reste valable ; si votre image couvre une autre zone ou se place à un autre zoom, les nombres de la transformation changent avec elle.
Zipper et envoyer
Zippez le contenu du dossier de tuiles, pas le dossier lui-même : les dossiers d’une archive sont conservés, tiles/ se retrouverait donc dans chaque lien :
# tiles/0/0/0.png, tiles/1/0/0.png, ... tiles/5/31/31.png
cd tiles && zip -r ../my-map.zip .FiveDock décompresse les archives .zip et .rar à l’upload. Dans le panel, ouvrez Fichiers, créez le dossier maps/my-map et ouvrez-le, puis cliquez sur Envoyer un fichier et déposez le zip. Garder les noms de fichiers est activé par défaut pour les archives ; laissez-le activé. L’archive contient directement les dossiers de zoom, pas tiles/, donc chaque tuile devient un fichier avec une clé stable comme maps/my-map/3/2/1.png. Le même upload par l’API, avec un token ayant le scope files:write :
curl -X POST https://api.fivedock.dev/api/v3/file/archive \
-H "Authorization: Bearer fd_live_…" \
-F "[email protected]" \
-F "path=maps/my-map"La réponse liste chaque fichier stocké sous uploaded et tout ce qui a été ignoré sous skipped, avec la raison.
Gros jeux de tuiles
Une archive peut contenir jusqu’à 5 000 fichiers. La carte de base, du zoom 0 au zoom 5, compte 1 365 tuiles et tient dans une seule. En découpant un niveau de plus, le zoom 6 ajoute à lui seul 4 096 tuiles, et l’ensemble ne tient plus : mettez ce niveau dans une archive à part (lancée depuis le même dossier, le dossier de zoom reste dans les clés). Chaque niveau suivant est de nouveau quatre fois plus gros ; pour de tels ensembles, copiez tout le dossier via l’endpoint S3 avec rclone :
cd tiles
zip -r ../my-map.zip . -x '6/*'
zip -r ../my-map-6.zip 6# my-city = your workspace slug (it is also your S3 bucket name)
rclone copy ./tiles fivedock:my-city/maps/my-map --transfers 16Faire pointer Leaflet vers vos tuiles
Chaque tuile a maintenant un lien qui ne change jamais, https://cdn.fivedock.dev/o/<your-workspace>/maps/my-map/{z}/{x}/{y}.png, où <your-workspace> est le slug de votre organisation dans le panel. Mettez-le dans la couche de tuiles :
L.tileLayer('https://cdn.fivedock.dev/o/<your-workspace>/maps/my-map/{z}/{x}/{y}.png', {
tileSize: TILE_SIZE,
minZoom: 0,
maxZoom: 6,
maxNativeZoom: NATIVE_ZOOM,
bounds: worldBounds,
}).addTo(map);Une mauvaise tuile ? Envoyez une version corrigée avec la même clé, par exemple maps/my-map/4/3/2.png. Elle remplace l’ancien fichier derrière le même lien, et les joueurs la voient en cinq minutes environ, quand leur copie en cache expire.
Voilà toute la carte : les six parties de app.js ci-dessus, dans l’ordre, dans un seul fichier. Faites un ensure de la resource, connectez-vous et tapez /map. Les mêmes liens à clé servent pour tout ce que votre UI affiche, des images d’items aux mugshots.