11、基于定時器的動畫

基于定時器的動畫

我可以指導你琉苇,但是你必須按照我說的做。 -- 駭客帝國

在第10章“緩沖”中悦施,我們研究了CAMediaTimingFunction并扇,它是一個通過控制動畫緩沖來模擬物理效果例如加速或者減速來增強現(xiàn)實感的東西,那么如果想更加真實地模擬物理交互或者實時根據(jù)用戶輸入修改動畫改怎么辦呢抡诞?在這一章中穷蛹,我們將繼續(xù)探索一種能夠允許我們精確地控制一幀一幀展示的基于定時器的動畫。

定時幀

動畫看起來是用來顯示一段連續(xù)的運動過程昼汗,但實際上當在固定位置上展示像素的時候并不能做到這一點肴熏。一般來說這種顯示都無法做到連續(xù)的移動,能做的僅僅是足夠快地展示一系列靜態(tài)圖片顷窒,只是看起來像是做了運動蛙吏。

我們之前提到過iOS按照每秒60次刷新屏幕,然后CAAnimation計算出需要展示的新的幀鞋吉,然后在每次屏幕更新的時候同步繪制上去鸦做,CAAnimation最機智的地方在于每次刷新需要展示的時候去計算插值和緩沖。

在第10章中谓着,我們解決了如何自定義緩沖函數(shù)泼诱,然后根據(jù)需要展示的幀的數(shù)組來告訴CAKeyframeAnimation的實例如何去繪制。所有的Core Animation實際上都是按照一定的序列來顯示這些幀赊锚,那么我們可以自己做到這些么治筒?

NSTimer

實際上,我們在第三章“圖層幾何學”中已經(jīng)做過類似的東西改抡,就是時鐘那個例子矢炼,我們用了NSTimer來對鐘表的指針做定時動畫,一秒鐘更新一次阿纤,但是如果我們把頻率調(diào)整成一秒鐘更新60次的話句灌,原理是完全相同的。

我們來試著用NSTimer來修改第十章中彈性球的例子欠拾。由于現(xiàn)在我們在定時器啟動之后連續(xù)計算動畫幀胰锌,我們需要在類中添加一些額外的屬性來存儲動畫的fromValuetoValue藐窄,duration和當前的timeOffset(見清單11.1)资昧。

清單11.1 使用NSTimer實現(xiàn)彈性球動畫

@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

很贊,而且和基于關(guān)鍵幀例子的代碼一樣很多荆忍,但是如果想一次性在屏幕上對很多東西做動畫格带,很明顯就會有很多問題撤缴。

NSTimer并不是最佳方案,為了理解這點叽唱,我們需要確切地知道NSTimer是如何工作的屈呕。iOS上的每個線程都管理了一個NSRunloop,字面上看就是通過一個循環(huán)來完成一些任務列表棺亭。但是對主線程虎眨,這些任務包含如下幾項:

  • 處理觸摸事件
  • 發(fā)送和接受網(wǎng)絡數(shù)據(jù)包
  • 執(zhí)行使用gcd的代碼
  • 處理計時器行為
  • 屏幕重繪

當你設置一個NSTimer,他會被插入到當前任務列表中镶摘,然后直到指定時間過去之后才會被執(zhí)行嗽桩。但是何時啟動定時器并沒有一個時間上限,而且它只會在列表中上一個任務完成之后開始執(zhí)行凄敢。這通常會導致有幾毫秒的延遲碌冶,但是如果上一個任務過了很久才完成就會導致延遲很長一段時間。

屏幕重繪的頻率是一秒鐘六十次贡未,但是和定時器行為一樣种樱,如果列表中上一個執(zhí)行了很長時間,它也會延遲俊卤。這些延遲都是一個隨機值,于是就不能保證定時器精準地一秒鐘執(zhí)行六十次害幅。有時候發(fā)生在屏幕重繪之后消恍,這就會使得更新屏幕會有個延遲,看起來就是動畫卡殼了以现。有時候定時器會在屏幕更新的時候執(zhí)行兩次狠怨,于是動畫看起來就跳動了。

