Making animation from a texture atlas...

I’m just learning to work with texture atlases (a sheet of images loaded into a ImageView and then you focus in on an x/y and w/h coordinate to show a particular image).

I’ve got my code working where I can load a series of images into the ImageView but I am using a for loop and am not sure how I can get a delay into there to animate it. All this did was slide the texture atlas within the image view between the last two images.

Here’s my code:

-(IBAction) makedance {

[UIView beginAnimations:nil context:NULL]; 


[UIView setAnimationDuration:0.5];


//make array 
NSArray* pimages = [NSArray arrayWithObjects: 
@"0.0:0.0:108.0:149.0", 
@"108.0:0.0:108.0:149.0", 
@"216.0:0.0:108.0:149.0", 
@"324.0:0.0:108.0:149.0", 
@"432.0:0.0:108.0:149.0",
@"540.0:0.0:108.0:149.0", 
@"648.0:0.0:108.0:149.0", 
@"756.0:0.0:108.0:149.0", 
		   nil];

int i = [pimages count];


for(int z=0; z<i; z++) {
	
	NSString* coordinateslist = [pimages objectAtIndex:z];
	NSArray* coordinatesarray = [coordinateslist componentsSeparatedByString:@":"];
	int x = [[coordinatesarray objectAtIndex:0] floatValue];
	int y = [[coordinatesarray objectAtIndex:1] floatValue];
	int w = [[coordinatesarray objectAtIndex:2] floatValue];
	int h = [[coordinatesarray objectAtIndex:3] floatValue];
	
	princessframes.layer.contentsRect = CGRectMake(x/1024.0, y/1024.0, w/1024.0, h/1024.0);
	[princessframes setBounds:CGRectMake(princessframes.bounds.origin.x,princessframes.bounds.origin.y,
										 (princessframes.layer.contentsRect.size.width)*1024.0, 
										 (princessframes.layer.contentsRect.size.height)*1024.0)];
	
}

[UIView commitAnimations];
//loop through array	

}

Through a lot of trial and error and some nifty Googling I solved this. Feel free to critique I in no way think I am beyond noob at this:

  1. Assume: a .png file with all your graphics on it and an xml file with the coordinates for those graphics. I looked at the PlantsVsZombies resources and pseudo copied their format for the xml. Include them in your project. The xml looks like this. :
<?xml version="1.0"?>
<Resources id="princessanimation">
	<SetDefaults path="images" imageprefix="princess_">
		<Atlas id="1">
			<AtlasEntry id="princess1" x="0" y="0" w="108" h="149" />
			<AtlasEntry id='princess2' x='108.0' y='0.0' w='108.0' h='149.0' />
			<AtlasEntry id='princess3' x='216.0' y='0.0' w='108.0' h='149.0' />
			<AtlasEntry id='princess4' x='324.0' y='0.0' w='108.0' h='149.0' />
			<AtlasEntry id='princess5' x='432.0' y='0.0' w='108.0' h='149.0' />
			<AtlasEntry id='princess6' x='540.0' y='0.0' w='108' h='149.0' />
			...//there are 22 frames 
		</Atlas>
	</SetDefaults>
</Resources>

You’ll need to add the QuartzCore framework to the project. (Framework–>Add–>existing framework). My .h file is set up as this:

#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>

@interface textureatlastest2ViewController : UIViewController {

NSMutableArray* addresses; 
NSMutableArray* frames;
NSXMLParser *addressParser;
int frameIndex;
NSString *pathToXML;
IBOutlet UIImageView * frameholder;
IBOutlet UIButton* btnStartAnimation; 

}

  • (void)parseXMLFile:(NSString *)pathToFile;
    -(IBAction) startanimation;
    -(void)tFire:(id)sender;

@end

in the .m file. The process works like so - on viewDidLoad we get the path to the xml file and then pass it to a xml parser array. In the parser array it is allocating NSXMLParser and setting the view up as its delegate. I get fuzzy as to the process here but I believe because this is done the parse method can be fired, which actually goes through the xml and stores the data in the array frames. Then I use a NSTimer (fired on a button click) set on .1 of a second and target it at the tFire method where the images in the image view are exchanged.

// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.

  • (void)viewDidLoad {

    frames = [[NSMutableArray alloc]init];

    //build a path to the xml file
    pathToXML = [[NSBundle mainBundle]pathForResource:@“princess_atlas” ofType:@“xml”];
    //call method to parse xml file and pass path to xml file to it
    [self parseXMLFile:pathToXML];

    [super viewDidLoad];
    }

  • (void)parseXMLFile:(NSString *)pathToFile {
    //set a boolean
    BOOL success;
    //set url to xml file
    NSURL *xmlURL = [NSURL fileURLWithPath:pathToFile];

    // addressParser is an NSXMLParser instance variable
    if (addressParser) {
    [addressParser release];
    }
    addressParser = [[NSXMLParser alloc] initWithContentsOfURL:xmlURL];
    [addressParser setDelegate:self];
    [addressParser setShouldResolveExternalEntities:YES];

    // return value not used
    success = [addressParser parse];
    // if not successful, delegate is informed of error

    //go to -(void)parser method
    }

