物理模擬(基於定時器的動畫 11.2)

来源:http://www.cnblogs.com/EchoHG/archive/2017/10/05/7629876.html
-Advertisement-
Play Games

物理模擬 即使使用了基於定時器的動畫來複制第10章中關鍵幀的行為,但還是會有一些本質上的區別:在關鍵幀的實現中,我們提前計算了所有幀,但是在新的解決方案中,我們實際上實在按需要在計算。意義在於我們可以根據用戶輸入實時修改動畫的邏輯,或者和別的實時動畫系統例如物理引擎進行整合。 Chipmunk 我們 ...


物理模擬

即使使用了基於定時器的動畫來複制第10章中關鍵幀的行為,但還是會有一些本質上的區別:在關鍵幀的實現中,我們提前計算了所有幀,但是在新的解決方案中,我們實際上實在按需要在計算。意義在於我們可以根據用戶輸入實時修改動畫的邏輯,或者和別的實時動畫系統例如物理引擎進行整合。

Chipmunk

我們來基於物理學創建一個真實的重力模擬效果來取代當前基於緩衝的彈性動畫,但即使模擬2D的物理效果就已近極其複雜了,所以就不要嘗試去實現它了,直接用開源的物理引擎庫好了。

我們將要使用的物理引擎叫做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的引用計數模型,所以我們需要準確的創建和釋放對象。為了簡化,我們把cpShapecpBody的生命周期和Crate類進行綁定,然後在木箱的-init方法中創建,在-dealloc中釋放。木箱物理屬性的配置很複雜,所以閱讀了Chipmunk文檔會很有意義。

視圖控制器用來管理cpSpace,還有和之前一樣的計時器邏輯。在每一步中,我們更新cpSpace(用來進行物理計算和所有結構體的重新擺放)然後迭代對象,然後再更新我們的木箱視圖的位置來匹配木箱的模型(在這裡,實際上只有一個結構體,但是之後我們將要添加更多)。

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

具體的代碼見清單11.3。註意到我們並沒有在任何地方釋放cpSpace對象。在這個例子中,記憶體空間將會在整個app的生命周期中一直存在,所以這沒有問題。但是在現實世界的場景中,我們需要像創建木箱結構體和形狀一樣去管理我們的空間,封裝在標準的Cocoa對象中,然後來管理Chipmunk對象的生命周期。圖11.1展示了掉落的木箱。

清單11.3 使用物理學來對掉落的木箱建模

  1 #import "ViewController.h" 
  2 #import 
  3 #import "chipmunk.h"
  4 
  5 @interface Crate : UIImageView
  6 
  7 @property (nonatomic, assign) cpBody *body;
  8 @property (nonatomic, assign) cpShape *shape;
  9 
 10 @end
 11 
 12 @implementation Crate
 13 
 14 #define MASS 100
 15 
 16 - (id)initWithFrame:(CGRect)frame
 17 {
 18     if ((self = [super initWithFrame:frame])) {
 19         //set image
 20         self.image = [UIImage imageNamed:@"Crate.png"];
 21         self.contentMode = UIViewContentModeScaleAspectFill;
 22         //create the body
 23         self.body = cpBodyNew(MASS, cpMomentForBox(MASS, frame.size.width, frame.size.height));
 24         //create the shape
 25         cpVect corners[] = {
 26             cpv(0, 0),
 27             cpv(0, frame.size.height),
 28             cpv(frame.size.width, frame.size.height),
 29             cpv(frame.size.width, 0),
 30         };
 31         self.shape = cpPolyShapeNew(self.body, 4, corners, cpv(-frame.size.width/2, -frame.size.height/2));
 32         //set shape friction & elasticity
 33         cpShapeSetFriction(self.shape, 0.5);
 34         cpShapeSetElasticity(self.shape, 0.8);
 35         //link the crate to the shape
 36         //so we can refer to crate from callback later on
 37         self.shape->data = (__bridge void *)self;
 38         //set the body position to match view
 39         cpBodySetPos(self.body, cpv(frame.origin.x + frame.size.width/2, 300 - frame.origin.y - frame.size.height/2));
 40     }
 41     return self;
 42 }
 43 
 44 - (void)dealloc
 45 {
 46     //release shape and body
 47     cpShapeFree(_shape);
 48     cpBodyFree(_body);
 49 }
 50 
 51 @end
 52 
 53 @interface ViewController ()
 54 
 55 @property (nonatomic, weak) IBOutlet UIView *containerView;
 56 @property (nonatomic, assign) cpSpace *space;
 57 @property (nonatomic, strong) CADisplayLink *timer;
 58 @property (nonatomic, assign) CFTimeInterval lastStep;
 59 
 60 @end
 61 
 62 @implementation ViewController
 63 
 64 #define GRAVITY 1000
 65 
 66 - (void)viewDidLoad
 67 {
 68     //invert view coordinate system to match physics
 69     self.containerView.layer.geometryFlipped = YES;
 70     //set up physics space
 71     self.space = cpSpaceNew();
 72     cpSpaceSetGravity(self.space, cpv(0, -GRAVITY));
 73     //add a crate
 74     Crate *crate = [[Crate alloc] initWithFrame:CGRectMake(100, 0, 100, 100)];
 75     [self.containerView addSubview:crate];
 76     cpSpaceAddBody(self.space, crate.body);
 77     cpSpaceAddShape(self.space, crate.shape);
 78     //start the timer
 79     self.lastStep = CACurrentMediaTime();
 80     self.timer = [CADisplayLink displayLinkWithTarget:self
 81                                              selector:@selector(step:)];
 82     [self.timer addToRunLoop:[NSRunLoop mainRunLoop]
 83                      forMode:NSDefaultRunLoopMode];
 84 }
 85 
 86 void updateShape(cpShape *shape, void *unused)
 87 {
 88     //get the crate object associated with the shape
 89     Crate *crate = (__bridge Crate *)shape->data;
 90     //update crate view position and angle to match physics shape
 91     cpBody *body = shape->body;
 92     crate.center = cpBodyGetPos(body);
 93     crate.transform = CGAffineTransformMakeRotation(cpBodyGetAngle(body));
 94 }
 95 
 96 - (void)step:(CADisplayLink *)timer
 97 {
 98     //calculate step duration
 99     CFTimeInterval thisStep = CACurrentMediaTime();
100     CFTimeInterval stepDuration = thisStep - self.lastStep;
101     self.lastStep = thisStep;
102     //update physics
103     cpSpaceStep(self.space, stepDuration);
104     //update all the shapes
105     cpSpaceEachShape(self.space, &updateShape, NULL);
106 }
107 
108 @end
View Code