我們可以通過一些途徑來優(yōu)化:

  • 我們可以用CADisplayLink讓更新頻率嚴格控制在每次屏幕刷新之后邑遏。
  • 基于真實幀的持續(xù)時間而不是假設的更新頻率來做動畫佣赖。
  • 調(diào)整動畫計時器的run loop模式,這樣就不會被別的事件干擾记盒。

CADisplayLink

CADisplayLink是CoreAnimation提供的另一個類似于NSTimer的類憎蛤,它總是在屏幕完成一次更新之前啟動,它的接口設計的和NSTimer很類似纪吮,所以它實際上就是一個內(nèi)置實現(xiàn)的替代俩檬,但是和timeInterval以秒為單位不同,CADisplayLink有一個整型的frameInterval屬性碾盟,指定了間隔多少幀之后才執(zhí)行棚辽。默認值是1,意味著每次屏幕更新之前都會執(zhí)行一次冰肴。但是如果動畫的代碼執(zhí)行起來超過了六十分之一秒屈藐,你可以指定frameInterval為2榔组,就是說動畫每隔一幀執(zhí)行一次(一秒鐘30幀)或者3,也就是一秒鐘20次联逻,等等瓷患。

CADisplayLink而不是NSTimer,會保證幀率足夠連續(xù)遣妥,使得動畫看起來更加平滑擅编,但即使CADisplayLink也不能保證每一幀都按計劃執(zhí)行,一些失去控制的離散的任務或者事件(例如資源緊張的后臺程序)可能會導致動畫偶爾地丟幀箫踩。當使用NSTimer的時候爱态,一旦有機會計時器就會開啟,但是CADisplayLink卻不一樣:如果它丟失了幀境钟,就會直接忽略它們锦担,然后在下一次更新的時候接著運行。

計算幀的持續(xù)時間

無論是使用NSTimer還是CADisplayLink慨削,我們?nèi)匀恍枰幚硪粠臅r間超出了預期的六十分之一秒洞渔。由于我們不能夠計算出一幀真實的持續(xù)時間,所以需要手動測量缚态。我們可以在每幀開始刷新的時候用CACurrentMediaTime()記錄當前時間磁椒,然后和上一幀記錄的時間去比較。

通過比較這些時間玫芦,我們就可以得到真實的每幀持續(xù)的時間浆熔,然后代替硬編碼的六十分之一秒。我們來更新一下上個例子(見清單11.2)桥帆。

清單11.2 通過測量沒幀持續(xù)的時間來使得動畫更加平滑

@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 模式

注意到當創(chuàng)建CADisplayLink的時候医增,我們需要指定一個run looprun loop mode,對于run loop來說老虫,我們就使用了主線程的run loop叶骨,因為任何用戶界面的更新都需要在主線程執(zhí)行,但是模式的選擇就并不那么清楚了祈匙,每個添加到run loop的任務都有一個指定了優(yōu)先級的模式忽刽,為了保證用戶界面保持平滑,iOS會提供和用戶界面相關(guān)任務的優(yōu)先級菊卷,而且當UI很活躍的時候的確會暫停一些別的任務缔恳。

一個典型的例子就是當是用UIScrollview滑動的時候,重繪滾動視圖的內(nèi)容會比別的任務優(yōu)先級更高洁闰,所以標準的NSTimer和網(wǎng)絡請求就不會啟動歉甚,一些常見的run loop模式如下:

  • NSDefaultRunLoopMode - 標準優(yōu)先級
  • NSRunLoopCommonModes - 高優(yōu)先級
  • UITrackingRunLoopMode - 用于UIScrollView和別的控件的動畫

在我們的例子中,我們是用了NSDefaultRunLoopMode扑眉,但是不能保證動畫平滑的運行纸泄,所以就可以用NSRunLoopCommonModes來替代赖钞。但是要小心,因為如果動畫在一個高幀率情況下運行聘裁,你會發(fā)現(xiàn)一些別的類似于定時器的任務或者類似于滑動的其他iOS動畫會暫停雪营,直到動畫結(jié)束。

