Crea un juego simple de asteroides usando entidades basadas en componentes

En el tutorial anterior, creamos un sistema Entity basado en componentes. Ahora usaremos este sistema para crear un juego simple de asteroides..


Vista previa del resultado final

Aquí está el simple juego de asteroides que crearemos en este tutorial. Está escrito con Flash y AS3, pero los conceptos generales se aplican a la mayoría de los idiomas..

El código fuente completo está disponible en GitHub.


Resumen de la clase

Hay seis clases:

  • AsteroidsGame, que amplía la clase de juego base y agrega la lógica específica a nuestro juego de disparos en el espacio.
  • Enviar, que es lo que controlas.
  • Asteroide, que es lo que disparas.
  • Bala, que es lo que disparas.
  • Pistola, que crea esas balas.
  • Enemigo, que es un extraterrestre errante que acaba de agregar un poco de variedad al juego.
  • Vayamos a través de estos tipos de entidades uno por uno.


    los Enviar Clase

    Comenzaremos con la nave del jugador:

 paquete de asteroides import com.iainlobb.gamepad.Gamepad; importar com.iainlobb.gamepad.KeyCode; importación engine.Body; motor de importación.Entidad; motor de importación. Juego; motor de importación.Salud; motor de importación. Física; motor de importación.Ver; importar flash.display.GraphicsPathWinding; importar flash.display.Sprite; / ** *… * @autor Iain Lobb - [email protected] * / clase pública Ship se extiende Entidad protegido var gamepad: Gamepad; función pública Ship () body = new Body (this); body.x = 400; body.y = 300; física = nueva física (esto); physics.drag = 0.9; view = new View (this); view.sprite = nuevo Sprite (); view.sprite.graphics.lineStyle (1.5, 0xFFFFFF); view.sprite.graphics.drawPath (Vector.([1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]), Vector.([-7.3, 10.3, -5.5, 10.3, -7, 0.6, -0.5, -2.8, 6.2, 0.3, 4.5, 10.3, 6.3, 10.3, 11.1, -1.4, -0.2, -9.6, -11.9, - 1.3, -7.3, 10.3]), GraphicsPathWinding.NON_ZERO); salud = nueva salud (esto); health.hits = 5; health.died.add (onDied); arma = arma nueva (esto); gamepad = nuevo Gamepad (Game.stage, false); gamepad.fire1.mapKey (KeyCode.SPACEBAR);  sobrescribir la función pública update (): void super.update (); body.angle + = gamepad.x * 0.1; physics.thrust (-gamepad.y); if (gamepad.fire1.isPressed) weapon.fire ();  función protegida onDied (entidad: Entidad): void destroy (); 

Hay algunos detalles de implementación aquí, pero lo más importante es que en el constructor creamos instancias y configuraciones. Cuerpo, Física, Salud, Ver y Arma componentes (Los Arma componente es, de hecho, una instancia de Pistola en lugar de la clase base de armas.)

Estoy usando las API de dibujo de gráficos Flash para crear mi nave (líneas 29-32), pero podríamos usar una imagen de mapa de bits con la misma facilidad. También estoy creando una instancia de mi clase de Gamepad: esta es una biblioteca de código abierto que escribí hace un par de años para facilitar la entrada del teclado en Flash.

También he anulado el actualizar Función de la clase base para agregar algún comportamiento personalizado: después de activar todo el comportamiento predeterminado con super.update () giramos y empujamos la nave según la entrada del teclado, y disparamos el arma si se presiona la tecla de disparo.

Escuchando el murió Señal del componente de salud, activamos el onDied funciona si el jugador se queda sin puntos de golpe. Cuando esto sucede solo le decimos a la nave que se destruya..


los Pistola Clase

A continuación vamos a encender eso Pistola clase:

 paquete asteroides import engine.Entity; motor de importación. / ** *… * @autor Iain Lobb - [email protected] * / clase pública Gun extiende Arma función pública Gun (entidad: Entidad) super (entidad);  anular la función pública fire (): void var bullet: Bullet = new Bullet (); bullet.targets = entity.targets; bullet.body.x = entity.body.x; bullet.body.y = entidad.body.y; bullet.body.angle = entity.body.angle; bullet.physics.thrust (10); entity.entityCreated.dispatch (bullet); super.fire (); 

