Entités scriptées
Des objets du monde qui ont leur propre comportement en Lua : un bouton, une pièce à ramasser, une zone de capture, un distributeur… Comme les SENT de Garry's Mod.
Déclarer une classe
Une entité se définit dans un fichier entities/<classe>.lua d'un gamemode (ou lua/entities/<classe>.lua d'un addon). Le nom du fichier est le nom de la classe. Pendant le chargement du fichier, la table ENT représente la classe :
-- entities/distributeur.lua
ENT.PrintName = "Distributeur"
ENT.Model = "builtin:crate_metal"
ENT.Physics = "static" -- apparaît figé
ENT.PhysgunDisabled = true -- le physgun ne peut pas l'attraper
if SERVER then
function ENT:Initialize()
self:SetNWInt("stock", 5)
end
function ENT:Use(ply)
local stock = self:GetNWInt("stock", 0)
if stock <= 0 then
ply:ChatPrint("Distributeur vide.")
return
end
self:SetNWInt("stock", stock - 1)
ents.SpawnProp("builtin:ball", self:GetPos() + Vector(0, 1, 0))
end
end
Ces fichiers sont partagés : ils sont chargés côté serveur et côté client. Mets les fonctions serveur dans if SERVER then … end.
Champs
| Champ | Rôle |
|---|---|
ENT.Model | Modèle (identifiant de prop, par exemple "builtin:crate" ou un modèle de pack). Par défaut, une petite caisse. |
ENT.Physics | "static" : l'entité apparaît figée. Sinon elle est physique. |
ENT.PhysgunDisabled | true : le physgun ne l'attrape pas (géré par GM:PhysgunPickup du gamemode Base). |
ENT.AlwaysActive | true : une entité posée dans la map ne s'endort jamais, même sans joueur autour (voir Proximité réseau). |
ENT.PrintName, autres champs | Libres : tu peux ranger tes propres réglages dans la classe. |
ENT.Type | "anim" (par défaut) : un prop normal. "point" : entité logique, invisible, sans collision et figée (minuterie, gestionnaire de manche…). "brush" : zone invisible, traversable et figée qui reçoit StartTouch / Touch / EndTouch (voir Zones de contact). "filter" se comporte comme "point". "nextbot" : un NextBot. |
ENT.Base | Classe dont celle-ci hérite (ses fonctions et champs). "base_nextbot" fait de la classe un NextBot (PNJ dont l'IA est écrite en Lua). |
ENT.Spawnable, ENT.Category | true : dans l'onglet Entités du menu de spawn, rangée dans Category (« Autres » par défaut) sous son PrintName. |
ENT.AdminOnly | true : seuls les admins la font apparaître depuis le menu de spawn ou la recollent depuis une dupe. |
ENT.DisableDuplicator | true : laissée de côté quand on colle une dupe (sauf classe enregistrée avec duplicator.RegisterEntityClass). |
Fonctions appelées par le jeu (serveur)
| Fonction | Quand |
|---|---|
ENT:Initialize() | Juste après ent:Spawn(). |
ENT:Use(ply) | Un joueur appuie sur E en la visant (à moins de 4 m). |
ENT:StartTouch(ent) | Un joueur, un prop (débris compris) ou un PNJ entre dans sa zone de contact (voir Zones de contact). |
ENT:Touch(ent) | À chaque image physique, pour chaque objet encore dans la zone. |
ENT:EndTouch(ent) | L'objet sort de la zone. |
ENT:Think() | Des deux côtés, à chaque image physique (60 fois par seconde) : garde-la légère. self:NextThink(CurTime() + s) saute les appels suivants jusqu'à ce moment (chaque côté a le sien). Côté client, seulement si la classe définit Think. Une entité de la map figée loin de tous les joueurs ne pense plus (voir Proximité réseau). |
ENT:OnRemove() | Juste avant sa suppression. |
ENT:PhysicsCollide(data) | Elle heurte un prop ou un joueur (au plus toutes les 0,05 s). Voir Réagir aux chocs. |
ENT:OnTakeDamage(dmginfo) | Elle reçoit des dégâts : balle, coup, projectile ou explosion d'une arme, util.BlastDamage, ou ent:TakeDamage(montant, attaquant, inflicteur). Appelée après le hook EntityTakeDamage, sauf si un hook a renvoyé true. dmginfo a les méthodes de celui de ScalePlayerDamage : GetDamage(), GetAttacker(), GetInflictor() (l'id de l'arme, texte), GetDamageType(), IsDamageType(DMG_…), IsBulletDamage(), IsExplosionDamage()… (DMG_BULLET pour les balles, DMG_CLUB pour les armes de corps à corps, DMG_BLAST pour les explosions, DMG_GENERIC pour ent:TakeDamage). Le jeu ne retire pas de points de vie tout seul : à toi de les compter avec SetHealth / Health (exemple ci-dessous). |
ENT:KeyValue(clé, valeur) | Pour chaque propriété réglée dans l'éditeur de map, avant Initialize, puis à chaque ent:SetKeyValue. |
ENT:SpawnFunction(ply, tr, classe) | Le joueur la fait apparaître depuis l'onglet Entités : crée l'entité où il vise (tr : trace du regard) et la renvoie. Sans cette fonction, le jeu la pose lui-même. self est la table de la classe. |
ENT:PreEntityCopy() | Avant que duplicator.Copy la lise. |
ENT:SetupDataTables() | Pour chaque entité, des deux côtés : déclarer ses NetworkVar (voir plus bas). |
ENT:UpdateTransmitState() | Après Initialize : TRANSMIT_NEVER cache l'entité aux joueurs (voir plus bas). |
ENT:PostEntityPaste(ply, ent, créées) | Elle vient d'être recréée par le collage d'une dupe (créées : toutes les entités de la dupe). |
Dans ces fonctions, self est l'entité : elle a toutes les méthodes des entités (self:GetPos(), self:Remove(), self:SetNWInt()…) et tu peux y stocker tes propres champs (self.compteur = 0).
-- entities/caisse_fragile.lua : cassée après 50 points de dégâts.
ENT.Model = "builtin:crate"
if SERVER then
function ENT:Initialize()
self:SetMaxHealth(50)
self:SetHealth(50)
end
function ENT:OnTakeDamage(dmg)
self:SetHealth(self:Health() - dmg:GetDamage())
if self:Health() <= 0 then
local attaquant = dmg:GetAttacker()
if IsValid(attaquant) and attaquant:IsPlayer() then
attaquant:ChatPrint("Caisse détruite (" .. tostring(dmg:GetInflictor()) .. ")")
end
self:Remove()
end
end
end
-- ailleurs, côté serveur : 20 points de dégâts sans arme
-- caisse:TakeDamage(20, ply, "piege")
Santé
Chaque entité (prop, entité scriptée, véhicule) a une santé enregistrée et envoyée à tous les joueurs : ent:Health() et ent:GetMaxHealth() des deux côtés (0 par défaut), ent:SetHealth(n) et ent:SetMaxHealth(n) côté serveur (nombres entiers). C'est une simple valeur : le jeu ne la baisse pas quand l'entité prend des dégâts et ne la supprime pas à 0. C'est ENT:OnTakeDamage (ou le hook EntityTakeDamage) qui décide. Règle-la dans ENT:Initialize : avant Spawn, elle n'est pas gardée.
Zones de contact et déclencheurs
Côté serveur, chaque entité scriptée dont la classe définit StartTouch, Touch ou EndTouch (ou qui est un déclencheur, SetTrigger) a une zone de contact : par défaut la boîte de son modèle agrandie de 10 cm. Les joueurs, props (débris compris) et PNJ qui y entrent lancent ENT:StartTouch(ent), puis ENT:Touch(ent) à chaque image physique tant qu'ils y sont, et ENT:EndTouch(ent) quand ils en sortent. Le décor de la map ne compte pas.
| Méthode | Côté | Rôle |
|---|---|---|
ent:SetCollisionBounds(mins, maxs) | serveur | Remplace la zone par une boîte de coins mins et maxs, dans le repère de l'entité (mètres, ±500 au plus). Changer la zone lance EndTouch pour ce qui était dedans ; les objets encore là rentrent aussitôt. |
ent:GetCollisionBounds() | partagé | Deux Vector : les bornes réglées, sinon la boîte du modèle. |
ent:SetTrigger(bool) / ent:GetTrigger() | serveur / partagé | Marque l'entité comme déclencheur (c'est le cas des entités ENT.Type = "brush"). Les entités scriptées ont une zone de contact dans tous les cas : pour une zone que l'on traverse, rends-la aussi non solide (SetSolid(SOLID_NONE), voir Forme et solidité). |
-- entities/zone_soin.lua : une zone invisible de 4 × 3 × 4 m qui soigne.
ENT.Type = "brush"
if SERVER then
function ENT:Initialize()
self:SetCollisionBounds(Vector(-2, 0, -2), Vector(2, 3, 2))
end
function ENT:StartTouch(ent)
if ent:IsPlayer() then ent:ChatPrint("Zone de soin") end
end
function ENT:Touch(ent)
if ent:IsPlayer() and ent:Health() < 100 then ent:SetHealth(ent:Health() + 1) end
end
function ENT:Think()
self:NextThink(CurTime() + 1) -- inutile ici d'y passer 60 fois par seconde
end
end
Les collisions elles-mêmes (forme, solidité, frottement, rebond, collisions au cas par cas avec ShouldCollide) sont dans Monde et physique.
Créer une entité
-- serveur
local ent = ents.Create("distributeur")
ent:SetPos(Vector(0, 0.5, 5))
ent:SetAngles(Vector(0, 90, 0))
ent:Spawn()
Entre ents.Create et Spawn, on peut régler SetPos, SetAngles, SetColor et SetModel. Une entité scriptée est un prop réseau comme les autres : elle apparaît chez tous les joueurs, et ents.FindByClass("distributeur") la retrouve.
Elle a toutes les méthodes de physique des entités (vitesse, forces, groupe de collision, SetParent…) : voir Monde et physique.
Le hook OnEntityCreated(ent) est appelé des deux côtés pour chaque prop, véhicule ou entité scriptée qui apparaît, à l'image suivante (l'entité est alors prête : modèle, classe, position). Une entité scriptée y est déjà passée par ENT:Initialize.
Ragdolls
Côté serveur, ents.Create("prop_ragdoll") prépare un corps qui tombe en ragdoll dès son apparition. Avant Spawn, on règle SetModel (un playermodel "addon:<clé>/playermodels/<fichier>.json", sinon le mannequin), SetPos et SetAngles (seul le yaw compte). Après Spawn, il a les méthodes des PNJ (GetPos, IsValid, Remove…) et GetClass() renvoie "prop_ragdoll".
- C'est un PNJ sans IA : il compte dans la limite
npc_limit(au-delà,Spawnne crée rien et un message s'affiche dans la console), maisnpc.GetAll()ne le liste pas. - Il reste jusqu'à
:Remove(). - Les ragdolls sont simulés par chaque joueur : la pose des membres n'est pas la même partout, et il n'y a pas de
GetRagdollEntity.
-- serveur : un corps tombe devant le joueur.
local corps = ents.Create("prop_ragdoll")
corps:SetPos(ply:GetPos() + ply:GetForward() * 3 + Vector(0, 1, 0))
corps:Spawn()
timer.Simple(30, function() if IsValid(corps) then corps:Remove() end end)
Pour faire tomber un joueur sans le tuer : ply:CreateRagdoll().
Menu de spawn et dupes
Avec ENT.Spawnable = true, l'entité apparaît dans l'onglet Entités du menu de spawn ; le clic passe par le hook PlayerSpawnSENT, puis ENT:SpawnFunction (ou la pose par défaut, sur la surface visée, tournée vers le joueur). Elle appartient au joueur et s'annule avec Annuler.
Dans une dupe, une entité scriptée est recréée en Lua : par la fonction de duplicator.RegisterEntityClass si sa classe en a une (ses champs nommés passent dans la dupe), sinon par une duplication générique (modèle, position, angles, couleur), sauf avec ENT.DisableDuplicator. Tout est expliqué, avec un exemple complet, dans Intégration au bac à sable.
Dessin, réseau et modèles client
Variables réseau typées : SetupDataTables et NetworkVar
Si la classe a une fonction ENT:SetupDataTables(), elle est appelée pour chaque entité de la classe, des deux côtés. Dedans, self:NetworkVar(type, numéro, nom) (ou self:NetworkVar(type, nom)) crée self:Get<nom>() et self:Set<nom>(valeur). La valeur est rangée dans la variable réseau "DT_<nom>" : changée par le serveur, elle est envoyée à tous les joueurs. Types : "String", "Bool", "Float", "Double", "Int", "UInt", "Vector", "Angle", "Entity". Le numéro est ignoré (gardé pour les scripts de GMod).
self:NetworkVarNotify(nom, fonction) appelle fonction(ent, nom, ancienne, nouvelle) à chaque Set<nom>, juste avant le changement, dans le realm qui l'appelle (pas quand la valeur arrive par le réseau). Côté client, Set<nom> ne change rien : il lance seulement ces fonctions.
-- lua/entities/compteur.lua
ENT.PrintName = "Compteur"
ENT.Spawnable = true
function ENT:SetupDataTables()
self:NetworkVar("Int", 0, "Clics")
self:NetworkVar("Entity", 1, "Dernier")
if SERVER then
self:NetworkVarNotify("Clics", function(ent, nom, avant, apres)
if apres >= 10 then PrintMessage("Le compteur a atteint 10 !") end
end)
end
end
if SERVER then
function ENT:Use(ply)
self:SetClics(self:GetClics() + 1)
self:SetDernier(ply)
end
end
Dessiner soi-même : ENT:Draw
Côté client, si la classe a ENT:Draw() ou ENT:DrawTranslucent(), elles sont appelées à chaque image pour chaque entité de la classe, et le modèle n'est affiché que si l'une d'elles appelle self:DrawModel() pendant cette image. Sans ces fonctions, le modèle est affiché normalement. Pour écrire au-dessus d'une entité, utilise plutôt cam.Start3D2D dans le hook PostDrawOpaqueRenderables (il ne marche pas dans ENT:Draw).
-- Partagé : le compteur n'est visible que de près (15 m), et clignote après 10 clics.
if CLIENT then
function ENT:Draw()
if LocalPlayer():GetPos():Distance(self:GetPos()) > 15 then return end
if self:GetClics() >= 10 and math.floor(CurTime() * 4) % 2 == 0 then return end
self:DrawModel()
end
end
Entités du serveur seulement : UpdateTransmitState
Côté serveur, juste après ENT:Initialize, le jeu appelle ENT:UpdateTransmitState() si la classe l'a. Si elle renvoie TRANSMIT_NEVER, l'entité est invisible et sans collision chez les joueurs connectés (l'hôte d'une partie hébergée, qui est le serveur, la voit toujours) : pratique pour une entité logique (minuterie, zone, gestionnaire de manche). TRANSMIT_ALWAYS et TRANSMIT_PVS laissent l'entité normale.
function ENT:UpdateTransmitState() return TRANSMIT_NEVER end
Modèles client : ents.CreateClientProp
Côté client, ents.CreateClientProp(modèle) crée un modèle que seul ce joueur voit, sans physique (512 au plus). Il apparaît après :Spawn(). Méthodes : SetModel / GetModel, SetPos / GetPos, SetAngles / GetAngles, SetColor / GetColor, SetNoDraw / GetNoDraw, SetModelScale / GetModelScale (0,01 à 50), SetParent(ent) (suit un prop, un PNJ ou un joueur en gardant l'écart actuel : place-le d'abord, après Spawn), DrawModel, Remove, IsValid, GetClass ("class C_PhysPropClientside"). ClientsideModel existe toujours, avec ses propres méthodes.
Supprimer sans erreur
SafeRemoveEntity(ent) supprime l'entité si elle existe encore (rien sinon) ; SafeRemoveEntityDelayed(ent, délai) fait de même après délai secondes.
Os, points d'attache, animations, sous-matériaux, mode de rendu, feu et désintégration : voir Os, animations et effets.
Placer une entité dans une map
Dans l'éditeur de map (onglet Entités), le bouton Entité scriptée et la liste des classes posent une entité qui sera créée au lancement de la map, avec un nom et des propriétés clé = valeur. La classe les reçoit dans ENT:KeyValue avant ENT:Initialize :
if SERVER then
function ENT:KeyValue(key, value)
if key == "stock" then self.stock = tonumber(value) or 0 end
end
end
En Lua : ent:GetName(), ent:GetKeyValue(clé), ent:GetKeyValues(), ent:SetKeyValue(clé, valeur) (serveur) et ents.FindByName(nom). Voir Points, zones et entités de map.
Proximité réseau
Une map peut contenir des milliers d'entités (toutes les portes d'une ville, par exemple). Pour qu'elles ne coûtent rien quand personne n'est là, les props et entités de la map figés ne marchent qu'à proximité des joueurs, quatre fois par seconde :
| Où | Quand | Effet |
|---|---|---|
| Serveur | Aucun joueur vivant à moins de 60 m (réveil quand un joueur revient à moins de 60 m, sommeil au-delà de 70 m) | En sommeil : plus de calcul physique, plus de zone de contact, plus de ENT:Think. Une position ou un angle changés par Lua pendant ce temps sont quand même envoyés aux joueurs. |
| Client (chaque joueur) | À plus de 150 m de la caméra, si l'objet mesure moins de 8 m | Caché, plus suivi, plus de ENT:Think client. Réaffiché en dessous de 140 m. |
Ne sont jamais concernés : les props et entités créés en jeu (par les joueurs ou par Lua avec ents.Create), les props non figés, attachés (SetParent) et les véhicules. Une animation lancée par Think (une porte qui s'ouvre) reprend où elle en est au réveil : basée sur CurTime(), elle se termine aussitôt. Une classe qui doit continuer à travailler sans personne autour (minuterie, générateur) déclare ENT.AlwaysActive = true.
ents.FindByClass utilise un index par classe (refait seulement quand une entité apparaît, disparaît ou change de classe) : l'appeler à chaque image pour quelques classes reste léger même avec des milliers d'entités. ents.GetAll() parcourt toujours tout.