即使使用了基于定時器的動畫來復(fù)制第10章中關(guān)鍵幀的行為,但還是會有一些本質(zhì)上的區(qū)別:在關(guān)鍵幀的實現(xiàn)中,我們提前計算了所有幀,但是在新的解決方案中,我們實際上實在按需要在計算。意義在于我們可以根據(jù)用戶輸入實時修改動畫的邏輯,或者和別的實時動畫系統(tǒng)例如物理引擎進行整合。
我們來基于物理學(xué)創(chuàng)建一個真實的重力模擬效果來取代當(dāng)前基于緩沖的彈性動畫,但即使模擬2D的物理效果就已近極其復(fù)雜了,所以就不要嘗試去實現(xiàn)它了,直接用開源的物理引擎庫好了。
我們將要使用的物理引擎叫做Chipmunk。另外的2D物理引擎也同樣可以(例如Box2D),但是Chipmunk使用純C寫的,而不是C++,好處在于更容易和Objective-C項目整合。Chipmunk有很多版本,包括一個和Objective-C綁定的“indie”版本。C語言的版本是免費的,所以我們就用它好了。在本書寫作的時候6.1.4是最新的版本;你可以從http://chipmunk-physics.net下載它。
Chipmunk完整的物理引擎相當(dāng)巨大復(fù)雜,但是我們只會使用如下幾個類:
cpSpace
?- 這是所有的物理結(jié)構(gòu)體的容器。它有一個大小和一個可選的重力矢量cpBody
?- 它是一個固態(tài)無彈力的剛體。它有一個坐標(biāo),以及其他物理屬性,例如質(zhì)量,運動和摩擦系數(shù)等等。cpShape
?- 它是一個抽象的幾何形狀,用來檢測碰撞??梢越o結(jié)構(gòu)體添加一個多邊形,而且cpShape
有各種子類來代表不同形狀的類型。在例子中,我們來對一個木箱建模,然后在重力的影響下下落。我們來創(chuàng)建一個Crate
類,包含屏幕上的可視效果(一個UIImageView
)和一個物理模型(一個cpBody
和一個cpPolyShape
,一個cpShape
的多邊形子類來代表矩形木箱)。
用C版本的Chipmunk會帶來一些挑戰(zhàn),因為它現(xiàn)在并不支持Objective-C的引用計數(shù)模型,所以我們需要準(zhǔn)確的創(chuàng)建和釋放對象。為了簡化,我們把cpShape
和cpBody
的生命周期和Crate
類進行綁定,然后在木箱的-init
方法中創(chuàng)建,在-dealloc
中釋放。木箱物理屬性的配置很復(fù)雜,所以閱讀了Chipmunk文檔會很有意義。
視圖控制器用來管理cpSpace
,還有和之前一樣的計時器邏輯。在每一步中,我們更新cpSpace
(用來進行物理計算和所有結(jié)構(gòu)體的重新擺放)然后迭代對象,然后再更新我們的木箱視圖的位置來匹配木箱的模型(在這里,實際上只有一個結(jié)構(gòu)體,但是之后我們將要添加更多)。
Chipmunk使用了一個和UIKit顛倒的坐標(biāo)系(Y軸向上為正方向)。為了使得物理模型和視圖之間的同步更簡單,我們需要通過使用geometryFlipped
屬性翻轉(zhuǎn)容器視圖的集合坐標(biāo)(第3章中有提到),于是模型和視圖都共享一個相同的坐標(biāo)系。
具體的代碼見清單11.3。注意到我們并沒有在任何地方釋放cpSpace
對象。在這個例子中,內(nèi)存空間將會在整個app的生命周期中一直存在,所以這沒有問題。但是在現(xiàn)實世界的場景中,我們需要像創(chuàng)建木箱結(jié)構(gòu)體和形狀一樣去管理我們的空間,封裝在標(biāo)準(zhǔn)的Cocoa對象中,然后來管理Chipmunk對象的生命周期。圖11.1展示了掉落的木箱。
清單11.3 使用物理學(xué)來對掉落的木箱建模
#import "ViewController.h"
#import
#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 真實引力場下的木箱交互
對于實現(xiàn)動畫的緩沖效果來說,計算每幀持續(xù)的時間是一個很好的解決方案,但是對模擬物理效果并不理想。通過一個可變的時間步長來實現(xiàn)有著兩個弊端:
如果時間步長不是固定的,精確的值,物理效果的模擬也就隨之不確定。這意味著即使是傳入相同的輸入值,也可能在不同場合下有著不同的效果。有時候沒多大影響,但是在基于物理引擎的游戲下,玩家就會由于相同的操作行為導(dǎo)致不同的結(jié)果而感到困惑。同樣也會讓測試變得麻煩。
我們想得到的理想的效果就是通過固定的時間步長來計算物理效果,但是在屏幕發(fā)生重繪的時候仍然能夠同步更新視圖(可能會由于在我們控制范圍之外造成不可預(yù)知的效果)。
幸運的是,由于我們的模型(在這個例子中就是Chipmunk的cpSpace
中的cpBody
)被視圖(就是屏幕上代表木箱的UIView
對象)分離,于是就很簡單了。我們只需要根據(jù)屏幕刷新的時間跟蹤時間步長,然后根據(jù)每幀去計算一個或者多個模擬出來的效果。
我們可以通過一個簡單的循環(huán)來實現(xiàn)。通過每次CADisplayLink
的啟動來通知屏幕將要刷新,然后記錄下當(dāng)前的CACurrentMediaTime()
。我們需要在一個小增量中提前重復(fù)物理模擬(這里用120分之一秒)直到趕上顯示的時間。然后更新我們的視圖,在屏幕刷新的時候匹配當(dāng)前物理結(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);
}
當(dāng)使用固定的模擬時間步長時候,有一件事情一定要注意,就是用來計算物理效果的現(xiàn)實世界的時間并不會加速模擬時間步長。在我們的例子中,我們隨意選擇了120分之一秒來模擬物理效果。Chipmunk很快,我們的例子也很簡單,所以cpSpaceStep()
會完成的很好,不會延遲幀的更新。
但是如果場景很復(fù)雜,比如有上百個物體之間的交互,物理計算就會很復(fù)雜,cpSpaceStep()
的計算也可能會超出1/120秒。我們沒有測量出物理步長的時間,因為我們假設(shè)了相對于幀刷新來說并不重要,但是如果模擬步長更久的話,就會延遲幀率。
如果幀刷新的時間延遲的話會變得很糟糕,我們的模擬需要執(zhí)行更多的次數(shù)來同步真實的時間。這些額外的步驟就會繼續(xù)延遲幀的更新,等等。這就是所謂的死亡螺旋,因為最后的結(jié)果就是幀率變得越來越慢,直到最后應(yīng)用程序卡死了。
我們可以通過添加一些代碼在設(shè)備上來對物理步驟計算真實世界的時間,然后自動調(diào)整固定時間步長,但是實際上它不可行。其實只要保證你給容錯留下足夠的邊長,然后在期望支持的最慢的設(shè)備上進行測試就可以了。如果物理計算超過了模擬時間的50%,就需要考慮增加模擬時間步長(或者簡化場景)。如果模擬時間步長增加到超過1/60秒(一個完整的屏幕更新時間),你就需要減少動畫幀率到一秒30幀或者增加CADisplayLink
的frameInterval
來保證不會隨機丟幀,不然你的動畫將會看起來不平滑。
更多建議: