I'm working on a game right now, in which every second, I want to create x number of new UIImage objects that begin at the top of the screen. After they have been instantiated, I want them automatically to fall down until they reach the bottom of the screen, at which point I no longer have any use for them.
Its almost like raindrops - X number of them are created every second, and they each fall down at different speeds.
I'm getting really confused as to how I would even just design my program to do this.
Any help would be greatly appreciated. Thanks.
Are they all copies of the same image, or at least copies of one among a limited set of images?
For such a game-like, graphics intensive app I would seriously consider using OpenGL ES, although the learning curve is steep for people familiar with UIKit only. Fortunately, there are third party, open-source libraries such as Cocos2d that make efficient 2d graphics almost as easy to code as UIKit.
Regarding your question in particular, I haven't watched the video mentioned by #ctrahey, but I can think of these patterns:
Have a (finite) 'pool' of reusable objects, which size is equal to the maximum amount of instances that might appear on screen at any given time. You definitely want to set this limit, since graphics performance is not infinite. Each time an object falls off-screen, 'reset' its state and reuse it (from the top, again). UITableView does something like this with table cells.
Create the instances on demand, and destroy them (release->dealloc) once they go off-screen.
You have to balance the runtime cost of creating/destroying instances vs. the cost/inconvenience of resetting objects.
Hope it helps
Step 1: Run a piece of code every second
timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(dispatchSomeRaindrops) userInfo:nil repeats:YES];
Step 2: Create some particles, send them down the screen, and clean them up when they reach the bottom.
- (void)dispatchSomeRaindrops
{
for (int i = 0; i < 5; i++) {
UIImageView *view = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"safari.png"]];
CGFloat halfHeight = view.frame.size.height / 2;
CGFloat x = arc4random() % (int)self.view.frame.size.width;
view.center = CGPointMake(x, -halfHeight);
[self.view addSubview:view];
NSTimeInterval duration = 10 + arc4random() % 10;
[UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
float endY = self.view.frame.size.height + halfHeight;
view.center = CGPointMake(x, endY);
} completion:^(BOOL finished) {
[view removeFromSuperview];
}];
}
}
Check out the Core Animation video from WWDC 2011, and near the end there is a bit on "Replicators", and it sounds like exactly what you are after.
FYI: found a terrific resource to handle this exact scenario. It does require you to use cocos2d, but it explains it in a very clear and understandable manner.
http://www.raywenderlich.com/352/how-to-make-a-simple-iphone-game-with-cocos2d-tutorial
Related
I have an image that I am animating in order to make it look as if it is "breathing".
Currently I have the image moving in a decent manner with the following code below: (I am animating a UIView that contains a few UIImageView's, which all move as one)
- (IBAction)animateButton:(id)sender {
[UIView animateWithDuration:0.64
delay:0
options:UIViewAnimationOptionAutoreverse | UIViewAnimationOptionRepeat
animations:^{
_testView.transform = CGAffineTransformMakeScale(1.08f, 1.02f);
} completion:nil];
}
HOWEVER, I can not seem to figure out how to animate stretching the image in the x at a different rate as the y. The point of this is to appear as if the image is actually alive without appearing to cycle through a clear repetitive motion.
I tried by attempting to anchor the center of the UIView to a specific location, then add some number to the width, through an animation of lets say 1.0 seconds.
I then tried to simultaneously call another animation that does the same animation only to the height, with a different amount added, for about 1.3 seconds. I could not get these two to perform at the same time though, as one would take precedence over the other.
If someone could lead me in the right direction as to animating a repetitive stretch of the width and height at different rates I would be most appreciative. Thanks!
Consider that two changes overlapping in time look like this:
|---- change x ---|
|---- change y ----|
If the two intervals are arbitrary and overlapping, the can be represented by three animations: one changing one dimension individually, one changing both dimensions together, and another changing one dimension individually.
You can see that there's numerous ways to specify this, but lets try a straight-forward one. To avoid the arithmetic of compounding scales, lets specify a dimension, a pixel change and a duration. For example...
#[ #{#"dimension":#"width", #"delta":#10, #"duration":0.2},
#{#"dimension":#"both", #"delta":#40, #"duration":0.8},
#{#"dimension":#"width", #"delta":#10, #"duration":0.2} ]
... means a longer change in width straddling a shorter change in height. You can see how this can be a pretty complete language to get done what you want.
We need an animation method that will perform the changes serially. A simple way to do this is to treat the array of actions as a to-do list. The recursive algorithm says: to do a list of things, do the first one, then do the rest....
- (void)animateView:(UIView *)view actions:(NSArray *)actions completion:(void (^)(BOOL))completion {
if (actions.count == 0) return completion(YES);
NSDictionary *action = actions[0];
NSArray *remainingActions = [actions subarrayWithRange:NSMakeRange(1, actions.count-1)];
[self animateView:view action:action completion:^(BOOL finished) {
[self animateView:view actions:remainingActions completion:completion];
}];
}
For the animation, you probably want to use a linear timing curve for the intermediate animations, though I can see you getting more elaborate and change the timing curve at the start and end of the list.
- (void)animateView:(UIView *)view action:(NSDictionary *)action completion:(void (^)(BOOL))completion {
NSString *dimension = action[#"dimension"];
CGFloat delta = [action[#"delta"] floatValue];
NSTimeInterval duration = [action[#"duration"] floatValue];
CGRect frame = view.frame;
if ([dimension isEqualToString:#"width"]) {
frame = CGRectInset(frame, -delta, 0);
} else if ([dimension isEqualToString:#"height"]) {
frame = CGRectInset(frame, 0, -delta);
} else {
frame = CGRectInset(frame, -delta, -delta);
}
[UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
view.frame = frame;
} completion:completion];
}
If the array of dictionaries is too clumsy to specify (and it is rather general), you could add some convenience methods on top that provide some simpler scheme to the caller and builds the array of more general representation.
There are a lot of similar questions but they all differ from this one.
I have UIScrollView which I could both scroll and stop programmatically.
I scroll via the following code:
[UIView animateWithDuration:3
delay:0
options:UIViewAnimationOptionCurveEaseInOut
animations:^{ [self.scrollView scrollRectToVisible:newPageRect animated:NO]; }];
And I don't know how to stop it at all. In all the cases it won't stop or will stop but it also jumps to newPageRect (for example in the case of removeAllAnimations).
Could you suggest how to stop it correctly? Should I possibly change my code for scrolling to another one?
I think this is something you best do yourself. It may take you a few hours to create a proper library to animate data but in the end it can be very rewarding.
A few components are needed:
A time bound animation should include either a CADispalyLink or a NSTimer. Create a public method such as animateWithDuration: which will start the timer, record a current date and set the target date. Insert a floating value as a property which should then be interpolated from 0 to 1 through date. Will most likely look something like that:
- (void)onTimer {
NSDate *currentTime = [NSDate date];
CGFloat interpolation = [currentTime timeIntervalSinceDate:self.startTime]/[self.targetTime timeIntervalSinceDate:self.startTime];
if(interpolation < .0f) { // this could happen if delay is implemented and start time may actually be larger then current
self.currentValue = .0f;
}
else if(interpolation > 1.0f) { // The animation has ended
self.currentValue = 1.0f;
[self.displayLink invalidate]; // stop the animation
// TODO: notify owner that the animation has ended
}
else {
self.currentValue = interpolation;
// TODO: notify owner of change made
}
}
As you can see from the comments you should have 2 more calls in this method which will notify the owner/listener to the changes of the animation. This may be achieved via delegates, blocks, invocations, target-selector pairs...
So at this point you have a floating value interpolating between 0 and 1 which can now be used to interpolate the rect you want to be visible. This is quite an easy method:
- (CGRect)interpolateRect:(CGRect)source to:(CGRect)target withScale:(CGFloat)scale
{
return CGRectMake(source.origin.x + (target.origin.x-source.origin.x)*scale,
source.origin.y + (target.origin.y-source.origin.y)*scale,
source.size.width + (target.size.width-source.size.width)*scale,
source.size.height + (target.size.height-source.size.height)*scale);
}
So now to put it all together it would look something like so:
- (void)animateVisibleRectTo:(CGRect)frame {
CGRect source = self.scrollView.visibleRect;
CGRect target = frame;
[self.animator animateWithDuration:.5 block:^(CGFloat scale, BOOL didFinish) {
CGRect interpolatedFrame = [Interpolator interpolateRect:source to:target withScale:scale];
[self.scrollView scrollRectToVisible:interpolatedFrame animated:NO];
}];
}
This can be a great system that can be used in very many systems when you want to animate something not animatable or simply have a better control over the animation. You may add the stop method which needs to invalidate the timer or display link and notify the owner.
What you need to look out for is not to create a retain cycle. If a class retains the animator object and the animator object retains the listener (the class) you will create a retain cycle.
Also just as a bonus you may very easily implement other properties of the animation such as delay by computing a larger start time. You can create any type of curve such as ease-in, ease-out by using an appropriate function for computing the currentValue for instance self.currentValue = pow(interpolation, 1.4) will be much like ease-in. A power of 1.0/1.4 would be a same version of ease-out.
This seems like such a basic thing to want, I can't believe I'm not able to find out how to do it. To make the description easy to understand, suppose I simply want to draw a bunch of random rectangles on the screen. These random rectangles would keep adding on top of each other repeatedly until something stopped the process. How would one do that?
The closest explanation I've seen is drawing applications, where the basic scheme is to draw into an image view, first copying the previous image into the new image and then adding the new content. Copying the original image sure seems like a waste of effort, and it sure seems like it should be possible to simply write the new content in place over whatever is there. Am I missing something obvious?
Note that drawRect replaces the entire frame. It works well for drawing a small set of objects, but it quickly becomes awkward when there's an indefinite amount of history that also needs to be displayed.
Edit: I'm attaching some sample images that are screen prints from a Mix C program that does what I'm after. Essentially, there are cellular automata that move around the screen leaving trails. The color of the trail depends upon the logic in the automaton as well as the color of the pixel where the automaton just traveled to. The automata should be able to move at rates of hundreds of pixels per second. Because of the logic used by the automata, I need to be able to not only write quickly to the image but also be able to inquire what the color of a pixel is (or mirrored data).
Typically you do this by either creating separate paths or layers for all your rectangles (if you want to keep track of them), or by drawing repeatedly into a CGBitmapContextRef, and then converting that into an image and drawing it in drawRect:. This is basically the same approach you're describing ("where the basic scheme is to draw into an image view…") except there's no need to copy the image. You just keep using the same context and making new images out of it.
The other tool you could use here is a CGLayer. The Core Graphics team discourages its use because of performance concerns, but it does make this kind of drawing much more convenient. When you look at the docs, and they say "benefit from improved performance," remember that this was written in 2006, and when I asked the Core Graphics team about it, they said that the faster answer today is CGBitmapContext. But you can't beat CGLayer for convenience on this kind of problem.
This should be fine by maintaining a CGBitmapContext that you continually write into (and that allows you to also read from it). When it changes, call setNeedsDisplayInRect:. In drawRect:, create the image, and draw it using CGContextDrawImage, passing the rect you were passed. (You may be passed the entire rect.)
It may be a little more flexible to do this on the CALayer instead of the UIView, but I doubt you'll see a great difference in performance. The view passes drawing to its layer.
The number of times a second this updates isn't really that important. drawRect: will not be called more often than the frame rate (max of 60 fps), no matter how often you call setNeedsDisplayInRect:. So you won't be creating images hundreds or thousands of times a second; just at the time that you need to draw something.
Are you seeing particular performance problems, or are you just concerned that you may in the future encounter performance problems? Do you have any sample code that shows the issue? I'm not saying it can't be slow (it might be if you're trying to do this full screen with retina). But you want to start with the simple solution and then optimize. Apple does a lot of graphics optimizations behind the scenes. Don't try to second guess them too much. They generate and draw images really well.
I've accepted another answer, but I'm including my own answer to show the alternative I used for testing.
-(void)viewWillLayoutSubviews {
self.pixelCount = 0;
self.seconds = 0;
CGRect frame = self.testImageView.bounds;
UIGraphicsBeginImageContextWithOptions(frame.size, YES, 0.0);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 1.0);
CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor);
CGContextMoveToPoint(context, 0.0, 0.0);
CGContextAddRect(context, frame);
CGContextFillRect(context, frame);
CGContextStrokePath(context);
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
CGContextStrokePath(context);
UIImage *blank = UIGraphicsGetImageFromCurrentImageContext();
self.context = context;
self.testImageView.image = blank;
// This timer has no delay, so it will draw squares as fast as possible.
[NSTimer scheduledTimerWithTimeInterval:0.0 target:self selector:#selector(drawRandomRectangle) userInfo:nil repeats:NO];
// This timer is used to control the frequency at which the imageViews image is updated.
[NSTimer scheduledTimerWithTimeInterval:1/20.f target:self selector:#selector(updateImage) userInfo:nil repeats:YES];
// This timer just outputs the counter once per second so I can record statistics.
[NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:#selector(displayCounter) userInfo:nil repeats:YES];
}
-(void)updateImage
{
self.testImageView.image = UIGraphicsGetImageFromCurrentImageContext();
}
-(void)displayCounter
{
self.seconds++;
NSLog(#"%d",self.pixelCount/self.seconds);
}
-(void)drawRandomRectangle
{
int x1 = arc4random_uniform(self.view.bounds.size.width);
int y1 = arc4random_uniform(self.view.bounds.size.height);
int xdif = 20;
int ydif = 20;
x1 -= xdif/2;
y1 -= ydif/2;
CGFloat red = (arc4random() % 256) / 255.0f;
CGFloat green = (arc4random() % 256) / 255.0f;
CGFloat blue = (arc4random() % 256) / 255.0f;
UIColor *randomColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0f];
CGRect frame = CGRectMake(x1*1.0f, y1*1.0f, xdif*1.0f, ydif*1.0f);
CGContextSetStrokeColorWithColor(self.context, [UIColor blackColor].CGColor);
CGContextSetFillColorWithColor(self.context, randomColor.CGColor);
CGContextSetLineWidth(self.context, 1.0);
CGContextAddRect(self.context, frame);
CGContextStrokePath(self.context);
CGContextFillRect(self.context, frame);
CGContextStrokePath(self.context);
if (self.pixelCount < 100000) {
[NSTimer scheduledTimerWithTimeInterval:0.0 target:self selector:#selector(drawRandomRectangle) userInfo:nil repeats:NO];
}
self.pixelCount ++;
}
The graph shows image updates per second on the x-axis and number of 20x20 squares drawn to the context per second in the y-axis.
Has someone of you ever noticed that scaling a sprite up to more than 100% can cause the render time per frame to increase and never go down again?
I've set up a test project based on a cocos2d 2.0 template (I also tested it with 2.1 and it also does happen). When touching the screen (testing with an iPad 3) it creates 100 sprites that are scaled by 1.5
When there are 5000 it removes them all. When they get removed the render time very often stays at 0.016. By double tapping the home button (causing some interrupt to happen) you can make it go back to 0.001 (when there are no sprites).
I did a lot of testing when and how to cause this and I came to the conclusion that it only happens when scaling something.
While in 0.016 "mode" there is a constant memory increase (look at the allocations tool) and as soon as you double tap the home button it goes down again slowly. These allocations come from the gyroscope / accelerometer code that I
put in there. When in 0.001 "mode" it works fine but as soon as the render time shows 0.016 memory gets allocated endlessly and it is never freed.
Especially with really big scalings this thing can be achieved easily. Does anyone have an idea how to fix this?
This does happen when touching the screen in my test project. You can download it here:
https://dl.dropboxusercontent.com/u/40859730/RenderTimeIssue.zip
static int spriteCount = 0;
- (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
for (int i = 0; i < 100; i++) {
CCSprite *icon = [CCSprite spriteWithFile: #"Icon.png"];
[icon setPosition: ccp(arc4random() % 1000, arc4random() % 1000)];
[self addChild: icon];
icon.tag = spriteCount;
// this line causes the "0.016 bug", comment it out and the frame rate does go back to 0.001 when 5000 sprites are reached
icon.scale = 1.5f; // while scaling up more than 1.0 causes the problem, scaling down does not
spriteCount++;
}
if (spriteCount >= 5000) {
for (int i = 0; i < spriteCount; i++) {
[self removeChildByTag: i cleanup: YES];
}
spriteCount = 0;
}
return YES;
}
EDIT: Here are some images that show the memory increase:
http://imgur.com/a/Jy70a
I've experienced this problem on cocos2d 2.0.
But when i've upgraded to 2.1, this dont happen anymore.
Try to upgrade cocos2d to 2.1 version. There's a lot of performance improvements on rendering images.
I am trying to move a UIView around the screen by incrementing the UIView's x property in an animation block. I want the element to move continuously so I cannot just specify an ending x and up the duration.
This code works but it is very choppy. Looks great in the simulator but choppy on the device.
-(void)moveGreyDocumentRight:(UIImageView*)greyFolderView
{
[UIView animateWithDuration:0.05 delay:0 options:UIViewAnimationOptionAllowUserInteraction animations:^{
NSInteger newX = greyFolderView.frame.origin.x + 5.0;
greyFolderView.frame = CGRectMake(newX, greyFolderView.frame.origin.y, greyFolderView.frame.size.width, greyFolderView.frame.size.height);
}
} completion:^(BOOL finished) {
[self moveGreyDocumentRight:greyFolderView];
}];
}
You're fighting the view animation here. Each one of your animations includes a UIViewAnimationOptionCurveEaseInOut timing curve. That means that every 0.05 seconds you try to ramp up your speed then slow down your speed then change to somewhere else.
The first and simplest solution is likely to change to a linear timing by passing the option UIViewAnimationOptionCurveLinear.
That said, making a new animation every 5ms really fights the point of Core Animation, complicating the code and hurting performance. Send the frame it to the place you currently want it to go. Whenever you want it to go somewhere else (even if it's still animating), send it to the new place passing the option UIViewAnimationOptionBeginFromCurrentState. It will automatically adjust to the new target. If you want it to repeat the animation or bounce back and forth, use the repeating options (UIViewAnimationOptionRepeat and UIViewAnimationOptionAutoreverse).