/in the parseXMLFile we alloc addressParser as an NSXMLParser, we then assign it a delegate, which in this case is “self”
which means the entire view (super view?) is the delegate. So somehow, because of that, this method is automagically
called (I think we get here from the [addressParser parse] call in the parseXMLFile method) and we can populate our
array with the data from the xml file
/

  • (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict {
    if ( [elementName isEqualToString:@“AtlasEntry”]) {

      // addresses is an NSMutableArray instance variable
      if (!addresses) { 
      	addresses = [[NSMutableArray alloc] init];
      }
      	
      NSString *thisOwner = [attributeDict objectForKey:@"id"];
      NSString *theX = [attributeDict objectForKey:@"x"];
      NSString *theY = [attributeDict objectForKey:@"y"];
      NSString *theWidth = [attributeDict objectForKey:@"w"];
      NSString *theHeight = [attributeDict objectForKey:@"h"];
      
      if (thisOwner) {
      	[frames addObject:[NSString stringWithFormat:@"%@, %@, %@, %@, %@",thisOwner, theX, theY, theWidth, theHeight]];
      }
    

    }
    }

/user clicks button, we then fire a NSTimer call, targeting the tFire method (@ selector below) for an interval of .1 second, and we have it repeating endlessly, which means every .1 second we are going to run the tFire method, we also set our frameindex here to 0 and will increment it in tFire so we can loop through the frames array/
-(IBAction) startanimation {

// we want this to be immediate
frameIndex = 0;
//[self changeImage];
[NSTimer scheduledTimerWithTimeInterval:.1 target:self selector:@selector(tFire:) userInfo:nil repeats:YES];

}

-(void)tFire:(id)sender {

/*we want this to be immediate, CATransaction is a method of Core Animation framework. Think of it as a means of
 storing all the animation data (transactions) for an object and then showing them when you make the CATransaction commit call*/
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];

/*this is where we are going to change the images in the image view, zeroing in on the x/y coordiantes we pass
 and display it for the w/h dimensions, these are all stored in the array frames*/

//declare a temp array, we'll overwrite this each time we call tFire from the startanimation method
NSArray* tempArray = [[NSMutableArray alloc]init];

//determine which index we are looking at in the frames array
int thisIndex=frameIndex; 

//get the value of that index
NSString *myAtlasEntry = [frames objectAtIndex:thisIndex];

//take those values, which will be delimited by a "," and place them into our temp array
tempArray = [myAtlasEntry componentsSeparatedByString:@", "];

//the first item is the file name, for our purposes here we don't need it but nice to have
NSString *theName = [tempArray objectAtIndex:0];

//finally, the x,y,w,h coordinates/dimensions
float x = [[tempArray objectAtIndex:1] floatValue];
float y = [[tempArray objectAtIndex:2] floatValue];
float w = [[tempArray objectAtIndex:3] floatValue];
float h = [[tempArray objectAtIndex:4] floatValue];

//pass these into our image view (frameholder) layer.contentsRect property
frameholder.layer.contentsRect = CGRectMake(x/1024.0, y/1024.0, w/1024.0, h/1024.0);
[frameholder setBounds:CGRectMake(frameholder.bounds.origin.x,
	frameholder.bounds.origin.y,
	(frameholder.layer.contentsRect.size.width)*1024.0,
	(frameholder.layer.contentsRect.size.height)*1024.0)];

//commit the changes
[CATransaction commit];

//move onto next frame
if (frameIndex < 21) { 
	frameIndex ++;
} else { 
	frameIndex = 0;
}

NSLog(@"%d", frameIndex);

}

/*this line - frameholder.layer.contentsRect = CGRectMake(x/1024.0, y/1024.0, w/1024.0, h/1024.0); - we divide everything by 1024, that’s the height and width of my .png file (in pixels), you can use any shape for your texture atlas (well, rectangle or square). I used square so I wouldn’t have to worry about mixing the numbers up. (w/divided by .png width - h/divided by .png height). You divide by the width and height of your png file to make those numbers normalized (between 0.0 and 1.0). I don’t know why they have to be normalized, Google is your friend for that one, or probably Stefan…:slight_smile: I hope this helps someone else getting started.