Build a Stage3D Shoot-'Em-Up: Sprite Test

In this tutorial series (part free, part Premium) we'll create a high-performance 2D shoot-em-up using the new hardware-accelerated Stage3D
rendering engine. We will be taking advantage of several hardcore optimization techniques to achieve great 2D sprite rendering performance. In this part, we'll build a high-performance demo that draws hundreds of moving sprites on-screen at once.
Final Result Preview
Let's take a look at the final result we will be working towards: a high-performance 2D sprite demo that uses Stage3D with optimizations that include a spritesheet and object pooling.
Introduction: Flash 11 Stage3D
If you're hoping to take your Flash games to the next level and are looking for loads of eye-candy and amazing framerate, Stage3D is going to be your new best friend.
The incredible speed of the new Flash 11 hardware accelerated Stage3D API is just begging to be used for 2D games. Instead of using old-fashioned Flash sprites on the DisplayList or last-gen blitting techniques as popularized by engines such as FlashPunk and Flixel, the new breed of 2D games uses the power of your video card's GPU to blaze through rendering tasks at up to 1000x the speed of anything Flash 10 could manage.
Although it has 3D in its name, this new API is also great for 2D games. We can render simple geometry in the form of 2D squares (called quads) and draw them on a flat plane. This will enable us to render tons of sprites on screen at a silky-smooth 60fps.
We'll make a side-scrolling shooter inspired by retro arcade titles such as R-Type or Gradius in ActionScript using Flash 11's Stage3D API. It isn't half as hard as some people say it is, and you won't need to learn assembly language AGAL opcodes.
In this 6-part tutorial series, we are going to program a simple 2D shoot-'em-up that delivers mind-blowing rendering performance. We are going to build it using pure AS3, compiled in FlashDevelop (read more about it here). FlashDevelop is great because it is 100% freeware - no need to buy any expensive tools to get the best AS3 IDE around.
Step 1: Create a New Project
If you don't already have it, be sure to download and install FlashDevelop. Once you're all set up (and you've allowed it to install the latest version of the Flex compiler automatically), fire it up and start a new "AS3 Project."



FlashDevelop will create a blank template project for you. We're going to fill in the blanks, piece-by-piece, until we have created a decent game.
Step 2: Target Flash 11
Go into the project menu and change a few options:
- Target Flash 11.1
- Change the size to 600x400px
- Change the background color to black
- Change the FPS to 60
- Change the SWF filename to a name of your choosing



Step 3: Imports
Now that our blank project is set up, let's dive in and do some coding. To begin with, we will need to import all the Stage3D functionality required. Add the following to the very top of your Main.as
file.
1 |
|
2 |
// Stage3D Shoot-em-up Tutorial Part 1
|
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.geom.Rectangle; |
18 |
import flash.utils.getTimer; |
Step 4: Initialize Stage3D
The next step is to wait for our game to appear on the Flash stage. Doing things this way allows for the future use of a preloader. For simplicity, we will be doing most of our game in a single little class that inherits from the Flash Sprite class as follows.
1 |
|
2 |
public class Main extends Sprite |
3 |
{
|
4 |
private var _entities : EntityManager; |
5 |
private var _spriteStage : LiteSpriteStage; |
6 |
private var _gui : GameGUI; |
7 |
private var _width : Number = 600; |
8 |
private var _height : Number = 400; |
9 |
public var context3D : Context3D; |
10 |
|
11 |
// constructor function for our game
|
12 |
public function Main():void |
13 |
{
|
14 |
if (stage) init(); |
15 |
else addEventListener(Event.ADDED_TO_STAGE, init); |
16 |
}
|
17 |
|
18 |
// called once Flash is ready
|
19 |
private function init(e:Event = null):void |
20 |
{
|
21 |
removeEventListener(Event.ADDED_TO_STAGE, init); |
22 |
stage.quality = StageQuality.LOW; |
23 |
stage.align = StageAlign.TOP_LEFT; |
24 |
stage.scaleMode = StageScaleMode.NO_SCALE; |
25 |
stage.addEventListener(Event.RESIZE, onResizeEvent); |
26 |
trace("Init Stage3D..."); |
27 |
_gui = new GameGUI("Simple Stage3D Sprite Demo v1"); |
28 |
addChild(_gui); |
29 |
stage.stage3Ds[0].addEventListener(Event.CONTEXT3D_CREATE, onContext3DCreate); |
30 |
stage.stage3Ds[0].addEventListener(ErrorEvent.ERROR, errorHandler); |
31 |
stage.stage3Ds[0].requestContext3D(Context3DRenderMode.AUTO); |
32 |
trace("Stage3D requested..."); |
33 |
}
|
After setting some stage-specific properties, we request a Stage3D context. This can take a while (a fraction of a second) as your video card is configured for hardware rendering, so we need to wait for the onContext3DCreate
event.
We also want to detect any errors that may occur, especially since Stage3D content does not run if the HTML embed code that loads your SWF doesn't include the parameter "wmode=direct"
. These errors can also happen if the user is running an old version of Flash or if they don't have a video card capable of handling pixel shader 2.0.
Step 5: Handle Any Events
Add the following functions that detect any events that might be triggered as specified above. In the case of errors due to running old Flash plugins, in future versions of this game we might want to output a message and remind the user to upgrade, but for now this error is simply ignored.
For users with old video cards (or drivers) that don't support shader model 2.0, the good news is that Flash 11 is smart enough to provide a software renderer. It doesn't run very fast but at least everyone will be able to play your game. Those with decent gaming rigs will get fantastic framerate like you've never seen in a Flash game before.
1 |
|
2 |
// this is called when the 3d card has been set up
|
3 |
// and is ready for rendering using stage3d
|
4 |
private function onContext3DCreate(e:Event):void |
5 |
{
|
6 |
trace("Stage3D context created! Init sprite engine..."); |
7 |
context3D = stage.stage3Ds[0].context3D; |
8 |
initSpriteEngine(); |
9 |
}
|
10 |
|
11 |
// this can be called when using an old version of Flash
|
12 |
// or if the html does not include wmode=direct
|
13 |
private function errorHandler(e:ErrorEvent):void |
14 |
{
|
15 |
trace("Error while setting up Stage3D: "+e.errorID+" - " +e.text); |
16 |
}
|
17 |
|
18 |
protected function onResizeEvent(event:Event) : void |
19 |
{
|
20 |
trace("resize event..."); |
21 |
|
22 |
// Set correct dimensions if we resize
|
23 |
_width = stage.stageWidth; |
24 |
_height = stage.stageHeight; |
25 |
|
26 |
// Resize Stage3D to continue to fit screen
|
27 |
var view:Rectangle = new Rectangle(0, 0, _width, _height); |
28 |
if ( _spriteStage != null ) { |
29 |
_spriteStage.position = view; |
30 |
}
|
31 |
if(_entities != null) { |
32 |
_entities.setPosition(view); |
33 |
}
|
34 |
}
|
The event handling code above detects when Stage3D is ready for hardware rendering and sets the variable context3D
for future use. Errors are ignored for now. The resize event simply updates the size of the stage and batch rendering system dimensions.
Step 6: Init the Sprite Engine
Once the context3D
has been received, we are ready to start the game running. Continuing with Main.as
, add the following.
1 |
|
2 |
private function initSpriteEngine():void |
3 |
{
|
4 |
// init a gpu sprite system
|
5 |
var stageRect:Rectangle = new Rectangle(0, 0, _width, _height); |
6 |
_spriteStage = new LiteSpriteStage(stage.stage3Ds[0], context3D, stageRect); |
7 |
_spriteStage.configureBackBuffer(_width,_height); |
8 |
|
9 |
// create a single rendering batch
|
10 |
// which will draw all sprites in one pass
|
11 |
var view:Rectangle = new Rectangle(0,0,_width,_height) |
12 |
_entities = new EntityManager(stageRect); |
13 |
_entities.createBatch(context3D); |
14 |
_spriteStage.addBatch(_entities._batch); |
15 |
// add the first entity right now
|
16 |
_entities.addEntity(); |
17 |
|
18 |
// tell the gui where to grab statistics from
|
19 |
_gui.statsTarget = _entities; |
20 |
|
21 |
// start the render loop
|
22 |
stage.addEventListener(Event.ENTER_FRAME,onEnterFrame); |
23 |
}
|
This function creates a sprite rendering engine (to be implemented below) on the stage, ready to use the full size of your flash file. We then add the entity manager and batched geometry system (which we will discuss below). We are now able to give a reference to the entity manager to our stats GUI class so that it can display some numbers on screen regarding how many sprites have been created or reused. Lastly, we start listening for the ENTER_FRAME
event, which will begin firing at a rate of up to 60 times per second.
Step 7: Start the Render Loop
Now that everything has been initialized, we are ready to play! The following function will be executed every single frame. For the purposes of this first tech demo, we are going to add one new sprite on stage each frame. Because we are going to implement an object pool (which you can read more about in this tutorial) instead of inifinitely creating new objects until we run out of RAM, we are going to be able to reuse old entities that have moved off screen.
After spawning another sprite, we clear the stage3D area of the screen (setting it to pure black). Next we update all the entities that are being controlled by our entity manager. This will move them a little more each frame. Once all sprites have been updated, we tell the batched geometry system to gather them all up into one large vertex buffer and bast them on screen in a single draw call, for efficiency. Finally, we tell the context3D to update the screen with our final render.
1 |
|
2 |
// this function draws the scene every frame
|
3 |
private function onEnterFrame(e:Event):void |
4 |
{
|
5 |
try
|
6 |
{
|
7 |
// keep adding more sprites - FOREVER!
|
8 |
// this is a test of the entity manager's
|
9 |
// object reuse "pool"
|
10 |
_entities.addEntity(); |
11 |
|
12 |
// erase the previous frame
|
13 |
context3D.clear(0, 0, 0, 1); |
14 |
|
15 |
// move/animate all entities
|
16 |
_entities.update(getTimer()); |
17 |
|
18 |
// draw all entities
|
19 |
_spriteStage.render(); |
20 |
|
21 |
// update the screen
|
22 |
context3D.present(); |
23 |
}
|
24 |
catch (e:Error) |
25 |
{
|
26 |
// this can happen if the computer goes to sleep and
|
27 |
// then re-awakens, requiring reinitialization of stage3D
|
28 |
// (the onContext3DCreate will fire again)
|
29 |
}
|
30 |
}
|
31 |
} // end class |
32 |
} // end package |
That's it for the inits! As simple as it sounds, we have now created a template project that is ready to blast out an insane number of sprites. We are not going to use any vector art. We aren't going to put any old-fashioned Flash sprites on the stage apart from the Stage3D window and a couple of GUI overlays. All the work of rendering our in-game graphics is going to be handled by Stage3D, so that we can enjoy improved performance.
Going Deeper: Why Is Stage3D So Fast?
Two reasons:
- It uses hardware acceleration, meaning that all drawing commands are sent to the 3D GPU on your video card in the same way that XBOX360 and PlayStation3 games get rendered.
- These rendering commands are processed in parallel to the rest of your ActionScript code. This means that once the commands are sent to your video card, all rendering is done at the same time as other code in your game is running - Flash doesn't have to wait for them to be finished. While pixels are being blasted onto your screen, Flash gets to do other things like handle the player input, play sounds and update enemy positions.
- object pooling
- spritesheet (texture atlas)
- batched geometry
That said, many Stage3D engines seem to get bogged down by a few hundred sprites. This is because they have been programmed without regard to the overhead that each draw command adds. When Stage3D first came out, some of the first 2D engines would draw each and every sprite individually in one giant (slow and inefficient) loop. Since this article is all about extreme optimization for a next-gen 2D game with fabulous framerate, we are going to implement an extremely efficient rendering system that buffers all geometry into one big batch so we can draw everything in only one or two commands.
How to Be Hardcore: Optimize!
Hardcore gamedevs love optimizations. In order to blast the most sprites on screen with the fewest number of state changes (such as switching textures, selecting a new vertex buffer, or having to update the transform once for each and every sprite on screen), we are going to take advantage of the following three performance optimizations:
These three hardcore gamedev tricks are the key to getting awesome FPS in your game. Let's implement them now. Before we do, we need to create some of the tiny classes that these techniques will make use of.
Step 8: The Stats Display
If we're going to be doing tons of optimizations and using Stage3D in an attempt to achieve blazingly fast rendering performance, we need a way to keep track of the statistics. A few little benchmarks can go a long way to prove that what we're doing is having a positive effect on the framerate. Before we go farther, create a new class called GameGUI.as
and implement a super-simple FPS and stats display as follows.
1 |
|
2 |
// Stage3D Shoot-em-up Tutorial Part 1
|
3 |
// by Christer Kaitila - www.mcfunkypants.com
|
4 |
|
5 |
// GameGUI.as
|
6 |
// A typical simplistic framerate display for benchmarking performance,
|
7 |
// plus a way to track rendering statistics from the entity manager.
|
8 |
|
9 |
package
|
10 |
{
|
11 |
import flash.events.Event; |
12 |
import flash.events.TimerEvent; |
13 |
import flash.text.TextField; |
14 |
import flash.text.TextFormat; |
15 |
import flash.utils.getTimer; |
16 |
|
17 |
public class GameGUI extends TextField |
18 |
{
|
19 |
public var titleText : String = ""; |
20 |
public var statsText : String = ""; |
21 |
public var statsTarget : EntityManager; |
22 |
private var frameCount:int = 0; |
23 |
private var timer:int; |
24 |
private var ms_prev:int; |
25 |
private var lastfps : Number = 60; |
26 |
|
27 |
public function GameGUI(title:String = "", inX:Number=8, inY:Number=8, inCol:int = 0xFFFFFF) |
28 |
{
|
29 |
super(); |
30 |
titleText = title; |
31 |
x = inX; |
32 |
y = inY; |
33 |
width = 500; |
34 |
selectable = false; |
35 |
defaultTextFormat = new TextFormat("_sans", 9, 0, true); |
36 |
text = ""; |
37 |
textColor = inCol; |
38 |
this.addEventListener(Event.ADDED_TO_STAGE, onAddedHandler); |
39 |
|
40 |
}
|
41 |
public function onAddedHandler(e:Event):void { |
42 |
stage.addEventListener(Event.ENTER_FRAME, onEnterFrame); |
43 |
}
|
44 |
|
45 |
private function onEnterFrame(evt:Event):void |
46 |
{
|
47 |
timer = getTimer(); |
48 |
|
49 |
if( timer - 1000 > ms_prev ) |
50 |
{
|
51 |
lastfps = Math.round(frameCount/(timer-ms_prev)*1000); |
52 |
ms_prev = timer; |
53 |
|
54 |
// grab the stats from the entity manager
|
55 |
if (statsTarget) |
56 |
{
|
57 |
statsText = |
58 |
statsTarget.numCreated + ' created ' + |
59 |
statsTarget.numReused + ' reused'; |
60 |
}
|
61 |
|
62 |
text = titleText + ' - ' + statsText + " - FPS: " + lastfps; |
63 |
frameCount = 0; |
64 |
}
|
65 |
|
66 |
// count each frame to determine the framerate
|
67 |
frameCount++; |
68 |
|
69 |
}
|
70 |
} // end class |
71 |
} // end package |
Step 9: The Entity Class
We are about to implement an entity manager class that will be the "object pool" as described above. We first need to create a simplistic class for each individual entity in our game. This class will be used for all in-game objects, from spaceships to bullets.
Create a new file called Entity.as
and add a few getters and setters now. For this first tech demo, this class is merely an empty placeholder without much functionality, but in later tutorials this is where we will be implementing much of the gameplay.
1 |
|
2 |
// Stage3D Shoot-em-up Tutorial Part 1
|
3 |
// by Christer Kaitila - www.mcfunkypants.com
|
4 |
|
5 |
// Entity.as
|
6 |
// The Entity class will eventually hold all game-specific entity logic
|
7 |
// for the spaceships, bullets and effects in our game. For now,
|
8 |
// it simply holds 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 |
|
11 |
package
|
12 |
{
|
13 |
public class Entity |
14 |
{
|
15 |
private var _speedX : Number; |
16 |
private var _speedY : Number; |
17 |
private var _sprite : LiteSprite; |
18 |
public var active : Boolean = true; |
19 |
|
20 |
public function Entity(gs:LiteSprite = null) |
21 |
{
|
22 |
_sprite = gs; |
23 |
_speedX = 0.0; |
24 |
_speedY = 0.0; |
25 |
}
|
26 |
public function die() : void |
27 |
{
|
28 |
// allow this entity to be reused by the entitymanager
|
29 |
active = false; |
30 |
// skip all drawing and updating
|
31 |
sprite.visible = false; |
32 |
}
|
33 |
public function get speedX() : Number |
34 |
{
|
35 |
return _speedX; |
36 |
}
|
37 |
public function set speedX(sx:Number) : void |
38 |
{
|
39 |
_speedX = sx; |
40 |
}
|
41 |
public function get speedY() : Number |
42 |
{
|
43 |
return _speedY; |
44 |
}
|
45 |
public function set speedY(sy:Number) : void |
46 |
{
|
47 |
_speedY = sy; |
48 |
}
|
49 |
public function get sprite():LiteSprite |
50 |
{
|
51 |
return _sprite; |
52 |
}
|
53 |
public function set sprite(gs:LiteSprite):void |
54 |
{
|
55 |
_sprite = gs; |
56 |
}
|
57 |
} // end class |
58 |
} // end package |
Step 10: Make a Spritesheet
An important optimization technique we are going to use is the use of a spritesheet - sometimes referred to as a Texture Atlas. Instead of uploading dozens or hundreds of individual images to video RAM for use during rendering, we are going to make a single image that holds all the sprites in our game. This way, we can use a single texture to draw tons of different kinds of enemies or terrain.
Using a spritesheet is a considered a best practice by veteran gamedevs who need to ensure their games run as fast as possible. The reason it speeds things up so much is much the same as the reason why we are going to use geometry batching: instead of having to tell the video card over and over to use a particular texture to draw a particular sprite, we can simply tell it to always use the same texture for all draw calls.
This cuts down on "state changes" which are extremely costly in terms of time. We no longer need to say "video card, start using texture 24... now draw sprite 14" and so on. We just say "draw everything using this one texture" in a single pass. This can increase performance by an order of magnitude.
For our example game we will be using a collection of legal-to-use freeware images by the talented DanC, which you can get here. Remember that if you use these images you should credit them in your game as follows: "Art Collection Title" art by Daniel Cook (Lostgarden.com).
Using Photoshop (or GIMP, or whatever image editor you prefer), cut and paste the sprites your game will need into a single PNG file that has a transparent background. Place each sprite on an evenly-spaced grid with a couple pixels of blank space between each. This small buffer is required to avoid any "bleeding" of edge pixels from adjacent sprites that can occur due to bilinear texture filtering that happens on the GPU. If each sprite is touching the next, your in-game sprites may have unwanted edges where they should be completely transparent.
For optimization reasons, GPUs work best with images (called textures) that are square and whose dimensions are equal to a power of two and evenly divisible by eight. Why? Because of the way that the pixel data is accessed, these magic numbers happen to align in VRAM in just the right way to be fastest to access, because the data is often read in chunks.
Therefore, ensure that your spritesheet is either 64x64, 128x128, 256x256, 512x512 or 1024x1024. As you might expect, the smaller the better - not just in terms of performance but because a smaller texture will naturally keep your game's final SWF smaller.
Here is the spritesheet that we will be using for our example. "Tyrian" art by Daniel Cook (Lostgarden.com).