¡Este es un bonito corto! Simplemente anulamos fuego() función para crear un nuevo Bala siempre que el jugador dispare. Después de hacer coincidir la posición y la rotación de la bala con la nave, y empujarla en la dirección correcta, enviamos entidad creada Para que se pueda añadir al juego..

Una gran cosa sobre esto Pistola clase es que es usado tanto por el jugador como por las naves enemigas.


los Bala Clase

UNA Pistola crea una instancia de esto Bala clase:

 paquete de asteroides import engine.Body; motor de importación.Entidad; motor de importación. Física; motor de importación.Ver; importar flash.display.Sprite; / ** *… * @autor Iain Lobb - [email protected] * / public class Bullet extiende Entidad public var age: int; función pública Bullet () body = new Body (this); body.radius = 5; física = nueva física (esto); view = new View (this); view.sprite = nuevo Sprite (); view.sprite.graphics.beginFill (0xFFFFFF); view.sprite.graphics.drawCircle (0, 0, body.radius);  sobrescribir la función pública update (): void super.update (); para cada (var target: Entidad en target) if (body.testCollision (target)) target.health.hit (1); destruir(); regreso;  edad ++; if (edad> 20) view.alpha - = 0.2; if (edad> 25) destruir (); 

El constructor crea una instancia y configura el cuerpo, la física y la vista. En la función de actualización, ahora puede ver la lista llamada objetivos es útil, ya que recorremos todas las cosas que queremos golpear y veremos si alguna de ellas se cruza con la bala.

Este sistema de colisión no escalaría a miles de balas, pero está bien para la mayoría de los juegos casuales.

Si la bala tiene más de 20 cuadros, comenzamos a desvanecerse, y si es más antigua que 25 cuadros, la destruimos. Como con el Pistola, la Bala es usado tanto por el jugador como por el enemigo - las instancias solo tienen una lista de objetivos diferente.

Hablando de que…


los Enemigo Clase

Ahora veamos esa nave enemiga:

 paquete de asteroides import engine.Body; motor de importación.Entidad; motor de importación.Salud; motor de importación. Física; motor de importación.Ver; importar flash.display.GraphicsPathWinding; importar flash.display.Sprite; / ** *… * @autor Iain Lobb - [email protected] * / public class EnemyShip extiende Entity protected var turnDirection: Number = 1; función pública EnemyShip () body = new Body (this); body.x = 750; body.y = 550; física = nueva física (esto); physics.drag = 0.9; view = new View (this); view.sprite = nuevo Sprite (); view.sprite.graphics.lineStyle (1.5, 0xFFFFFF); view.sprite.graphics.drawPath (Vector.([1, 2, 2, 2, 2]), Vector.([0, 10, 10, -10, 0, 0, -10, -10, 0, 10]), GraphicsPathWinding.NON_ZERO); salud = nueva salud (esto); health.hits = 5; health.died.add (onDied); arma = arma nueva (esto);  sobrescribir la función pública update (): void super.update (); si (Math.random () < 0.1) turnDirection = -turnDirection; body.angle += turnDirection * 0.1; physics.thrust(Math.random()); if (Math.random() < 0.05) weapon.fire();  protected function onDied(entity:Entity):void  destroy();   

Como puedes ver, es bastante similar a la clase de nave del jugador. La única diferencia real es que en el actualizar() función, en lugar de tener el control del jugador a través del teclado, tenemos alguna "estupidez artificial" para hacer que la nave vague y dispare al azar.


los Asteroide Clase

