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..
En la serie hasta ahora creamos los efectos de juego, floración y partículas. En esta parte final, crearemos una cuadrícula de fondo dinámica y combada..
Advertencia: Alto!Uno de los efectos más geniales en Geometry Wars es la cuadrícula de fondo combada. Examinaremos cómo crear un efecto similar en Shape Blaster. La cuadrícula reaccionará a las balas, los agujeros negros y el reaparición del jugador. No es difícil de hacer y se ve increíble.
Haremos la grilla utilizando una simulación de resorte. En cada intersección de la rejilla, pondremos un pequeño peso y uniremos un resorte en cada lado. Estos resortes solo tirarán y nunca empujarán, como una banda de goma. Para mantener la cuadrícula en posición, las masas en el borde de la cuadrícula se anclarán en su lugar. A continuación se muestra un diagrama de la disposición..
Vamos a crear una clase llamada Cuadrícula
para crear este efecto. Sin embargo, antes de trabajar en la propia cuadrícula, necesitamos hacer dos clases auxiliares: Primavera
y PointMass
.
los PointMass
clase representa las masas a las que uniremos los manantiales. Los resortes nunca se conectan directamente a otros resortes. En cambio, aplican una fuerza a las masas que conectan, que a su vez pueden estirar otros resortes..
clase privada PointMass public Vector3 Position; Vector3 Velocity público; flotador público InverseMass; aceleración privada Vector3; Amortiguación del flotador privado = 0.98f; PointMass pública (posición Vector3, invMass flotante) Posición = posición; InverseMass = invMass; Public void ApplyForce (Vector3 force) aceleración + = force * InverseMass; void público IncreaseDamping (factor flotante) amortiguando * = factor; Public void Update () Velocity + = aceleración; Posición + = Velocidad; aceleración = Vector3.Zero; if (Velocity.LengthSquared () < 0.001f * 0.001f) Velocity = Vector3.Zero; Velocity *= damping; damping = 0.98f;
Hay algunos puntos interesantes sobre esta clase. Primero, note que almacena el inverso de la misa, 1 / masa
. A menudo, esto es una buena idea en las simulaciones de física porque las ecuaciones de la física tienden a usar la inversa de la masa con mayor frecuencia, y porque nos brinda una manera fácil de representar objetos inmóviles, pesados e infinitos al establecer la masa inversa en cero..
La clase también contiene un mojadura variable. Esto se usa aproximadamente como fricción o resistencia del aire. Poco a poco disminuye la masa hacia abajo. Esto ayuda a que la cuadrícula se detenga y aumente la estabilidad de la simulación del resorte..
los Actualizar()
El método hace el trabajo de mover la masa puntual de cada cuadro. Comienza haciendo una integración de Euler simpléctica, lo que significa que solo agregamos la aceleración a la velocidad y luego agregamos la velocidad actualizada a la posición. Esto difiere de la integración estándar de Euler en la que actualizaríamos la velocidad. después actualizando la posición.
Propina: Symplectic Euler es mejor para las simulaciones de primavera porque conserva la energía. Si utiliza la integración regular de Euler y crea resortes sin amortiguación, tenderán a estirarse más y más a medida que ganen energía, rompiendo su simulación..
Después de actualizar la velocidad y la posición, verificamos si la velocidad es muy pequeña, y si es así, la ponemos a cero. Esto puede ser importante para el rendimiento debido a la naturaleza de los números de punto flotante desnormalizados.
(Cuando los números de punto flotante se vuelven muy pequeños, usan una representación especial llamada número denormal. Esto tiene la ventaja de permitir que flotar represente números más pequeños, pero tiene un precio. La mayoría de los conjuntos de chips no pueden usar sus operaciones aritméticas estándar en números denormalizados y en su lugar, deben emularlos usando una serie de pasos. Esto puede ser de decenas a cientos de veces más lento que realizar operaciones en números de punto flotante normalizados. Ya que multiplicamos nuestra velocidad por nuestro factor de amortiguamiento en cada cuadro, eventualmente se volverá muy pequeño. . En realidad no nos importan velocidades tan pequeñas, por lo que simplemente lo ponemos a cero.)
los IncreaseDamping ()
El método se utiliza para aumentar temporalmente la cantidad de amortiguación. Usaremos esto más adelante para ciertos efectos..
Un resorte conecta dos masas puntuales y, si se extiende más allá de su longitud natural, aplica una fuerza que une las masas. Los resortes siguen una versión modificada de la Ley de Hooke con amortiguación:
\ [f = -kx - bv \]
El código para el Primavera
la clase es la siguiente.
estructura privada Spring public PointMass End1; PointMass End2 pública; flotador público TargetLength; flotación pública rigidez; Amortiguadores de flotadores públicos; resorte público (PointMass end1, PointMass end2, rigidez del flotador, amortiguación del flotador) End1 = end1; End2 = end2; Rigidez = rigidez; Amortiguación = Amortiguación; TargetLength = Vector3.Distance (end1.Position, end2.Position) * 0.95f; Public void Update () var x = End1.Position - End2.Position; longitud de flotación = x.Longitud (); // estos resortes solo pueden tirar, no empujar si (longitud <= TargetLength) return; x = (x / length) * (length - TargetLength); var dv = End2.Velocity - End1.Velocity; var force = Stiffness * x - dv * Damping; End1.ApplyForce(-force); End2.ApplyForce(force);
Cuando creamos un resorte, establecemos que la longitud natural del resorte sea ligeramente menor que la distancia entre los dos puntos finales. Esto mantiene la rejilla tensa incluso cuando está en reposo y mejora un poco la apariencia..
los Actualizar()
El método primero verifica si el resorte se estira más allá de su longitud natural. Si no se estira, no pasa nada. Si es así, usamos la Ley de Hooke modificada para encontrar la fuerza del resorte y aplicarla a las dos masas conectadas.
Ahora que tenemos las clases anidadas necesarias, estamos listos para crear la cuadrícula. Comenzamos creando PointMass
Objetos en cada intersección de la cuadrícula. También creamos un ancla inamovible. PointMass
Objetos para mantener la rejilla en su lugar. Luego unimos las masas con manantiales..
Primavera [] manantiales; PointMass [,] puntos; cuadrícula pública (tamaño de rectángulo, espaciado Vector2) var springList = new List (); int numColumns = (int) (size.Width / spacing.X) + 1; int numRows = (int) (size.Height / spacing.Y) + 1; points = new PointMass [numColumns, numRows]; // estos puntos fijos se utilizarán para anclar la cuadrícula en posiciones fijas en la pantalla PointMass [,] fixedPoints = new PointMass [numColumns, numRows]; // crear las masas de puntos int column = 0, row = 0; para (float y = size.Top; y <= size.Bottom; y += spacing.Y) for (float x = size.Left; x <= size.Right; x += spacing.X) points[column, row] = new PointMass(new Vector3(x, y, 0), 1); fixedPoints[column, row] = new PointMass(new Vector3(x, y, 0), 0); column++; row++; column = 0; // link the point masses with springs for (int y = 0; y < numRows; y++) for (int x = 0; x < numColumns; x++) if (x == 0 || y == 0 || x == numColumns - 1 || y == numRows - 1) // anchor the border of the grid springList.Add(new Spring(fixedPoints[x, y], points[x, y], 0.1f, 0.1f)); else if (x % 3 == 0 && y % 3 == 0) // loosely anchor 1/9th of the point masses springList.Add(new Spring(fixedPoints[x, y], points[x, y], 0.002f, 0.02f)); const float stiffness = 0.28f; const float damping = 0.06f; if (x > 0) springList.Add (nuevo resorte (puntos [x - 1, y], puntos [x, y], rigidez, amortiguamiento)); if (y> 0) springList.Add (nuevo Spring (puntos [x, y - 1], puntos [x, y], rigidez, amortiguación)); springs = springList.ToArray ();
El primero para
el bucle crea masas regulares e inmóviles en cada intersección de la cuadrícula. En realidad no usaremos todas las masas inamovibles, y las masas no utilizadas simplemente serán recolectadas en algún momento después de que el constructor termine. Podríamos optimizar evitando la creación de objetos innecesarios, pero como la cuadrícula normalmente solo se crea una vez, no habrá mucha diferencia..
Además de usar masas de puntos de anclaje alrededor del borde de la cuadrícula, también usaremos algunas masas de anclaje dentro de la cuadrícula. Se utilizarán para ayudar a que la rejilla vuelva a su posición original muy suavemente después de deformarse..
Como los puntos de anclaje nunca se mueven, no es necesario actualizarlos en cada fotograma. Simplemente podemos engancharlos a los resortes y olvidarlos. Por lo tanto, no tenemos una variable miembro en el Cuadrícula
clase para estas masas.
Hay una serie de valores que puede modificar en la creación de la cuadrícula. Los más importantes son la rigidez y la amortiguación de los resortes. La rigidez y la amortiguación de los anclajes de borde y los anclajes interiores se establecen independientemente de los resortes principales. Los valores más altos de rigidez harán que los resortes oscilen más rápidamente, y los valores más altos de amortiguación harán que los resortes se desaceleren más rápido.
Para que la cuadrícula se mueva, debemos actualizarla cada cuadro. Esto es muy simple como ya hicimos todo el trabajo duro en el PointMass
y Primavera
clases.
Public void Update () foreach (var spring in springs) spring.Update (); foreach (masa var en puntos) masa.Actualización ();
Ahora agregaremos algunos métodos que manipulan la cuadrícula. Puedes agregar métodos para cualquier tipo de manipulación que puedas imaginar. Aquí implementaremos tres tipos de manipulaciones: empujar parte de la cuadrícula en una dirección determinada, empujar la cuadrícula hacia afuera desde algún punto y tirar de la cuadrícula hacia algún punto. Los tres afectarán a la cuadrícula dentro de un radio dado desde algún punto objetivo. A continuación se muestran algunas imágenes de estas manipulaciones en acción..
public void ApplyDirectedForce (Vector3 force, Vector3 position, float radius) foreach (var mass en puntos) if (Vector3.DistanceSquared (position, mass.Position) < radius * radius) mass.ApplyForce(10 * force / (10 + Vector3.Distance(position, mass.Position))); public void ApplyImplosiveForce(float force, Vector3 position, float radius) foreach (var mass in points) float dist2 = Vector3.DistanceSquared(position, mass.Position); if (dist2 < radius * radius) mass.ApplyForce(10 * force * (position - mass.Position) / (100 + dist2)); mass.IncreaseDamping(0.6f); public void ApplyExplosiveForce(float force, Vector3 position, float radius) foreach (var mass in points) float dist2 = Vector3.DistanceSquared(position, mass.Position); if (dist2 < radius * radius) mass.ApplyForce(100 * force * (mass.Position - position) / (10000 + dist2)); mass.IncreaseDamping(0.6f);
Usaremos estos tres métodos en Shape Blaster para diferentes efectos..
Dibujaremos la cuadrícula dibujando segmentos de línea entre cada par de puntos vecinos. Primero, haremos un método de extensión en SpriteBatch
eso nos permite dibujar segmentos de línea tomando una textura de un solo píxel y estirándola en una línea.
Abre el Art º
Clase y declara una textura para el pixel..
pública estática Texture2D Pixel get; conjunto privado
Puede configurar la textura de píxeles de la misma manera que configuramos las otras imágenes, o simplemente puede agregar las siguientes dos líneas a la Carga de Art. ()
método.
Pixel = new Texture2D (Player.GraphicsDevice, 1, 1); Pixel.SetData (nuevo [] Color.White);
Esto simplemente crea una nueva textura de 1x1px y establece el píxel único en blanco. Ahora agregue el siguiente método en el Extensiones
clase.
DrawLine estático público vacío (este SpriteBatch spriteBatch, Vector2 inicio, Vector2 fin, Color color, flotación espesor = 2f) Vector2 delta = fin - inicio; spriteBatch.Draw (Art.Pixel, inicio, nulo, color, delta.ToAngle (), nuevo Vector2 (0, 0.5f), nuevo Vector2 (delta.Length (), espesor), SpriteEffects.None, 0f);
Este método estira, rota y tiñe la textura de píxeles para producir la línea que deseamos.
A continuación, necesitamos un método para proyectar los puntos de cuadrícula 3D en nuestra pantalla 2D. Normalmente, esto se puede hacer utilizando matrices, pero aquí transformaremos las coordenadas manualmente..
Agregue lo siguiente a la Cuadrícula
clase.
public Vector2 ToVec2 (Vector3 v) // hacer un factor flotante de proyección en perspectiva = (v.Z + 2000) / 2000; return (nuevo Vector2 (v.X, v.Y) - screenSize / 2f) * factor + screenSize / 2;
Esta transformación le dará a la cuadrícula una vista en perspectiva donde los puntos lejanos aparecen más cerca en la pantalla. Ahora podemos dibujar la cuadrícula iterando a través de las filas y columnas y dibujando líneas entre ellas.
Draw público vacío (SpriteBatch spriteBatch) int width = points.GetLength (0); int height = points.GetLength (1); Color Color = Nuevo Color (30, 30, 139, 85); // azul oscuro para (int y = 1; y < height; y++) for (int x = 1; x < width; x++) Vector2 left = new Vector2(), up = new Vector2(); Vector2 p = ToVec2(points[x, y].Position); if (x > 1) izquierda = ToVec2 (puntos [x - 1, y]. Posición); espesor del flotador = y% 3 == 1? 3f: 1f; spriteBatch.DrawLine (izquierda, p, color, grosor); if (y> 1) up = ToVec2 (puntos [x, y - 1] .Position); espesor del flotador = x% 3 == 1? 3f: 1f; spriteBatch.DrawLine (arriba, p, color, grosor);
En el codigo anterior, pag
Es nuestro punto actual en la grilla., izquierda
Es el punto directamente a su izquierda y arriba
Es el punto directamente encima de él. Dibujamos cada tercera línea más gruesa tanto horizontal como verticalmente para obtener un efecto visual.
Podemos optimizar la cuadrícula mejorando la calidad visual para un número determinado de resortes sin aumentar significativamente el costo de rendimiento. Vamos a hacer dos optimizaciones de este tipo..
Haremos que la cuadrícula sea más densa agregando segmentos de línea dentro de las celdas de cuadrícula existentes. Lo hacemos dibujando líneas desde el punto medio de un lado de la celda hasta el punto medio del lado opuesto. La imagen de abajo muestra las nuevas líneas interpoladas en rojo..
Dibujar las líneas interpoladas es sencillo. Si tienes dos puntos, una
y segundo
, su punto medio es (a + b) / 2
. Entonces, para dibujar las líneas interpoladas, agregamos el siguiente código dentro de para
bucles de nuestro Dibujar()
método.
if (x> 1 && y> 1) Vector2 upLeft = ToVec2 (puntos [x - 1, y - 1] .Posición); spriteBatch.DrawLine (0.5f * (upLeft + up), 0.5f * (left + p), color, 1f); // línea vertical spriteBatch.DrawLine (0.5f * (upLeft + left), 0.5f * (up + p), color, 1f); // linea horizontal
La segunda mejora es realizar la interpolación en nuestros segmentos de línea recta para convertirlos en curvas más suaves. XNA proporciona la mano Vector2.CatmullRom ()
Método que realiza la interpolación Catmull-Rom. Pase el método cuatro puntos secuenciales en una línea curva, y devolverá puntos a lo largo de una curva suave entre el segundo y el tercer punto que proporcionó.
El quinto argumento para Vector2.CatmullRom ()
es un factor de ponderación que determina qué punto de la curva interpolada retorna. Un factor de ponderación de 0
o 1
devolverá respectivamente el segundo o tercer punto que proporcionó, y un factor de ponderación de 0.5
devolverá el punto en la curva interpolada a medio camino entre los dos puntos. Moviendo gradualmente el factor de ponderación de cero a uno y dibujando líneas entre los puntos devueltos, podemos producir una curva perfectamente suave. Sin embargo, para mantener el costo de rendimiento bajo, solo tomaremos en consideración un único punto interpolado, con un factor de ponderación de 0.5
. Luego reemplazamos la línea recta original en la cuadrícula con dos líneas que se encuentran en el punto interpolado.
El siguiente diagrama muestra el efecto de esta interpolación..
Dado que los segmentos de línea en la cuadrícula ya son pequeños, el uso de más de un punto interpolado generalmente no hace una diferencia notable.
A menudo, las líneas en nuestra cuadrícula serán muy rectas y no requerirán ningún suavizado. Podemos verificar esto y evitar tener que dibujar dos líneas en lugar de una. Verificamos si la distancia entre el punto interpolado y el punto medio de la línea recta es mayor que un píxel. Si es así, asumimos que la línea es curva y dibujamos dos segmentos de línea. La modificación a nuestra Dibujar()
el método para agregar la interpolación Catmull-Rom para las líneas horizontales se muestra a continuación.
izquierda = ToVec2 (puntos [x - 1, y]. Posición); espesor del flotador = y% 3 == 1? 3f: 1f; // usa la interpolación Catmull-Rom para ayudar a suavizar las curvas en la cuadrícula int clampedX = Math.Min (x + 1, width - 1); Vector2 mid = Vector2.CatmullRom (ToVec2 (puntos [x - 2, y] .Position), izquierda, p, ToVec2 (puntos [clampedX, y] .Position), 0.5f); // Si la cuadrícula es muy recta aquí, dibuja una sola línea recta. De lo contrario, dibuje líneas en nuestro // nuevo punto intermedio interpolado if (Vector2.DistanceSquared (mid, (left + p) / 2)> 1) spriteBatch.DrawLine (left, mid, color, grosor); spriteBatch.DrawLine (mid, p, color, grosor); else spriteBatch.DrawLine (izquierda, p, color, grosor);
La imagen de abajo muestra los efectos del suavizado. Se dibuja un punto verde en cada punto interpolado para ilustrar mejor dónde se suavizan las líneas.
Ahora es el momento de usar la cuadrícula en nuestro juego. Comenzamos por declarar un público, estático. Cuadrícula
variable en GameRoot
y creando la grilla en el GameRoot.Initialize ()
método. Crearemos una cuadrícula con aproximadamente 1600 puntos como tal..
const int maxGridPoints = 1600; Vector2 gridSpacing = nuevo Vector2 ((float) Math.Sqrt (Viewport.Width * Viewport.Height / maxGridPoints)); Grid = new Grid (Viewport.Bounds, gridSpacing);
Entonces llamamos Grid.Update ()
y Grid.Draw ()
desde el Actualizar()
y Dibujar()
métodos en GameRoot
. Esto nos permitirá ver la cuadrícula cuando ejecutemos el juego. Sin embargo, todavía tenemos que hacer que varios objetos del juego interactúen con la cuadrícula..
Las balas repelerán la rejilla. Ya hicimos un método para hacerlo llamado. AplicarExplosivoFuerza ()
. Agregue la siguiente línea a la Bullet.Update ()
método.
GameRoot.Grid.ApplyExplosiveForce (0.5f * Velocity.Length (), Position, 80);
Esto hará que las balas repelan la cuadrícula proporcionalmente a su velocidad. Eso fue bastante facil.
Ahora trabajemos en agujeros negros. Añade esta línea a BlackHole.Update ()
.
GameRoot.Grid.ApplyImplosiveForce ((float) Math.Sin (sprayAngle / 2) * 10 + 20, Position, 200);
Esto hace que el agujero negro aspire la rejilla con una cantidad variable de fuerza. Reutilizé el sprayAngle
variable, lo que hará que la fuerza en la cuadrícula vibre en sincronía con el ángulo en que rocía las partículas (aunque a la mitad de la frecuencia debida a la división en dos). La fuerza pasada variará sinusoidalmente entre 10 y 30.
Finalmente, crearemos una onda de choque en la cuadrícula cuando el barco del jugador reaparezca después de la muerte. Lo haremos tirando de la rejilla a lo largo del eje z y luego permitiendo que la fuerza se propague y rebote a través de los resortes. De nuevo, esto solo requiere una pequeña modificación de PlayerShip.Update ()
.
if (IsDead) if (--framesUntilRespawn == 0) GameRoot.Grid.ApplyDirectedForce (nuevo Vector3 (0, 0, 5000), nuevo Vector3 (Posición, 0), 50); regreso;
Tenemos el juego básico y los efectos implementados. Depende de usted convertirlo en un juego completo y pulido con su propio sabor. Intente agregar algunas nuevas mecánicas interesantes, algunos efectos nuevos geniales o una historia única. En caso de que no esté seguro de por dónde empezar, aquí hay algunas sugerencias..
Gracias por leer!