注意到當建立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
來保證不會隨機丟幀,否則你的動畫將會看起來不平滑。
在這一章中,咱們瞭解瞭如何經過一個計時器建立一幀幀的實時動畫,包括緩衝,物理模擬等等一系列動畫技術,以及用戶輸入(經過加速計)。