Timer-Based Animation - Setting Things in Motion - iOS Core Animation: Advanced Techniques (2014)

iOS Core Animation: Advanced Techniques (2014)

Part II. Setting Things in Motion

Chapter 11. Timer-Based Animation

I can guide you, but you must do exactly as I say.

Morpheus, The Matrix

In Chapter 10, “Easing,” we looked at CAMediaTimingFunction, which enables us to control the easing of animations to add realism by simulating physical effects such as acceleration and deceleration. But what if we want to simulate even more realistic physical interactions or to modify our animation on-the-fly in response to user input? In this chapter, we explore timer-based animations that allow us to precisely control how our animation will behave on a frame-by-frame basis.

Frame Timing

Animation appears to show continuous movement, but in reality that’s impossible when the display pixels are at fixed locations. Ordinary displays cannot display continuous movement; all they can do is display a sequence of static images fast enough that you perceive it as motion.

We mentioned previously that iOS refreshes the screen 60 times per second. What CAAnimation does is calculate a new frame to display and then draws it in sync with each screen update. Most of the cleverness of CAAnimation is in doing the interpolation and easing calculations to work out what to display each time.

In Chapter 10, we worked out how to do the interpolation and easing ourselves, and then essentially told a CAKeyframeAnimation instance exactly what to draw by providing an array of frames to be displayed. All Core Animation was doing for us at that point was displaying those frames in sequence. Surely we can do that part ourselves as well?

NSTimer

Actually, we already did something like that in Chapter 3, “Layer Geometry,” with the clock example, when we used an NSTimer to animate the hand movement. In that example, the hand only updated once per second, but the principle is really no different if we speed up the timer to fire 60 times per second instead.

Let’s try modifying the bouncing ball animation from Chapter 10 to use an NSTimer instead of a CAKeyframeAnimation. Because we will now be calculating the animation frames continuously as the timer fires (instead of in advance), we need some additional properties in our class to store the animation’s fromValue, toValue, duration, and the current timeOffset (see Listing 11.1).

Listing 11.1 Implementing the Bouncing Ball Animation Using NSTimer


@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, strong) UIImageView *ballView;
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, assign) NSTimeInterval duration;
@property (nonatomic, assign) NSTimeInterval timeOffset;
@property (nonatomic, strong) id fromValue;
@property (nonatomic, strong) id toValue;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];

//add ball image view
UIImage *ballImage = [UIImage imageNamed:@"Ball.png"];
self.ballView = [[UIImageView alloc] initWithImage:ballImage];
[self.containerView addSubview:self.ballView];

//animate
[self animate];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//replay animation on tap
[self animate];
}

float interpolate(float from, float to, float time)
{
return (to - from) * time + from;
}

- (id)interpolateFromValue:(id)fromValue
toValue:(id)toValue
time:(float)time
{
if ([fromValue isKindOfClass:[NSValue class]])
{
//get type
const char *type = [(NSValue *)fromValue objCType];
if (strcmp(type, @encode(CGPoint)) == 0)
{
CGPoint from = [fromValue CGPointValue];
CGPoint to = [toValue CGPointValue];
CGPoint result = CGPointMake(interpolate(from.x, to.x, time),
interpolate(from.y, to.y, time));
return [NSValue valueWithCGPoint:result];
}
}

//provide safe default implementation
return (time < 0.5)? fromValue: toValue;
}

float bounceEaseOut(float t)
{
if (t < 4/11.0)
{
return (121 * t * t)/16.0;
}
else if (t < 8/11.0)
{
return (363/40.0 * t * t) - (99/10.0 * t) + 17/5.0;
}
else if (t < 9/10.0)
{
return (4356/361.0 * t * t) - (35442/1805.0 * t) + 16061/1805.0;
}
return (54/5.0 * t * t) - (513/25.0 * t) + 268/25.0;
}

- (void)animate
{
//reset ball to top of screen
self.ballView.center = CGPointMake(150, 32);

//configure the animation
self.duration = 1.0;
self.timeOffset = 0.0;
self.fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)];
self.toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)];

//stop the timer if it's already running

[self.timer invalidate];

//start the timer
self.timer = [NSTimer scheduledTimerWithTimeInterval:1/60.0
target:self
selector:@selector(step:)
userInfo:nil
repeats:YES];
}

