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/ :

DossierSonsJoué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.wavPas 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.wavChaque tir du toolgun accepté par le serveur
physicore/world/explosion.wav, glass_break.wav, splash.wavExplosions (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.wavBoutons 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")
Qui entend quoi ?Les joueurs n'entendent que les sons qu'ils ont aussi : ceux du jeu, et ceux des addons et gamemodes du Workshop (téléchargés en rejoignant). Les sons d'un addon local (pas publié) ne sont entendus que chez l'hôte.

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.

sound.Play(chemin, position, niveau, hauteur, volume)

Pareil, à une position du monde (un Vector).

Côté client : surface.PlaySound et sound.PlayFile

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.

ChampDéfautRô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
level75Portée, comme le niveau de EmitSound
pitch100Hauteur, ou { min, max } : une hauteur au hasard entre les deux à chaque lecture
volume1De 0 à 1
channel0Gardé (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éthodeEffet
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 canalEffet
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.

OptionValeurDéfaut
type"omni" (dans toutes les directions) ou "spot" (cône, comme un phare)"omni"
colorColorblanc
brightnessintensité, 0 à 641
rangeportée en mètres5
angleouverture du cône en degrés (spot), 1 à 8930
shadowsprojette des ombres (plus coûteux)false
offsetVector : 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ètresorigine de la pièce
directionVector : 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èces sans origine propreBeaucoup de modèles convertis (FBX de GTA, véhicules « ELS ») ont toutes leurs pièces avec l'origine au centre du véhicule : une lumière attachée à « siren1 » se retrouverait au milieu de l'habitacle. Attache-la alors au véhicule (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 :

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.

FonctionCôtéRôle
ply:FlashlightIsOn(), ply:CanUseFlashlight()partagéAllumée ? Autorisée ?
ply:Flashlight(bool)serveurAllume ou éteint la lampe, sans passer par le hook
ply:AllowFlashlight(bool)serveurfalse : la touche F ne marche plus, et la lampe s'éteint
Hook PlayerSwitchFlashlight(ply, allumée)serveurLe 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.

Lumières sans modèlePour une lumière brève à un endroit du monde (éclair d'un tir, explosion) ou un projecteur qui projette une image, sans prop : 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 :

ChampValeur
Hittrue si le rayon a touché quelque chose
HitPos, HitNormalPoint touché et orientation de la surface (Vector)
EntityProp ou joueur touché, sinon nil
HitWorldTouché la map (sol, murs)
StartPos, Normal, FractionDé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