Skip to content
Nicky Weber edited this page Jan 6, 2015 · 22 revisions

Packages

Table of Contents

Please report Sprite Builder bugs and feature requests to [Sprite Builder's github page](https://github.com/Sprite Builder/Sprite Builder/). Cocos2d bugs and feature requests to Cocos2d's github home. If you need help using packages the [Sprite Builder forum](http://forum.Sprite Builder.com/) or Cocos2d forum are good places to start. We are looking forward to getting your feedback.

Other resources:

A package is a zipped folder usually created with Spritebuilder. The general idea is to add content to an app during runtime which is located on a remote host. A package can be used to add any kind of file to an app. Another nice feature is to patch existing assets, like add higher quality assets to an app that has been shipped with small sized assets to stay below the threshold to download from a mobile connection.

Within an app a package is unique by it's name and resolution. OS is implicitly determined.

Central organ of the packages module is the CCPackageManager. It's supposed to be used and accessed as a singleton. Use the class method sharedManager to get hold of it.

You can easily create your own packages without Spritebuilder by keeping the simple structure in mind described in Anatomy of a Package.

General Flow of the Package Manager

  • I. Download a package from a remote host using http
  • II. Unzipping after download
  • III. Install: Copy contents of unzipped archive to the installation folder, usually /Library/Caches/Packages/<package-identifier> (See naming convention)
  • IV. Enable in Cocos2d: Include new packages in search path, reload all sprite sheets and filename lookups

(Click to enlarge)

Package Manager Flow

Getting Started

First of all you will need a package to be downloadable on a host. Note down the URL. Let's assume a package archive name: DLC-iOS-phonehd.zip (See below for naming conventions). The full URL is http://foobar.com/packages/DLC-iOS-phonehd.zip. OS is iOS and the device is an iPhone 4+ as the resolution will be derived from the device. See chapter Use Cases for more details about resolutions.

1. Add the Package to your App

To make the package available in your app the only the following lines are needed:

// 1.
[CCPackageManager sharedManager].baseURL = [NSURL URLWithString:@"http://foobar.com/packages"];
// 2.
[[CCPackageManager sharedManager] downloadPackageWithName:@"DLC"
// 3.
                                      enableAfterDownload:YES]
// 4.                                      
[CCPackageManager sharedManager].delegate = aDelegate;                                                                            
  1. Is setting the baseURL to the host, note that the package file name is removed. This is a one time setup, especially helpful if you have several packages ready for download. This method also returns a freshly created CCPackage instance if needed.
  2. This schedules a download as mentioned in the general flow of the Package Manager
  3. If enableAfterDownload: is set to NO the package is installed but not enabled in cocos2d
  4. Provide an instance adopting the CCPackageManagerDelegate protocol and implementing the required methods. This will make things a lot easier if something goes wrong.

Note: The final URL is created as the OS and resolution is known. The resolution is determined by CCFileUtils' searchResolutionsOrder firs entry which is mapped to a Sprite Builder resolution(phone, phonehd, tablet or tablethd).

2. Loading and Saving Packages

Loading is done in [self setupCocos2dWithOptions:cocos2dSetup]. So you don't have to do anything.

Note: Calling the loadPackages method more than once won't do anything. If you need a full reset, look at the Use Cases chapter.

Saving is done for you in CCAppDelegate's methods:

- (void)applicationDidEnterBackground:(UIApplication*)application

- (void)applicationWillTerminate:(UIApplication *)application

- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application

Note: If you implement those methods in your own AppDelegate, don't forget to call super!

3. Progress Feedback

Processing packages is time consuming and therefore done in the background. The CCPackageManagerDelegate protocol provides several methods to let you know when a certain step is finished(or failed). For fine grained feedback on downloads and unzipping there are optional delegate methods to give a regular update on progress.

4. Using a package

After packageInstallationFinished:is called on your package manager'S delegate instance it should be ready. You can access your assets as usual: Create sprites, load sounds and scenes etc using the corresponding classes.

Naming Convention

The <package identifier> should follow the naming convention <name>-<os>-<resolution>

  • name: freely chosen name, try to avoid white spaces
  • os: iOS, Android, Mac
  • resolution: usually phone, phonehd, tablet, tablethd

A zipped package is named <name>-<os>-<resolution>.zip

Anatomy of a Package

A zipped package should contain only one folder with the naming convention of the package identifier. That folder is the package folder. That folder can contain anything you'd like to integrate in your app.

The loading mechanism for assets is based on cocos2d's CCFileUtils class. You can structure the contents of a packages freely: Use nested folders or just put every asset on the top level. Usually you don't need to access CCFileUtil's methods at all to load sprites or play audio. Please refer to cocos2d's documentation for details.

Usually the content will be similarly structured like the Published folder created by Spritebuilder, keeping the project's folder structre intact.

If you have spritesheets there should be a spriteFrameFileList.plist file located at the top level within the package folder. It hosts a list of all spritesheets that should be available during runtime. It's loaded at startup of cocos2d.

Format is XML looking like this for two spritesheets:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>metadata</key>
	<dict>
		<key>version</key>
		<integer>1</integer>
	</dict>
	<key>spriteFrameFiles</key>
	<array>
		<string>moreSprites.plist</string>
		<string>ccbResources.plist</string>
	</array>
</dict>
</plist>

A fileLookup.plist should be present if files are aliased. Usually this is only done by Spritebuilder, like an origin wave file converted to ogg for Android target. This file is generated by Spritebuilder during publishing.

An example for two files published as jpegs:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>filenames</key>
	<dict>
		<key>ccbResources/ccbButtonHighlighted.png</key>
		<string>ccbResources/ccbButtonHighlighted.jpg</string>
		<key>ccbResources/ccbButtonNormal.png</key>
		<string>ccbResources/ccbButtonNormal.jpg</string>
	</dict>
	<key>metadata</key>
	<dict>
		<key>version</key>
		<integer>1</integer>
	</dict>
</dict>
</plist>

An example for a package's contents called DLC targeted for iOS, resolution is phonehd, containing a spritesheet:

DLC-iOS-phonehd.zip
	/DLC-iOS-phonehd
		/sounds
			mow.mp4
		/resources-phonehd
			spritesheet_spaceships.png
			spritesheet_spaceships.plist
			non_spritesheet_sprite.png
		spriteFrameFileList.plist
		fileLookup.plist

Packages created with Sprite Builder meet all these requirements.

Patching content

Patching content is simply using the same asset name as in the main bundle. The ordering of Cocos2d search paths is important. Usually the package manager will make packages take precedence over the main published resources folder. See also notes below regarding taking effect of patched scenes/sprites.

General Notes

  • It is not recommended to install more than one resolution of a package on a device. This can lead to unwanted search path ordering and wrong assets being loaded.

  • After installing and enabling a package patched assets won't take effect in present scenes residing in memory. You will have to reload those scenes/assets.

  • From Apple's documentation regarding the /Library/Caches folder default location for installed packages:

    • Use this directory to write any app-specific support files that your app can re-create easily. Your app is generally responsible for managing the contents of this directory and for adding and deleting files as needed.
    • In iOS 2.2 and later, the contents of this directory are not backed up by iTunes. In addition, iTunes removes files in this directory during a full restoration of the device.
    • On iOS 5.0 and later, the system may delete the Caches directory on rare occasions when the system is very low on disk space. This will never occur while an app is running. However, you should be aware that iTunes restore is not necessarily the only condition under which the Caches directory can be erased.
  • /Library/Application Support might also be a good place to install packages to, the contents of this directory are backed up by iTunes.

More Use Cases

Configuring the shared instance of CCPackageManager

If you need to configure the package manager the right place to do this is before cocos2d is set up. Example:

[CCPackageManager sharedManager].resumeDownload = NO;
// More configuration code

[self setupCocos2dWithOptions:cocos2dSetup];`

Create packages without starting a download

If you're in need to get hold of a CCPackage instance before starting any download, you can create that with the initializers found in CCPackage.h. Add the package to the package manager by calling addPackage:. The package is now registered in the package manager and will be persisted. You can then start the download with CCPackageManager's downloadPackage:enableAfterDownload: method.

This can be handy if like to write a full fledged UI for package management.

Disabling (Enabling) a Package

If you just want to disable a package without deleting it permanently from disk use CCPackageManager's disablePackage: method. Scenes and sprites will have to reloaded to make changes visible. Same applies to enabling, use enablePackage:

Download from a custom URL, no baseURL

If packages are in a CDN there may be no baseURL and even a filename might not be available in the URL of a package. There are two ways to get this done:

Use CCPackage's initializer

- (instancetype)initWithName:(NSString *)name
                  resolution:(NSString *)resolution
                   remoteURL:(NSURL *)remoteURL;

and provide the full URL aby baseURL set will be ignored for this package. Then add and start downloading with the package manager.

A shortcut approach is to use CCPackageManager's

- (CCPackage *)downloadPackageWithName:(NSString *)name 
                            resolution:(NSString *)resolution 
                             remoteURL:(NSURL *)remoteURL 
                   enableAfterDownload:(BOOL)enableAfterDownload;

to start downloading immediately.

Custom requests / Special needs for auth

If you need to get a hold of the request object to add special headers or payload just implement the CCPackageManagerDelegate's method

- (void)request:(NSMutableURLRequest *)request ofPackage:(CCPackage *)package;

You can do whatever you need to make the download possible. Note that the Range header will be set after this method returns in case of a resumed download. To turn off resuming of downloads set CCPackageManager's property resumeDownloads to NO.

Default resolution and how to change it

If you don't specify the resolution to download or to create an instance of a CCPackage class it will be derived from the device cocos2d is running on. More precisely the CCFileUtils's searchResolutionsOrder property is first setup depending on the device and then a Sprite Builder resolution(phone, phonehd, tablet or tablethd) is mapped depending on the first mappable entry. If there is nothing out of the ordinary in that array then every entry can be mapped and it is safe to say the first one is used. The mapping is as follows:

CCFileUtilsSuffixiPhone5HD
CCFileUtilsSuffixiPhone5
CCFileUtilsSuffixiPhoneHD
CCFileUtilsSuffixDefault
    -> phonehd

CCFileUtilsSuffixiPhone
    -> phone

CCFileUtilsSuffixiPadHD
CCFileUtilsSuffixMacHD
    -> tablethd

CCFileUtilsSuffixMac
CCFileUtilsSuffixiPad
    -> tablet 

So if you are in need of a different default resolution you can change the searchResolutionsOrderarray. However this has an impact on resolving searches for an asset. The easiest way would be to use the following methods without reconfiguring cocos2d:

CCPackage:

- (instancetype)initWithName:(NSString *)name
                  resolution:(NSString *)resolution
                   remoteURL:(NSURL *)remoteURL;                                     

CCPackageManager:

- (CCPackage *)downloadPackageWithName:(NSString *)name 
                            resolution:(NSString *)resolution 
                   enableAfterDownload:(BOOL)enableAfterDownload;
                   
// or                   
                   
- (CCPackage *)downloadPackageWithName:(NSString *)name
                            resolution:(NSString *)resolution
                             remoteURL:(NSURL *)remoteURL
                   enableAfterDownload:(BOOL)enableAfterDownload;                   

Just make sure the that your package's resolution is within the searchResolutionsOrder, otherwise the assets may not be found.

Pausing and Resuming Downloads

CCPackageManager provides several methods to pause and resume single packages as well as all downloads.

Unzipping on a Custom Queue

Set CCPackageManager's property unzippingQueue. Default is the global low priority queue.

Passwords and Zip Archives

Implement the optional delegate method passwordForPackageZipFile: to provide a password for a certain package. See also FAQ regarding zip and passwords.

Removing all Packages

Get a hold on all packages, iterate and call CCPackageManager's deletePackage: with every pacakge like this:

// 1.
for (CCPackage *aPackage in [[CCPackageManager sharedManager].allPackages copy])
{
    NSError *error;
    if (![[CCPackageManager sharedManager] deletePackage:aPackage error:&error])
    {
    	// 2.
        NSLog(@"Something went wrong: %@", error);
    }
}
  1. Use a copy of the array since it will be mutated while deleting packages.
  2. Pass an error object by reference to get more details about a failed delete operation

Troubleshooting

Although installing packages should be as simple as possible there are some things that can go wrong. First of all check the different CCPackageManagerDelegate methods and the error objects:

- (void)packageDownloadFailed:(CCPackage *)package error:(NSError *)error;

- (void)packageUnzippingFailed:(CCPackage *)package error:(NSError *)error;

- (void)packageInstallationFailed:(CCPackage *)package error:(NSError *)error;

Make sure to enable cocos2d debug logging by defining COCOS2D_DEBUG=2 for example in your project's preprocessor macros.

If that doesn't give you the right clue, follow these steps:

Download Issues

Unzipping Issues

  • Was your package archived with zip? Verify on the command line to unzip it with:
$ unzip yourpackage.zip
  • Does your package need a password? First try manually as in the example above and check if your client actually contains the right password (typo, spelling). Try copy paste the password. Have a look at the Q&A about zip archives and passwords.

Installation Issues

  • Make sure the the package is actually enabled by making sure the status is CCPackageStatusInstalledEnable.
  • Ensure your package actually works under normal conditions:
    • Check the layout of the package, refer to chapter "Anatomy of a Package"
    • Publish the package with Main Package instead of Zip in Sprite Builder and see if you can access assets. This won't make use of the packages module as this is published to the already set searchPath in cocos2d, so no further work needed here.
    • If the above work undo the publish to Main Package. Add the unzipped package as a referenced(blue) resource folder to your bundle to fake the whole downloading and unzipping process and make use of the CCPackageCocos2dEnabler:
// 1.
CCPackage *aPackage = [[CCPackage alloc] initWithName:@"packagename"
                                           resolution:@"resolution"
                                                   os:@"os"
                                            remoteURL:[NSURL URLWithString:@"http://doesnotmatter"]];

// 2.
NSString *pathToPackage = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"Resources/PackageToTest"];
// 3.
[aPackage setValue:[NSURL fileURLWithPath:pathToPackage] forKey:NSStringFromSelector(@selector(installURL))];
// 4.
CCPackageCocos2dEnabler *enabler = [[CCPackageCocos2dEnabler alloc] init];
[enabler enablePackages:@[aPackage]];

// 5.
...

This is only meant for debugging purposes!

  1. The enabler only works with packages, set the values, all the values must be set but don't need to be something valid
  2. Get the path to the package added to the bundle
  3. Set some internal value usually the installer will set with KVC
  4. Get an instance of the enabler and enable the package
  5. Access something from the bundle and see if gets loaded

FAQ

Q: Why is a download's progress total bytes 0?

A: A server has to provide the Content-Length|Content-Range headers to let the download know how big it actually will be. However this does not prevent the download from downloading more if more data is sent. If the response does provide a length, then total bytes is 0. Current downloaded bytes is not affected by this.

Q: Why do downloads start over instead of resuming?

A: A server has to support range requests. If that is not the case a paused download can only be restarted since the server will always send the full package. Also check the CCPackageManager's resumeDownloads property.

Q: Why did my password protected package get installed although I provided no password?

A: This is tricky: The SSZipArchive module and the underlying unzip.c do not test if the archive is password protected. The table of contents of a zip file can ALWAYS be read so SSZipArchive will actually create folders and files with 0 bytes or some meaningless bytes. There is no workaround except putting a lot of work into the unzip module, which we can't at the moment. Test properly. You can always add a text file to your package and read the contents. If the contents of that file compares to something you hardcode unzipping was successful.

Q: Why is my app becoming slow after I added a lot of packages?

A: CCFileUtils is the bottleneck. Although it caches file lookups it will still search recursively in all search paths(Packages and Main Resources) for a non cached asset. At the moment there is only advice: Is the package containing additional content, then enable it only when needed. CCFileUtils may get some love in the future to become more performant.

Q: Why is packageDownloadProgress | packageUnzippingProgress not called?

A: This might happen if your connection was quite fast and/or your package is not large. As long as the corresponding finished method is called everything should be fine. If you are updating a progress bar or other user interface elements it is good measure to update one last time in the finish methods with "100%" completeness.