- (void)step:(NSTimer *)step
{
//update time offset
self.timeOffset = MIN(self.timeOffset + 1/60.0, self.duration);

//get normalized time offset (in range 0 - 1)
float time = self.timeOffset / self.duration;

//apply easing
time = bounceEaseOut(time);

//interpolate position
id position = [self interpolateFromValue:self.fromValue
toValue:self.toValue
time:time];

//move ball view to new position
self.ballView.center = [position CGPointValue];

//stop the timer if we've reached the end of the animation
if (self.timeOffset >= self.duration)
{
[self.timer invalidate];
self.timer = nil;
}
}

@end


That works pretty well, and is roughly the same amount of code as the keyframe-based version. But there are some problems with this approach that would become apparent if we tried to animate a lot of things onscreen at once.

An NSTimer is not the optimal way to draw stuff to the screen that needs to refresh every frame. To understand why, we need to look at exactly how NSTimer works. Every thread on iOS maintains an NSRunloop, which in simple terms is a loop that endlessly works through a list of tasks it needs to perform. For the main thread, these tasks might include the following:

Image Processing touch events

Image Sending and receiving network packets

Image Executing code scheduled using GCD (Grand Central Dispatch)

Image Handling timer actions

Image Redrawing the screen

When you set up an NSTimer, it gets inserted into this task list with an instruction that it must not be executed until at least the specified time has elapsed. There is no upper limit on how long a timer may wait before it fires; it will only happen after the previous task in the list has finished. This will usually be within a few milliseconds of the scheduled time, but it might take longer if the previous task is slow to complete.

The screen redrawing is scheduled to happen every sixtieth of a second, but just like a timer action, it may get delayed by an earlier task in the list that takes too long to execute. Because these delays are effectively random, the result is that it is impossible to guarantee that a timer scheduled to fire every sixtieth of a second will always fire before the screen is redrawn. Sometimes it will happen too late, resulting in a delay in the update that will make the animation appear choppy. Sometimes it may fire twice between screen updates, resulting in a skipped frame that will make the animation appear to jump forward.

We can do some things to improve this:

Image We can use a special type of timer called CADisplayLink that is designed to fire in lockstep with the screen refresh.

Image We can base our animations on the actual recorded frame duration instead of assuming that frames will fire on time.

Image We can adjust the run loop mode of our animation timer so that it won’t be delayed by other events.

CADisplayLink

CADisplayLink is an NSTimer-like class provided by Core Animation that always fires immediately prior to the screen being redrawn. Its interface is very similar to NSTimer, so it is essentially a drop-in replacement, but instead of a timeInterval specified in seconds, theCADisplayLink has an integer frameInterval property that specifies how many frames to skip each time it fires. By default, this has a value of 1, meaning that it should fire every frame. But if your animation code takes too long to reliably execute within a sixtieth of a second, you can specify a frameInterval of 2, meaning that your animation will update every other frame (30 frames per second) or 3, resulting in 20 frames per second, and so on.

Using a CADisplayLink instead of an NSTimer will produce a smoother animation by ensuring that the frame rate is as consistent as possible. But even CADisplayLink cannot guarantee that every frame will happen on schedule. A stray task or an event outside of your control (such as a resource-hungry background application) might still cause your animation to occasionally skip frames. When using an NSTimer, the timer will simply fire whenever it gets a chance, but CADisplayLink works differently: If it misses a scheduled frame, it will skip it altogether and update at the next scheduled frame time.

Measuring Frame Duration

Regardless of whether we use an NSTimer or CADisplayLink, we still need to handle the scenario where a frame takes longer to calculate than the expected time of one-sixtieth of a second. Because we cannot know the actual frame duration in advance, we have to measure it as it happens. We can do this by recording the time at the start of each frame using the CACurrentMediaTime() function, and then comparing it to the time recorded for the previous frame.

By comparing these times, we get an accurate frame duration measurement that we can use in place of the hard-coded one-sixtieth value for our timing calculations. Let’s update our example with these improvements (see Listing 11.2).

Listing 11.2 Measuring Frame Duration for Smoother Animation


@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, strong) UIImageView *ballView;
@property (nonatomic, strong) CADisplayLink *timer;
@property (nonatomic, assign) CFTimeInterval duration;
@property (nonatomic, assign) CFTimeInterval timeOffset;
@property (nonatomic, assign) CFTimeInterval lastStep;
@property (nonatomic, strong) id fromValue;
@property (nonatomic, strong) id toValue;

@end

@implementation ViewController

...

- (void)animate
{
//reset ball to top of screen
self.ballView.center = CGPointMake(150, 32);

//configure the animation
self.duration = 1.0;
self.timeOffset = 0.0;
self.fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)];
self.toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)];

