Maquetación Web Dinámica / Canvas y videojuegos 2D
Sistema de partículas: confeti, chispas y humo
El confeti que estalla al ganar, las chispas de una explosión, la nieve que cae, el humo de un cohete: todos son lo mismo — cientos de partículas diminutas, cada una con su propia física, que nacen, viven y mueren. Un sistema de partículas suena avanzado, pero es tu game loop y tu física de siempre, aplicados a un ARRAY de objetos. Y es el efecto que hace que un juego se sienta vivo y jugoso.
Un enjambre de bolitas tontas
La partícula y su ciclo de vida
Cada partícula es un objeto; el sistema es un array de ellos:
function crearParticula(x, y) {
const angulo = Math.random() * Math.PI * 2; // dirección al azar (360°)
const rapidez = Math.random() * 3 + 1;
return {
x, y,
vx: Math.cos(angulo) * rapidez, // velocidad descompuesta en X e Y
vy: Math.sin(angulo) * rapidez,
vida: 1, // 1 = recién nacida, 0 = muerta
color: `hsl(${Math.random() * 60 + 260}, 80%, 60%)`, // violetas variados
tam: Math.random() * 3 + 2,
};
}
const particulas = [];
// emitir un puñado desde un punto:
for (let i = 0; i < 30; i++) particulas.push(crearParticula(cx, cy));Math.cos(angulo) y Math.sin(angulo) convierten "una dirección" en
vx/vy — reparten la rapidez entre los dos ejes según el ángulo (la
trigonometría justa, sin dramatismo: coseno para X, seno para Y). El azar
en ángulo, rapidez, color y tamaño es lo que hace que 30 partículas se
vean como una explosión y no como una formación militar.
El loop del sistema: actualizar y PODAR
function actualizarParticulas() {
for (const p of particulas) {
p.vy += 0.08; // gravedad
p.x += p.vx; p.y += p.vy;
p.vida -= 0.012; // envejece: se acerca a la muerte
}
// PODAR las muertas: sin esto, el array crece infinito y el juego se traba
for (let i = particulas.length - 1; i >= 0; i--) {
if (particulas[i].vida <= 0) particulas.splice(i, 1);
}
}
function dibujarParticulas() {
for (const p of particulas) {
ctx.globalAlpha = p.vida; // se desvanece al envejecer
ctx.fillStyle = p.color;
ctx.beginPath(); ctx.arc(p.x, p.y, p.tam, 0, Math.PI * 2); ctx.fill();
}
ctx.globalAlpha = 1; // restaurar, o TODO lo demás sale translúcido
}Dos detalles que separan un sistema sano de uno que revienta: podar las
partículas muertas (recorrer el array AL REVÉS para poder splice sin
saltarte elementos) evita que el array crezca sin fin y congele el juego; y
restaurar globalAlpha = 1 tras dibujar, porque globalAlpha es
estado global — si lo dejas en 0.3, todo lo que pintes después (el
puntaje, el personaje) saldrá translúcido.
Una fuente de partículas
<canvas id="particulas" width="340" height="240"></canvas>body { font-family: system-ui; padding: 16px; background: #f8fafc; }
canvas { background: #0b1120; border: 1px solid #e2e8f0; border-radius: 12px; }const canvas = document.getElementById("particulas");
const ctx = canvas.getContext("2d");
const quieto = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const particulas = [];
const fuenteX = 170, fuenteY = 210;
function emitir() {
const angulo = -Math.PI / 2 + (Math.random() - 0.5) * 0.9; // hacia arriba, con dispersión
const rapidez = Math.random() * 3 + 4;
particulas.push({
x: fuenteX, y: fuenteY,
vx: Math.cos(angulo) * rapidez,
vy: Math.sin(angulo) * rapidez,
vida: 1,
color: `hsl(${Math.random() * 70 + 250}, 85%, 65%)`,
tam: Math.random() * 3 + 2,
});
}
function draw() {
for (const p of particulas) {
ctx.globalAlpha = Math.max(p.vida, 0);
ctx.fillStyle = p.color;
ctx.beginPath(); ctx.arc(p.x, p.y, p.tam, 0, Math.PI * 2); ctx.fill();
}
ctx.globalAlpha = 1;
}
function loop() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < 5; i++) emitir(); // 5 partículas nuevas por fotograma
for (const p of particulas) {
p.vy += 0.12; p.x += p.vx; p.y += p.vy; p.vida -= 0.011;
}
for (let i = particulas.length - 1; i >= 0; i--) {
if (particulas[i].vida <= 0) particulas.splice(i, 1);
}
draw();
requestAnimationFrame(loop);
}
if (quieto) {
// fotograma estático representativo, sin movimiento perpetuo
for (let i = 0; i < 60; i++) { emitir(); const p = particulas[i]; p.x += p.vx*6; p.y += p.vy*6 + 4; p.vida = 0.6; }
draw();
} else requestAnimationFrame(loop);Cambiando cuatro números tienes efectos distintos: apunta el ángulo hacia
arriba con poca dispersión y es una fuente; 360° de dispersión desde un
punto y es una explosión; emite desde arriba con vy positivo y poca
vx, sin gravedad fuerte, y es nieve cayendo. Mismo sistema, otra física
inicial. (Este patrón es, de hecho, el mismo que mueve el confeti que
viste al completar una lección en esta plataforma.)
Tu efecto de confeti se ve genial 10 segundos y luego el juego entero empieza a ir a tirones cada vez peor. ¿La causa casi segura?
Mini-reto
Conecta las partículas a una acción: emite una ráfaga de 40 partículas en la posición donde el jugador RECOGE una moneda (reutiliza la colisión de la lección 7). Ese "feedback de partículas" al recoger, golpear o saltar es lo que los diseñadores llaman "juice" — y es la diferencia entre un juego que funciona y uno que se siente delicioso de jugar.
Qué sigue
Ya construyes efectos dignos de mostrar. La próxima lección, corta y práctica, resuelve una pregunta muy concreta: "¿cómo convierto mi animación de canvas en un GIF o un video para compartirla?". Las técnicas y las librerías para exportar, antes de lanzarnos a los dos proyectos grandes del módulo.