El otro tipo de entidad que el jugador puede disparar es el propio asteroide:

 paquete de asteroides import engine.Body; motor de importación.Entidad; motor de importación.Salud; motor de importación. Física; motor de importación.Ver; importar flash.display.Sprite; / ** *… * @autor Iain Lobb - [email protected] * / public class Asteroid extiende Entidad public function Asteroid () body = new Body (this); body.radius = 20; body.x = Math.random () * 800; body.y = Math.random () * 600; física = nueva física (esto); physics.velocityX = (Math.random () * 10) - 5; physics.velocityY = (Math.random () * 10) - 5; view = new View (this); view.sprite = nuevo Sprite (); view.sprite.graphics.lineStyle (1.5, 0xFFFFFF); view.sprite.graphics.drawCircle (0, 0, body.radius); salud = nueva salud (esto); health.hits = 3; health.hurt.add (onHurt);  sobrescribir la función pública update (): void super.update (); para cada (var target: Entidad en target) if (body.testCollision (target)) target.health.hit (1); destruir(); regreso;  función protegida onHurt (entidad: entidad): void body.radius * = 0.75; view.scale * = 0.75; si (body.radius) < 10)  destroy(); return;  var asteroid:Asteroid = new Asteroid(); asteroid.targets = targets; group.push(asteroid); asteroid.group = group; asteroid.body.x = body.x; asteroid.body.y = body.y; asteroid.body.radius = body.radius; asteroid.view.scale = view.scale; entityCreated.dispatch(asteroid);   

Esperemos que se estén acostumbrando a cómo se ven estas clases de entidades ahora.

En el constructor inicializamos nuestros componentes y aleatorizamos la posición y la velocidad..

En el actualizar() En esta función, verificamos las colisiones con nuestra lista de objetivos, que en este ejemplo solo tendrá un elemento: el barco del jugador. Si encontramos una colisión, dañamos al objetivo y luego destruimos el asteroide. Por otro lado, si el asteroide está dañado (es decir, es alcanzado por una bala de jugador), lo reducimos y creamos un segundo asteroide, creando la ilusión de que ha sido volado en dos partes. Sabemos cuándo hacer esto escuchando la señal de "daño" del componente de Salud.


los AsteroidsGame Clase

Finalmente, echemos un vistazo a la clase AsteroidsGame que controla todo el espectáculo:

 paquete asteroides import engine.Entity; motor de importación. Juego; import flash.events.MouseEvent; importar flash.filters.GlowFilter; importar flash.text.TextField; / ** *… * @autor Iain Lobb - [email protected] * / public class AsteroidsGame extiende el juego public var players: Vector. = nuevo vector.(); var enemigos públicos: vector. = nuevo vector.(); public var messageField: TextField; función pública AsteroidsGame ()  anula la función protegida startGame (): void var asteroid: Asteroid; para (var i: int = 0; i < 10; i++)  asteroid = new Asteroid(); asteroid.targets = players; asteroid.group = enemies; enemies.push(asteroid); addEntity(asteroid);  var ship:Ship = new Ship(); ship.targets = enemies; ship.destroyed.add(onPlayerDestroyed); players.push(ship); addEntity(ship); var enemyShip:EnemyShip = new EnemyShip(); enemyShip.targets = players; enemyShip.group = enemies; enemies.push(enemyShip); addEntity(enemyShip); filters = [new GlowFilter(0xFFFFFF, 0.8, 6, 6, 1)]; update(); render(); isPaused = true; if (messageField)  addChild(messageField);  else  createMessage();  stage.addEventListener(MouseEvent.MOUSE_DOWN, start);  protected function createMessage():void  messageField = new TextField(); messageField.selectable = false; messageField.textColor = 0xFFFFFF; messageField.width = 600; messageField.scaleX = 2; messageField.scaleY = 3; messageField.text = "CLICK TO START"; messageField.x = 400 - messageField.textWidth; messageField.y = 240; addChild(messageField);  protected function start(event:MouseEvent):void  stage.removeEventListener(MouseEvent.MOUSE_DOWN, start); isPaused = false; removeChild(messageField); stage.focus = stage;  protected function onPlayerDestroyed(entity:Entity):void  gameOver();  protected function gameOver():void  addChild(messageField); isPaused = true; stage.addEventListener(MouseEvent.MOUSE_DOWN, restart);  protected function restart(event:MouseEvent):void  stopGame(); startGame(); stage.removeEventListener(MouseEvent.MOUSE_DOWN, restart); isPaused = false; removeChild(messageField); stage.focus = stage;  override protected function stopGame():void  super.stopGame(); players.length = 0; enemies.length = 0;  override protected function update():void  super.update(); for each (var entity:Entity in entities)  if (entity.body.x > 850) entity.body.x - = 900; si (entity.body.x < -50) entity.body.x += 900; if (entity.body.y > 650) entity.body.y - = 700; si (entity.body.y < -50) entity.body.y += 700;  if (enemies.length == 0) gameOver();   