Right-click to download
Step 11: The Entity Manager
The first optimization technique we're going to take advantage of to achieve blazing performance is the use of "object pools". Instead of constantly allocating more ram for objects like bullets or enemies, we're going to make a reuse pool that recycles unused sprites over and over again.
This technique ensures that RAM use stays very low and GC (garbage collection) hiccups rarely occur. The result is that framerate will be higher and your game will run smoothly no matter how long you play.
Create a new class in your project called EntityManager.as
and implement a simple recycle-on-demand mechanism as follows.
1 |
|
2 |
// Stage3D Shoot-em-up Tutorial Part 1
|
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 |
// This is where you would add all in-game simulation steps,
|
12 |
// such as gravity, movement, collision detection and more.
|
13 |
|
14 |
package
|
15 |
{
|
16 |
import flash.display.Bitmap; |
17 |
import flash.display3D.*; |
18 |
import flash.geom.Point; |
19 |
import flash.geom.Rectangle; |
20 |
|
21 |
public class EntityManager |
22 |
{
|
23 |
// the sprite sheet image
|
24 |
private var _spriteSheet : LiteSpriteSheet; |
25 |
private const SpritesPerRow:int = 8; |
26 |
private const SpritesPerCol:int = 8; |
27 |
[Embed(source="../assets/sprites.png")] |
28 |
private var SourceImage : Class; |
29 |
|
30 |
// a reusable pool of entities
|
31 |
private var _entityPool : Vector.<Entity>; |
32 |
|
33 |
// all the polygons that make up the scene
|
34 |
public var _batch : LiteSpriteBatch; |
35 |
|
36 |
// for statistics
|
37 |
public var numCreated : int = 0; |
38 |
public var numReused : int = 0; |
39 |
|
40 |
private var maxX:int; |
41 |
private var minX:int; |
42 |
private var maxY:int; |
43 |
private var minY:int; |
44 |
|
45 |
public function EntityManager(view:Rectangle) |
46 |
{
|
47 |
_entityPool = new Vector.<Entity>(); |
48 |
setPosition(view); |
49 |
}
|
Step 12: Set Boundaries
Our entity manager is going to recycle entities when they move off the left edge of the screen. The function below is called during inits or when the resize event is fired. We add a few extra pixels to the edges so that sprites don't suddenly pop in or out of existence.
1 |
|
2 |
public function setPosition(view:Rectangle):void |
3 |
{
|
4 |
// allow moving fully offscreen before looping around
|
5 |
maxX = view.width + 32; |
6 |
minX = view.x - 32; |
7 |
maxY = view.height; |
8 |
minY = view.y; |
9 |
}
|
Step 13: Set Up the Sprites
The entity manager runs this function once at startup. It creates a new geometry batch using the spritesheet image that was embedded in our code above. It sends the bitmapData
to the spritesheet class constructor, which will be used to generate a texture that has all the available sprite images on it in a grid. We tell our spritesheet that we're going to use 64 different sprites (8 by 8) on the one texture. This spritesheet will be used by the batch geometry renderer.
If we wanted, we could use more than one spritesheet, by initializing additional images and batches as required. In the future, this might be where you create a second batch for all terrain tiles that go underneath your spaceship sprites. You could even implement a third batch which is layered on top of everything for fancy particle effects and eye candy. For now, this simple tech demo only needs a single spritesheet texture and geometry batch.
1 |
|
2 |
|
3 |
public function createBatch(context3D:Context3D) : LiteSpriteBatch |
4 |
{
|
5 |
var sourceBitmap:Bitmap = new SourceImage(); |
6 |
|
7 |
// create a spritesheet with 8x8 (64) sprites on it
|
8 |
_spriteSheet = new LiteSpriteSheet(sourceBitmap.bitmapData, 8, 8); |
9 |
|
10 |
// Create new render batch
|
11 |
_batch = new LiteSpriteBatch(context3D, _spriteSheet); |
12 |
|
13 |
return _batch; |
14 |
}
|
Step 14: The Object Pool
This is where the entity manager increases performance. This one optimization (an object reuse pool) will allow us to only create new entities on demand (when there aren't any inactive ones that can be reused). Note how we reuse any sprites that are currently marked as inactive, unless they are all currently being used, in which case we spawn a new one. This way, our object pool only every holds as many sprites as are even visible at the same time. After the first few seconds that our game has been running, the entity pool will remain constant - rarely will a new entity need to be created once there are enough to handle what's going on on-screen.
Continue adding to EntityManager.as
as follows:
1 |
|
2 |
// search the entity pool for unused entities and reuse one
|
3 |
// if they are all in use, create a brand new one
|
4 |
public function respawn(sprID:uint=0):Entity |
5 |
{
|
6 |
var currentEntityCount:int = _entityPool.length; |
7 |
var anEntity:Entity; |
8 |
var i:int = 0; |
9 |
// search for an inactive entity
|
10 |
for (i = 0; i < currentEntityCount; i++ ) |
11 |
{
|
12 |
anEntity = _entityPool[i]; |
13 |
if (!anEntity.active && (anEntity.sprite.spriteId == sprID)) |
14 |
{
|
15 |
//trace('Reusing Entity #' + i);
|
16 |
anEntity.active = true; |
17 |
anEntity.sprite.visible = true; |
18 |
numReused++; |
19 |
return anEntity; |
20 |
}
|
21 |
}
|
22 |
// none were found so we need to make a new one
|
23 |
//trace('Need to create a new Entity #' + i);
|
24 |
var sprite:LiteSprite; |
25 |
sprite = _batch.createChild(sprID); |
26 |
anEntity = new Entity(sprite); |
27 |
_entityPool.push(anEntity); |
28 |
numCreated++; |
29 |
return anEntity; |
30 |
}
|
31 |
|
32 |
// for this test, create random entities that move
|
33 |
// from right to left with random speeds and scales
|
34 |
public function addEntity():void |
35 |
{
|
36 |
var anEntity:Entity; |
37 |
var randomSpriteID:uint = Math.floor(Math.random() * 64); |
38 |
// try to reuse an inactive entity (or create a new one)
|
39 |
anEntity = respawn(randomSpriteID); |
40 |
// give it a new position and velocity
|
41 |
anEntity.sprite.position.x = maxX; |
42 |
anEntity.sprite.position.y = Math.random() * maxY; |
43 |
anEntity.speedX = (-1 * Math.random() * 10) - 2; |
44 |
anEntity.speedY = (Math.random() * 5) - 2.5; |
45 |
anEntity.sprite.scaleX = 0.5 + Math.random() * 1.5; |
46 |
anEntity.sprite.scaleY = anEntity.sprite.scaleX; |
47 |
anEntity.sprite.rotation = 15 - Math.random() * 30; |
48 |
}
|
The functions above are run whenever a new sprite needs to be added on screen. The entity manager scans the entity pool for one that is currently not in use and returns it when possible. If the list is fully of active entities, a brand new one needs to be created.
Step 15: Simulate!
The final function that is the responsibility of our entity manager is the one that gets called every frame. It is used to do any simulation, AI, collision detection, physics or animation as required. For the current simplistic tech demo, it simply loops through the list of active entities in the pool and updates their positions based on velocity. Each entity is moved according to their current velocity. Just for fun, they are set to spin a little each frame as well.
Any entity that goes past the left side of the screen is "killed" and is marked as inactive and invisible, ready to be reused in the functions above. If an entity touches the other three screen edges, the velocity is reversed so it will "bounce" off that edge. Continue adding to EntityManager.as
as follows:
1 |
|
2 |
// called every frame: used to update the simulation
|
3 |
// this is where you would perform AI, physics, etc.
|
4 |
public function update(currentTime:Number) : void |
5 |
{
|
6 |
var anEntity:Entity; |
7 |
for(var i:int=0; i<_entityPool.length;i++) |
8 |
{
|
9 |
anEntity = _entityPool[i]; |
10 |
if (anEntity.active) |
11 |
{
|
12 |
anEntity.sprite.position.x += anEntity.speedX; |
13 |
anEntity.sprite.position.y += anEntity.speedY; |
14 |
anEntity.sprite.rotation += 0.1; |
15 |
|
16 |
if (anEntity.sprite.position.x > maxX) |
17 |
{
|
18 |
anEntity.speedX *= -1; |
19 |
anEntity.sprite.position.x = maxX; |
20 |
}
|
21 |
else if (anEntity.sprite.position.x < minX) |
22 |
{
|
23 |
// if we go past the left edge, become inactive
|
24 |
// so the sprite can be respawned
|
25 |
anEntity.die(); |
26 |
}
|
27 |
if (anEntity.sprite.position.y > maxY) |
28 |
{
|
29 |
anEntity.speedY *= -1; |
30 |
anEntity.sprite.position.y = maxY; |
31 |
}
|
32 |
else if (anEntity.sprite.position.y < minY) |
33 |
{
|
34 |
anEntity.speedY *= -1; |
35 |
anEntity.sprite.position.y = minY; |
36 |
}
|
37 |
}
|
38 |
}
|
39 |
}
|
40 |
} // end class |
41 |
} // end package |
Step 16: The Sprite Class
The final step to get everything up and running is to implement the four classes that make up our "rendering engine" system. Because the word Sprite is already in use in Flash, the next few classes will use the term LiteSprite
, which is not just a catchy name but implies the lightweight and simplistic nature of this engine.
To begin, we will create the simple 2D sprite class that our entity class above refers to. There will be many sprites in our game, each of which is collected into a large batch of polygons and rendered in a single pass.
Create a new file in your project called LiteSprite.as
and implement some getters and setters as follows. We could probably get away with simply using public variables, but in future versions changing some of these values will require running some code first, so this technique will prove invaluable.
1 |
|
2 |
// Stage3D Shoot-em-up Tutorial Part 1
|
3 |
// by Christer Kaitila - www.mcfunkypants.com
|
4 |
|
5 |
// LiteSprite.as
|
6 |
// A 2d sprite that is rendered by Stage3D as a textured quad
|
7 |
// (two triangles) to take advantage of hardware acceleration.
|
8 |
// Based on example code by Chris Nuuja which is a port
|
9 |
// of the haXe+NME bunnymark demo by Philippe Elsass
|
10 |
// which is itself a port of Iain Lobb's original work.
|
11 |
// Also includes code from the Starling framework.
|
12 |
// Grateful acknowledgements to all involved.
|
13 |
|
14 |
package
|
15 |
{
|
16 |
import flash.geom.Point; |
17 |
import flash.geom.Rectangle; |
18 |
|
19 |
public class LiteSprite |
20 |
{
|
21 |
internal var _parent : LiteSpriteBatch; |
22 |
internal var _spriteId : uint; |
23 |
internal var _childId : uint; |
24 |
private var _pos : Point; |
25 |
private var _visible : Boolean; |
26 |
private var _scaleX : Number; |
27 |
private var _scaleY : Number; |
28 |
private var _rotation : Number; |
29 |
private var _alpha : Number; |
30 |
|
31 |
public function get visible() : Boolean |
32 |
{
|
33 |
return _visible; |
34 |
}
|
35 |
public function set visible(isVisible:Boolean) : void |
36 |
{
|
37 |
_visible = isVisible; |
38 |
}
|
39 |
public function get alpha() : Number |
40 |
{
|
41 |
return _alpha; |
42 |
}
|
43 |
public function set alpha(a:Number) : void |
44 |
{
|
45 |
_alpha = a; |
46 |
}
|
47 |
public function get position() : Point |
48 |
{
|
49 |
return _pos; |
50 |
}
|
51 |
public function set position(pt:Point) : void |
52 |
{
|
53 |
_pos = pt; |
54 |
}
|
55 |
public function get scaleX() : Number |
56 |
{
|
57 |
return _scaleX; |
58 |
}
|
59 |
public function set scaleX(val:Number) : void |
60 |
{
|
61 |
_scaleX = val; |
62 |
}
|
63 |
public function get scaleY() : Number |
64 |
{
|
65 |
return _scaleY; |
66 |
}
|
67 |
public function set scaleY(val:Number) : void |
68 |
{
|
69 |
_scaleY = val; |
70 |
}
|
71 |
public function get rotation() : Number |
72 |
{
|
73 |
return _rotation; |
74 |
}
|
75 |
public function set rotation(val:Number) : void |
76 |
{
|
77 |
_rotation = val; |
78 |
}
|
79 |
public function get rect() : Rectangle |
80 |
{
|
81 |
return _parent._sprites.getRect(_spriteId); |
82 |
}
|
83 |
public function get parent() : LiteSpriteBatch |
84 |
{
|
85 |
return _parent; |
86 |
}
|
87 |
public function get spriteId() : uint |
88 |
{
|
89 |
return _spriteId; |
90 |
}
|
91 |
public function set spriteId(num : uint) : void |
92 |
{
|
93 |
_spriteId = num; |
94 |
}
|
95 |
public function get childId() : uint |
96 |
{
|
97 |
return _childId; |
98 |
}
|
99 |
|
100 |
// LiteSprites are typically constructed by calling LiteSpriteBatch.createChild()
|
101 |
public function LiteSprite() |
102 |
{
|
103 |
_parent = null; |
104 |
_spriteId = 0; |
105 |
_childId = 0; |
106 |
_pos = new Point(); |
107 |
_scaleX = 1.0; |
108 |
_scaleY = 1.0; |
109 |
_rotation = 0; |
110 |
_alpha = 1.0; |
111 |
_visible = true; |
112 |
}
|
113 |
} // end class |
114 |
} // end package |
Each sprite can now keep track of where it is on screen, as well as how big it is, how transparent, and what angle it is facing. The spriteID property is a number used during rendering to look up which UV (texture) coordinate needs to be used as the source rectangle for the pixels of the spritesheet image it uses.
Step 17: The Spritesheet Class
We now need to implement a mechanism to process the spritesheet image that we embedded above and use portions of it on all our rendered geometry. Create a new file in your project called LiteSpriteSheet.as
and begin by importing the functionality required, defining a few class variables and a constructor function.
1 |
|
2 |
// Stage3D Shoot-em-up Tutorial Part 1
|
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 |
public function LiteSpriteSheet(SpriteSheetBitmapData:BitmapData, numSpritesW:int = 8, numSpritesH:int = 8) |
38 |
{
|
39 |
_uvCoords = new Vector.<Number>(); |
40 |
_rects = new Vector.<Rectangle>(); |
41 |
_spriteSheet = SpriteSheetBitmapData; |
42 |
createUVs(numSpritesW, numSpritesH); |
43 |
}
|
The class constructor above is given a BitmapData
for our spritesheet as well as the number of sprites that are on it (in this demo, 64).
Step 18: Chop It Up
Because we are using a single texture to store all of the sprite images, we need to divide the image into several parts (one for each sprite on it) when rendering. We do this by assigning different coordinates for each vertex (corner) of each quad mesh used to draw a sprite.
These coordinates are called UVs, and each goes from 0 to 1 and represents where on the texture stage3D should start sampling pixels when rendering. The UV coordinates and pixel rectangles are stored in an array for later using during rendering so that we don't have to calculate them every frame. We also store the size and shape of each sprite (which in this demo are all identical) so that when we rotate a sprite we know its radius (which is used to keep the pivot in the very centre of the sprite).
1 |
|
2 |
|
3 |
// generate a list of uv coordinates for a grid of sprites
|
4 |
// on the spritesheet texture for later reference by ID number
|
5 |
// sprite ID numbers go from left to right then down
|
6 |
public function createUVs(numSpritesW:int, numSpritesH:int) : void |
7 |
{
|
8 |
trace('creating a '+_spriteSheet.width+'x'+_spriteSheet.height+ |
9 |
' spritesheet texture with '+numSpritesW+'x'+ numSpritesH+' sprites.'); |
10 |
|
11 |
var destRect : Rectangle; |
12 |
|
13 |
for (var y:int = 0; y < numSpritesH; y++) |
14 |
{
|
15 |
for (var x:int = 0; x < numSpritesW; x++) |
16 |
{
|
17 |
_uvCoords.push( |
18 |
// bl, tl, tr, br
|
19 |
x / numSpritesW, (y+1) / numSpritesH, |
20 |
x / numSpritesW, y / numSpritesH, |
21 |
(x+1) / numSpritesW, y / numSpritesH, |
22 |
(x + 1) / numSpritesW, (y + 1) / numSpritesH); |
23 |
|
24 |
destRect = new Rectangle(); |
25 |
destRect.left = 0; |
26 |
destRect.top = 0; |
27 |
destRect.right = _spriteSheet.width / numSpritesW; |
28 |
destRect.bottom = _spriteSheet.height / numSpritesH; |
29 |
_rects.push(destRect); |
30 |
}
|
31 |
}
|
32 |
}
|
33 |
|
34 |
public function removeSprite(spriteId:uint) : void |
35 |
{
|
36 |
if ( spriteId < _uvCoords.length ) { |
37 |
_uvCoords = _uvCoords.splice(spriteId * 8, 8); |
38 |
_rects.splice(spriteId, 1); |
39 |
}
|
40 |
}
|
41 |
|
42 |
public function get numSprites() : uint |
43 |
{
|
44 |
return _rects.length; |
45 |
}
|
46 |
|
47 |
public function getRect(spriteId:uint) : Rectangle |
48 |
{
|
49 |
return _rects[spriteId]; |
50 |
}
|
51 |
|
52 |
public function getUVCoords(spriteId:uint) : Vector.<Number> |
53 |
{
|
54 |
var startIdx:uint = spriteId * 8; |
55 |
return _uvCoords.slice(startIdx, startIdx + 8); |
56 |
}
|
Step 19: Generate Mipmaps
Now we need to process this image during the init. We are going to upload it for use as a texture by your GPU. As we do so, we are going to create smaller copies that are called "mipmaps". Mip-mapping is used by 3d hardware to further speed up rendering by using smaller versions of the same texture whenever it is seen from far away (scaled down) or, in true 3D games, when it is being viewed at an oblique angle. This avoids any "moiree" effects (flickers) than can happen if mipmapping is not used. Each mipmap is half the width and height as the previous.
Continuing with LiteSpriteSheet.as
, let's implement the routine we need that will generate mipmaps and upload them all to the GPU on your video card.
1 |
|
2 |
public function uploadTexture(context3D:Context3D) : void |
3 |
{
|
4 |
if ( _texture == null ) { |
5 |
_texture = context3D.createTexture(_spriteSheet.width, _spriteSheet.height, Context3DTextureFormat.BGRA, false); |
6 |
}
|
7 |
|
8 |
_texture.uploadFromBitmapData(_spriteSheet); |
9 |
|
10 |
// generate mipmaps
|
11 |
var currentWidth:int = _spriteSheet.width >> 1; |
12 |
var currentHeight:int = _spriteSheet.height >> 1; |
13 |
var level:int = 1; |
14 |
var canvas:BitmapData = new BitmapData(currentWidth, currentHeight, true, 0); |
15 |
var transform:Matrix = new Matrix(.5, 0, 0, .5); |
16 |
while ( currentWidth >= 1 || currentHeight >= 1 ) { |
17 |
canvas.fillRect(new Rectangle(0, 0, Math.max(currentWidth,1), Math.max(currentHeight,1)), 0); |
18 |
canvas.draw(_spriteSheet, transform, null, null, null, true); |
19 |
_texture.uploadFromBitmapData(canvas, level++); |
20 |
transform.scale(0.5, 0.5); |
21 |
currentWidth = currentWidth >> 1; |
22 |
currentHeight = currentHeight >> 1; |
23 |
}
|
24 |
}
|
25 |
} // end class |
26 |
} // end package |
Step 20: Batched Geometry
The final hardcore optimization we are going to implement is a batched geometry rendering system. This "batched geometry" technique is often used in particle systems. We are going to use it for everything. This way, we can tell your GPU to draw everything in one go instead of naively sending hundreds of draw commands (one for each sprite on screen).
In order to minimize the number of draw calls and rendering everything in one go, we will be batching all game sprites into a long list of (x,y) coordinates. Essentially, the geometry batch is treated by your video hardware as a single 3D mesh. Then, once per frame, we will upload the entire buffer to Stage3D in a single function call. Doing things this way is far faster than uploading the individual coordinates of each sprite separately.
Create a new file in your project called LiteSpriteBatch.as
and begin by including all the imports for functionality it will need, the class variables it will use, and the constructor as follows:
1 |
|
2 |
// Stage3D Shoot-em-up Tutorial Part 1
|
3 |
// by Christer Kaitila - www.mcfunkypants.com
|
4 |
|
5 |
// LiteSpriteBatch.as
|
6 |
// An optimization used to increase performance that renders multiple
|
7 |
// sprites in a single pass by grouping all polygons together,
|
8 |
// allowing stage3D to treat it as a single mesh that can be
|
9 |
// rendered in a single drawTriangles call.
|
10 |
// Each frame, the positions of each
|
11 |
// vertex is updated and re-uploaded to video ram.
|
12 |
// Based on example code by Chris Nuuja which is a port
|
13 |
// of the haXe+NME bunnymark demo by Philippe Elsass
|
14 |
// which is itself a port of Iain Lobb's original work.
|
15 |
// Also includes code from the Starling framework.
|
16 |
// Grateful acknowledgements to all involved.
|
17 |
|
18 |
package
|
19 |
{
|
20 |
import com.adobe.utils.AGALMiniAssembler; |
21 |
|
22 |
import flash.display.BitmapData; |
23 |
import flash.display3D.Context3D; |
24 |
import flash.display3D.Context3DBlendFactor; |
25 |
import flash.display3D.Context3DCompareMode; |
26 |
import flash.display3D.Context3DProgramType; |
27 |
import flash.display3D.Context3DTextureFormat; |
28 |
import flash.display3D.Context3DVertexBufferFormat; |
29 |
import flash.display3D.IndexBuffer3D; |
30 |
import flash.display3D.Program3D; |
31 |
import flash.display3D.VertexBuffer3D; |
32 |
import flash.display3D.textures.Texture; |
33 |
import flash.geom.Matrix; |
34 |
import flash.geom.Matrix3D; |
35 |
import flash.geom.Point; |
36 |
import flash.geom.Rectangle; |
37 |
|
38 |
public class LiteSpriteBatch |
39 |
{
|
40 |
internal var _sprites : LiteSpriteSheet; |
41 |
internal var _verteces : Vector.<Number>; |
42 |
internal var _indeces : Vector.<uint>; |
43 |
internal var _uvs : Vector.<Number>; |
44 |
|
45 |
protected var _context3D : Context3D; |
46 |
protected var _parent : LiteSpriteStage; |
47 |
protected var _children : Vector.<LiteSprite>; |
48 |
|
49 |
protected var _indexBuffer : IndexBuffer3D; |
50 |
protected var _vertexBuffer : VertexBuffer3D; |
51 |
protected var _uvBuffer : VertexBuffer3D; |
52 |
protected var _shader : Program3D; |
53 |
protected var _updateVBOs : Boolean; |
54 |
|
55 |
|
56 |
public function LiteSpriteBatch(context3D:Context3D, spriteSheet:LiteSpriteSheet) |
57 |
{
|
58 |
_context3D = context3D; |
59 |
_sprites = spriteSheet; |
60 |
|
61 |
_verteces = new Vector.<Number>(); |
62 |
_indeces = new Vector.<uint>(); |
63 |
_uvs = new Vector.<Number>(); |
64 |
|
65 |
_children = new Vector.<LiteSprite>; |
66 |
_updateVBOs = true; |
67 |
setupShaders(); |
68 |
updateTexture(); |
69 |
}
|
Step 21: Batch Parent and Children
Continue by implementing getters and setters and functionality for handling the addition of any new sprites to the batch. The parent refers to the sprite stage object used by our game engine, while the children are all the sprites in this one rendering batch. When we add a child sprite, we add more data to the list of verteces (which supplies the locations on screen of that particular sprite) as well as the UV coordinates (the location on the spritesheet texture that this particular sprite is stored at). When a child sprite is added or removed from the batch, we set a boolean variable to tell our batch system that the buffers need to be re-uploaded now that they have changed.
1 |
|
2 |
public function get parent() : LiteSpriteStage |
3 |
{
|
4 |
return _parent; |
5 |
}
|
6 |
|
7 |
public function set parent(parentStage:LiteSpriteStage) : void |
8 |
{
|
9 |
_parent = parentStage; |
10 |
}
|
11 |
|
12 |
public function get numChildren() : uint |
13 |
{
|
14 |
return _children.length; |
15 |
}
|
16 |
|
17 |
// Constructs a new child sprite and attaches it to the batch
|
18 |
public function createChild(spriteId:uint) : LiteSprite |
19 |
{
|
20 |
var sprite : LiteSprite = new LiteSprite(); |
21 |
addChild(sprite, spriteId); |
22 |
return sprite; |
23 |
}
|
24 |
|
25 |
public function addChild(sprite:LiteSprite, spriteId:uint) : void |
26 |
{
|
27 |
sprite._parent = this; |
28 |
sprite._spriteId = spriteId; |
29 |
|
30 |
// Add to list of children
|
31 |
sprite._childId = _children.length; |
32 |
_children.push(sprite); |
33 |
|
34 |
// Add vertex data required to draw child
|
35 |
var childVertexFirstIndex:uint = (sprite._childId * 12) / 3; |
36 |
_verteces.push(0, 0, 1, 0, 0,1, 0, 0,1, 0, 0,1); // placeholders |
37 |
_indeces.push(childVertexFirstIndex, childVertexFirstIndex+1, childVertexFirstIndex+2, |
38 |
childVertexFirstIndex, childVertexFirstIndex+2, childVertexFirstIndex+3); |
39 |
|
40 |
var childUVCoords:Vector.<Number> = _sprites.getUVCoords(spriteId); |
41 |
_uvs.push( |
42 |
childUVCoords[0], childUVCoords[1], |
43 |
childUVCoords[2], childUVCoords[3], |
44 |
childUVCoords[4], childUVCoords[5], |
45 |
childUVCoords[6], childUVCoords[7]); |
46 |
|
47 |
_updateVBOs = true; |
48 |
}
|
49 |
|
50 |
public function removeChild(child:LiteSprite) : void |
51 |
{
|
52 |
var childId:uint = child._childId; |
53 |
if ( (child._parent == this) && childId < _children.length ) { |
54 |
child._parent = null; |
55 |
_children.splice(childId, 1); |
56 |
|
57 |
// Update child id (index into array of children) for remaining children
|
58 |
var idx:uint; |
59 |
for ( idx = childId; idx < _children.length; idx++ ) { |
60 |
_children[idx]._childId = idx; |
61 |
}
|
62 |
|
63 |
// Realign vertex data with updated list of children
|
64 |
var vertexIdx:uint = childId * 12; |
65 |
var indexIdx:uint= childId * 6; |
66 |
_verteces.splice(vertexIdx, 12); |
67 |
_indeces.splice(indexIdx, 6); |
68 |
_uvs.splice(vertexIdx, 8); |
69 |
|
70 |
_updateVBOs = true; |
71 |
}
|
72 |
}
|
Step 22: Set Up the Shader
A shader is a set of commands that is uploaded directly to your video card for extremely fast rendering. In Flash 11 Stage3D, you write them in a kind of assembly language called AGAL. This shader needs only be created once, at startup. You don't need to understand assembly language opcodes for this tutorial. Instead, simply implement the creation of a vertex program (which calculates the locations of your sprites on screen) and a fragment program (which calculates the color of each pixel) as follows.
1 |
|
2 |
protected function setupShaders() : void |
3 |
{
|
4 |
var vertexShaderAssembler:AGALMiniAssembler = new AGALMiniAssembler(); |
5 |
vertexShaderAssembler.assemble( Context3DProgramType.VERTEX, |
6 |
"dp4 op.x, va0, vc0 \n"+ // transform from stream 0 to output clipspace |
7 |
"dp4 op.y, va0, vc1 \n"+ // do the same for the y coordinate |
8 |
"mov op.z, vc2.z \n"+ // we don't need to change the z coordinate |
9 |
"mov op.w, vc3.w \n"+ // unused, but we need to output all data |
10 |
"mov v0, va1.xy \n"+ // copy UV coords from stream 1 to fragment program |
11 |
"mov v0.z, va0.z \n" // copy alpha from stream 0 to fragment program |
12 |
); |
13 |
|
14 |
var fragmentShaderAssembler:AGALMiniAssembler = new AGALMiniAssembler(); |
15 |
fragmentShaderAssembler.assemble( Context3DProgramType.FRAGMENT, |
16 |
"tex ft0, v0, fs0 <2d,clamp,linear,mipnearest> \n"+ // sample the texture |
17 |
"mul ft0, ft0, v0.zzzz\n" + // multiply by the alpha transparency |
18 |
"mov oc, ft0 \n" // output the final pixel color |
19 |
); |
20 |
|
21 |
_shader = _context3D.createProgram(); |
22 |
_shader.upload( vertexShaderAssembler.agalcode, fragmentShaderAssembler.agalcode ); |
23 |
}
|
24 |
|
25 |
protected function updateTexture() : void |
26 |
{
|
27 |
_sprites.uploadTexture(_context3D); |
28 |
}
|
Step 23: Move the Sprites Around
Just before being rendered, each sprite's vertex coordinates on screen will have most likely changed as the sprite moves around or rotates. The following function calculates where each vertex (corner of the geometry) needs to be. Because each quad (the square that makes up one sprite) has four vertices each, and each vertex needs an x, y and z coordinate, there are twelve values to update. As a little optimization, if the sprite is not visible we simply write zeroes into our vertex buffer to avoid doing unnecessary calculations.
1 |
|
2 |
protected function updateChildVertexData(sprite:LiteSprite) : void |
3 |
{
|
4 |
var childVertexIdx:uint = sprite._childId * 12; |
5 |
|
6 |
if ( sprite.visible ) { |
7 |
var x:Number = sprite.position.x; |
8 |
var y:Number = sprite.position.y; |
9 |
var rect:Rectangle = sprite.rect; |
10 |
var sinT:Number = Math.sin(sprite.rotation); |
11 |
var cosT:Number = Math.cos(sprite.rotation); |
12 |
var alpha:Number = sprite.alpha; |
13 |
|
14 |
var scaledWidth:Number = rect.width * sprite.scaleX; |
15 |
var scaledHeight:Number = rect.height * sprite.scaleY; |
16 |
var centerX:Number = scaledWidth * 0.5; |
17 |
var centerY:Number = scaledHeight * 0.5; |
18 |
|
19 |
_verteces[childVertexIdx] = x - (cosT * centerX) - (sinT * (scaledHeight - centerY)); |
20 |
_verteces[childVertexIdx+1] = y - (sinT * centerX) + (cosT * (scaledHeight - centerY)); |
21 |
_verteces[childVertexIdx+2] = alpha; |
22 |
|
23 |
_verteces[childVertexIdx+3] = x - (cosT * centerX) + (sinT * centerY); |
24 |
_verteces[childVertexIdx+4] = y - (sinT * centerX) - (cosT * centerY); |
25 |
_verteces[childVertexIdx+5] = alpha; |
26 |
|
27 |
_verteces[childVertexIdx+6] = x + (cosT * (scaledWidth - centerX)) + (sinT * centerY); |
28 |
_verteces[childVertexIdx+7] = y + (sinT * (scaledWidth - centerX)) - (cosT * centerY); |
29 |
_verteces[childVertexIdx+8] = alpha; |
30 |
|
31 |
_verteces[childVertexIdx+9] = x + (cosT * (scaledWidth - centerX)) - (sinT * (scaledHeight - centerY)); |
32 |
_verteces[childVertexIdx+10] = y + (sinT * (scaledWidth - centerX)) + (cosT * (scaledHeight - centerY)); |
33 |
_verteces[childVertexIdx+11] = alpha; |
34 |
|
35 |
}
|
36 |
else { |
37 |
for (var i:uint = 0; i < 12; i++ ) { |
38 |
_verteces[childVertexIdx+i] = 0; |
39 |
}
|
40 |
}
|
41 |
}
|
Step 24: Draw the Geometry
Finally, continue adding to the LiteSpriteBatch.as
class by implementing the drawing function. This is where we tell stage3D to render all the sprites in a single pass. First, we loop through all known children (the individual sprites) and update the verterx positions based on where they are on screen. We then tell stage3D which shader and texture to use, as well as set the blend factors for rendering.
What is a blend factor? It defines whether or not we should use transparency, and how to deal with transparent pixels on our texture. You could change the options in the setBlendFactors
call to use additive blanding, for example, which looks great for particle effects like explosions, since pixels will increase the brightness on screen as they overlap. In the case of regular sprites, all we want is to draw them at the exact color as stored in our spritesheet texture and to allow transparent regions.
The final step in our draw function is to update the UV and index buffers if the batch has changed size, and to always upload the vertex data because our sprites are exected to be constantly moving. We tell stage3D which buffers to use and finally render the entire giant list of geometry as if it were a single 3D mesh, so that it gets drawn using a single, fast, drawTriangles
call.
1 |
|
2 |
|
3 |
public function draw() : void |
4 |
{
|
5 |
var nChildren:uint = _children.length; |
6 |
if ( nChildren == 0 ) return; |
7 |
|
8 |
// Update vertex data with current position of children
|
9 |
for ( var i:uint = 0; i < nChildren; i++ ) { |
10 |
updateChildVertexData(_children[i]); |
11 |
}
|
12 |
|
13 |
_context3D.setProgram(_shader); |
14 |
_context3D.setBlendFactors(Context3DBlendFactor.ONE, |
15 |
Context3DBlendFactor.ONE_MINUS_SOURCE_ALPHA); |
16 |
_context3D.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, |
17 |
0, _parent.modelViewMatrix, true); |
18 |
_context3D.setTextureAt(0, _sprites._texture); |
19 |
|
20 |
if ( _updateVBOs ) { |
21 |
_vertexBuffer = _context3D.createVertexBuffer(_verteces.length/3, 3); |
22 |
_indexBuffer = _context3D.createIndexBuffer(_indeces.length); |
23 |
_uvBuffer = _context3D.createVertexBuffer(_uvs.length/2, 2); |
24 |
_indexBuffer.uploadFromVector(_indeces, 0, _indeces.length); // indices won't change |
25 |
_uvBuffer.uploadFromVector(_uvs, 0, _uvs.length / 2); // child UVs won't change |
26 |
_updateVBOs = false; |
27 |
}
|
28 |
|
29 |
// we want to upload the vertex data every frame
|
30 |
_vertexBuffer.uploadFromVector(_verteces, 0, _verteces.length / 3); |
31 |
_context3D.setVertexBufferAt(0, _vertexBuffer, 0, Context3DVertexBufferFormat.FLOAT_3); |
32 |
_context3D.setVertexBufferAt(1, _uvBuffer, 0, Context3DVertexBufferFormat.FLOAT_2); |
33 |
|
34 |
_context3D.drawTriangles(_indexBuffer, 0, nChildren * 2); |
35 |
}
|
36 |
} // end class |
37 |
} // end package |
Step 25: The Sprite Stage Class
The final class required by our fancy (and speedy) hardware-accelerated sprite rendering engine is the sprite stage class. This stage, much like the traditional Flash stage, holds a list of all the batches that are used for your game. In this first demo, our stage will only be using a single batch of sprites, which itself only uses a single spritesheet.
Create one last file in your project called LiteSpriteStage.as
and begin by creating the class as follows:
1 |
|
2 |
// Stage3D Shoot-em-up Tutorial Part 1
|
3 |
// by Christer Kaitila - www.mcfunkypants.com
|
4 |
|
5 |
// LiteSpriteStage.as
|
6 |
// The stage3D renderer of any number of batched geometry
|
7 |
// meshes of multiple sprites. Handles stage3D inits, etc.
|
8 |
// Based on example code by Chris Nuuja which is a port
|
9 |
// of the haXe+NME bunnymark demo by Philippe Elsass
|
10 |
// which is itself a port of Iain Lobb's original work.
|
11 |
// Also includes code from the Starling framework.
|
12 |
// Grateful acknowledgements to all involved.
|
13 |
|
14 |
package
|
15 |
{
|
16 |
import flash.display.Stage3D; |
17 |
import flash.display3D.Context3D; |
18 |
import flash.geom.Matrix3D; |
19 |
import flash.geom.Rectangle; |
20 |
|
21 |
public class LiteSpriteStage |
22 |
{
|
23 |
protected var _stage3D : Stage3D; |
24 |
protected var _context3D : Context3D; |
25 |
protected var _rect : Rectangle; |
26 |
protected var _batches : Vector.<LiteSpriteBatch>; |
27 |
protected var _modelViewMatrix : Matrix3D; |
28 |
|
29 |
public function LiteSpriteStage(stage3D:Stage3D, context3D:Context3D, rect:Rectangle) |
30 |
{
|
31 |
_stage3D = stage3D; |
32 |
_context3D = context3D; |
33 |
_batches = new Vector.<LiteSpriteBatch>; |
34 |
|
35 |
this.position = rect; |
36 |
}
|
Step 26: The Camera Matrix
In order to know exactly where on screen each sprite needs to go, we will track the location and size of the rendering window. During our game's initializations (or if it changes) we create a model view matrix which is used by Stage3D to transform the internal 3D coordinates of our geometry batches to the proper on-screen locations.
1 |
|
2 |
|
3 |
public function get position() : Rectangle |
4 |
{
|
5 |
return _rect; |
6 |
}
|
7 |
|
8 |
public function set position(rect:Rectangle) : void |
9 |
{
|
10 |
_rect = rect; |
11 |
_stage3D.x = rect.x; |
12 |
_stage3D.y = rect.y; |
13 |
configureBackBuffer(rect.width, rect.height); |
14 |
|
15 |
_modelViewMatrix = new Matrix3D(); |
16 |
_modelViewMatrix.appendTranslation(-rect.width/2, -rect.height/2, 0); |
17 |
_modelViewMatrix.appendScale(2.0/rect.width, -2.0/rect.height, 1); |
18 |
}
|
19 |
|
20 |
internal function get modelViewMatrix() : Matrix3D |
21 |
{
|
22 |
return _modelViewMatrix; |
23 |
}
|
24 |
|
25 |
public function configureBackBuffer(width:uint, height:uint) : void |
26 |
{
|
27 |
_context3D.configureBackBuffer(width, height, 0, false); |
28 |
}
|
Step 27: Handle Batches
The final step in the creation of our Stage3D game demo is to handle the addition and removal of geometry batches as well as a loop that calls the draw function on each batch. This way, when our game's main ENTER_FRAME
event is fired, it will move the sprites around on screen via the entity manager and then tell the sprite stage system to draw itself, which in turn tells all known batches to draw.
Because this is a heavily optimized demo, there will only be one batch in use, but this will change in future tutorials as we add more eye candy.
1 |
|
2 |
public function addBatch(batch:LiteSpriteBatch) : void |
3 |
{
|
4 |
batch.parent = this; |
5 |
_batches.push(batch); |
6 |
}
|
7 |
|
8 |
public function removeBatch(batch:LiteSpriteBatch) : void |
9 |
{
|
10 |
for ( var i:uint = 0; i < _batches.length; i++ ) { |
11 |
if ( _batches[i] == batch ) { |
12 |
batch.parent = null; |
13 |
_batches.splice(i, 1); |
14 |
}
|
15 |
}
|
16 |
}
|
17 |
|
18 |
// loop through all batches
|
19 |
// (this demo uses only one)
|
20 |
// and tell them to draw themselves
|
21 |
public function render() : void |
22 |
{
|
23 |
for ( var i:uint = 0; i < _batches.length; i++ ) { |
24 |
_batches[i].draw(); |
25 |
}
|
26 |
}
|
27 |
} // end class |
28 |
} // end package |
Step 28: Compile and Run!
We're almost done! Compile your SWF, fix any typos, and check out the graphical goodness. You should have a demo that looks like this:



If you are having difficulties compiling, note that this project needs a class that was made by Adobe which handles the compilation of AGAL shaders, which is included in the source code .zip file download.
Just for reference, and to ensure that you've used the correct filenames and locations for everything, here is what your FlashDevelop project should look like:

Tutorial Complete: You Are Awesome
That's it for tutorial one in this series! Tune in next week to watch the game slowly evolve into a great-looking, silky-smooth 60 FPS shoot-em-up. In the next part, we will implement player controls (using the keyboard to move around) and add some movement, sounds and music to the game.
I'd love to hear from you regarding this tutorial. I warmly welcome all readers to get in touch with me via twitter: @mcfunkypants or my blog mcfunkypants.com or on Google+ any time. I'm always looking for new topics to write future tutorials on, so feel free to request one. Finally, I'd love to see the games you make using this code!
Thanks for reading. See you next week. Good luck and HAVE FUN!