圖11.1 一個木箱圖片,根據模擬的重力掉落

添加用戶交互

下一步就是在視圖周圍添加一道不可見的牆,這樣木箱就不會掉落出屏幕之外。或許你會用另一個矩形的cpPolyShape來實現,就和之前創建木箱那樣,但是我們需要檢測的是木箱何時離開視圖,而不是何時碰撞,所以我們需要一個空心而不是固體矩形。

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

同樣可以再添加一些木箱來做一些交互。最後再添加一個加速器,這樣可以通過傾斜手機來調整重力矢量(為了測試需要在一臺真實的設備上運行程式,因為模擬器不支持加速器事件,即使旋轉屏幕)。清單11.4展示了更新後的代碼,運行結果見圖11.2。

由於示例只支持橫屏模式,所以交換加速計矢量的x和y值。如果在豎屏下運行程式,請把他們換回來,不然重力方向就錯亂了。試一下就知道了,木箱會沿著橫向移動。

清單11.4 使用圍牆和多個木箱的更新後的代碼

 1 - (void)addCrateWithFrame:(CGRect)frame
 2 {
 3     Crate *crate = [[Crate alloc] initWithFrame:frame];
 4     [self.containerView addSubview:crate];
 5     cpSpaceAddBody(self.space, crate.body);
 6     cpSpaceAddShape(self.space, crate.shape);
 7 }
 8 
 9 - (void)addWallShapeWithStart:(cpVect)start end:(cpVect)end
