Crear un Neon Vector Shooter en XNA: Efecto de particulas
Spanish (Español) translation by Elías Nicolás (you can also view the original English article)
En esta serie de tutoriales, te mostraré cómo hacer un juego de disparos de neón, como Geometry Wars, en XNA. El objetivo de estos tutoriales no es dejarte con una réplica exacta de Geometry Wars, sino repasar los elementos necesarios que te permitirán crear tu propia variante de alta calidad.
Visión general
En la serie hasta el momento, hemos configurado el juego y agregado floración. A continuación, añadiremos efectos de partículas.
Los efectos de partículas se crean al hacer un gran número de partículas pequeñas. Son muy versátiles y se pueden usar para agregar estilo a casi cualquier juego. En Shape Blaster realizaremos explosiones utilizando efectos de partículas. También utilizaremos efectos de partículas para crear fuego de escape para la nave del jugador y para agregar un toque visual a los agujeros negros. Además, veremos cómo hacer que las partículas interactúen con la gravedad de los agujeros negros.
La clase ParticleManager
Comenzaremos creando una clase ParticleManager que almacenará, actualizará y dibujará todas las partículas. Haremos esta clase lo suficientemente general como para poder reutilizarla fácilmente en otros proyectos. Para mantener general a ParticleManager, no será responsable de cómo se ven o se mueven las partículas; manejaremos eso en otro lugar.
Las partículas tienden a ser creadas y destruidas rápidamente y en grandes cantidades. Usaremos un conjunto de objetos para evitar la creación de grandes cantidades de basura. Esto significa que asignaremos una gran cantidad de partículas por adelantado y luego seguiremos reutilizando estas mismas partículas. También haremos que ParticleManager tenga una capacidad fija. Esto lo simplificará y ayudará a garantizar que no excedamos nuestro rendimiento o limitaciones de memoria al crear demasiadas partículas. Cuando se exceda el número máximo de partículas, comenzaremos a reemplazar las partículas más antiguas por otras nuevas.
Haremos del ParticleManager una clase genérica. Esto nos permitirá almacenar información de estado personalizada para las partículas sin codificarla en el ParticleManager. También crearemos una clase Particle anidada.
1 |
|
2 |
public class ParticleManager<T> |
3 |
{
|
4 |
public class Particle |
5 |
{
|
6 |
public Texture2D Texture; |
7 |
public Vector2 Position; |
8 |
public float Orientation; |
9 |
|
10 |
public Vector2 Scale = Vector2.One; |
11 |
|
12 |
public Color Color; |
13 |
public float Duration; |
14 |
public float PercentLife = 1f; |
15 |
public T State; |
16 |
}
|
17 |
}
|
La clase Particle tiene toda la información necesaria para mostrar una partícula y administrar su vida útil. El parámetro genérico, T State, está ahí para contener cualquier información adicional que podamos necesitar para nuestras partículas. Los datos necesarios variarán dependiendo de los efectos de partículas deseados; podría usarse para almacenar velocidad, aceleración, velocidad de rotación o cualquier otra cosa que pueda necesitar.
Para ayudar a administrar las partículas, necesitaremos una clase que funcione como una matriz circular, lo que significa que los índices que normalmente estarían fuera de los límites se ajustarán al principio de la matriz. Esto facilitará la sustitución de las partículas más antiguas primero si nos quedamos sin espacio para nuevas partículas en nuestra matriz. Agregamos lo siguiente como una clase anidada en ParticleManager.
1 |
|
2 |
private class CircularParticleArray |
3 |
{
|
4 |
private int start; |
5 |
public int Start |
6 |
{
|
7 |
get { return start; } |
8 |
set { start = value % list.Length; } |
9 |
}
|
10 |
|
11 |
public int Count { get; set; } |
12 |
public int Capacity { get { return list.Length; } } |
13 |
private Particle[] list; |
14 |
|
15 |
public CircularParticleArray(int capacity) |
16 |
{
|
17 |
list = new Particle[capacity]; |
18 |
}
|
19 |
|
20 |
public Particle this[int i] |
21 |
{
|
22 |
get { return list[(start + i) % list.Length]; } |
23 |
set { list[(start + i) % list.Length] = value; } |
24 |
}
|
25 |
}
|
Podemos configurar la propiedad Start para que ajuste el índice cero en nuestra CircularParticleArray en la matriz subyacente, y se usará Count para rastrear cuántas partículas activas hay en la lista. Nos aseguraremos de que la partícula en el índice cero sea siempre la partícula más antigua. Si reemplazamos la partícula más antigua por una nueva, simplemente incrementaremos el Start, que básicamente gira la matriz circular.
Ahora que tenemos nuestras clases de ayuda, podemos comenzar a completar la clase ParticleManager. Necesitaremos algunas variables miembro, y un constructor.
1 |
|
2 |
// This delegate will be called for each particle.
|
3 |
private Action<Particle> updateParticle; |
4 |
private CircularParticleArray particleList; |
5 |
|
6 |
public ParticleManager(int capacity, Action<Particle> updateParticle) |
7 |
{
|
8 |
this.updateParticle = updateParticle; |
9 |
particleList = new CircularParticleArray(capacity); |
10 |
|
11 |
// Populate the list with empty particle objects, for reuse.
|
12 |
for (int i = 0; i < capacity; i++) |
13 |
particleList[i] = new Particle(); |
14 |
}
|
La primera variable declarada, updateParticle, será un método personalizado que actualiza las partículas apropiadamente para el efecto deseado. Un juego puede tener múltiples ParticleManagers que se actualizan de forma diferente si es necesario. También creamos una CircularParticleList y la llenamos con partículas vacías. El constructor es el único lugar donde ParticleManager asigna memoria.
A continuación, agregamos el método CreateParticle(), que crea una nueva partícula utilizando la siguiente partícula no utilizada en el conjunto, o la partícula más antigua si no hay partículas no utilizadas.
1 |
|
2 |
public void CreateParticle(Texture2D texture, Vector2 position, Color tint, float duration, Vector2 scale, T state, float theta = 0) |
3 |
{
|
4 |
Particle particle; |
5 |
if (particleList.Count == particleList.Capacity) |
6 |
{
|
7 |
// if the list is full, overwrite the oldest particle, and rotate the circular list
|
8 |
particle = particleList[0]; |
9 |
particleList.Start++; |
10 |
}
|
11 |
else
|
12 |
{
|
13 |
particle = particleList[particleList.Count]; |
14 |
particleList.Count++; |
15 |
}
|
16 |
|
17 |
// Create the particle
|
18 |
particle.Texture = texture; |
19 |
particle.Position = position; |
20 |
particle.Tint = tint; |
21 |
|
22 |
particle.Duration = duration; |
23 |
particle.PercentLife = 1f; |
24 |
particle.Scale = scale; |
25 |
particle.Orientation = theta; |
26 |
particle.State = state; |
27 |
}
|
Las partículas pueden ser destruidas en cualquier momento. Necesitamos eliminar estas partículas mientras aseguramos que las otras partículas permanezcan en el mismo orden. Podemos hacerlo iterando a través de la lista de partículas mientras hacemos un seguimiento de cuántas han sido destruidas. A medida que avanzamos, movemos cada partícula activa frente a todas las partículas destruidas intercambiándola con la primera partícula destruida. Una vez que todas las partículas destruidas están al final de la lista, las desactivamos configurando la variable Count de la lista al número de partículas activas. Las partículas destruidas permanecerán en la matriz subyacente, pero no se actualizarán ni dibujarán.
ParticleManager.Update() maneja la actualización de cada partícula y la eliminación de las partículas destruidas de la lista.
1 |
|
2 |
public void Update() |
3 |
{
|
4 |
int removalCount = 0; |
5 |
for (int i = 0; i < particleList.Count; i++) |
6 |
{
|
7 |
var particle = particleList[i]; |
8 |
updateParticle(particle); |
9 |
particle.PercentLife -= 1f / particle.Duration; |
10 |
|
11 |
// sift deleted particles to the end of the list
|
12 |
Swap(particleList, i - removalCount, i); |
13 |
|
14 |
// if the particle has expired, delete this particle
|
15 |
if (particle.PercentLife < 0) |
16 |
removalCount++; |
17 |
}
|
18 |
particleList.Count -= removalCount; |
19 |
}
|
20 |
|
21 |
private static void Swap(CircularParticleArray list, int index1, int index2) |
22 |
{
|
23 |
var temp = list[index1]; |
24 |
list[index1] = list[index2]; |
25 |
list[index2] = temp; |
26 |
}
|
Lo último para implementar en ParticleManager es dibujar las partículas.
1 |
|
2 |
public void Draw(SpriteBatch spriteBatch) |
3 |
{
|
4 |
for (int i = 0; i < particleList.Count; i++) |
5 |
{
|
6 |
var particle = particleList[i]; |
7 |
|
8 |
Vector2 origin = new Vector2(particle.Texture.Width / 2, particle.Texture.Height / 2); |
9 |
spriteBatch.Draw(particle.Texture, particle.Position, null, particle.Color, particle.Orientation, origin, particle.Scale, 0, 0); |
10 |
}
|
11 |
}
|
La Struct ParticleState
Lo siguiente que debe hacer es crear una clase o estructura personalizada para personalizar el aspecto de las partículas en Shape Blaster. Habrá varios tipos diferentes de partículas en Shape Blaster que se comportarán de manera ligeramente diferente, así que comenzaremos creando una enum para el tipo de partícula. También necesitaremos variables para la velocidad y la longitud inicial de la partícula.
1 |
|
2 |
public enum ParticleType { None, Enemy, Bullet, IgnoreGravity } |
3 |
|
4 |
public struct ParticleState |
5 |
{
|
6 |
public Vector2 Velocity; |
7 |
public ParticleType Type; |
8 |
public float LengthMultiplier; |
9 |
}
|
Ahora estamos listos para escribir el método de actualización de la partícula. Es una buena idea hacer que este método sea rápido, ya que podría ser necesario para una gran cantidad de partículas.
Vamos a empezar simple. Agregue el siguiente método a ParticleState.
1 |
|
2 |
public static void UpdateParticle(ParticleManager.Particle particle) |
3 |
{
|
4 |
var vel = particle.State.Velocity; |
5 |
|
6 |
particle.Position += vel; |
7 |
particle.Orientation = vel.ToAngle(); |
8 |
|
9 |
// denormalized floats cause significant performance issues
|
10 |
if (Math.Abs(vel.X) + Math.Abs(vel.Y) < 0.00000000001f) |
11 |
vel = Vector2.Zero; |
12 |
|
13 |
vel *= 0.97f; // particles gradually slow down |
14 |
x.State.Velocity = vel; |
15 |
}
|
Explosiones enemigas
Volveremos y mejoraremos este método en un momento. Primero, vamos a crear algunos efectos de partículas para que podamos probar nuestros cambios. En GameRoot, declare un nuevo ParticleManager y llame a sus métodos Update() y Draw().
1 |
|
2 |
// in GameRoot
|
3 |
public static ParticleManager ParticleManager { get; private set; } |
4 |
|
5 |
// in GameRoot.Initialize()
|
6 |
ParticleManager = new ParticleManager(1024 * 20, ParticleState.UpdateParticle); |
7 |
|
8 |
// in GameRoot.Update()
|
9 |
ParticleManager.Update(); |
10 |
|
11 |
// in GameRoot.Draw()
|
12 |
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive); |
13 |
ParticleManager.Draw(); |
14 |
spriteBatch.End(); |
Además, declare una nueva Texture2D llamada LineParticle para la textura de la partícula en la clase Art, y cargue la textura como hicimos para los otros sprites.
Ahora hagamos explotar a los enemigos. Modifique el método Enemy.WasShot() de la siguiente manera.
1 |
|
2 |
public void WasShot() |
3 |
{
|
4 |
IsExpired = true; |
5 |
|
6 |
for (int i = 0; i < 120; i++) |
7 |
{
|
8 |
float speed = 18f * (1f - 1 / rand.NextFloat(1f, 10f)); |
9 |
var state = new ParticleState() |
10 |
{
|
11 |
Velocity = rand.NextVector2(speed, speed), |
12 |
Type = ParticleType.Enemy, |
13 |
LengthMultiplier = 1f |
14 |
};
|
15 |
|
16 |
GameRoot.ParticleManager.CreateParticle(Art.LineParticle, Position, Color.LightGreen, 190, 1.5f, state); |
17 |
}
|
18 |
}
|
Esto crea 120 partículas que dispararán hacia el exterior con diferentes velocidades en todas las direcciones. La velocidad aleatoria está ponderada de modo que las partículas tienen más probabilidades de viajar cerca de la velocidad máxima. Esto hará que haya más partículas en el borde de la explosión a medida que se expande. Las partículas duran 190 cuadros, o algo más de tres segundos.
Ahora puedes correr el juego y ver explotar a los enemigos. Sin embargo, todavía hay algunas mejoras por hacer para los efectos de partículas.
El primer problema es que las partículas desaparecen bruscamente una vez que se agota su duración. Sería mejor si pudieran desaparecer sin problemas. Pero vayamos un poco más lejos que esto y hagamos que las partículas se vuelvan más brillantes cuando se mueven rápido. Además, se ve bien si alargamos las partículas de movimiento rápido y acortamos las de movimiento lento.
Modifique el método ParticleState.UpdateParticle() de la siguiente manera (los cambios están resaltados).
1 |
|
2 |
public static void UpdateParticle(ParticleManager.Particle particle) |
3 |
{
|
4 |
var vel = particle.State.Velocity; |
5 |
|
6 |
particle.Position += vel; |
7 |
particle.Orientation = vel.ToAngle(); |
8 |
|
9 |
float speed = vel.Length(); |
10 |
float alpha = Math.Min(1, Math.Min(particle.PercentLife * 2, speed * 1f)); |
11 |
alpha *= alpha; |
12 |
|
13 |
particle.Color.A = (byte)(255 * alpha); |
14 |
|
15 |
particle.Scale.X = particle.State.LengthMultiplier * Math.Min(Math.Min(1f, 0.2f * speed + 0.1f), alpha); |
16 |
|
17 |
if (Math.Abs(vel.X) + Math.Abs(vel.Y) < 0.00000000001f) // denormalized floats cause significant performance issues |
18 |
vel = Vector2.Zero; |
19 |
|
20 |
vel *= 0.97f; // particles gradually slow down |
21 |
x.State.Velocity = vel; |
22 |
}
|
Las explosiones se ven mucho mejor ahora, pero todas son del mismo color. Podemos darles más variedad eligiendo colores al azar. Un método para producir colores aleatorios es elegir los componentes rojo, azul y verde al azar, pero esto producirá muchos colores apagados y nos gustaría que nuestras partículas tuvieran una apariencia de luz de neón. Podemos tener más control sobre nuestros colores al especificarlos en el espacio de color HSV. HSV significa tono, saturación y valor. Nos gustaría elegir colores con un tono aleatorio pero con una saturación y un valor fijos. Necesitamos una función auxiliar que pueda producir un color a partir de valores de HSV.
1 |
|
2 |
static class ColorUtil |
3 |
{
|
4 |
public static Color HSVToColor(float h, float s, float v) |
5 |
{
|
6 |
if (h == 0 && s == 0) |
7 |
return new Color(v, v, v); |
8 |
|
9 |
float c = s * v; |
10 |
float x = c * (1 - Math.Abs(h % 2 - 1)); |
11 |
float m = v - c; |
12 |
|
13 |
if (h < 1) return new Color(c + m, x + m, m); |
14 |
else if (h < 2) return new Color(x + m, c + m, m); |
15 |
else if (h < 3) return new Color(m, c + m, x + m); |
16 |
else if (h < 4) return new Color(m, x + m, c + m); |
17 |
else if (h < 5) return new Color(x + m, m, c + m); |
18 |
else return new Color(c + m, m, x + m); |
19 |
}
|
20 |
}
|
Ahora podemos modificar Enemy.WasShot() para usar colores aleatorios. Para hacer que el color de la explosión sea menos monótono, elegiremos dos colores clave cercanos para cada explosión e interpolaremos linealmente entre ellos con una cantidad aleatoria para cada partícula.
1 |
|
2 |
public void WasShot() |
3 |
{
|
4 |
IsExpired = true; |
5 |
|
6 |
float hue1 = rand.NextFloat(0, 6); |
7 |
float hue2 = (hue1 + rand.NextFloat(0, 2)) % 6f; |
8 |
Color color1 = ColorUtil.HSVToColor(hue1, 0.5f, 1); |
9 |
Color color2 = ColorUtil.HSVToColor(hue2, 0.5f, 1); |
10 |
|
11 |
for (int i = 0; i < 120; i++) |
12 |
{
|
13 |
float speed = 18f * (1f - 1 / rand.NextFloat(1f, 10f)); |
14 |
var state = new ParticleState() |
15 |
{
|
16 |
Velocity = rand.NextVector2(speed, speed), |
17 |
Type = ParticleType.Enemy, |
18 |
LengthMultiplier = 1 |
19 |
};
|
20 |
|
21 |
Color color = Color.Lerp(color1, color2, rand.NextFloat(0, 1)); |
22 |
GameRoot.ParticleManager.CreateParticle(Art.LineParticle, Position, color, 190, 1.5f, state); |
23 |
}
|
24 |
}
|
Las explosiones deben verse como la animación de abajo.

