您现在的位置是:首页 > 文章详情

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

日期:2024-06-28点击:155

谈谈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无缝使用。最后,对本篇文章的任何讨论,都欢迎留言交流。

原文链接:https://my.oschina.net/u/2340880/blog/11238463
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章