10 {
11     cpShape *wall = cpSegmentShapeNew(self.space->staticBody, start, end, 1);
12     cpShapeSetCollisionType(wall, 2);
13     cpShapeSetFriction(wall, 0.5);
14     cpShapeSetElasticity(wall, 0.8);
15     cpSpaceAddStaticShape(self.space, wall);
16 }
17 
18 - (void)viewDidLoad
19 {
20     //invert view coordinate system to match physics
21     self.containerView.layer.geometryFlipped = YES;
22     //set up physics space
23     self.space = cpSpaceNew();
24     cpSpaceSetGravity(self.space, cpv(0, -GRAVITY));
25     //add wall around edge of view
26     [self addWallShapeWithStart:cpv(0, 0) end:cpv(300, 0)];
27     [self addWallShapeWithStart:cpv(300, 0) end:cpv(300, 300)];
28     [self addWallShapeWithStart:cpv(300, 300) end:cpv(0, 300)];
29     [self addWallShapeWithStart:cpv(0, 300) end:cpv(0, 0)];
30     //add a crates
31     [self addCrateWithFrame:CGRectMake(0, 0, 32, 32)];
32     [self addCrateWithFrame:CGRectMake(32, 0, 32, 32)];
33     [self addCrateWithFrame:CGRectMake(64, 0, 64, 64)];
34     [self addCrateWithFrame:CGRectMake(128, 0, 32, 32)];
35     [self addCrateWithFrame:CGRectMake(0, 32, 64, 64)];
36     //start the timer
37     self.lastStep = CACurrentMediaTime();
38     self.timer = [CADisplayLink displayLinkWithTarget:self
39                                              selector:@selector(step:)];
40     [self.timer addToRunLoop:[NSRunLoop mainRunLoop]
41                      forMode:NSDefaultRunLoopMode];
42     //update gravity using accelerometer
43     [UIAccelerometer sharedAccelerometer].delegate = self;
44     [UIAccelerometer sharedAccelerometer].updateInterval = 1/60.0;
45 }
46 
47 - (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration
48 {
49     //update gravity
50     cpSpaceSetGravity(self.space, cpv(acceleration.y * GRAVITY, -acceleration.x * GRAVITY));
51 }
View Code

圖11.1 真實引力場下的木箱交互

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

對於實現動畫的緩衝效果來說,計算每幀持續的時間是一個很好的解決方案,但是對模擬物理效果並不理想。通過一個可變的時間步長來實現有著兩個弊端:

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

  • 由於性能故常造成的丟幀或者像電話呼入的中斷都可能會造成不正確的結果。考慮一個像子彈那樣快速移動物體,每一幀的更新都需要移動子彈,檢測碰撞。如果兩幀之間的時間加長了,子彈就會在這一步移動更遠的距離,穿過圍牆或者是別的障礙,這樣就丟失了碰撞。

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

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

我們可以通過一個簡單的迴圈來實現。通過每次CADisplayLink的啟動來通知屏幕將要刷新,然後記錄下當前的CACurrentMediaTime()。我們需要在一個小增量中提前重覆物理模擬(這裡用120分之一秒)直到趕上顯示的時間。然後更新我們的視圖,在屏幕刷新的時候匹配當前物理結構體的顯示位置。

清單11.5展示了固定時間步長版本的代碼

清單11.5 固定時間步長的木箱模擬

 

避免死亡螺旋

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

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

如果幀刷新的時間延遲的話會變得很糟糕,我們的模擬需要執行更多的次數來同步真實的時間。這些額外的步驟就會繼續延遲幀的更新,等等。這就是所謂的死亡螺旋,因為最後的結果就是幀率變得越來越慢,直到最後應用程式卡死了。

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

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • [1]router-link [2]router-view [3]路由信息對象 [4]Router構造配置 [5]Router實例 [6]對組件註入 ...
  • [1]概述 [2]使用 [3]路由模式 [4]重定向和別名 [5]根路徑 [6]嵌套路由 [7]命名路由 [8]命名視圖 [9]動態路由 [10]查詢字元串 [11]滾動行為 [12]過渡動效 [13]路由元數據 [14]編程式導航 [15]導航鉤子 [16]懶載入 ...
  • 前面的話 作為程式員,每天與電腦打交道的時間可能比家人還多。所以,掌握一些電腦常識,處理棘手問題是必備技能 刪除文件 一些文件由於各種原因,無法直接刪除。例如,我在卸載git時,安裝目錄下有一個git_shell_ext64.dll文件無法刪除 解決辦法是修改其尾碼名,如git_shell_ext6 ...
  • Js 的射擊小游戲 玩法按下求 技能準備 點擊左鍵射擊,射擊到後面的球得分 代碼如下:直接粘到html文件中即可暢玩: <!DOCTYPE html> <html lang="en" xmlns="http://www.w3.org/1999/xhtml"> <head> <meta charset ...
  • Android Studio(2.3.3) 在給代碼混淆時,提示: 簡單說就是找不到"aapt_rules.txt"這個文件,解決方法也很簡單: 1、菜單欄選擇 build clean project 2、重新打包(build) 這裡只列出解決方法,詳細的說明請看以下這篇文章: http://blo ...
  • iOS9 發佈後,產生了一個使 App Thinning 無法正常運行的 bug。在iOS9.0.2 版本中,這個 bug 已經被修複,App Thinning 已經可以正常使用。當你從應用商店(App Store)下載應用時,請註意這點。iOS9 推出之後,大受歡迎。僅僅數周,已經有超過半數的 i ...
  • 之前沒有設置過打包的命名,每次打包都是預設的"app realease.apk",之後手動修改名字來顯示出它是一個新版本。 晚上學習瞭如何配置打包名稱,很簡單,修改build.gradle里的代碼就行。 詳細記錄如下: 1、打開app這個directory下的build.gradle 2、定義打包時 ...
  • 軟體繪圖 術語繪圖通常在Core Animation的上下文中指代軟體繪圖(意即:不由GPU協助的繪圖)。在iOS中,軟體繪圖通常是由Core Graphics框架完成來完成。但是,在一些必要的情況下,相比Core Animation和OpenGL,Core Graphics要慢了不少。 軟體繪圖不 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...