同樣可以同時對CADisplayLink指定多個run loop模式衡便,于是我們可以同時加入NSDefaultRunLoopModeUITrackingRunLoopMode來保證它不會被滑動打斷献起,也不會被其他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模式配置谴餐,通過別的函數(shù),而不是+scheduledTimerWithTimeInterval:構(gòu)造器

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

物理模擬

即使使用了基于定時器的動畫來復制第10章中關(guān)鍵幀的行為呆抑,但還是會有一些本質(zhì)上的區(qū)別:在關(guān)鍵幀的實現(xiàn)中岂嗓,我們提前計算了所有幀,但是在新的解決方案中鹊碍,我們實際上實在按需要在計算厌殉。意義在于我們可以根據(jù)用戶輸入實時修改動畫的邏輯,或者和別的實時動畫系統(tǒng)例如物理引擎進行整合侈咕。

Chipmunk

我們來基于物理學創(chuàng)建一個真實的重力模擬效果來取代當前基于緩沖的彈性動畫公罕,但即使模擬2D的物理效果就已近極其復雜了,所以就不要嘗試去實現(xiàn)它了乎完,直接用開源的物理引擎庫好了熏兄。

我們將要使用的物理引擎叫做Chipmunk。另外的2D物理引擎也同樣可以(例如Box2D)树姨,但是Chipmunk使用純C寫的,而不是C++桥状,好處在于更容易和Objective-C項目整合帽揪。Chipmunk有很多版本,包括一個和Objective-C綁定的“indie”版本辅斟。C語言的版本是免費的转晰,所以我們就用它好了。在本書寫作的時候6.1.4是最新的版本士飒;你可以從http://chipmunk-physics.net下載它查邢。

Chipmunk完整的物理引擎相當巨大復雜,但是我們只會使用如下幾個類:

  • cpSpace - 這是所有的物理結(jié)構(gòu)體的容器酵幕。它有一個大小和一個可選的重力矢量
  • cpBody - 它是一個固態(tài)無彈力的剛體扰藕。它有一個坐標,以及其他物理屬性芳撒,例如質(zhì)量邓深,運動和摩擦系數(shù)等等未桥。
  • cpShape - 它是一個抽象的幾何形狀,用來檢測碰撞芥备《ⅲ可以給結(jié)構(gòu)體添加一個多邊形,而且cpShape有各種子類來代表不同形狀的類型萌壳。

在例子中亦镶,我們來對一個木箱建模,然后在重力的影響下下落袱瓮。我們來創(chuàng)建一個Crate類缤骨,包含屏幕上的可視效果(一個UIImageView)和一個物理模型(一個cpBody和一個cpPolyShape,一個cpShape的多邊形子類來代表矩形木箱)懂讯。

用C版本的Chipmunk會帶來一些挑戰(zhàn)荷憋,因為它現(xiàn)在并不支持Objective-C的引用計數(shù)模型,所以我們需要準確的創(chuàng)建和釋放對象褐望。為了簡化勒庄,我們把cpShapecpBody的生命周期和Crate類進行綁定,然后在木箱的-init方法中創(chuàng)建瘫里,在-dealloc中釋放实蔽。木箱物理屬性的配置很復雜,所以閱讀了Chipmunk文檔會很有意義谨读。

視圖控制器用來管理cpSpace局装,還有和之前一樣的計時器邏輯。在每一步中劳殖,我們更新cpSpace(用來進行物理計算和所有結(jié)構(gòu)體的重新擺放)然后迭代對象铐尚,然后再更新我們的木箱視圖的位置來匹配木箱的模型(在這里,實際上只有一個結(jié)構(gòu)體哆姻,但是之后我們將要添加更多)宣增。

Chipmunk使用了一個和UIKit顛倒的坐標系(Y軸向上為正方向)。為了使得物理模型和視圖之間的同步更簡單矛缨,我們需要通過使用geometryFlipped屬性翻轉(zhuǎn)容器視圖的集合坐標(第3章中有提到)爹脾,于是模型和視圖都共享一個相同的坐標系。

