Points, zones et entités de map

Dans l'éditeur de map, place des repères nommés (spawns d'équipe, objectifs, butin), des zones qui préviennent le Lua quand on y entre, et des entités scriptées avec leurs propriétés. Le Lua du gamemode les retrouve par leur nom ou leur catégorie.

Dans l'éditeur de map

Panneau de gauche, onglet Entités : trois boutons s'ajoutent à Point d'apparition, Lumière, Spot et Volume d'eau.

BoutonC'est quoiDans l'éditeurEn jeu
Point nomméUn repère (nom, position, orientation, catégorie facultative) lu par le Lua : spawns d'équipe, objectifs, points de butin…Petit losange rose, flèche vers l'avant (-Z) et étiquette « nom [catégorie] »Invisible, rien n'est créé : le Lua lit le point avec map.GetPoint / map.GetPoints
Zone déclencheurUne boîte (position, rotation, taille) qui prévient le Lua quand un joueur ou un prop entre ou sortBoîte orange semi-transparente et étiquetteInvisible ; côté serveur, appelle les hooks TriggerStartTouch / TriggerEndTouch
Entité scriptéeUne entité Lua (entities/<classe>.lua) créée au lancement de la map, avec des propriétés clé = valeurPetit cube violet, flèche et étiquette « nom / classe » (l'éditeur n'exécute pas le Lua, il ne montre pas le modèle de la classe)Créée comme ents.Create(classe) + Spawn()

Inspecteur

