En esta serie de tutoriales, te mostraré cómo hacer un juego de disparos de neón doble como Geometry Wars, que llamaremos Shape Blaster, 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..
Le animo a ampliar y experimentar con el código dado en estos tutoriales. Cubriremos estos temas a través de la serie:
Esto es lo que tendremos al final de la serie:
Advertencia: Alto!Y esto es lo que tendremos al final de esta primera parte:
Advertencia: Alto!La música y los efectos de sonido que puedes escuchar en estos videos fueron creados por RetroModular, y puedes leer cómo lo hizo en Audiotuts.+.
Los sprites son de Jacob Zinman-Jeanes, nuestro diseñador residente de Tuts +. Todas las ilustraciones se pueden encontrar en el archivo fuente de descarga zip.
La fuente es Nova Square, por Wojciech Kalinowski.Empecemos.
En este tutorial crearemos un shooter de doble palo; el jugador controlará la nave con el teclado, el teclado y el mouse, o con los dos pulgares de un gamepad.
Usamos varias clases para lograr esto:
Entidad
: La clase base para enemigos, balas y la nave del jugador. Las entidades pueden moverse y ser dibujadas..Bala
y JugadorEnvío
.EntityManager
: Realiza un seguimiento de todas las entidades en el juego y realiza la detección de colisiones.Entrada
: Ayuda a administrar la entrada desde el teclado, el mouse y el gamepad.Art º
: Carga y guarda referencias a las texturas necesarias para el juego.Sonar
: Carga y guarda referencias a los sonidos y la música..Matemáticas
y Extensiones
: Contiene algunos métodos estáticos útiles y métodos de extensión.GameRoot
: Controla el bucle principal del juego. Este es el Juego1
clase XNA genera automáticamente, renombrado.El código en este tutorial pretende ser simple y fácil de entender. No tendrá todas las características o una arquitectura complicada diseñada para satisfacer todas las necesidades posibles. Más bien, hará solo lo que necesita hacer. Manteniéndolo simple te hará más fácil entender los conceptos, y luego modificarlos y expandirlos en tu propio juego único..
Crear un nuevo proyecto XNA. Renombrar Juego1
Clase a algo mas adecuado. lo llamé GameRoot
.
Ahora vamos a empezar por crear una clase base para nuestras entidades de juego..
clase abstracta Entidad imagen Texture2D protegida; // El tinte de la imagen. Esto también nos permitirá cambiar la transparencia. color de color protegido = color.blanco; Posición Vector2 pública, Velocidad; flotación pública Orientación; flotador público Radio = 20; // utilizado para la detección de colisión circular bool público IsExpired; // verdadero si la entidad fue destruida y debería ser eliminada. Public Vector2 Size get return image == null? Vector2.Zero: nuevo Vector2 (image.Width, image.Height); public abstract void Update (); vacío público público Draw (SpriteBatch spriteBatch) spriteBatch.Draw (imagen, posición, nulo, color, orientación, tamaño / 2f, 1f, 0, 0);
Todas nuestras entidades (enemigos, balas y la nave del jugador) tienen algunas propiedades básicas, como una imagen y una posición.. Está expirado
se utilizará para indicar que la entidad ha sido destruida y debe eliminarse de cualquier lista que contenga una referencia a ella.
A continuación creamos un EntityManager
Rastrear nuestras entidades y actualizarlas y dibujarlas..
clase estática EntityManager lista estáticaentidades = nueva lista (); estática bool isUpdating; lista estática addedEntities = nueva Lista (); public static int Count get return entities.Count; public static void Agregar (entidad de entidad) if (! isUpdating) entes. Añadir (entidad); else AddedEntities.Add (entidad); Actualización estática pública vacía () isUpdating = true; foreach (entidad var en entidades) entity.Update (); isUpdating = false; Foreach (entidad de var en entidades agregadas). Agregar (entidad); addedEntities.Clear (); // eliminar cualquier entidad caducada. Entidades = Entidades. Donde (x =>! x.IsExpired) .ToList (); public static void Draw (SpriteBatch spriteBatch) foreach (entidad de var en entidades) entidad.Draw (spriteBatch);
Recuerde, si modifica una lista mientras está iterando sobre ella, obtendrá una excepción. El código anterior se ocupa de esto al poner en cola las entidades agregadas durante la actualización en una lista separada, y agregarlas después de que finalice la actualización de las entidades existentes.
Tendremos que cargar algunas texturas si queremos dibujar algo. Haremos una clase estática para contener referencias a todas nuestras texturas..
clase estática Arte public static Texture2D Player get; conjunto privado Público estático Texture2D Seeker get; conjunto privado public static Texture2D Wanderer get; conjunto privado public static Texture2D Bullet get; conjunto privado Puntero Texture2D estático público obtener; conjunto privado Carga estática pública vacía (contenido de ContentManager) Player = content.Load("Jugador"); Buscador = contenido.Cargar ("Buscador"); Wanderer = content.Load ("Vagabundo"); Bullet = content.Load ("Bala"); Puntero = contenido.Cargar ("Puntero");
Carga el arte llamando Art.Load (Contenido)
en GameRoot.LoadContent ()
. Además, varias clases necesitarán conocer las dimensiones de la pantalla, así que agregue las siguientes propiedades a GameRoot
:
instancia pública de GameRoot estática get; conjunto privado public static Viewport Viewport get return Instance.GraphicsDevice.Viewport; public static Vector2 ScreenSize get return new Vector2 (Viewport.Width, Viewport.Height);
Y en el GameRoot
constructor, añadir:
Instancia = esto;
Ahora empezaremos a escribir el JugadorEnvío
clase.
clase PlayerShip: Entidad instancia estática privada de PlayerShip; instancia estática pública de PlayerShip get if (instance == null) instance = new PlayerShip (); instancia de retorno Private PlayerShip () image = Art.Player; Position = GameRoot.ScreenSize / 2; Radio = 10; Public Override void Update () // la lógica de envío va aquí
Nosotros hicimos JugadorEnvío
Un singleton, establece su imagen, y la coloca en el centro de la pantalla..
Finalmente, agreguemos la nave del jugador a la EntityManager
y actualízalo y dibújalo. Agrega el siguiente código en GameRoot
:
// en Initialize (), después de la llamada a base.Initialize () EntityManager.Add (PlayerShip.Instance); // en Update () EntityManager.Update (); // en Draw () GraphicsDevice.Clear (Color.Black); spriteBatch.Begin (SpriteSortMode.Texture, BlendState.Additive); EntityManager.Draw (spriteBatch); spriteBatch.End ();
Dibujamos los sprites con mezcla aditiva, Que es parte de lo que les dará su look neón. Si ejecutas el juego en este punto, deberías ver tu nave en el centro de la pantalla. Sin embargo, todavía no responde a la entrada. Vamos a arreglar eso.
Para el movimiento, el jugador puede usar WASD en el teclado o la palanca de control izquierda en un gamepad. Para apuntar, pueden usar las teclas de flecha, la barra de control derecha o el mouse. No requerimos que el jugador mantenga presionado el botón del mouse para disparar porque es incómodo sostener el botón continuamente. Esto nos deja con un pequeño problema: ¿cómo sabemos si el jugador está apuntando con el mouse, el teclado o el gamepad??
Usaremos el siguiente sistema: agregaremos entrada de teclado y gamepad juntos. Si el jugador mueve el mouse, cambiamos a apuntar con el mouse. Si el jugador presiona las teclas de flecha o usa la palanca de control derecha, desactivamos el puntero del mouse.
Una cosa a tener en cuenta: al empujar un pulgar hacia adelante se devolverá un positivo y valor. En coordenadas de pantalla, los valores de y aumentan en curso. hacia abajo. Queremos invertir el eje y en el controlador para que empujar la palanca hacia arriba apunte o nos mueva hacia la parte superior de la pantalla..
Haremos una clase estática para realizar un seguimiento de los distintos dispositivos de entrada y cuidar de cambiar entre los diferentes tipos de puntería.
clase estática Entrada private static KeyboardState keyboardState, lastKeyboardState; MouseState estático privado mouseState, lastMouseState; GamePadState estático privado gamepadState, lastGamepadState; bool estático privado isAimingWithMouse = false; Public2 Vector2 MousePosition estático obtener devolver nuevo Vector2 (mouseState.X, mouseState.Y); public static void Update () lastKeyboardState = keyboardState; lastMouseState = mouseState; lastGamepadState = gamepadState; keyboardState = Keyboard.GetState (); mouseState = Mouse.GetState (); gamepadState = GamePad.GetState (PlayerIndex.One); // Si el jugador presionó una de las teclas de flecha o está usando un gamepad para apuntar, queremos desactivar el apuntar con el mouse. De lo contrario, // si el jugador mueve el mouse, habilite la orientación del mouse. if (new [] Keys.Left, Keys.Right, Keys.Up, Keys.Down .Any (x => keyboardState.IsKeyDown (x)) || gamepadState.ThumbSticks.Right! = Vector2.Zero) isAimingWithMouse = falso; de lo contrario, si (MousePosition! = new Vector2 (lastMouseState.X, lastMouseState.Y)) esAimingWithMouse = true; // Comprueba si se acaba de presionar una tecla pública. public static bool WasButtonPressed (Botón de botones) return lastGamepadState.IsButtonUp (botón) && gamepadState.IsButtonDown (botón); Vector estático Vector2 GetMovementDirection () Vector2 direction = gamepadState.ThumbSticks.Left; dirección.Y * = -1; // invierte el eje y si (keyboardState.IsKeyDown (Keys.A)) direction.X - = 1; if (keyboardState.IsKeyDown (Keys.D)) direction.X + = 1; if (keyboardState.IsKeyDown (Keys.W)) direction.Y - = 1; if (keyboardState.IsKeyDown (Keys.S)) direction.Y + = 1; // Sujeta la longitud del vector a un máximo de 1. if (direction.LengthSquared ()> 1) direction.Normalize (); direccion de retorno public static Vector2 GetAimDirection () if (isAimingWithMouse) return getMouseAimDirection (); Vector2 direction = gamepadState.ThumbSticks.Right; dirección.Y * = -1; if (keyboardState.IsKeyDown (Keys.Left)) direction.X - = 1; if (keyboardState.IsKeyDown (Keys.Right)) direction.X + = 1; if (keyboardState.IsKeyDown (Keys.Up)) direction.Y - = 1; if (keyboardState.IsKeyDown (Keys.Down)) direction.Y + = 1; // Si no hay entrada de objetivo, devuelve cero. De lo contrario, normalice la dirección para que tenga una longitud de 1. if (direction == Vector2.Zero) devuelva Vector2.Zero; de lo contrario devuelve Vector2.Normalize (dirección); privada estática Vector2 GetMouseAimDirection () Vector2 direction = MousePosition - PlayerShip.Instance.Position; if (direction == Vector2.Zero) devuelve Vector2.Zero; de lo contrario devuelve Vector2.Normalize (dirección); público estático bool WasBombButtonPressed () return WasButtonPressed (Buttons.Left Trigger) || WasButtonPressed (Buttons.RightTrigger) || WasKeyPressed (Keys.Space);
Llamada Input.Update ()
al principio de GameRoot.Update ()
para que la clase de entrada funcione.
Propina: Puedes notar que incluí un método para bombas. No implementaremos bombas ahora, pero ese método está ahí para uso futuro..
También puede notar en GetMovementDirection ()
escribí direction.LengthSquared ()> 1
. Utilizando LengthSquared ()
Es una pequeña optimización de rendimiento; calcular el cuadrado de la longitud es un poco más rápido que calcular la longitud en sí misma porque evita la operación relativamente lenta de la raíz cuadrada. Verá el código usando los cuadrados de longitudes o distancias a lo largo del programa. En este caso particular, la diferencia de rendimiento es insignificante, pero esta optimización puede marcar la diferencia cuando se utiliza en bucles ajustados.
Ahora estamos listos para hacer que el barco se mueva. Agregue este código a la PlayerShip.Update ()
método:
velocidad de flotación constante = 8; Velocidad = velocidad * Input.GetMovementDirection (); Posición + = Velocidad; Position = Vector2.Clamp (Position, Size / 2, GameRoot.ScreenSize - Size / 2); if (Velocity.LengthSquared ()> 0) Orientación = Velocity.ToAngle ();
Esto hará que la nave se mueva a una velocidad de hasta ocho píxeles por fotograma, fije su posición para que no pueda salir de la pantalla y gire la nave para orientarse en la dirección en la que se está moviendo.
ToAngle ()
Es un método de extensión simple definido en nuestra Extensiones
clase así
flotador estático público ToAngle (este vector Vector2) return (float) Math.Atan2 (vector.Y, vector.X);
Si ejecutas el juego ahora, deberías poder volar la nave. Ahora vamos a hacer que dispare.
Primero, necesitamos una clase de balas..
clase Bullet: Entidad bullet pública (posición Vector2, velocidad Vector2) image = Art.Bullet; Posición = posición; Velocidad = velocidad; Orientación = Velocity.ToAngle (); Radio = 8; Public Override void Update () if (Velocity.LengthSquared ()> 0) Orientación = Velocity.ToAngle (); Posición + = Velocidad; // eliminar las viñetas que salen de la pantalla si (! GameRoot.Viewport.Bounds.Contains (Position.ToPoint ())) IsExpired = true;
Queremos un breve período de enfriamiento entre balas, así que agregue los siguientes campos a la JugadorEnvío
clase.
const int cooldownFrames = 6; int cooldownRemaining = 0; estático Random rand = new Random ();
Además, agregue el siguiente código a PlayerShip.Update ()
.
var aim = Input.GetAimDirection (); if (aim.LengthSquared ()> 0 && cooldownRemaining <= 0) cooldownRemaining = cooldownFrames; float aimAngle = aim.ToAngle(); Quaternion aimQuat = Quaternion.CreateFromYawPitchRoll(0, 0, aimAngle); float randomSpread = rand.NextFloat(-0.04f, 0.04f) + rand.NextFloat(-0.04f, 0.04f); Vector2 vel = MathUtil.FromPolar(aimAngle + randomSpread, 11f); Vector2 offset = Vector2.Transform(new Vector2(25, -8), aimQuat); EntityManager.Add(new Bullet(Position + offset, vel)); offset = Vector2.Transform(new Vector2(25, 8), aimQuat); EntityManager.Add(new Bullet(Position + offset, vel)); if (cooldownRemaining > 0) ReoldownRemaining--;
Este código crea dos balas que viajan paralelas entre sí. Añade una pequeña cantidad de aleatoriedad a la dirección. Esto hace que los disparos se extiendan un poco como una ametralladora. Sumamos dos números aleatorios juntos porque esto hace que su suma sea más centrada (alrededor de cero) y menos probable que envíe balas muy lejos. Utilizamos un cuaternión para rotar la posición inicial de las balas en la dirección en la que viajan..
También utilizamos dos nuevos métodos de ayuda:
Aleatorio.SiguienteFloat ()
devuelve un flotante entre un valor mínimo y máximo.MathUtil.FromPolar ()
crea un Vector2
desde un ángulo y magnitud.// en Extensiones públicas static float NextFloat (este Random aleatorio, float minValue, float maxValue) return (float) rand.NextDouble () * (maxValue - minValue) + minValue; // en MathUtil public static Vector2 FromPolar (ángulo de flotación, magnitud de flotación) magnitud de retorno * nuevo Vector2 ((float) Math.Cos (angle), (float) Math.Sin (angle));
Hay una cosa más que debemos hacer ahora que tenemos la Entrada
clase. Dibujemos un cursor de ratón personalizado para que sea más fácil ver hacia dónde apunta el barco. En JuegoRoot.Draw
, simplemente dibuja Art.Pointer
en la posición del ratón.
spriteBatch.Begin (SpriteSortMode.Texture, BlendState.Additive); EntityManager.Draw (spriteBatch); // dibujar el cursor del mouse personalizado spriteBatch.Draw (Art.Pointer, Input.MousePosition, Color.White); spriteBatch.End ();
Si prueba el juego ahora, podrá mover la nave con las teclas WASD o la palanca de mando izquierda, y apuntar el flujo continuo de balas con las teclas de flecha, el mouse o la palanca de control derecha.
En la siguiente parte, completaremos el juego agregando enemigos y una puntuación..