I have a "ball" sprite that changes it's texture when a certain scenario occurs. I change it's texture like this:
[ball setTexture:[[CCTextureCache sharedTextureCache] addImage:#"red.png"]];
This does work; it changes the ball sprite to use the red.png image. How would I handle this if I have about 20 balls that need to switch to using this sprite? Would I run through each ball and "addImage"?
If I could, I would like to load the texture once, save it in a variable (called like "redTexture"), and then be able to assign it to any of the ball objects.
Any advice on how to approach this would be a huge help, thank you!
If you were to put that statement inside, say, a for loop, you'd be adding the red.png image to the shared texture cache over and over again, which I doubt is what you want.
Let's back up and re-write things a bit, starting by adding the red.png image to the shared texture cache, on a line by itself:
[[CCTextureCache sharedTextureCache] addImage:#"red.png"];
You'd subsequently get the same texture again simply by calling [CCTextureCache sharedTextureCache]. Until you add another image to the shared texture cache, that is.
CCTextureCache is a singleton, and its docs don't suggest there's a way to make a copy of the shared texture cache (which would be ideal for preserving your redTexture). That being the case, just create a variable and point it at [CCTextureCache sharedTextureCache]; just be careful not to add any other images to it before you're done with it:
CCTextureCache *redTexture = [CCTextureCache sharedTextureCache];
Now let's assume you already have an array (or mutable array) called ballArray, that contains 20 ball objects. You could loop through them like this:
for (YourBallObject *ball in ballArray)
{
[ball setTexture:redTexture];
}
Or even better, you could do this:
[ballArray makeObjectsPerformSelector:#selector(setTexture:) withObject:redTexture];
Good luck in your endeavors.
Related
I do not understand what I do exactly when I add a CCSpriteFrameCache or CCSpriteBatchNode to my cocos2d application. Can somebody please explain the following points (it would be helpful if you could explain a few; please write the corresponding letter in front of your answer according to which question you are answering):
[all questions imply the achievement of best performance and lowest memory-use]
a) Is it crucial to create spritesheets for every single layer ? (For example: Menu - own spritesheet, GameLayer - own spritesheet...)
b) Can somebody explain why I have to add sprites to the batch node, and what a batch node generally is ?
b1)So, why can't I just do something like:
[[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:#"menusprites.plist"];
CCSpriteBatchNode *spriteSheet = [CCSpriteBatchNode batchNodeWithFile:#"menusprites.png"];
[self addChild:spriteSheet];
And then just add sprites to my layer by calling
CCSprite *mySprite = [CCSprite spriteWithSpriteFrameName:#""];
[self addChild:mySprite];
without adding them to the batch node ? Because from what I understand it works like this :
I add my spritesheet with all the sprites on it to the screen. My app then goes into the plist and looks for the coordinates of the sprite I want to display and then places it on the screen. So why should I call
[spriteSheet addChild:mySprite];
?
c) How do I then get rid of the spritesheet for memory purposes when I do not need it anymore ?
a) It is best to create as few spritesheets (CCSpriteBatchNodes) as is possible. Sprite batching reduces draw calls. Draw calls are expensive. Still, every batch node creates one draw call. So you want to use as few as possible because the ultimate goal is to keep draw calls as low as possible.
b) The CCSpriteBatchNode renders all of its children in one go, in one batched draw call. That's why you need to add sprites to the batch node so it can render them all together. Only sprites using the same texture as the batch node can be added to a batch node, because you can only batch draw from the same texture. Whenever the engine has to switch from one texture to another, it issues a new draw call.
b1) You can't do this because the batch node renders its children. If you add the sprites to any other node, each sprite draws itself, which means one additional draw call per sprite. And the sprite batch node has nothing to do.
c) The CCSpriteBatchNode is just a regular node. You can remove it from the scene like any other node. The texture and sprite frames are cached in the CCTextureCache and CCSpriteFrameCache singleton classes. If you want to remove the textures and sprite frames from memory, you have to do it through the cache classes.
a) no
b) batchNode increases your performance when you need to draw many sprites at the same time, in case of small number of sprites (10, 20. etc.) i dont think that you will notice any performance increasing. batchNode is much faster because opengl should draw only it to see all content. in other case opengl will draw all your objects separately. that is - if you will have 500, 600, 700 sprites, the draw() and visit() methods will be called for each one. if they all will be placed to the batchNode, it will be only one draw() call and one visit() call
c) you can purge cached data manually to force memory freeing by calling these methods:
[CCTextureCache purgeSharedTextureCache];
[CCSpriteFrameCache purgeSharedSpriteFrameCache];
[CCAnimationCache purgeSharedAnimationCache];
I add my sprite frames to CCSpriteFrameCache. Then I create a CCSpriteBatchNode with my desired image file.
This is what I don't quite understand:
When I make a CCSprite, if I want to take advantage of the CCSpriteBatchNode, I need to initialize the CCSprite with [CCSprite spriteWithBatchNode: rect:]? But if that's the case, I don't see how am I taking advantage of CCSpriteFrameCache to get the frames, since now I would be manually making the rect.
So I guess I use [CCSprite spriteWithSpriteFrameName:] and then I add this sprite to the batch node. But I am still unsure.
You should use:
CCSprite *sp = [CCSprite spriteWithSpriteFrameName:#"monster.png"];
The .plist that you specified in the SpriteFrameCache will take care of the frames for you.
Then you create the sprite and add to the batch.
If you create the batchnode with a file called "myArt.png", you CAN ONLY add a sprite to it that is contained inside "myArt.png".
Hope it helps!
According to what I've learned of cocos2d. SpriteFrameCache and SpriteBatchNode have the same result but are used differently and can notice a slight performance difference if your game is very big...
CCSpriteFrameCache loads your frames according to when they are called by their named according to the plist file it was given. The atlas associated with the plist has to be added to the project as well or else the frames will be called but nothing will be found to be drawn. The Plist is like the address of where the image is located inside the image atlas.
The good part of CCSpriteFrameCache is that the code is neater, and smaller than CCSpriteBatchNode method, at the cost that for every call of that frame, it goes to that specific atlas and draws it.
CCSpriteBatchNode, on the other hand, loads the atlas and loads it in one draw call. This is efficient because it reduces the amount of times the draw has to be done per need in the game. The only difficulty here is that you need to do math for the rectangles of each sprite in the atlas. This is because lets say your atlas is of 2 actions of a character, the atlas image file has a size of 1024x1024, and each sprite has a size of 128x128. so you would do the math to get each rectangle for the whole jump action for example.(This is where .plist come in handy to avoid doing such math)
The code gets complicated as you can see but it will only do one call, making it performance-wise your best call.
Another way to use CCSpriteBatchNode is to have different static sprites and you would just do one draw call for those multiple static images or sprites.
If you need example code just ask, I would be more than happy to provide it.
Update: Adding Link for SpriteBatchNode and an Example of my own.
SpriteBatchNode:
Example using SpriteBatchNode with Ray Wenderlich
I believe in this guy, and I have learned alot of Cocos2d from his tutorials. I would suggest you to read other of his tutorials.
In a nutshell, CCSpriteBatchNode is the exact same process we did below with the CCSpriteFrameCache the ONLY difference and its that you add the Sprite Child Node to the CCSpriteBatchNode and not the Layer, BUT you do Add the CCSpriteBatchNode to the Layer.
This is the hard concept that new comers to Cocos2d get entangled at.
SpriteFrameCache:
The SpriteFrameCache I couldn't find a good example so here is one simple one.
//By doing this your sprites are now in the cache ready to be used
//by their names declared in the .plist file.
-(void) loadingSprites:(NSString*) plistName {
[[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:plistName];
}
-(id)initGameLayer {
//CCSprite accepts CCSpriteFrame and your img is now ready to be displayed.
//However is still not drawn yet.
CCSprite * mySprite = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:framename];
//set a position if desired
//20 pixels to the right and 0 pixels to the top.
mySprite.position = CGPointMake(20,0);
//Now the Image has been drawn, making 1 draw call.
[self addChild:mySprite];
}
It is noteworthy to point out that CCSpriteBatchNode makes just 1 drawcall, HOWEVER all the sprites being added to the batchnode have to be part of the same SpriteAtlas.
And using SpriteFrameCache only its easier and simpler, but for every child added to the layer it means +1 draw call is being done.(This is the downside, performance)
So if you add 10 Sprites to the layer with SpriteFrameCache you will have 10 drawcalls.
However if you implement the SpriteBatchNode and add those 10 Sprites in the CCSpriteBatchNode instead and just add that CCSpriteBatchNode to the layer, you will have the same 10 sprites added but only ONE draw call will be done. Hence the Performance difference(for the best) will be significant in larger games.
Hope it helps, Cheers!
[[CCSpriteFrameCache sharedSpriteFrameCache]addSpriteFramesWithFile:];
That will add sprite frames through a .plist file. I would like to ask: if I tried, for whatever reason, to load the same .plist file again, is CCSpriteFrameCache smart enough as to ignore it? Or will it reload it all again, consuming more processing/memory?
It will re-use the already loaded texture, but reload the sprite frames in the plist. It will create new sprite frames and throw out the existing ones.
If you plan to modify a texture atlas and reload it, you'll have to make sure to remove the texture from the texture cache as well, so that it gets reloaded as well.
If you're just lazy and call this method over and over again, it shouldn't be an issue if you do this between scenes but I wouldn't do that during gameplay, since each sprite frame is deallocated and a new one allocated, and a lot of other code runs behind the scenes to load the sprite frames.
This is a multiple part question, mostly because my ignorance on the matter has multiple layers.
First, I put together a caching system for caching CGImageRef objects. I keep it at the CGImageRef level (rather than UIImage) as I am loading images in background threads. When an image is loaded I put it into a NSMutableDictionary. I had to do a bit of arm twisting to get CGImageRef's into the array:
//Bunch of stuff drawing into a context
CGImageRef imageRef = CGBitmapContextCreateImage(context);
CGContextRelease(context);
[(id)imageRef autorelease];
[self.cache setObject:(id)imageRef forKey:#"SomeKey"];
So, as you can see, I'm trying to treat the Image Ref as an NSObject, setting it to autorelease then placing it in the dictionary. My expectation is this will allow the image to be cleaned up after being removed from the dictionary. Now, I am beginning to have my doubts.
My application clears the cache array when the user "restarts" to play with different images. Running the application in Instruments shows that the memory is not dropping back to the "start" level on restart, but instead remains steady. My gut tells me that when the array has all objects removed the CGImageRef is not being cleared.
However, I'm unable to confirm this as I don't quite know how to track down the actual source of the memory in instruments. It's just a list of (Malloc 16 Bytes, Malloc 32 Bytes, etc), drilling into them just show a list of dyld callers. Not sure how to properly read it.
So, first question, is my way of caching CGImageRef objects completely flawed? And is there a better way to confirm such things in instruments?
First of all, caching CGImages is OK and I don't see any problems with the code you posted.
Am I correctly assuming you use an NSMutableDictionary as the cache? If so, you can clear it by sending it -removeAllObjects, which should release all the keys and values. If you just set different images for the same keys, memory usage may remain roughly the same because you replace previous images with new ones. If the images have the same size, memory usage should be constant except brief spikes when you create a new batch of images.
As for Instruments, I've seen it both report false positives and miss real leaks. Try running it several times, making pauses, if possible, for the Leaks instrument to "catch up". This sounds crazy, but I think it may make it a bit more reliable.
If all else fails, you can log the contents of the cache before and after loading a set of images to make sure the cache itself works as expected.
Why not just cache UIImage objects; you can make them fine on a background thread?
It's UIImageView objects that you have to be more careful with and even they are OK for most operations in the background.
In my application I needed something like a particle system so I did the following:
While the application initializes I load a UIImage
laserImage = [UIImage imageNamed:#"laser.png"];
UIImage *laserImage is declared in the Interface of my Controller. Now every time I need a new particle this code makes one:
// add new Laserimage
UIImageView *newLaser = [[UIImageView alloc] initWithImage:laserImage];
[newLaser setTag:[model.lasers count]-9];
[newLaser setBounds:CGRectMake(0, 0, 17, 1)];
[newLaser setOpaque:YES];
[self.view addSubview:newLaser];
[newLaser release];
Please notice that the images are only 17px * 1px small and model.lasers is a internal array to do all the calculating seperated from graphical output. So in my main drawing loop I set all the UIImageView's positions to the calculated positions in my model.lasers array:
for (int i = 0; i < [model.lasers count]; i++) {
[[self.view viewWithTag:i+10] setCenter:[[model.lasers objectAtIndex:i] pos]];
}
I incremented the tags by 10 because the default is 0 and I don't want to move all the views with the default tag.
So the animation looks fine with about 10 - 20 images but really gets slow when working with about 60 images. So my question is: Is there any way to optimize this without starting over in OpenGl ES?
As jeff7 and FenderMostro said, you're using the high-level API (UIKit), and you'd have better performance using the lower APIs, either CoreAnimation or OpenGL. (cocos2d is built on top of OpenGL)
Your best option would be to use CALayers instead of UIImageViews, get a CGImageRef from your UIImage and set it as the contents for these layers.
Also, you might want to keep a pool of CALayers and reuse them by hiding/showing as necessary. 60 CALayers of 17*1 pixels is not much, I've been doing it with hundreds of them without needing extra optimization.
This way, the images will already be decompressed and available in video memory. When using UIKit, everything goes through the CPU, not to mention the creation of UIViews which are pretty heavy objects.
Seems like you're trying to code a game by using the UIKit API, which is not really very suitable for this kind of purpose. You are expending the device's resources whenever you allocate a UIView, which incurs slowdowns because object creation is costly. You might be able to obtain the performance you want by dropping to CoreAnimation though, which is really good at drawing hundreds of images in a limited time frame, although it would still be much better if you used OpenGL or an engine like Cocos2d.
The UIImageView is made to display single OR multiple images. So, instead of creating every time a UIImageView, you should consider creating a new image and add it to the UIImageView instead.
See here.
I'd recommend starting over using OpenGL ES, there is an excellent framework called cocos2d for iPhone that can make this type of programming very easy and fast. From a quick look at your code, you're lasers can be remodeled as CCSprite which is an easy way to move images around a scene among many other things.