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

Construye un Shoot-'Em-Up de Stage3D: Prueba de Sprite

Scroll to top
Read Time: 47 min
This post is part of a series called Shoot-'Em-Up.
Create a Simple Space Shooter Game in HTML5 With EaselJS
Build a Stage3D Shoot-'Em-Up: Interaction

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

En esta serie de tutoriales (parte gratuita, parte Premium) crearemos un juego de disparos en 2D de alto rendimiento utilizando el nuevo motor de renderizado Stage3D acelerado por hardware. Aprovecharemos varias técnicas de optimización para conseguir un gran rendimiento en el renderizado de sprites 2D. En esta parte, construiremos una demostración de alto rendimiento que dibuja cientos de sprites en movimiento en la pantalla a la vez.


Avance del resultado final

Echemos un vistazo al resultado final en el que trabajaremos: una demo de sprite 2D de alto rendimiento que utiliza Stage3D con optimizaciones que incluyen una hoja de sprites y la agrupación de objetos.


Introducción: Flash 11 Stage3D

Si quieres llevar tus juegos Flash al siguiente nivel y buscas un montón de efectos visuales y una velocidad de fotogramas increíble, Stage3D va a ser tu nuevo mejor amigo.

La increíble velocidad de la nueva API Stage3D acelerada por hardware de Flash 11 está pidiendo que se utilice para juegos 2D. En lugar de utilizar los anticuados sprites de Flash en la DisplayList o las técnicas de blitting de última generación popularizadas por motores como FlashPunk y Flixel, la nueva generación de juegos en 2D utiliza la potencia de la GPU de tu tarjeta de vídeo para realizar las tareas de renderizado a una velocidad hasta 1000 veces superior a la que podía alcanzar Flash 10.

Aunque lleva 3D en su nombre, esta nueva API también es estupenda para los juegos en 2D. Podemos renderizar geometría simple en forma de cuadrados 2D (llamados quads) y dibujarlos en un plano. Esto nos permitirá renderizar toneladas de sprites en pantalla a unos sedosos 60fps.

Haremos un juego de disparos de desplazamiento lateral inspirado en títulos de arcade retro como R-Type o Gradius en ActionScript utilizando la API Stage3D de Flash 11. No es ni la mitad de difícil de lo que algunos dicen, y no necesitarás aprender los opcodes AGAL en lenguaje ensamblador.

En esta serie de tutoriales de 6 partes, vamos a programar un sencillo juego de disparos en 2D que ofrece un rendimiento de renderizado alucinante. Vamos a construirlo usando AS3 puro, compilado en FlashDevelop (lee más sobre él aquí). FlashDevelop es genial porque es 100% freeware, no necesitas comprar ninguna herramienta cara para tener el mejor IDE de AS3.


Paso 1: Crear un nuevo proyecto

Si aún no lo tienes, asegúrate de descargar e instalar FlashDevelop. Una vez que esté todo configurado (y le hayas permitido instalar la última versión del compilador de Flex automáticamente), enciéndelo e inicia un nuevo "Proyecto AS3".

Create an .AS3 project using FlashDevelopCreate an .AS3 project using FlashDevelopCreate an .AS3 project using FlashDevelop

FlashDevelop creará un proyecto de plantilla en blanco para ti. Vamos a rellenar los espacios en blanco, pieza por pieza, hasta que hayamos creado un juego decente.


Paso 2: Flash de destino 11

Entra en el menú del proyecto y cambia algunas opciones:

  1. Target Flash 11.1
  2. Cambia el tamaño a 600x400px
  3. Cambia el color de fondo a negro
  4. Cambia los FPS a 60
  5. Cambia el nombre del archivo SWF por un nombre de tu elección
Project propertiesProject propertiesProject properties

Paso 3: Importaciones

Ahora que nuestro proyecto en blanco está configurado, vamos a sumergirnos en la codificación. Para empezar, necesitaremos importar toda la funcionalidad de Stage3D requerida. Añade lo siguiente al principio de tu archivo Main.as.

1
// Stage3D Shoot-em-up Tutorial Part 1

2
// by Christer Kaitila - www.mcfunkypants.com

3
// Created for active.tutsplus.com

4
5
package 
6
{
7
  [SWF(width = "600", height = "400", frameRate = "60", backgroundColor = "#000000")]
8
9
  import flash.display3D.*;
10
  import flash.display.Sprite;
11
  import flash.display.StageAlign;
12
  import flash.display.StageQuality;
13
  import flash.display.StageScaleMode;
14
  import flash.events.Event;
15
  import flash.events.ErrorEvent;
16
  import flash.geom.Rectangle;
17
  import flash.utils.getTimer;

Paso 4: Inicializar Stage3D

El siguiente paso es esperar a que nuestro juego aparezca en el escenario de Flash. Hacer las cosas de esta manera permite el uso futuro de un precargador. Para simplificar, vamos a hacer la mayor parte de nuestro juego en una sola clase pequeña que hereda de la clase Sprite de Flash como sigue.

1
public class Main extends Sprite 
2
{
3
private var _entities : EntityManager;
4
private var _spriteStage : LiteSpriteStage;
5
private var _gui : GameGUI;
6
private var _width : Number = 600;
7
private var _height : Number = 400;
8
public var context3D : Context3D;
9
10
// constructor function for our game

11
public function Main():void 
12
{
13
  if (stage) init();
14
  else addEventListener(Event.ADDED_TO_STAGE, init);
15
}
16
17
// called once Flash is ready

18
private function init(e:Event = null):void 
19
{
20
  removeEventListener(Event.ADDED_TO_STAGE, init);
21
  stage.quality = StageQuality.LOW;
22
  stage.align = StageAlign.TOP_LEFT;
23
  stage.scaleMode = StageScaleMode.NO_SCALE;
24
  stage.addEventListener(Event.RESIZE, onResizeEvent);
25
  trace("Init Stage3D...");
26
  _gui = new GameGUI("Simple Stage3D Sprite Demo v1");
27
  addChild(_gui);
28
  stage.stage3Ds[0].addEventListener(Event.CONTEXT3D_CREATE, onContext3DCreate);
29
  stage.stage3Ds[0].addEventListener(ErrorEvent.ERROR, errorHandler);
30
  stage.stage3Ds[0].requestContext3D(Context3DRenderMode.AUTO);
31
  trace("Stage3D requested...");    
32
}

Después de establecer algunas propiedades específicas del escenario, solicitamos un contexto Stage3D. Esto puede tardar un poco (una fracción de segundo) ya que tu tarjeta de vídeo está configurada para el renderizado por hardware, así que tenemos que esperar al evento onContext3DCreate.

También queremos detectar cualquier error que pueda producirse, especialmente porque el contenido de Stage3D no se ejecuta si el código HTML incrustado que carga tu SWF no incluye el parámetro "wmode=direct". Estos errores también pueden producirse si el usuario está ejecutando una versión antigua de Flash o si no tiene una tarjeta de vídeo capaz de manejar el sombreado de píxeles 2.0.


Paso 5: Manejar cualquier evento

Añade las siguientes funciones que detectan cualquier evento que pueda activarse como se ha especificado anteriormente. En el caso de errores debidos a la ejecución de plugins de Flash antiguos, en futuras versiones de este juego podríamos querer emitir un mensaje y recordar al usuario que se actualice, pero por ahora este error simplemente se ignora.

Para los usuarios con tarjetas de vídeo antiguas (o controladores) que no soportan el modelo de sombreado 2.0, la buena noticia es que Flash 11 es lo suficientemente inteligente como para proporcionar un renderizador de software. No se ejecuta muy rápido, pero al menos todo el mundo podrá jugar a tu juego. Los que tengan equipos de juego decentes obtendrán una velocidad de fotogramas fantástica como nunca antes se había visto en un juego Flash.

1
// this is called when the 3d card has been set up

2
// and is ready for rendering using stage3d

3
private function onContext3DCreate(e:Event):void 
4
{
5
  trace("Stage3D context created! Init sprite engine...");
6
  context3D = stage.stage3Ds[0].context3D;
7
  initSpriteEngine();
8
}
9
10
// this can be called when using an old version of Flash

11
// or if the html does not include wmode=direct

12
private function errorHandler(e:ErrorEvent):void 
13
{
14
  trace("Error while setting up Stage3D: "+e.errorID+" - " +e.text);
15
}
16
17
protected function onResizeEvent(event:Event) : void
18
{
19
  trace("resize event...");
20
  
21
  // Set correct dimensions if we resize

22
  _width = stage.stageWidth;
23
  _height = stage.stageHeight;
24
  
25
  // Resize Stage3D to continue to fit screen

26
  var view:Rectangle = new Rectangle(0, 0, _width, _height);
27
  if ( _spriteStage != null ) {
28
    _spriteStage.position = view;
29
  }
30
  if(_entities != null) {
31
    _entities.setPosition(view);
32
  }
33
}

El código de manejo de eventos anterior detecta cuando Stage3D está listo para el renderizado por hardware y establece la variable context3D para su uso futuro. Los errores se ignoran por ahora. El evento de redimensionamiento simplemente actualiza el tamaño del escenario y las dimensiones del sistema de renderizado por lotes.


Paso 6: Iniciar el motor de sprites

Una vez recibido el contexto3D, estamos listos para comenzar a ejecutar el juego. Continuando con Main.as, añade lo siguiente.

1
private function initSpriteEngine():void 
2
{
3
  // init a gpu sprite system

4
  var stageRect:Rectangle = new Rectangle(0, 0, _width, _height); 
5
  _spriteStage = new LiteSpriteStage(stage.stage3Ds[0], context3D, stageRect);
6
  _spriteStage.configureBackBuffer(_width,_height);
7
  
8
  // create a single rendering batch

9
  // which will draw all sprites in one pass

10
  var view:Rectangle = new Rectangle(0,0,_width,_height)
11
  _entities = new EntityManager(stageRect);
12
  _entities.createBatch(context3D);
13
  _spriteStage.addBatch(_entities._batch);
14
  // add the first entity right now

15
  _entities.addEntity();
16
  
17
  // tell the gui where to grab statistics from

18
  _gui.statsTarget = _entities; 
19
  
20
  // start the render loop

21
  stage.addEventListener(Event.ENTER_FRAME,onEnterFrame);
22
}

Esta función crea un motor de renderizado de sprites (que se implementará más adelante) en el escenario, listo para utilizar el tamaño completo de tu archivo flash. A continuación, añadimos el gestor de entidades y el sistema de geometría por lotes (del que hablaremos más adelante). Ahora podemos dar una referencia al gestor de entidades a nuestra clase GUI de estadísticas para que pueda mostrar algunos números en pantalla sobre cuántos sprites se han creado o reutilizado. Por último, empezamos a escuchar el evento ENTER_FRAME, que empezará a disparar a un ritmo de hasta 60 veces por segundo.


Paso 7: Iniciar el bucle de renderización

Ahora que todo ha sido inicializado, ¡estamos listos para jugar! La siguiente función se ejecutará cada fotograma. Para los propósitos de esta primera demostración técnica, vamos a añadir un nuevo sprite en el escenario cada fotograma. Como vamos a implementar un pool de objetos (del que puedes leer más en este tutorial) en lugar de crear inifinitamente nuevos objetos hasta que se nos acabe la RAM, vamos a poder reutilizar las entidades antiguas que se hayan movido fuera de la pantalla.

Después de engendrar otro sprite, limpiamos el área stage3D de la pantalla (poniéndola en negro puro). A continuación actualizamos todas las entidades que están siendo controladas por nuestro gestor de entidades. Esto hará que se muevan un poco más en cada fotograma. Una vez que todos los sprites han sido actualizados, le decimos al sistema de geometría por lotes que los reúna a todos en un gran búfer de vértices y los ponga en pantalla en una sola llamada de dibujo, por eficiencia. Por último, le decimos al context3D que actualice la pantalla con nuestro renderizado final.

1
    // this function draws the scene every frame

2
    private function onEnterFrame(e:Event):void 
3
    {
4
      try 
5
      {
6
        // keep adding more sprites - FOREVER!

7
        // this is a test of the entity manager's

8
        // object reuse "pool"

9
        _entities.addEntity();
10
        
11
        // erase the previous frame

12
        context3D.clear(0, 0, 0, 1);
13
        
14
        // move/animate all entities

15
        _entities.update(getTimer());
16
        
17
        // draw all entities

18
        _spriteStage.render();
19
20
        // update the screen

21
        context3D.present();
22
      }
23
      catch (e:Error) 
24
      {
25
        // this can happen if the computer goes to sleep and

26
        // then re-awakens, requiring reinitialization of stage3D

27
        // (the onContext3DCreate will fire again)

28
      }
29
    }
30
  } // end class

31
} // end package