//stop the timer if it's already running
[self.timer invalidate];

//start the timer
self.lastStep = CACurrentMediaTime();
self.timer = [CADisplayLink displayLinkWithTarget:self
selector:@selector(step:)];
[self.timer addToRunLoop:[NSRunLoop mainRunLoop]
forMode:NSDefaultRunLoopMode];

}

- (void)step:(CADisplayLink *)timer
{
//calculate time delta
CFTimeInterval thisStep = CACurrentMediaTime();
CFTimeInterval stepDuration = thisStep - self.lastStep;
self.lastStep = thisStep;

//update time offset
self.timeOffset = MIN(self.timeOffset + stepDuration, self.duration);

//get normalized time offset (in range 0 - 1)
float time = self.timeOffset / self.duration;

//apply easing
time = bounceEaseOut(time);

//interpolate position
id position = [self interpolateFromValue:self.fromValue
toValue:self.toValue
time:time];

//move ball view to new position
self.ballView.center = [position CGPointValue];

//stop the timer if we've reached the end of the animation
if (self.timeOffset >= self.duration)
{
[self.timer invalidate];
self.timer = nil;
}
}

@end


Run Loop Modes

Notice that when we create the CADisplayLink, we are required to specify a run loop and run loop mode. For the run loop, we’ve used the main run loop (the run loop hosted by the main thread) because any user interface updates should always be performed on the main thread. The choice of mode is less clear, though. Every task that is added to the run loop has a mode that determines its priority. To ensure that the user interface remains smooth at all times, iOS will give priority to user interface related tasks and may actually stop executing other tasks altogether for a brief time if there is too much UI activity.

A typical example of this is when you are scrolling using a UIScrollView. During the scroll, redrawing the scrollview content takes priority over other tasks, so standard NSTimer and network events may not fire while this is happening. Some common choices for the run loop mode are as follows:

Image NSDefaultRunLoopMode—The standard priority

Image NSRunLoopCommonModes—High priority

Image UITrackingRunLoopMode—Used for animating UIScrollView and other controls

In our example, we have used NSDefaultRunLoopMode, but to ensure that our animation runs smoothly, we could use NSRunLoopCommonModes instead. Just be cautious when using this mode because when your animation is running at a high frame rate, you may find that other tasks such as timers or other iOS animations such as scrolling will stop updating until your animation has finished.

It’s possible to use a CADisplayLink with multiple run loop modes at the same time, so we could add it to both NSDefaultRunLoopMode and UITrackingRunLoopMode to ensure that it is not disrupted by scrolling, without interfering with the performance of other UIKit control animations, like this:

self.timer = [CADisplayLink displayLinkWithTarget:self
selector:@selector(step:)];
[self.timer addToRunLoop:[NSRunLoop mainRunLoop]
forMode:NSDefaultRunLoopMode];
[self.timer addToRunLoop:[NSRunLoop mainRunLoop]
forMode:UITrackingRunLoopMode];

Like CADisplayLink, NSTimer can also be configured to use different run loop modes by using this alternative setup code instead of the +scheduledTimerWithTimeInterval: constructor:

self.timer = [NSTimer timerWithTimeInterval:1/60.0
target:self
selector:@selector(step:)
userInfo:nil
repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.timer
forMode:NSRunLoopCommonModes];

Physical Simulation

Although we’ve used our timer-based animation to replicate the behavior of the keyframe animation from Chapter 10, there is actually a fundamental difference in the way that it works: In the keyframe implementation, we had to calculate all the frames in advance, but in our new solution, we are calculating them on demand. The significance of this is that it means we can modify the animation logic on-the-fly in response to user input, or integrate with other real-time animation systems such as a physics engine.

Chipmunk

Instead of our current easing-based bounce animation, let’s use physics to create a truly realistic gravity simulation. Simulating physics accurately—even in 2D—is incredibly complex, so we won’t attempt to implement this from scratch. Instead, we’ll use an open source physics library, orengine.

The physics engine we’re going to use is called Chipmunk. Other 2D physics libraries are available (such as Box2D), but Chipmunk is written in pure C instead of C++, making it easier to integrate into an Objective-C project. Chipmunk comes in various flavors, including an “indie” version with Objective-C bindings. The plain C version is free, though, so that’s what we will use for this example. Version 6.1.4 is the latest at the time of writing; you can download it from http://chipmunk-physics.net.

The Chipmunk physics engine as a whole is quite large and complex, but we will be using only the following classes:

