Haz un Neon Vector Shooter en XNA más juego

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 esta parte nos basaremos en el tutorial anterior agregando enemigos, detección de colisiones y puntuación.

Esto es lo que tendremos al final:

Advertencia: Alto!

Agregaremos las siguientes nuevas clases para manejar esto:

  • Enemigo
  • EnemySpawner: Responsable de crear enemigos y aumentar gradualmente la dificultad del juego.
  • Estado de jugador: Sigue la puntuación del jugador, la puntuación más alta y las vidas.

Puede que hayas notado que hay dos tipos de enemigos en el video, pero solo hay uno Enemigo clase. Podríamos derivar subclases de Enemigo para cada tipo de enemigo. Sin embargo, prefiero evitar las jerarquías de clases profundas porque tienen algunos inconvenientes:

  • Añaden más código repetitivo..
  • Pueden aumentar la complejidad del código y hacer que sea más difícil de entender. El estado y la funcionalidad de un objeto se extienden a lo largo de toda su cadena de herencia..
  • No son muy flexibles. No puede compartir piezas de funcionalidad entre diferentes ramas del árbol de herencia si esa funcionalidad no está en la clase base. Por ejemplo, considere hacer dos clases, Mamífero y Pájaro, que ambos derivan de Animal. los Pájaro clase tiene un Volar() método. Entonces decides agregar un Murciélago clase que se deriva de Mamífero y también puede volar. Para compartir esta funcionalidad usando solo la herencia, tendría que mover la Volar() método para el Animal Clase a la que no pertenece. Además, no puede eliminar métodos de clases derivadas, por lo que si realizó una Pingüino clase que deriva de Pájaro, también tendría un Volar() método.

Para este tutorial, favoreceremos la composición sobre la herencia para implementar los diferentes tipos de enemigos. Haremos esto creando varios comportamientos reutilizables que podemos agregar a los enemigos. Entonces podemos mezclar y combinar comportamientos fácilmente cuando creamos nuevos tipos de enemigos. Por ejemplo, si ya tuviéramos un FollowPlayer comportamiento y un DodgeBullet comportamiento, podríamos crear un nuevo enemigo que haga ambos simplemente agregando ambos comportamientos.

Artículos Relacionados
  • Introducción a la programación orientada a objetos para el desarrollo de juegos
  • Un enfoque pragmático de la composición de la entidad

Enemigos

Los enemigos tendrán algunas propiedades adicionales sobre las entidades. Para que el jugador tenga tiempo de reaccionar, haremos que los enemigos desaparezcan gradualmente antes de que se vuelvan activos y peligrosos..

Vamos a codificar la estructura básica de la Enemigo clase.

 clase Enemigo: Entidad private int timeUntilStart = 60; public bool IsActive get return timeUntilStart <= 0;   public Enemy(Texture2D image, Vector2 position)  this.image = image; Position = position; Radius = image.Width / 2f; color = Color.Transparent;  public override void Update()  if (timeUntilStart <= 0)  // enemy behaviour logic goes here.  else  timeUntilStart--; color = Color.White * (1 - timeUntilStart / 60f);  Position += Velocity; Position = Vector2.Clamp(Position, Size / 2, GameRoot.ScreenSize - Size / 2); Velocity *= 0.8f;  public void WasShot()  IsExpired = true;  

Este código hará que los enemigos se desvanezcan durante 60 cuadros y permitirá que funcione su velocidad. Multiplicando la velocidad por 0.8 se falsifica un efecto similar a la fricción. Si hacemos que los enemigos aceleren a una velocidad constante, esta fricción hará que se acerquen suavemente a una velocidad máxima. Me gusta la simplicidad y la suavidad de este tipo de fricción, pero es posible que desee utilizar una fórmula diferente según el efecto que desee.

los Fue disparado() Método será llamado cuando el enemigo recibe un disparo. Lo añadiremos más adelante en la serie..

Queremos que los diferentes tipos de enemigos se comporten de manera diferente. Lo lograremos asignando comportamientos. Un comportamiento usará alguna función personalizada que ejecuta cada cuadro para controlar al enemigo. Implementaremos el comportamiento utilizando un iterador.

Los iteradores (también llamados generadores) en C # son métodos especiales que pueden detenerse a medio camino y luego reanudarse donde se quedaron. Puedes hacer un iterador haciendo un método con un tipo de retorno de IEnumerable <> y usando la palabra clave de rendimiento donde desea que regrese y luego se reanude. Los iteradores en C # requieren que devuelvas algo cuando cedes. Realmente no necesitamos devolver nada, por lo que nuestros iteradores simplemente obtendrán cero.

Nuestro comportamiento más simple será el FollowPlayer () comportamiento mostrado abajo.

 Enumerable FollowPlayer (aceleración flotante = 1f) while (verdadero) Velocity + = (PlayerShip.Instance.Position - Position) .ScaleTo (aceleración); if (Velocity! = Vector2.Zero) Orientation = Velocity.ToAngle (); rendimiento de retorno 0; 

Esto simplemente hace que el enemigo acelere hacia el jugador a una velocidad constante. La fricción que agregamos anteriormente asegurará que finalmente se complete a una velocidad máxima (5 píxeles por cuadro cuando la aceleración es 1 desde \ (0.8 \ veces 5 + 1 = 5 \)). En cada cuadro, este método se ejecutará hasta que llegue a la declaración de rendimiento y luego se reanudará donde lo dejó el siguiente cuadro..

Quizás se esté preguntando por qué nos molestamos con los iteradores, ya que podríamos haber logrado la misma tarea más fácilmente con un simple delegado. El uso de iteradores se amortiza con métodos más complejos que de otra manera nos requerirían almacenar el estado en las variables miembro en la clase.

Por ejemplo, a continuación hay un comportamiento que hace que un enemigo se mueva en un patrón cuadrado:

 Enumerable MoveInASquare () const int framesPerSide = 30; while (verdadero) // moverse a la derecha durante 30 cuadros para (int i = 0; i < framesPerSide; i++)  Velocity = Vector2.UnitX; yield return 0;  // move down for (int i = 0; i < framesPerSide; i++)  Velocity = Vector2.UnitY; yield return 0;  // move left for (int i = 0; i < framesPerSide; i++)  Velocity = -Vector2.UnitX; yield return 0;  // move up for (int i = 0; i < framesPerSide; i++)  Velocity = -Vector2.UnitY; yield return 0;   

Lo bueno de esto es que no solo nos ahorra algunas variables de instancia, sino que también estructura el código de una manera muy lógica. Puede ver de inmediato que el enemigo se moverá hacia la derecha, luego hacia abajo, luego hacia la izquierda, luego hacia arriba y luego repita. Si tuviera que implementar este método como una máquina de estado, el flujo de control sería menos obvio.

Agreguemos los andamios necesarios para que los comportamientos funcionen. Los enemigos necesitan almacenar sus comportamientos, por lo que agregaremos una variable a la Enemigo clase.

 Lista privada> comportamientos = nueva lista> ();

Tenga en cuenta que un comportamiento tiene el tipo IEnumerador, no Enumerable. Puedes pensar en el Enumerable como la plantilla para el comportamiento y la IEnumerador como la instancia en ejecución. los IEnumerador recuerda dónde nos encontramos en el comportamiento y continuaremos donde lo dejó cuando usted llama a su MoveNext () método. Cada cuadro repasaremos todos los comportamientos que tiene el enemigo y lo llamaremos. MoveNext () en cada uno de ellos. Si MoveNext () devuelve false, significa que el comportamiento se ha completado, por lo que deberíamos eliminarlo de la lista.

Agregaremos los siguientes métodos a la Enemigo clase:

 vacío AddBehaviour privado (IEnumerable comportamiento) behaviours.Add (behaviour.GetEnumerator ());  vacío privado ApplyBehaviours () para (int i = 0; i < behaviours.Count; i++)  if (!behaviours[i].MoveNext()) behaviours.RemoveAt(i--);  

Y modificaremos la Actualizar() método para llamar AplicarBehaviours ():

 if (timeUntilStart <= 0) ApplyBehaviours(); //… 

