CADisplayLink を作成するときは、実行ループと実行ループ モードを指定する必要があることに注意してください。実行ループには、メイン スレッドの実行ループが使用されます。インターフェイスはメインスレッドで実行する必要がありますが、実行ループに追加される各タスクには、ユーザー インターフェイスをスムーズに保つために指定された優先度を持つモードが用意されています。ユーザー インターフェイス関連のタスクの優先順位。UI が非常にアクティブな場合、他のタスクは実際に一時停止されます。
典型的な例は、UIScrollview でスライドする場合、スクロール ビューのコンテンツの再描画が他のタスクよりも優先されるため、標準の NSTimer およびネットワーク リクエストが開始されないことです。いくつかの一般的な実行ループ モードは次のとおりです。
NSDefaultRunLoopMode - 標準優先度
NSRunLoopCommonModes - 高優先度
UITrackingRunLoopMode - UIScrollView およびその他のコントロールのアニメーション
この例では、NSDefaultRunLoopMode を使用しました。ただし、アニメーションがスムーズに実行されることは保証できないため、代わりに NSRunLoopCommonModes を使用できます。ただし、アニメーションが高いフレーム レートで実行されている場合、タイマーなどの他のタスクやスライドなどの他の iOS アニメーションがアニメーションが終了するまで一時停止される可能性があるので注意してください。
CADisplayLink に複数の実行ループ モードを同時に指定することもできるので、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 も、+scheduledTimerWithTimeInterval の代わりに他の関数を通じて異なる実行ループ モードを使用して設定できます。constructor
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 物理的エフェクトのシミュレーションですらすでに非常に複雑なので、試さないでください。これを実装するには、オープンソースの物理エンジン ライブラリを使用するだけです。
これから使用する物理エンジンは Chipmunk と呼ばれます。他の 2D 物理エンジン (Box2D など) も利用できますが、Chipmunk は C++ ではなく純粋な C で書かれているため、Objective-C プロジェクトとの統合が容易です。 Chipmunk には、Objective-C にバインドされた「インディー」バージョンを含む、多くのバージョンがあります。 C バージョンは無料なので、そのまま使用します。この記事の執筆時点では、6.1.4 が最新バージョンです。http://chipmunk-physics.net からダウンロードできます。
シマリスの完全な物理エンジンは非常に巨大で複雑ですが、次のクラスのみを使用します:
cpSpace - これはすべての物理構造のコンテナです。サイズとオプションの重力ベクトルがあります。
cpBody - 固体の非弾性剛体です。座標に加えて、質量、運動係数、摩擦などの他の物理的特性も持っています。
cpShape - 衝突を検出するために使用される抽象的な幾何学的形状です。構造にポリゴンを追加できます。cpShape には、さまざまな形状タイプを表すさまざまなサブクラスがあります。
この例では、重力の影響下で落下する木箱をモデル化します。画面上のビジュアル (UIImageView) と物理モデル (cpBody および cpPolyShape、長方形の木箱を表す cpShape のポリゴン サブクラス) を含む Crate クラスを作成しましょう。
Chipmunk の C バージョンを使用すると、現在 Objective-C 参照カウント モデルをサポートしていないため、いくつかの課題が発生します。そのため、オブジェクトを正確に作成してリリースする必要があります。簡略化するために、cpShape と cpBody のライフ サイクルを Crate クラスにバインドし、木箱の -init メソッドで作成し、-dealloc で解放します。木箱の物理的プロパティの設定は複雑なので、Chipmunk のドキュメントを読むことが重要です。
ビュー コントローラーは cpSpace の管理に使用され、以前と同じタイマー ロジックを備えています。各ステップで、cpSpace (物理計算を実行し、すべての構造を再配置するために使用されます) を更新し、オブジェクトを反復処理してから、ボックス モデルに一致するようにボックス ビューの位置を更新します (ここでは、実際には A 構造体しかありませんが、後でさらに追加します)。
シマリスは、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 秒 (完全な画面更新時間) を超える場合は、アニメーション フレーム レートを 1 秒あたり 30 フレームに下げるか、CADisplayLink の FrameInterval を増やして、フレームがランダムにドロップされないようにする必要があります。アニメーションが滑らかに見えなくなります。
この章では、バッファリング、物理シミュレーション、一連のアニメーション技術など、タイマーを使用してフレームごとのリアルタイム アニメーションを作成する方法を学習しました。ユーザー入力 (加速度センサー経由)。