Image cpSpace—This is the container for all of the physics bodies. It has a size and (optionally) a gravity vector.

Image cpBody—This is a solid, inelastic object. It has a position in space and other physical properties such as mass, moment, coefficient of friction, and so on.

Image cpShape—This is an abstract geometric shape, used for detecting collisions. Multiple shapes may be attached to a body, and there are concrete subclasses of cpShape to represent different shape types.

In our example, we will model a wooden crate that falls under the influence of gravity. We will create a Crate class that encompasses both the visual representation of the crate onscreen (a UIImageView) and the physical model that represents it (a cpBody and cpPolyShape, which is a polygonal cpShape subclass that we will use to represent the rectangular crate).

Using the C version of Chipmunk introduces some challenges because it doesn’t support Objective-C’s reference counting model, so we need to explicitly create and free objects. To simplify this, we tie the lifespan of the cpShape and cpBody to our Crate class by creating them in the crate’s -init method and freeing them in -dealloc. The configuration of the crate’s physical properties is fairly complex, but it should make sense if you read the Chipmunk documentation.

The view controller will manage the cpSpace, along with the timer logic as before. At each step, we’ll update the cpSpace (which performs the physics calculations and repositions all the bodies in the world) and then iterate through the bodies and update the positions of our crate views to match the bodies that model them. (In this case, there is actually only one body, but we will add more later.)

Chipmunk uses an inverted coordinate system with respect to UIKit (the Y axis points upward). To make it easier to keep our physics model in sync with our view, we’ll invert our container view’s geometry using the geometryFlipped property (mentioned in Chapter 3) so that model and view are both using the same coordinate system.

The code for the crate example is shown in Listing 11.3. Note that we aren’t freeing the cpSpace object anywhere. In this simple example, the space will exist for the duration of the app lifespan anyway, so this isn’t really an issue but in a real-world scenario, we should really handle this in the same way we do with the crate body and shape, by wrapping it in standalone Cocoa object and using that to manage the Chipmunk object’s lifecycle. Figure 11.1 shows the falling crate.

Listing 11.3 Modeling a Falling Crate Using Realistic Physics


#import "ViewController.h"
#import <QuartzCore/QuartzCore.h>
#import "chipmunk.h"

@interface Crate : UIImageView

@property (nonatomic, assign) cpBody *body;
@property (nonatomic, assign) cpShape *shape;

@end

@implementation Crate

#define MASS 100

- (id)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame]))
{
//set image
self.image = [UIImage imageNamed:@"Crate.png"];
self.contentMode = UIViewContentModeScaleAspectFill;

//create the body
self.body = cpBodyNew(MASS, cpMomentForBox(MASS, frame.size.width,
frame.size.height));

//create the shape
cpVect corners[] = {
cpv(0, 0),
cpv(0, frame.size.height),
cpv(frame.size.width, frame.size.height),
cpv(frame.size.width, 0),
};
self.shape = cpPolyShapeNew(self.body, 4, corners,
cpv(-frame.size.width/2,
-frame.size.height/2));

//set shape friction & elasticity
cpShapeSetFriction(self.shape, 0.5);
cpShapeSetElasticity(self.shape, 0.8);

//link the crate to the shape
//so we can refer to crate from callback later on
self.shape->data = (__bridge void *)self;

//set the body position to match view
cpBodySetPos(self.body, cpv(frame.origin.x + frame.size.width/2,
300 - frame.origin.y - frame.size.height/2));

}
return self;
}

- (void)dealloc
{
//release shape and body
cpShapeFree(_shape);
cpBodyFree(_body);
}

@end

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, assign) cpSpace *space;
@property (nonatomic, strong) CADisplayLink *timer;
@property (nonatomic, assign) CFTimeInterval lastStep;

@end

@implementation ViewController

#define GRAVITY 1000

- (void)viewDidLoad
{
//invert view coordinate system to match physics
self.containerView.layer.geometryFlipped = YES;

//set up physics space
self.space = cpSpaceNew();
cpSpaceSetGravity(self.space, cpv(0, -GRAVITY));

//add a crate
Crate *crate = [[Crate alloc] initWithFrame:CGRectMake(100, 0, 100, 100)];
[self.containerView addSubview:crate];
cpSpaceAddBody(self.space, crate.body);
cpSpaceAddShape(self.space, crate.shape);

//start the timer
self.lastStep = CACurrentMediaTime();
self.timer = [CADisplayLink displayLinkWithTarget:self
selector:@selector(step:)];
[self.timer addToRunLoop:[NSRunLoop mainRunLoop]
forMode:NSDefaultRunLoopMode];
}