Ahora podemos hacer un método estático para crear enemigos en busca. Todo lo que tenemos que hacer es seleccionar la imagen que queremos y agregar la FollowPlayer () comportamiento.

 Enemigo público estático CreateSeeker (posición Vector2) var enemy = new Enemy (Art.Seeker, position); enemy.AddBehaviour (enemy.FollowPlayer ()); enemigo de vuelta 

Para hacer que un enemigo se mueva al azar, haremos que elija una dirección y luego hagamos pequeños ajustes al azar en esa dirección. Sin embargo, si ajustamos la dirección en cada fotograma, el movimiento será inestable, por lo que solo ajustaremos la dirección periódicamente. Si el enemigo se topa con el borde de la pantalla, haremos que elija una nueva dirección aleatoria que apunte lejos de la pared..

 Enumerable MoveRandomly () float direction = rand.NextFloat (0, MathHelper.TwoPi); while (verdadero) dirección + = rand.NextFloat (-0.1f, 0.1f); direction = MathHelper.WrapAngle (direction); para (int i = 0; i < 6; i++)  Velocity += MathUtil.FromPolar(direction, 0.4f); Orientation -= 0.05f; var bounds = GameRoot.Viewport.Bounds; bounds.Inflate(-image.Width, -image.Height); // if the enemy is outside the bounds, make it move away from the edge if (!bounds.Contains(Position.ToPoint())) direction = (GameRoot.ScreenSize / 2 - Position).ToAngle() + rand.NextFloat(-MathHelper.PiOver2, MathHelper.PiOver2); yield return 0;   

Ahora podemos hacer un método de fábrica para crear enemigos errantes, como lo hicimos para el buscador:

 public Enemy CreateWanderer (posición Vector2) var enemy = new Enemy (Art.Wanderer, position); enemy.AddBehaviour (enemy.MoveRandomly ()); enemigo de vuelta 

Detección de colisiones

Para la detección de colisiones, modelaremos la nave del jugador, los enemigos y las balas como círculos. La detección de colisiones circulares es buena porque es simple, rápida y no cambia cuando los objetos giran. Si recuerdas, el Entidad la clase tiene un radio y una posición (la posición se refiere al centro de la entidad). Esto es todo lo que necesitamos para la detección de colisiones circulares..

Probar cada entidad contra todas las demás entidades que potencialmente podrían colisionar puede ser muy lento si tiene una gran cantidad de entidades. Existen muchas técnicas que puede utilizar para acelerar la detección de colisiones de fase amplia, como quadtrees, barrido y podado, y árboles BSP. Sin embargo, por ahora, solo tendremos unas pocas docenas de entidades en pantalla a la vez, por lo que no nos preocuparemos por estas técnicas más complejas. Siempre podemos agregarlos más tarde si los necesitamos..

En Shape Blaster, no todas las entidades pueden colisionar con cualquier otro tipo de entidad. Las balas y la nave del jugador solo pueden chocar con enemigos. Los enemigos también pueden chocar con otros enemigos, lo que evitará que se superpongan..

Para lidiar con estos diferentes tipos de colisiones, agregaremos dos nuevas listas a la EntityManager Para realizar un seguimiento de las balas y los enemigos. Cada vez que agregamos una entidad a la EntityManager, Queremos agregarlo a la lista correspondiente, por lo que haremos un privado AddEntity () Método para hacerlo. También nos aseguraremos de eliminar cualquier entidad caducada de todas las listas de cada fotograma..

 lista estática enemigos = nueva lista(); lista estática viñetas = nueva lista(); anulación estática privada AddEntity (entidad de entidad) entidades.Add (entidad); si (la entidad es viñeta) bullets.Add (entidad como viñeta); de lo contrario, si (la entidad es Enemigo) enemigos. Agrega (entidad como Enemigo);  //… // en las viñetas Update () = bullets.Where (x =>! X.IsExpired) .ToList (); enemigos = enemigos.Donde (x =>! x.IsExpired) .ToList ();

Reemplace las llamadas a entity.Add () en EntityManager.Add () y EntityManager.Update () con llamadas a AddEntity ().

Ahora agreguemos un método que determinará si dos entidades están chocando:

 bool estático privado IsColliding (Entidad a, Entidad b) radio flotante = a.Radio + b.Radio; volver! a.IsExpired &&! b.IsExpired && Vector2.DistanceSquared (a.Position, b.Position) < radius * radius; 

