注意到当建立CADisplayLink
的时候,咱们须要指定一个run loop
和run loop mode
,对于run loop来讲,咱们就使用了主线程的run loop,由于任何用户界面的更新都须要在主线程执行,可是模式的选择就并不那么清楚了,每一个添加到run loop的任务都有一个指定了优先级的模式,为了保证用户界面保持平滑,iOS会提供和用户界面相关任务的优先级,并且当UI很活跃的时候的确会暂停一些别的任务。git
一个典型的例子就是当是用UIScrollview
滑动的时候,重绘滚动视图的内容会比别的任务优先级更高,因此标准的NSTimer
和网络请求就不会启动,一些常见的run loop模式以下:github
NSDefaultRunLoopMode
- 标准优先级网络
NSRunLoopCommonModes
- 高优先级app
UITrackingRunLoopMode
- 用于UIScrollView
和别的控件的动画函数
在咱们的例子中,咱们是用了NSDefaultRunLoopMode
,可是不能保证动画平滑的运行,因此就能够用NSRunLoopCommonModes
来替代。可是要当心,由于若是动画在一个高帧率状况下运行,你会发现一些别的相似于定时器的任务或者相似于滑动的其余iOS动画会暂停,直到动画结束。oop
一样能够同时对CADisplayLink
指定多个run loop模式,因而咱们能够同时加入 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];
和CADisplayLink
相似,NSTimer
一样也可使用不一样的run loop模式配置,经过别的函数,而不是 +scheduledTimerWithTimeInterval: 构造器测试
self.timer = [NSTimer timerWithTimeInterval:1/60.0 target:self selector:@selector(step:) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
即便使用了基于定时器的动画来复制第10章中关键帧的行为,但仍是会有一些本质上的区别:在关键帧的实现中,咱们提早计算了全部帧,可是在新的解决方案中,咱们实际上实在按须要在计算。意义在于咱们能够根据用户输入实时修改动画的逻辑,或者和别的实时动画系统例如物理引擎进行整合。动画
咱们来基于物理学建立一个真实的重力模拟效果来取代当前基于缓冲的弹性动画,但即便模拟2D的物理效果就已近极其复杂了,因此就不要尝试去实现它了,直接用开源的物理引擎库好了。this
咱们将要使用的物理引擎叫作Chipmunk。另外的2D物理引擎也一样能够(例如Box2D),可是Chipmunk使用纯C写的,而不是C++,好处在于更容易和Objective-C项目整合。Chipmunk有不少版本,包括一个和Objective-C绑定的“indie”版本。C语言的版本是免费的,因此咱们就用它好了。在本书写做的时候6.1.4是最新的版本;你能够从http://chipmunk-physics.net下载它。
Chipmunk完整的物理引擎至关巨大复杂,可是咱们只会使用以下几个类:
cpSpace
- 这是全部的物理结构体的容器。它有一个大小和一个可选的重力矢量
cpBody
- 它是一个固态无弹力的刚体。它有一个坐标,以及其余物理属性,例如质量,运动和摩擦系数等等。
cpShape
- 它是一个抽象的几何形状,用来检测碰撞。能够给结构体添加一个多边形,并且cpShape
有各类子类来表明不一样形状的类型。
在例子中,咱们来对一个木箱建模,而后在重力的影响下下落。咱们来建立一个Crate
类,包含屏幕上的可视效果(一个UIImageView
)和一个物理模型(一个cpBody
和一个cpPolyShape
,一个cpShape
的多边形子类来表明矩形木箱)。
用C版本的Chipmunk会带来一些挑战,由于它如今并不支持Objective-C的引用计数模型,因此咱们须要准确的建立和释放对象。为了简化,咱们把cpShape
和cpBody
的生命周期和Crate
类进行绑定,而后在木箱的-init
方法中建立,在-dealloc
中释放。木箱物理属性的配置很复杂,因此阅读了Chipmunk文档会颇有意义。
视图控制器用来管理cpSpace
,还有和以前同样的计时器逻辑。在每一步中,咱们更新cpSpace
(用来进行物理计算和全部结构体的从新摆放)而后迭代对象,而后再更新咱们的木箱视图的位置来匹配木箱的模型(在这里,实际上只有一个结构体,可是以后咱们将要添加更多)。
Chipmunk使用了一个和UIKit颠倒的坐标系(Y轴向上为正方向)。为了使得物理模型和视图之间的同步更简单,咱们须要经过使用geometryFlipped
属性翻转容器视图的集合坐标(第3章中有提到),因而模型和视图都共享一个相同的坐标系。
具体的代码见清单11.3。注意到咱们并无在任何地方释放cpSpace
对象。在这个例子中,内存空间将会在整个app的生命周期中一直存在,因此这没有问题。可是在现实世界的场景中,咱们须要像建立木箱结构体和形状同样去管理咱们的空间,封装在标准的Cocoa对象中,而后来管理Chipmunk对象的生命周期。图11.1展现了掉落的木箱。
清单11.3 使用物理学来对掉落的木箱建模
#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
图11.1 一个木箱图片,根据模拟的重力掉落
下一步就是在视图周围添加一道不可见的墙,这样木箱就不会掉落出屏幕以外。或许你会用另外一个矩形的cpPolyShape
来实现,就和以前建立木箱那样,可是咱们须要检测的是木箱什么时候离开视图,而不是什么时候碰撞,因此咱们须要一个空心而不是固体矩形。
咱们能够经过给cpSpace
添加四个cpSegmentShape
对象(cpSegmentShape
表明一条直线,因此四个拼起来就是一个矩形)。而后赋给空间的staticBody
属性(一个不被重力影响的结构体)而不是像木箱那样一个新的cpBody
实例,由于咱们不想让这个边框矩形滑出屏幕或者被一个下落的木箱击中而消失。
一样能够再添加一些木箱来作一些交互。最后再添加一个加速器,这样能够经过倾斜手机来调整重力矢量(为了测试须要在一台真实的设备上运行程序,由于模拟器不支持加速器事件,即便旋转屏幕)。清单11.4展现了更新后的代码,运行结果见图11.2。
因为示例只支持横屏模式,因此交换加速计矢量的x和y值。若是在竖屏下运行程序,请把他们换回来,否则重力方向就错乱了。试一下就知道了,木箱会沿着横向移动。
清单11.4 使用围墙和多个木箱的更新后的代码
- (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)); }
图11.1 真实引力场下的木箱交互
对于实现动画的缓冲效果来讲,计算每帧持续的时间是一个很好的解决方案,可是对模拟物理效果并不理想。经过一个可变的时间步长来实现有着两个弊端:
若是时间步长不是固定的,精确的值,物理效果的模拟也就随之不肯定。这意味着即便是传入相同的输入值,也可能在不一样场合下有着不一样的效果。有时候没多大影响,可是在基于物理引擎的游戏下,玩家就会因为相同的操做行为致使不一样的结果而感到困惑。一样也会让测试变得麻烦。
因为性能故常形成的丢帧或者像电话呼入的中断均可能会形成不正确的结果。考虑一个像子弹那样快速移动物体,每一帧的更新都须要移动子弹,检测碰撞。若是两帧之间的时间加长了,子弹就会在这一步移动更远的距离,穿过围墙或者是别的障碍,这样就丢失了碰撞。
咱们想获得的理想的效果就是经过固定的时间步长来计算物理效果,可是在屏幕发生重绘的时候仍然可以同步更新视图(可能会因为在咱们控制范围以外形成不可预知的效果)。
幸运的是,因为咱们的模型(在这个例子中就是Chipmunk的cpSpace
中的cpBody
)被视图(就是屏幕上表明木箱的UIView
对象)分离,因而就很简单了。咱们只须要根据屏幕刷新的时间跟踪时间步长,而后根据每帧去计算一个或者多个模拟出来的效果。
咱们能够经过一个简单的循环来实现。经过每次CADisplayLink
的启动来通知屏幕将要刷新,而后记录下当前的CACurrentMediaTime()
。咱们须要在一个小增量中提早重复物理模拟(这里用120分之一秒)直到遇上显示的时间。而后更新咱们的视图,在屏幕刷新的时候匹配当前物理结构体的显示位置。
清单11.5展现了固定时间步长版本的代码
清单11.5 固定时间步长的木箱模拟
#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); }
当使用固定的模拟时间步长时候,有一件事情必定要注意,就是用来计算物理效果的现实世界的时间并不会加速模拟时间步长。在咱们的例子中,咱们随意选择了120分之一秒来模拟物理效果。Chipmunk很快,咱们的例子也很简单,因此 cpSpaceStep() 会完成的很好,不会延迟帧的更新。
可是若是场景很复杂,好比有上百个物体之间的交互,物理计算就会很复杂, cpSpaceStep() 的计算也可能会超出1/120秒。咱们没有测量出物理步长的时间,由于咱们假设了相对于帧刷新来讲并不重要,可是若是模拟步长更久的话,就会延迟帧率。
若是帧刷新的时间延迟的话会变得很糟糕,咱们的模拟须要执行更多的次数来同步真实的时间。这些额外的步骤就会继续延迟帧的更新,等等。这就是所谓的死亡螺旋,由于最后的结果就是帧率变得愈来愈慢,直到最后应用程序卡死了。
咱们能够经过添加一些代码在设备上来对物理步骤计算真实世界的时间,而后自动调整固定时间步长,可是实际上它不可行。其实只要保证你给容错留下足够的边长,而后在指望支持的最慢的设备上进行测试就能够了。若是物理计算超过了模拟时间的50%,就须要考虑增长模拟时间步长(或者简化场景)。若是模拟时间步长增长到超过1/60秒(一个完整的屏幕更新时间),你就须要减小动画帧率到一秒30帧或者增长CADisplayLink
的frameInterval
来保证不会随机丢帧,否则你的动画将会看起来不平滑。
在这一章中,咱们了解了如何经过一个计时器建立一帧帧的实时动画,包括缓冲,物理模拟等等一系列动画技术,以及用户输入(经过加速计)。