ObjetRéglages
TousNom, Position, Rotation
Point nomméCatégorie (tag) : texte libre, 64 caractères au plus, peut rester vide
Zone déclencheurTaille (0,1 à 2000 m par axe), Catégorie (tag)
Entité scriptéeClasse (saisie libre, ou menu « Choisir une classe… » ; un avertissement s'affiche si la classe n'est pas trouvée ici, car elle doit exister là où la map sera jouée) et Propriétés : lignes « clé | valeur », « + Ajouter une propriété » et « × » pour supprimer (32 propriétés au plus, clé de 64 caractères, valeur de 256 ; une clé vide est ignorée ; tout est du texte)
Nom ou catégorie ?Le nom sert à retrouver un objet en Lua (map.GetPoint("objectif_a"), ents.FindByName("porte_1")). Plusieurs objets peuvent porter le même nom, mais pour un groupe (dix points « loot », les spawns d'une équipe), utilise plutôt une catégorie.

Dans le fichier .pcmap

Le format reste en version 1 : une map ancienne s'ouvre telle quelle, et une version plus ancienne de PhysiCore ignore simplement ces objets (les types inconnus sont écartés). Champs communs : id, type, name, pos [x, y, z], rot [x, y, z] (degrés, ordre YXZ).

{ "id": "1a2b3c4d5e6f7a8b", "type": "point", "name": "spawn_rouge", "pos": [12, 0, -4], "rot": [0, 90, 0],
  "tag": "spawn_rouge" }

{ "id": "9f8e7d6c5b4a3f2e", "type": "trigger", "name": "zone_capture", "pos": [0, 1.5, 0], "rot": [0, 0, 0],
  "size": [6, 3, 6], "tag": "" }

{ "id": "0011223344556677", "type": "entity", "name": "distrib_hall", "pos": [3, 0, 2], "rot": [0, 180, 0],
  "class": "distributeur", "keyvalues": { "stock": "10", "objet": "builtin:ball" } }
ChampVérification à la lecture (et à chaque modification en édition à plusieurs)
name, tagTexte, 64 caractères au plus
size0,1 à 2000 par axe (défaut [4, 3, 4])
classTexte de 64 caractères au plus, nom de fichier valide (sinon la valeur précédente est gardée). Vide autorisé : l'entité n'est alors pas créée en jeu (message d'erreur)
keyvaluesObjet { "clé": "valeur" }, 32 paires au plus, clés de 64 caractères (vides ignorées), valeurs converties en texte (256 caractères ; un nombre entier 3 devient "3")

API Lua

Disponible des deux côtés : les points et zones viennent du fichier de la map, que le serveur envoie à chaque joueur. Seuls les hooks et la détection exacte de zone:GetTouching() sont côté serveur. Sur une map intégrée (pas un .pcmap), ces fonctions renvoient des listes vides ou nil.

Quand lire la map ?Côté serveur, les scripts du gamemode se chargent avant la map : pendant le chargement des fichiers et le hook Initialize, map.GetPoints() est encore vide. Utilise le hook InitPostEntity, appelé une fois la map chargée et ses entités créées. Côté client, la map est déjà reçue quand les scripts démarrent chez un joueur qui rejoint, mais pas forcément chez l'hôte : lis-la plutôt dans un hook ou au moment d'en avoir besoin.

map.GetPoints(tag) → table

Liste (indexée à partir de 1) des points nommés, triés par nom. Avec tag, seulement ceux de cette catégorie. Chaque point est une table { name = "…", pos = Vector, ang = Vector (degrés), tag = "…" } (une copie : la modifier ne change pas la map).

map.GetPoint(nom) → table ou nil

Le premier point (dans l'ordre des noms) qui porte ce nom.

map.GetTriggers(tag) → table

Liste des zones, triées par nom ; filtrées par catégorie si tag est donné.

map.GetTrigger(nom) → zone ou nil

La première zone de ce nom.

map.IsInTrigger(nom, pos) → booléen

true si pos est dans au moins une zone portant ce nom (boîte orientée, bords inclus).

ents.FindByName(nom) → table

Les entités scriptées placées dans la map dont le nom (dans l'éditeur) est nom, puis les zones de ce nom. Vide si le nom est vide. Les props et entités créés par le Lua n'ont pas de nom.

Objet zone

Renvoyé par map.GetTriggers, map.GetTrigger et ents.FindByName, et premier argument des hooks de zone. La même zone donne toujours la même table (tu peux y ranger tes champs : zone.equipe = 2), tant que les scripts ne sont pas rechargés.

MéthodeRenvoie
zone:IsValid()true tant que la zone existe et que les scripts tournent (IsValid(zone) marche aussi)
zone:GetName(), zone:GetTag()Nom, catégorie
zone:GetPos(), zone:GetAngles()Centre de la boîte, rotation en degrés
zone:GetSize()Taille de la boîte (Vector, mètres)
zone:IsInside(pos)true si le point est dans la boîte
zone:GetTouching()Joueurs et props dedans. Serveur : ce que la zone détecte (corps qui la touchent, mêmes règles que les hooks). Client : calcul par position (pieds du joueur, centre du prop)
zone:IsTrigger() → true, zone:IsPlayer() → false, zone:GetClass() → "trigger"Pour distinguer une zone d'une entité

Propriétés des entités placées

MéthodeCôtéDescription
ent:GetName()partagéNom donné dans l'éditeur à une entité placée dans la map ; "" sinon
ent:GetKeyValue(clé)partagéValeur (texte) d'une propriété de l'éditeur, ou nil
ent:GetKeyValues()partagéTable { clé = "valeur" } de toutes les propriétés
ent:SetKeyValue(clé, valeur)serveurChange ou ajoute une propriété (convertie en texte, 256 caractères, 32 propriétés au plus), visible par tous, puis appelle ENT:KeyValue(clé, valeur). Seulement sur une entité déjà créée (pas entre ents.Create et Spawn)

ENT:KeyValue(clé, valeur) (fonction de classe, serveur) est appelée pour chaque propriété de l'éditeur avant ENT:Initialize(), comme dans Garry's Mod, puis à chaque ent:SetKeyValue. valeur est toujours du texte : convertis-la avec tonumber.

Hooks

Côté serveur. Comme les autres hooks : hook.Add("TriggerStartTouch", "id", fonction) ou function GM:TriggerStartTouch(zone, ent).

HookQuand
TriggerStartTouch(zone, ent)Un joueur ou un prop (véhicules et entités scriptées compris) entre dans la zone. ent est un joueur ou une entité (ent:IsPlayer())
TriggerEndTouch(zone, ent)Il en sort (aussi quand le prop est supprimé)
InitPostEntity()Une fois par lancement des scripts, quand la map est chargée et ses props et entités placées créés (aussi sur une map intégrée)
PostCleanupMap()Après game.CleanUpMap(), quand les props et entités de la map ont été recréés

Création des entités placées

Au lancement de la map (et après game.CleanUpMap()), pour chaque entité scriptée placée, côté serveur :

  1. la classe doit exister (fichier chargé), sinon un message s'affiche dans la console Lua (« Map : classe d'entité inconnue… ») ; une entité sans classe donne aussi un message ;
  2. le prop est créé avec ENT.Model (petite caisse par défaut) à la position et la rotation de l'éditeur, figé si ENT.Physics = "static" ; le hook PropSpawned est appelé ;
  3. ENT:KeyValue(clé, valeur) pour chaque propriété, dans l'ordre de l'éditeur ;
  4. ENT:Initialize().

Les zones ne sont pas recréées par game.CleanUpMap() : elles restent, avec ce qu'elles contiennent.

Exemples

Spawns d'équipe

-- gamemodes/mon_mode/init.lua : chaque équipe apparaît sur ses points.
hook.Add("PlayerSpawn", "SpawnEquipe", function(ply)
	local tag = ply:Team() == 1 and "spawn_rouge" or "spawn_bleu"
	local points = map.GetPoints(tag)
	if #points > 0 then
		local p = points[math.random(#points)]
		ply:SetPos(p.pos)
	end
end)

Zone de capture

hook.Add("TriggerStartTouch", "Capture", function(zone, ent)
	if zone:GetName() == "zone_capture" and ent:IsPlayer() then
		PrintMessage(ent:Nick() .. " entre dans la zone de capture !")
	end
end)

hook.Add("TriggerEndTouch", "Capture", function(zone, ent)
	if zone:GetName() == "zone_capture" and ent:IsPlayer() then
		ent:ChatPrint("Tu quittes la zone.")
	end
end)

-- Un point par seconde passée dans la zone.
timer.Create("CaptureScore", 1, 0, function()
	local zone = map.GetTrigger("zone_capture")
	if not zone then return end
	for _, ent in ipairs(zone:GetTouching()) do
		if ent:IsPlayer() then ent:AddFrags(1) end
	end
end)

Lave

-- Catégorie « lave » sur plusieurs zones.
hook.Add("TriggerStartTouch", "Lave", function(zone, ent)
	if zone:GetTag() == "lave" then
		if ent:IsPlayer() then ent:Kill() else ent:Remove() end
	end
end)

Entité réglée dans l'éditeur

-- entities/distributeur.lua : propriétés stock = 10, objet = builtin:ball dans l'éditeur.
ENT.Model = "builtin:crate_metal"
ENT.Physics = "static"

if SERVER then
	function ENT:KeyValue(key, value)
		if key == "stock" then self.stock = tonumber(value) or 0 end
	end

	function ENT:Initialize()
		self.stock = self.stock or 5
		self:SetNWInt("stock", self.stock)
	end

	function ENT:Use(ply)
		if self.stock <= 0 then return end
		self.stock = self.stock - 1
		self:SetNWInt("stock", self.stock)
		ents.SpawnProp(self:GetKeyValue("objet") or "builtin:ball", self:GetPos() + Vector(0, 1, 0))
	end
end

Préparer la map

hook.Add("InitPostEntity", "PrepareMap", function()
	local objectif = map.GetPoint("objectif_a")
	if not objectif then print("Cette map n'a pas de point « objectif_a ».") return end
	for _, ent in ipairs(ents.FindByName("distrib_hall")) do
		if not ent.IsTrigger then ent:SetKeyValue("stock", 20) end
	end
end)

Zone sûre affichée au joueur (client)

hook.Add("HUDPaint", "ZoneSafe", function()
	local me = LocalPlayer()
	for _, zone in ipairs(map.GetTriggers("safe")) do
		if zone:IsInside(me:GetPos()) then
			draw.SimpleText("Zone sûre", "Big", ScrW() / 2, 40, Color(120, 255, 120), TEXT_ALIGN_CENTER)
			return
		end
	end
end)

Limites