玩转iOS 9的UIDynamics
UIDynamics 是 iOS 7 SDK 中颇受欢迎的新增特性,它本质上是一个支持 UIView
的物理引擎,借助该引擎,我们能够自定义 UI 控件的物理特性。此 API 易于理解,利用它可以轻松创建出色的动画或过渡效果。本文将深入探究 iOS 9 中 UIDynamics 的新特性。
碰撞边界(Collision Bounds)
在 UIDynamics 的首个版本中,其碰撞系统(在 UICollisionBehavior
里)仅支持矩形碰撞边界。这不难理解,因为 UIView
通常采用矩形架构,而圆形碰撞边界较为少见,更不用说自定义的贝塞尔曲线了。不过在 iOS 9 中,UIDynamicItem
协议新增了一个属性:UIDynamicItemCollisionBoundsType
,它支持以下枚举类型:
Rectangle
Ellipse
Path
该属性为只读属性,若要修改其值,需提供自定义子类,示例代码如下:
class Ellipse: UIView {
override var collisionBoundsType: UIDynamicItemCollisionBoundsType {
return .ellipse
}
}
上述代码展示了默认碰撞边界的 UIView
以及带有 .ellipse
属性的 UIView
。对于圆形视图,我们可以使用 .ellipse
类型;若要绘制更复杂的连续刚体图形,则可使用 .path
枚举类型,同时需要重写以下属性:
var collisionBoundingPath: UIBezierPath { get }
这里的路径可以是任意形状,但需满足凸面条件(即多边形内任意两点间的线段完全包含在多边形内),并且路径方向为逆时针。由于凸面条件限制较为严格,因此引入了 UIDynamicItemGroup
,它可以详细描绘一组不同图形的组合图形。只要组合中的每个图形都是凸面的,即便最终得到的多边形是凹面的也没问题。
场行为(Field Behavior)
场行为是在整个场景中应用的一种新行为。一个常见的例子是我们一直在使用的 UIGravityBehavior
,在该行为下,场景中的每个物体都会受到向下的重力作用。如今,我们可以使用一组新的场力,例如径向力(距离场景中心越近,力越大)、噪声力(在场景内随机产生不同的力)等。
动力元素行为(Dynamic Item Behavior)
UIDynamicItemBehavior
包含了几个有趣的新特性:
var charge: CGFloat
:代表能够影响元素在电磁场上移动的电荷。var anchored: Bool
:本质上是将图形变为碰撞中的静态物体,且不响应事件(当有物体撞上它时,它不会移动),可用于表示地板或墙壁。
吸附行为(Attachment Behavior)
UIAttachmentBehavior
经过改进后,具备了新的方法和属性,如 frictionTorque
和 attachmentRange
。现在,吸附行为变得更加灵活,我们可以指定相对滑动动作、固定吸附、绳索链接以及针型吸附。以针型吸附为例,可想象两个钉在一起的物体。
实战:打造简单篮球游戏(BallSwift)
我上周花费了不少时间玩《球王(Ball King)》这款游戏,它的理念简单却表现出色,并且采用了与获得苹果设计奖的《Crossy Road》相同的理念,不会以任何方式影响玩家,例如游戏内荣誉。我尤其喜欢游戏中球的物理模型以及球打到篮板上时篮板的反应,接下来我们将利用 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 = .white
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 = .clear
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 = .clear
leftHoop.layer.cornerRadius = 3
rightHoop = Ellipse(frame: CGRect(x: hoopPosition.x + 70, y: hoopPosition.y + 80, width: 10, height: 6))
rightHoop.backgroundColor = .clear
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].forEach { self.view.addSubview($0) }
}
在上述代码中,篮框以编程方式创建在常量 CGPoint hoopPosition
上。需要注意的是,视图的顺序很重要,因为我们希望篮框位于篮球抛投点的上方。
固定篮框部件
篮框的左右臂需要具备物理圆形身体,以实现与球的自然碰撞,并通过螺栓固定在篮板和前框上。这两个部件将作为基本的 UIDynamicItem
,但不直接参与碰撞。新推出的针型吸附功能正好适用于此,代码如下:
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)
]
zip([leftHoop, rightHoop, hoop, hoop], offsets).forEach { (item, offset) in
animator?.addBehavior(UIAttachmentBehavior.pinAttachment(with: item, attachedTo: board, attachmentAnchor: offset))
}
通过上述代码,实现了以下固定效果:
- 左臂用螺栓固定在篮板的左侧。
- 右臂用螺栓固定在篮板右侧。
- 篮框用螺栓固定在篮板的左侧。
接下来,我们需要将篮板悬挂起来,使其在球碰撞时能够转动,就像《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 !self.ball.frame.intersects(self.view.frame) {
self.setupBehaviors()
self.ball.center = CGPoint(x: 40, y: self.view.frame.size.height - 100)
}
}
return behavior
}()
在上述代码中,我们设置了球的弹性大小(碰撞后反弹的幅度)、密度(可看作重量),并添加了当球超出弹跳范围时重置游戏状态的事件。
添加碰撞和重力效果
使用 UIDynamicItemBehavior
的 anchored
属性,我们可以创建一个坚固的地板:
// 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]))
为了让球运动起来,我们可以在球上施加一个瞬发力,通过手指划过屏幕来实现:
let push = UIPushBehavior(items: [ball], mode: .instantaneous)
push.angle = -1.35
push.magnitude = 1.56
animator?.addBehavior(push)
至此,游戏搭建完成。尽管场景边缘的绘制可能不够精美,但搭建过程十分有趣。