In this tutorial I’m going to expand on my previous example TestGame featured in my post Getting Started with the Cocos2d Game Engine. This follow up borrows heavily from the cocos2d sample file SpritesTest. If you haven’t seen the SpritesTest demo, see my article on Running the Cocos2d examples. The objective is to reverse engineer that demo and use it to show you how to build your own test app.
Why Build a Test App ?
My background is in testing software libraries. I have a lot of experience breaking down functionality into manageable test harnesses. The test harnesses usually let you select one isolated feature and test it. It’s a great way for learning how a library works. It’s also a great way to try something. By building a test harness you don’t have to build a whole project from scratch just to test one feature. This is especially handy when you run into features that you can only test on the actual device.
Getting Started
In order to complete this tutorial you first you need to go through the steps of my post Getting Started with the Cocos2d Game Engine.
If you are using git you may want to save and tag the original project and try expanding on it here in a new branch.
Graphic Files
For this tutorial you will need to copy some additional graphic files from the cocos2d-iphone sample folders to your projects Resources folder:
- Resources/Images/grossini images/grossini.png (male cartoon character)
- Resources/Images/grossini images/grossini_sister1.png (female cartoon character)
- Resources/Images/menu related images/b1.png (back arrow)
- Resources/Images/menu related images/b2.png (back arrow highlighted)
- Resources/Images/menu related images/f1.png (next arrow)
- Resources/Images/menu related images/f2.png (next arrow highlighted)
- Resources/Images/menu related images/r1.png (reset button)
- Resources/Images/menu related images/r2.png (reset button highlighted)
Classes / TestGameAppDelegate.m
For this tutorial I’ve borrowed from the SpritesTest code and added some new functionality to TestGameAppDelegate.m:
// // TestGameAppDelegate.m // TestGame // // Created by Mitchell Allen on 9/16/09. // Copyright __MyCompanyName__ 2009. All rights reserved. // // Some code and images borrowed from cocos2d SpritesTest sample code. // // See these links for additional info and credit: // http://mitchallen.com/iphone/archives/371 // http://mitchallen.com/iphone/archives/304 #import "TestGameAppDelegate.h" @implementation TestGameAppDelegate - (void)applicationDidFinishLaunching:(UIApplication *)application { UIWindow *window = [[UIWindow alloc] initWithFrame: [[UIScreen mainScreen] bounds]]; [window setUserInteractionEnabled:YES]; [window setMultipleTouchEnabled:YES]; // DEPRECATED: [[Director sharedDirector] setLandscape: YES]; [[Director sharedDirector] setDeviceOrientation: CCDeviceOrientationLandscapeLeft ]; [[Director sharedDirector] attachInWindow: window]; // display FPS (useful when debugging) [[Director sharedDirector] setDisplayFPS:YES]; // frames per second [[Director sharedDirector] setAnimationInterval:1.0/60]; [window makeKeyAndVisible]; // Default texture format for PNG/BMP/TIFF/JPEG/GIF images // It can be RGBA8888, RGBA4444, RGB5_A1, RGB565 // You can change anytime. [Texture2D setDefaultAlphaPixelFormat:kTexture2DPixelFormat_RGBA8888]; TestScene *game = [TestScene node]; [[Director sharedDirector] runWithScene: game]; } // getting a call, pause the game -(void) applicationWillResignActive:(UIApplication *)application { [[Director sharedDirector] pause]; } // call got rejected -(void) applicationDidBecomeActive:(UIApplication *)application { [[Director sharedDirector] resume]; } // purge memroy - (void)applicationDidReceiveMemoryWarning:(UIApplication *)application { [[TextureMgr sharedTextureMgr] removeAllTextures]; } - (void) dealloc { [super dealloc]; } // next delta time will be zero -(void) applicationSignificantTimeChange:(UIApplication *)application { [[Director sharedDirector] setNextDeltaTimeZero:YES]; } @end
Test Scene
There are no changes to TestScene from the previous project. As a reminder:
- A simple TestScene class is defined based on cocos2d Scene class
- The TestScene loads a TestLayer based on a cocos2d Layer class
Test Layer
There have been several changes made to TestLayer.
- Now TestLayer serves as a base class for additional test layers that you can customize based on what you want to test
- It manages the loading of two sprites from the cocos2d samples: grossini and tamara
- It loads a graphical menu for navigating between layers using arrows and restarting a layer
- It has several functions to work with the menu to switch between layers
- It has a centerSprites method for centering the two test sprites
- It has a title method used by the UI that can be overridden by derived classes
Classes/TestScene.h
Both TestScene and TestLayer are defined in Classes/TestScene.*
// // TestScene.h // TestGame // // Created by Mitchell Allen on 9/16/09. // Copyright 2009 __MyCompanyName__. All rights reserved. // // See these links for additional info and credit: // http://mitchallen.com/iphone/archives/371 // http://mitchallen.com/iphone/archives/304 #import #import "cocos2d.h" #import "chipmunk.h" #pragma mark - @interface TestScene : Scene { } @end #pragma mark - @interface TestLayer : Layer { Sprite * grossini; Sprite *tamara; } -(void) centerSprites; -(NSString*) title; -(void) setLayer: (Class) classLayer; -(void) backCallback:(id) sender; -(void) nextCallback:(id) sender; -(void) restartCallback:(id) sender; @end
Classes/TestScene.m
One important thing to note here is the global static transitions string array. When you add a new class based on TestLayer (see MyTests.* below), you need to add a string containing the name to the array. The navigation code uses NSClassFromString to convert the currently indexed string to a layer object to load.
// // TestScene.m // TestGame // // Created by Mitchell Allen on 9/16/09. // Copyright 2009 __MyCompanyName__. All rights reserved. // // See these links for additional info and credit: // http://mitchallen.com/iphone/archives/371 // http://mitchallen.com/iphone/archives/304 // #import "TestScene.h" #pragma mark - @implementation TestScene -(id) init { self = [super init]; if( self != nil ) { [self addChild: [TestLayer node] z:1]; } return self; } @end #pragma mark - static int sceneIdx=-1; /////////////////////////////////////////////////// // In order for a test layer to appear you must // add a string for its class name to this array. // Test layers are defined in MyTests.* static NSString *transitions[] = { @"TestLayer01", @"TestLayer02" }; Class nextAction() { // Retrieve the name of the next layer class // and return a class based on that string. sceneIdx++; sceneIdx = sceneIdx % ( sizeof(transitions) / sizeof(transitions[0]) ); NSString *r = transitions[sceneIdx]; Class c = NSClassFromString(r); return c; } Class backAction() { // Retrieve the name of the previous layer class // and return a class based on that string. sceneIdx--; if( sceneIdx < 0 ) sceneIdx = sizeof(transitions) / sizeof(transitions[0]) -1; NSString *r = transitions[sceneIdx]; Class c = NSClassFromString(r); return c; } Class restartAction() { // Retrieve the name of the current layer class // and return a class based on that string. if( sceneIdx < 0 || sceneIdx >= sizeof(transitions) ) return nil; NSString *r = transitions[sceneIdx]; Class c = NSClassFromString(r); return c; } #pragma mark - @implementation TestLayer -(id) init { self = [super init]; if(self != nil) { // Initialize and add test images so they can be used by derived test layers. // Note that because we 'retain' the images, // we will need to release them in dealloc. grossini = [[Sprite spriteWithFile:@"grossini.png"] retain]; tamara = [[Sprite spriteWithFile:@"grossinis_sister1.png"] retain]; [self addChild: grossini z:1]; [self addChild: tamara z:2]; [self centerSprites]; CGSize s = [[Director sharedDirector] winSize]; // Display the title property for the current layer. Label* label = [Label labelWithString:[self title] fontName:@"Arial" fontSize:32]; [self addChild: label]; [label setPosition: ccp(s.width/2, s.height-50)]; // Create a graphics-based layer navigation menu to be used by all derived test layers. MenuItemImage *item1 = [MenuItemImage itemFromNormalImage:@"b1.png" selectedImage:@"b2.png" target:self selector:@selector(backCallback:)]; MenuItemImage *item2 = [MenuItemImage itemFromNormalImage:@"r1.png" selectedImage:@"r2.png" target:self selector:@selector(restartCallback:)]; MenuItemImage *item3 = [MenuItemImage itemFromNormalImage:@"f1.png" selectedImage:@"f2.png" target:self selector:@selector(nextCallback:)]; Menu *menu = [Menu menuWithItems:item1, item2, item3, nil]; menu.position = CGPointZero; item1.position = ccp(480/2-100,30); item2.position = ccp(480/2, 30); item3.position = ccp(480/2+100,30); [self addChild: menu z:1]; } return self; } -(void) centerSprites { // Take our two test sprites and center them on the screen. CGSize s = [[Director sharedDirector] winSize]; [grossini setPosition: ccp(s.width/3, s.height/2)]; [tamara setPosition: ccp(2*s.width/3, s.height/2)]; } -(NSString*) title { // Create a default title for the opening layer. // Test layers should override this call. return @"Press Arrows to Continue"; } -(void) dealloc { // Since our test sprites were retained, // we must now release them. [grossini release]; [tamara release]; [super dealloc]; } -(void) setLayer: (Class) classLayer { // Replace the current layer with one based another class object. if( classLayer == nil ) return; Scene *s = [Scene node]; [s addChild: [classLayer node]]; [[Director sharedDirector] replaceScene: s]; } -(void) restartCallback: (id) sender { // When the user clicks the restart button // (the green circle in the current UI) // reload the current layer, // triggering a restart of the action [self setLayer: restartAction()]; } -(void) nextCallback: (id) sender { // When the user clicks the next arrow // (the green right arrow in the current UI) // load the next layer in the list. [self setLayer: nextAction()]; } -(void) backCallback: (id) sender { // When the user clicks the back arrow // (the green left arrow in the current UI) // load the previous layer in the list. [self setLayer: backAction()]; } @end
Classes/MyTests.h
When adding a new derived TestLayer you generally don’t have to do a lot in the header file (unless you want to add more functions and things like retained sprites to track). For simple layers you just need to define interfaces based on TestLayer.
// // MyTests.h // TestGame // // Created by Mitchell Allen on 9/20/09. // Copyright 2009 __MyCompanyName__. All rights reserved. // // See these links for additional info and credit: // http://mitchallen.com/iphone/archives/371 // http://mitchallen.com/iphone/archives/304 #import "TestScene.h" /////////////////////////////////////////////////////// // // Adding Test Layers to Test Game // // When a dervied TestLayer is added, you must add it's name to // the NSString transitions array in TestScene.m for it to appear // in the test game. @interface TestLayer01 : TestLayer {} @end @interface TestLayer02 : TestLayer {} @end
Classes/MyTests.m
For simple layers you just need to define two things:
- An onEnter method for setting up the actions in your layer
- A title method that overrides the base TestLayer title method and returns a pointer to an NSString used by the UI
You also need to remember this important step:
- When you add a new layer you need to add the name as a string to the transistions array in TestScene.m
For this tutorial I just borrowed the SpritesTest demo code from the SpriteMove and SpriteRotate classes.
You can use this file to add as many test layers as you would like.
// // MyTests.m // TestGame // // Created by Mitchell Allen on 9/20/09. // Copyright 2009 __MyCompanyName__. All rights reserved. // // See these links for additional info and credit: // http://mitchallen.com/iphone/archives/371 // http://mitchallen.com/iphone/archives/304 #import "MyTests.h" #pragma mark - @implementation TestLayer01 -(void) onEnter { // Code borrowed from cocos2d sample: // SpritesTest.m - SpriteMove class [super onEnter]; CGSize s = [[Director sharedDirector] winSize]; id actionTo = [MoveTo actionWithDuration: 2 position:ccp(s.width-40, s.height-40)]; id actionBy = [MoveBy actionWithDuration:2 position: ccp(80,80)]; id actionByBack = [actionBy reverse]; [tamara runAction: actionTo]; [grossini runAction: [Sequence actions:actionBy, actionByBack, nil]]; } -(NSString *) title { // Override the base classes title // that appears on the screen. return @"Test Layer 01 - Move"; } @end #pragma mark - @implementation TestLayer02 -(void) onEnter { // Code borrowed from cocos2d sample: // SpritesTest.m - SpriteRotate class [super onEnter]; [self centerSprites]; id actionTo = [RotateTo actionWithDuration: 2 angle:45]; id actionBy = [RotateBy actionWithDuration:2 angle: 360]; id actionByBack = [actionBy reverse]; [tamara runAction: actionTo]; [grossini runAction: [Sequence actions:actionBy, actionByBack, nil]]; } -(NSString *) title { // Override the base classes title // that appears on the screen. return @"Test Layer 02 - Rotate"; } @end
Compile and Run
- Click: Build and Go
Git Clone
If you have git installed, you can clone a copy of this project with the following command:
git clone git://github.com/mitchallen/TestGame.git
For my next tutorial I will be adding code to the repository. So to get back to the original state of this tutorial, you will need to execute the following at the command line:
git checkout v200
To get back to the state used in the previous tutorial you can use:
git checkout v100
Both v100 and v200 are tags that I’ve set on the master branch.
Tags: Cocos2D
October 30th, 2009 at 10:49 am
[...] Creating a Cocos2D Layer Test Harness [...]