Para determinar si dos círculos se superponen, simplemente verifique si la distancia entre ellos es menor que la suma de sus radios. Nuestro método optimiza esto ligeramente al verificar si el cuadrado de la distancia es menor que el cuadrado de la suma de los radios. Recuerde que es un poco más rápido calcular la distancia al cuadrado que la distancia real.

Diferentes cosas sucederán dependiendo de que dos objetos colisionen. Si dos enemigos chocan, queremos que se empujen entre sí. Si una bala golpea a un enemigo, la bala y el enemigo deben ser destruidos. Si el jugador toca a un enemigo, el jugador debería morir y el nivel debería restablecerse..

Añadiremos un HandleCollision () método para el Enemigo clase para manejar las colisiones entre enemigos:

 Public void HandleCollision (otro enemigo) var d = Position - other.Position; Velocidad + = 10 * d / (d.LengthSquared () + 1); 

Este método empujará al enemigo actual lejos del otro enemigo. Cuanto más cerca estén, más será empujado, porque la magnitud de (d / d.LengthSquared ()) es solo uno sobre la distancia.

Reapareciendo el jugador

A continuación necesitamos un método para controlar la muerte del jugador. Cuando esto sucede, la nave del jugador desaparecerá por un corto tiempo antes de reaparecer.

Comenzamos agregando dos nuevos miembros a JugadorEnvío.

 int framesUntilRespawn = 0; public bool IsDead get return framesUntilRespawn> 0; 

Al principio de PlayerShip.Update (), agregue lo siguiente:

 if (IsDead) framesUntilRespawn--; regreso; 

Y anulamos Dibujar() como se muestra:

 public override void Draw (SpriteBatch spriteBatch) if (! IsDead) base.Draw (spriteBatch); 

Finalmente, agregamos un Matar() método para JugadorEnvío.

 public void Kill () framesUntilRespawn = 60; 

Ahora que todas las piezas están en su lugar, agregaremos un método a la EntityManager Que pasa por todas las entidades y verifica colisiones..

 estático void HandleCollisions () // manejar colisiones entre enemigos para (int i = 0; i < enemies.Count; i++) for (int j = i + 1; j < enemies.Count; j++)  if (IsColliding(enemies[i], enemies[j]))  enemies[i].HandleCollision(enemies[j]); enemies[j].HandleCollision(enemies[i]);   // handle collisions between bullets and enemies for (int i = 0; i < enemies.Count; i++) for (int j = 0; j < bullets.Count; j++)  if (IsColliding(enemies[i], bullets[j]))  enemies[i].WasShot(); bullets[j].IsExpired = true;   // handle collisions between the player and enemies for (int i = 0; i < enemies.Count; i++)  if (enemies[i].IsActive && IsColliding(PlayerShip.Instance, enemies[i]))  PlayerShip.Instance.Kill(); enemies.ForEach(x => x.WasShot ()); descanso; 

Llame a este método desde Actualizar() inmediatamente después de configurar isUpdating a cierto.


Engendrador enemigo

Lo último que hay que hacer es hacer el EnemySpawner Clase, que se encarga de crear enemigos. Queremos que el juego comience fácil y se vuelva más difícil, por lo que EnemySpawner Creará enemigos a un ritmo cada vez mayor a medida que avanza el tiempo. Cuando el jugador muere, restableceremos la EnemySpawner a su dificultad inicial.

 clase estática EnemySpawner static Random rand = new Random (); flotador estático inverseSpawnChance = 60; Actualización estática pública vacía () si (! PlayerShip.Instance.IsDead && EntityManager.Count < 200)  if (rand.Next((int)inverseSpawnChance) == 0) EntityManager.Add(Enemy.CreateSeeker(GetSpawnPosition())); if (rand.Next((int)inverseSpawnChance) == 0) EntityManager.Add(Enemy.CreateWanderer(GetSpawnPosition()));  // slowly increase the spawn rate as time progresses if (inverseSpawnChance > 20) inverseSpawnChance - = 0.005f;  privada estática Vector2 GetSpawnPosition () Vector2 pos; do pos = new Vector2 (rand.Next ((int) GameRoot.ScreenSize.X), rand.Next ((int) GameRoot.ScreenSize.Y));  while (Vector2.DistanceSquared (pos, PlayerShip.Instance.Position) < 250 * 250); return pos;  public static void Reset()  inverseSpawnChance = 60;  