具體的代碼見清單11.3箕昭。注意到我們并沒有在任何地方釋放cpSpace對象灵妨。在這個例子中,內(nèi)存空間將會在整個app的生命周期中一直存在落竹,所以這沒有問題泌霍。但是在現(xiàn)實世界的場景中,我們需要像創(chuàng)建木箱結(jié)構(gòu)體和形狀一樣去管理我們的空間筋量,封裝在標準的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 一個木箱圖片肋拔,根據(jù)模擬的重力掉落

添加用戶交互

下一步就是在視圖周圍添加一道不可見的墻锈津,這樣木箱就不會掉落出屏幕之外×狗洌或許你會用另一個矩形的cpPolyShape來實現(xiàn)琼梆,就和之前創(chuàng)建木箱那樣,但是我們需要檢測的是木箱何時離開視圖窿吩,而不是何時碰撞茎杂,所以我們需要一個空心而不是固體矩形。

我們可以通過給cpSpace添加四個cpSegmentShape對象(cpSegmentShape代表一條直線纫雁,所以四個拼起來就是一個矩形)煌往。然后賦給空間的staticBody屬性(一個不被重力影響的結(jié)構(gòu)體)而不是像木箱那樣一個新的cpBody實例,因為我們不想讓這個邊框矩形滑出屏幕或者被一個下落的木箱擊中而消失轧邪。

同樣可以再添加一些木箱來做一些交互刽脖。最后再添加一個加速器,這樣可以通過傾斜手機來調(diào)整重力矢量(為了測試需要在一臺真實的設備上運行程序忌愚,因為模擬器不支持加速器事件曲管,即使旋轉(zhuǎn)屏幕)。清單11.4展示了更新后的代碼硕糊,運行結(jié)果見圖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 真實引力場下的木箱交互

模擬時間以及固定的時間步長

對于實現(xiàn)動畫的緩沖效果來說胶逢,計算每幀持續(xù)的時間是一個很好的解決方案,但是對模擬物理效果并不理想饰潜。通過一個可變的時間步長來實現(xiàn)有著兩個弊端:

  • 如果時間步長不是固定的初坠,精確的值,物理效果的模擬也就隨之不確定彭雾。這意味著即使是傳入相同的輸入值碟刺,也可能在不同場合下有著不同的效果。有時候沒多大影響薯酝,但是在基于物理引擎的游戲下半沽,玩家就會由于相同的操作行為導致不同的結(jié)果而感到困惑爽柒。同樣也會讓測試變得麻煩。

  • 由于性能故常造成的丟幀或者像電話呼入的中斷都可能會造成不正確的結(jié)果者填『拼澹考慮一個像子彈那樣快速移動物體,每一幀的更新都需要移動子彈占哟,檢測碰撞心墅。如果兩幀之間的時間加長了,子彈就會在這一步移動更遠的距離榨乎,穿過圍墻或者是別的障礙怎燥,這樣就丟失了碰撞。

我們想得到的理想的效果就是通過固定的時間步長來計算物理效果蜜暑,但是在屏幕發(fā)生重繪的時候仍然能夠同步更新視圖(可能會由于在我們控制范圍之外造成不可預知的效果)铐姚。

幸運的是,由于我們的模型(在這個例子中就是Chipmunk的cpSpace中的cpBody)被視圖(就是屏幕上代表木箱的UIView對象)分離肛捍,于是就很簡單了隐绵。我們只需要根據(jù)屏幕刷新的時間跟蹤時間步長,然后根據(jù)每幀去計算一個或者多個模擬出來的效果篇梭。

我們可以通過一個簡單的循環(huán)來實現(xiàn)氢橙。通過每次CADisplayLink的啟動來通知屏幕將要刷新,然后記錄下當前的CACurrentMediaTime()恬偷。我們需要在一個小增量中提前重復物理模擬(這里用120分之一秒)直到趕上顯示的時間悍手。然后更新我們的視圖,在屏幕刷新的時候匹配當前物理結(jié)構(gòu)體的顯示位置袍患。