void updateShape(cpShape *shape, void *unused)
{
//get the crate object associated with the shape
Crate *crate = (__bridge Crate *)shape->data;

//update crate view position and angle to match physics shape
cpBody *body = shape->body;
crate.center = cpBodyGetPos(body);
crate.transform = CGAffineTransformMakeRotation(cpBodyGetAngle(body));
}

- (void)step:(CADisplayLink *)timer
{
//calculate step duration
CFTimeInterval thisStep = CACurrentMediaTime();
CFTimeInterval stepDuration = thisStep - self.lastStep;
self.lastStep = thisStep;

//update physics
cpSpaceStep(self.space, stepDuration);

//update all the shapes
cpSpaceEachShape(self.space, &updateShape, NULL);
}

@end


Image

Figure 11.1 A crate image, falling due to simulated gravity

Adding User Interaction

The next step is to add an invisible wall around the edge of the view so that the crate doesn’t fall off the bottom of the screen. You might think that we would implement this with another rectangular cpPolyShape, like we used for our crate, but we want to detect when our crate leaves the view, not when it is colliding with it, so we need a hollow rectangle, not a solid one.

We can implement this by adding four cpSegmentShape objects to our cpSpace. (cpSegmentShape represents a straight line segment, so four of them can be combined to form a rectangle.) We will attach these to the space’s staticBody property (an immovable body that is not affected by gravity) instead of a new cpBody instance as we did with each crate, because we do not want the bounding rectangle to fall off the screen or be dislodged when hit by a falling crate.

We’ll also add a few more crates so they can interact with one another. Finally, we’ll add accelerometer support so that tilting the phone adjusts the gravity vector. (Note that to test this you will need to run on a real device because the simulator doesn’t generate accelerometer events, even if you rotate the screen.) Listing 11.4 shows the updated code, and Figure 11.2 shows the result.

Image

Figure 11.2 Multiple crates interacting with realistic physics

Because our example is locked to landscape mode, we’ve swapped the x and y values for the accelerometer vector. If you are running the example in portrait mode, you will need to swap them back to ensure the gravity direction matches up with what’s displayed on the screen. You’ll know if you get it wrong; the crates will fall upward or sideways!

Listing 11.4 Updated Physics Example with Walls and Multiple Crates


- (void)addCrateWithFrame:(CGRect)frame
{
Crate *crate = [[Crate alloc] initWithFrame:frame];
[self.containerView addSubview:crate];
cpSpaceAddBody(self.space, crate.body);
cpSpaceAddShape(self.space, crate.shape);
}

- (void)addWallShapeWithStart:(cpVect)start end:(cpVect)end
{
cpShape *wall = cpSegmentShapeNew(self.space->staticBody, start, end, 1);
cpShapeSetCollisionType(wall, 2);
cpShapeSetFriction(wall, 0.5);
cpShapeSetElasticity(wall, 0.8);
cpSpaceAddStaticShape(self.space, wall);
}

- (void)viewDidLoad
{
//invert view coordinate system to match physics
self.containerView.layer.geometryFlipped = YES;

//set up physics space
self.space = cpSpaceNew();
cpSpaceSetGravity(self.space, cpv(0, -GRAVITY));

//add wall around edge of view
[self addWallShapeWithStart:cpv(0, 0) end:cpv(300, 0)];
[self addWallShapeWithStart:cpv(300, 0) end:cpv(300, 300)];
[self addWallShapeWithStart:cpv(300, 300) end:cpv(0, 300)];
[self addWallShapeWithStart:cpv(0, 300) end:cpv(0, 0)];

//add a crates
[self addCrateWithFrame:CGRectMake(0, 0, 32, 32)];
[self addCrateWithFrame:CGRectMake(32, 0, 32, 32)];
[self addCrateWithFrame:CGRectMake(64, 0, 64, 64)];
[self addCrateWithFrame:CGRectMake(128, 0, 32, 32)];
[self addCrateWithFrame:CGRectMake(0, 32, 64, 64)];

//start the timer
self.lastStep = CACurrentMediaTime();
self.timer = [CADisplayLink displayLinkWithTarget:self
selector:@selector(step:)];
[self.timer addToRunLoop:[NSRunLoop mainRunLoop]
forMode:NSDefaultRunLoopMode];

//update gravity using accelerometer
[UIAccelerometer sharedAccelerometer].delegate = self;
[UIAccelerometer sharedAccelerometer].updateInterval = 1/60.0;
}

