写点什么

谈谈 iOS 中的原生物理引擎——UIDynamic 的应用

作者:珲少
  • 2024-06-28
    上海
  • 本文字数:9270 字

    阅读完需:约 30 分钟

谈谈 iOS 中的原生物理引擎——UIDynamic 的应用

UIDynamic 是 iOS 中 UIKit 框架提供的接口,其用来为 UI 元素增加符合物理世界运动规则的动画行为。简单来说,UIDynamic 提供的实际上是一个物理引擎,由于它是 iOS 原生系统支持的(iOS 7 以上),因此兼容性和易用性非常好,使用它开发者可以非常方便的创建出物理动画。本篇文章,我们将讨论 UIDynamic 的设计架构、使用方法以及做一些简单的物理动画示例,希望可以在应用开发中为你带来一些启发。

从一个碰撞动画示例开始

在开始具体完整的介绍 UIDynamic 相关功能和接口前,我们可以先通过一个示例来体验下 UIDynamic 强大的功能以及开发流程。


假如我们要实现这样一个动画效果:


模拟一个台球游戏,首先在窗口中显示一个矩形区域作为球桌,其中放置一个台球元素,给其一个初始的速度和方向来模拟发球动作,之后台球将按照预设的物理规律在桌面上进行碰撞运动。


先来看一下效果实例:



实现上面效果的核心代码如下:


    var animator: UIDynamicAnimator!    // 执行动画    func addCollision() {        animator = UIDynamicAnimator(referenceView: box)        // 添加碰撞行为        let collision = UICollisionBehavior()        collision.translatesReferenceBoundsIntoBoundary = true        collision.addItem(ball)        // 添加推动行为        let push =  UIPushBehavior(items: [ball], mode: .instantaneous)        push.addItem(ball)        push.pushDirection = .init(dx: 0.5, dy: 0.5)        push.magnitude = 5        // 定义元素行为        let item = UIDynamicItemBehavior(items: [ball])        item.resistance = 1        item.elasticity = 0.8        // 将定义的行为添加到引擎动画中        animator.addBehavior(push)        animator.addBehavior(collision)        animator.addBehavior(item)    }
复制代码


可以看到,实现此物理动画的主要流程包括 3 个要素:


动画元素 View


物理仿真器 Animator


物理行为 Behavior


上面示例代码中,添加了 3 种物理行为,UICollisionBehavior 用来定义碰撞行为,可以定义要产生碰撞的元素。UIPushBehavior 用来定义推动行为,可以给物理元素一个推力。UIDynamicItemBehavior 用来定义物理元素本身的性质,例如摩擦力,质量等。下面我们会逐一讨论这些要素。

关于动画元素的定义

定义可动画元素:UIDynamicItem

任何物理行为都需要作用在某一个具体的 UI 元素上,要支持物理引擎的元素需要实现 UIDynamicItem 协议,此协议的定义如下:


@MainActor public protocol UIDynamicItem : NSObjectProtocol {    // 物理上元素的中心    var center: CGPoint { get set }    // 物理上元素的bounds    var bounds: CGRect { get }    // transform    var transform: CGAffineTransform { get set }    // 边界类型    @available(iOS 9.0, *)    optional var collisionBoundsType: UIDynamicItemCollisionBoundsType { get }    // 边界Path    @available(iOS 9.0, *)    optional var collisionBoundingPath: UIBezierPath { get }}
复制代码


UIKit 框架中的 UIView 类默认实现了 UIDynamicItem 协议,因此所有 UIView 的子类都可以直接无缝使用 UIDynamic 提供的物理能力。通过子类对 collisionBoundsType 属性,可以对物理元素的边界进行细化的定制:


@available(iOS 9.0, *)public enum UIDynamicItemCollisionBoundsType : UInt, @unchecked Sendable {    // 矩形边界(bounds来控制)        case rectangle = 0    // 椭圆边界(有width和height来控制椭圆的两个轴)    case ellipse = 1     // 路径边界(自定义边界)    case path = 2}
复制代码

定义动画元素的属性:UIDynamicItemBehavior

UIDynamicItemBehavior 本身也是 Behavior 的那种,和其他物理行为不同的是,UIDynamicItemBehavior 侧重于定义动画元素本身的属性。UIDynamicItemBehavior 的定义会影响元素本身运动过程中的阻力、弹力、惯性等行为。解析如下:


@available(iOS 7.0, *)@MainActor open class UIDynamicItemBehavior : UIDynamicBehavior {    // 初始化方法,添加一组作用于的元素    public init(items: [any UIDynamicItem])    // 添加作用于的元素    open func addItem(_ item: any UIDynamicItem)    // 移除某个作用的元素    open func removeItem(_ item: any UIDynamicItem)    open var items: [any UIDynamicItem] { get }    // 弹性设置,0到1之间,值越大元素的弹性越大    open var elasticity: CGFloat    // 摩擦力设置,0表示无摩擦力,当两个物理元素接触滑动时,此值会有影响    open var friction: CGFloat // 0 being no friction between objects slide along each other    // 密度设置 默认为1,密度大的元素加速和减速都能加困难    open var density: CGFloat     // 阻尼设置,控制物体运动的阻力    open var resistance: CGFloat    // 旋转阻尼设置,控制物理旋转的阻力    open var angularResistance: CGFloat // 0: no angular velocity damping    // 电荷设置,决定电磁感应的程度    @available(iOS 9.0, *)    open var charge: CGFloat    // 设置当前元素是否作为锚定元素,锚定的元素会作用碰撞,但不会被碰撞影响,通常用来做碰撞的边界    @available(iOS 9.0, *)    open var isAnchored: Bool    // 是否允许元素旋转    open var allowsRotation: Bool 
// 为一个元素添加线性的加速度 open func addLinearVelocity(_ velocity: CGPoint, for item: any UIDynamicItem) open func linearVelocity(for item: any UIDynamicItem) -> CGPoint // 为一个元素添加一个角加速度 open func addAngularVelocity(_ velocity: CGFloat, for item: any UIDynamicItem) open func angularVelocity(for item: any UIDynamicItem) -> CGFloat}
复制代码

物理仿真器

物理仿真器由 UIDynamicAnimator 类来描述。UIDynamicAnimator 的主要作用是将动画元素和物力行为进行结合,驱动出仿真的物理动画。此类定义如下:


@available(iOS 7.0, *)open class UIDynamicAnimator : NSObject {    // 初始化,并关联一个视图,关联的视图将作为参照坐标系    public init(referenceView view: UIView)    // 添加一个物理行为    open func addBehavior(_ behavior: UIDynamicBehavior)    // 移除物理行为    open func removeBehavior(_ behavior: UIDynamicBehavior)    // 移除所有物理行为    open func removeAllBehaviors()    // 参照视图    open var referenceView: UIView? { get }    // 所有行为对象    open var behaviors: [UIDynamicBehavior] { get }    // 左右作用的物理元素    open func items(in rect: CGRect) -> [any UIDynamicItem]    // 更新一个元素的状态(当此物理引擎系统外部造成的改变时调用此方法更新)    open func updateItem(usingCurrentState item: any UIDynamicItem)    // 是否正在执行    open var isRunning: Bool { get }    // 目前执行的时长    open var elapsedTime: TimeInterval { get }    // 设置代理    weak open var delegate: (any UIDynamicAnimatorDelegate)?}
复制代码


UIDynamicAnimatorDelegate 协议可以监听物理动画的状态:


@MainActor public protocol UIDynamicAnimatorDelegate : NSObjectProtocol {    // 动画即将恢复    @available(iOS 7.0, *)    optional func dynamicAnimatorWillResume(_ animator: UIDynamicAnimator)    // 动画暂停    @available(iOS 7.0, *)    optional func dynamicAnimatorDidPause(_ animator: UIDynamicAnimator)}
复制代码

物理行为定义

物理行为可以实现复杂的 2D 物理动画,我们可以单独使用这些物理行为,也可以将物理行为进行组合使用。

UIDynamicBehavior 基类

UIDynamicBehavior 是所有物理行为的基类,其中定义了一些公共的方法和属性:


@available(iOS 7.0, *)@MainActor open class UIDynamicBehavior : NSObject {    // 添加子行为    open func addChildBehavior(_ behavior: UIDynamicBehavior)    // 移除子行为    open func removeChildBehavior(_ behavior: UIDynamicBehavior)    open var childBehaviors: [UIDynamicBehavior] { get }    // 物理仿真器在执行动画时会调用此方法    open var action: (() -> Void)?    // 添加和移除仿真器    open func willMove(to dynamicAnimator: UIDynamicAnimator?) // nil when being removed from an animator    // 关联的仿真器    open var dynamicAnimator: UIDynamicAnimator? { get }}
复制代码

依附行为:UIAttachmentBehavior

简单理解,依附行为就像将元素与锚点间连接上了一条线,效果如下:



示例代码如下:


    func addAttachment () {        animator = UIDynamicAnimator(referenceView: box)                // 添加重力行为        let gravity = UIGravityBehavior(items: [ball])        gravity.gravityDirection = .init(dx: 0, dy: 1)                // 添加附着力行为        let attachment = UIAttachmentBehavior(item: ball, attachedToAnchor: CGPoint(x: box.frame.width / 2 + 35 , y: box.frame.height / 2))        attachment.length = 50                animator.addBehavior(gravity)        animator.addBehavior(attachment)
}
复制代码


UIAttachmentBehavior 提供了多种初始化的方式,可以使用一个点作为锚点,也可以将另一个视图作为锚点:


    // 以一个点作为锚点进行依附    public convenience init(item: any UIDynamicItem, attachedToAnchor point: CGPoint)    // 设置物理元素的中心偏移,可以理解为将绳子连接在物理元素的哪个位置    public init(item: any UIDynamicItem, offsetFromCenter offset: UIOffset, attachedToAnchor point: CGPoint)
// 以另一个物理元素作为锚点元素 public convenience init(item item1: any UIDynamicItem, attachedTo item2: any UIDynamicItem) // 设置两个物理元素的中心偏移,即绳子的两端位置 public init(item item1: any UIDynamicItem, offsetFromCenter offset1: UIOffset, attachedTo item2: any UIDynamicItem, offsetFromCenter offset2: UIOffset)
复制代码


UIAttachmentBehavior 还有很多可配置的属性,如下:


@available(iOS 7.0, *)@MainActor open class UIAttachmentBehavior : UIDynamicBehavior {    // 物理元素       open var items: [any UIDynamicItem] { get }    // 依附行为的类型    open var attachedBehaviorType: UIAttachmentBehavior.AttachmentType { get }    // 锚点    open var anchorPoint: CGPoint    // 依附距离(绳子的长度)    open var length: CGFloat    // 依附行为的阻尼量    open var damping: CGFloat // 1: critical damping    // 依附行为的震荡频率(绳子的弹性)    open var frequency: CGFloat // in Hertz    // 旋转阻力    @available(iOS 9.0, *)    open var frictionTorque: CGFloat // default is 0.0    // 运动范围    @available(iOS 9.0, *)    open var attachmentRange: UIFloatRange // default is UIFloatRangeInfinite}
复制代码


AttachmentType 枚举定义了是以点为锚点还是元素为锚点进行依附:


    @available(iOS 7.0, *)    public enum AttachmentType : Int, @unchecked Sendable {        case items = 0        case anchor = 1    }
复制代码

碰撞行为:UICollisionBehavior


碰撞行为比较好理解,在本文也是以一个台球碰撞示例开始。UICollisionBehavior 解析如下:


@available(iOS 7.0, *)@MainActor open class UICollisionBehavior : UIDynamicBehavior {    // 初始化    public init(items: [any UIDynamicItem])    // 元素添加与移除    open func addItem(_ item: any UIDynamicItem)    open func removeItem(_ item: any UIDynamicItem)    open var items: [any UIDynamicItem] { get }    // 碰撞模式    open var collisionMode: UICollisionBehavior.Mode
// 设置是否激活参照物的边界(创建仿真器时会指定一个参照元素,此属性控制是否将参照元素的边界作为碰撞边界进行激活) open var translatesReferenceBoundsIntoBoundary: Bool // 设置参照元素边界的偏移 open func setTranslatesReferenceBoundsIntoBoundary(with insets: UIEdgeInsets)
// 将指令的边界添加到碰撞行为中 open func addBoundary(withIdentifier identifier: any NSCopying, for bezierPath: UIBezierPath) open func addBoundary(withIdentifier identifier: any NSCopying, from p1: CGPoint, to p2: CGPoint) open func boundary(withIdentifier identifier: any NSCopying) -> UIBezierPath? // 移除指定的碰撞边界 open func removeBoundary(withIdentifier identifier: any NSCopying) open var boundaryIdentifiers: [any NSCopying]? { get } open func removeAllBoundaries() // 代理 weak open var collisionDelegate: (any UICollisionBehaviorDelegate)?}
复制代码


collisionMode 可以指定碰撞的模式:


    @available(iOS 7.0, *)    public struct Mode : OptionSet, @unchecked Sendable {
public init(rawValue: UInt) // 仅仅物理元素间进行碰撞 public static var items: UICollisionBehavior.Mode { get } // 仅仅和边界进行碰撞 public static var boundaries: UICollisionBehavior.Mode { get } // 物理元素间和边界都进行碰撞 public static var everything: UICollisionBehavior.Mode { get } }
复制代码


UICollisionBehaviorDelegate 代理中定义了碰撞过程的回调:


@MainActor public protocol UICollisionBehaviorDelegate : NSObjectProtocol {    // 两个物理元素间开始产生碰撞时调用    @available(iOS 7.0, *)    optional func collisionBehavior(_ behavior: UICollisionBehavior, beganContactFor item1: any UIDynamicItem, with item2: any UIDynamicItem, at p: CGPoint)    // 两个物理元素间碰撞结束时调用    @available(iOS 7.0, *)    optional func collisionBehavior(_ behavior: UICollisionBehavior, endedContactFor item1: any UIDynamicItem, with item2: any UIDynamicItem)    // 物理元素与边界开始产生碰撞时调用    @available(iOS 7.0, *)    optional func collisionBehavior(_ behavior: UICollisionBehavior, beganContactFor item: any UIDynamicItem, withBoundaryIdentifier identifier: (any NSCopying)?, at p: CGPoint)    // 物理元素与边界碰撞结束时调用    @available(iOS 7.0, *)    optional func collisionBehavior(_ behavior: UICollisionBehavior, endedContactFor item: any UIDynamicItem, withBoundaryIdentifier identifier: (any NSCopying)?)}
复制代码

场行为:UIFieldBehavior

场也是物理学中物理运动重要模型,生活中电场、磁场、重力场等场无处不在,iOS 9 之后引入了 UIFieldBehavior 来仿真场行为。场行为本身运动规律复杂,UIFieldBehavior 中提供了一些静态方法能方便的创建不同的场模型:


// 创建一个拉力场行为(进入场后减速物理的运动)open class func dragField() -> Self// 创建一个弹力场行为(弹簧震荡的效果)open class func springField() -> Self// 加速度场 (场中的物理元素会被叠加上指定方向的加速度)open class func velocityField(direction: CGVector) -> Self// 创建电场行为 (与物体所携带的电荷量有关)open class func electricField() -> Self// 创建磁场行为 (与物体所携带的电荷量有关)open class func magneticField() -> Self// 在指定的点创建重力场行为 (有质量的物体会被吸引,设置负值则排斥)open class func radialGravityField(position: CGPoint) -> Self// 创建一个方向上的引力场行为 (与radialGravityField不同的是此场的力会均匀作用在指定方向)open class func linearGravityField(direction: CGVector) -> Self// 创建涡流场行为(场中附加的力是沿速度方向的切线)open class func vortexField() -> Self// 噪声场,此场通常与其他场结合使用,用来在纯粹的物理场中增加一些噪声,更好的模拟现实open class func noiseField(smoothness: CGFloat, animationSpeed speed: CGFloat) -> Self// 湍流场,也用于增加噪声,此场中的噪声与速度成正比open class func turbulenceField(smoothness: CGFloat, animationSpeed speed: CGFloat) -> Self// 完全自定义场行为open class func field(evaluationBlock block: @escaping (UIFieldBehavior, CGPoint, CGVector, CGFloat, CGFloat, TimeInterval) -> CGVector) -> Self
复制代码


通过上面的静态方法可以直接创建出复杂的场效果,并且场可以叠加进行使用。下面分别演示了拉力场,弹力场的行为运动效果,这些行为本身都是符合具体的物理公式,可以通过参数调整来仿真所需要的场景。


拉力场示例:


    // 拉力场    func addDragField() {        animator = UIDynamicAnimator(referenceView: box)                // 添加重力行为        let gravity = UIGravityBehavior(items: [ball])        gravity.gravityDirection = .init(dx: 0, dy: 1)                // 添加拉力场行为        let field = UIFieldBehavior.dragField()        field.addItem(ball)        field.position = CGPoint(x: ball.frame.origin.x, y: ball.frame.origin.y + 100)        // 力度        field.strength = 3        // 场影响的范围        field.region = .init(size: .init(width: 100, height: 100))        animator.addBehavior(field)        animator.addBehavior(gravity)    }
复制代码



可以看到,当物理元素位于拉力场范围内时,物体下落速度非常慢,脱离场影响范围后,在重力作用下,快速下落。


弹力场示例:


    // 弹力场    func addSpringField() {        animator = UIDynamicAnimator(referenceView: box)                // 添加重力行为        let gravity = UIGravityBehavior(items: [ball])        gravity.gravityDirection = .init(dx: 0, dy: 1)                // 添加弹力场行为        let field = UIFieldBehavior.springField()        field.addItem(ball)        field.position = CGPoint(x: ball.frame.origin.x, y: ball.frame.origin.y - 100)                animator.addBehavior(field)        animator.addBehavior(gravity)    }
复制代码



UIFieldBehavior 创建的场可以通过设置参数来控制场中的力、方向等属性,如下:


@available(iOS 9.0, *)@MainActor open class UIFieldBehavior : UIDynamicBehavior {    // 场的位置    open var position: CGPoint    // 场的作用范围    open var region: UIRegion    // 场的力大小,默认为1    open var strength: CGFloat    // 定义随着离场中心距离的增加,场强度减弱的速度,默认0 表示场是均匀的,不会减弱    open var falloff: CGFloat    // 设置场中心的半径最小值,小于此值时,场作用不会进行衰减    open var minimumRadius: CGFloat    // 指定速度场和线性方向的重力场的速度方向    open var direction: CGVector     // 设置噪声场合湍流场的噪声强度 0为最强 1为最弱    open var smoothness: CGFloat    // 噪声场合湍流场的动画速度    open var animationSpeed: CGFloat}
复制代码

重力行为:UIGravityBehavior

UIGravityBehavior 与 UIFieldBehavior 中的重力场功能有重复,这是由于 UIGravityBehavior 是 iOS7 之后就已经存在的行为,UIFieldBehavior 是 iOS9 后为了增强对物理场模型的支持新增的,对应也覆盖了重力场的场景。UIGravityBehavior 比较简单,解析如下:


@available(iOS 7.0, *)@MainActor open class UIGravityBehavior : UIDynamicBehavior {    // 重力方向    open var gravityDirection: CGVector    // 设置角度    open var angle: CGFloat    // 重力大小    open var magnitude: CGFloat    // 设置角度和重力大小    open func setAngle(_ angle: CGFloat, magnitude: CGFloat)}
复制代码

推动行为:UIPushBehavior

UIPushBehavior 用来仿真推动行为,其可以为物理元素提供一个推力。UIPushBehavior 的模式有两种,分别可以添加瞬时推力与持续推力。


extension UIPushBehavior {    // 模式    @available(iOS 7.0, *)    public enum Mode : Int, @unchecked Sendable {        // 持续的力        case continuous = 0        // 瞬时力        case instantaneous = 1    }}
@available(iOS 7.0, *)@MainActor open class UIPushBehavior : UIDynamicBehavior { // 初始化方法 public init(items: [any UIDynamicItem], mode: UIPushBehavior.Mode) // 添加和移除物理元素 open func addItem(_ item: any UIDynamicItem) open func removeItem(_ item: any UIDynamicItem) open var items: [any UIDynamicItem] { get }
// 推力作用于的点的偏移 open func targetOffsetFromCenter(for item: any UIDynamicItem) -> UIOffset open func setTargetOffsetFromCenter(_ o: UIOffset, for item: any UIDynamicItem)
// 模式 open var mode: UIPushBehavior.Mode { get } // 推力是否激活中 open var active: Bool // 角度 open var angle: CGFloat open func setAngle(_ angle: CGFloat, magnitude: CGFloat) // 设置推力大小 open var magnitude: CGFloat // 设置推力的方向 open var pushDirection: CGVector}
复制代码

捕获行为:UISnapBehavior

捕获行为与弹簧行为类似,其描述运动随时间而衰减,逐渐将物理元素固定到某个点。


@available(iOS 7.0, *)@MainActor open class UISnapBehavior : UIDynamicBehavior {    // 初始化方法 设置最终物理元素固定在的位置    public init(item: any UIDynamicItem, snapTo point: CGPoint)    @available(iOS 9.0, *)    open var snapPoint: CGPoint    // 设置震荡幅度 0-1之间    open var damping: CGFloat }
复制代码

写在最后

物理引擎是许多游戏开发中的必备,使用物理引擎也可以为应用增加许多有趣的交互。另外,UIKit 提供的物理引擎有着很好的性能,且可以和 UIView 无缝使用。最后,对本篇文章的任何讨论,都欢迎留言交流。

发布于: 刚刚阅读数: 6
用户头像

珲少

关注

还未添加个人签名 2022-07-26 加入

还未添加个人简介

评论

发布
暂无评论
谈谈iOS中的原生物理引擎——UIDynamic的应用_珲少_InfoQ写作社区