Construye un Shoot-'Em-Up en Stage3D: Terreno, IA del enemigo y datos del nivel
Spanish (Español) translation by Esther (you can also view the original English article)
Estamos creando un juego de disparos en 2D de alto rendimiento utilizando el nuevo motor de renderizado Stage3D acelerado por hardware de Flash. En esta parte de la serie añadimos nuevos modos de movimiento de los enemigos, enemigos que devuelven los disparos y niveles hechos a mano con terreno de fondo.
También disponible en esta serie:
- Construye un Shoot-'Em-Up de Stage3D: Prueba de Sprite
- Construye un Shoot-'Em-Up de Stage3D: Interacción
- Construye un Shoot-'Em-Up en Stage3D: Explosiones, paralaje y colisiones
- Construye un Shoot-'Em-Up en Stage3D: Terreno, IA del enemigo y datos del nivel
- Construye un Shoot-'Em-Up en Stage3D: Puntuación, salud, vidas, HUD y transiciones
- Construye un Shoot-'Em-Up en Stage3D: Batallas de jefes a pantalla completa y pulido
Avance del resultado final
Echemos un vistazo al resultado final en el que trabajaremos: una demo de shoot-em-up acelerada por hardware que incluye todo lo de las partes uno a tres de esta serie, además de nuevos modos de movimiento de los enemigos, enemigos que devuelven los disparos y niveles hechos a mano que incluyen terreno de fondo.
Introducción: ¡Bienvenido al nivel cuatro!
Sigamos haciendo un shooter de desplazamiento lateral inspirado en títulos arcade retro como R-Type o Gradius en AS3.
En la primera parte de esta serie, implementamos un motor de sprites 2D básico que consigue un gran rendimiento mediante el uso del renderizado por hardware Stage3D y como varias optimizaciones.
En la primera parte, implementamos una pantalla de título, el menú principal, el sonido y la música, y un sistema de entrada para que el jugador pueda controlar su nave espacial con el teclado.
Y en la tercera parte, añadimos todo el atractivo: un sistema de partículas completo con chispas, escombros voladores, ondas de choque, estelas de fuego de motores y toneladas de explosiones.
En esta parte, vamos a actualizar varios componentes principales de nuestro motor de juego. En primer lugar, vamos a añadir I.A. (inteligencia artificial) a nuestros enemigos creando varios comportamientos y estilos de movimiento diferentes. Por fin van a empezar a devolver los disparos, y ya no se moverán simplemente en línea recta. Algunos ni siquiera se moverán, sino que apuntarán al jugador: perfecto para los cañones centinela.
En segundo lugar, vamos a implementar un mecanismo de análisis de datos de nivel que te permitirá diseñar vastos mundos de juego utilizando un editor de niveles.
En tercer lugar, vamos a crear una nueva hoja de sprites y un lote de renderizado para un conjunto no interactivo de sprites de terreno que se utilizarán como parte del fondo. De esta manera, volaremos sobre estaciones espaciales y asteroides detallados en lugar de solo espacio vacío.
Paso 1: Abre tu proyecto existente
Vamos a basarnos en el código fuente escrito en los tutoriales anteriores, gran parte del cual no cambiará. Si aún no lo tienes, asegúrate de descargar el código fuente del tutorial anterior. Abre el archivo del proyecto en FlashDevelop (información aquí) y prepárate para actualizar tu juego. Este código fuente funcionará en cualquier otro compilador AS3, desde CS6 hasta Flash Builder.
Paso 2: Actualizar la clase de entidad
Primero vamos a implementar un nuevo movimiento AI a nuestra clase de entidad. Para esta funcionalidad vamos a requerir algunos datos de estado más para ser rastreados para cada entidad. En particular, necesitaremos información sobre la trayectoria, y varios temporizadores que nos permitan saber con qué frecuencia un enemigo debe disparar al jugador. Abre el archivo Entity.as existente de la última vez y agrega las siguientes nuevas variables de clase como sigue. Para evitar confusiones, toda la sección superior del archivo se incluye aquí, pero solo las variables de IA en la parte superior son nuevas.
1 |
|
2 |
// Stage3D Shoot-em-up Tutorial Part 4
|
3 |
// by Christer Kaitila - www.mcfunkypants.com
|
4 |
|
5 |
// Entity.as
|
6 |
// The Entity class will eventually hold all game-specific entity stats
|
7 |
// for the spaceships, bullets and effects in our game.
|
8 |
// It stores a reference to a gpu sprite and a few demo properties.
|
9 |
// This is where you would add hit points, weapons, ability scores, etc.
|
10 |
// This class handles any AI (artificial intelligence) for enemies as well.
|
11 |
|
12 |
package
|
13 |
{
|
14 |
import flash.geom.Point; |
15 |
import flash.geom.Rectangle; |
16 |
|
17 |
public class Entity |
18 |
{
|
19 |
// AI VARIABLES BEGIN
|
20 |
// if this is set, custom behaviors are run
|
21 |
public var aiFunction : Function; |
22 |
// the AI routines might want access to the entity manager
|
23 |
public var gfx:EntityManager; |
24 |
// AI needs to have access to time passing (in seconds)
|
25 |
public var age:Number = 0; |
26 |
public var fireTime:Number = 0; |
27 |
public var fireDelayMin:Number = 1; |
28 |
public var fireDelayMax:Number = 6; |
29 |
// an array of points defining a movement path for the AI
|
30 |
public var aiPathWaypoints:Array; |
31 |
// how fast we travel from one spline node to the next in seconds
|
32 |
public var pathNodeTime:Number = 1; |
33 |
// these offsets are added to the sprite location
|
34 |
// so that ships move around but eventually scroll offscreen
|
35 |
public var aiPathOffsetX:Number = 0; |
36 |
public var aiPathOffsetY:Number = 0; |
37 |
// how much big the path is (max)
|
38 |
public var aiPathSize:Number = 128; |
39 |
// how many different nodes in the path
|
40 |
public var aiPathWaypointCount:int = 8; |
41 |
// AI VARIABLES END
|
42 |
|
43 |
private var _speedX : Number; |
44 |
private var _speedY : Number; |
45 |
private var _sprite : LiteSprite; |
46 |
public var active : Boolean = true; |
47 |
|
48 |
// collision detection
|
49 |
public var isBullet:Boolean = false; // only these check collisions |
50 |
public var leavesTrail:Boolean = false; // creates particles as it moves |
51 |
public var collidemode:uint = 0; // 0=none, 1=sphere, 2=box, etc. |
52 |
public var collideradius:uint = 32; // used for sphere collision |
53 |
// box collision is not implemented (yet)
|
54 |
public var collidebox:Rectangle = new Rectangle(-16, -16, 32, 32); |
55 |
public var collidepoints:uint = 25; // score earned if destroyed |
56 |
public var touching:Entity; // what entity just hit us? |
57 |
|
58 |
public var owner:Entity; // so your own bullets don't hit you |
59 |
public var orbiting:Entity; // entities can orbit (circle) others |
60 |
public var orbitingDistance:Number; // how far in px from the orbit center |
61 |
|
62 |
// used for particle animation (in units per second)
|
63 |
public var fadeAnim:Number = 0; |
64 |
public var zoomAnim:Number = 0; |
65 |
public var rotationSpeed:Number = 0; |
66 |
|
67 |
// used to mark whether or not this entity was
|
68 |
// freshly created or reused from an inactive one
|
69 |
public var recycled:Boolean = false; |
Paso 3: No despedir inmediatamente
Vamos a llevar la cuenta del tiempo transcurrido desde que un enemigo disparó al jugador para que los malos no disparen demasiado a menudo. También necesitamos que los enemigos recuerden que no deben disparar instantáneamente justo cuando aparecen por primera vez, así que tenemos que añadir una pequeña cantidad de tiempo aleatorio antes de que empiecen a disparar.
Continuando con Entity.as, actualiza la función del constructor de la clase de la siguiente manera, y simplemente toma nota de las funciones getter y setter sin cambios, así como el código de detección de colisiones idéntico de la última vez.
1 |
|
2 |
public function Entity(gs:LiteSprite, myManager:EntityManager) |
3 |
{
|
4 |
_sprite = gs; |
5 |
_speedX = 0.0; |
6 |
_speedY = 0.0; |
7 |
// we need a reference to the entity manager
|
8 |
gfx = myManager; |
9 |
// we don't want everyone shooting on the first frame
|
10 |
fireTime = (gfx.fastRandom() * (fireDelayMax - fireDelayMin)) + fireDelayMin; |
11 |
}
|
12 |
|
13 |
public function die() : void |
14 |
{
|
15 |
// allow this entity to be reused by the entitymanager
|
16 |
active = false; |
17 |
// skip all drawing and updating
|
18 |
sprite.visible = false; |
19 |
// reset some things that might affect future reuses:
|
20 |
leavesTrail = false; |
21 |
isBullet = false; |
22 |
touching = null; |
23 |
owner = null; |
24 |
age = 0; |
25 |
collidemode = 0; |
26 |
}
|
27 |
|
28 |
public function get speedX() : Number |
29 |
{
|
30 |
return _speedX; |
31 |
}
|
32 |
public function set speedX(sx:Number) : void |
33 |
{
|
34 |
_speedX = sx; |
35 |
}
|
36 |
public function get speedY() : Number |
37 |
{
|
38 |
return _speedY; |
39 |
}
|
40 |
public function set speedY(sy:Number) : void |
41 |
{
|
42 |
_speedY = sy; |
43 |
}
|
44 |
public function get sprite():LiteSprite |
45 |
{
|
46 |
return _sprite; |
47 |
}
|
48 |
public function set sprite(gs:LiteSprite):void |
49 |
{
|
50 |
_sprite = gs; |
51 |
}
|
52 |
|
53 |
// used for collision callback performed in GameActorpool
|
54 |
public function colliding(checkme:Entity):Entity |
55 |
{
|
56 |
if (collidemode == 1) // sphere |
57 |
{
|
58 |
if (isCollidingSphere(checkme)) |
59 |
return checkme; |
60 |
}
|
61 |
return null; |
62 |
}
|
63 |
|
64 |
// simple sphere to sphere collision
|
65 |
public function isCollidingSphere(checkme:Entity):Boolean |
66 |
{
|
67 |
// never collide with yourself
|
68 |
if (this == checkme) return false; |
69 |
// only check if these shapes are collidable
|
70 |
if (!collidemode || !checkme.collidemode) return false; |
71 |
// don't check your own bullets
|
72 |
if (checkme.owner == this) return false; |
73 |
// don't check things on the same "team"
|
74 |
if (checkme.owner == owner) return false; |
75 |
// don't check if no radius
|
76 |
if (collideradius == 0 || checkme.collideradius == 0) return false; |
77 |
|
78 |
// this is the simpler way to do it, but it runs really slow
|
79 |
// var dist:Number = Point.distance(sprite.position, checkme.sprite.position);
|
80 |
// if (dist <= (collideradius+checkme.collideradius))
|
81 |
|
82 |
// this looks wierd but is 6x faster than the above
|
83 |
// see: http://www.emanueleferonato.com/2010/10/13/as3-geom-point-vs-trigonometry/
|
84 |
if (((sprite.position.x - checkme.sprite.position.x) * |
85 |
(sprite.position.x - checkme.sprite.position.x) + |
86 |
(sprite.position.y - checkme.sprite.position.y) * |
87 |
(sprite.position.y - checkme.sprite.position.y)) |
88 |
<=
|
89 |
(collideradius+checkme.collideradius)*(collideradius+checkme.collideradius)) |
90 |
{
|
91 |
touching = checkme; // remember who hit us |
92 |
return true; |
93 |
}
|
94 |
|
95 |
// default: too far away
|
96 |
// trace("No collision. Dist = "+dist);
|
97 |
return false; |
98 |
|
99 |
}
|
Paso 4: Movimiento de la curva Spline
Uno de los nuevos modos de IA va a ser una trayectoria de movimiento curvilínea y aleatoria. Un gran algoritmo utilizado en muchos juegos es la clásica curva spline de Catmull-Rom, que toma un conjunto de puntos e interpola una trayectoria suave entre todos ellos. Este camino hará un bucle en los extremos si es necesario.
La primera rutina nueva que necesitamos para nuestra clase de entidad es la función de cálculo del spline. Toma tres puntos y un número de "porcentaje" t que debe ir de cero a uno en el tiempo. A medida que t se acerca a 1 el punto devuelto estará en el final del segmento de la curva, y a la inversa, si es cero el punto devuelto es la posición inicial de la spline.
Para nuestra actual demostración del juego, solo vamos a generar un montón de puntos aleatorios para cada nueva entidad que utilice este tipo de momento, pero podrías añadir fácilmente tus propias rutas predefinidas para todo tipo de patrones de movimiento de enemigos interesantes, desde una figura de ocho hasta un simple patrón en zig-zag.
Puedes leer más sobre las curvas splione de Catmull-Rom en AS3 consultando esta demo y este tutorial.
1 |
|
2 |
// Calculates 2D cubic Catmull-Rom spline.
|
3 |
// See http://www.mvps.org/directx/articles/catmull/
|
4 |
public function spline (p0:Point, p1:Point, p2:Point, p3:Point, t:Number):Point |
5 |
{
|
6 |
return new Point ( |
7 |
0.5 * ((2 * p1.x) + |
8 |
t * (( -p0.x + p2.x) + |
9 |
t * ((2 * p0.x -5 * p1.x +4 * p2.x -p3.x) + |
10 |
t * ( -p0.x +3 * p1.x -3 * p2.x +p3.x)))), |
11 |
0.5 * ((2 * p1.y) + |
12 |
t * (( -p0.y + p2.y) + |
13 |
t * ((2 * p0.y -5 * p1.y +4 * p2.y -p3.y) + |
14 |
t * ( -p0.y +3 * p1.y -3 * p2.y +p3.y)))) |
15 |
); |
16 |
}
|
17 |
|
18 |
// generate a random path
|
19 |
public function generatePath():void |
20 |
{
|
21 |
trace("Generating AI path"); |
22 |
aiPathWaypoints = []; |
23 |
var N:int = aiPathWaypointCount; |
24 |
for (var i:int = 0; i < N; i++) |
25 |
{
|
26 |
aiPathWaypoints.push (new Point (aiPathSize * Math.random (), aiPathSize * Math.random ())); |
27 |
}
|
28 |
}
|
29 |
|
30 |
// find the point on a spline at ratio (0 to 1)
|
31 |
public function calculatePathPosition(ratio:Number = 0):Point |
32 |
{
|
33 |
var i:int = int(ratio); |
34 |
var pointratio:Number = ratio - i; |
35 |
//trace(ratio + ' ratio = path point ' + i + ' segment ratio ' + pointratio);
|
36 |
var p0:Point = aiPathWaypoints [(i -1 + aiPathWaypoints.length) % aiPathWaypoints.length]; |
37 |
var p1:Point = aiPathWaypoints [i % aiPathWaypoints.length]; |
38 |
var p2:Point = aiPathWaypoints [(i +1 + aiPathWaypoints.length) % aiPathWaypoints.length]; |
39 |
var p3:Point = aiPathWaypoints [(i +2 + aiPathWaypoints.length) % aiPathWaypoints.length]; |
40 |
// figure out current position
|
41 |
var q:Point = spline (p0, p1, p2, p3, pointratio); |
42 |
return q; |
43 |
}
|
Paso 5: Decide cuándo disparar
Cada tipo de enemigo debe disparar al jugador (excepto los asteroides que no disparan, que simplemente girarán y flotarán en el espacio). Por ahora, simplemente vamos a llevar la cuenta del tiempo transcurrido desde que disparamos por última vez nuestra arma y añadiremos un rango aleatorio de un par de segundos entre disparos.
1 |
|
2 |
// we could optionally implement many different
|
3 |
// versions of this routine with different randomness
|
4 |
public function maybeShoot(bulletNum:int = 1, |
5 |
delayMin:Number = NaN, |
6 |
delayMax:Number = NaN):void |
7 |
{
|
8 |
// is it time to shoot a bullet?
|
9 |
if (fireTime < age) |
10 |
{
|
11 |
// if delay parameters were not set, use class defaults
|
12 |
if (isNaN(delayMin)) delayMin = fireDelayMin; |
13 |
if (isNaN(delayMax)) delayMax = fireDelayMax; |
14 |
|
15 |
// shoot one from the current location
|
16 |
gfx.shootBullet(bulletNum, this); |
17 |
// randly choose the next time to shoot
|
18 |
fireTime = age + (gfx.fastRandom() * (delayMax - delayMin)) + delayMin; |
19 |
}
|
20 |
}
|
Podrías añadir más inteligencia a esta rutina disparando solo cuando el jugador esté a cierta distancia, o disparando solo una vez y autodestruyéndose si tu juego requiere algún tipo de efecto "bomba de relojería".
Paso 6: IA #1: Moverse en línea recta
La primera función del "cerebro" de la IA que vamos a implementar es la más sencilla: el movimiento directo a lo largo de una línea como se vio en el tutorial de la semana pasada. Todo lo que hacemos aquí es apuntar en la dirección correntina y movernos en la trayectoria que se nos asignó aleatoriamente cuando fuimos engendrados por primera vez. No hay nada que hacer.
1 |
|
2 |
// moves forward and points at current destination based on speed
|
3 |
public function straightAI(seconds:Number):void |
4 |
{
|
5 |
age += seconds; |
6 |
maybeShoot(1); |
7 |
sprite.rotation = gfx.pointAtRad(speedX, speedY) |
8 |
- (90*gfx.DEGREES_TO_RADIANS); |
9 |
}
|
Paso 7: IA #2: Bamboleo sinusoidal
Uno de los patrones de movimiento más comunes en cualquier shoot-em-up es un movimiento de "onda" sinusoidal. Vamos a utilizar una onda sinusoidal que se tambalea hacia arriba y hacia abajo a lo largo del tiempo mientras el enemigo se mueve en una línea casi recta hacia el jugador. Este patrón se ve realmente bien y añade un movimiento agradable a tus enemigos sin demasiado caos, lo que hace que este tipo de enemigos sean fáciles de apuntar y destruir.
1 |
|
2 |
// a very simple up/down wobble movement
|
3 |
public function wobbleAI(seconds:Number):void |
4 |
{
|
5 |
age += seconds; |
6 |
maybeShoot(1); |
7 |
aiPathOffsetY = (Math.sin(age*2) / Math.PI) * 128; |
8 |
}
|
Paso 8: IA #3: Armas centinelas
Otro estilo muy práctico y común de IA enemiga que utilizan la mayoría de los shoot-em-ups es un "cañón centinela" o torreta inmóvil. Este tipo de enemigo se queda sentado y apunta al jugador. Puede ser aterrador para los jugadores ver los cañones centinela siguiendo cada uno de sus movimientos y seguramente provocará algunas maniobras evasivas.
1 |
|
2 |
// simply point at the player: good for sentry guns
|
3 |
public function sentryAI(seconds:Number):void |
4 |
{
|
5 |
age += seconds; |
6 |
maybeShoot(3,3,6); |
7 |
if (gfx.thePlayer) |
8 |
sprite.rotation = gfx.pointAtRad( |
9 |
gfx.thePlayer.sprite.position.x - sprite.position.x, |
10 |
gfx.thePlayer.sprite.position.y - sprite.position.y) |
11 |
- (90*gfx.DEGREES_TO_RADIANS); |
12 |
}
|
Como necesitamos acceder a la posición de la entidad del jugador, hacemos una comprobación rápida para asegurarnos de que existe, ya que las funciones de la IA podrían ejecutarse durante el menú principal del "modo de atracción" antes de que haya un jugador al que apuntar.
Paso 9: IA #4: Trayectorias de Spline
El último estilo de movimiento de la IA va a utilizar las rutinas de interpolación de curvas spline de Catmull-Rom que programamos anteriormente. Los enemigos de este tipo se tambalearán y darán vueltas de una manera caótica y frustrante. Lo mejor es incluir solo unos pocos enemigos de este tipo en tu juego, a menos que hayas mejorado la función generatePath para utilizar una matriz de puntos introducida manualmente que no sea tan aleatoria.
Para que las cosas se vean más bonitas, una vez que averiguamos las nuevas coordenadas de nuestro enemigo, orientamos el sprite para que mire en la dirección en la que se está moviendo, de modo que gire mientras hace un bucle.
1 |
|
2 |
// move around on a random spline path
|
3 |
// in future versions, you could upgrade this function
|
4 |
// to (instead of random) follow a predefined array of points
|
5 |
// that were designed by hand in code (or even in the level editor!)
|
6 |
public function droneAI(seconds:Number):void |
7 |
{
|
8 |
//trace('droneAI');
|
9 |
|
10 |
age += seconds; |
11 |
maybeShoot(1); |
12 |
|
13 |
// movement style inspired by Galaga, R-Type, Centipede
|
14 |
// performed by easing through a catmull-rom spline curve
|
15 |
// defined by an array of points
|
16 |
if (aiPathWaypoints == null) |
17 |
generatePath(); |
18 |
|
19 |
// how many spline nodes have we passed? (loops around to beginning)
|
20 |
var pathProgress:Number = age / pathNodeTime; |
21 |
|
22 |
var newPos:Point = calculatePathPosition(pathProgress); |
23 |
|
24 |
// point in the correct direction
|
25 |
sprite.rotation = gfx.pointAtRad(newPos.x-aiPathOffsetX,newPos.y-aiPathOffsetY) |
26 |
- (90*gfx.DEGREES_TO_RADIANS); |
27 |
|
28 |
// change path offset location
|
29 |
// this is added to the sprite scrolling location
|
30 |
// so that ships eventually move offscreen
|
31 |
// sprite.position.x = newPos.x;
|
32 |
// sprite.position.y = newPos.y;
|
33 |
|
34 |
aiPathOffsetX = newPos.x; |
35 |
aiPathOffsetY = newPos.x; |
36 |
|
37 |
}
|
38 |
|
39 |
} // end class |
40 |
} // end package |
Eso es todo para nuestra clase de entidad recién actualizada. Hemos añadido un nuevo código de movimiento y, por fin, ¡nuestras vastas flotas de naves enemigas son capaces de disparar al jugador!
Paso 10: La hoja de sprites del terreno
Antes de continuar, vamos a crear una nueva hoja de sprites para utilizarla como bloques de construcción para nuestros gráficos del terreno. De nuevo vamos a utilizar el maravilloso, legal y gratuito arte "Tyrian" de Daniel Cook (disponible en Lostgarden.com). Como siempre, recuerda usar una imagen cuadrada que tenga una potencia de dos en dimensiones (128x128, 256x256, 512x512, etc) cuando crees tu spritesheet. Esta es la hoja de sprites que he creado para esta demostración:



Paso 11: ¡Hora del editor de niveles!
Por fin ha llegado el momento de implementar una forma de analizar los datos generados por un editor de niveles adecuado. En lugar de una oleada aleatoria e interminable de enemigos, queremos ser capaces de crear una experiencia de juego interesante. Para ello, vamos a crear una clase simple que analiza la salida de datos de un popular editor de nivel de código abierto llamado OGMO. Puedes leer todo sobre OGMO aquí.
No tienes que usar OGMO: podrías modificar estas rutinas para analizar un XML como salida de "Tiled" o "DAME", o un archivo .CSV como salida de Excel, o incluso un .GIF o .PNG como salida de Photoshop (dibujando píxeles individuales y generando diferentes tipos de enemigos dependiendo del color de cada píxel).
La rutina de análisis de cualquier tipo de datos de nivel es trivial en comparación con nuestra funcionalidad en el juego. Por su simplicidad y pequeño tamaño de descarga, el CSV (valores separados por comas) es una gran alternativa al hinchado y complejo XML. Y lo que es más importante, la naturaleza lineal de nuestro juego de disparos requiere una larga cadena de datos a la que sea conveniente acceder columna por columna y fila por fila, en lugar de una "sopa" de entidades XML que podrían estar en cualquier orden. El formato CSV hace que nuestros datos fluyan de izquierda a derecha, al igual que nuestro juego. Como OGMO puede guardar los datos en este formato, encaja perfectamente.
Descarga el instalador y, opcionalmente, el código fuente. Una vez que lo hayas instalado, crea dos nuevos proyectos: uno para el terreno y otro para los sprites enemigos. Asegúrate de que tus datos de nivel se guarden en el formato compacto y simplista "CSV recortado".