¡Eso es todo para los inits! Tan simple como parece, ahora hemos creado un proyecto de plantilla que está listo para lanzar un número insano de sprites. No vamos a utilizar ningún arte vectorial. No vamos a poner ningún sprites de Flash anticuado en el escenario, aparte de la ventana Stage3D y un par de superposiciones de GUI. Todo el trabajo de renderización de nuestros gráficos en el juego va a ser manejado por Stage3D, para que podamos disfrutar de un rendimiento mejorado.


Profundizando: ¿Por qué Stage3D es tan rápido?

Por dos razones:

  1. Utiliza la aceleración por hardware, lo que significa que todos los comandos de dibujo se envían a la GPU 3D de tu tarjeta de vídeo de la misma manera que se renderizan los juegos de XBOX360 y PlayStation3.
  2. Estos comandos de renderizado se procesan en paralelo al resto de tu código ActionScript. Esto significa que una vez que los comandos se envían a tu tarjeta de vídeo, todo el renderizado se realiza al mismo tiempo que se ejecuta el resto del código de tu juego, Flash no tiene que esperar a que terminen. Mientras los píxeles aparecen en la pantalla, Flash se encarga de otras cosas, como manejar la entrada del jugador, reproducir sonidos y actualizar las posiciones de los enemigos.
  3. Dicho esto, muchos motores Stage3D parecen atascarse con unos cientos de sprites. Esto se debe a que han sido programados sin tener en cuenta la sobrecarga que añade cada comando de dibujo. Cuando Stage3D salió por primera vez, algunos de los primeros motores 2D dibujaban todos y cada uno de los sprites individualmente en un bucle gigante (lento e ineficiente). Como este artículo trata de la optimización extrema para un juego 2D de nueva generación con un framerate fabuloso, vamos a implementar un sistema de renderizado extremadamente eficiente que almacena toda la geometría en un gran lote para que podamos dibujar todo en solo uno o dos comandos.


    Cómo ser un Hardcore: ¡Optimizar!

    A los gamedevs más duros les encantan las optimizaciones. Con el fin de hacer estallar la mayor cantidad de sprites en la pantalla con el menor número de cambios de estado (como el cambio de texturas, la selección de un nuevo búfer de vértices, o tener que actualizar la transformación una vez para todos y cada uno de los sprites en la pantalla), vamos a tomar ventaja de las siguientes tres optimizaciones de rendimiento:

    1. agrupación de objetos
    2. spritesheet (atlas de texturas)
    3. geometría de lotes

    Estos tres trucos de gamedev son la clave para conseguir unos FPS increíbles en tu juego. Vamos a implementarlos ahora. Antes de hacerlo, necesitamos crear algunas de las pequeñas clases de las que harán uso estas técnicas.


    Paso 8: La pantalla de estadísticas

    Si vamos a realizar toneladas de optimizaciones y a utilizar Stage3D en un intento de conseguir un rendimiento de renderizado increíblemente rápido, necesitamos una forma de llevar un registro de las estadísticas. Unas pequeñas pruebas de referencia pueden servir para demostrar que lo que estamos haciendo tiene un efecto positivo en la velocidad de fotogramas. Antes de ir más lejos, crea una nueva clase llamada GameGUI.as e implementa una pantalla de FPS y estadísticas super simple como sigue.

    1
    // Stage3D Shoot-em-up Tutorial Part 1
    
    
    2
    // by Christer Kaitila - www.mcfunkypants.com
    
    
    3
    4
    // GameGUI.as
    
    
    5
    // A typical simplistic framerate display for benchmarking performance,
    
    
    6
    // plus a way to track rendering statistics from the entity manager.
    
    
    7
    8
    package
    
    9
    {
    
    10
      import flash.events.Event;
    
    11
      import flash.events.TimerEvent;
    
    12
      import flash.text.TextField;
    
    13
      import flash.text.TextFormat;
    
    14
      import flash.utils.getTimer;
    
    15
      
    
    16
      public class GameGUI extends TextField
    
    17
      {
    
    18
        public var titleText : String = "";
    
    19
        public var statsText : String = "";
    
    20
        public var statsTarget : EntityManager;
    
    21
        private var frameCount:int = 0;
    
    22
        private var timer:int;
    
    23
        private var ms_prev:int;
    
    24
        private var lastfps : Number = 60;
    
    25
        
    
    26
        public function GameGUI(title:String = "", inX:Number=8, inY:Number=8, inCol:int = 0xFFFFFF)
    
    27
        {
    
    28
          super();
    
    29
          titleText = title;
    
    30
          x = inX;
    
    31
          y = inY;
    
    32
          width = 500;
    
    33
          selectable = false;
    
    34
          defaultTextFormat = new TextFormat("_sans", 9, 0, true);
    
    35
          text = "";
    
    36
          textColor = inCol;
    
    37
          this.addEventListener(Event.ADDED_TO_STAGE, onAddedHandler);
    
    38
          
    
    39
        }
    
    40
        public function onAddedHandler(e:Event):void {
    
    41
          stage.addEventListener(Event.ENTER_FRAME, onEnterFrame);
    
    42
        }
    
    43
        
    
    44
        private function onEnterFrame(evt:Event):void
    
    45
        {
    
    46
          timer = getTimer();
    
    47
          
    
    48
          if( timer - 1000 > ms_prev )
    
    49
          {
    
    50
            lastfps = Math.round(frameCount/(timer-ms_prev)*1000);
    
    51
            ms_prev = timer;
    
    52
            
    
    53
            // grab the stats from the entity manager
    
    
    54
            if (statsTarget)
    
    55
            {
    
    56
              statsText = 
    
    57
                statsTarget.numCreated + ' created ' +
    
    58
                statsTarget.numReused + ' reused';
    
    59
            }
    
    60
            
    
    61
            text = titleText + ' - ' + statsText + " - FPS: " + lastfps;
    
    62
            frameCount = 0;
    
    63
          }
    
    64
        
    
    65
          // count each frame to determine the framerate
    
    
    66
          frameCount++;
    
    67
            
    
    68
        }
    
    69
      } // end class
    
    
    70
    } // end package
    

    Paso 9: La clase de entidad

    Vamos a implementar una clase gestora de entidades que será el "pool de objetos" descrito anteriormente. Primero tenemos que crear una clase simplista para cada entidad individual en nuestro juego. Esta clase se utilizará para todos los objetos del juego, desde las naves espaciales hasta las balas.

    Crea un nuevo archivo llamado Entity.as y añade unos cuantos getters y setters ahora. Para esta primera demostración técnica, esta clase es simplemente un marcador de posición vacío sin mucha funcionalidad, pero en tutoriales posteriores es donde implementaremos gran parte del juego.

    1
    // Stage3D Shoot-em-up Tutorial Part 1
    
    
    2
    // by Christer Kaitila - www.mcfunkypants.com
    
    
    3
    4
    // Entity.as
    
    
    5
    // The Entity class will eventually hold all game-specific entity logic
    
    
    6
    // for the spaceships, bullets and effects in our game. For now,
    
    
    7
    // it simply holds a reference to a gpu sprite and a few demo properties.
    
    
    8
    // This is where you would add hit points, weapons, ability scores, etc.
    
    
    9
    10
    package
    
    11
    {
    
    12
      public class Entity
    
    13
      {
    
    14
        private var _speedX : Number;
    
    15
        private var _speedY : Number;
    
    16
        private var _sprite : LiteSprite;
    
    17
        public var active : Boolean = true;
    
    18
        
    
    19
        public function Entity(gs:LiteSprite = null)
    
    20
        {
    
    21
          _sprite = gs;
    
    22
          _speedX = 0.0;
    
    23
          _speedY = 0.0;
    
    24
        }
    
    25
        public function die() : void
    
    26
        {
    
    27
          // allow this entity to be reused by the entitymanager
    
    
    28
          active = false;
    
    29
          // skip all drawing and updating
    
    
    30
          sprite.visible = false;
    
    31
        }
    
    32
        public function get speedX() : Number 
    
    33
        {
    
    34
          return _speedX;
    
    35
        }
    
    36
        public function set speedX(sx:Number) : void 
    
    37
        {
    
    38
          _speedX = sx;
    
    39
        }
    
    40
        public function get speedY() : Number 
    
    41
        {
    
    42
          return _speedY;
    
    43
        }
    
    44
        public function set speedY(sy:Number) : void 
    
    45
        {
    
    46
          _speedY = sy;
    
    47
        }
    
    48
        public function get sprite():LiteSprite 
    
    49
        { 
    
    50
          return _sprite;
    
    51
        }
    
    52
        public function set sprite(gs:LiteSprite):void 
    
    53
        {
    
    54
          _sprite = gs;
    
    55
        }
    
    56
      } // end class
    
    
    57
    } // end package
    

    Paso 10: Hacer una hoja de sprites

    Una importante técnica de optimización que vamos a utilizar es el uso de una hoja de sprites, a veces denominada Atlas de Textura. En lugar de cargar docenas o cientos de imágenes individuales en la memoria RAM de vídeo para utilizarlas durante el renderizado, vamos a hacer una sola imagen que contenga todos los sprites de nuestro juego. De esta manera, podemos utilizar una sola textura para dibujar toneladas de diferentes tipos de enemigos o terrenos.

    El uso de una hoja de sprites se considera una buena práctica por parte de los gamedevs veteranos que necesitan asegurarse de que sus juegos se ejecutan lo más rápido posible. La razón por la que acelera tanto las cosas es muy parecida a la razón por la que vamos a usar la geometría por lotes: en lugar de tener que decirle a la tarjeta de vídeo una y otra vez que use una textura concreta para dibujar un sprite concreto, podemos simplemente decirle que use siempre la misma textura para todas las llamadas de dibujo.

    Esto reduce los "cambios de estado" que son extremadamente costosos en términos de tiempo. Ya no tenemos que decir "tarjeta de vídeo, empieza a usar la textura 24... ahora dibuja el sprite 14" y así sucesivamente. Simplemente decimos "dibujar todo usando esta textura" en una sola pasada. Esto puede aumentar el rendimiento en un orden de magnitud.

    Para nuestro juego de ejemplo utilizaremos una colección de imágenes gratuitas de uso legal del talentoso DanC, que puedes conseguir aquí. Recuerda que si utilizas estas imágenes debes acreditarlas en tu juego de la siguiente manera "Título de la colección de arte" arte de Daniel Cook (Lostgarden.com).

    Utilizando Photoshop (o GIMP, o el editor de imágenes que prefieras), corta y pega los sprites que necesitará tu juego en un único archivo PNG que tenga un fondo transparente. Coloca cada uno de los sprites en una cuadrícula uniforme con un par de píxeles de espacio en blanco entre cada uno. Este pequeño búfer es necesario para evitar cualquier "sangría" de los píxeles de los bordes de los sprites adyacentes que puede ocurrir debido al filtrado bilineal de la textura que ocurre en la GPU. Si cada sprite está tocando al siguiente, tus sprites en el juego pueden tener bordes no deseados donde deberían ser completamente transparentes.

    Por razones de optimización, las GPUs funcionan mejor con imágenes (llamadas texturas) que son cuadradas y cuyas dimensiones son iguales a una potencia de dos y divisibles uniformemente por ocho. ¿Por qué? Debido a la forma en que se accede a los datos de los píxeles, estos números mágicos se alinean en la VRAM de la forma correcta para que el acceso sea más rápido, ya que los datos se suelen leer en trozos.

    Por lo tanto, asegúrate de que tu hoja de sprites sea de 64x64, 128x128, 256x256, 512x512 o 1024x1024. Como es de esperar, cuanto más pequeño sea, mejor, no solo en términos de rendimiento, sino porque una textura más pequeña hará que el SWF final del juego sea más pequeño.

    Aquí está la hoja de sprites que usaremos para nuestro ejemplo. Arte de "Tyrian" por Daniel Cook (Lostgarden.com).

    The spritesheet imageHaz clic con el botón derecho para descargar

    Paso 11: El Gestor de Entidades

    La primera técnica de optimización que vamos a aprovechar para conseguir un rendimiento fulgurante es el uso de "object pools". En lugar de asignar constantemente más ram para objetos como balas o enemigos, vamos a hacer un pool de reutilización que reciba los sprites no utilizados una y otra vez.

    Esta técnica garantiza que el uso de la memoria RAM sea muy bajo y que los problemas de recolección de basura rara vez se produzcan. El resultado es que la velocidad de fotogramas será mayor y tu juego funcionará sin problemas, independientemente del tiempo que juegues.

    Crea una nueva clase en tu proyecto llamada EntityManager.as e implementa un sencillo mecanismo de reciclaje bajo demanda como el siguiente.

    1
    // Stage3D Shoot-em-up Tutorial Part 1
    
    
    2
    // by Christer Kaitila - www.mcfunkypants.com
    
    
    3
    4
    // EntityManager.as
    
    
    5
    // The entity manager handles a list of all known game entities.
    
    
    6
    // This object pool will allow for reuse (respawning) of
    
    
    7
    // sprites: for example, when enemy ships are destroyed,
    
    
    8
    // they will be re-spawned when needed as an optimization 
    
    
    9
    // that increases fps and decreases ram use.
    
    
    10
    // This is where you would add all in-game simulation steps,
    
    
    11
    // such as gravity, movement, collision detection and more.
    
    
    12
    13
    package
    
    14
    {
    
    15
      import flash.display.Bitmap;
    
    16
      import flash.display3D.*;
    
    17
      import flash.geom.Point;
    
    18
      import flash.geom.Rectangle;
    
    19
      
    
    20
      public class EntityManager
    
    21
      {
    
    22
        // the sprite sheet image
    
    
    23
        private var _spriteSheet : LiteSpriteSheet;
    
    24
        private const SpritesPerRow:int = 8;
    
    25
        private const SpritesPerCol:int = 8;
    
    26
        [Embed(source="../assets/sprites.png")]
    
    27
        private var SourceImage : Class;
    
    28
        
    
    29
        // a reusable pool of entities
    
    
    30
        private var _entityPool : Vector.<Entity>;
    
    31
        
    
    32
        // all the polygons that make up the scene
    
    
    33
        public var _batch : LiteSpriteBatch;
    
    34
        
    
    35
        // for statistics
    
    
    36
        public var numCreated : int = 0;
    
    37
        public var numReused : int = 0;
    
    38
        
    
    39
        private var maxX:int;
    
    40
        private var minX:int;
    
    41
        private var maxY:int;
    
    42
        private var minY:int;
    
    43
        
    
    44
        public function EntityManager(view:Rectangle)
    
    45
        {
    
    46
          _entityPool = new Vector.<Entity>();
    
    47
          setPosition(view);  
    
    48
        }
    

    Paso 12: Establecer límites

    Nuestro gestor de entidades va a reciclar las entidades cuando se muevan fuera del borde izquierdo de la pantalla. La función de abajo es llamada durante los inits o cuando se dispara el evento de redimensionamiento. Añadimos algunos píxeles adicionales a los bordes para que los sprites no aparezcan o desaparezcan de repente.

    1
    public function setPosition(view:Rectangle):void 
    
    2
    {
    
    3
      // allow moving fully offscreen before looping around
    
    
    4
      maxX = view.width + 32;
    
    5
      minX = view.x - 32;
    
    6
      maxY = view.height;
    
    7
      minY = view.y;
    
    8
    }
    

    Paso 13: Configurar los sprites

    El gestor de entidades ejecuta esta función una vez al inicio. Crea un nuevo lote de geometría utilizando la imagen de la hoja de sprites que fue incrustada en nuestro código anterior. Envía el bitmapData al constructor de la clase spritesheet, que se utilizará para generar una textura que tiene todas las imágenes de sprites disponibles en una cuadrícula. Le decimos a nuestra hoja de sprites que vamos a usar 64 sprites diferentes (8 por 8) en una textura. Esta hoja de sprites será utilizada por el renderizador de geometría por lotes.

    Si quisiéramos, podríamos utilizar más de una hoja de sprites, inicializando imágenes y lotes adicionales según sea necesario. En el futuro, esto podría ser donde se crea un segundo lote para todos los azulejos del terreno que van debajo de los sprites de la nave espacial. Incluso se podría implementar un tercer lote que se superponga a todo para obtener efectos de partículas de fantasía y un toque visual. Por ahora, esta sencilla demostración técnica solo necesita un único lote de texturas y geometría de spritesheet.

    1
        
    
    2
        public function createBatch(context3D:Context3D) : 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, 8, 8);
    
    8
          
    
    9
          // Create new render batch 
    
    
    10
          _batch = new LiteSpriteBatch(context3D, _spriteSheet);
    
    11
          
    
    12
          return _batch;
    
    13
        }
    

    Paso 14: La reserva de objetos

    Aquí es donde el gestor de entidades aumenta el rendimiento. Esta optimización (un pool de reutilización de objetos) nos permitirá crear nuevas entidades solo bajo demanda (cuando no haya ninguna inactiva que pueda ser reutilizada). Observa cómo reutilizamos los sprites que están marcados como inactivos, a menos que estén siendo utilizados, en cuyo caso creamos uno nuevo. De esta manera, nuestro pool de objetos solo contiene tantos sprites como sean visibles al mismo tiempo. Después de los primeros segundos de ejecución de nuestro juego, la reserva de entidades se mantendrá constante, rara vez será necesario crear una nueva entidad una vez que haya suficientes para manejar lo que sucede en la pantalla.

    Continúa añadiendo a EntityManager.as lo siguiente:

    1
        // search the entity pool for unused entities and reuse one
    
    
    2
        // if they are all in use, create a brand new one
    
    
    3
        public function respawn(sprID:uint=0):Entity
    
    4
        {
    
    5
          var currentEntityCount:int = _entityPool.length;
    
    6
          var anEntity:Entity;
    
    7
          var i:int = 0;
    
    8
          // search for an inactive entity
    
    
    9
          for (i = 0; i < currentEntityCount; i++ ) 
    
    10
          {
    
    11
            anEntity = _entityPool[i];
    
    12
            if (!anEntity.active && (anEntity.sprite.spriteId == sprID))
    
    13
            {
    
    14
              //trace('Reusing Entity #' + i);
    
    
    15
              anEntity.active = true;
    
    16
              anEntity.sprite.visible = true;
    
    17
              numReused++;
    
    18
              return anEntity;
    
    19
            }
    
    20
          }
    
    21
          // none were found so we need to make a new one
    
    
    22
          //trace('Need to create a new Entity #' + i);
    
    
    23
          var sprite:LiteSprite;
    
    24
          sprite = _batch.createChild(sprID);
    
    25
          anEntity = new Entity(sprite);
    
    26
          _entityPool.push(anEntity);
    
    27
          numCreated++;
    
    28
          return anEntity;
    
    29
        }
    
    30
        
    
    31
        // for this test, create random entities that move 
    
    
    32
        // from right to left with random speeds and scales
    
    
    33
        public function addEntity():void 
    
    34
        {
    
    35
          var anEntity:Entity;
    
    36
          var randomSpriteID:uint = Math.floor(Math.random() * 64);
    
    37
          // try to reuse an inactive entity (or create a new one)
    
    
    38
          anEntity = respawn(randomSpriteID);
    
    39
          // give it a new position and velocity
    
    
    40
          anEntity.sprite.position.x = maxX;
    
    41
          anEntity.sprite.position.y = Math.random() * maxY;
    
    42
          anEntity.speedX = (-1 * Math.random() * 10) - 2;
    
    43
          anEntity.speedY = (Math.random() * 5) - 2.5;
    
    44
          anEntity.sprite.scaleX = 0.5 + Math.random() * 1.5;
    
    45
          anEntity.sprite.scaleY = anEntity.sprite.scaleX;
    
    46
          anEntity.sprite.rotation = 15 - Math.random() * 30;
    
    47
        }
    

    Las funciones anteriores se ejecutan cada vez que hay que añadir un nuevo sprite en la pantalla. El gestor de entidades escanea la lista de entidades en busca de una que no esté en uso actualmente y la devuelve cuando es posible. Si la lista está llena de entidades activas, hay que crear una nueva.


    Paso 15: ¡Simular!

    La última función que es responsabilidad de nuestro gestor de entidades es la que se llama en cada fotograma. Se utiliza para hacer cualquier simulación, IA, detección de colisiones, física o animación según sea necesario. Para la actual demostración técnica simplista, simplemente recorre la lista de entidades activas en el pool y actualiza sus posiciones en función de la velocidad. Cada entidad se mueve según su velocidad actual. Solo para divertirse, también están configuradas para girar un poco en cada fotograma.

    Cualquier entidad que pase del lado izquierdo de la pantalla se "mata" y se marca como inactiva e invisible, lista para ser reutilizada en las funciones anteriores. Si una entidad toca los otros tres bordes de la pantalla, la velocidad se invierte para que "rebote" en ese borde. Continúa añadiendo a EntityManager.as lo siguiente:

    1
        // called every frame: used to update the simulation
    
    
    2
        // this is where you would perform AI, physics, etc.
    
    
    3
        public function update(currentTime:Number) : void
    
    4
        {   
    
    5
          var anEntity:Entity;
    
    6
          for(var i:int=0; i<_entityPool.length;i++)
    
    7
          {
    
    8
            anEntity = _entityPool[i];
    
    9
            if (anEntity.active)
    
    10
            {
    
    11
              anEntity.sprite.position.x += anEntity.speedX;
    
    12
              anEntity.sprite.position.y += anEntity.speedY;
    
    13
              anEntity.sprite.rotation += 0.1;
    
    14
              
    
    15
              if (anEntity.sprite.position.x > maxX)
    
    16
              {
    
    17
                anEntity.speedX *= -1;
    
    18
                anEntity.sprite.position.x = maxX;
    
    19
              }
    
    20
              else if (anEntity.sprite.position.x < minX)
    
    21
              {
    
    22
                // if we go past the left edge, become inactive
    
    
    23
                // so the sprite can be respawned
    
    
    24
                anEntity.die();
    
    25
              }
    
    26
              if (anEntity.sprite.position.y > maxY)
    
    27
              {
    
    28
                anEntity.speedY *= -1;
    
    29
                anEntity.sprite.position.y = maxY;
    
    30
              }
    
    31
              else if (anEntity.sprite.position.y < minY)
    
    32
              {
    
    33
                anEntity.speedY *= -1;
    
    34
                anEntity.sprite.position.y = minY;
    
    35
              }
    
    36
            }
    
    37
          }
    
    38
        }
    
    39
      } // end class
    
    
    40
    } // end package
    

    Paso 16: La clase Sprite

    El último paso para poner todo en marcha es implementar las cuatro clases que componen nuestro sistema de "motor de renderizado". Debido a que la palabra Sprite ya está en uso en Flash, las próximas clases utilizarán el término LiteSprite, que no es solo un nombre pegadizo sino que implica la naturaleza ligera y simplista de este motor.

    Para empezar, crearemos la clase simple de sprite 2D a la que se refiere nuestra clase de entidad anterior. Habrá muchos sprites en nuestro juego, cada uno de los cuales se recoge en un gran lote de polígonos y se renderiza en una sola pasada.

    Crea un nuevo archivo en tu proyecto llamado LiteSprite.as e implementa algunos getters y setters como sigue. Probablemente podríamos salirnos con la nuestra utilizando simplemente variables públicas, pero en futuras versiones el cambio de algunos de estos valores requerirá la ejecución de algún código primero, por lo que esta técnica resultará inestimable.

    1
    // Stage3D Shoot-em-up Tutorial Part 1
    
    
    2
    // by Christer Kaitila - www.mcfunkypants.com
    
    
    3
    4
    // LiteSprite.as
    
    
    5
    // A 2d sprite that is rendered by Stage3D as a textured quad
    
    
    6
    // (two triangles) to take advantage of hardware acceleration.
    
    
    7
    // Based on example code by Chris Nuuja which is a port
    
    
    8
    // of the haXe+NME bunnymark demo by Philippe Elsass
    
    
    9
    // which is itself a port of Iain Lobb's original work.
    
    
    10
    // Also includes code from the Starling framework.
    
    
    11
    // Grateful acknowledgements to all involved.
    
    
    12
    13
    package
    
    14
    {
    
    15
        import flash.geom.Point;
    
    16
        import flash.geom.Rectangle;
    
    17
        
    
    18
        public class LiteSprite
    
    19
        {
    
    20
            internal var _parent : LiteSpriteBatch;        
    
    21
            internal var _spriteId : uint;
    
    22
            internal var _childId : uint;
    
    23
            private var _pos : Point;
    
    24
            private var _visible : Boolean;
    
    25
            private var _scaleX : Number;
    
    26
            private var _scaleY : Number;
    
    27
            private var _rotation : Number;
    
    28
            private var _alpha : Number;
    
    29
    30
            public function get visible() : Boolean
    
    31
            {
    
    32
                return _visible;
    
    33
            }
    
    34
            public function set visible(isVisible:Boolean) : void
    
    35
            {
    
    36
                _visible = isVisible;
    
    37
            }
    
    38
        public function get alpha() : Number 
    
    39
        {
    
    40
          return _alpha;
    
    41
        }
    
    42
        public function set alpha(a:Number) : void 
    
    43
        {
    
    44
          _alpha = a;
    
    45
        }
    
    46
            public function get position() : Point
    
    47
            {
    
    48
                return _pos;
    
    49
            }
    
    50
            public function set position(pt:Point) : void
    
    51
            {
    
    52
                _pos = pt;
    
    53
            }
    
    54
            public function get scaleX() : Number
    
    55
            {
    
    56
                return _scaleX;
    
    57
            }
    
    58
            public function set scaleX(val:Number) : void
    
    59
            {
    
    60
                _scaleX = val;
    
    61
            }
    
    62
            public function get scaleY() : Number
    
    63
            {
    
    64
                return _scaleY;
    
    65
            }
    
    66
            public function set scaleY(val:Number) : void
    
    67
            {
    
    68
                _scaleY = val;
    
    69
            }
    
    70
            public function get rotation() : Number
    
    71
            {
    
    72
                return _rotation;
    
    73
            }
    
    74
            public function set rotation(val:Number) : void
    
    75
            {
    
    76
                _rotation = val;    
    
    77
            }
    
    78
            public function get rect() : Rectangle
    
    79
            {
    
    80
                return _parent._sprites.getRect(_spriteId);
    
    81
            }
    
    82
            public function get parent() : LiteSpriteBatch
    
    83
            {
    
    84
                return _parent;
    
    85
            }
    
    86
            public function get spriteId() : uint
    
    87
            {
    
    88
                return _spriteId;
    
    89
            }
    
    90
            public function set spriteId(num : uint) : void
    
    91
            {
    
    92
                _spriteId = num;
    
    93
            }
    
    94
            public function get childId() : uint
    
    95
            {
    
    96
                return _childId;
    
    97
            }
    
    98
            
    
    99
            // LiteSprites are typically constructed by calling LiteSpriteBatch.createChild()
    
    
    100
            public function LiteSprite()
    
    101
            {
    
    102
                _parent = null;
    
    103
                _spriteId = 0;
    
    104
                _childId = 0;
    
    105
                _pos = new Point();
    
    106
                _scaleX = 1.0;
    
    107
                _scaleY = 1.0;
    
    108
                _rotation = 0;
    
    109
                _alpha = 1.0;
    
    110
                _visible = true;
    
    111
            }
    
    112
        } // end class
    
    
    113
    } // end package
    

    Cada sprite puede ahora llevar la cuenta de dónde se encuentra en la pantalla, así como su tamaño, su transparencia y el ángulo al que está orientado. La propiedad spriteID es un número que se utiliza durante el renderizado para buscar qué coordenada UV (textura) debe utilizarse como rectángulo de origen para los píxeles de la imagen de la hoja de sprites que utiliza.


    Paso 17: La clase Spritesheet

    Ahora necesitamos implementar un mecanismo para procesar la imagen de la hoja de sprites que hemos incrustado arriba y utilizar partes de ella en toda nuestra geometría renderizada. Crea un nuevo archivo en tu proyecto llamado LiteSpriteSheet.as y comienza importando la funcionalidad requerida, definiendo algunas variables de clase y una función constructora.

    1
    // Stage3D Shoot-em-up Tutorial Part 1
    
    
    2
    // by Christer Kaitila - www.mcfunkypants.com
    
    
    3
    4
    // LiteSpriteSheet.as
    
    
    5
    // An optimization used to improve performance, all sprites used
    
    
    6
    // in the game are packed onto a single texture so that
    
    
    7
    // they can be rendered in a single pass rather than individually.
    
    
    8
    // This also avoids the performance penalty of 3d stage changes.
    
    
    9
    // Based on example code by Chris Nuuja which is a port
    
    
    10
    // of the haXe+NME bunnymark demo by Philippe Elsass
    
    
    11
    // which is itself a port of Iain Lobb's original work.
    
    
    12
    // Also includes code from the Starling framework.
    
    
    13
    // Grateful acknowledgements to all involved.
    
    
    14
    15
    package
    
    16
    {
    
    17
        import flash.display.Bitmap;
    
    18
        import flash.display.BitmapData;
    
    19
        import flash.display.Stage;
    
    20
        import flash.display3D.Context3D;
    
    21
        import flash.display3D.Context3DTextureFormat;
    
    22
        import flash.display3D.IndexBuffer3D;
    
    23
        import flash.display3D.textures.Texture;
    
    24
        import flash.geom.Point;
    
    25
        import flash.geom.Rectangle;
    
    26
        import flash.geom.Matrix;
    
    27
        
    
    28
        public class LiteSpriteSheet
    
    29
        {
    
    30
            internal var _texture : Texture;
    
    31
            
    
    32
            protected var _spriteSheet : BitmapData;    
    
    33
            protected var _uvCoords : Vector.<Number>;
    
    34
            protected var _rects : Vector.<Rectangle>;
    
    35
            
    
    36
            public function LiteSpriteSheet(SpriteSheetBitmapData:BitmapData, numSpritesW:int = 8, numSpritesH:int = 8)
    
    37
            {
    
    38
                _uvCoords = new Vector.<Number>();
    
    39
                _rects = new Vector.<Rectangle>();
    
    40
          _spriteSheet = SpriteSheetBitmapData;
    
    41
          createUVs(numSpritesW, numSpritesH);
    
    42
        }
    

    El constructor de la clase anterior recibe un BitmapData para nuestra hoja de sprites, así como el número de sprites que hay en ella (en esta demo, 64).


    Paso 18: Picarla

    Debido a que estamos utilizando una sola textura para almacenar todas las imágenes de los sprites, necesitamos dividir la imagen en varias partes (una para cada sprite en ella) al renderizar. Esto lo hacemos asignando coordenadas diferentes para cada vértice (esquina) de cada malla quad utilizada para dibujar un sprite.

    Estas coordenadas se denominan UVs, y cada una va de 0 a 1 y representa en qué parte de la textura stage3D debe empezar a muestrear los píxeles al renderizar. Las coordenadas UV y los rectángulos de píxeles se almacenan en una matriz para su posterior uso durante el renderizado, de modo que no tengamos que calcularlos en cada fotograma. También almacenamos el tamaño y la forma de cada sprite (que en esta demo son todos idénticos) para que cuando rotemos un sprite sepamos su radio (que se utiliza para mantener el pivote en el mismo centro del sprite).

    1
            // generate a list of uv coordinates for a grid of sprites
    
    
    2
        // on the spritesheet texture for later reference by ID number
    
    
    3
        // sprite ID numbers go from left to right then down
    
    
    4
        public function createUVs(numSpritesW:int, numSpritesH:int) : void
    
    5
        {
    
    6
          trace('creating a '+_spriteSheet.width+'x'+_spriteSheet.height+
    
    7
            ' spritesheet texture with '+numSpritesW+'x'+ numSpritesH+' sprites.');
    
    8
      
    
    9
          var destRect : Rectangle;
    
    10
      
    
    11
          for (var y:int = 0; y < numSpritesH; y++)
    
    12
          {
    
    13
            for (var x:int = 0; x < numSpritesW; x++)
    
    14
            {
    
    15
              _uvCoords.push(
    
    16
                // bl, tl, tr, br 
    
    
    17
                x / numSpritesW, (y+1) / numSpritesH,
    
    18
                x / numSpritesW, y / numSpritesH,
    
    19
                (x+1) / numSpritesW, y / numSpritesH,
    
    20
                (x + 1) / numSpritesW, (y + 1) / numSpritesH);
    
    21
                
    
    22
                  destRect = new Rectangle();
    
    23
                destRect.left = 0;
    
    24
                destRect.top = 0;
    
    25
                destRect.right = _spriteSheet.width / numSpritesW;
    
    26
                destRect.bottom = _spriteSheet.height / numSpritesH;
    
    27
                _rects.push(destRect);          
    
    28
            }
    
    29
          }
    
    30
            }
    
    31
    32
            public function removeSprite(spriteId:uint) : void
    
    33
            {
    
    34
                if ( spriteId < _uvCoords.length ) {
    
    35
                    _uvCoords = _uvCoords.splice(spriteId * 8, 8);
    
    36
                    _rects.splice(spriteId, 1);
    
    37
                }
    
    38
            }
    
    39
    40
            public function get numSprites() : uint
    
    41
            {
    
    42
                return _rects.length;
    
    43
            }
    
    44
    45
            public function getRect(spriteId:uint) : Rectangle
    
    46
            {
    
    47
                return _rects[spriteId];
    
    48
            }
    
    49
            
    
    50
            public function getUVCoords(spriteId:uint) : Vector.<Number>
    
    51
            {
    
    52
                var startIdx:uint = spriteId * 8;
    
    53
                return _uvCoords.slice(startIdx, startIdx + 8);
    
    54
            }
    

    Paso 19: Generar Mipmaps

    Ahora necesitamos procesar esta imagen durante el init. Vamos a cargarla para que sea utilizada como textura por la GPU. Mientras lo hacemos, vamos a crear copias más pequeñas que se llaman "mipmaps". El hardware 3D utiliza el mapeo de mip para acelerar el renderizado utilizando versiones más pequeñas de la misma textura cuando se ve desde lejos (a escala) o, en los verdaderos juegos 3D, cuando se ve en un ángulo oblicuo. Esto evita los efectos de "moiree" (parpadeos) que pueden ocurrir si no se utiliza el mipmapping. Cada mipmap es la mitad de ancho y alto que el anterior.

    Continuando con LiteSpriteSheet.as, vamos a implementar la rutina que necesitamos para generar mipmaps y cargarlos todos en la GPU de tu tarjeta de vídeo.

    1
            public function uploadTexture(context3D:Context3D) : void
    
    2
            {
    
    3
                if ( _texture == null ) {
    
    4
                    _texture = context3D.createTexture(_spriteSheet.width, _spriteSheet.height, Context3DTextureFormat.BGRA, false);
    
    5
                }
    
    6
     
    
    7
                _texture.uploadFromBitmapData(_spriteSheet);
    
    8
                
    
    9
                // generate mipmaps
    
    
    10
                var currentWidth:int = _spriteSheet.width >> 1;
    
    11
                var currentHeight:int = _spriteSheet.height >> 1;
    
    12
                var level:int = 1;
    
    13
                var canvas:BitmapData = new BitmapData(currentWidth, currentHeight, true, 0);
    
    14
                var transform:Matrix = new Matrix(.5, 0, 0, .5);
    
    15
                while ( currentWidth >= 1 || currentHeight >= 1 ) {
    
    16
                    canvas.fillRect(new Rectangle(0, 0, Math.max(currentWidth,1), Math.max(currentHeight,1)), 0);
    
    17
                    canvas.draw(_spriteSheet, transform, null, null, null, true);
    
    18
                    _texture.uploadFromBitmapData(canvas, level++);
    
    19
                    transform.scale(0.5, 0.5);
    
    20
                    currentWidth = currentWidth >> 1;
    
    21
                    currentHeight = currentHeight >> 1;
    
    22
                }
    
    23
            }
    
    24
        } // end class
    
    
    25
    } // end package
    

    Paso 20: Geometría de lotes

    La última optimización de hardcore que vamos a implementar es un sistema de renderizado de geometría por lotes. Esta técnica de "geometría por lotes" se utiliza a menudo en los sistemas de partículas. La última optimización de hardcore que vamos a implementar es un sistema de renderizado de geometría por lotes. Esta técnica de "geometría por lotes" se utiliza a menudo en los sistemas de partículas.

    Para minimizar el número de llamadas de dibujo y renderizar todo de una sola vez, vamos a agrupar todos los sprites del juego en una larga lista de coordenadas (x,y). Esencialmente, el lote de geometría es tratado por tu hardware de vídeo como una única malla 3D. Luego, una vez por fotograma, subiremos todo el buffer a Stage3D en una sola llamada a la función. Hacer las cosas de esta manera es mucho más rápido que subir las coordenadas individuales de cada sprite por separado.

    Crea un nuevo archivo en tu proyecto llamado LiteSpriteBatch.as y comienza incluyendo todas las importaciones para la funcionalidad que necesitará, las variables de clase que utilizará y el constructor como sigue:

    1
    // Stage3D Shoot-em-up Tutorial Part 1
    
    
    2
    // by Christer Kaitila - www.mcfunkypants.com
    
    
    3
    4
    // LiteSpriteBatch.as
    
    
    5
    // An optimization used to increase performance that renders multiple
    
    
    6
    // sprites in a single pass by grouping all polygons together,
    
    
    7
    // allowing stage3D to treat it as a single mesh that can be
    
    
    8
    // rendered in a single drawTriangles call. 
    
    
    9
    // Each frame, the positions of each
    
    
    10
    // vertex is updated and re-uploaded to video ram.
    
    
    11
    // Based on example code by Chris Nuuja which is a port
    
    
    12
    // of the haXe+NME bunnymark demo by Philippe Elsass
    
    
    13
    // which is itself a port of Iain Lobb's original work.
    
    
    14
    // Also includes code from the Starling framework.
    
    
    15
    // Grateful acknowledgements to all involved.
    
    
    16
    17
    package
    
    18
    {
    
    19
        import com.adobe.utils.AGALMiniAssembler;
    
    20
        
    
    21
        import flash.display.BitmapData;
    
    22
        import flash.display3D.Context3D;
    
    23
        import flash.display3D.Context3DBlendFactor;
    
    24
        import flash.display3D.Context3DCompareMode;
    
    25
        import flash.display3D.Context3DProgramType;
    
    26
        import flash.display3D.Context3DTextureFormat;
    
    27
        import flash.display3D.Context3DVertexBufferFormat;
    
    28
        import flash.display3D.IndexBuffer3D;
    
    29
        import flash.display3D.Program3D;
    
    30
        import flash.display3D.VertexBuffer3D;
    
    31
        import flash.display3D.textures.Texture;
    
    32
        import flash.geom.Matrix;
    
    33
        import flash.geom.Matrix3D;
    
    34
        import flash.geom.Point;
    
    35
        import flash.geom.Rectangle;
    
    36
        
    
    37
        public class LiteSpriteBatch
    
    38
        {
    
    39
            internal var _sprites : LiteSpriteSheet;        
    
    40
            internal var _verteces : Vector.<Number>;
    
    41
            internal var _indeces : Vector.<uint>;
    
    42
            internal var _uvs : Vector.<Number>;
    
    43
            
    
    44
            protected var _context3D : Context3D;
    
    45
            protected var _parent : LiteSpriteStage;
    
    46
            protected var _children : Vector.<LiteSprite>;
    
    47
    48
            protected var _indexBuffer : IndexBuffer3D;
    
    49
            protected var _vertexBuffer : VertexBuffer3D;
    
    50
            protected var _uvBuffer : VertexBuffer3D;
    
    51
            protected var _shader : Program3D;
    
    52
            protected var _updateVBOs : Boolean;
    
    53
    54
    55
            public function LiteSpriteBatch(context3D:Context3D, spriteSheet:LiteSpriteSheet)
    
    56
            {
    
    57
                _context3D = context3D;
    
    58
                _sprites = spriteSheet;
    
    59
                
    
    60
                _verteces = new Vector.<Number>();
    
    61
                _indeces = new Vector.<uint>();
    
    62
                _uvs = new Vector.<Number>();
    
    63
                
    
    64
                _children = new Vector.<LiteSprite>;
    
    65
                _updateVBOs = true;
    
    66
                setupShaders();
    
    67
                updateTexture();  
    
    68
            }
    

    Paso 21: Lote de padres e hijos

    Continúa implementando getters y setters y la funcionalidad para manejar la adición de cualquier nuevo sprites al lote. El padre se refiere al objeto sprite stage utilizado por nuestro motor de juego, mientras que los hijos son todos los sprites de este lote de renderizado. Cuando añadimos un sprite hijo, añadimos más datos a la lista de vértices (que proporciona las ubicaciones en pantalla de ese sprite en particular) así como las coordenadas UV (la ubicación en la textura de la hoja de sprites en la que se almacena este sprite en particular). Cuando se añade o se elimina un sprite hijo del lote, establecemos una variable booleana para indicar a nuestro sistema de lotes que los buffers deben volver a cargarse ahora que han cambiado.

    1
            public function get parent() : LiteSpriteStage
    
    2
            {
    
    3
                return _parent;
    
    4
            }
    
    5
            
    
    6
            public function set parent(parentStage:LiteSpriteStage) : void
    
    7
            {
    
    8
                _parent = parentStage;
    
    9
            }
    
    10
            
    
    11
            public function get numChildren() : uint
    
    12
            {
    
    13
                return _children.length;
    
    14
            }
    
    15
            
    
    16
            // Constructs a new child sprite and attaches it to the batch
    
    
    17
            public function createChild(spriteId:uint) : LiteSprite
    
    18
            {
    
    19
                var sprite : LiteSprite = new LiteSprite();
    
    20
                addChild(sprite, spriteId);
    
    21
                return sprite;
    
    22
            }
    
    23
            
    
    24
            public function addChild(sprite:LiteSprite, spriteId:uint) : void
    
    25
            {
    
    26
                sprite._parent = this;
    
    27
                sprite._spriteId = spriteId;
    
    28
                
    
    29
                // Add to list of children
    
    
    30
                sprite._childId = _children.length;
    
    31
                _children.push(sprite);
    
    32
    33
                // Add vertex data required to draw child
    
    
    34
                var childVertexFirstIndex:uint = (sprite._childId * 12) / 3; 
    
    35
                _verteces.push(0, 0, 1, 0, 0,1, 0, 0,1, 0, 0,1); // placeholders
    
    
    36
                _indeces.push(childVertexFirstIndex, childVertexFirstIndex+1, childVertexFirstIndex+2, 
    
    37
              childVertexFirstIndex, childVertexFirstIndex+2, childVertexFirstIndex+3);
    
    38
    39
                var childUVCoords:Vector.<Number> = _sprites.getUVCoords(spriteId); 
    
    40
                _uvs.push(
    
    41
                    childUVCoords[0], childUVCoords[1], 
    
    42
                    childUVCoords[2], childUVCoords[3],
    
    43
                    childUVCoords[4], childUVCoords[5],
    
    44
                    childUVCoords[6], childUVCoords[7]);
    
    45
                
    
    46
                _updateVBOs = true;
    
    47
            }
    
    48
            
    
    49
            public function removeChild(child:LiteSprite) : void
    
    50
            {
    
    51
                var childId:uint = child._childId;
    
    52
                if ( (child._parent == this) && childId < _children.length ) {
    
    53
                    child._parent = null;
    
    54
                    _children.splice(childId, 1);
    
    55
                    
    
    56
                    // Update child id (index into array of children) for remaining children
    
    
    57
                    var idx:uint;
    
    58
                    for ( idx = childId; idx < _children.length; idx++ ) {
    
    59
                        _children[idx]._childId = idx;
    
    60
                    }
    
    61
                    
    
    62
                    // Realign vertex data with updated list of children
    
    
    63
                    var vertexIdx:uint = childId * 12;
    
    64
                    var indexIdx:uint= childId * 6;
    
    65
                    _verteces.splice(vertexIdx, 12);
    
    66
                    _indeces.splice(indexIdx, 6);
    
    67
                    _uvs.splice(vertexIdx, 8);
    
    68
                    
    
    69
                    _updateVBOs = true;
    
    70
                }
    
    71
            }
    

    Paso 22: Configurar el sombreado

    Un sombreador es un conjunto de comandos que se cargan directamente en la tarjeta de vídeo para conseguir una renderización extremadamente rápida. En Flash 11 Stage3D, se escriben en una especie de lenguaje ensamblador llamado AGAL. Este sombreador solo necesita ser creado una vez, al inicio. No es necesario que entiendas los opcodes del lenguaje ensamblador para este tutorial. En su lugar, simplemente implementa la creación de un programa de vértices (que calcula las ubicaciones de tus sprites en la pantalla) y un programa de fragmentos (que calcula el color de cada píxel) como sigue.

    1
            protected function setupShaders() : void
    
    2
            {
    
    3
                var vertexShaderAssembler:AGALMiniAssembler = new AGALMiniAssembler();
    
    4
                vertexShaderAssembler.assemble( Context3DProgramType.VERTEX,
    
    5
                    "dp4 op.x, va0, vc0 \n"+ // transform from stream 0 to output clipspace
    
    
    6
                    "dp4 op.y, va0, vc1 \n"+ // do the same for the y coordinate
    
    
    7
                    "mov op.z, vc2.z    \n"+ // we don't need to change the z coordinate
    
    
    8
                    "mov op.w, vc3.w    \n"+ // unused, but we need to output all data
    
    
    9
                    "mov v0, va1.xy     \n"+ // copy UV coords from stream 1 to fragment program
    
    
    10
                    "mov v0.z, va0.z    \n"  // copy alpha from stream 0 to fragment program
    
    
    11
                );
    
    12
          
    
    13
                var fragmentShaderAssembler:AGALMiniAssembler = new AGALMiniAssembler();
    
    14
                fragmentShaderAssembler.assemble( Context3DProgramType.FRAGMENT,
    
    15
                    "tex ft0, v0, fs0 <2d,clamp,linear,mipnearest> \n"+ // sample the texture
    
    
    16
                    "mul ft0, ft0, v0.zzzz\n" + // multiply by the alpha transparency
    
    
    17
                    "mov oc, ft0 \n" // output the final pixel color 
    
    
    18
                );
    
    19
                
    
    20
                _shader = _context3D.createProgram();
    
    21
                _shader.upload( vertexShaderAssembler.agalcode, fragmentShaderAssembler.agalcode );
    
    22
            }
    
    23
            
    
    24
            protected function updateTexture() : void
    
    25
            {
    
    26
                _sprites.uploadTexture(_context3D);    
    
    27
            }
    

    Paso 23: Mover los Sprites

    Justo antes de ser renderizado, las coordenadas de los vértices de cada sprite en la pantalla probablemente habrán cambiado a medida que el sprite se mueve o gira. La siguiente función calcula dónde debe estar cada vértice (esquina de la geometría). Como cada quad (el cuadrado que compone un sprite) tiene cuatro vértices cada uno, y cada vértice necesita una coordenada x, y y z, hay doce valores que actualizar. Como una pequeña optimización, si el sprite no es visible simplemente escribimos ceros en nuestro buffer de vértices para evitar hacer cálculos innecesarios.

    1
            protected function updateChildVertexData(sprite:LiteSprite) : void
    
    2
            {
    
    3
                var childVertexIdx:uint = sprite._childId * 12;
    
    4
    5
                if ( sprite.visible ) {
    
    6
                    var x:Number = sprite.position.x;
    
    7
                    var y:Number = sprite.position.y;
    
    8
                    var rect:Rectangle = sprite.rect;
    
    9
                    var sinT:Number = Math.sin(sprite.rotation);
    
    10
                    var cosT:Number = Math.cos(sprite.rotation);
    
    11
            var alpha:Number = sprite.alpha;
    
    12
                    
    
    13
                    var scaledWidth:Number = rect.width * sprite.scaleX;
    
    14
                    var scaledHeight:Number = rect.height * sprite.scaleY;
    
    15
                    var centerX:Number = scaledWidth * 0.5;
    
    16
                    var centerY:Number = scaledHeight * 0.5;
    
    17
                    
    
    18
                    _verteces[childVertexIdx] = x - (cosT * centerX) - (sinT * (scaledHeight - centerY));
    
    19
                    _verteces[childVertexIdx+1] = y - (sinT * centerX) + (cosT * (scaledHeight - centerY));
    
    20
            _verteces[childVertexIdx+2] = alpha;
    
    21
            
    
    22
                    _verteces[childVertexIdx+3] = x - (cosT * centerX) + (sinT * centerY);
    
    23
                    _verteces[childVertexIdx+4] = y - (sinT * centerX) - (cosT * centerY);
    
    24
            _verteces[childVertexIdx+5] = alpha;
    
    25
            
    
    26
                    _verteces[childVertexIdx+6] = x + (cosT * (scaledWidth - centerX)) + (sinT * centerY);
    
    27
                    _verteces[childVertexIdx+7] = y + (sinT * (scaledWidth - centerX)) - (cosT * centerY);
    
    28
            _verteces[childVertexIdx+8] = alpha;
    
    29
            
    
    30
                    _verteces[childVertexIdx+9] = x + (cosT * (scaledWidth - centerX)) - (sinT * (scaledHeight - centerY));
    
    31
                    _verteces[childVertexIdx+10] = y + (sinT * (scaledWidth - centerX)) + (cosT * (scaledHeight - centerY));
    
    32
            _verteces[childVertexIdx+11] = alpha;
    
    33
            
    
    34
                }
    
    35
                else {
    
    36
                    for (var i:uint = 0; i < 12; i++ ) {
    
    37
                        _verteces[childVertexIdx+i] = 0;
    
    38
                    }
    
    39
                }
    
    40
            }
    

    Paso 24: Dibuja la geometría

    Por último, continúa añadiendo a la clase LiteSpriteBatch.as implementando la función de dibujo. Aquí es donde le decimos a stage3D que renderice todos los sprites en una sola pasada. En primer lugar, hacemos un bucle a través de todos los hijos conocidos (los sprites individuales) y actualizamos las posiciones del vértice en función de dónde están en la pantalla. A continuación, le decimos a stage3D qué sombreador y textura debe utilizar, así como establecer los factores de mezcla para la representación.

    ¿Qué es un factor de mezcla? Define si debemos utilizar o no la transparencia, y cómo tratar los píxeles transparentes en nuestra textura. Podrías cambiar las opciones en la llamada setBlendFactors para usar blanding aditivo, por ejemplo, lo que se ve muy bien para efectos de partículas como explosiones, ya que los píxeles aumentarán el brillo en la pantalla a medida que se superponen. En el caso de los sprites normales, todo lo que queremos es dibujarlos con el color exacto almacenado en la textura de nuestra hoja de sprites y permitir regiones transparentes.

    El paso final de nuestra función de dibujo es actualizar los buffers de UV e índice si el lote ha cambiado de tamaño, y cargar siempre los datos de vértices porque se espera que nuestros sprites estén en constante movimiento. Le decimos a stage3D qué búferes debe utilizar y, por último, renderizamos toda la gigantesca lista de geometría como si fuera una única malla 3D, para que se dibuje utilizando una única y rápida llamada a drawTriangles.

    1
            
    
    2
            public function draw() : void
    
    3
            {
    
    4
                var nChildren:uint = _children.length;
    
    5
                if ( nChildren == 0 ) return;
    
    6
                
    
    7
                // Update vertex data with current position of children
    
    
    8
                for ( var i:uint = 0; i < nChildren; i++ ) {
    
    9
                    updateChildVertexData(_children[i]);
    
    10
                }
    
    11
                
    
    12
                _context3D.setProgram(_shader);
    
    13
                _context3D.setBlendFactors(Context3DBlendFactor.ONE, 
    
    14
              Context3DBlendFactor.ONE_MINUS_SOURCE_ALPHA);            
    
    15
                _context3D.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 
    
    16
              0, _parent.modelViewMatrix, true); 
    
    17
                _context3D.setTextureAt(0, _sprites._texture);
    
    18
                
    
    19
                if ( _updateVBOs ) {
    
    20
            _vertexBuffer = _context3D.createVertexBuffer(_verteces.length/3, 3);   
    
    21
            _indexBuffer = _context3D.createIndexBuffer(_indeces.length);
    
    22
            _uvBuffer = _context3D.createVertexBuffer(_uvs.length/2, 2);
    
    23
            _indexBuffer.uploadFromVector(_indeces, 0, _indeces.length); // indices won't change                
    
    
    24
            _uvBuffer.uploadFromVector(_uvs, 0, _uvs.length / 2); // child UVs won't change
    
    
    25
            _updateVBOs = false;
    
    26
          }
    
    27
          
    
    28
                // we want to upload the vertex data every frame
    
    
    29
          _vertexBuffer.uploadFromVector(_verteces, 0, _verteces.length / 3);
    
    30
                _context3D.setVertexBufferAt(0, _vertexBuffer, 0, Context3DVertexBufferFormat.FLOAT_3);
    
    31
                _context3D.setVertexBufferAt(1, _uvBuffer, 0, Context3DVertexBufferFormat.FLOAT_2);
    
    32
                
    
    33
                _context3D.drawTriangles(_indexBuffer, 0,  nChildren * 2);
    
    34
            }
    
    35
        } // end class
    
    
    36
    } // end package
    

    Paso 25: La clase de escenario Sprite

    La última clase requerida por nuestro elegante (y veloz) motor de renderizado de sprites acelerado por hardware es la clase sprite stage. Esta etapa, al igual que la etapa tradicional de Flash, contiene una lista de todos los lotes que se utilizan para tu juego. En esta primera demostración, nuestro escenario sólo utilizará un único lote de sprites, que a su vez solo utiliza una única hoja de sprites.

    Crea un último archivo en tu proyecto llamado LiteSpriteStage.as y comienza creando la clase como sigue:

    1
    // Stage3D Shoot-em-up Tutorial Part 1
    
    
    2
    // by Christer Kaitila - www.mcfunkypants.com
    
    
    3
    4
    // LiteSpriteStage.as
    
    
    5
    // The stage3D renderer of any number of batched geometry
    
    
    6
    // meshes of multiple sprites. Handles stage3D inits, etc.
    
    
    7
    // Based on example code by Chris Nuuja which is a port
    
    
    8
    // of the haXe+NME bunnymark demo by Philippe Elsass
    
    
    9
    // which is itself a port of Iain Lobb's original work.
    
    
    10
    // Also includes code from the Starling framework.
    
    
    11
    // Grateful acknowledgements to all involved.
    
    
    12
    13
    package
    
    14
    {
    
    15
        import flash.display.Stage3D;
    
    16
        import flash.display3D.Context3D;
    
    17
        import flash.geom.Matrix3D;
    
    18
        import flash.geom.Rectangle;
    
    19
      
    
    20
        public class LiteSpriteStage
    
    21
        {
    
    22
            protected var _stage3D : Stage3D;
    
    23
            protected var _context3D : Context3D;        
    
    24
            protected var _rect : Rectangle;
    
    25
            protected var _batches : Vector.<LiteSpriteBatch>;
    
    26
            protected var _modelViewMatrix : Matrix3D;
    
    27
            
    
    28
            public function LiteSpriteStage(stage3D:Stage3D, context3D:Context3D, rect:Rectangle)
    
    29
            {
    
    30
                _stage3D = stage3D;
    
    31
                _context3D = context3D;
    
    32
                _batches = new Vector.<LiteSpriteBatch>;
    
    33
                
    
    34
                this.position = rect;
    
    35
            }
    

    Paso 26: La matriz de la cámara

    Para saber exactamente en qué lugar de la pantalla debe ir cada sprite, haremos un seguimiento de la ubicación y el tamaño de la ventana de renderizado. Durante las inicializaciones de nuestro juego (o si cambia) creamos una matriz de vista del modelo que es utilizada por Stage3D para transformar las coordenadas 3D internas de nuestros lotes de geometría a las ubicaciones adecuadas en pantalla.

    1
            
    
    2
            public function get position() : Rectangle
    
    3
            {
    
    4
                return _rect;
    
    5
            }
    
    6
            
    
    7
            public function set position(rect:Rectangle) : void
    
    8
            {
    
    9
                _rect = rect;
    
    10
                _stage3D.x = rect.x;
    
    11
                _stage3D.y = rect.y;
    
    12
                configureBackBuffer(rect.width, rect.height);
    
    13
                
    
    14
                _modelViewMatrix = new Matrix3D();
    
    15
                _modelViewMatrix.appendTranslation(-rect.width/2, -rect.height/2, 0);            
    
    16
                _modelViewMatrix.appendScale(2.0/rect.width, -2.0/rect.height, 1);
    
    17
            }
    
    18
            
    
    19
            internal function get modelViewMatrix() : Matrix3D
    
    20
            {
    
    21
                return _modelViewMatrix;
    
    22
            }
    
    23
            
    
    24
            public function configureBackBuffer(width:uint, height:uint) : void
    
    25
            {
    
    26
                 _context3D.configureBackBuffer(width, height, 0, false);
    
    27
            }
    

    Paso 27: Manejar los lotes

    El paso final en la creación de nuestra demo del juego Stage3D es manejar la adición y eliminación de lotes de geometría, así como un bucle que llama a la función de dibujo en cada lote. De esta manera, cuando el evento principal ENTER_FRAME de nuestro juego se dispara, moverá los sprites por la pantalla a través del gestor de entidades y luego le dirá al sistema de escenarios de sprites que se dibuje a sí mismo, que a su vez le dice a todos los lotes conocidos que se dibujen.

    Debido a que esta es una demo muy optimizada, solo habrá un lote en uso, pero esto cambiará en futuros tutoriales a medida que vayamos añadiendo más caramelos para la vista.

    1
        public function addBatch(batch:LiteSpriteBatch) : void
    
    2
            {
    
    3
                batch.parent = this;
    
    4
                _batches.push(batch);
    
    5
            }
    
    6
            
    
    7
            public function removeBatch(batch:LiteSpriteBatch) : void
    
    8
            {
    
    9
                for ( var i:uint = 0; i < _batches.length; i++ ) {
    
    10
                    if ( _batches[i] == batch ) {
    
    11
                        batch.parent = null;
    
    12
                        _batches.splice(i, 1);
    
    13
                    }
    
    14
                }
    
    15
            }
    
    16
            
    
    17
        // loop through all batches 
    
    
    18
        // (this demo uses only one)
    
    
    19
        // and tell them to draw themselves
    
    
    20
        public function render() : void
    
    21
        {
    
    22
          for ( var i:uint = 0; i < _batches.length; i++ ) {
    
    23
            _batches[i].draw();       
    
    24
          }
    
    25
        }
    
    26
        } // end class
    
    
    27
    } // end package
    

    Paso 28: ¡Compilar y ejecutar!

    ¡Ya casi hemos terminado! Compila tu SWF, corrige cualquier error tipográfico y comprueba la bondad gráfica. Deberías tener una demo con este aspecto:

    Screenshot of our final .SWF in action. Sprite-o-licious!Screenshot of our final .SWF in action. Sprite-o-licious!Screenshot of our final .SWF in action. Sprite-o-licious!

    Si tienes dificultades para compilar, ten en cuenta que este proyecto necesita una clase hecha por Adobe que se encarga de la compilación de los shaders AGAL, que se incluye en la descarga del archivo .zip del código fuente.

    Solo como referencia, y para asegurarte de que has utilizado los nombres de archivo y las ubicaciones correctas para todo, este es el aspecto que debería tener tu proyecto de FlashDevelop:

    The project window - what each file is named.

    Tutorial completo: Eres increíble

    Eso es todo para el primer tutorial de esta serie. La semana que viene podrás ver cómo el juego evoluciona poco a poco hasta convertirse en un juego de disparos a 60 FPS de gran aspecto y fluidez. En la siguiente parte, implementaremos los controles del jugador (usando el teclado para moverse) y añadiremos algo de movimiento, sonidos y música al juego.

    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 o mi blog mcfunkypants.com o en Google+ en cualquier momento. Siempre estoy buscando nuevos temas para escribir futuros tutoriales, así que no dudes en solicitar uno. Por último, ¡me encantaría ver los juegos que hagas con este código!

    Gracias por leer. Nos vemos la semana que viene. Buena suerte y ¡diviértete!

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.