Cada cuadro, hay uno en inverseSpawnChance De generar cada tipo de enemigo. La probabilidad de engendrar a un enemigo aumenta gradualmente hasta alcanzar un máximo de uno en veinte. Los enemigos siempre se crean al menos a 250 píxeles del jugador..

Tenga cuidado con el bucle while en GetSpawnPosition (). Funcionará de manera eficiente siempre y cuando el área en la que los enemigos puedan engendrar sea más grande que el área donde no puedan engendrar. Sin embargo, si hace que el área prohibida sea demasiado grande, obtendrá un bucle infinito.

Llamada EnemySpawner.Update () desde GameRoot.Update () y llama EnemySpawner.Reset () cuando el jugador muere.


Score and Lives

En Shape Blaster, comenzarás con cuatro vidas y ganarás una vida adicional cada 2000 puntos. Recibes puntos por destruir enemigos, con diferentes tipos de enemigos que valen diferentes cantidades de puntos. Cada enemigo destruido también aumenta tu puntaje multiplicador en uno. Si no matas a ningún enemigo en un corto período de tiempo, tu multiplicador se reiniciará. La cantidad total de puntos recibidos de cada enemigo que destruyas es el número de puntos que vale el enemigo multiplicado por tu multiplicador actual. Si pierdes todas tus vidas, el juego termina y comienzas un juego nuevo con tu puntuación restablecida a cero.

Para manejar todo esto, haremos una clase estática llamada Estado de jugador.

 clase estática PlayerStatus // cantidad de tiempo que lleva, en segundos, que un multiplicador caduque. private const float multiplierExpiryTime = 0.8f; privado const int maxMultiplier = 20; public static int Lives get; conjunto privado  public static int Score get; conjunto privado  public static int Multiplicador obtener; conjunto privado  private static float multiplicierTimeLeft; // tiempo transcurrido hasta que el multiplicador actual caduca privado estático int scoreForExtraLife; // puntaje requerido para obtener una vida extra // constructor estático static PlayerStatus () Reset ();  restablecer el vacío estático público () Puntuación = 0; Multiplicador = 1; Vidas = 4; scoreForExtraLife = 2000; multiplierTimeLeft = 0;  Actualización de anulación estática pública () si (Multiplicador> 1) // actualiza el temporizador multiplicador si ((multiplicarTimeLeft - = (float) GameRoot.GameTime.ElapsedGameTime.TotalSeconds) <= 0)  multiplierTimeLeft = multiplierExpiryTime; ResetMultiplier();    public static void AddPoints(int basePoints)  if (PlayerShip.Instance.IsDead) return; Score += basePoints * Multiplier; while (Score >= scoreForExtraLife) scoreForExtraLife + = 2000; Vidas ++;  public static void IncreaseMultiplier () if (PlayerShip.Instance.IsDead) return; multiplierTimeLeft = multiplierExpiryTime; si (multiplicador < maxMultiplier) Multiplier++;  public static void ResetMultiplier()  Multiplier = 1;  public static void RemoveLife()  Lives--;  

Llamada PlayerStatus.Update () desde GameRoot.Update () cuando el juego no se detiene.

A continuación queremos mostrar tu puntuación, vidas y multiplicador en la pantalla. Para ello tendremos que añadir un SpriteFont en el Contenido proyecto y una variable correspondiente en el Art º clase, que nombraremos Fuente. Cargar la fuente en Carga de Art. () Como hicimos con las texturas..

Nota: Hay una fuente llamada Nova Square incluida con los archivos de origen de Shape Blaster que puede usar. Para usar la fuente, primero debe instalarla y luego reiniciar Visual Studio si estaba abierta. A continuación, puede cambiar el nombre de la fuente en el archivo de fuente sprite a "Nova Square". El proyecto de demostración no usa esta fuente por defecto porque evitará que el proyecto se compile si la fuente no está instalada.