Esta clase es bastante larga (bueno, ¡más de 100 líneas!) Porque hace muchas cosas..

En empezar juego() crea y configura 10 asteroides, la nave y la nave enemiga, y también crea el mensaje "HAGA CLIC PARA INICIAR".

los comienzo() La función anula el juego y elimina el mensaje, mientras que juego terminado La función detiene el juego nuevamente y restaura el mensaje. los reiniciar() La función escucha un clic del mouse en la pantalla Game Over: cuando esto sucede, detiene el juego y lo inicia de nuevo..

los actualizar() La función recorre todos los enemigos y deforma a cualquiera que se haya salido de la pantalla, así como a verificar la condición de victoria, que es que no hay enemigos en la lista de enemigos..


Llevándolo más lejos

Este es un motor bastante básico y un juego simple, así que ahora pensemos en maneras en que podamos expandirlo..

  • Podríamos agregar un valor de prioridad para cada entidad y ordenar la lista antes de cada actualización, para que podamos asegurarnos de que algunos tipos de Entidad siempre se actualicen después de otros tipos.
  • Podríamos usar la agrupación de objetos para reutilizar objetos muertos (por ejemplo, viñetas) en lugar de crear solo cientos de nuevos..
  • Podríamos agregar un sistema de cámara para poder desplazar y hacer zoom en la escena. Podríamos extender los componentes de Cuerpo y Física para agregar soporte para Box2D u otro motor de física.
  • Podríamos crear un componente de inventario, para que las entidades puedan llevar artículos.

Además de ampliar los componentes individuales, en ocasiones es posible que tengamos que ampliar el IEntity Interfaz para crear tipos especiales de Entidad con componentes especializados..

Por ejemplo, si estamos creando un juego de plataformas y tenemos un nuevo componente que maneja todas las cosas muy específicas que necesita un personaje de juego de plataformas: ¿están en el suelo, están tocando una pared, cuánto tiempo han estado en el aire, ¿pueden hacer un salto doble, etc.? Otras entidades también pueden necesitar acceder a esta información. Pero no es parte de la API principal de la entidad, que se mantiene intencionalmente muy general. Por lo tanto, necesitamos definir una nueva interfaz, que proporciona acceso a todos los componentes de la entidad estándar, pero agrega acceso a la PlatformController componente.

Para esto, haríamos algo como:

 package platformgame import engine.IEntity; / ** *… * @autor Iain Lobb - [email protected] * / interfaz pública IPlatformEntity amplía IEntity function set platformController (value: PlatformController): void; función obtener platformController (): PlatformController; 

Cualquier entidad que necesite la funcionalidad de "plataforma" implementa esta interfaz, permitiendo que otras entidades interactúen con el PlatformController componente.


Conclusiones

Incluso si me atrevo a escribir sobre la arquitectura del juego, me temo que estoy despertando la opinión de los avispones, pero eso (en su mayoría) siempre es algo bueno, y espero que al menos te haya hecho pensar en cómo organizar tu código.

En última instancia, no creo que debas colgarte demasiado de cómo estructuras las cosas; Lo que mejor te funcione para hacer tu juego es la mejor estrategia. Sé que hay sistemas mucho más avanzados que el que describo aquí, que resuelven una gama de problemas más allá de los que he discutido, pero pueden comenzar a parecer muy poco familiares si está acostumbrado a una arquitectura tradicional basada en herencia..

Me gusta el enfoque que he sugerido aquí porque permite que el código se organice por propósito, en clases enfocadas pequeñas, a la vez que proporciona una interfaz extensible y de tipo estático y sin depender de las características dinámicas del lenguaje o Cuerda búsquedas Si desea alterar el comportamiento de un componente en particular, puede extender ese componente y anular los métodos que desee cambiar. Las clases tienden a ser muy cortas, por lo que nunca me encuentro recorriendo miles de líneas para encontrar el código que estoy buscando.

Lo mejor de todo es que puedo tener un solo motor que es lo suficientemente flexible como para usarlo en todos los juegos que hago, lo que me ahorra una gran cantidad de tiempo.