Puedes jugar con la generación de colores que se adapte a tus preferencias. Una técnica alternativa que funciona bien es elegir a mano una serie de patrones de color para las explosiones y elegir aleatoriamente entre los esquemas de color preseleccionados.
Explosiones de bala
También podemos hacer explotar las balas cuando llegan al borde de la pantalla. Básicamente haremos lo mismo que hicimos para las explosiones enemigas.
Agregue un miembro Random estático a la clase Bullet.
1 |
|
2 |
private static Random rand = new Random(); |
Luego modifique Bullet.Update() de la siguiente manera.
1 |
|
2 |
// delete bullets that go off-screen
|
3 |
if (!GameRoot.Viewport.Bounds.Contains(Position.ToPoint())) |
4 |
{
|
5 |
IsExpired = true; |
6 |
|
7 |
for (int i = 0; i < 30; i++) |
8 |
GameRoot.ParticleManager.CreateParticle(Art.LineParticle, Position, Color.LightBlue, 50, 1, |
9 |
new ParticleState() { Velocity = rand.NextVector2(0, 9), Type = ParticleType.Bullet, LengthMultiplier = 1 }); |
10 |
|
11 |
}
|
Puede notar que dar una dirección aleatoria a las partículas es un desperdicio, porque al menos la mitad de las partículas saldrán inmediatamente de la pantalla (más si la bala explota en una esquina). Podríamos hacer un trabajo extra para asegurar que las partículas solo tengan velocidades opuestas a la pared que están enfrentando. Sin embargo, en cambio, seguiremos el ejemplo de Geometry Wars y haremos que todas las partículas reboten en las paredes. Cualquier partícula que salga de la pantalla será devuelta.
Agregue las siguientes líneas a ParticleState.UpdateParticle() en cualquier lugar entre la primera y la última línea.
1 |
|
2 |
var pos = x.Position; |
3 |
int width = (int)GameRoot.ScreenSize.X; |
4 |
int height = (int)GameRoot.ScreenSize.Y; |
5 |
|
6 |
// collide with the edges of the screen
|
7 |
if (pos.X < 0) vel.X = Math.Abs(vel.X); else if (pos.X > width) |
8 |
vel.X = -Math.Abs(vel.X); |
9 |
if (pos.Y < 0) vel.Y = Math.Abs(vel.Y); else if (pos.Y > height) |
10 |
vel.Y = -Math.Abs(vel.Y); |
Explosión del barco del jugador
Haremos una explosión realmente grande cuando el jugador muere. Modificar PlayerShip.Kill() de esta manera:
1 |
|
2 |
public void Kill() |
3 |
{
|
4 |
framesUntilRespawn = 60; |
5 |
|
6 |
Color yellow = new Color(0.8f, 0.8f, 0.4f); |
7 |
|
8 |
for (int i = 0; i < 1200; i++) |
9 |
{
|
10 |
float speed = 18f * (1f - 1 / rand.NextFloat(1f, 10f)); |
11 |
Color color = Color.Lerp(Color.White, yellow, rand.NextFloat(0, 1)); |
12 |
var state = new ParticleState() |
13 |
{
|
14 |
Velocity = rand.NextVector2(speed, speed), |
15 |
Type = ParticleType.None, |
16 |
LengthMultiplier = 1 |
17 |
};
|
18 |
|
19 |
GameRoot.ParticleManager.CreateParticle(Art.LineParticle, Position, color, 190, 1.5f, state); |
20 |
}
|
21 |
}
|
Esto es similar a las explosiones enemigas, pero usamos más partículas y siempre usamos el mismo esquema de color. El tipo de partícula también se establece en ParticleType.None.
En la demostración, las partículas de las explosiones enemigas se ralentizan más rápido que las partículas de la nave del jugador que explotan. Esto hace que la explosión del jugador dure un poco más y se vea un poco más épica.
Agujeros negros revisados
Ahora que tenemos efectos de partículas, revisemos los agujeros negros y hagamos que interactúen con las partículas.
Efecto sobre las partículas
Los agujeros negros deberían afectar a las partículas además de otras entidades, por lo que necesitamos modificar ParticleState.UpdateParticle(). Añade las siguientes líneas.
1 |
|
2 |
if (x.State.Type != ParticleType.IgnoreGravity) |
3 |
{
|
4 |
foreach (var blackHole in EntityManager.BlackHoles) |
5 |
{
|
6 |
var dPos = blackHole.Position - pos; |
7 |
float distance = dPos.Length(); |
8 |
var n = dPos / distance; |
9 |
vel += 10000 * n / (distance * distance + 10000); |
10 |
|
11 |
// add tangential acceleration for nearby particles
|
12 |
if (distance < 400) |
13 |
vel += 45 * new Vector2(n.Y, -n.X) / (distance + 100); |
14 |
}
|
15 |
}
|
Aquí, n es el vector unitario que apunta hacia el agujero negro. La fuerza atractiva es una versión modificada de la función del cuadrado inverso. La primera modificación es que el denominador es \(distancia ^2 + 10,000\). Esto hace que la fuerza de atracción se acerque a un valor máximo en lugar de tender hacia el infinito a medida que la distancia se vuelve muy pequeña. Cuando la distancia es mucho mayor que 100 píxeles, \(distancia^2\) se vuelve mucho mayor que 10,000. Por lo tanto, agregar 10,000 a \(distancia^2\) tiene un efecto muy pequeño, y la función se aproxima a una función cuadrada inversa normal. Sin embargo, cuando la distancia es mucho menor que 100 píxeles, la distancia tiene un pequeño efecto sobre el valor del denominador, y la ecuación se vuelve aproximadamente igual a:
1 |
|
2 |
vel += n; |
La segunda modificación es agregar un componente lateral a la velocidad cuando las partículas se acercan lo suficiente al agujero negro. Esto tiene dos propósitos. Primero, hace que las partículas formen una espiral en el sentido de las agujas del reloj hacia el agujero negro. Segundo, cuando las partículas se acercan lo suficiente, alcanzarán el equilibrio y formarán un círculo brillante alrededor del agujero negro.
(V.Y, -V.X). De manera similar, para rotar 90 ° en sentido antihorario, tome (-V.Y, V.X).Partículas productoras
Los agujeros negros producirán dos tipos de partículas. Primero, rociarán periódicamente partículas que orbitarán alrededor de ellas. Segundo, cuando se dispara un agujero negro, rociará partículas especiales que no se ven afectadas por su gravedad.
Agregue el siguiente código al método BlackHole.WasShot()
1 |
|
2 |
float hue = (float)((3 * GameRoot.GameTime.TotalGameTime.TotalSeconds) % 6); |
3 |
Color color = ColorUtil.HSVToColor(hue, 0.25f, 1); |
4 |
const int numParticles = 150; |
5 |
float startOffset = rand.NextFloat(0, MathHelper.TwoPi / numParticles); |
6 |
|
7 |
for (int i = 0; i < numParticles; i++) |
8 |
{
|
9 |
Vector2 sprayVel = MathUtil.FromPolar(MathHelper.TwoPi * i / numParticles + startOffset, rand.NextFloat(8, 16)); |
10 |
Vector2 pos = Position + 2f * sprayVel; |
11 |
var state = new ParticleState() |
12 |
{
|
13 |
Velocity = sprayVel, |
14 |
LengthMultiplier = 1, |
15 |
Type = ParticleType.IgnoreGravity |
16 |
};
|
17 |
|
18 |
GameRoot.ParticleManager.CreateParticle(Art.LineParticle, pos, color, 90, 1.5f, state); |
19 |
}
|
Esto funciona principalmente de la misma manera que las otras explosiones de partículas. Una diferencia es que elegimos el tono del color en función del tiempo total transcurrido del juego. Si disparas al agujero negro varias veces en rápida sucesión, verás que el matiz de las explosiones gira gradualmente. Esto parece menos desordenado que el uso de colores aleatorios mientras se permite la variación.
Para el rociado de partículas en órbita, debemos agregar una variable a la clase BlackHole para rastrear la dirección en la que actualmente estamos rociando partículas.
1 |
|
2 |
private float sprayAngle = 0; |
Ahora agregue lo siguiente al método BlackHole.Update().
1 |
|
2 |
// The black holes spray some orbiting particles. The spray toggles on and off every quarter second.
|
3 |
if ((GameRoot.GameTime.TotalGameTime.Milliseconds / 250) % 2 == 0) |
4 |
{
|
5 |
Vector2 sprayVel = MathUtil.FromPolar(sprayAngle, rand.NextFloat(12, 15)); |
6 |
Color color = ColorUtil.HSVToColor(5, 0.5f, 0.8f); // light purple |
7 |
Vector2 pos = Position + 2f * new Vector2(sprayVel.Y, -sprayVel.X) + rand.NextVector2(4, 8); |
8 |
var state = new ParticleState() |
9 |
{
|
10 |
Velocity = sprayVel, |
11 |
LengthMultiplier = 1, |
12 |
Type = ParticleType.Enemy |
13 |
};
|
14 |
|
15 |
GameRoot.ParticleManager.CreateParticle(Art.LineParticle, pos, color, 190, 1.5f, state); |
16 |
}
|
17 |
|
18 |
// rotate the spray direction
|
19 |
sprayAngle -= MathHelper.TwoPi / 50f; |
Esto hará que los agujeros negros rocíen chorros de partículas púrpuras que formarán un anillo brillante que orbita alrededor del agujero negro, de esta manera:
Fuego de escape de la nave
Según lo dictado por las leyes de la física geométrica-neón, la nave del jugador se propulsa lanzando un chorro de partículas ardientes por su tubo de escape. Con nuestro motor de partículas en su lugar, este efecto es fácil de hacer y agrega un toque visual al movimiento de la nave.
A medida que la nave se mueve, creamos tres corrientes de partículas: una corriente central que se dispara directamente desde la parte posterior de la nave, y dos corrientes laterales cuyos ángulos giran de un lado a otro en relación con la nave. Las dos corrientes laterales giran en direcciones opuestas para hacer un patrón entrecruzado. Las corrientes laterales tienen un color más rojo, mientras que la transmisión central tiene un color amarillo-blanco más cálido. La siguiente animación muestra el efecto.



