本文由CocoaChina翻譯小組成員AGSpider(博客)翻譯自fancypixel的博客,
原文:Playing With UIDynamics in iOS 9
碰撞邊界(Collision Bounds)
UIDynamics 的第一個版本帶有碰撞系統(在 UICollisionBehavior 中)只支持矩形。這可以理解,因為UIViews都是矩形架構,但是圓形的卻不常見,更不用說優化一個自定義的貝塞爾曲線。在iOS 9中,UIDynamicItem協議裡加了一個新屬性:UIDynamicItemCollisionBoundsType,支持以下枚舉類型:
Rectangle
Ellipse
Path
這個屬性是只讀的,如果我們想修改它的話,需要提供我們的子類:
class Ellipse: UIView { override var collisionBoundsType: UIDynamicItemCollisionBoundsType { return .Ellipse } }
這是默認碰撞邊界的UIView。
而這是同一個帶.Ellipse屬性的UIView。
這涵蓋了圓形的視圖,如果我們突發奇想,畫一個更復雜,有連續的剛體,我們就可以使用.path枚舉類型啦,並且也要重寫該屬性:
var collisionBoundingPath: UIBezierPath { get }
這個路線可以是任意你能所想到的,只要它的樣子是凸面的(即任意在多邊形內兩個的點,兩點間的線段完全包含在多邊形內),並且是逆時針繞的。凸面這個條件也許限制的太死了,於是引入了UIDynamicItemGroup,它可以詳細描繪一組不同圖形的的組合圖形。這樣,只要該組合中的每個圖形都是凸面,即使得到的多邊形是凹面的也OK。
Field Behavior
Field Behavior是在整個場景中運用的一種新behavior。一個最普通的例子就是我們一直默默地使用著的UIGravityBehavior,即場景中的每個物體都會受到一個向下的重力。現在我們可以使用一組新的場力,就像徑向(距離場景中心越近,力越大)、噪聲(在場景內隨機產生的不同的力)等等。
Dynamic Item Behavior(動力元素行為)
UIDynamicItemBehavior 包含了幾個有趣的新特性:
var charge: CGFloat
var anchored: Bool
charge 代表能夠影響一個元素在電磁場上如何移動的電荷(是的,聽起來很瘋狂),而anchored本質上是將圖形變成了碰撞中的一個靜態物體,但沒有響應事件(如果有什麼東西撞上了它,它會絲毫不動),所以可以完美地用來表示地板或牆壁。
Attachment Behavior(吸附行為)
UIAttachmentBehavior改進後,現在像個具有新方法和屬性的偵探,如同frictionTorque和attachmentRange一樣。現在吸附行為變得更加靈活,我們可以指定相對滑動的動作、固定吸附、繩索鏈接和我最喜歡的:針型吸附。想像一下兩個釘在一起的物體你就明白了。這些基本涵蓋了UIDynamics的新特性,現在,是時候丟下這個更新日志,並開始搭建一些很二的東西了。
讓我們玩球吧
我上周花了很多時間在球王(Ball King)上。這一個很棒的消磨時間的東西,這游戲的的理念很簡單,但是表現很出色。並且,它采用了獲得蘋果設計獎的Crossy Roadde 相同的理念:它不會以任何方式影響到玩家,比如游戲內的榮譽。
我非常喜歡它的一點是球的物理模型,以及當球打到籃板上時籃板的反應。看起來用它來測試上面提到的UIDynamics新特性應該會非常不錯。讓我們來看看如何一步步地打造屬於自己的簡單的版本吧:BallSwift
籃框
籃球架可以用一個UIView作為籃板,幾個UIView作為籃框的左右兩邊,最前面的view作為籃框本身(不帶物理剛體)。使用我們之前定義的類Ellipse,我們就可以創造我們的游戲場景的視覺表現:
/* Build the hoop, setup the world appearance */ func buildViews() { board = UIView(frame: CGRect(x: hoopPosition.x, y: hoopPosition.y, width: 100, height: 100)) board.backgroundColor = .whiteColor() board.layer.borderColor = UIColor(red: 0.98, green: 0.98, blue: 0.98, alpha: 1).CGColor board.layer.borderWidth = 2 board.addSubview({ let v = UIView(frame: CGRect(x: 30, y: 43, width: 40, height: 40)) v.backgroundColor = .clearColor() v.layer.borderColor = UIColor(red: 0.4, green: 0.4, blue: 0.4, alpha: 1).CGColor v.layer.borderWidth = 5 return v }()) leftHoop = Ellipse(frame: CGRect(x: hoopPosition.x + 20, y: hoopPosition.y + 80, width: 10, height: 6)) leftHoop.backgroundColor = .clearColor() leftHoop.layer.cornerRadius = 3 rightHoop = Ellipse(frame: CGRect(x: hoopPosition.x + 70, y: hoopPosition.y + 80, width: 10, height: 6)) rightHoop.backgroundColor = .clearColor() rightHoop.layer.cornerRadius = 3 hoop = UIView(frame: CGRect(x: hoopPosition.x + 20, y: hoopPosition.y + 80, width: 60, height: 6)) hoop.backgroundColor = UIColor(red: 177.0/255.0, green: 25.0/255.0, blue: 25.0/255.0, alpha: 1) hoop.layer.cornerRadius = 3 [board, leftHoop, rightHoop, floor, ball, hoop].map({self.view.addSubview($0)}) }
這裡其實沒有什麼新東西, 籃框以編程形式被創建在常量CGPoint hoopPosition上。但是視圖的順序很重要,因為我們想讓籃框要高於籃球(的拋投點)。
螺母和螺栓(Nuts and bolts)
籃框的最重要的部分是左右臂,它們需要一個物理圓形身體(使得與球的碰撞顯得自然),需要用螺栓固定在板和前框。這兩個將成為基本的UIDynamicItems,並不會直接碰撞參與碰撞。新推出的針型吸附就是為此而生的,它可以把一切都完美地結合在一起,因為我們可以在這個比較粗糙的圖畫上看到:
在給定的確定空間點內,pin一次僅可連接幾個視圖:
let bolts = [ CGPoint(x: hoopPosition.x + 25, y: hoopPosition.y + 85), // leftHoop -> Board CGPoint(x: hoopPosition.x + 75, y: hoopPosition.y + 85), // rightHoop -> Board CGPoint(x: hoopPosition.x + 25, y: hoopPosition.y + 85), // hoop -> Board (L) CGPoint(x: hoopPosition.x + 75, y: hoopPosition.y + 85)] // hoop -> Board (R) // Build the board zip([leftHoop, rightHoop, hoop, hoop], offsets).map({ (item, offset) in animator?.addBehavior(UIAttachmentBehavior.pinAttachmentWithItem(item, attachedToItem: board, attachmentAnchor: bolts)) })
如果你不准備繼續看看swfit版裡的奇妙的功能,那你很可能不熟悉zip和map。這一開始看起來似乎有些刻意而為,但其實很簡單:每個視圖都用一個偏移點釘住吸附,然後我們得到一系列的元組,稍後將會在映射函數中使用。顧名思義,它創建了所給定的物體的數組中的每個元素間的映射。這使得籃框的左右邊都用螺栓固定在板和前框,如下:
左臂用螺栓固定在籃板的左側
右臂用螺栓固定在籃板右側
籃框用螺栓固定在籃板的左側
下一個步驟要求我們將籃板懸掛上,別將它固定死,這樣球一個碰撞就可以使得它轉動,就像在Ball King這個游戲中一樣:
// Set the density of the hoop, and fix its angle // Hang the hoop animator?.addBehavior({ let attachment = UIAttachmentBehavior(item: board, attachedToAnchor: CGPoint(x: hoopPosition.x, y: hoopPosition.y)) attachment.length = 2 attachment.damping = 5 return attachment }()) animator?.addBehavior({ let behavior = UIDynamicItemBehavior(items: [leftHoop, rightHoop]) behavior.density = 10 behavior.allowsRotation = false return behavior }()) // Block the board rotation animator?.addBehavior({ let behavior = UIDynamicItemBehavior(items: [board]) behavior.allowsRotation = false return behavior }())
籃框已經做好了,下面該到籃球了,用一個圓形的自定義UIImageView子類視圖,如同Ellipse類:
然後,我們可以將球作為一個普通的UIImageView實例化:
let ball: Ball = { let ball = Ball(frame: CGRect(x: 0, y: 0, width: 28, height: 28)) ball.image = UIImage(named: "ball") return ball }()
最後我們設置他的 物理屬性:
// Set the elasticity and density of the ball animator?.addBehavior({ let behavior = UIDynamicItemBehavior(items: [ball]) behavior.elasticity = 1 behavior.density = 3 behavior.action = { if !CGRectIntersectsRect(self.ball.frame, self.view.frame) { self.setupBehaviors() self.ball.center = CGPoint(x: 40, y: self.view.frame.size.height - 100) } } return behavior }())
這段代碼裡我設置了彈性大小(碰撞後反彈的幅度)、密度(就把它看作重量吧),還有當球超出彈跳范圍時立即結束游戲的事件,即為重置游戲狀態(在主視圖中)。
Collisions and gravity(碰撞和重力)
我提到了UIDynamicItemBehavior的新屬性anchored,即禁用了對象的動態behavior,同時將其保留在在碰撞循環裡。聽起來用它來搭建一個堅固的地板會很不錯:
// Anchor the floor animator?.addBehavior({ let behavior = UIDynamicItemBehavior(items: [floor]) behavior.anchored = true return behavior }())
如果你忘了設置這個屬性,你就會抓耳撓腮。反正我是這樣的。
好吧,一切都設置好啦,現在只需要一些重力和一組碰撞事件了:
animator?.addBehavior(UICollisionBehavior(items: [leftHoop, rightHoop, floor, ball])) animator?.addBehavior(UIGravityBehavior(items: [ball]))
重力是應用在默認每秒一點作為的向下的力的場景behavior。碰撞behavior 作為相互碰撞的view的參數。游戲已經搭建好啦,現在我們可以在球上施加一個瞬發力,並用我們的手指劃過屏幕:
let push = UIPushBehavior(items: [ball], mode: .Instantaneous) push.angle = -1.35 push.magnitude = 1.56 animator?.addBehavior(push)
這下你該明白了吧,雖然場景邊緣真的畫的很low,但是搭建它真的很有趣(是的,雲朵和灌木叢都是一樣的勾畫,就像 超級馬裡奧 裡中的)。
老規矩,你可以在我們的 GitHub頁面 找到源代碼。
下次見,
安德烈 - @theandreamazz