Modificar el final de GameRoot.Draw () donde se dibuja el cursor como se muestra abajo.

 spriteBatch.Begin (0, BlendState.Additive); spriteBatch.DrawString (Art.Font, "Lives:" + PlayerStatus.Lives, nuevo Vector2 (5), Color.White); DrawRightAlignedString ("Score:" + PlayerStatus.Score, 5); DrawRightAlignedString ("Multiplier:" + PlayerStatus.Multiplier, 35); // dibujar el cursor del mouse personalizado spriteBatch.Draw (Art.Pointer, Input.MousePosition, Color.White); spriteBatch.End ();

DrawRightAlignedString () es un método auxiliar para dibujar texto alineado en el lado derecho de la pantalla. Agregarlo a GameRoot añadiendo el siguiente código.

 private void DrawRightAlignedString (texto de cadena, flotante y) var textWidth = Art.Font.MeasureString (text) .X; spriteBatch.DrawString (Art.Font, text, new Vector2 (ScreenSize.X - textWidth - 5, y), Color.White); 

Ahora sus vidas, puntaje y multiplicador deberían aparecer en pantalla. Sin embargo, todavía necesitamos modificar estos valores en respuesta a los eventos del juego. Añadir una propiedad llamada PuntoValor al Enemigo clase.

 public int PointValue get; conjunto privado 

Establezca el valor de puntos para diferentes enemigos a algo que considere apropiado. Hice que los enemigos errantes valieran un punto, y los enemigos que buscaban valían dos puntos.

A continuación, agregue las siguientes dos líneas a Enemigo.WasShot () Para aumentar el puntaje y el multiplicador del jugador:

 PlayerStatus.AddPoints (PointValue); PlayerStatus.IncreaseMultiplier ();

Llamada PlayerStatus.RemoveLife () en PlayerShip.Kill (). Si el jugador pierde todas sus vidas, llame PlayerStatus.Reset () para restablecer su puntuación y vidas al comienzo de un nuevo juego.

Puntuaciones altas

Agreguemos la habilidad del juego para rastrear tu mejor puntaje. Queremos que esta puntuación persista en todas las jugadas, así que la guardaremos en un archivo. Lo mantendremos realmente simple y guardaremos la puntuación más alta como un solo número de texto sin formato en un archivo en el directorio de trabajo actual (este será el mismo directorio que contiene el .exe expediente).

Agregue los siguientes métodos para Estado de jugador:

 private const string highScoreFilename = "highscore.txt"; private static int LoadHighScore () // devolver la puntuación más alta guardada si es posible y devolver 0 de lo contrario int score; volver File.Exists (highScoreFilename) && int.TryParse (File.ReadAllText (highScoreFilename), fuera de la puntuación)? puntuación: 0;  privado static void SaveHighScore (puntuación int) File.WriteAllText (highScoreFilename, score.ToString ()); 

los LoadHighScore () el método primero verifica que el archivo de puntaje alto exista y luego verifica que contenga un entero válido La segunda verificación probablemente nunca fallará a menos que el usuario edite manualmente el archivo de puntaje alto a algo no válido, pero es bueno ser cauteloso.

Queremos cargar la puntuación más alta cuando se inicia el juego, y guardarla cuando el jugador obtenga una nueva puntuación más alta. Modificaremos el constructor estático y Reiniciar() métodos en Estado de jugador para hacerlo También añadiremos una propiedad auxiliar., IsGameOver que usaremos en un momento.

 bool estático público IsGameOver get return Lives == 0;  static PlayerStatus () HighScore = LoadHighScore (); Reiniciar();  restablecer el vacío estático público () si (Puntuación> HighScore) SaveHighScore (HighScore = Puntuación); Puntuación = 0; Multiplicador = 1; Vidas = 4; scoreForExtraLife = 2000; multiplierTimeLeft = 0; 

Eso se encarga de rastrear la puntuación más alta. Ahora tenemos que mostrarlo. Agregue el siguiente código a GameRoot.Draw () en el mismo SpriteBatch Bloque donde se dibuja el otro texto:

 if (PlayerStatus.IsGameOver) string text = "Game Over \ n" + "Tu puntaje:" + PlayerStatus.Score + "\ n" + "Puntaje alto:" + PlayerStatus.HighScore; Vector2 textSize = Art.Font.MeasureString (text); spriteBatch.DrawString (Art.Font, text, ScreenSize / 2 - textSize / 2, Color.White); 

