Advertisement
  1. Code
  2. Coding Fundamentals
  3. Game Development

Crear un juego simple de asteroides usando entidades basadas en componentes

Scroll to top
Read Time: 14 min

() translation by (you can also view the original English article)

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


Vista previa del resultado final


Este es el sencillo juego de asteroides que vamos a crear 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.


Descripción de la Clase

Hay seis clases:

  • AsteroidsGame, que extiende la clase de juego base y agrega la lógica específica a nuestro espacio dispararles.    
  • Ship, que es lo que controlas.
  • Asteroid, que es la cosa en la que disparas.
  • Bullet, que es lo que disparas.
  • Gun, que crea esas balas.
  • EnemyShip, que es un extranjero errante que está ahí para añadir un poco de variedad al juego.
  • Vamos a pasar por estos tipos de entidad, uno por uno.


    La clase Ship

    Comenzaremos con la nave del jugador:

1
package asteroids 
2
{
3
  import com.iainlobb.gamepad.Gamepad;
4
	import com.iainlobb.gamepad.KeyCode;
5
	import engine.Body;
6
	import engine.Entity;
7
	import engine.Game;
8
	import engine.Health;
9
	import engine.Physics;
10
	import engine.View;
11
	import flash.display.GraphicsPathWinding;
12
	import flash.display.Sprite;
13
	/**

14
	 * ...

15
	 * @author Iain Lobb - iainlobb@gmail.com

16
	 */
17
	public class Ship extends Entity
18
	{
19
		protected var gamepad:Gamepad;
20
		
21
		public function Ship() 
22
		{
23
			body = new Body(this);
24
			body.x = 400;
25
			body.y = 300;
26
			
27
			physics = new Physics(this);
28
			physics.drag = 0.9;
29
			
30
			view = new View(this);
31
			view.sprite = new Sprite();
32
			view.sprite.graphics.lineStyle(1.5, 0xFFFFFF);
33
			view.sprite.graphics.drawPath(Vector.<int>([1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]), 
34
			                              Vector.<Number>([ -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]),
35
			                              GraphicsPathWinding.NON_ZERO);
36
			
37
			health = new Health(this);
38
			health.hits = 5;
39
			health.died.add(onDied);
40
											
41
			weapon = new Gun(this);
42
											
43
			gamepad = new Gamepad(Game.stage, false);
44
			gamepad.fire1.mapKey(KeyCode.SPACEBAR);
45
		}
46
		
47
		override public function update():void 
48
		{
49
			super.update();
50
			
51
			body.angle += gamepad.x * 0.1;
52
			
53
			physics.thrust(-gamepad.y);
54
			
55
			if (gamepad.fire1.isPressed) weapon.fire();
56
		}
57
		
58
		protected function onDied(entity:Entity):void
59
		{
60
			destroy();
61
		}
62
	}
63
64
}

