Flash 10: drawTriangles() disappointments #
I've been switching to Flash 10 for my game experiments. Flash 10 offers new graphics APIs, and opens up new approaches for how to deal with game objects. I wanted to compare these approaches:
-
(Flash 9) Use multiple
Sprite
objects for hierarchical layers, filters, rotation, and translation. It's convenient to have one or more Sprites per game object, and then move and rotate them without having to manage them manually. For vector drawing, I can usebeginFill
andlineStyle
with (Flash 10)drawPath
. For bitmap (textured) drawing, I can usebeginBitmapFill
, or there's a specialBitmap
class for rectangular bitmaps. -
(Flash 10) Manage my own list of objects, and then build up a vector of
IGraphicsData
objects, and pass that toGraphics.drawGraphicsData
. This puts the burden on me to handle translation and rotation, and I don't see a great way to add filters. Here too I can use eitherbeginFill
+lineStyle
orbeginBitmapFill
for vector or bitmap drawing. -
(Flash 10) Manage my own list of objects, build up a vector of vertices and another vector of texture coordinates, and use
Graphics.drawTriangles
. I have to handle translation and rotation of the vertices, but Flash will handle the translation, rotation, and scaling of the textures.
Note that there's actually a lot more flexibility than just three
options. There are three ways to draw polygons (lineTo
, drawPath
,
or drawTriangles
), two object management approaches (using
Sprite
, or managing the list manually), and whether to defer
drawing using IGraphicsData
. Which approach should I use for a game
like my spaceship
demo?
Convenience
One of the things I love about Flash is that drawing is pretty
convenient, especially with Sprite
objects, bitmap handling,
translation/rotation, easy filters like drop shadow, and vector
drawing. For example:
var s:Sprite = new Sprite(); s.beginBitmapFill(texture, null, false, true); s.drawPath (Vector.<int>([1, 2, 2, 2]), Vector.<Number>([100, 0, 164, 0, 164, 64, 100, 64])); s.endFill();
To rotate or translate the game objects, set the s.x
, s.y
, and
s.rotation
fields. When you need a new game object, instantiate a
Sprite
and insert it into the hierachy; when you want to remove one,
remove the shape. Flash handles the rest.
With Flash 10, the IGraphicsData
interface lets you store graphics
commands in objects, and then draw with them later. You can either
call commands similar to the Flash 9 API, or you can store graphics
data in a more direct form. Once you've assembled all the graphics
objects, you can draw them all using Graphics.drawGraphicsData
.
v[0] = new GraphicsBitmapFill(texture, null, false, true); v[1] = new GraphicsPath (Vector.<int>([1, 2, 2, 2]), Vector.<Number>([100, 0, 164, 0, 164, 64, 100, 64])); v[2] = new GraphicsEndFill(); graphics.drawGraphicsData(v);
You can draw each of these to a separate Sprite
, or you can assemble
them all into one big vector and make a single call to
Graphics.drawGraphicsData
. The main value I think is to be able to
batch them up, so I decided to make a single call, but that means I
need to manage translation and rotation myself. Instead of creating
new graphics data objects, you can update them every frame. Each of
my game objects can keep a reference to where in the graphics data
array it is represented, and then when I want to update the game
object, I can change the corresponding GraphicsPath
object. For
rotation, I need to edit the path and also the bitmap rotation in the
GraphicsBitmapFill
object.
Adding and removing game objects is a little trickier. Either I can rebuild the entire vector every time, or I can build something similar to a memory allocator, so that I can reuse parts of the vector. I haven't yet decided how I'm going to handle this (my tests so far have a fixed number of game objects).
The Flash 10 Graphics.drawTriangles
interface is somewhat
inconvenient when working with 2D data. It requires that all polygons
be decomposed into triangles (not hard to do but it's an extra
step). The function takes a list of vertices, a list of indices that
point into the vertex vector, and texture coordinates.
graphics.beginBitmapFill(texture, null, false, true); graphics.drawTriangles (Vector.<Number>([100, 0, 164, 0, 164, 64, 100, 64]), Vector.<int>([0, 1, 2, 2, 3, 0]), Vector.<Number>([0, 0, 1, 0, 1, 1, 0, 1])); graphics.endFill();
Here too I need to handle translation and rotation myself, by updating
the vector. However I don't need to rotate the bitmaps because
drawTriangles
takes care of this for me. Adding and removing
objects is similar to the previous case, in that I need to handle it
myself.
Overall, the traditional API is most convenient, but deferring the
drawPath
by putting it into a GraphicsDataPath
is convenient
too. The drawTriangles
interface is least convenient, except for not
needing to rotate bitmaps.
Performance
I've found that my intuition hasn't been a great guide for
understanding Flash performance. My intuition told me that approach 1
(Sprite
objects) would be slower than approach 2 (global graphics
data list), and approach 3 (triangles) would be fastest of all.
I wrote a test program that drew 400–1000 square objects on the screen at once and moved them around. I also put in options for rotation (because many of the objects in the spaceship demo were rotating), use bitmap or vector graphics (I like noise in art and bitmaps seem to be the way to make that), using outlines for vector graphics (I like outlines!), and animating bitmaps.
My general conclusions, for this style of game:
- Vector graphics are faster than bitmap graphics (especially when rotation is involved), but that using outlines on the vector graphics slows them down, about as much as bitmaps do. Also, if the vector graphics have any additional complexity, the bitmaps end up being faster. Since I almost always want outlines or some other edge effect, bitmaps shouldn't cost me that much, and they'll give me a lot more options for adding details.
-
Using the separate
Sprite
objects is about the same speed as using a singleIGraphicsData
vector. Sprite objects are more convenient. However, once I want animation, theIGraphicsData
approach shines: I can swap in alternative paths and bitmaps at no cost, whereas with drawing directly, I have to redraw to get animation (it's possible that I can create lots ofSprite
objects and then swap them out but I haven't tried this). -
For consistent frame rates, it's best to minimize allocation, and reuse some objects. The
IGraphicsData
objects, the vector of them, and the vectors fo vertex and index data all can be reused from frame to frame, with additional complexity to track where everything is. -
With the triangle API, I can put all my textures into one big bitmap, and then access them using texture coordinates. This allows me to make a single
drawTriangles
call. However, using hundreds of ofGraphicsBitmapFill
andGraphicsDrawPath
objects is suprisingly faster than using a singledrawTriangles
call. I was unable to make the triangle approach faster.
The performance of drawTriangles
(even when I used
GraphicsTrianglePath
) was a disappointment. My thanks to Alex from Wild Shadow Studios for his help in understanding the APIs and performance.
Rendering
It wasn't enough for drawTriangles
to be slower for this type of game. I've also had issues with the bitmap rendering. Here's a 64x64 test image drawn in several ways:
-
A
Bitmap
object. -
A polygon drawn with
beginBitmapFill
. -
A
Bitmap
object rotated 90 degrees clockwise. -
A polygon drawn with
beginBitmapFill
, rotated 90 degrees. -
Two triangles drawn with
drawTriangles
, and u/v range from 0 to 1. - Same as 5, except u/v adjusted by 1/64.
- Same as 5, rotated 90 degrees.
- Same as 6, rotated 90 degrees.
Note that images 1 and 2 look the same. This is as it should be. If I believe the documentation, image 5 should look the same as 1 and 2. But it doesn't.
As far as I can tell, drawTriangles
is assigning a u/v coordinate of
1.0 to be pixel 62 instead of pixel 63. If you look closely, you can
see the gray pixels are getting blurred (stretched). A “fix” for this
is in image 6, where I set the u/v coordinate to 1.0 + 1/64, so
that it picks pixel 63. With this hack, the image matches the
reference image 1.
Images 3 and 4 are the same as 1 and 2, but rotated 90 degrees clockwise. Images 7 and 8 are the same as 5 and 6, rotated 90 degrees clockwise. Notice that 7 is incorrect (it should match 3), but it's incorrect in a different way than 5. Instead of losing the yellow and blue edges, it loses the green and yellow edges. The hack in image 6, when applied to 8, fails. That means it's not as obvious as pixel 62 instead of 63; there must be a weirder issue that I don't understand.
These issues with drawTriangles
rendering make it hard to recommend
for 2D graphics. I don't need everything to be exactly the same, but I
do plan to draw 1-pixel outlines around my sprites, and
drawTriangles
can't preserve the outline. Even worse, as I rotate my
game's objects, the outlines will appear and disappear.
The drawTriangles
API also has some weird effects when using Flash's
built in zoom function (right click and choose Zoom In). It also
doesn't seem to be happy when using bitmap fills with repeat
set to
true. And it doesn't seem to handle texture scaling as well as the
regular APIs (it's possible they aren't using
mipmaps).
Conclusions
For both convenience and performance for games like my spaceship demo,
I think I should have every game object store some IGraphicsData
objects, and also store a vector of all of them. I had thought that
drawTriangles
might be better, but it's both slower and less
convenient. It also doesn't look as good. I think I might take a look
at drawTriangles
for 3D perspective graphics, or if I had lots of
triangle data, but for 2D or orthographic 3D, I think it's probably
not the best choice for me right now.
Labels: flash