清單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);
}

避免死亡螺旋

當使用固定的模擬時間步長時候坦康,有一件事情一定要注意,就是用來計算物理效果的現(xiàn)實世界的時間并不會加速模擬時間步長诡延。在我們的例子中滞欠,我們隨意選擇了120分之一秒來模擬物理效果。Chipmunk很快肆良,我們的例子也很簡單筛璧,所以cpSpaceStep()會完成的很好,不會延遲幀的更新惹恃。

但是如果場景很復雜夭谤,比如有上百個物體之間的交互,物理計算就會很復雜巫糙,cpSpaceStep()的計算也可能會超出1/120秒朗儒。我們沒有測量出物理步長的時間,因為我們假設了相對于幀刷新來說并不重要,但是如果模擬步長更久的話醉锄,就會延遲幀率乏悄。

如果幀刷新的時間延遲的話會變得很糟糕,我們的模擬需要執(zhí)行更多的次數(shù)來同步真實的時間恳不。這些額外的步驟就會繼續(xù)延遲幀的更新檩小,等等。這就是所謂的死亡螺旋妆够,因為最后的結(jié)果就是幀率變得越來越慢识啦,直到最后應用程序卡死了。

我們可以通過添加一些代碼在設備上來對物理步驟計算真實世界的時間神妹,然后自動調(diào)整固定時間步長颓哮,但是實際上它不可行。其實只要保證你給容錯留下足夠的邊長鸵荠,然后在期望支持的最慢的設備上進行測試就可以了冕茅。如果物理計算超過了模擬時間的50%,就需要考慮增加模擬時間步長(或者簡化場景)蛹找。如果模擬時間步長增加到超過1/60秒(一個完整的屏幕更新時間)姨伤,你就需要減少動畫幀率到一秒30幀或者增加CADisplayLinkframeInterval來保證不會隨機丟幀,不然你的動畫將會看起來不平滑庸疾。

總結(jié)

在這一章中乍楚,我們了解了如何通過一個計時器創(chuàng)建一幀幀的實時動畫,包括緩沖届慈,物理模擬等等一系列動畫技術(shù)徒溪,以及用戶輸入(通過加速計)。

在第三部分中金顿,我們將研究動畫性能是如何被被設備限制所影響的臊泌,以及如何調(diào)整我們的代碼來活的足夠好的幀率。

文章摘錄自:https://github.com/AttackOnDobby/iOS-Core-Animation-Advanced-Techniques

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末揍拆,一起剝皮案震驚了整個濱河市渠概,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌嫂拴,老刑警劉巖播揪,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異筒狠,居然都是意外死亡剪芍,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進店門窟蓝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事运挫∽垂玻” “怎么了?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵谁帕,是天一觀的道長峡继。 經(jīng)常有香客問我,道長匈挖,這世上最難降的妖魔是什么碾牌? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮儡循,結(jié)果婚禮上茉帅,老公的妹妹穿的比我還像新娘何吝。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布爽醋。 她就那樣靜靜地躺著,像睡著了一般鬓长。 火紅的嫁衣襯著肌膚如雪乏德。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天齿穗,我揣著相機與錄音傲隶,去河邊找鬼。 笑死窃页,一個胖子當著我的面吹牛跺株,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播腮出,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼帖鸦,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了胚嘲?” 一聲冷哼從身側(cè)響起作儿,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎馋劈,沒想到半個月后攻锰,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡妓雾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年娶吞,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片械姻。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡妒蛇,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情绣夺,我是刑警寧澤吏奸,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站陶耍,受9級特大地震影響奋蔚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜烈钞,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一泊碑、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧毯欣,春花似錦馒过、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至算吩,卻和暖如春留凭,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背偎巢。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工蔼夜, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人压昼。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓求冷,卻偏偏與公主長得像,于是被迫代替她去往敵國和親窍霞。 傳聞我的和親對象是個殘疾皇子匠题,可洞房花燭夜當晚...
    茶點故事閱讀 44,601評論 2 353

推薦閱讀更多精彩內(nèi)容