- (void)accelerometer:(UIAccelerometer *)accelerometer
didAccelerate:(UIAcceleration *)acceleration
{
//update gravity
cpSpaceSetGravity(self.space, cpv(acceleration.y * GRAVITY,
-acceleration.x * GRAVITY));
}


Simulation Time and Fixed Time Steps

Calculating the frame duration and using it to advance the animation was a good solution for the easing-based animation, but it’s not ideal for advancing our physics simulation. Having a variable time step for a physics simulation is a bad thing for two reasons:

Image If the time step is not set to a fixed, precise value, the physics simulation will not be deterministic. This means that given the same exact inputs it may produce different results on different occasions. Sometimes this doesn’t matter, but in a physics-based game, players might be confused when the exact same actions on their part lead to different outcomes. It also makes testing more difficult.

Image A skipped frame due to a performance glitch or an interruption like a phone call might cause incorrect behavior. Consider a fast-moving object like a bullet: Each frame, the simulation will move the bullet forward and check for collisions. If the time between frames increases, the bullet will move further in a single simulation step and may pass right through a wall or other obstacle without ever intersecting it, causing the collision to be missed.

What we ideally want is to have short fixed time steps for our physics simulation, but still update our views in sync with the display redraw (which may be unpredictable due to circumstances outside our control).

Fortunately, because our models (in this case, the cpBody objects in our Chipmunk cpSpace) are separated from our views (the UIView objects representing our crates onscreen), this is quite easy. We just need to keep track of the simulation step time independently of our display frame time, and potentially perform multiple simulation steps for each display frame.

We can do this using a simple loop. Each time our CADisplayLink fires to let us know the frame needs redrawing, we make a note of the CACurrentMediaTime(). We then advance our physics simulation repeatedly in small increments (1/120th of a second in this case) until the physics time catches up to the display time. We can then update our views to match the current positions of the physics bodies in preparation for the screen refresh.

Listing 11.5 shows the code for the fixed-step version of our crate simulation.

Listing 11.5 Fixed Time Step Crate Simulation


#define SIMULATION_STEP (1/120.0)

- (void)step:(CADisplayLink *)timer
{
//calculate frame step duration
CFTimeInterval frameTime = CACurrentMediaTime();

//update simulation
while (self.lastStep < frameTime)
{
cpSpaceStep(self.space, SIMULATION_STEP);
self.lastStep += SIMULATION_STEP;
}

//update all the shapes
cpSpaceEachShape(self.space, &updateShape, NULL);
}


Avoiding the Spiral of Death

One thing you must ensure when using fixed simulation time steps is that the real-world time taken to perform the physics calculations does not exceed the simulated time step. In our example, we’ve chosen the arbitrary time step of 1/120th of a second for our physics simulation. Chipmunk is fast and our example is simple, so our cpSpaceStep() should complete well within that time and is unlikely to delay our frame update.

But suppose we had a more complex scene with hundreds of objects all interacting: The physics calculations would become more complex and the cpSpaceStep() might actually take more than 1/120th of a second to complete. We aren’t measuring the time for our physics step because we are assuming that it will be trivial compared to the frame update, but if our simulation steps take longer to execute than the time period they are simulating, they will delay the frame update.

It gets worse: If the frame takes longer to update, our simulation will need to perform more steps to keep the simulated time in sync with real time. Those additional steps will then delay the frame update further, and so on. This is known as the spiral of death because the result is that the frame rate becomes slower and slower until the application effectively freezes.

We could add code to measure the real-world time for our physics step on a given device and adjust the fixed time step automatically, but in practice this is probably overkill. Just ensure that you leave a generous margin for error and test on the slowest device you intend to support. If the physics calculations take more than ~50% of the simulated time, consider increasing your simulation time step (or simplifying your scene). If your simulation time step increases to the point that it takes more than one-sixtieth of a second (an entire screen frame update), you will need to reduce your animation frame rate to 30 frames per second or less by increasing the CADisplayLink frameInterval so that you don’t start randomly skipping frames, which will make your animation look unsmooth.

Summary

In this chapter, you learned how to create animations on a frame-by-frame basis using a timer combined with a variety of animation techniques including easing, physics simulation, and user input (via the accelerometer).

In Part III, we look at how animation performance is impacted by the constraints of the hardware, and learn how to tune our code to get the best frame rate possible.