Asegúrate de guardar en formato CSV
Para el proyecto del terreno, tenemos que crear una capa para el terreno que utiliza nuestra hoja de sprites recién photoshopeada arriba.



Ahora, simplemente dibuja el mapa como quieras. Puedes hacer clic en cada uno de los sprites de tu hoja de sprites en la paleta de azulejos y dibujar o rellenar tu nivel como mejor te parezca. Haz clic con el botón derecho del ratón en el nivel para borrar esa baldosa, y mantén pulsada la barra espaciadora mientras haces clic y arrastras para desplazarte.



Ahora haz lo mismo para tus sprites enemigos. Crea una capa que utilice la hoja de sprites que hicimos en los tutoriales anteriores de la siguiente manera:



Por último, llena tu nivel con todo tipo de escuadrones interesantes de naves enemigas según te parezca. Para la demo actual, empecé con solo unos pocos enemigos y literalmente llené cada espacio con asteroides hacia el final del nivel.



Los archivos fuente de OMGO para los niveles utilizados en la demo del juego están incluidos en la carpeta /assets/ del archivo zip del código fuente.
Paso 10: Incrustar los datos de nivel
Simplemente vamos a incrustar los niveles directamente en nuestro SWF para que siga siendo un juego independiente que no requiera la descarga de ningún archivo externo. Puedes tener el editor de niveles abierto mientras programas tu juego. Cada vez que modifiques tu nivel, simplemente guárdalo y haz clic en el botón RUN de FlashDevelop para ver los cambios en acción. Te darás cuenta de la nueva marca de tiempo en tu archivo de nivel y recompilar el SWF en consecuencia.
Dependiendo del nivel solicitado, rellenamos una matriz bidimensional de valores enteros basada en los datos de nivel emitidos por el editor. Durante el juego, nuestro gestor de entidades va a generar periódicamente otra columna de terreno o sprites enemigos basándose en estos datos. Crea un nuevo archivo en tu proyecto de código llamado GameLevels.as e incrusta los datos del nivel de la siguiente manera.
1 |
|
2 |
// Stage3D Shoot-em-up Tutorial Part 4
|
3 |
// by Christer Kaitila - www.mcfunkypants.com
|
4 |
|
5 |
// GameLevels.as
|
6 |
// This class parses .CSV level data strings
|
7 |
// that define the locations of tiles from a spritesheet
|
8 |
// Example levels were created using the OGMO editor,
|
9 |
// but could be designed by hand or any number of other
|
10 |
// freeware game level editors that can output .csv
|
11 |
// This can be a .txt, .csv, .oel, .etc file
|
12 |
// - we will strip all xml/html tags (if any)
|
13 |
// - we only care about raw csv data
|
14 |
// Our game can access the current level with:
|
15 |
// spriteId = myLevel.data[x][y];
|
16 |
|
17 |
package
|
18 |
{
|
19 |
import flash.display3D.Context3DProgramType; |
20 |
public class GameLevels |
21 |
{
|
22 |
// the "demo" level seen during the title screen
|
23 |
[Embed(source = '../assets/level0.oel', mimeType = 'application/octet-stream')] |
24 |
private static const LEVEL0:Class; |
25 |
private var level0data:String = new LEVEL0; |
26 |
|
27 |
// the "demo" level background TERRAIN
|
28 |
[Embed(source = '../assets/terrain0.oel', mimeType = 'application/octet-stream')] |
29 |
private static const LEVEL0TERRAIN:Class; |
30 |
private var level0terrain:String = new LEVEL0TERRAIN; |
31 |
|
32 |
// the first level that the player actually experiences
|
33 |
[Embed(source = '../assets/level1.oel', mimeType = 'application/octet-stream')] |
34 |
private static const LEVEL1:Class; |
35 |
private var level1data:String = new LEVEL1; |
36 |
|
37 |
// the first level background TERRAIN
|
38 |
[Embed(source = '../assets/terrain1.oel', mimeType = 'application/octet-stream')] |
39 |
private static const LEVEL1TERRAIN:Class; |
40 |
private var level1terrain:String = new LEVEL1TERRAIN; |
41 |
|
42 |
// the currently loaded level data
|
43 |
public var data:Array = []; |
44 |
|
45 |
public function GameLevels() |
46 |
{
|
47 |
}
|
Paso 11: Analizar los datos de nivel
Nuestra nueva clase de análisis de datos de nivel va a ser increíblemente simplista: simplemente eliminamos cualquier XML superfluo y engullimos los datos .CSV dividiendo cada línea por comas. Esto es suficiente para nuestros propósitos.
Continúa con GameLevels.as e implementa la función de análisis de datos de nivel como sigue:
1 |
|
2 |
private function stripTags(str:String):String |
3 |
{
|
4 |
var pattern:RegExp = /<\/?[a-zA-Z0-9]+.*?>/gim; |
5 |
return str.replace(pattern, ""); |
6 |
}
|
7 |
|
8 |
private function parseLevelData(lvl:String):Array |
9 |
{
|
10 |
var levelString:String; |
11 |
var temps:Array; |
12 |
var nextValue:int; |
13 |
var output:Array = []; |
14 |
var nextrow:int; |
15 |
switch (lvl) |
16 |
{
|
17 |
case "level0" : levelString = stripTags(level0data); break; |
18 |
case "terrain0" : levelString = stripTags(level0terrain); break; |
19 |
case "level1" : levelString = stripTags(level1data); break; |
20 |
case "terrain1" : levelString = stripTags(level1terrain); break; |
21 |
default: |
22 |
return output; |
23 |
}
|
24 |
//trace("Level " + num + " data:\n" + levelString);
|
25 |
var lines:Array = levelString.split(/\r\n|\n|\r/); |
26 |
for (var row:int = 0; row < lines.length; row++) |
27 |
{
|
28 |
// split the string by comma
|
29 |
temps = lines[row].split(","); |
30 |
if (temps.length > 1) |
31 |
{
|
32 |
nextrow = output.push([]) - 1; |
33 |
// turn the string values into integers
|
34 |
for (var col:int = 0; col < temps.length; col++) |
35 |
{
|
36 |
if (temps[col] == "") temps[col] = "-1"; |
37 |
nextValue = parseInt(temps[col]); |
38 |
if (nextValue < 0) nextValue = -1; // we still need blanks |
39 |
trace('row '+ nextrow + ' nextValue=' + nextValue); |
40 |
output[nextrow].push(nextValue); |
41 |
}
|
42 |
//trace('Level row '+nextrow+':\n' + String(output[nextrow]));
|
43 |
}
|
44 |
}
|
45 |
//trace('Level output data:\n' + String(output));
|
46 |
return output; |
47 |
}
|
48 |
|
49 |
public function loadLevel(lvl:String):void |
50 |
{
|
51 |
trace("Loading level " + lvl); |
52 |
data = parseLevelData(lvl); |
53 |
}
|
54 |
|
55 |
} // end class |
56 |
} // end package |
Eso es todo para nuestra clase de análisis de niveles. Aunque es simplista, ocupa muy poco espacio en nuestro SWF, se ejecuta bastante rápido y nos permite iterar nuestros diseños de niveles con facilidad al tener FlashDevelop y OGMO abiertos al mismo tiempo. Basta con dos clics para probar una nueva versión de tu nivel, lo que significa que el ciclo diseño-prueba-repetición es de apenas unos segundos.
Solo por diversión, aquí hay algunas capturas de pantalla del estilo de niveles que vamos a poder jugar en nuestro juego:












Paso 12: Actualizar el Gestor de Entidades
Tenemos que aprovechar estos nuevos y geniales modos de movimiento de la IA y el nuevo e impresionante sistema de terreno que acabamos de crear. Esto requerirá algunos ajustes menores en el archivo EntityManager.as de la última vez. Para evitar confusiones, toda la clase se presenta aquí, pero solo unas pocas líneas aquí y allá han cambiado.
En particular, en lugar de forzar al gestor de entidades a usar una imagen particular de spritesheet, vamos a permitir que sea definida por nuestro Main.as para que podamos tener más de una. Esto se debe a que ahora tenemos un enemigo y un gestor de entidades del terreno que se ejecutan simultáneamente.
Otros ajustes menores incluyen una mayor distancia de culling (los bordes exteriores del mundo del juego donde los sprites que van más allá son reciclados para su reutilización en nuestro pool de sprites), además de varias variables de clase nuevas que son necesarias para nuestra rutina de análisis de datos de nivel.
Comienza actualizando todas las variables de la clase en la parte superior del archivo de la siguiente manera:
1 |
|
2 |
// Stage3D Shoot-em-up Tutorial Part 4
|
3 |
// by Christer Kaitila - www.mcfunkypants.com
|
4 |
|
5 |
// EntityManager.as
|
6 |
// The entity manager handles a list of all known game entities.
|
7 |
// This object pool will allow for reuse (respawning) of
|
8 |
// sprites: for example, when enemy ships are destroyed,
|
9 |
// they will be re-spawned when needed as an optimization
|
10 |
// that increases fps and decreases ram use.
|
11 |
|
12 |
package
|
13 |
{
|
14 |
import flash.display.Bitmap; |
15 |
import flash.display3D.*; |
16 |
import flash.geom.Point; |
17 |
import flash.geom.Rectangle; |
18 |
|
19 |
public class EntityManager |
20 |
{
|
21 |
// the level data parser
|
22 |
public var level:GameLevels; |
23 |
// the current level number
|
24 |
public var levelNum:int = 0; |
25 |
// where in the level we are in pixels
|
26 |
public var levelCurrentScrollX:Number = 0; |
27 |
// the last spawned column of level data
|
28 |
public var levelPrevCol:int = -1; |
29 |
// pixels we need to scroll before spawning the next col
|
30 |
public var levelTilesize:int = 48; |
31 |
// this is used to ensure all terrain tiles line up exactly
|
32 |
public var lastTerrainEntity:Entity; |
33 |
|
34 |
// we need to allow at least enough space for ship movement
|
35 |
// entities that move beyond the edges of the screen
|
36 |
// plus this amount are recycled (destroyed for reuse)
|
37 |
public var cullingDistance:Number = 200; |
38 |
|
39 |
// a particle system class that updates our sprites
|
40 |
public var particles:GameParticles; |
41 |
|
42 |
// so that explosions can be played
|
43 |
public var sfx:GameSound; |
44 |
|
45 |
// the sprite sheet image
|
46 |
public var spriteSheet : LiteSpriteSheet; |
47 |
public var SpritesPerRow:int = 8; |
48 |
public var SpritesPerCol:int = 8; |
49 |
// we no longer force a particular spritesheet here
|
50 |
//[Embed(source="../assets/sprites.png")]
|
51 |
//private var SourceImage : Class;
|
52 |
public var SourceImage : Class; |
53 |
|
54 |
// the general size of the player and enemies
|
55 |
public var defaultScale:Number = 1; |
56 |
// how fast the default scroll (enemy flying) speed is
|
57 |
public var defaultSpeed:Number = 128; |
58 |
// how fast player bullets go per second
|
59 |
public var bulletSpeed:Number = 250; |
60 |
|
61 |
// for framerate-independent timings
|
62 |
public var currentFrameSeconds:Number = 0; |
63 |
|
64 |
// sprite IDs (indexing the spritesheet)
|
65 |
public const spritenumFireball:uint = 63; |
66 |
public const spritenumFireburst:uint = 62; |
67 |
public const spritenumShockwave:uint = 61; |
68 |
public const spritenumDebris:uint = 60; |
69 |
public const spritenumSpark:uint = 59; |
70 |
public const spritenumBullet3:uint = 58; |
71 |
public const spritenumBullet2:uint = 57; |
72 |
public const spritenumBullet1:uint = 56; |
73 |
public const spritenumPlayer:uint = 10; |
74 |
public const spritenumOrb:uint = 17; |
75 |
|
76 |
// reused for calculation speed
|
77 |
public const DEGREES_TO_RADIANS:Number = Math.PI / 180; |
78 |
public const RADIANS_TO_DEGREES:Number = 180 / Math.PI; |
79 |
|
80 |
// the player entity - a special case
|
81 |
public var thePlayer:Entity; |
82 |
// a "power orb" that orbits the player
|
83 |
public var theOrb:Entity; |
84 |
|
85 |
// a reusable pool of entities
|
86 |
// this contains every known Entity
|
87 |
// including the contents of the lists below
|
88 |
public var entityPool : Vector.<Entity>; |
89 |
// these pools contain only certain types
|
90 |
// of entity as an optimization for smaller loops
|
91 |
public var allBullets : Vector.<Entity>; |
92 |
public var allEnemies : Vector.<Entity>; |
93 |
|
94 |
// all the polygons that make up the scene
|
95 |
public var batch : LiteSpriteBatch; |
96 |
|
97 |
// for statistics
|
98 |
public var numCreated : int = 0; |
99 |
public var numReused : int = 0; |
100 |
|
101 |
public var maxX:int; |
102 |
public var minX:int; |
103 |
public var maxY:int; |
104 |
public var minY:int; |
105 |
public var midpoint:int; |
Paso 13: Actualizar los Inits
Necesitamos actualizar el constructor de la clase de nuestro gestor de entidades para crear una instancia de la clase del analizador de datos a nivel de juego que escribimos anteriormente. Además, queremos almacenar el punto medio de la pantalla para usarlo como posición inicial de juego y extender las ubicaciones mínima y máxima de los sprites cada vez que se cambie el tamaño del juego. Continuando con EntityManager.as, actualiza lo siguiente.
1 |
|
2 |
public function EntityManager(view:Rectangle) |
3 |
{
|
4 |
entityPool = new Vector.<Entity>(); |
5 |
allBullets = new Vector.<Entity>(); |
6 |
allEnemies = new Vector.<Entity>(); |
7 |
particles = new GameParticles(this); |
8 |
setPosition(view); |
9 |
level = new GameLevels(); |
10 |
}
|
11 |
|
12 |
public function setPosition(view:Rectangle):void |
13 |
{
|
14 |
// allow moving fully offscreen before
|
15 |
// automatically being culled (and reused)
|
16 |
maxX = view.width + cullingDistance; |
17 |
minX = view.x - cullingDistance; |
18 |
maxY = view.height + cullingDistance; |
19 |
minY = view.y - cullingDistance; |
20 |
midpoint = view.height / 2; |
21 |
}
|
22 |
|
23 |
// this XOR based fast random number generator runs 4x faster
|
24 |
// than Math.random() and also returns a number from 0 to 1
|
25 |
// see http://www.calypso88.com/?cat=7
|
26 |
private const FASTRANDOMTOFLOAT:Number = 1 / uint.MAX_VALUE; |
27 |
private var fastrandomseed:uint = Math.random() * uint.MAX_VALUE; |
28 |
public function fastRandom():Number |
29 |
{
|
30 |
fastrandomseed ^= (fastrandomseed << 21); |
31 |
fastrandomseed ^= (fastrandomseed >>> 35); |
32 |
fastrandomseed ^= (fastrandomseed << 4); |
33 |
return (fastrandomseed * FASTRANDOMTOFLOAT); |
34 |
}
|
Paso 14: Tener en cuenta el acolchado UV
Hay un ajuste especial que se requiere para nuestra función createBatch. Resulta que la hoja de sprites del terreno, que utiliza sprites en mosaico que están uno al lado del otro y no incluye ningún espacio vacío entre mosaicos, puede producir fallos visuales en nuestro juego si se utiliza tal cual. Esto se debe a la forma en que la GPU de tu tarjeta de vídeo muestrea cada textura del lote de sprites cuando renderiza todos los sprites. Aquí hay un ejemplo de nuestro terreno renderizado usando las rutinas de la semana pasada:



Lo que ocurre en el ejemplo nº 1 es que los píxeles de los bordes de los mosaicos pueden "traspasar" a los mosaicos adyacentes debido a la interpolación bilineal de los valores RGB. Para tener en cuenta esto, tenemos que permitir un pequeño valor de desplazamiento de las coordenadas de la textura (UV), que "ampliará" cada baldosa en una cantidad minúscula. Sin este cambio, el juego tendría artefactos como los que se ven arriba.
1 |
|
2 |
public function createBatch(context3D:Context3D, uvPadding:Number=0) : LiteSpriteBatch |
3 |
{
|
4 |
var sourceBitmap:Bitmap = new SourceImage(); |
5 |
|
6 |
// create a spritesheet with 8x8 (64) sprites on it
|
7 |
spriteSheet = new LiteSpriteSheet(sourceBitmap.bitmapData, SpritesPerRow, SpritesPerCol, uvPadding); |
8 |
|
9 |
// Create new render batch
|
10 |
batch = new LiteSpriteBatch(context3D, spriteSheet); |
11 |
|
12 |
return batch; |
13 |
}
|
Paso 15: Rutinas de desove
Las siguientes rutinas son prácticamente idénticas a las de la semana pasada, aparte del uso del valor del punto medio en el desove del jugador y algunas diferencias de tamaño. Se incluyen aquí para completarlas.
Por último, la función addRandomEntity definida a continuación es lo que en versiones anteriores era la función addEntities. No se utiliza en esta demo y podría eliminarse, ya que estamos cambiando nuestro juego para dejar de utilizar enemigos creados al azar y pasar a niveles creados a mano. Esta función puede ser útil en sus pruebas para contabilizar el tiempo cuando no hay datos de nivel restantes. Puedes simplemente copiar y pegar este código encima de tus rutinas originales y seguir adelante sin echar un vistazo más profundo.
1 |
|
2 |
|
3 |
// search the entity pool for unused entities and reuse one
|
4 |
// if they are all in use, create a brand new one
|
5 |
public function respawn(sprID:uint=0):Entity |
6 |
{
|
7 |
var currentEntityCount:int = entityPool.length; |
8 |
var anEntity:Entity; |
9 |
var i:int = 0; |
10 |
// search for an inactive entity
|
11 |
for (i = 0; i < currentEntityCount; i++ ) |
12 |
{
|
13 |
anEntity = entityPool[i]; |
14 |
if (!anEntity.active && (anEntity.sprite.spriteId == sprID)) |
15 |
{
|
16 |
//trace('Reusing Entity #' + i);
|
17 |
anEntity.active = true; |
18 |
anEntity.sprite.visible = true; |
19 |
anEntity.recycled = true; |
20 |
numReused++; |
21 |
return anEntity; |
22 |
}
|
23 |
}
|
24 |
// none were found so we need to make a new one
|
25 |
//trace('Need to create a new Entity #' + i);
|
26 |
var sprite:LiteSprite; |
27 |
sprite = batch.createChild(sprID); |
28 |
anEntity = new Entity(sprite, this); |
29 |
entityPool.push(anEntity); |
30 |
numCreated++; |
31 |
return anEntity; |
32 |
}
|
33 |
|
34 |
// this entity is the PLAYER
|
35 |
public function addPlayer(playerController:Function):Entity |
36 |
{
|
37 |
trace("Adding Player Entity"); |
38 |
thePlayer = respawn(spritenumPlayer); |
39 |
thePlayer.sprite.position.x = 64; |
40 |
thePlayer.sprite.position.y = midpoint; |
41 |
thePlayer.sprite.rotation = 180 * DEGREES_TO_RADIANS; |
42 |
thePlayer.sprite.scaleX = thePlayer.sprite.scaleY = defaultScale; |
43 |
thePlayer.speedX = 0; |
44 |
thePlayer.speedY = 0; |
45 |
thePlayer.active = true; |
46 |
thePlayer.collidemode = 1; |
47 |
thePlayer.collideradius = 10; |
48 |
thePlayer.owner = thePlayer; // collisions require this |
49 |
thePlayer.aiFunction = playerController; |
50 |
|
51 |
// just for fun, spawn an orbiting "power orb"
|
52 |
theOrb = respawn(spritenumOrb); |
53 |
theOrb.rotationSpeed = 720 * DEGREES_TO_RADIANS; |
54 |
theOrb.sprite.scaleX = theOrb.sprite.scaleY = defaultScale / 2; |
55 |
theOrb.leavesTrail = true; |
56 |
theOrb.collidemode = 1; |
57 |
theOrb.collideradius = 12; |
58 |
theOrb.isBullet = true; |
59 |
theOrb.owner = thePlayer; |
60 |
theOrb.orbiting = thePlayer; |
61 |
theOrb.orbitingDistance = 180; |
62 |
|
63 |
return thePlayer; |
64 |
}
|
65 |
|
66 |
// shoot a bullet
|
67 |
public function shootBullet(powa:uint=1, shooter:Entity = null):Entity |
68 |
{
|
69 |
// just in case the AI is running during the main menu
|
70 |
// and we've not yet created the player entity
|
71 |
if (thePlayer == null) return null; |
72 |
|
73 |
var theBullet:Entity; |
74 |
// assume the player shot it
|
75 |
// otherwise maybe an enemy did
|
76 |
if (shooter == null) |
77 |
shooter = thePlayer; |
78 |
|
79 |
// three possible bullets, progressively larger
|
80 |
if (powa == 1) |
81 |
theBullet = respawn(spritenumBullet1); |
82 |
else if (powa == 2) |
83 |
theBullet = respawn(spritenumBullet2); |
84 |
else
|
85 |
theBullet = respawn(spritenumBullet3); |
86 |
theBullet.sprite.position.x = shooter.sprite.position.x + 8; |
87 |
theBullet.sprite.position.y = shooter.sprite.position.y + 2; |
88 |
theBullet.sprite.rotation = 180 * DEGREES_TO_RADIANS; |
89 |
theBullet.sprite.scaleX = theBullet.sprite.scaleY = 1; |
90 |
if (shooter == thePlayer) |
91 |
{
|
92 |
theBullet.speedX = bulletSpeed; |
93 |
theBullet.speedY = 0; |
94 |
}
|
95 |
else // enemy bullets move slower and towards the player |
96 |
{
|
97 |
theBullet.sprite.rotation = |
98 |
pointAtRad(theBullet.sprite.position.x - thePlayer.sprite.position.x, |
99 |
theBullet.sprite.position.y-thePlayer.sprite.position.y) - (90*DEGREES_TO_RADIANS); |
100 |
|
101 |
// move in the direction we're facing
|
102 |
theBullet.speedX = defaultSpeed*1.5*Math.cos(theBullet.sprite.rotation); |
103 |
theBullet.speedY = defaultSpeed*1.5*Math.sin(theBullet.sprite.rotation); |
104 |
|
105 |
// optionally, we could just fire straight ahead in the direction we're heading:
|
106 |
// theBullet.speedX = shooter.speedX * 1.5;
|
107 |
// theBullet.speedY = shooter.speedY * 1.5;
|
108 |
// and we could point where we're going like this:
|
109 |
// pointAtRad(theBullet.speedX,theBullet.speedY) - (90*DEGREES_TO_RADIANS);
|
110 |
}
|
111 |
theBullet.owner = shooter; |
112 |
theBullet.collideradius = 10; |
113 |
theBullet.collidemode = 1; |
114 |
theBullet.isBullet = true; |
115 |
if (!theBullet.recycled) |
116 |
allBullets.push(theBullet); |
117 |
return theBullet; |
118 |
}
|
119 |
|
120 |
// Unused: this was "addEntities()" in the previous tutorials.
|
121 |
// It spawns random enemies that move in a straight line
|
122 |
public function addRandomEntity():void |
123 |
{
|
124 |
var anEntity:Entity; |
125 |
var sprID:int; |
126 |
sprID = Math.floor(fastRandom() * 55); |
127 |
// try to reuse an inactive entity (or create a new one)
|
128 |
anEntity = respawn(sprID); |
129 |
// give it a new position and velocity
|
130 |
anEntity.sprite.position.x = maxX; |
131 |
anEntity.sprite.position.y = fastRandom() * maxY; |
132 |
anEntity.speedX = 15 * ((-1 * fastRandom() * 10) - 2); |
133 |
anEntity.speedY = 15 * ((fastRandom() * 5) - 2.5); |
134 |
anEntity.sprite.scaleX = defaultScale; |
135 |
anEntity.sprite.scaleY = defaultScale; |
136 |
anEntity.sprite.rotation = pointAtRad(anEntity.speedX,anEntity.speedY) - (90*DEGREES_TO_RADIANS); |
137 |
anEntity.collidemode = 1; |
138 |
anEntity.collideradius = 16; |
139 |
if (!anEntity.recycled) |
140 |
allEnemies.push(anEntity); |
141 |
}
|
142 |
|
143 |
// returns the angle in radians of two points
|
144 |
public function pointAngle(point1:Point, point2:Point):Number |
145 |
{
|
146 |
var dx:Number = point2.x - point1.x; |
147 |
var dy:Number = point2.y - point1.y; |
148 |
return -Math.atan2(dx,dy); |
149 |
}
|
150 |
|
151 |
// returns the angle in degrees of 0,0 to x,y
|
152 |
public function pointAtDeg(x:Number, y:Number):Number |
153 |
{
|
154 |
return -Math.atan2(x,y) * RADIANS_TO_DEGREES; |
155 |
}
|
156 |
|
157 |
// returns the angle in radians of 0,0 to x,y
|
158 |
public function pointAtRad(x:Number, y:Number):Number |
159 |
{
|
160 |
return -Math.atan2(x,y); |
161 |
}
|
162 |
|
163 |
// as an optimization to saver millions of checks, only
|
164 |
// the player's bullets check for collisions with all enemy ships
|
165 |
// (enemy bullets only check to hit the player)
|
166 |
public function checkCollisions(checkMe:Entity):Entity |
167 |
{
|
168 |
var anEntity:Entity; |
169 |
var collided:Boolean = false; |
170 |
if (checkMe.owner != thePlayer) |
171 |
{ // quick check ONLY to see if we have hit the player |
172 |
anEntity = thePlayer; |
173 |
if (checkMe.colliding(anEntity)) |
174 |
{
|
175 |
trace("Player was HIT!"); |
176 |
collided = true; |
177 |
}
|
178 |
}
|
179 |
else // check all active enemies |
180 |
{
|
181 |
for(var i:int=0; i< allEnemies.length;i++) |
182 |
{
|
183 |
anEntity = allEnemies[i]; |
184 |
if (anEntity.active && anEntity.collidemode) |
185 |
{
|
186 |
if (checkMe.colliding(anEntity)) |
187 |
{
|
188 |
collided = true; |
189 |
break; |
190 |
}
|
191 |
}
|
192 |
}
|
193 |
}
|
194 |
if (collided) |
195 |
{
|
196 |
//trace('Collision!');
|
197 |
if (sfx) sfx.playExplosion(int(fastRandom() * 2 + 1.5)); |
198 |
particles.addExplosion(checkMe.sprite.position); |
199 |
if ((checkMe != theOrb) && (checkMe != thePlayer)) |
200 |
checkMe.die(); // the bullet |
201 |
if ((anEntity != theOrb) && ((anEntity != thePlayer))) |
202 |
anEntity.die(); // the victim |
203 |
return anEntity; |
204 |
}
|
205 |
return null; |
206 |
}
|
Paso 16: Actualizar el bucle de renderizado
La función update() se ejecuta cada fotograma, igual que antes. Se han hecho varias modificaciones para tener en cuenta la nueva funcionalidad de la IA enemiga que hemos añadido a la clase de entidad anterior, así como nuestra nueva funcionalidad de análisis de datos de nivel.
Por ejemplo, la semana pasada solo ejecutamos el paso de actualización de la simulación de la entidad si no había una aiFunction definida. Ahora, vamos a llamar a esta función en casi todas las entidades del juego que se mueven, así que la ejecutamos y luego continuamos con la animación estándar comprobando las velocidades de varios parámetros de la entidad. También solíamos comprobar las colisiones por balas, pero ahora una nave enemiga también puede colisionar con el jugador.
Siguiendo con EntityManager.as, implementa estos cambios como sigue.
1 |
|
2 |
|
3 |
// called every frame: used to update the simulation
|
4 |
// this is where you would perform AI, physics, etc.
|
5 |
// in this version, currentTime is seconds since the previous frame
|
6 |
public function update(currentTime:Number) : void |
7 |
{
|
8 |
var anEntity:Entity; |
9 |
var i:int; |
10 |
var max:int; |
11 |
|
12 |
// what portion of a full second has passed since the previous update?
|
13 |
currentFrameSeconds = currentTime / 1000; |
14 |
|
15 |
// handle all other entities
|
16 |
max = entityPool.length; |
17 |
for (i = 0; i < max; i++) |
18 |
{
|
19 |
anEntity = entityPool[i]; |
20 |
if (anEntity.active) |
21 |
{
|
22 |
// subtract the previous aiPathOffset
|
23 |
anEntity.sprite.position.x -= anEntity.aiPathOffsetX; |
24 |
anEntity.sprite.position.y -= anEntity.aiPathOffsetY; |
25 |
|
26 |
// calculate location on screen with scrolling
|
27 |
anEntity.sprite.position.x += anEntity.speedX * currentFrameSeconds; |
28 |
anEntity.sprite.position.y += anEntity.speedY * currentFrameSeconds; |
29 |
|
30 |
// is a custom AI specified? if so, run it now
|
31 |
if (anEntity.aiFunction != null) |
32 |
{
|
33 |
anEntity.aiFunction(currentFrameSeconds); |
34 |
}
|
35 |
|
36 |
// add the new aiPathOffset
|
37 |
anEntity.sprite.position.x += anEntity.aiPathOffsetX; |
38 |
anEntity.sprite.position.y += anEntity.aiPathOffsetY; |
39 |
|
40 |
// collision detection
|
41 |
if (anEntity.collidemode) |
42 |
{
|
43 |
checkCollisions(anEntity); |
44 |
}
|
45 |
|
46 |
// entities can orbit other entities
|
47 |
// (uses their rotation as the position)
|
48 |
if (anEntity.orbiting != null) |
49 |
{
|
50 |
anEntity.sprite.position.x = anEntity.orbiting.sprite.position.x + |
51 |
((Math.sin(anEntity.sprite.rotation/4)/Math.PI) * anEntity.orbitingDistance); |
52 |
anEntity.sprite.position.y = anEntity.orbiting.sprite.position.y - |
53 |
((Math.cos(anEntity.sprite.rotation/4)/Math.PI) * anEntity.orbitingDistance); |
54 |
}
|
55 |
|
56 |
// entities can leave an engine emitter trail
|
57 |
if (anEntity.leavesTrail) |
58 |
{
|
59 |
// leave a trail of particles
|
60 |
if (anEntity == theOrb) |
61 |
particles.addParticle(63, |
62 |
anEntity.sprite.position.x, anEntity.sprite.position.y, |
63 |
0.25, 0, 0, 0.6, NaN, NaN, -1.5, -1); |
64 |
else // other enemies |
65 |
particles.addParticle(63, anEntity.sprite.position.x + 12, |
66 |
anEntity.sprite.position.y + 2, |
67 |
0.5, 3, 0, 0.6, NaN, NaN, -1.5, -1); |
68 |
|
69 |
}
|
70 |
|
71 |
if ((anEntity.sprite.position.x > maxX) || |
72 |
(anEntity.sprite.position.x < minX) || |
73 |
(anEntity.sprite.position.y > maxY) || |
74 |
(anEntity.sprite.position.y < minY)) |
75 |
{
|
76 |
// if we go past any edge, become inactive
|
77 |
// so the sprite can be respawned
|
78 |
if ((anEntity != thePlayer) && (anEntity != theOrb)) |
79 |
anEntity.die(); |
80 |
}
|
81 |
|
82 |
if (anEntity.rotationSpeed != 0) |
83 |
anEntity.sprite.rotation += anEntity.rotationSpeed * currentFrameSeconds; |
84 |
|
85 |
if (anEntity.fadeAnim != 0) |
86 |
{
|
87 |
anEntity.sprite.alpha += anEntity.fadeAnim * currentFrameSeconds; |
88 |
if (anEntity.sprite.alpha <= 0.001) |
89 |
{
|
90 |
anEntity.die(); |
91 |
}
|
92 |
else if (anEntity.sprite.alpha > 1) |
93 |
{
|
94 |
anEntity.sprite.alpha = 1; |
95 |
}
|
96 |
}
|
97 |
if (anEntity.zoomAnim != 0) |
98 |
{
|
99 |
anEntity.sprite.scaleX += anEntity.zoomAnim * currentFrameSeconds; |
100 |
anEntity.sprite.scaleY += anEntity.zoomAnim * currentFrameSeconds; |
101 |
if (anEntity.sprite.scaleX < 0 || anEntity.sprite.scaleY < 0) |
102 |
anEntity.die(); |
103 |
}
|
104 |
}
|
105 |
}
|
106 |
}
|
Paso 17: Cambiar de nivel
El resto de las funciones de EntityManager.as son nuevas. Necesitamos un mecanismo para destruir instantáneamente todas las entidades conocidas en todo el mundo del juego. Esto ocurrirá cada vez que el jugador pase al siguiente nivel. También ocurre inmediatamente cuando el juego sale del "menú principal" del modo de atracción para que los sprites que estaban allí no contaminen el mundo del juego real del jugador. Cuando el juego se inicia, analizaremos también el siguiente conjunto de datos del nivel.
1 |
|
2 |
// kill (recycle) all known entities
|
3 |
// this is run when we change levels
|
4 |
public function killEmAll():void |
5 |
{
|
6 |
//trace('Killing all entities...');
|
7 |
var anEntity:Entity; |
8 |
var i:int; |
9 |
var max:int; |
10 |
max = entityPool.length; |
11 |
for (i = 0; i < max; i++) |
12 |
{
|
13 |
anEntity = entityPool[i]; |
14 |
if ((anEntity != thePlayer) && (anEntity != theOrb)) |
15 |
anEntity.die(); |
16 |
}
|
17 |
}
|
18 |
|
19 |
// load a new level for entity generation
|
20 |
public function changeLevels(lvl:String):void |
21 |
{
|
22 |
killEmAll(); |
23 |
level.loadLevel(lvl); |
24 |
levelCurrentScrollX = 0; |
25 |
levelPrevCol = -1; |
26 |
}
|
Paso 18: Transmisión del nivel
La última función que tenemos que añadir a nuestro gestor de entidades es la que genera nuevas entidades basadas en los datos del nivel. Mide la distancia que hemos recorrido, y cuando se requiere el siguiente conjunto de baldosas de nivel, genera otra columna de entidades según lo especificado por los datos de nivel. Si el gestor de entidades que ejecuta esta rutina se encarga del terreno, no hay que hacer nada más, pero si estamos generando naves enemigas, asteroides y cañones centinela, tenemos que decidir qué tipo de rutina de IA dar a cada entidad.
Una consideración importante tiene que ver con los fallos del terreno, como se ilustra en la imagen anterior, que mostraba "costuras" entre las baldosas.



En las primeras versiones de esta función, simplemente medíamos la distancia recorrida en función del tiempo transcurrido en cada fotograma e incrementábamos una variable contadora, generando la siguiente fila de baldosas cuando era necesario. El problema con este enfoque es que los números de punto flotante (cualquier cosa con un punto decimal) no son 100% precisos. Dado que solo podemos almacenar cierta cantidad de información en un tipo Number, algunas cantidades realmente pequeñas se redondean.
Esto es inaceptable en la mayoría de las situaciones, pero con el tiempo las ligeras discrepancias se acumulan hasta que finalmente los azulejos del terreno están fuera de un píxel. Por lo tanto, mantenemos la pista de las baldosas de terreno de la columna anterior y forzamos a la siguiente a estar exactamente a la distancia correcta de ella. Simplemente no podemos asumir que el terreno se ha desplazado exactamente 48 píxeles desde la última vez que generamos fichas. Puede que se haya desplazado 48,00000000001 píxeles.
Puedes leer más sobre los muchos problemas que pueden producir los acumuladores de punto flotante en los juegos en este interesantísimo artículo.
1 |
|
2 |
// check to see if another row from the level data should be spawned
|
3 |
public function streamLevelEntities(theseAreEnemies:Boolean = false):void |
4 |
{
|
5 |
var anEntity:Entity; |
6 |
var sprID:int; |
7 |
// time-based with overflow remembering (increment and floor)
|
8 |
levelCurrentScrollX += defaultSpeed * currentFrameSeconds; |
9 |
// is it time to spawn the next col from our level data?
|
10 |
if (levelCurrentScrollX >= levelTilesize) |
11 |
{
|
12 |
levelCurrentScrollX = 0; |
13 |
levelPrevCol++; |
14 |
|
15 |
// this prevents small "seams" due to floating point inaccuracies over time
|
16 |
var currentLevelXCoord:Number; |
17 |
if (lastTerrainEntity && !theseAreEnemies) |
18 |
currentLevelXCoord = lastTerrainEntity.sprite.position.x + levelTilesize; |
19 |
else
|
20 |
currentLevelXCoord = maxX; |
21 |
|
22 |
var rows:int = level.data.length; |
23 |
//trace('levelCurrentScrollX = ' + levelCurrentScrollX +
|
24 |
//' - spawning next level column ' + levelPrevCol + ' row count: ' + rows);
|
25 |
|
26 |
if (level.data && level.data.length) |
27 |
{
|
28 |
for (var row:int = 0; row < rows; row++) |
29 |
{
|
30 |
if (level.data[row].length > levelPrevCol) // data exists? NOP? |
31 |
{
|
32 |
//trace('Next row data: ' + String(level.data[row]));
|
33 |
sprID = level.data[row][levelPrevCol]; |
34 |
if (sprID > -1) // zero is a valid number, -1 means blank |
35 |
{
|
36 |
anEntity = respawn(sprID); |
37 |
anEntity.sprite.position.x = currentLevelXCoord; |
38 |
anEntity.sprite.position.y = (row * levelTilesize) + (levelTilesize/2); |
39 |
trace('Spawning a level sprite ID ' + sprID + ' at ' |
40 |
+ anEntity.sprite.position.x + ',' + anEntity.sprite.position.y); |
41 |
anEntity.speedX = -defaultSpeed; |
42 |
anEntity.speedY = 0; |
43 |
anEntity.sprite.scaleX = defaultScale; |
44 |
anEntity.sprite.scaleY = defaultScale; |
Paso 19: Dar a los enemigos cerebros
Siguiendo con la función streamLevelEntities, simplemente tenemos que elegir qué tipo de IA usar para cada enemigo recién engendrado (si lo hay). Como referencia, esta es la hoja de sprites que estamos utilizando:

La hoja de sprites del enemigo se ha dividido en filas. La primera fila de sprites simplemente se mueve hacia adelante en línea recta en un ángulo aleatorio. La segunda fila utiliza nuestro recién creado movimiento sinusoidal "como una ola" que se pone en una línea recta. Tenemos en cuenta las dos baldosas del cañón centinela y las tres imágenes del asteroide como caso especial. Las siguientes filas se mueven en un ángulo aleatorio con un bamboleo y, finalmente, todos los sprites restantes utilizarán nuestro movimiento aleatorio de curva spline Catmull-Rom.
1 |
|
2 |
if (theseAreEnemies) |
3 |
{
|
4 |
// which AI should we give this enemy?
|
5 |
switch (sprID) |
6 |
{
|
7 |
case 1: |
8 |
case 2: |
9 |
case 3: |
10 |
case 4: |
11 |
case 5: |
12 |
case 6: |
13 |
case 7: |
14 |
// move forward at a random angle
|
15 |
anEntity.speedX = 15 * ((-1 * fastRandom() * 10) - 2); |
16 |
anEntity.speedY = 15 * ((fastRandom() * 5) - 2.5); |
17 |
anEntity.aiFunction = anEntity.straightAI; |
18 |
break; |
19 |
case 8: |
20 |
case 9: |
21 |
case 10: |
22 |
case 11: |
23 |
case 12: |
24 |
case 13: |
25 |
case 14: |
26 |
case 15: |
27 |
// move straight with a wobble
|
28 |
anEntity.aiFunction = anEntity.wobbleAI; |
29 |
break
|
30 |
case 16: |
31 |
case 24: // sentry guns don't move and always look at the player |
32 |
anEntity.aiFunction = anEntity.sentryAI; |
33 |
anEntity.speedX = -90; // same speed as background |
34 |
break; |
35 |
case 17: |
36 |
case 18: |
37 |
case 19: |
38 |
case 20: |
39 |
case 21: |
40 |
case 22: |
41 |
case 23: |
42 |
// move at a random angle with a wobble
|
43 |
anEntity.speedX = 15 * ((-1 * fastRandom() * 10) - 2); |
44 |
anEntity.speedY = 15 * ((fastRandom() * 5) - 2.5); |
45 |
anEntity.aiFunction = anEntity.wobbleAI; |
46 |
break; |
47 |
case 32: |
48 |
case 40: |
49 |
case 48: // asteroids don't move or shoot but they do spin and drift |
50 |
anEntity.aiFunction = null; |
51 |
anEntity.rotationSpeed = fastRandom() * 8 - 4 |
52 |
anEntity.speedY = fastRandom() * 64 - 32; |
53 |
break; |
54 |
default: // follow a complex random spline curve path |
55 |
anEntity.aiFunction = anEntity.droneAI; |
56 |
break; |
57 |
}
|
58 |
|
59 |
anEntity.sprite.rotation = pointAtRad(anEntity.speedX, anEntity.speedY) |
60 |
- (90*DEGREES_TO_RADIANS); |
61 |
anEntity.collidemode = 1; |
62 |
anEntity.collideradius = 16; |
63 |
if (!anEntity.recycled) |
64 |
allEnemies.push(anEntity); |
65 |
} // end if these were enemies |
66 |
}// end loop for level data rows |
67 |
}
|
68 |
}
|
69 |
}
|
70 |
// remember the last created terrain entity
|
71 |
// (might be null if the level data was blank for this column)
|
72 |
// to avoid slight seams due to terrain scrolling speed over time
|
73 |
if (!theseAreEnemies) lastTerrainEntity = anEntity; |
74 |
}
|
75 |
}
|
76 |
} // end class |
77 |
} // end package |
Eso es todo para nuestra nueva clase de gestor de entidades. Ahora se aprovecha de nuestro terreno de "streaming", da a los enemigos la IA adecuada, y ya no se limita a engendrar infinitas corrientes aleatorias de malos.
Paso 20: Tener en cuenta el acolchado UV
En nuestras actualizaciones anteriores, evitamos los pequeños fallos gráficos teniendo en cuenta dos cosas: la imprecisión de los puntos flotantes y la interpolación del muestreo de la textura, que provoca una sangría de píxeles de los bordes en las baldosas del terreno adyacentes. Esto último requirió que "ampliáramos" los mosaicos del terreno un poco para asegurarnos de que los bordes se vieran bien. Tenemos que actualizar nuestra clase existente LiteSpriteSheet.as para tener en cuenta este pequeño desplazamiento al crear todas las coordenadas de textura UV para cada sprite.
1 |
|
2 |
// Stage3D Shoot-em-up Tutorial Part 4
|
3 |
// by Christer Kaitila - www.mcfunkypants.com
|
4 |
|
5 |
// LiteSpriteSheet.as
|
6 |
// An optimization used to improve performance, all sprites used
|
7 |
// in the game are packed onto a single texture so that
|
8 |
// they can be rendered in a single pass rather than individually.
|
9 |
// This also avoids the performance penalty of 3d stage changes.
|
10 |
// Based on example code by Chris Nuuja which is a port
|
11 |
// of the haXe+NME bunnymark demo by Philippe Elsass
|
12 |
// which is itself a port of Iain Lobb's original work.
|
13 |
// Also includes code from the Starling framework.
|
14 |
// Grateful acknowledgements to all involved.
|
15 |
|
16 |
package
|
17 |
{
|
18 |
import flash.display.Bitmap; |
19 |
import flash.display.BitmapData; |
20 |
import flash.display.Stage; |
21 |
import flash.display3D.Context3D; |
22 |
import flash.display3D.Context3DTextureFormat; |
23 |
import flash.display3D.IndexBuffer3D; |
24 |
import flash.display3D.textures.Texture; |
25 |
import flash.geom.Point; |
26 |
import flash.geom.Rectangle; |
27 |
import flash.geom.Matrix; |
28 |
|
29 |
public class LiteSpriteSheet |
30 |
{
|
31 |
internal var _texture : Texture; |
32 |
|
33 |
protected var _spriteSheet : BitmapData; |
34 |
protected var _uvCoords : Vector.<Number>; |
35 |
protected var _rects : Vector.<Rectangle>; |
36 |
|
37 |
// because the edge pixels of some sprites are bleeding through,
|
38 |
// we zoom in the texture just the slightest bit for terrain tiles
|
39 |
public var uvPadding:Number = 0; // 0.01; |
40 |
|
41 |
public function LiteSpriteSheet(SpriteSheetBitmapData:BitmapData, numSpritesW:int = 8, numSpritesH:int = 8, uvPad:Number = 0) |
42 |
{
|
43 |
_uvCoords = new Vector.<Number>(); |
44 |
_rects = new Vector.<Rectangle>(); |
45 |
_spriteSheet = SpriteSheetBitmapData; |
46 |
uvPadding = uvPad; |
47 |
createUVs(numSpritesW, numSpritesH); |
48 |
}
|
49 |
|
50 |
// generate a list of uv coordinates for a grid of sprites
|
51 |
// on the spritesheet texture for later reference by ID number
|
52 |
// sprite ID numbers go from left to right then down
|
53 |
public function createUVs(numSpritesW:int, numSpritesH:int) : void |
54 |
{
|
55 |
trace('creating a '+_spriteSheet.width+'x'+_spriteSheet.height+ |
56 |
' spritesheet texture with '+numSpritesW+'x'+ numSpritesH+' sprites.'); |
57 |
|
58 |
var destRect : Rectangle; |
59 |
|
60 |
for (var y:int = 0; y < numSpritesH; y++) |
61 |
{
|
62 |
for (var x:int = 0; x < numSpritesW; x++) |
63 |
{
|
64 |
_uvCoords.push( |
65 |
// bl, tl, tr, br
|
66 |
(x / numSpritesW) + uvPadding, ((y+1) / numSpritesH) - uvPadding, |
67 |
(x / numSpritesW) + uvPadding, (y / numSpritesH) + uvPadding, |
68 |
((x+1) / numSpritesW) - uvPadding, (y / numSpritesH) + uvPadding, |
69 |
((x + 1) / numSpritesW) - uvPadding, ((y + 1) / numSpritesH) - uvPadding); |
70 |
|
71 |
destRect = new Rectangle(); |
72 |
destRect.left = 0; |
73 |
destRect.top = 0; |
74 |
destRect.right = _spriteSheet.width / numSpritesW; |
75 |
destRect.bottom = _spriteSheet.height / numSpritesH; |
76 |
_rects.push(destRect); |
77 |
}
|
78 |
}
|
79 |
}
|
80 |
|
81 |
// when the automated grid isn't what we want
|
82 |
// we can define any rectangle and return a new sprite ID
|
83 |
public function defineSprite(x:uint, y:uint, w:uint, h:uint) : uint |
84 |
{
|
85 |
var destRect:Rectangle = new Rectangle(); |
86 |
destRect.left = x; |
87 |
destRect.top = y; |
88 |
destRect.right = x + w; |
89 |
destRect.bottom = y + h; |
90 |
_rects.push(destRect); |
91 |
|
92 |
_uvCoords.push( |
93 |
destRect.x/_spriteSheet.width, destRect.y/_spriteSheet.height + destRect.height/_spriteSheet.height, |
94 |
destRect.x/_spriteSheet.width, destRect.y/_spriteSheet.height, |
95 |
destRect.x/_spriteSheet.width + destRect.width/_spriteSheet.width, destRect.y/_spriteSheet.height, |
96 |
destRect.x/_spriteSheet.width + destRect.width/_spriteSheet.width, destRect.y/_spriteSheet.height + destRect.height/_spriteSheet.height); |
97 |
|
98 |
return _rects.length - 1; |
99 |
}
|
100 |
|
101 |
public function removeSprite(spriteId:uint) : void |
102 |
{
|
103 |
if ( spriteId < _uvCoords.length ) { |
104 |
_uvCoords = _uvCoords.splice(spriteId * 8, 8); |
105 |
_rects.splice(spriteId, 1); |
106 |
}
|
107 |
}
|
108 |
|
109 |
public function get numSprites() : uint |
110 |
{
|
111 |
return _rects.length; |
112 |
}
|
113 |
|
114 |
public function getRect(spriteId:uint) : Rectangle |
115 |
{
|
116 |
return _rects[spriteId]; |
117 |
}
|
118 |
|
119 |
public function getUVCoords(spriteId:uint) : Vector.<Number> |
120 |
{
|
121 |
var startIdx:uint = spriteId * 8; |
122 |
return _uvCoords.slice(startIdx, startIdx + 8); |
123 |
}
|
124 |
|
125 |
public function uploadTexture(context3D:Context3D) : void |
126 |
{
|
127 |
if ( _texture == null ) { |
128 |
_texture = context3D.createTexture(_spriteSheet.width, _spriteSheet.height, Context3DTextureFormat.BGRA, false); |
129 |
}
|
130 |
|
131 |
_texture.uploadFromBitmapData(_spriteSheet); |
132 |
|
133 |
// generate mipmaps
|
134 |
var currentWidth:int = _spriteSheet.width >> 1; |
135 |
var currentHeight:int = _spriteSheet.height >> 1; |
136 |
var level:int = 1; |
137 |
var canvas:BitmapData = new BitmapData(currentWidth, currentHeight, true, 0); |
138 |
var transform:Matrix = new Matrix(.5, 0, 0, .5); |
139 |
|
140 |
while ( currentWidth >= 1 || currentHeight >= 1 ) { |
141 |
canvas.fillRect(new Rectangle(0, 0, Math.max(currentWidth,1), Math.max(currentHeight,1)), 0); |
142 |
canvas.draw(_spriteSheet, transform, null, null, null, true); |
143 |
_texture.uploadFromBitmapData(canvas, level++); |
144 |
transform.scale(0.5, 0.5); |
145 |
currentWidth = currentWidth >> 1; |
146 |
currentHeight = currentHeight >> 1; |
147 |
}
|
148 |
}
|
149 |
} // end class |
150 |
} // end package |
Como puedes ver, los únicos cambios anteriores están relacionados con el parámetro uvPadding en el constructor de la clase para cada spritesheet.
Paso 21: ¡Actualizaciones finales!
¡Ya casi hemos terminado! Todo lo que tenemos que hacer ahora es actualizar el Main.as existente en nuestro proyecto para tener en cuenta los diversos cambios menores en la forma de crear nuevas instancias de nuestro gestor de entidades y sus hojas de sprites. Los cambios en este archivo son triviales, pero se incluye aquí en su totalidad para evitar confusiones.
Las principales diferencias incluyen el hecho de que ahora estamos incrustando las dos hojas de sprites principales aquí en lugar de en el gestor de entidades, la adición de una capa de terreno, tratando con dos velocidades de desplazamiento diferentes (ya que queremos que el terreno se mueva más lento que los enemigos en primer plano, y la activación de los nuevos niveles a cargar.
1 |
|
2 |
// Stage3D Shoot-em-up Tutorial Part 4
|
3 |
// by Christer Kaitila - www.mcfunkypants.com
|
4 |
// Created for active.tutsplus.com
|
5 |
|
6 |
package
|
7 |
{
|
8 |
[SWF(width = "600", height = "400", frameRate = "60", backgroundColor = "#000000")] |
9 |
|
10 |
import flash.display3D.*; |
11 |
import flash.display.Sprite; |
12 |
import flash.display.StageAlign; |
13 |
import flash.display.StageQuality; |
14 |
import flash.display.StageScaleMode; |
15 |
import flash.events.Event; |
16 |
import flash.events.ErrorEvent; |
17 |
import flash.events.MouseEvent; |
18 |
import flash.geom.Rectangle; |
19 |
import flash.utils.getTimer; |
20 |
|
21 |
public class Main extends Sprite |
22 |
{
|
23 |
// the entity spritesheet (ships, particles)
|
24 |
[Embed(source="../assets/sprites.png")] |
25 |
private var EntitySourceImage : Class; |
26 |
|
27 |
// the terrain spritesheet
|
28 |
[Embed(source="../assets/terrain.png")] |
29 |
private var TerrainSourceImage : Class; |
30 |
|
31 |
// the keyboard control system
|
32 |
private var _controls : GameControls; |
33 |
// don't update the menu too fast
|
34 |
private var nothingPressedLastFrame:Boolean = false; |
35 |
// timestamp of the current frame
|
36 |
public var currentTime:int; |
37 |
// for framerate independent speeds
|
38 |
public var currentFrameMs:int; |
39 |
public var previousFrameTime:int; |
40 |
|
41 |
// player one's entity
|
42 |
public var thePlayer:Entity; |
43 |
// movement speed in pixels per second
|
44 |
public var playerSpeed:Number = 128; |
45 |
// timestamp when next shot can be fired
|
46 |
private var nextFireTime:uint = 0; |
47 |
// how many ms between shots
|
48 |
private var fireDelay:uint = 200; |
49 |
|
50 |
// main menu = 0 or current level number
|
51 |
private var _state : int = 0; |
52 |
// the title screen batch
|
53 |
private var _mainmenu : GameMenu; |
54 |
// the sound system
|
55 |
private var _sfx : GameSound; |
56 |
// the background stars
|
57 |
private var _bg : GameBackground; |
58 |
|
59 |
private var _terrain : EntityManager; |
60 |
private var _entities : EntityManager; |
61 |
private var _spriteStage : LiteSpriteStage; |
62 |
private var _gui : GameGUI; |
63 |
private var _width : Number = 600; |
64 |
private var _height : Number = 400; |
65 |
public var context3D : Context3D; |
66 |
|
67 |
// constructor function for our game
|
68 |
public function Main():void |
69 |
{
|
70 |
if (stage) init(); |
71 |
else addEventListener(Event.ADDED_TO_STAGE, init); |
72 |
}
|
73 |
|
74 |
// called once flash is ready
|
75 |
private function init(e:Event = null):void |
76 |
{
|
77 |
_controls = new GameControls(stage); |
78 |
removeEventListener(Event.ADDED_TO_STAGE, init); |
79 |
stage.quality = StageQuality.LOW; |
80 |
stage.align = StageAlign.TOP_LEFT; |
81 |
stage.scaleMode = StageScaleMode.NO_SCALE; |
82 |
stage.addEventListener(Event.RESIZE, onResizeEvent); |
83 |
trace("Init Stage3D..."); |
84 |
_gui = new GameGUI("Stage3D Shoot-em-up Tutorial Part 3"); |
85 |
addChild(_gui); |
86 |
stage.stage3Ds[0].addEventListener(Event.CONTEXT3D_CREATE, onContext3DCreate); |
87 |
stage.stage3Ds[0].addEventListener(ErrorEvent.ERROR, errorHandler); |
88 |
stage.stage3Ds[0].requestContext3D(Context3DRenderMode.AUTO); |
89 |
trace("Stage3D requested..."); |
90 |
_sfx = new GameSound(); |
91 |
}
|
92 |
|
93 |
// this is called when the 3d card has been set up
|
94 |
// and is ready for rendering using stage3d
|
95 |
private function onContext3DCreate(e:Event):void |
96 |
{
|
97 |
trace("Stage3D context created! Init sprite engine..."); |
98 |
context3D = stage.stage3Ds[0].context3D; |
99 |
initSpriteEngine(); |
100 |
}
|
101 |
|
102 |
// this can be called when using an old version of flash
|
103 |
// or if the html does not include wmode=direct
|
104 |
private function errorHandler(e:ErrorEvent):void |
105 |
{
|
106 |
trace("Error while setting up Stage3D: "+e.errorID+" - " +e.text); |
107 |
}
|
108 |
|
109 |
protected function onResizeEvent(event:Event) : void |
110 |
{
|
111 |
trace("resize event..."); |
112 |
|
113 |
// Set correct dimensions if we resize
|
114 |
_width = stage.stageWidth; |
115 |
_height = stage.stageHeight; |
116 |
|
117 |
// Resize Stage3D to continue to fit screen
|
118 |
var view:Rectangle = new Rectangle(0, 0, _width, _height); |
119 |
if ( _spriteStage != null ) { |
120 |
_spriteStage.position = view; |
121 |
}
|
122 |
if(_terrain != null) { |
123 |
_terrain.setPosition(view); |
124 |
}
|
125 |
if(_entities != null) { |
126 |
_entities.setPosition(view); |
127 |
}
|
128 |
if(_mainmenu != null) { |
129 |
_mainmenu.setPosition(view); |
130 |
}
|
131 |
}
|
132 |
|
133 |
private function initSpriteEngine():void |
134 |
{
|
135 |
// init a gpu sprite system
|
136 |
//var view:Rectangle = new Rectangle(0,0,_width,_height)
|
137 |
var stageRect:Rectangle = new Rectangle(0, 0, _width, _height); |
138 |
_spriteStage = new LiteSpriteStage(stage.stage3Ds[0], context3D, stageRect); |
139 |
_spriteStage.configureBackBuffer(_width,_height); |
140 |
|
141 |
// create the background stars
|
142 |
trace("Init background..."); |
143 |
_bg = new GameBackground(stageRect); |
144 |
_bg.createBatch(context3D); |
145 |
_spriteStage.addBatch(_bg.batch); |
146 |
_bg.initBackground(); |
147 |
|
148 |
// create the terrain spritesheet and batch
|
149 |
trace("Init Terrain..."); |
150 |
_terrain = new EntityManager(stageRect); |
151 |
_terrain.SourceImage = TerrainSourceImage; |
152 |
_terrain.SpritesPerRow = 16; |
153 |
_terrain.SpritesPerCol = 16; |
154 |
_terrain.defaultSpeed = 90; |
155 |
_terrain.defaultScale = 1.5; |
156 |
_terrain.levelTilesize = 48; |
157 |
_terrain.createBatch(context3D, 0.001); // a little UV padding required |
158 |
_spriteStage.addBatch(_terrain.batch); |
159 |
_terrain.level.loadLevel('terrain0'); // demo level NOW |
160 |
|
161 |
// create a single rendering batch
|
162 |
// which will draw all sprites in one pass
|
163 |
trace("Init Entities..."); |
164 |
_entities = new EntityManager(stageRect); |
165 |
_entities.SourceImage = EntitySourceImage; |
166 |
_entities.defaultScale = 1.5; // 1 |
167 |
_entities.levelTilesize = 48; |
168 |
_entities.createBatch(context3D); |
169 |
_entities.sfx = _sfx; |
170 |
_spriteStage.addBatch(_entities.batch); |
171 |
_entities.level.loadLevel('level0'); // demo level NOW |
172 |
_entities.streamLevelEntities(true); // spawn first row of the level immediately |
173 |
|
174 |
// create the logo/titlescreen main menu
|
175 |
_mainmenu = new GameMenu(stageRect); |
176 |
_mainmenu.createBatch(context3D); |
177 |
_spriteStage.addBatch(_mainmenu.batch); |
178 |
|
179 |
// tell the gui where to grab statistics from
|
180 |
_gui.statsTarget = _entities; |
181 |
|
182 |
// start the render loop
|
183 |
stage.addEventListener(Event.ENTER_FRAME,onEnterFrame); |
184 |
|
185 |
// only used for the menu
|
186 |
stage.addEventListener(MouseEvent.MOUSE_DOWN, mouseDown); |
187 |
stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseMove); |
188 |
}
|
189 |
|
190 |
public function playerLogic(seconds:Number):void |
191 |
{
|
192 |
var me:Entity = _entities.thePlayer; |
193 |
me.speedY = me.speedX = 0; |
194 |
if (_controls.pressing.up) |
195 |
me.speedY = -playerSpeed; |
196 |
if (_controls.pressing.down) |
197 |
me.speedY = playerSpeed; |
198 |
if (_controls.pressing.left) |
199 |
me.speedX = -playerSpeed; |
200 |
if (_controls.pressing.right) |
201 |
me.speedX = playerSpeed; |
202 |
|
203 |
// keep on screen
|
204 |
if (me.sprite.position.x < 0) |
205 |
me.sprite.position.x = 0; |
206 |
if (me.sprite.position.x > _width) |
207 |
me.sprite.position.x = _width; |
208 |
if (me.sprite.position.y < 0) |
209 |
me.sprite.position.y = 0; |
210 |
if (me.sprite.position.y > _height) |
211 |
me.sprite.position.y = _height; |
212 |
|
213 |
//
|
214 |
// leave a trail of particles
|
215 |
_entities.particles.addParticle(63, |
216 |
me.sprite.position.x - 12, |
217 |
me.sprite.position.y + 2, |
218 |
0.75, -200, 0, 0.4, NaN, NaN, -1, -1.5); |
219 |
}
|
220 |
|
221 |
private function mouseDown(e:MouseEvent):void |
222 |
{
|
223 |
trace('mouseDown at '+e.stageX+','+e.stageY); |
224 |
if (_state == 0) // are we at the main menu? |
225 |
{
|
226 |
if (_mainmenu && _mainmenu.activateCurrentMenuItem(getTimer())) |
227 |
{ // if the above returns true we should start the game |
228 |
startGame(); |
229 |
}
|
230 |
}
|
231 |
}
|
232 |
|
233 |
private function mouseMove(e:MouseEvent):void |
234 |
{
|
235 |
if (_state == 0) // are we at the main menu? |
236 |
{
|
237 |
// select menu items via mouse
|
238 |
if (_mainmenu) _mainmenu.mouseHighlight(e.stageX, e.stageY); |
239 |
}
|
240 |
}
|
241 |
|
242 |
// handle any player input
|
243 |
private function processInput():void |
244 |
{
|
245 |
if (_state == 0) // are we at the main menu? |
246 |
{
|
247 |
// select menu items via keyboard
|
248 |
if (_controls.pressing.down || _controls.pressing.right) |
249 |
{
|
250 |
if (nothingPressedLastFrame) |
251 |
{
|
252 |
_sfx.playGun(1); |
253 |
_mainmenu.nextMenuItem(); |
254 |
nothingPressedLastFrame = false; |
255 |
}
|
256 |
}
|
257 |
else if (_controls.pressing.up || _controls.pressing.left) |
258 |
{
|
259 |
if (nothingPressedLastFrame) |
260 |
{
|
261 |
_sfx.playGun(1); |
262 |
_mainmenu.prevMenuItem(); |
263 |
nothingPressedLastFrame = false; |
264 |
}
|
265 |
}
|
266 |
else if (_controls.pressing.fire) |
267 |
{
|
268 |
if (_mainmenu.activateCurrentMenuItem(getTimer())) |
269 |
{ // if the above returns true we should start the game |
270 |
startGame(); |
271 |
}
|
272 |
}
|
273 |
else
|
274 |
{
|
275 |
// this ensures the menu doesn't change too fast
|
276 |
nothingPressedLastFrame = true; |
277 |
}
|
278 |
}
|
279 |
else
|
280 |
{
|
281 |
// we are NOT at the main menu: we are actually playing the game
|
282 |
// in future versions we will add projectile
|
283 |
// spawning functinality here to fire bullets
|
284 |
if (_controls.pressing.fire) |
285 |
{
|
286 |
// is it time to fire again?
|
287 |
if (currentTime >= nextFireTime) |
288 |
{
|
289 |
//trace("Fire!");
|
290 |
nextFireTime = currentTime + fireDelay; |
291 |
_sfx.playGun(1); |
292 |
_entities.shootBullet(3); |
293 |
}
|
294 |
}
|
295 |
}
|
296 |
}
|
297 |
|
298 |
private function startGame():void |
299 |
{
|
300 |
trace("Starting game!"); |
301 |
_state = 1; |
302 |
_spriteStage.removeBatch(_mainmenu.batch); |
303 |
_sfx.playMusic(); |
304 |
// add the player entity to the game!
|
305 |
thePlayer = _entities.addPlayer(playerLogic); |
306 |
// load level one (and clear demo entities)
|
307 |
_entities.changeLevels('level1'); |
308 |
_terrain.changeLevels('terrain1'); |
309 |
}
|
310 |
|
311 |
// this function draws the scene every frame
|
312 |
private function onEnterFrame(e:Event):void |
313 |
{
|
314 |
try
|
315 |
{
|
316 |
// grab timestamp of current frame
|
317 |
currentTime = getTimer(); |
318 |
currentFrameMs = currentTime - previousFrameTime; |
319 |
previousFrameTime = currentTime; |
320 |
|
321 |
// erase the previous frame
|
322 |
context3D.clear(0, 0, 0, 1); |
323 |
|
324 |
// for debugging the input manager, update the gui
|
325 |
_gui.titleText = _controls.textDescription(); |
326 |
|
327 |
// process any player input
|
328 |
processInput(); |
329 |
|
330 |
// scroll the background
|
331 |
if (_entities.thePlayer) _bg.yParallax(_entities.thePlayer.sprite.position.y / _height); |
332 |
_bg.update(currentTime); |
333 |
|
334 |
// update the main menu titlescreen
|
335 |
if (_state == 0) |
336 |
_mainmenu.update(currentTime); |
337 |
|
338 |
// move/animate all entities
|
339 |
_terrain.update(currentFrameMs); |
340 |
_entities.update(currentFrameMs); |
341 |
|
342 |
// keep adding more sprites - IF we need to
|
343 |
_terrain.streamLevelEntities(false); |
344 |
_entities.streamLevelEntities(true); |
345 |
|
346 |
// draw all entities
|
347 |
_spriteStage.render(); |
348 |
|
349 |
// update the screen
|
350 |
context3D.present(); |
351 |
}
|
352 |
catch (e:Error) |
353 |
{
|
354 |
// this can happen if the computer goes to sleep and
|
355 |
// then re-awakens, requiring reinitialization of stage3D
|
356 |
// (the onContext3DCreate will fire again)
|
357 |
}
|
358 |
}
|
359 |
} // end class |
360 |
} // end package |
¡Ya hemos terminado! Compila tu proyecto, corrige cualquier error tipográfico y ejecuta el juego. Si tienes problemas con el código que has escrito o simplemente quieres la gratificación instantánea de todo en un solo lugar, recuerda que puedes descargar el código fuente completo aquí.
Aquí tienes algunos consejos por si tienes problemas:
- Si utilizas FlashBuilder, asegúrate de incluir "
-default-frame-rate 60" en las opciones del compilador para garantizar el mejor rendimiento. - Si usas Linux o Mac, puedes compilar esto desde la línea de comandos (o en un makefile) usando algo similar a "
mxmlc -load-config+=obj\shmup_tutorial_part4Config.xml -swf-version=13", dependiendo de tu entorno de trabajo. - Recuerda que como estamos usando Flash 11 necesitarás compilar usando la última versión del compilador de Flex y
playerglobal.swc. - Lo más importante es que recuerdes que tu HTML de incrustación de Flash tiene que incluir "
wmode=direct" para habilitar Stage3D.
Esta fuente solo ha sido probada usando FlashDevelop en Windows, y los consejos anteriores han sido amablemente enviados por tus compañeros lectores.
Una vez que todo se compila y se ejecuta correctamente, deberías ver algo parecido a esto: un juego de disparos en 3D de acción rápida, con terreno de desplazamiento de paralaje, montones de enemigos para destruir, sonidos, música y, por último, pero no menos importante, una velocidad de fotogramas de 60 cuadros por segundo.



