Sons et lumières
Faire du bruit (sirènes, klaxons, alarmes), nommer des sons avec sound.Add et les filtrer avec EntityEmitSound, allumer des lumières qui suivent une pièce d'un modèle (gyrophares, phares), faire briller ou cacher des pièces, gérer la lampe torche des joueurs, et savoir ce que vise un joueur.
Les fichiers son
Range tes sons dans un dossier sound/ à la racine de ton addon (ou de ton gamemode), en .ogg, .wav ou .mp3. Un script désigne un son par son chemin dans sound/ :
mon_addon/
└── sound/
└── mon_addon/
├── sirene.ogg → "mon_addon/sirene.ogg"
└── klaxon.wav → "mon_addon/klaxon.wav"
Le jeu cherche le chemin dans le gamemode en cours (puis ses bases), puis dans tous les addons activés : un addon peut donc jouer les sons d'un pack de sons. Pour éviter qu'un autre addon ait un fichier du même nom, range tes sons dans un sous-dossier à ton nom, comme ci-dessus.
Sons fournis avec le jeu, chemins qui commencent par physicore/ : physicore/bip.wav, physicore/clic.wav, physicore/sirene.wav (se répète sans coupure), physicore/klaxon.wav, physicore/alarme.wav.
Sons des armes du jeu, réutilisables dans tes scripts : physicore/weapons/ suivi de pistol_fire.wav, shotgun_fire.wav, shotgun_pump.wav, rifle_fire.wav, reload_pistol.wav, reload_shotgun.wav, reload_rifle.wav, dry_fire.wav, crowbar_swing.wav, crowbar_hit.wav, hit_flesh.wav, impact_hard.wav, impact_metal.wav, impact_wood.wav ou equip.wav.
Depuis octobre 2026, tous les sons du jeu viennent de paquets libres de droits (CC0 : Kenney, OpenGameArt, BigSoundBank ; liste dans sound/CREDITS.txt), assemblés par tools/build_cc0_sounds.py. Ils sont tous utilisables en Lua avec le préfixe physicore/ :
| Dossier | Sons | Joués par le jeu |
|---|---|---|
physicore/player/ | footstep_<surface>_1.wav à _5.wav (surfaces concrete, wood, metal, grass, carpet, snow), jump.wav, land.wav, pain_1.wav à pain_3.wav, death_1.wav, death_2.wav, respawn.wav, water_enter.wav, water_exit.wav | Pas selon le sol (terrain : herbe ; props et décor en bois ou en métal), saut, atterrissage, dégâts, mort, réapparition, entrée et sortie de l'eau. Entendus de tous les joueurs |
physicore/props/ | impact_<matière>_soft_1.wav à _3.wav et impact_<matière>_hard_1.wav à _3.wav (matières wood, metal, plastic, rubber, concrete, glass) | Chocs des props selon leur matière physique ("physics" de props.json) : léger au-dessus de 1,5 m/s, fort au-dessus de 6 m/s |
physicore/physgun/ | grab.wav, drop.wav, freeze.wav, hold_loop.wav (boucle) | Physgun : attraper, lâcher, figer, bourdonnement pendant qu'il tient un objet |
physicore/toolgun/ | shoot.wav | Chaque tir du toolgun accepté par le serveur |
physicore/world/ | explosion.wav, glass_break.wav, splash.wav | Explosions (si l'arme n'a pas son propre son), vitres brisées, prop qui tombe dans l'eau |
physicore/ui/ | click.wav, hover.wav, menu_open.wav, menu_close.wav, notify.wav, error.wav, chat.wav, spawn.wav, remove.wav | Boutons des menus (clic et survol), menu de spawn, notifications, messages du chat, apparition d'un prop, annulation (Z) |
-- Exemple : un bruit de pas sur du métal joué par une entité
self:EmitSound("physicore/player/footstep_metal_" .. math.random(1, 5) .. ".wav")
Jouer un son
ent:EmitSound(chemin, niveau, hauteur, volume)
Joue un son une fois, depuis un prop ou un joueur (il suit l'entité). Côté serveur, tous les joueurs l'entendent ; côté client, seulement ce joueur.
niveau: portée, 75 par défaut (≈ 90 m) ; 100 porte plus loin, 60 très près.hauteur: 100 = normale, 50 = une octave plus grave, 200 = une octave plus aiguë.volume: de 0 à 1.
sound.Play(chemin, position, niveau, hauteur, volume)
Pareil, à une position du monde (un Vector).
Côté client : surface.PlaySound et sound.PlayFile
surface.PlaySound(chemin)joue un son une fois, aux oreilles du joueur (interface, alerte), chez lui seulement.sound.PlayFile(chemin, options, fonction)prépare un son aux oreilles du joueur et appelle tout de suitefonction(son, 0, ""), oufonction(nil, 2, "son introuvable").sonest un objetCreateSound(Play,Stop,SetLooping,ChangeVolume…) qui ne démarre pas tout seul : appelleson:Play().optionsest ignoré. Pour un fichier sur internet, voirsound.PlayURL.
Les deux acceptent un chemin de sound/ ou un nom de son scripté.
-- Client : musique de victoire en boucle, coupée en fondu au bout de 10 s.
sound.PlayFile("mon_mode/victoire.ogg", "", function(son, code, erreur)
if not son then print("Musique :", erreur) return end
son:SetLooping(true)
son:PlayEx(0.6, 100)
timer.Simple(10, function() son:ChangeVolume(0, 2) end)
end)
surface.PlaySound("physicore/ui/notify.wav")
Sons scriptés : sound.Add
Comme dans GMod, sound.Add donne un nom à un son, ou à plusieurs fichiers parmi lesquels le jeu tire au hasard, avec sa portée, sa hauteur et son volume. Le nom s'utilise ensuite partout à la place d'un chemin : ent:EmitSound, sound.Play, CreateSound, surface.PlaySound, sound.PlayFile, Primary.Sound d'une arme Lua.
| Champ | Défaut | Rôle |
|---|---|---|
name | — | Le nom (obligatoire), par exemple "MonMode.Tir" |
sound | — | Un chemin, ou une liste de chemins (32 au plus) : un au hasard à chaque lecture. Les préfixes de GMod ), ^, * sont retirés |
level | 75 | Portée, comme le niveau de EmitSound |
pitch | 100 | Hauteur, ou { min, max } : une hauteur au hasard entre les deux à chaque lecture |
volume | 1 | De 0 à 1 |
channel | 0 | Gardé (rendu par GetProperties), sans effet |
Les réglages du son scripté s'appliquent quand EmitSound / sound.Play reçoit les valeurs par défaut (niveau 75, hauteur 100, volume 1) ; une autre valeur passée à l'appel l'emporte. CreateSound ne prend que le fichier (tiré au hasard à la création). Un son scripté n'existe que du côté où sound.Add a été appelé : mets-le dans un fichier partagé. 4 096 noms au plus.
sound.GetTable() renvoie la liste triée des noms ; sound.GetProperties(nom) renvoie { name, sound, level, volume, pitch = { min, max }, channel } (sound est un texte s'il n'y a qu'un fichier, sinon une liste), ou nil.
-- shared.lua : trois variantes de pas sur du métal, légèrement désaccordées.
sound.Add({
name = "MonMode.PasMetal",
sound = { "physicore/player/footstep_metal_1.wav", "physicore/player/footstep_metal_2.wav", "physicore/player/footstep_metal_3.wav" },
level = 70,
pitch = { 92, 108 },
volume = 0.8,
})
-- serveur : joué chez tous, un fichier au hasard à chaque fois.
ent:EmitSound("MonMode.PasMetal")
Hook EntityEmitSound
EntityEmitSound(data) est appelé avant chaque ent:EmitSound et sound.Play, du côté qui le joue (serveur : avant l'envoi à tous ; client : pour ce joueur). data contient SoundName, OriginalSoundName, Entity (nil pour sound.Play), Pos, SoundLevel, Pitch, Volume (réglages du son scripté déjà appliqués). Renvoyer false empêche le son ; renvoyer true le joue avec les champs que le hook a changés ; ne rien renvoyer le joue tel quel. Les sons joués par le jeu lui-même (pas, chocs, physgun) et CreateSound n'y passent pas.
-- Serveur : voix plus grave pour l'équipe des géants, aucun son pendant la préparation.
hook.Add("EntityEmitSound", "sons_geants", function(data)
if GetGlobalBool("preparation") then return false end
local ent = data.Entity
if IsValid(ent) and ent:IsPlayer() and ent:Team() == 3 then
data.Pitch = data.Pitch * 0.7
return true
end
end)
Sons contrôlables : CreateSound
CreateSound(ent, chemin) crée un son attaché à une entité, qu'on peut lancer, arrêter, faire boucler et moduler. Côté serveur, il est joué chez tous les joueurs, y compris ceux qui arrivent pendant qu'il joue ; côté client, chez ce joueur seulement. Il s'arrête avec l'entité.
| Méthode | Effet |
|---|---|
snd:Play() | Joue depuis le début |
snd:PlayEx(volume, hauteur) | Joue avec ce volume et cette hauteur |
snd:Stop() | Arrête |
snd:SetLooping(bool) | Boucle (une sirène, un moteur). Les fichiers .wav, .ogg et .mp3 bouclent sans coupure : un son court (Yelp, sirène rapide) se répète parfaitement s'il contient un nombre entier de cycles |
snd:ChangePitch(hauteur, durée) | Change la hauteur (100 = normale), en glissant pendant durée secondes (0 = tout de suite) |
snd:ChangeVolume(volume, durée) | Change le volume (0 à 1), en fondu |
snd:SetSoundLevel(niveau) | Portée, à régler avant le premier Play |
snd:IsPlaying(), snd:GetPitch(), snd:GetVolume() | État |
-- Serveur : une voiture de police avec sa sirène.
local voiture = ents.SpawnProp("addon:ws_3123456789/models/police.glb", Vector(0, 1, 0))
local sirene = CreateSound(voiture, "physicore/sirene.wav")
sirene:SetLooping(true)
sirene:Play()
-- Plus tard : la sirène monte puis s'éteint en fondu.
sirene:ChangePitch(130, 2)
timer.Simple(5, function() sirene:ChangeVolume(0, 1.5) end)
Son depuis internet : sound.PlayURL
Côté client, sound.PlayURL(url, options, fonction) télécharge un fichier son complet (.mp3, .ogg ou .wav, 32 Mo au plus), puis appelle fonction(canal), ou fonction(nil, code, raison) en cas d'échec. Les radios en flux continu (qui ne finissent jamais) ne marchent pas. Il faut que le serveur autorise les requêtes web (sv_allow_http 1, domaines de sv_http_whitelist). options : "3d" (son placé dans le monde, avec canal:SetPos), "noplay" (ne démarre pas tout seul) ; "noblock" est accepté et ignoré. 32 sons ouverts au plus.
| Méthode du canal | Effet |
|---|---|
Play(), Pause(), Stop() | Joue (reprend après une pause), met en pause, arrête et libère le canal |
SetVolume(v) / GetVolume() | Volume (1 = normal, 4 au plus) |
SetPos(pos) / GetPos(), Set3DFadeDistance(près, loin), Is3D() | Position et portée (mètres) d'un son "3d" |
SetTime(s) / GetTime(), GetLength() | Position de lecture et durée (secondes) |
EnableLooping(bool) / IsLooping() | Boucle |
SetPlaybackRate(v) / GetPlaybackRate() | Vitesse et hauteur (0,05 à 4) |
GetState() | GMOD_CHANNEL_PLAYING, GMOD_CHANNEL_PAUSED ou GMOD_CHANNEL_STOPPED (GMOD_CHANNEL_STALLED existe aussi) |
IsValid(), GetFileName(), IsOnline() | Canal encore là ; l'URL ; toujours false |
-- Client : une musique d'ambiance en boucle.
sound.PlayURL("https://exemple.fr/musique.ogg", "", function(canal, code, raison)
if not IsValid(canal) then print("Musique :", raison) return end
canal:SetVolume(0.4)
canal:EnableLooping(true)
end)
Lumières attachées à une pièce
ents.CreateClientLight(ent, pièce, options) (client) crée une vraie lumière qui éclaire autour et se reflète, attachée à une pièce du modèle (voir ent:GetNodeNames()), ou à l'entité elle-même si pièce vaut nil. Elle suit la pièce quand le prop bouge.
| Option | Valeur | Défaut |
|---|---|---|
type | "omni" (dans toutes les directions) ou "spot" (cône, comme un phare) | "omni" |
color | Color | blanc |
brightness | intensité, 0 à 64 | 1 |
range | portée en mètres | 5 |
angle | ouverture du cône en degrés (spot), 1 à 89 | 30 |
shadows | projette des ombres (plus coûteux) | false |
offset | Vector : position de la lumière dans le repère de la pièce (ou du véhicule / prop si pièce vaut nil), en mètres | origine de la pièce |
direction | Vector : direction où éclaire un spot, dans ce même repère (ex. Vector(0, -0.1, -1) = vers l'avant d'un véhicule, un peu vers le sol) | -Z de la pièce |
Un spot éclaire vers l'axe -Z de la pièce dans le jeu, c'est-à-dire son axe +Y local dans Blender (l'export glTF fait la conversion). Le plus simple : ajoute dans Blender un objet vide (« Phare_Gauche »), tourne-le pour que son +Y pointe là où le phare doit éclairer, et attache la lumière à lui. La lumière renvoyée a :SetEnabled(bool), :IsEnabled(), :SetColor(Color), :SetBrightness(n), :SetRange(m), :SetAngle(degrés), :SetShadows(bool), :SetOffset(Vector), :SetDirection(Vector) (un gyrophare tournant : change la direction à chaque image), :Remove() et :IsValid(). 64 lumières au plus par joueur ; elles disparaissent avec le prop ou au rechargement des scripts. Pour une lumière qui n'est attachée à rien, voir DynamicLight.
pièce = nil) et place-la avec offset, dans le même repère que vehicle.json (mètres, Y vers le haut, avant vers -Z).-- Projecteur « takedown » sur le toit, qui éclaire vers l'avant.
local takedown = ents.CreateClientLight(voiture, nil, {
type = "spot", color = Color(255, 245, 235), brightness = 8, range = 45, angle = 30,
offset = Vector(0, 1.74, -0.1), direction = Vector(0, -0.12, -1),
})
Exemple : gyrophares
Côté client, tout est local : aucun message réseau, et chaque joueur voit le même clignotement s'il démarre en même temps (ici, calé sur CurTime(), qui suit l'horloge du serveur).
-- cl_init.lua ou lua/autorun/client/gyros.lua
local gyros = {} -- voiture -> { rouge = lumière, bleu = lumière }
local function equiper(voiture)
gyros[voiture] = {
rouge = ents.CreateClientLight(voiture, "Gyro_Rouge_1", { color = Color(255, 30, 30), brightness = 4, range = 8 }),
bleu = ents.CreateClientLight(voiture, "Gyro_Bleu_1", { color = Color(40, 80, 255), brightness = 4, range = 8 }),
}
end
hook.Add("Think", "gyros", function()
local rouge = math.floor(CurTime() * 2.5) % 2 == 0
for voiture, l in pairs(gyros) do
if not IsValid(voiture) or not l.rouge:IsValid() then
gyros[voiture] = nil
else
l.rouge:SetEnabled(rouge)
l.bleu:SetEnabled(not rouge)
voiture:SetNodeEmission("Gyro_Rouge_1", rouge and Color(255, 30, 30) or nil, 6)
voiture:SetNodeEmission("Gyro_Bleu_1", (not rouge) and Color(40, 80, 255) or nil, 6)
end
end
end)
-- Équipe les voitures dont le serveur a allumé les gyros (variable réseau).
timer.Create("gyros_scan", 1, 0, function()
for _, ent in ipairs(ents.GetAll()) do
if ent:GetNWBool("gyros") and not gyros[ent] and ent:LookupNode("Gyro_Rouge_1") then
equiper(ent)
end
end
end)
Côté serveur, il suffit alors de voiture:SetNWBool("gyros", true).
Pour un système complet façon ELS de GTA V / FiveM (patterns, sirènes Wail / Yelp / Phaser / Hi-Lo, airhorn, takedown, alley, cruise, HUD), l'addon local ELS (addons/els) sert d'exemple : chaque véhicule y est décrit par une table Lua (pièces, couleurs, positions des lumières) avec ELS.Register.
Pièces du modèle : effets locaux
ent:SetNodeVisible(nom, bool) et ent:SetNodeEmission(nom, couleur, intensité) marchent des deux côtés :
- serveur : le changement est envoyé à tous les joueurs (et à ceux qui arrivent) ;
- client : seulement chez ce joueur, par-dessus l'état venu du serveur (une émission
nilou d'intensité 0 force la pièce éteinte).ent:ClearNodeOverrides(nom)retire les effets locaux d'une pièce (sans nom : toutes) pour revenir à l'état du serveur.
Les effets locaux disparaissent au rechargement des scripts. Voir aussi Pièces du modèle en Lua.
Lampe torche
Chaque joueur a une lampe torche intégrée : touche F (modifiable dans Paramètres → Touches). C'est une lampe sur la tête du joueur, qui suit sa vue et que tout le monde voit.
| Fonction | Côté | Rôle |
|---|---|---|
ply:FlashlightIsOn(), ply:CanUseFlashlight() | partagé | Allumée ? Autorisée ? |
ply:Flashlight(bool) | serveur | Allume ou éteint la lampe, sans passer par le hook |
ply:AllowFlashlight(bool) | serveur | false : la touche F ne marche plus, et la lampe s'éteint |
Hook PlayerSwitchFlashlight(ply, allumée) | serveur | Le joueur appuie sur la touche ; false refuse |
-- Mode horreur : lampe allumée d'office, mais elle s'éteint au bout de 30 secondes.
hook.Add("PlayerSpawn", "horreur_lampe", function(ply)
ply:AllowFlashlight(true)
ply:Flashlight(true)
timer.Simple(30, function()
if IsValid(ply) then ply:Flashlight(false) ply:AllowFlashlight(false) end
end)
end)
Pour d'autres effets visuels côté client (particules, décalcomanies, contours, effets d'écran, modèles visibles par un seul joueur), voir Rendu 3D et effets.
DynamicLight(id) et ProjectedTexture() (client).Ce que vise un joueur
ply:GetEyeTrace()
Lance un rayon depuis les yeux du joueur, dans la direction où il regarde (1000 m au plus), et renvoie une table :
| Champ | Valeur |
|---|---|
Hit | true si le rayon a touché quelque chose |
HitPos, HitNormal | Point touché et orientation de la surface (Vector) |
Entity | Prop ou joueur touché, sinon nil |
HitWorld | Touché la map (sol, murs) |
StartPos, Normal, Fraction | Départ, direction, part du trajet parcourue (0 à 1) |
Pour choisir le départ, la longueur ou ce qui est ignoré, utilise util.TraceLine. Aussi : ply:EyePos() et ply:GetAimVector(). Côté client, pour LocalPlayer(), c'est la caméra exacte ; pour les autres joueurs (et côté serveur), la visée transmise par le réseau.
-- Serveur : /klaxon fait klaxonner la voiture que l'on vise.
concommand.Add("klaxon", function(ply)
local tr = ply:GetEyeTrace()
-- Fraction × 1000 = distance en mètres.
if tr.Entity and not tr.Entity:IsPlayer() and tr.Fraction * 1000 < 10 then
tr.Entity:EmitSound("physicore/klaxon.wav")
end
end)
ents.FindInSphere(position, rayon)
Props et joueurs dont la position est à moins de rayon mètres (10 000 au plus). Des deux côtés.
for _, ent in ipairs(ents.FindInSphere(Vector(0, 0, 0), 5)) do
if ent:IsPlayer() then ent:ChatPrint("Tu es dans la zone !") end
end