Para hacer que el fuego brille más intensamente de lo que lo haría solo con la floración, haremos que la nave emita partículas adicionales que se ven así:

Estas partículas se teñirán y se mezclarán con las partículas regulares. El código para el efecto completo se muestra a continuación.
1 |
|
2 |
private void MakeExhaustFire() |
3 |
{
|
4 |
if (Velocity.LengthSquared() > 0.1f) |
5 |
{
|
6 |
// set up some variables
|
7 |
Orientation = Velocity.ToAngle(); |
8 |
Quaternion rot = Quaternion.CreateFromYawPitchRoll(0f, 0f, Orientation); |
9 |
|
10 |
double t = GameRoot.GameTime.TotalGameTime.TotalSeconds; |
11 |
// The primary velocity of the particles is 3 pixels/frame in the direction opposite to which the ship is travelling.
|
12 |
Vector2 baseVel = Velocity.ScaleTo(-3); |
13 |
// Calculate the sideways velocity for the two side streams. The direction is perpendicular to the ship's velocity and the
|
14 |
// magnitude varies sinusoidally.
|
15 |
Vector2 perpVel = new Vector2(baseVel.Y, -baseVel.X) * (0.6f * (float)Math.Sin(t * 10)); |
16 |
Color sideColor = new Color(200, 38, 9); // deep red |
17 |
Color midColor = new Color(255, 187, 30); // orange-yellow |
18 |
Vector2 pos = Position + Vector2.Transform(new Vector2(-25, 0), rot); // position of the ship's exhaust pipe. |
19 |
const float alpha = 0.7f; |
20 |
|
21 |
// middle particle stream
|
22 |
Vector2 velMid = baseVel + rand.NextVector2(0, 1); |
23 |
GameRoot.ParticleManager.CreateParticle(Art.LineParticle, pos, Color.White * alpha, 60f, new Vector2(0.5f, 1), |
24 |
new ParticleState(velMid, ParticleType.Enemy)); |
25 |
GameRoot.ParticleManager.CreateParticle(Art.Glow, pos, midColor * alpha, 60f, new Vector2(0.5f, 1), |
26 |
new ParticleState(velMid, ParticleType.Enemy)); |
27 |
|
28 |
// side particle streams
|
29 |
Vector2 vel1 = baseVel + perpVel + rand.NextVector2(0, 0.3f); |
30 |
Vector2 vel2 = baseVel - perpVel + rand.NextVector2(0, 0.3f); |
31 |
GameRoot.ParticleManager.CreateParticle(Art.LineParticle, pos, Color.White * alpha, 60f, new Vector2(0.5f, 1), |
32 |
new ParticleState(vel1, ParticleType.Enemy)); |
33 |
GameRoot.ParticleManager.CreateParticle(Art.LineParticle, pos, Color.White * alpha, 60f, new Vector2(0.5f, 1), |
34 |
new ParticleState(vel2, ParticleType.Enemy)); |
35 |
|
36 |
GameRoot.ParticleManager.CreateParticle(Art.Glow, pos, sideColor * alpha, 60f, new Vector2(0.5f, 1), |
37 |
new ParticleState(vel1, ParticleType.Enemy)); |
38 |
GameRoot.ParticleManager.CreateParticle(Art.Glow, pos, sideColor * alpha, 60f, new Vector2(0.5f, 1), |
39 |
new ParticleState(vel2, ParticleType.Enemy)); |
40 |
}
|
41 |
}
|
No hay nada furtivo en este código. Usamos una función sinusoidal para producir el efecto de giro en los flujos laterales variando su velocidad lateral a lo largo del tiempo. Para cada flujo, creamos dos partículas superpuestas por fotograma: una LineParticle blanca semitransparente y una partícula de color brillante detrás de ella. Llame a MakeExhaustFire() al final de PlayerShip.Update(), inmediatamente antes de establecer la velocidad del barco en cero.
Conclusión
Con todos estos efectos de partículas, Shape Blaster está empezando a verse muy bien. En la parte final de esta serie, agregaremos un efecto impresionante más: la cuadrícula de fondo combada.