Esto hará que muestre tu puntuación y tu puntuación más alta en el juego, centrado en la pantalla.

Como ajuste final, aumentaremos el tiempo antes de que la nave reaparezca en el juego para darle tiempo al jugador para ver su puntuación. Modificar PlayerShip.Kill () estableciendo el tiempo de reaparición en 300 cuadros (cinco segundos) si el jugador está fuera de vida.

 // en PlayerShip.Kill () PlayerStatus.RemoveLife (); framesUntilRespawn = PlayerStatus.IsGameOver? 300: 120;

El juego ya está listo para jugar. Puede que no parezca mucho, pero tiene implementados todos los mecanismos básicos. En futuros tutoriales agregaremos un filtro de floración y efectos de partículas para enriquecerlo. Pero en este momento, agreguemos rápidamente algo de sonido y música para hacerlo más interesante..


Sonido y musica

Reproducir sonido y música es fácil en XNA. Primero, agregamos nuestros efectos de sonido y música a la tubería de contenido. En el Propiedades panel, asegúrese de que el procesador de contenido esté configurado en Canción por la musica y Efecto de sonido por los sonidos.

A continuación, hacemos una clase de ayuda estática para los sonidos..

 Clase estática Sonido música estática pública de canciones obtener; conjunto privado  private static readonly Random rand = new Random (); explosiones privadas de efectos de sonido [] estáticas; // devolver una explosión aleatoria sonido público estático SoundEffect Explosion get return explosions [rand.Next (explosions.Length)];  SoundEffect [] tiros privados estáticos; SoundEffect Shot estático público get return shots [rand.Next (shots.Length)];  el efecto estático privado SoundEffect [] genera; SoundEffect Spawn estático público get return engendra [rand.Next (spawns.Length)];  Public static void Load (contenido de ContentManager) Music = content.Load("Sonido / Música"); // Estas expresiones linq son solo una forma elegante de cargar todos los sonidos de cada categoría en una matriz. explosions = Enumerable.Range (1, 8) .Select (x => content.Load)("Sound / explosion-0" + x)). ToArray (); disparos = Enumerable. Rango (1, 4). Seleccione (x => contenido. Cargar("Sound / shoot-0" + x)). ToArray (); spawns = Enumerable.Range (1, 8) .Select (x => content.Load)("Sound / spawn-0" + x)). ToArray (); 

Como tenemos múltiples variaciones de cada sonido, el Explosión, Disparo, y Desovar Las propiedades elegirán un sonido al azar entre las variantes..

Llamada Sonido.carga () en GameRoot.LoadContent (). Para reproducir la música, agregue las siguientes dos líneas al final de GameRoot.Initialize ().

 MediaPlayer.IsRepeating = true; MediaPlayer.Play (Sound.Music);

Para reproducir sonidos en XNA, simplemente puede llamar al Jugar() método en un Efecto de sonido. Este método también proporciona una sobrecarga que le permite ajustar el volumen, el tono y el panorama del sonido. Un truco para hacer que nuestros sonidos sean más variados es ajustar estas cantidades en cada juego..

Para activar el efecto de sonido para el disparo, agregue la siguiente línea en PlayerShip.Update (), dentro de la sentencia if donde se crean las balas. Tenga en cuenta que cambiamos aleatoriamente el tono hacia arriba o hacia abajo, hasta una quinta parte de una octava, para hacer que los sonidos sean menos repetitivos.

 Sound.Shot.Play (0.2f, rand.NextFloat (-0.2f, 0.2f), 0);

Del mismo modo, active un efecto de sonido de explosión cada vez que se destruya un enemigo agregando lo siguiente a Enemigo.WasShot ().

 Sound.Explosion.Play (0.5f, rand.NextFloat (-0.2f, 0.2f), 0);

Ahora tienes sonido y música en tu juego. Fácil no lo es?


Conclusión

Eso envuelve la mecánica de juego básica. En el siguiente tutorial, agregaremos un filtro de floración para hacer que las luces de neón se iluminen..