I can get a sublayer at any point through hittest at that point by coding. But i want to get all the layers at that point.
The problem is:
By hit test i can only get the top most layer at hat point, it does not give any information about the layers beneath that top layer at that point.
Is there any way to find all the layers at a specific point in ios?
Hit testing is used for the purpose of delivering events. Since events are always delivered to the topmost view it makes no sense for hit test to continue testing the ones below.
However if you do want to find all the layers at a point you can write your own version of hitTest as a category on CALayer that does this :
-(NSMutableSet*)allLayersAtPoint:(CGPoint)aPoint
{
NSMutableSet *layers = [[NSMutableSet alloc] init];
if(! CGRectContainsPoint([self frame],aPoint))
{
return layers;
}
[layers addObject:self]
for(CALayer *layer in self.sublayers) {
CGPoint converted = [self convertPoint:aPoint toLayer:layer]
[layers unionSet:[layer allLayersAtPoint:converted]]
}
return layers
}
Beware of layers with sublayer transforms though. Not sure how to handle that. Usually this is done on UIViews , I'm not sure why you're doing this on layers. Just implement this as a category on UIView instead of CALayer if you want this to work on UIViews.
i am copying a sprite from one NSMutableArray to another, but when i delete the CCSprite from the first NSMutableArray then it is also deleted in the second Array.
How can i prevent this?
In the init method the Arrays are initialized like below.
spriteTempArray = [[NSMutableArray alloc] init] ;
myPowerUpArray = [[NSMutableArray alloc] init] ;
This is the first method were the sprite is placed on screen and animated somewhere on the screen
CCSprite *powerUpSprite = [[CCSprite alloc] initWithFile:SpriteFileName ] ;
powerUpSprite.position = ccp(xcenter,ycenter);
powerUpSprite.scale = 0;
[self addChild:powerUpSprite z:20 tag:puTag];
[spriteTempArray addObject:powerUpSprite];
id zoomIn = [CCScaleTo actionWithDuration:0.2 scale:1] ;
id moveTo = [CCMoveTo actionWithDuration:0.2 position:ccp(winSize.width/2,120)];
[powerUpSprite runAction:zoomIn];
[powerUpSprite runAction:moveTo];
Then when de Sprite is touched, it's moved to the corner of the screen and also stored in another NSMutableArray (myPowerUpArray). But the delete action erases the sprite in both arrays.
CCSprite *powerUpSprite = [spriteTempArray objectAtIndex:0];
id zoomOut = [CCScaleTo actionWithDuration:0.2 scale:0.35] ;
id moveTo = [CCMoveTo actionWithDuration:0.2 position:ccp(winSize.width - 24,winSize.height -31 * myPowerUps -80)];
[powerUpSprite runAction:zoomOut];
[powerUpSprite runAction:moveTo]
[myPowerUpArray addObject:powerUpSprite];
[self deleteSpriteTempArray];
Below the sprite delete method.
-(void)deleteSpriteTempArray{
NSMutableArray *filesToRemove = [[NSMutableArray alloc] init];
for ( id obj in spriteTempArray) {
[filesToRemove addObject:obj];
[self removeChild:obj cleanup:YES];
}
[spriteTempArray removeObjectsInArray:filesToRemove];
}
I haven't observed the Array, other than de sprite is disappearing from the screen.
Your delete array method is odd. To begin why are you calling:
[self removeChild:obj cleanup:YES];
If your intent is not to remove the sprite from the scene? You are telling your code here to remove the sprite from the scene and to clean it up, but based on your post I don't get the impression you actually want that to happen at that moment. If it is your intent that it gets removed after it moves to the corner of your screen, then you should do the move and scale in a CCSpawn (let's simply call it moveScale) and in a CCSequence you should be adding that moveScale action followed by a block action that calls a method on the sprite to remove itself from its parent with cleanup:
CGSize winSize = [[CCDirector sharedDirector] winSize];
CCSprite* powerUp = ...;
float duration = 0.2f;
float desiredScale = 1.0f;
CGPoint desiredPosition = ccp(winSize.width / 2,120);
id zoomIn = [CCScaleTo actionWithDuration:duration scale:desiredScale] ;
id moveTo = [CCMoveTo actionWithDuration:duration position:desiredPosition];
id remove = [CCCallBlock actionWithBlock:^
{
[powerUp removeFromParentAndCleanup:YES];
}];
id moveScale = [CCSpawn actions:zoomIn, moveTo, nil];
id moveScaleRemove = [CCSequence actions:moveScale, remove, nil];
[powerUp runAction:moveScaleRemove];
Secondly why do you have a "files to remove" array in your deletion method that is built with objects to remove? That "files to remove" array is adding all objects from the sprite temp array to it, so it seems a little pointless. Just remove all objects from your sprite temp array. No need to build another temp list with everything to remove, just to essentially be saying remove everything from my original temp array list. That would only be useful if you were removing only SOME of the objects in the temp array. Since you are removing all, the "files to remove" array doesn't serve a purpose. Or at least that is what your code is showing that you are doing. Whether you intend on that behavior is another question.
Also if you have two arrays and an object in both, removing it from one array wouldn't have an impact on the second.
Another issue with your code is in the first code block. If you intend on running a move and a scale action together, they need to be executed via a CCSpawn. Instead you are telling it to scale, then immediately telling it to move which therefore it will be moving but not scaling as it moves. In other words both are not occurring at the same time. On the other hand if you intend for them to be done in sequence, you should be executing them via a CCSequence.
Another thing I noticed about your code is why are you deleting the entire sprite temp array when only one sprite was touched? Did you intend to only remove that one sprite that was touched? Why not just remove that one object, unless I am missing something?
[spriteTempArray removeObject:thisPowerUpIJustTouched];
A final issue I see is with your naming. Your code example indicates that the order of each example code block is the order in which events are occurring right? So why is the initial array you add powerups to called spriteTempArray while the array you add objects to after it is touched is called powerUpArray? Based on the code you've provided and the order they are shown, there is no point to the spriteTempArray. And even if there was, the naming looks backwards. Unless of course I'm missing something. It looks like your intention is to have a power ups array and when a power up is touched it is added to another array to mark that it is to be deleted. But as I've shown earlier in this post, it doesn't look like that second array has a true purpose since your desired behavior is A) not being done and B) would be done using the CCSpawn/CCSequence combo I showed earlier.
Hope this helped. I just woke up so hopefully I addressed your issue properly.
I'm plotting ~768 points for a graph using CGContextStrokePath. The problem is that every second I get a new data point, and thus redraw the graph. This is currently taking 50% CPU in what's already a busy App.
Graph drawing is done in drawRect in a UIView. The graph is time based, so new data points always arrive on the right hand side.
I'm thinking a few alternative approaches:
Draw with GLKit (at cost of not supporting older devices) and seems like a lot of work.
Do some kind of screen grab (renderInContext?), shift left by 1 px, blit, and only draw a line for the last two data points.
Have a very wide CALayer and pan along it?
Smooth the data set, but this feels like cheating :)
It's also possible I'm missing something obvious here that I'm seeing such poor performance?
CGContextBeginPath(context);
CGContextSetLineWidth(context, 2.0);
UIColor *color = [UIColor whiteColor];
CGContextSetStrokeColorWithColor(context, [color CGColor]);
…
CGContextAddLines(context, points, index);
CGContextMoveToPoint(context, startPoint.x, startPoint.y);
CGContextClosePath(context);
CGContextStrokePath(context);
Let's implement a graphing view that uses a bunch of tall, skinny layers to reduce the amount of redrawing needed. We'll slide the layers to the left as we add samples, so at any time, we probably have one layer hanging off the left edge of the view and one hanging off the right edge of the view:
You can find a complete working example of the code below on my github account.
Constants
Let's make each layer 32 points wide:
#define kLayerWidth 32
And let's say we're going to space the samples along the X axis at one sample per point:
#define kPointsPerSample 1
So we can deduce the number of samples per layer. Let's call one layer's worth of samples a tile:
#define kSamplesPerTile (kLayerWidth / kPointsPerSample)
When we're drawing a layer, we can't just draw the samples strictly inside the layer. We have to draw a sample or two past each edge, because the lines to those samples cross the edge of the layer. We'll call these the padding samples:
#define kPaddingSamples 2
The maximum dimension of an iPhone screen is 320 points, so we can compute the maximum number of samples we need to retain:
#define kMaxVisibleSamples ((320 / kPointsPerSample) + 2 * kPaddingSamples)
(You should change the 320 if you want to run on an iPad.)
We'll need to be able to compute which tile contains a given sample. And as you'll see, we'll want to do this even if the sample number is negative, because it will make later computations easier:
static inline NSInteger tileForSampleIndex(NSInteger sampleIndex) {
// I need this to round toward -∞ even if sampleIndex is negative.
return (NSInteger)floorf((float)sampleIndex / kSamplesPerTile);
}
Instance Variables
Now, to implement GraphView, we'll need some instance variables. We'll need to store the layers that we're using to draw the graph. And we want to be able to look up each layer according to which tile it's graphing:
#implementation GraphView {
// Each key in _tileLayers is an NSNumber whose value is a tile number.
// The corresponding value is the CALayer that displays the tile's samples.
// There will be tiles that don't have a corresponding layer.
NSMutableDictionary *_tileLayers;
In a real project, you'd want to store the samples in a model object and give the view a reference to the model. But for this example, we'll just store the samples in the view:
// Samples are stored in _samples as instances of NSNumber.
NSMutableArray *_samples;
Since we don't want to store an arbitrarily large number of samples, we'll discard old samples when _samples gets big. But it will simplify the implementation if we can mostly pretend that we never discard samples. To do that, we keep track of the total number of samples ever received.
// I discard old samples from _samples when I have more than
// kMaxTiles' worth of samples. This is the total number of samples
// ever collected, including discarded samples.
NSInteger _totalSampleCount;
We should avoid blocking the main thread, so we'll do our drawing on a separate GCD queue. We need to keep track of which tiles need to be drawn on that queue. To avoid drawing a pending tile more than once, we use a set (which eliminates duplicates) instead of an array:
// Each member of _tilesToRedraw is an NSNumber whose value
// is a tile number to be redrawn.
NSMutableSet *_tilesToRedraw;
And here's the GCD queue on which we'll do the drawing.
// Methods prefixed with rq_ run on redrawQueue.
// All other methods run on the main queue.
dispatch_queue_t _redrawQueue;
}
Initialization / Destruction
To make this view work whether you create it in code or in a nib, we need two initialization methods:
- (id)initWithFrame:(CGRect)frame {
if ((self = [super initWithFrame:frame])) {
[self commonInit];
}
return self;
}
- (void)awakeFromNib {
[self commonInit];
}
Both methods call commonInit to do the real initialization:
- (void)commonInit {
_tileLayers = [[NSMutableDictionary alloc] init];
_samples = [[NSMutableArray alloc] init];
_tilesToRedraw = [[NSMutableSet alloc] init];
_redrawQueue = dispatch_queue_create("MyView tile redraw", 0);
}
ARC won't clean up the GCD queue for us:
- (void)dealloc {
if (_redrawQueue != NULL) {
dispatch_release(_redrawQueue);
}
}
Adding a sample
To add a new sample, we pick a random number and append it to _samples. We also increment _totalSampleCount. We discard the oldest samples if _samples has gotten big.
- (void)addRandomSample {
[_samples addObject:[NSNumber numberWithFloat:120.f * ((double)arc4random() / UINT32_MAX)]];
++_totalSampleCount;
[self discardSamplesIfNeeded];
Then, we check if we've started a new tile. If so, we find the layer that was drawing the oldest tile, and reuse it to draw the newly-created tile.
if (_totalSampleCount % kSamplesPerTile == 1) {
[self reuseOldestTileLayerForNewestTile];
}
Now we recompute the layout of all the layers, which will to the left a bit so the new sample will be visible in the graph.
[self layoutTileLayers];
Finally, we add tiles to the redraw queue.
[self queueTilesForRedrawIfAffectedByLastSample];
}
We don't want to discard samples one at a time. That would be inefficient. Instead, we let the garbage build up for a while, then throw it away all at once:
- (void)discardSamplesIfNeeded {
if (_samples.count >= 2 * kMaxVisibleSamples) {
[_samples removeObjectsInRange:NSMakeRange(0, _samples.count - kMaxVisibleSamples)];
}
}
To reuse a layer for the new tile, we need to find the layer of the oldest tile:
- (void)reuseOldestTileLayerForNewestTile {
// The oldest tile's layer should no longer be visible, so I can reuse it as the new tile's layer.
NSInteger newestTile = tileForSampleIndex(_totalSampleCount - 1);
NSInteger reusableTile = newestTile - _tileLayers.count;
NSNumber *reusableTileObject = [NSNumber numberWithInteger:reusableTile];
CALayer *layer = [_tileLayers objectForKey:reusableTileObject];
Now we can remove it from the _tileLayers dictionary under the old key and store it under the new key:
[_tileLayers removeObjectForKey:reusableTileObject];
[_tileLayers setObject:layer forKey:[NSNumber numberWithInteger:newestTile]];
By default, when we move the reused layer to its new position, Core Animation will animate it sliding over. We don't want that, because it will be a big empty orange rectangle sliding across our graph. We want to move it instantly:
// The reused layer needs to move instantly to its new position,
// lest it be seen animating on top of the other layers.
[CATransaction begin]; {
[CATransaction setDisableActions:YES];
layer.frame = [self frameForTile:newestTile];
} [CATransaction commit];
}
When we add a sample, we'll always want to redraw the tile containing the sample. We also need to redraw the prior tile, if the new sample is within the padding range of the prior tile.
- (void)queueTilesForRedrawIfAffectedByLastSample {
[self queueTileForRedraw:tileForSampleIndex(_totalSampleCount - 1)];
// This redraws the second-newest tile if the new sample is in its padding range.
[self queueTileForRedraw:tileForSampleIndex(_totalSampleCount - 1 - kPaddingSamples)];
}
Queuing a tile for redraw is just a matter of adding it to the redraw set and dispatching a block to redraw it on _redrawQueue.
- (void)queueTileForRedraw:(NSInteger)tile {
[_tilesToRedraw addObject:[NSNumber numberWithInteger:tile]];
dispatch_async(_redrawQueue, ^{
[self rq_redrawOneTile];
});
}
Layout
The system will send layoutSubviews to the GraphView when it first appears, and any time its size changes (such as if a device rotation resizes it). And we only get the layoutSubviews message when we're really about to appear on the screen, with our final bounds set. So layoutSubviews is a good place to set up the tile layers.
First, we need to create or remove layers as necessary so we have the right layers for our size. Then we need to lay out the layers by setting their frames appropriately. Finally, for each layer, we need to queue its tile for redraw.
- (void)layoutSubviews {
[self adjustTileDictionary];
[CATransaction begin]; {
// layoutSubviews only gets called on a resize, when I will be
// shuffling layers all over the place. I don't want to animate
// the layers to their new positions.
[CATransaction setDisableActions:YES];
[self layoutTileLayers];
} [CATransaction commit];
for (NSNumber *key in _tileLayers) {
[self queueTileForRedraw:key.integerValue];
}
}
Adjusting the tile dictionary means setting up a layer for each visible tile and removing layers for non-visible tiles. We'll just reset the dictionary from scratch each time, but we'll try to reuse the layer's we've already created. The tiles that need layers are the newest tile, and preceding tiles so we have enough layers to cover the view.
- (void)adjustTileDictionary {
NSInteger newestTile = tileForSampleIndex(_totalSampleCount - 1);
// Add 1 to account for layers hanging off the left and right edges.
NSInteger tileLayersNeeded = 1 + ceilf(self.bounds.size.width / kLayerWidth);
NSInteger oldestTile = newestTile - tileLayersNeeded + 1;
NSMutableArray *spareLayers = [[_tileLayers allValues] mutableCopy];
[_tileLayers removeAllObjects];
for (NSInteger tile = oldestTile; tile <= newestTile; ++tile) {
CALayer *layer = [spareLayers lastObject];
if (layer) {
[spareLayers removeLastObject];
} else {
layer = [self newTileLayer];
}
[_tileLayers setObject:layer forKey:[NSNumber numberWithInteger:tile]];
}
for (CALayer *layer in spareLayers) {
[layer removeFromSuperlayer];
}
}
The first time through, and any time the view gets sufficiently wider, we need to create new layers. While we're creating the view, we'll tell it to avoid animating its contents or position. Otherwise it will animate them by default.
- (CALayer *)newTileLayer {
CALayer *layer = [CALayer layer];
layer.backgroundColor = [UIColor greenColor].CGColor;
layer.actions = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNull null], #"contents",
[NSNull null], #"position",
nil];
[self.layer addSublayer:layer];
return layer;
}
Actually laying out the tile layers is just a matter of setting each layer's frame:
- (void)layoutTileLayers {
[_tileLayers enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
CALayer *layer = obj;
layer.frame = [self frameForTile:[key integerValue]];
}];
}
Of course the trick is computing the frame for each layer. And the y, width, and height parts are easy enough:
- (CGRect)frameForTile:(NSInteger)tile {
CGRect myBounds = self.bounds;
CGFloat x = [self xForTile:tile myBounds:myBounds];
return CGRectMake(x, myBounds.origin.y, kLayerWidth, myBounds.size.height);
}
To compute the x coordinate of the tile's frame, we compute the x coordinate of the first sample in the tile:
- (CGFloat)xForTile:(NSInteger)tile myBounds:(CGRect)myBounds {
return [self xForSampleAtIndex:tile * kSamplesPerTile myBounds:myBounds];
}
Computing the x coordinate for a sample requires a little thought. We want the newest sample to be at the right edge of the view, and the second-newest to be kPointsPerSample points to the left of that, and so on:
- (CGFloat)xForSampleAtIndex:(NSInteger)index myBounds:(CGRect)myBounds {
return myBounds.origin.x + myBounds.size.width - kPointsPerSample * (_totalSampleCount - index);
}
Redrawing
Now we can talk about how to actually draw tiles. We're going to do the drawing on a separate GCD queue. We can't safely access most Cocoa Touch objects from two threads simultaneously, so we need to be careful here. We'll use a prefix of rq_ on all the methods that run on _redrawQueue to remind ourselves that we're not on the main thread.
To redraw one tile, we need to get the tile number, the graphical bounds of the tile, and the points to draw. All of those things come from data structures that we might be modifying on the main thread, so we need to access them only on the main thread. So we dispatch back to the main queue:
- (void)rq_redrawOneTile {
__block NSInteger tile;
__block CGRect bounds;
CGPoint pointStorage[kSamplesPerTile + kPaddingSamples * 2];
CGPoint *points = pointStorage; // A block cannot reference a local variable of array type, so I need a pointer.
__block NSUInteger pointCount;
dispatch_sync(dispatch_get_main_queue(), ^{
tile = [self dequeueTileToRedrawReturningBounds:&bounds points:points pointCount:&pointCount];
});
It so happens that we might not have any tiles to redraw. If you look back at queueTilesForRedrawIfAffectedByLastSample, you'll see that it usually tries to queue the same tile twice. Since _tilesToRedraw is a set (not an array), the duplicate was discarded, but rq_redrawOneTile was dispatched twice anyway. So we need to check that we actually have a tile to redraw:
if (tile == NSNotFound)
return;
Now we need to actually draw the tile's samples:
UIImage *image = [self rq_imageWithBounds:bounds points:points pointCount:pointCount];
Finally we need to update the tile's layer to show the new image. We can only touch a layer on the main thread:
dispatch_async(dispatch_get_main_queue(), ^{
[self setImage:image forTile:tile];
});
}
Here's how we actually draw the image for the layer. I will assume you know enough Core Graphics to follow this:
- (UIImage *)rq_imageWithBounds:(CGRect)bounds points:(CGPoint *)points pointCount:(NSUInteger)pointCount {
UIGraphicsBeginImageContextWithOptions(bounds.size, YES, 0); {
CGContextRef gc = UIGraphicsGetCurrentContext();
CGContextTranslateCTM(gc, -bounds.origin.x, -bounds.origin.y);
[[UIColor orangeColor] setFill];
CGContextFillRect(gc, bounds);
[[UIColor whiteColor] setStroke];
CGContextSetLineWidth(gc, 1.0);
CGContextSetLineJoin(gc, kCGLineCapRound);
CGContextBeginPath(gc);
CGContextAddLines(gc, points, pointCount);
CGContextStrokePath(gc);
}
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
But we still have to get the tile, the graphics bounds, and the points to draw. We dispatched back to the main thread to do it:
// I return NSNotFound if I couldn't dequeue a tile.
// The `pointsOut` array must have room for at least
// kSamplesPerTile + 2*kPaddingSamples elements.
- (NSInteger)dequeueTileToRedrawReturningBounds:(CGRect *)boundsOut points:(CGPoint *)pointsOut pointCount:(NSUInteger *)pointCountOut {
NSInteger tile = [self dequeueTileToRedraw];
if (tile == NSNotFound)
return NSNotFound;
The graphics bounds are just the bounds of the tile, just like we computed earlier to set the frame of the layer:
*boundsOut = [self frameForTile:tile];
I need to start graphing from the padding samples before the first sample of the tile. But, prior to having enough samples to fill the view, my tile number may actually be negative! So I need to be sure not to try to access a sample at a negative index:
NSInteger sampleIndex = MAX(0, tile * kSamplesPerTile - kPaddingSamples);
We also need to make sure we don't try to run past the end of the samples when we compute the sample at which we stop graphing:
NSInteger endSampleIndex = MIN(_totalSampleCount, tile * kSamplesPerTile + kSamplesPerTile + kPaddingSamples);
And when I actually access the sample values, I need to account for the samples I've discarded:
NSInteger discardedSampleCount = _totalSampleCount - _samples.count;
Now we can compute the actual points to graph:
CGFloat x = [self xForSampleAtIndex:sampleIndex myBounds:self.bounds];
NSUInteger count = 0;
for ( ; sampleIndex < endSampleIndex; ++sampleIndex, ++count, x += kPointsPerSample) {
pointsOut[count] = CGPointMake(x, [[_samples objectAtIndex:sampleIndex - discardedSampleCount] floatValue]);
}
And I can return the number of points and the tile:
*pointCountOut = count;
return tile;
}
Here's how we actually pull a tile off the redraw queue. Remember that the queue might be empty:
- (NSInteger)dequeueTileToRedraw {
NSNumber *number = [_tilesToRedraw anyObject];
if (number) {
[_tilesToRedraw removeObject:number];
return number.integerValue;
} else {
return NSNotFound;
}
}
And finally, here's how we actually set the tile layer's contents to the new image. Remember that we dispatched back to the main queue to do this:
- (void)setImage:(UIImage *)image forTile:(NSInteger)tile {
CALayer *layer = [_tileLayers objectForKey:[NSNumber numberWithInteger:tile]];
if (layer) {
layer.contents = (__bridge id)image.CGImage;
}
}
Making it sexier
If you do all of that, it will work fine. But you can actually make it slightly nicer-looking by animating the repositioning of the layers when a new sample comes in. This is very easy. We just modify newTileLayer so that it adds an animation for the position property:
- (CALayer *)newTileLayer {
CALayer *layer = [CALayer layer];
layer.backgroundColor = [UIColor greenColor].CGColor;
layer.actions = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNull null], #"contents",
[self newTileLayerPositionAnimation], #"position",
nil];
[self.layer addSublayer:layer];
return layer;
}
and we create the animation like this:
- (CAAnimation *)newTileLayerPositionAnimation {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position"];
animation.duration = 0.1;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
return animation;
}
You will want to set the duration to match the speed at which new samples arrive.
You don't have to rasterize whole path every time you draw it - you can cache it as raster bitmap. BTW, your idea with "scrolling" is standard solution for such task...
Create a bitmap context the same height as your view but twice the width. Start drawing your points into the context, then in drawRect create a CGImageRef. The idea is to as you initially fill the screen your image will start at the beginning. The image you will draw will have the proper width and height, but the bytesPerRow will be 2x (more on that). You continue to draw new points as they come until you get to the last point - now x is exhausted.
Continue writing points in your context, but now, when you create the image, offset the initial pointer by one pixel. Continue doing this until you have done 2x lines - you are now at the very very end of your context.
At that one time, you will need to move the "right" side of the image to the left, and reset your offset count. That is, you will need to memcpy(starOfBitMap, startOfBitMap+bytesPerRow/2, sizeOfBitMap - bytesPerRow/2). In essence, you are left shifting one visible frame.
Now as you add new lines, its at the end of the first frame, and you start offseting by one pixel as you draw.
I am looking to do something similar to the API in UIImageView where you supply say 5 images and through a certain duration the UIImageView will switch through those images. I am looking to do this in a CCSprite but the only ways I hear of doing this is customizing frames of the Node or something like that.
Does someone know an easy way to achieve something like this where I provide say 5 images and I want it to cycle though them in 4 seconds?
Thanks!
//initialize anitmation
CCAnimation *anime= [[CCAnimation alloc] initWithName:#"anime" delay:4.0];
for(int i = 1; i <= 5; i++){
[anime addFrameWithFilename:[NSString stringWithFormat:#"frame%d.png", i]];
}
id animeAction = [CCRepeatForever actionWithAction:[CCAnimate actionWithAnimation:anime]];
[self runAction:animeAction];
It takes 5 images and animate them in 4.0 periods.
----- Edit -----
Here is how can you do it with new Cocos2d Api 1.0.1 :
//initialize anitmation
CCAnimation *anime= [CCAnimation animation];
anime.delay = 4.0;
for(int i = 1; i <= 5; i++){
[anime addFrameWithFilename:[NSString stringWithFormat:#"frame%d.png", i]];
}
id animeAction = [CCRepeatForever actionWithAction:[CCAnimate actionWithAnimation:anime]];
[self runAction:animeAction];
Create 5 Sprites with the 5 different images. Place them all at the same position. Set 4 of the sprites to be invisible (sprite.visible = NO).
When you want to change the sprites you only need to set another sprite to visible and the currently visible one to invisible. You can also apply actions like CCFadeTo to fade out one sprite while fading in another.
If you use fading, make sure you still set the visible property for performance reasons. Sprites with opacity of 0 are still rendered, while sprites that are not visible aren't.
Have a look at the CCAnimation, CCAnimate and CCSpriteFrame classes.
The CCSpriteFrame represents a piece of a larger texture known as a sprite sheet. You can put your five images into one sheet.
CCAnimation allows you to create an animation out of a sequence of such frames and to set the speed at which the animation runs.
And CCAnimate allows you to run that animation as an action:
[node runAction:[CCAnimate actionWithAnimation:animationInstance restoreOriginal:NO]];
.
Links
CCSpriteFrame class reference
CCAnimation class reference
CCAnimate class reference
Zwoptex for creating sprite sheets - There is a free flash version around too
I have a collection of six seat objects (UIViews with the alpha property set to 0) on my screen and I have player objects basically placed on top of them. The seats may or may not have a player on top of it. What I have right now is I've programmed the player's touchesMoved event so that when I drag a player on top of a seat object the seat's alpha property will go from 0 to 0.6. And then while still dragging the player, if I drag him off the seat the alpha property will go back to 0.
Instead, is there a built in UIView animation that could instead cause the alpha property to kind of fluctuate back and forth between .6 and .2? Kind of a throbbing effect? Would this require core animation or something more advanced?
I'm using the following in the Player's touchesMoved method to drag a player and to detect if it's above a seat:
UITouch *aTouch = [touches anyObject];
self.center = [aTouch locationInView:[self.superview]];
Seat *seat = [controller seatAtPoint:[aTouch locationInView:self.superview]];
if (seat) {
self.hoverSeat = seat;
seat.alpha = .6;
} else {
self.hoverSeat.alpha = 0;
}
The seatAtPoint method in my controller is as follows:
- (Seat *) seatAtPoint:(CGPoint)point {
NSMutableArray seats = [NSMutableArray arrayWithCapacity:6];
for (int i = 1; i <= 6; i++) {
Seat *aSeat = (Seat*)[self.view viewWithTag:i];
[seats addObject:aSeat];
}
for (Seat *seat in seats) {
if (CGRectContainsPoint([seat frame], point)) {
return seat;
}
}
return nil;
}
I use a hoverSeat ivar to hold the seat above which the player is hovering. And then if the seat returned is nil then it sets that seat's alpha to 0.
A bug I'm seeing with this code is if I move the player around the screen a little too quickly sometimes the alpha property won't go back to 0. Can anyone think of a more effective way to ensure that it goes back to 0?
Thank you for any suggestions.
I would look into using CoreAnimation; it's not that hard to use. Here's what it might look like for a fade:
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:.5];
seat.alpha = 1;
[UIView commitAnimations];
You can look into the callbacks that methods like these use to create some kind of pulsing animation ( Trigerring other animation after first ending Animation (Objetive-C) ) when combined with a condition you can probably set it up to loop forever. Also of note is that UIView Animations disable user input when they're running ( Infinitely looping animation ).
As for the bug you mentioned, if it's still happening, you could set up a timer that when fired, iterates through all the seats and checks for ones that aren't being displayed correctly. You might want to set this interval to be pretty infrequent so that it doesn't interrupt or impact the performance of your other animations.