//nstimer
//[self timerTest];
//cadisplaylink
// [self displayLinkTest];
//chipmunk
//[self chipmunkTest];
定时帧
//iOS按照每秒60次刷新屏幕,然后CAAnimation计算出需要展示的新的帧,然后在每次屏幕更新的时候同步绘制上去,CAAnimation最机智的地方在于每次刷新需要展示的时候去计算插值和缓冲。
//NSTimer
- (void)timerTest {
containerView = [[UIView alloc] init];
containerView.frame = CGRectMake(20.f, 64.0f, 300.0f, 300.0f);
containerView.backgroundColor = [UIColor blackColor];
[self.view addSubview:containerView];
//add image view
imageView = [[UIImageView alloc] init];
imageView.frame = CGRectMake(0.0, 0.0f, 30.0, 30.0f);
imageView.image = [UIImage imageNamed:@"Star"];
[containerView addSubview:imageView];
//animate
// [self pointToLineAnimation];
[self timerAnimation];
}
- (void)timerAnimation {
//reset ball to top of screen
imageView.center = CGPointMake(150, 32);
//configure the animation
duration = 1.0;
timeOffset = 0.0;
fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)];
toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)];
//stop the timer if it's already running
[timer invalidate];
//start the timer
timer = [NSTimer scheduledTimerWithTimeInterval:1/60.0f
target:self
selector:@selector(timerAction:)
userInfo:nil
repeats:YES];
}
- (void)timerAction:(NSTimer *)step {
//update time offset
timeOffset = MIN(timeOffset + 1/60.0, duration);
//get normalized time offset (in range 0-1)
float time = timeOffset / duration;
//apply easing
time = bounceEaseOut(time);
//interpolate position
id position = [self interpolateFromValue:fromValue
toValure:toValue
time:time];
//move ball view to new position
imageView.center = [position CGPointValue];
//stop the timer if we've reached the end of the animation
if (timeOffset >= duration) {
[timer invalidate];
timer = nil;
}
}
//对主线程,这些任务包含如下几项
//处理触摸事件
//发送和接受网络数据包
//执行使用gcd的代码
//处理计时器行为
//屏幕重绘
//我们可以通过一些途径来优化:
//我们可以用CADisplayLink让更新频率严格控制在每次屏幕刷新之后。
//基于真实帧的持续时间而不是假设的更新频率来做动画。
//调整动画计时器的run loop模式,这样就不会被别的事件干扰。
CADisplayLink
//CADisplayLink是CoreAnimation提供的另一个类似于NSTimer的类,它总是在屏幕完成一次更新之前启动
//CADisplayLink有一个整型的frameInterval属性,指定了间隔多少帧之后才执行。默认值是1,意味着每次屏幕更新之前都会执行一次,如果动画的代码执行起来超过了六十分之一秒,你可以指定frameInterval为2,就是说动画每隔一帧执行一次(一秒钟30帧)或者3,也就是一秒钟20次
//当使用NSTimer的时候,一旦有机会计时器就会开启,但是CADisplayLink却不一样:如果它丢失了帧,就会直接忽略它们,然后在下一次更新的时候接着运行。
计算帧的持续时间
//我们可以在每帧开始刷新的时候用CACurrentMediaTime()记录当前时间,然后和上一帧记录的时间去比较。
- (void)displayLinkTest {
containerView = [[UIView alloc] init];
containerView.frame = CGRectMake(20.f, 64.0f, 300.0f, 300.0f);
containerView.backgroundColor = [UIColor blackColor];
[self.view addSubview:containerView];
//add image view
imageView = [[UIImageView alloc] init];
imageView.frame = CGRectMake(0.0, 0.0f, 30.0, 30.0f);
imageView.image = [UIImage imageNamed:@"Star"];
[containerView addSubview:imageView];
//animate
//[self timerAnimation];
[self displayLinkAnimation];
}
- (void)displayLinkAnimation {
//reset ball to top of screen
imageView.center = CGPointMake(150.0, 32);
//configure the animation
duration2 = 1.0f;timeOffset2 = 0.0;
fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)];
toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)];
//stop the timer if it's already running
[timer2 invalidate];
//start the timer2
lastStep = CACurrentMediaTime();
timer2 = [CADisplayLink displayLinkWithTarget:self
selector:@selector(timer2Action:)];
[timer2 addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)timer2Action:(CADisplayLink *)timer {
//calculate time delta
CFTimeInterval thisStep = CACurrentMediaTime();
CFTimeInterval stepDuration = thisStep - lastStep;
lastStep = thisStep;
//upadate time offset
timeOffset2 = MIN(timeOffset2 + stepDuration, duration2);
//get normalized time offset (in range 0 - 1)
float time = timeOffset2 / duration2;
//apply easing
time = bounceEaseOut(time);
//interpolate position
id position = [self interpolateFromValue:fromValue toValure:toValue time:time];
//move ball view to new position
imageView.center = [position CGPointValue];
//stop the timer if we've reached the end of the animation
if (timeOffset2 >= duration2) {
[timer2 invalidate];
timer2 = nil;
}
}
Run Loop 模式
//run loop模式如下
//NSDefaultRunLoopMode - 标准优先级
//NSRunLoopCommonModes - 高优先级
//UITrackingRunLoopMode - 用于UIScrollView和别的控件的动画
//于是我们可以同时加入NSDefaultRunLoopMode和UITrackingRunLoopMode来保证它不会被滑动打断,也不会被其他UIKit控件动画影响性能,像这样:
//self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(step:)];
//[self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
//[self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:UITrackingRunLoopMode];
//NSTimer同样也可以使用不同的run loop模式配置,通过self.timer = [NSTimer timerWithTimeInterval:1/60.0 target:self selector:@selector(step:) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
物理模拟
//Chipmunk:我们将要使用的物理引擎叫做Chipmunk。Chipmunk使用纯C写的,而不是C++,好处在于更容易和Objective-C项目整合
//包括一个和Objective-C绑定的“indie”版本 http://chipmunk-physics.net下载它
//cpSpace - 这是所有的物理结构体的容器。它有一个大小和一个可选的重力矢量
//cpBody - 它是一个固态无弹力的刚体。它有一个坐标,以及其他物理属性,例如质量,运动和摩擦系数等等。
//cpShape - 它是一个抽象的几何形状,用来检测碰撞。可以给结构体添加一个多边形,而且cpShape有各种子类来代表不同形状的类型。 在例子中,我们来对一个木箱建模,然后在重力的影响下下落。我们来创建一个Crate类,包含屏幕上的可视效果(一个UIImageView)和一个物理模型(一个cpBody和一个cpPolyShape,一个cpShape的多边形子类来代表矩形木箱)。
//Chipmunk使用了一个和UIKit颠倒的坐标系(Y轴向上为正方向)。为了使得物理模型和视图之间的同步更简单,我们需要通过使用geometryFlipped属性翻转容器视图的集合坐标(第3章中有提到),于是模型和视图都共享一个相同的坐标系。
/*
#define GRAVITY 1000
- (void)chipmunkTest {
containerView = [[UIView alloc] init];
containerView.frame = CGRectMake(20.f, 64.0f, 300.0f, 300.0f);
containerView.backgroundColor = [UIColor blackColor];
[self.view addSubview:containerView];
//invert view coordinate system to match physics
containerView.layer.geometryFlipped = YES;
//set up physics space
space = cpSpaceNew();
cpSpaceSetGravity(space, cpv(0, -GRAVITY));
//add a crate
Crate *crate = [[Crate alloc] initWithFrame:CGRectMake(100, 0, 100, 100)];
[containerView addSubview:crate];
cpSpaceAddBody(space, crate.body);
cpSpaceAddShape(space, crate.shape);
//start the timer
lastStep = CACurrentMediaTime();
timer2 = [CADisplayLink displayLinkWithTarget:self selector:@selector(step:)];
[timer2 addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}
void updateShape(cpShape *shape, void *unused) {
//get the create object associated with the shape
// Crate *crate = (__bridge Crate *)shape->data;
Crate *crate = (__bridge Crate *)shape;
//update crate view position and angle to match physics shape
// cpBody *body = shape->body;
cpBody *body = crate.body;
// crate.center = cpBodyGetPos(body);
crate.center = CGPointMake(0, 0);
crate.transform = CGAffineTransformMakeRotation(cpBodyGetAngle(body));
}
- (void)step:(CADisplayLink *)timer
{
//calculate step duration
CFTimeInterval thisStep = CACurrentMediaTime();
CFTimeInterval stepDuration = thisStep - lastStep;
lastStep = thisStep;
//update physics
cpSpaceStep(space, stepDuration);
//update all the shapes
cpSpaceEachShape(space, &updateShape, NULL);
}
*/
添加用户交互
//不想让这个边框矩形滑出屏幕或者被一个下落的木箱击中而消失,可以通过给cpSpace添加四个cpSegmentShape对象(cpSegmentShape代表一条直线,所以四个拼起来就是一个矩形)。然后赋给空间的staticBody属性(一个不被重力影响的结构体),而不是像木箱那样一个新的cpBody实例
/*
- (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)chipmunkTest {
containerView = [[UIView alloc] init];
containerView.frame = CGRectMake(20.f, 64.0f, 300.0f, 300.0f);
containerView.backgroundColor = [UIColor blackColor];
[self.view addSubview:containerView];
//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));
}
*/
模拟时间以及固定的时间步长
//通过每次CADisplayLink的启动来通知屏幕将要刷新,然后记录下当前的CACurrentMediaTime()。
/*
#define SIMULATION_STEP (1/120.0)
- (void)step:(CADisplayLink *)timer
{
//calculate frame step duration
CFTimeInterval frameTime = CACurrentMediaTime();
//update simulation
while (lastStep < frameTime) {
cpSpaceStep(space, SIMULATION_STEP);
lastStep += SIMULATION_STEP;
}
//update all the shapes
cpSpaceEachShape(space, &updateShape, NULL);
}
*/
避免死亡螺旋
//如果帧刷新的时间延迟的话会变得很糟糕,我们的模拟需要执行更多的次数来同步真实的时间。这些额外的步骤就会继续延迟帧的更新,等等。这就是所谓的死亡螺旋,因为最后的结果就是帧率变得越来越慢,直到最后应用程序卡死了。
//只要保证你给容错留下足够的边长,然后在期望支持的最慢的设备上进行测试就可以了。如果物理计算超过了模拟时间的50%,就需要考虑增加模拟时间步长(或者简化场景)。如果模拟时间步长增加到超过1/60秒(一个完整的屏幕更新时间),你就需要减少动画帧率到一秒30帧或者增加CADisplayLink的frameInterval来保证不会随机丢帧,不然你的动画将会看起来不平滑。