Parte cuatro completa: ¡Prepárate para el nivel cinco!
Eso es todo para el tutorial número cuatro de esta serie. Ahora podemos presumir de tener un mundo de juego detallado, enemigos con cierta variedad y la posibilidad de utilizar un editor de niveles. ¡Vamos por buen camino hacia el producto final!
En el próximo tutorial, actualizaremos nuestro nuevo e impresionante juego de disparos para tener en cuenta la salud y la puntuación del jugador. También añadiremos el concepto de "vidas" para poder llegar a una condición de victoria "game over" o "créditos finales". Añadiremos transiciones para dar al jugador un descanso entre niveles, y permitiremos a los usuarios continuar donde lo dejaron. En lugar de una demo infinita en la que no se puede morir, nuestro proyecto será por fin un juego totalmente jugable que realmente suponga un reto para el jugador. Intentaremos escalar la dificultad en su justa medida para que empieces fácil y se vaya haciendo más difícil a medida que avances.
Después de la semana que viene, el último tutorial, el número seis, consistirá en añadir el pulido final, ¡además de la creación de un épico BOSS BATTLE! ¡Esto va a ser divertido!
Me encantaría que me comentaras algo sobre este tutorial. Invito a todos los lectores a ponerse en contacto conmigo a través de Twitter: @McFunkypants, mi blog mcfunkypants.com o en google+ en cualquier momento. En particular, me encantaría ver los juegos que haces usando este código y siempre estoy buscando nuevos temas para escribir futuros tutoriales. Ponte en contacto conmigo cuando quieras.
Si has disfrutado de estos tutoriales hasta ahora, ¿quizás quieras aprender más sobre Stage3D? Si es así, ¡por qué no comprar mi libro de Stage3d!
¡Buena suerte y diviértete!



