El Feedback Visual
En el capítol anterior vam aconseguir que els enemics morissin i caiguessin. Funciona, però no hi ha feedback quan reben dany. Perquè un joc se senti professional, necessitem Feedback Visual immediat. Això és el que a la indústria s’anomena “Game Juice”.
L’eina més potent per aconseguir aquest efecte visual són els Shaders.
1. Què és un Shader?
Un Shader és un petit programa que no s’executa a la CPU, sinó a la GPU (la targeta gràfica).
- CPU: Executa el teu codi GDScript. Processa la lògica del joc (moviment, col·lisions, puntuació).
- GPU: Dibuixa els píxels a la pantalla. És un processador especialitzat en operacions gràfiques massives.
Quan escrius un Shader, estàs donant instruccions directes a la GPU sobre com pintar cada píxel.
2. Arquitectura: Nuclis i Paral·lelisme
Per què usar la GPU en lloc de la CPU per a efectes visuals?
CPU: Pocs Nuclis, Molt Potents
Una CPU moderna com l’Intel Core i7-14700K té:
- 20 nuclis físics (8 d’alt rendiment + 12 d’eficiència).
- 28 fils (threads) gràcies a Hyper-Threading.
Cada nucli pot executar instruccions complexes i diferents entre si. Però fins i tot amb 28 fils, si necessites processar 10.000 píxels, la CPU hauria de repartir-los entre aquests fils i executar-los en lots seqüencials.
GPU: Milers de Nuclis, Molt Simples
Una GPU moderna com la NVIDIA RTX 4080 té:
- 9.728 nuclis CUDA.
- Cada nucli és molt més simple que un de CPU.
- Però poden executar la mateixa instrucció en milers de dades diferents alhora.
Aquesta arquitectura s’anomena SIMD (Single Instruction, Multiple Data): una sola instrucció aplicada a moltes dades en paral·lel.
L’Exemple Concret
Si el teu sprite de l’enemic mesura 100x100 píxels, són 10.000 píxels en total.
| Processador | Fils Paral·lels | Operacions per Lot |
|---|---|---|
| CPU (i7-14700K) | ~28 | 10.000 ÷ 28 = ~357 lots |
| GPU (RTX 4080) | ~9.728+ | 10.000 ÷ 9.728 = ~1 lot |
La GPU pot processar tots els píxels pràcticament d’una sola passada. Per això els Shaders són tan ràpids per a efectes visuals.
3. GLSL: El Llenguatge de la GPU
Els Shaders a Godot s’escriuen en un llenguatge anomenat GLSL (OpenGL Shading Language), similar a C. No t’espantis. Anem a explicar cada concepte abans d’usar-lo.
shader_type
Tot shader de Godot comença amb una declaració de tipus:
shader_type canvas_item;shader_type canvas_item;Què significa?
shader_type canvas_item: Aquest shader és per a elements 2D (sprites, nodes de canvas).- Existeixen altres tipus com
spatial(3D) oparticles, però en aquest curs usaremcanvas_item.
fragment()
És la funció principal del shader. S’executa un cop per cada píxel del sprite.
void fragment() {
// Aquest codi s'executa per CADA píxel
// La teva feina aquí: calcular el color final del píxel
}void fragment() {
// Aquest codi s'executa per CADA píxel
// La teva feina aquí: calcular el color final del píxel
}Important: Si el teu sprite té 10.000 píxels, aquesta funció s’executa 10.000 vegades en paral·lel.
COLOR
És la variable de sortida obligatòria. És on guardes el color final que tindrà el píxel.
void fragment() {
COLOR = vec4(1.0, 0.0, 0.0, 1.0); // Pinta TOT el sprite de vermell sòlid
}void fragment() {
COLOR = vec4(1.0, 0.0, 0.0, 1.0); // Pinta TOT el sprite de vermell sòlid
}vec4
Un color en GLSL es representa amb 4 números del 0.0 a l’1.0:
| Component | Significat | Rang |
|---|---|---|
| R (Red) | Quantitat de vermell | 0.0 - 1.0 |
| G (Green) | Quantitat de verd | 0.0 - 1.0 |
| B (Blue) | Quantitat de blau | 0.0 - 1.0 |
| A (Alpha) | Transparència | 0.0 (invisible) - 1.0 (sòlid) |
Exemples:
vec4(1.0, 1.0, 1.0, 1.0)= Blanc opacvec4(0.0, 0.0, 0.0, 1.0)= Negre opacvec4(1.0, 0.0, 0.0, 0.5)= Vermell semitransparent
TEXTURE
És una variable especial que referencia la imatge del sprite (la textura que vas carregar a Sprite2D).
No pots usar-la directament com a color. Necessites “llegir-la” amb la funció texture().
UV
Són les coordenades normalitzades de cada píxel dins del sprite.
- Van de
(0.0, 0.0)(cantó superior esquerre) a(1.0, 1.0)(cantó inferior dret). - Cada fil d’execució (cada píxel) té el seu propi valor d’
UV. - El fil del píxel central tindria
UV = (0.5, 0.5).
Nota tècnica: UV és als shaders el que self.position és als scripts. Cada fil sap la seva pròpia coordenada.
texture()
És una funció que llegeix el color d’un píxel d’una imatge donada una coordenada.
vec4 color = texture(TEXTURE, UV);vec4 color = texture(TEXTURE, UV);Desglossament:
TEXTURE: La imatge del sprite.UV: La coordenada d’aquest píxel concret.- Resultat: Retorna el
vec4(color RGBA) del píxel en aquella posició de la imatge.
Amb això, cada un dels 10.000 fils llegeix el seu propi color de la imatge original, reconstruint l’sprite complet.
4. La Missió: Hit Flash
Volem un efecte on, en rebre dany, l’enemic parpellegi en blanc pur durant una fracció de segon.
Preparant el Material
- Selecciona el
Sprite2Dde l’enemic. - A l’Inspector, busca
CanvasItem > Material. - Fes clic al desplegable i selecciona
New ShaderMaterial. - Fes clic en el nou material i a
Shader, seleccionaNew Shader. - Crea la carpeta
shadersal teu projecte i guarda l’arxiu comshaders/hit_flash.gdshader.
Fase 1: Estructura Buida
Comencem amb l’estructura mínima d’un shader 2D:
shader_type canvas_item;
void fragment() {
// Per ara, no fem res
// L'sprite es veurà normal
}shader_type canvas_item;
void fragment() {
// Per ara, no fem res
// L'sprite es veurà normal
}Si proves ara, l’sprite es veurà invisible perquè no hem assignat res a COLOR.
Fase 2: Recuperar el Color Original
Perquè l’sprite es vegi normal, hem de llegir la seva textura i assignar-la a COLOR:
shader_type canvas_item;
void fragment() {
// Llegim el color original de l'sprite en aquesta coordenada (UV)
vec4 color_original = texture(TEXTURE, UV);
// Assignem aquest color com a sortida
COLOR = color_original;
}shader_type canvas_item;
void fragment() {
// Llegim el color original de l'sprite en aquesta coordenada (UV)
vec4 color_original = texture(TEXTURE, UV);
// Assignem aquest color com a sortida
COLOR = color_original;
}Ara l’sprite es veu exactament igual que abans del shader. Hem “reconstruït” la imatge original.
Fase 3: Afegir la Variable de Control (Uniform)
Necessitem una forma d’activar/desactivar el flash des del codi GDScript.
Per això usem una variable uniform:
shader_type canvas_item;
// Variable GLOBAL: la mateixa per a tots els píxels
// La controlarem des de GDScript
uniform bool flash_active = false;
void fragment() {
vec4 color_original = texture(TEXTURE, UV);
COLOR = color_original;
}shader_type canvas_item;
// Variable GLOBAL: la mateixa per a tots els píxels
// La controlarem des de GDScript
uniform bool flash_active = false;
void fragment() {
vec4 color_original = texture(TEXTURE, UV);
COLOR = color_original;
}Per què uniform?
- És una variable el valor de la qual ve de fora del shader (des de GDScript/CPU).
- Tots els 10.000 fils reben el mateix valor. Per això s’anomena “uniforme”.
- A diferència d’
UV(que és diferent per a cada fil),flash_activeseràtrueofalseper a TOTS alhora.
Fase 4: La Lògica del Flash
Ara afegim la lògica condicional. Si flash_active és true, pintem blanc. Si no, pintem normal:
shader_type canvas_item;
uniform bool flash_active = false;
void fragment() {
vec4 color_original = texture(TEXTURE, UV);
if (flash_active) {
// Si està actiu, pintem BLANC
// Usem la transparència original (color_original.a) per no pintar píxels invisibles
COLOR = vec4(1.0, 1.0, 1.0, color_original.a);
} else {
// Si no, pintem el color normal
COLOR = color_original;
}
}shader_type canvas_item;
uniform bool flash_active = false;
void fragment() {
vec4 color_original = texture(TEXTURE, UV);
if (flash_active) {
// Si està actiu, pintem BLANC
// Usem la transparència original (color_original.a) per no pintar píxels invisibles
COLOR = vec4(1.0, 1.0, 1.0, color_original.a);
} else {
// Si no, pintem el color normal
COLOR = color_original;
}
}Per què color_original.a?
Si usàvem vec4(1.0, 1.0, 1.0, 1.0), pintaríem un quadrat blanc sòlid (incloent les àrees transparents del sprite).
En preservar la transparència original (.a), només es tornen blancs els píxels que ja eren visibles.
5. Connectant CPU i GPU
El shader està llest, però no fa res perquè flash_active sempre és false.
Ara tornem a GDScript (enemy_plane.gd) per activar-lo quan rebem dany.
Fase 1: Accedir al Material
Primer necessitem crear la funció hit_flash(), creem una variable amb la referència de Sprite2D i una altra amb la referència del material.
func hit_flash():
# 1. Obtenim el node Sprite2D
var sprite = $Sprite2D
# 2. Accedim al seu material i el tractem com ShaderMaterial
var shader_material = sprite.materialfunc hit_flash():
# 1. Obtenim el node Sprite2D
var sprite = $Sprite2D
# 2. Accedim al seu material i el tractem com ShaderMaterial
var shader_material = sprite.materialsprite.material retorna un tipus genèric Material. Per accedir a set_shader_parameter(), necessitem que GDScript sàpiga que és un ShaderMaterial.
La solució és usar if shader_material is ShaderMaterial. Això mata dos ocells d’un tret, després ho explicaré:
Fase 2: Enviar la Dada a la GPU
Usem set_shader_parameter() per canviar el valor del uniform:
func hit_flash():
var sprite = $Sprite2D
var shader_material = sprite.material
if shader_material is ShaderMaterial:
# Activem el flash (enviem 'true' a la GPU)
shader_material.set_shader_parameter("flash_active", true)func hit_flash():
var sprite = $Sprite2D
var shader_material = sprite.material
if shader_material is ShaderMaterial:
# Activem el flash (enviem 'true' a la GPU)
shader_material.set_shader_parameter("flash_active", true)Com funciona if shader_material is ShaderMaterial?
Això s’anomena type narrowing (estrenyiment de tipus):
- Verifica en temps d’execució si
shader_materialés realment unShaderMaterial. - Dins del bloc
if, GDScript sap queshader_materialés unShaderMaterial, habilitant l’autocompletat deset_shader_parameter().
Es pot assignar el tipus d’una altra manera usant as ShaderMaterial, però si el tipus no coincideix, retornarà null i podries tenir errors en intentar usar el resultat. Amb is, evitem aquest risc.
Què fa set_shader_parameter?
"flash_active"és el nom exacte de la variableuniformal shader.trueés el valor que volem assignar.- En cridar això, els 10.000 fils de la GPU reben
truei pinten blanc.
Fase 3: Apagar el Flash
Si deixem el flash actiu per sempre, l’enemic es queda blanc. Necessitem apagar-lo després d’un instant:
func hit_flash():
var sprite = $Sprite2D
var shader_material = sprite.material
# Verifiquem que el material sigui un ShaderMaterial
if shader_material is ShaderMaterial:
# Activar flash
shader_material.set_shader_parameter("flash_active", true)
# Esperar 0.1 segons (100 mil·lisegons)
await get_tree().create_timer(0.1).timeout
# Desactivar flash
shader_material.set_shader_parameter("flash_active", false)func hit_flash():
var sprite = $Sprite2D
var shader_material = sprite.material
# Verifiquem que el material sigui un ShaderMaterial
if shader_material is ShaderMaterial:
# Activar flash
shader_material.set_shader_parameter("flash_active", true)
# Esperar 0.1 segons (100 mil·lisegons)
await get_tree().create_timer(0.1).timeout
# Desactivar flash
shader_material.set_shader_parameter("flash_active", false)Què fa await?
await és una paraula clau que pausa l’execució d’aquesta funció específica fins que passi alguna cosa (en aquest cas, que passi el temps del timer).
Important: await NO congela el joc. La resta del motor segueix funcionant:
- L’enemic segueix movent-se.
- Altres bales segueixen volant.
- El jugador pot disparar.
Només AQUESTA funció (hit_flash) està “adormida” esperant. Quan el timer acaba, la funció “es desperta” i continua des d’on es va quedar (desactivar el flash).
És com posar un recordatori al teu mòbil: “En 0.1 segons, apaga el flash”. Mentrestant, segueixes amb la teva vida.
Fase 4: Cridar al Flash al Rebre Dany
Finalment, integrem la funció a _on_area_entered:
func _on_area_entered(area):
if is_dying:
return
hp -= 1
area.queue_free()
hit_flash() # Nou: activar el feedback visual
if hp <= 0:
die()func _on_area_entered(area):
if is_dying:
return
hp -= 1
area.queue_free()
hit_flash() # Nou: activar el feedback visual
if hp <= 0:
die()6. Script Complet del Shader
shader_type canvas_item;
uniform bool flash_active = false;
void fragment() {
vec4 color_original = texture(TEXTURE, UV);
if (flash_active) {
COLOR = vec4(1.0, 1.0, 1.0, color_original.a);
} else {
COLOR = color_original;
}
}shader_type canvas_item;
uniform bool flash_active = false;
void fragment() {
vec4 color_original = texture(TEXTURE, UV);
if (flash_active) {
COLOR = vec4(1.0, 1.0, 1.0, color_original.a);
} else {
COLOR = color_original;
}
}Repassem el que hem après
- Shader: Programa que s’executa a la GPU per modificar píxels.
- CPU vs GPU: Serial (una tasca alhora) vs Paral·lel (milers simultànies).
shader_type canvas_item: Declaració per a shaders 2D.fragment(): Funció que s’executa un cop per cada píxel.COLOR: Variable de sortida que defineix el color final del píxel.vec4: Estructura de 4 floats per a colors (RGBA).TEXTURE: Referència a la imatge del sprite.UV: Coordenades normalitzades (0-1) de cada píxel.texture(): Funció per llegir un color d’una imatge.uniform: Variable global controlada des de la CPU.set_shader_parameter(): Funció de GDScript per enviar dades a la GPU.
En el proper capítol, aprendrem a donar vida als nostres enemics usant Trigonometria per crear patrons de moviment ondulants i circulars.