Hay un poco de detalles de implementación aquí, pero lo más importante es notar que en el constructor instanciamos y configuramos los componentes Body, Physics, Health, View y Weapon. (El componente Weapon es de hecho una instancia de Gun 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 utilizar con igual facilidad una imagen de mapa de bits. También estoy creando una instancia de mi clase Gamepad - esta es una biblioteca de código abierto que escribí hace un par de años para facilitar la entrada de teclado en Flash.

También he reemplazado la función update de la clase base para añadir un comportamiento personalizado: después de activar todo el comportamiento por defecto con super.update() rotaremos y empujaremos la nave en base a la entrada del teclado, y dispararemos el arma si la tecla de disparo es presionado.

Al escuchar la señal de muerte died del componente health, activamos la función onDied si el jugador se queda sin puntos de golpe. Cuando esto sucede, le decimos al barco que se destruya a sí mismo.


La clase Gun

A continuación vamos a usar la clase Gun:

1
package asteroids 
2
{
3
	import engine.Entity;
4
	import engine.Weapon;
5
	/**

6
	 * ...

7
	 * @author Iain Lobb - iainlobb@gmail.com

8
	 */
9
	public class Gun extends Weapon 
10
	{
11
		public function Gun(entity:Entity) 
12
		{
13
			super(entity);
14
		}
15
		
16
		override public function fire():void 
17
		{
18
			var bullet:Bullet = new Bullet();
19
			bullet.targets = entity.targets;
20
			bullet.body.x = entity.body.x;
21
			bullet.body.y = entity.body.y;
22
			bullet.body.angle = entity.body.angle;
23
			bullet.physics.thrust(10);
24
			entity.entityCreated.dispatch(bullet);
25
			
26
			super.fire();
27
		}
28
		
29
	}
30
31
}

¡Este es corto! Simplemente anulamos la función fire() para crear una nueva Bullet cada vez que el jugador se dispara. Después de hacer coincidir la posición y la rotación de la bala con el barco, y empujándolo en la dirección correcta, enviamos entityCreated para que pueda ser añadido al juego.

Una gran cosa acerca de esta clase Gun es que es utilizado por el jugador y las naves enemigas.


La clase Bullet

Un Gun crea una instancia de esta clase Bullet:

1
package asteroids 
2
{
3
	import engine.Body;
4
	import engine.Entity;
5
	import engine.Physics;
6
	import engine.View;
7
	import flash.display.Sprite;
8
	/**

9
	 * ...

10
	 * @author Iain Lobb - iainlobb@gmail.com

11
	 */
12
	public class Bullet extends Entity
13
	{
14
		public var age:int;
15
		
16
		public function Bullet() 
17
		{
18
			body = new Body(this);
19
			body.radius = 5;
20
			
21
			physics = new Physics(this);
22
			
23
			view = new View(this);
24
			view.sprite = new Sprite();
25
			view.sprite.graphics.beginFill(0xFFFFFF);
26
			view.sprite.graphics.drawCircle(0, 0, body.radius);
27
		}
28
		
29
		override public function update():void 
30
		{
31
			super.update();
32
			
33
			for each (var target:Entity in targets)
34
			{
35
				if (body.testCollision(target))
36
				{
37
					target.health.hit(1);
38
					destroy();
39
					return;
40
				}
41
			}
42
			
43
			age++;
44
			if (age > 20) view.alpha -= 0.2;
45
			if (age > 25) destroy();
46
		}
47
		
48
	}
49
50
}

El constructor instancia y configura el cuerpo, la física y la vista. En la función de actualización, ahora puede ver la lista de objetivos targets se vuelve util, ya que el bucle a través de todas las cosas que queremos golpear y ver si alguno de ellos están intersectando 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 marcos de edad, comenzamos a desaparecer, y si es más de 25 marcos lo destruimos. Al igual que con el arma Gun, la bala Bullet es utilizado por el jugador y el enemigo - las instancias sólo tienen una lista de objetivos diferentes.

Hablando de que...


La Clase EnemyShip

Ahora veamos esa nave enemiga:

1
package asteroids 
2
{
3
	import engine.Body;
4
	import engine.Entity;
5
	import engine.Health;
6
	import engine.Physics;
7
	import engine.View;
8
	import flash.display.GraphicsPathWinding;
9
	import flash.display.Sprite;
10
	/**

11
	 * ...

12
	 * @author Iain Lobb - iainlobb@gmail.com

13
	 */
14
	public class EnemyShip extends Entity
15
	{
16
		protected var turnDirection:Number = 1;
17
		
18
		public function EnemyShip() 
19
		{
20
			body = new Body(this);
21
			body.x = 750;
22
			body.y = 550;
23
			
24
			physics = new Physics(this);
25
			physics.drag = 0.9;
26
			
27
			view = new View(this);
28
			view.sprite = new Sprite();
29
			view.sprite.graphics.lineStyle(1.5, 0xFFFFFF);
30
			view.sprite.graphics.drawPath(Vector.<int>([1, 2, 2, 2, 2]), 
31
			                              Vector.<Number>([ 0, 10, 10, -10, 0, 0, -10, -10, 0, 10]),
32
			                              GraphicsPathWinding.NON_ZERO);
33
			
34
			health = new Health(this);
35
			health.hits = 5;
36
			health.died.add(onDied);
37
		
38
			weapon = new Gun(this);
39
		}
40
		
41
		override public function update():void 
42
		{
43
			super.update();
44
			
45
			if (Math.random() < 0.1) turnDirection = -turnDirection;
46
			
47
			body.angle += turnDirection * 0.1;
48
			
49
			physics.thrust(Math.random());
50
			
51
			if (Math.random() < 0.05) weapon.fire();
52
		}
53
		
54
		protected function onDied(entity:Entity):void
55
		{
56
			destroy();
57
		}
58
		
59
	}
60
61
}

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


La clase Asteroid

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

1
package asteroids 
2
{
3
	import engine.Body;
4
	import engine.Entity;
5
	import engine.Health;
6
	import engine.Physics;
7
	import engine.View;
8
	import flash.display.Sprite;
9
	/**

10
	 * ...

11
	 * @author Iain Lobb - iainlobb@gmail.com

12
	 */
13
	public class Asteroid extends Entity
14
	{
15
		public function Asteroid() 
16
		{
17
			body = new Body(this);
18
			body.radius = 20;
19
			body.x = Math.random() * 800;
20
			body.y = Math.random() * 600;
21
			
22
			physics = new Physics(this);
23
			physics.velocityX = (Math.random() * 10) - 5;
24
			physics.velocityY = (Math.random() * 10) - 5;
25
			
26
			view = new View(this);
27
			view.sprite = new Sprite();
28
			view.sprite.graphics.lineStyle(1.5, 0xFFFFFF);
29
			view.sprite.graphics.drawCircle(0, 0, body.radius);
30
			
31
			health = new Health(this);
32
			health.hits = 3;
33
			health.hurt.add(onHurt);
34
		}
35
		
36
		override public function update():void 
37
		{
38
			super.update();
39
			
40
			for each (var target:Entity in targets)
41
			{
42
				if (body.testCollision(target))
43
				{
44
					target.health.hit(1);
45
					destroy();
46
					return;
47
				}
48
			}
49
		}
50
		
51
		protected function onHurt(entity:Entity):void
52
		{
53
			body.radius *= 0.75;
54
			view.scale *= 0.75;
55
			
56
			if (body.radius < 10)
57
			{
58
				destroy();
59
				return;
60
			}
61
			
62
			var asteroid:Asteroid = new Asteroid();
63
			asteroid.targets = targets;
64
			group.push(asteroid);
65
			asteroid.group = group;
66
			asteroid.body.x = body.x;
67
			asteroid.body.y = body.y;
68
			asteroid.body.radius = body.radius;
69
			asteroid.view.scale = view.scale;
70
			entityCreated.dispatch(asteroid);
71
		}
72
		
73
	}
74
75
}

Esperemos que te acostumbras a cómo estas clases de entidad parecen ahora.

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

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


La clase AsteroidsGame

Por último, veamos la clase AsteroidsGame que controla todo el espectáculo:

1
package asteroids 
2
{
3
	import engine.Entity;
4
	import engine.Game;
5
	import flash.events.MouseEvent;
6
	import flash.filters.GlowFilter;
7
	import flash.text.TextField;
8
	/**

9
	 * ...

10
	 * @author Iain Lobb - iainlobb@gmail.com

11
	 */
12
	public class AsteroidsGame extends Game
13
	{
14
		public var players:Vector.<Entity> = new Vector.<Entity>();
15
		public var enemies:Vector.<Entity> = new Vector.<Entity>();
16
		public var messageField:TextField;
17
		
18
		public function AsteroidsGame() 
19
		{
20
			
21
		}
22
		
23
		override protected function startGame():void 
24
		{
25
			var asteroid:Asteroid;
26
			for (var i:int = 0; i < 10; i++)
27
			{
28
				asteroid = new Asteroid();
29
				asteroid.targets = players;
30
				asteroid.group = enemies;
31
				enemies.push(asteroid);
32
				addEntity(asteroid);
33
			}
34
			
35
			var ship:Ship = new Ship();
36
			ship.targets = enemies;
37
			ship.destroyed.add(onPlayerDestroyed);
38
			players.push(ship);
39
			addEntity(ship);
40
			
41
			var enemyShip:EnemyShip = new EnemyShip();
42
			enemyShip.targets = players;
43
			enemyShip.group = enemies;
44
			enemies.push(enemyShip);
45
			addEntity(enemyShip);
46
			
47
			filters = [new GlowFilter(0xFFFFFF, 0.8, 6, 6, 1)];
48
			
49
			update();
50
			
51
			render();
52
			
53
			isPaused = true;
54
			
55
			if (messageField)
56
			{
57
				addChild(messageField);
58
			}
59
			else
60
			{
61
				createMessage();
62
			}
63
			
64
			stage.addEventListener(MouseEvent.MOUSE_DOWN, start);
65
		}
66
		
67
		protected function createMessage():void
68
		{
69
			messageField = new TextField();
70
			messageField.selectable = false;
71
			messageField.textColor = 0xFFFFFF;
72
			messageField.width = 600;
73
			messageField.scaleX = 2;
74
			messageField.scaleY = 3;
75
			messageField.text = "CLICK TO START";
76
			messageField.x = 400 - messageField.textWidth;
77
			messageField.y = 240;
78
			addChild(messageField);
79
		}
80
		
81
		protected function start(event:MouseEvent):void
82
		{
83
			stage.removeEventListener(MouseEvent.MOUSE_DOWN, start);
84
			isPaused = false;
85
			removeChild(messageField);
86
			stage.focus = stage;
87
		}
88
		
89
		protected function onPlayerDestroyed(entity:Entity):void
90
		{
91
			gameOver();
92
		}
93
		
94
		protected function gameOver():void
95
		{
96
			addChild(messageField);
97
			isPaused = true;
98
			stage.addEventListener(MouseEvent.MOUSE_DOWN, restart);
99
		}
100
		
101
		protected function restart(event:MouseEvent):void
102
		{
103
			stopGame();
104
			startGame();
105
			
106
			stage.removeEventListener(MouseEvent.MOUSE_DOWN, restart);
107
			isPaused = false;
108
			removeChild(messageField);
109
			stage.focus = stage;
110
		}
111
		
112
		override protected function stopGame():void 
113
		{
114
			super.stopGame();
115
			
116
			players.length = 0;
117
			enemies.length = 0;
118
		}
119
		
120
		override protected function update():void 
121
		{
122
			super.update();
123
			
124
			for each (var entity:Entity in entities)
125
			{
126
				if (entity.body.x > 850) entity.body.x -= 900;
127
				if (entity.body.x < -50) entity.body.x += 900;
128
			
129
				if (entity.body.y > 650) entity.body.y -= 700;
130
				if (entity.body.y < -50) entity.body.y += 700;
131
			}
132
			
133
			if (enemies.length == 0) gameOver();
134
		}
135
		
136
	}
137
}

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

En startGame() crea y configura 10 asteroides, la nave y la nave enemiga, y también crea el mensaje "CLICK TO START".

La función start() reanuda el juego y elimina el mensaje, mientras que la función gameOver hace una pausa en el juego y restaura el mensaje. La función restart() escucha un clic del ratón en la pantalla Game Over - cuando esto sucede, detiene el juego y lo vuelve a iniciar.

La función update() recorre a través de todos los enemigos y distorsiona cualquiera que haya quedado fuera de la pantalla, así como la comprobación de la condición de victoria, que es que no hay enemigos en la lista de enemigos.


Tomándolo más lejos

Este es un motor de huesos muy simple y un juego simple, así que ahora vamos a pensar en maneras de expandirlo.

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

Además de ampliar los componentes individuales, es posible que a veces necesitamos ampliar la interfaz IEntity para crear tipos especiales de Entity con componentes especializados.

Por ejemplo, si estamos haciendo un juego de plataforma, y ​​tenemos un nuevo componente que maneja todas las cosas muy específicas que un personaje de juego de plataforma necesita - están en el suelo, están tocando una pared, cuánto tiempo han sido En el aire, pueden saltar de doble salto, etc. - otras entidades también podrían tener acceso a esta información. Pero no es parte de la API Entity principal, que se mantiene intencionalmente muy general. Así que tenemos que definir una nueva interfaz, que proporciona acceso a todos los componentes de entidad estándar, pero añade acceso al componente PlatformController.

Para esto, haríamos algo como:

1
package platformgame 
2
{
3
	import engine.IEntity;
4
	
5
	/**

6
	 * ...

7
	 * @author Iain Lobb - iainlobb@gmail.com

8
	 */
9
	public interface IPlatformEntity extends IEntity
10
	{
11
		function set platformController(value:PlatformController):void;
12
		function get platformController():PlatformController;
13
	}
14
}

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


Conclusiones

Incluso atreviéndome a escribir sobre la arquitectura del juego, temo que estoy moviendo un nido de opinión de avispones - pero eso es (en su mayoría) siempre una buena cosa, y espero que por lo menos te he hecho pensar en cómo organizar tu código.

En última instancia, no creo que usted debe ponerse demasiado colgado sobre cómo estructurar las cosas; Lo que funciona para que usted haga su juego es la mejor estrategia. Sé que hay sistemas mucho más avanzados que el que aquí delineo, que resuelven una gama de problemas más allá de los que he discutido, pero pueden tender a comenzar a parecer muy desconocido si estás acostumbrado a una arquitectura basada en herencia tradicional.

Me gusta el acercamiento que he sugerido aquí porque permite que el código sea organizado por el propósito, en pequeñas clases enfocadas, mientras que proporciona una interfaz extensible y sin el confiar en características dinámicas del lenguaje o las búsquedas de String. Si desea modificar el comportamiento de un componente en particular, puede ampliar ese componente y anular los métodos que desee cambiar. Las clases tienden a permanecer muy cortas, así que nunca me encuentro desplazándome por miles de líneas para encontrar el código que busco.

Lo mejor de todo, soy capaz de tener un solo motor que es lo suficientemente flexible como para usar en todos los juegos que hago, ahorrándome una enorme cantidad de tiempo.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.