Notes on Performance
Many of the new graphics changes and features have “performance improvements” as the driving factor behind them. However, keep in mind that while these changes may result in noticeably better performance (especially on older devices, such as the iPhone 3GS), it may not be noticeable with all projects, nor with every device—especially given the fact that Corona was already considered “high performance” by many standards before these changes were pushed in.
Always keep in mind that nothing contributes to performance better than disciplined code, and that’s another reason why we included some of these features—to optimize certain workflows; to make you think about the decisions you make that will ultimately be the deciding factor in your app’s performance levels.
Image Groups, for example, provide you with some pretty nice under-the-hood benefits, but also pose a some limitations with the objects that use them (more details in the tutorial).
When using these new features, you will have to think about when the best time to use it is, and also weight out the performance gains vs. limitations and make the decision—on your own—which is better for your app. Ultimately, this is the type of discipline and workflow that will not only make you a better programmer, but will also help you create better apps.
One of the major contributors to performance increases is the addition of “off-screen culling”, which basically means that objects that are not on-screen will not be rendered (resulting in increased performance). The good news is, you don’t have to do anything to take advantage of this improvement—you simply download the latest build to take advantage of it!
This is a brand new API in Corona, and is also known as a “texture atlas” for those who are familiar with the technical term.
To better explain what this feature is (as well as its purpose), I’ll use a metaphor. Imagine all of the images in your app as literal “files” (e.g. sheets of paper) with a single image in the center of each page. Wouldn’t it be great if you could use up some of the white space on those pages and fit several—or even ALL—of your images on just one sheet? Well, that is essentially what this new image sheet feature allows you to do.
In the real-life scenario, you’d be saving trees. In your app, you’ll instead be preserving texture memory and optimizing graphics rendering.
In general, taking a file from the internal disk and rendering it as a texture in your app is an “expensive” task. So instead of having to perform that task for each and every image-based object in your app, you can instead put several images in a single file and Corona’s graphics engine will only have to perform that task just once for each image sheet rather than for every new object you create.
Texture Size Limit
And while there is no limit to how many images you can place in a single file, it is important to keep in mind that there is a maximum texture size limit per device. And by size, I’m talking about the actual pixel dimensions of the width and height of the image file you’ll be using as an image sheet.
You can find out what the limit is by using the following code on the device that you are targeting:
print( system.getInfo( "maxTextureSize" ) )
The good news is, on just about any device that Corona supports, the maximum limit usually mounts up to a pretty large image. Even if you have hundreds of objects in your app, chances are, you could probably fit them all into just a few image sheets (or less).
Wait, I’m already doing this!
It is very possible that you may already be implementing this type of functionality using the previous (and now deprecated) sprite API. The problem with that is, the sprite API was made to be used for animations, so there was a lot of unnecessary “baggage” that came along with any of your static objects that were created in that manner. On top of that, the API was a little confusing to say the least, and a bit tiring to use for static images just for the sake of texture memory optimization.
So with that said, you’ll be glad to know that image sheets are much more efficient, are much easier to use, were designed to be used with Corona’s existing display API’s (more on that in a moment), and are recommended for use by static images AND animations.
As an added bonus, image sheets are also compatible with dynamic image resolution support (e.g. “retina graphics”) while the old sprite API required inefficient hacks to accomplish the same. Editor’s Note: This is great news, especially in light of tomorrow’s impending iPad announcement from Apple.
How to Use Image Sheets
I’m going to go over the syntax, and show you a few examples of how to use image sheets with display.newImage() and display.newImageRect(). You can also use Image Sheets with Image Groups and Sprite Animations, but those will be covered in their own sections later on in this tutorial.
Image Sheet Syntax:
graphics.newImageSheet( filename, [baseDir, ] options )
Simple enough right? The filename parameter is the actual image file, relative to baseDir (if specified, or system.ResourceDirectory if not). The options parameter (required) is a table that holds specific data about the frames in your image sheet.
NOTE: To prevent confusion, the term image refers to the image sheet file that you are loading with graphics.newImageSheet(), and the word frame refers to an individual image within an image sheet.
There are three different “cases” that correspond to the options table, for maximum flexibility:
- Simple: Used when every frame in your image sheet has the same width and height.
- Complex: Used when frames in your image sheet have varying widths and/or heights.
- Old-Style: This case is backwards-compatible with the existing sprite API’s data format (previously used with the sprite.newSpriteSheetFromData() function).
From here on out, your options table should fall into either the simple or complex case. Since the “old style” sprite data format is now deprecated, I won’t be showing you how to use it, but it’s there to prevent breakage and a smoother transition for any third-party tools that currently export in the—now deprecated—data format.
Here’s an example of how to use the simple options case when loading an image sheet. Remember, this is for when every frame in your image sheet file is exactly the same width and height (if you can’t see the code below, go here to view it):
As you can see from the example, the simple case really is simple! The only required parameters in the options table is width, height, and numFrames. The remaining two optional parameters is if you are using dynamic resolution images (such as an @2x version of your image sheet for retina graphics), and correspond to the width and height of the original 1x source image.
The more likely scenario, especially if you’re going to be using image sheets for several different objects, is the complex-case options table. In this case, you’ll need to specify data that corresponds to every single frame in your image sheet (since they can have different values). If you cannot see the code below, go here to view it.
In the example above, the image sheet has two frames. As you can see, it’s different from the simple case because we have to define data for each frame individually within a frames array. Feel free to use the example above as a template for whenever you need to construct a complex-case options table for image sheets.
Using with Existing Image Functions
Next, I’ll show you how to use the image sheets we created in the previous examples in conjunction with display.newImage() and display.newImageRect() to create actual objects from your image sheets!
The example below assumes you’ve already created an image sheet object using either the simple or complex cases (go here to view the code if you cannot see it below):
As you can see, the display.newImage() and display.newImageRect() functions have been extended to include support for image sheets. The only difference is, when using an image sheet, instead of specifying a filename you specify the corresponding frame index number (see the ‘SYNTAX’ sections within the comments of the example).
As a reminder, the example above works whether or not you’re using the ‘simple’ or ‘complex’ data structure for your options table when loading your image sheet (as well as the old-style structure).
Image Sheet Objects Pose No Limitations
Here’s some great news for you: I mentioned limitations with some of the new features at the beginning of this post, but that does NOT apply to the new Image Sheets feature. Any display objects created with image sheets have no more limitations than objects created directly from individual image files.
Power of 2 Dimensions
Image files that will serve as image sheets must have dimensions that are a power of 2. In short, what this means that the width and height of your image sheet files should conform to the following dimensions: 8, 16, 32, 64, 128, 256, 512, 1024, 2048 (and so on). If you were to go beyond 2048, you’d simply keep doubling up; however, many devices texture limit doesn’t go beyond 2048 so chances are your image files won’t need to be any larger than that. This restriction is more of an optimization than a limitation. If your image’s width or height goes beyond any of those dimensions, say “9″ for example, your image will actually take the same amount of texture memory as if it was “16″. If you are able to size it down by 1 pixel, however, you’ll be preserving a lot more texture memory with minimal changes to your actual image file.
UPDATE: As of build 2012.264, the “power of 2″ restriction has been removed, as well as all associated issues. To reiterate, with both the old and new sprite API, your image sheets can be any dimension and still work fine. For best results, in terms of optimizing texture memory, it is still recommended that you have power of 2 dimensions, though it’s no longer required.
Removing Image Sheets
To remove an image sheet, you simply remove any objects that are using the image sheet (which includes image objects, sprites, and image groups), and then set the reference to the image sheet to nil.
Here’s a quick example:
-- obj1 and obj2 are using the image sheet
obj1 = nil
obj2 = nil
-- remove reference to the image sheet
imageSheet = nil
This is another brand-new Corona feature that, while bearing a similar name to the just-covered “image sheets” feature, is completely different. Image Groups are sub-classes of normal display groups (which was the topic of last week’s tutorial).
Here are the primary differences between Image Groups and normal Display Groups:
- Are created from an existing Image Sheet object.
- Its children must all be from the same image sheet.
- There are limitations posed to all child objects within an image group.
- Image Groups cannot be “nested” (e.g. inserted into other image groups).
Limited to a single image sheet? Limitations on child objects? You’re probably thinking, why in the world would you use Image Groups instead of regular groups, and rightfully so! But here’s what I haven’t mentioned yet:
When objects are inserted into an image group, there are several under-the-hood optimizations that normal display objects can’t take advantage of. When you need to squeeze maximum performance from your app and take advantage of all the internal graphics optimizations, it is recommended you use Image Groups—that is, if the following limitations do not hinder your app’s needs:
- Masks cannot be applied individually to objects that are inserted into an image group.
- No nested image groups, so only non-group objects that use the same image sheet as the image group can be inserted into the image group.
- No per-object blend mode (for objects inserted into the image group).
How to Use Image Groups
Remember, the Image Groups feature relies directly on Image Sheets (so if you skipped that section, you should go back and read it first).
Image Group Syntax:
display.newImageGroup( imageSheet )
The imageSheet parameter is a reference to an image sheet object created using graphics.newImageSheet(). Once you create an Image Group, you can insert child objects into the group as long as the objects were created using the same image sheet you used to create the image group.
Here’s an example (go here to view the code if you cannot see it below):
As you can see from the above example, image groups are very picky about what you put into them. If you take the time to learn when to use image groups, however—despite their the limitations they impose on their children and their “picky” nature—they can really help to solve some previously “show-stopping” issues.
Tile-based worlds with large maps is a scenario that comes to mind, where Corona would previously have performance issues on some devices, is now completely solved with the inclusion of image groups.
Removing Image Groups
You can remove image groups as you would any other display object, by using object:removeSelf() and display.remove( object ). Don’t forget to set the reference to nil after removal!
If the image group has child objects, it is recommended you remove the children individually before removing the image group.
Along with all these exciting new changes, the Corona Sprite API got a complete makeover. It is now easier than ever to create sprite-based animated objects in Corona! Here is a summary of the changes to the sprite API:
- You get to leverage image sheets (no more confusing concepts such as “sprite sheets” and “sprite sets”).
- Revised API that is straight-forward and more intuitive.
- Inherit dynamic image resolution support through image sheets. In other words, sprites now support “retina” graphics very easy.
- New properties and methods for more power and flexibility.
Additionally, you can also insert sprite objects into image groups that use the same image sheet! However, the limitations mentioned in the previous section still apply to sprite objects.
display.newSprite( [parent, ] imageSheet, sequenceData )
[Editors Note: Multisprites are not available yet. Sorry for the confusion]
The above two functions are now the only sprite-specific functions in Corona. The first, display.newSprite(), is for creating an animated object with sequences from a single image sheet.
The other function, display.newMultiSprite(), is used when an object has animation sequences that span across multiple image sheets. [Editors Note: Multisprites are not available yet. Sorry for the confusion]
sequenceData is a table that holds data for a specific sequence. Or, if you have more than one animation sequence for a single object, sequenceData is then an array of tables that hold data for each sequence. For instance, you might have a ‘character’ object with several different animation sequences such as: “walking”, “jumping”, “swimming”.
Frames in a given sequence may be consecutive (1,2,3,4,…) or non-consecutive (1,3,4,6,9,…) within the image sheet. I will provide examples for both.
Single Sequence; Consecutive Frames
This type of sequence is when the animation frames in your image sheet are in consecutive order.
Single Sequence; Non-Consecutive Frames
This is essentially the same as the previous example, but instead, you pass a ‘frames’ array that specifies the non-consecutive frameIndex order in which the animation will play.
The example below shows how to have several animation sequences for a single sprite object. Notice how you can have both consecutive and non-consecutive frame sequences in the sequenceData array.
Apart from the start/count and frames parameters, there are some sequenceData parameters that are common to all sequences. Below is a quick overview of all of them:
- name – This is a unique name for your sequence, and is used to load the sequence when it is time to play the animation (I’ll go over this in a moment).
- start & count – For consecutive-frame sequences, start represents the starting frame, and count represents how many frames from the start that the sequence should end.
- frames – For non-consecutive-frame sequences, this is an array that holds the frameIndex in the exact order animation should be played.
- time – This is the time (in milliseconds) between each frame. If this is not specified, then animation will be based on the framerate of your app.
- loopCount – This is a number that represents how many times you want the sequence to loop when it is played. Setting this to 1 will play the sequence once and then stop. The default is 0, which will loop the sequence indefinitely.
- loopDirection – This can either be “forward” or “bounce”, with “forward” being the default if don’t set this parameter. The “bounce” option will play forward, then backwards through the sequence of frames.
- [Editors Note: Multisprites are not available yet. Sorry for the confusion]
imageSheet – This should ONLY be set if you are using a multi-sprite (via display.newMultiSprite), and is a reference to the image sheet object that the sequence belongs to.
Using “Old” Spritesheet Data
Many third-party tools currently export a .lua data file in the old spritesheet data format. The good news is, you can still use these data files when creating an image sheet object. Here’s how you’d do it:
local oldStyleSpriteSheetData = require("uma").getSpriteSheetData()
local options =
spriteSheetFrames = oldStyleSpriteSheetData.frames
local imageSheet = graphics.newImageSheet( "uma.png", options )
In addition to common display object properties, sprite objects have additional properties associated with them (for now, are all read-only):
- spriteObject.frame – Read-only – Currently shown frame index of loaded sequence.
- spriteObject.numFrames – Read-only – Number of frames in currently loaded sequence.
- spriteObject.isPlaying – Read-only – Self explanatory.
Below are some sprite-specific methods associated with all objects created using display.newSprite().
- spriteObject:setSequence( name ) – This will load an animation sequence by name. If you do not specify a name parameter, then the first frame in the currently loaded sequence will be shown instead.
- spriteObject:play() – Plays the currently loaded sequence.
- spriteObject:pause() – Pauses playback at the currently shown frame in the sequence.
As with the old API, sprite objects can “listen” to sprite events with a listener function. The event properties have changed, so for now there is only a “began” and “ended” event phase.
Here’s an example:
local function spriteListener( event )
if event.phase == "began" then
-- sequence has began playing; do something
elseif event.phase == "ended" then
-- sequence has reached the last frame; do something
-- Add sprite listener
spriteObject:addEventListener( "sprite", spriteListener )
We’re working on extending the functionality of the sprite listener to make it even more useful, so stay tuned!
Removing Sprite Objects
You can remove sprite objects as you would any other display object, by using object:removeSelf() and display.remove( object ). Don’t forget to set the reference to nil after removal!
Frame Trimming Not Yet Supported
Something that’s missing from both Image Sheets and the new Sprite API is the ability to trim/crop frames in an image sheet, which is a common practice when it comes to constructing texture atlases. We are currently working on integrating this feature into the new API’s and will be rolled into the daily builds once it’s ready.
Wrapping it all up…
I wasn’t joking when I said this was going to be a long post! In this single blog post we covered Image Sheets, Image Groups, and the brand new Sprite API.
One thing I did not cover was how to construct an image sheet file from multiple images. This can be done with most image-editing software, but if you take this route, it can be difficult to place the frames accurately in the image. This is a purpose best served by third-party software.
Remember, these new features and graphics performance tune-ups are available to subscribers only from the Daily Builds page. If you’re not a subscriber and want to take advantage of what all Image Sheets, Image Groups, and the new Sprite API has to offer, subscribe now and get build 2012.761 today!