UIDynamicBehavior的简单使用:接球小游戏
一、概念扩充:
1、在开发中,我们可以使用UIKit中提供的仿真行为,实现与现实生活中类似的物理仿真动画,UIKit动力学最大的特点是将现实世界动力驱动的动画引入了UIKit,比如重力,铰链连接,碰撞,悬挂等效果。我们使用仿真引擎(UIDynamicAnimator)或者叫仿真者来管理和控制各种仿真行为,同时各种仿真行为可以叠加使用,可以实现力的合成。
2、只有遵守了UIDynamicItem协议的对象才可以参与到UI动力学仿真中,从iOS 7开始,UIView和UICollectionViewLayoutAttributes 类默认实现了该协议。
二、主要涉及到系统提供的类(API):
1、UIDynamicBehavior:仿真行为。是基本动力学行为的父类,可以理解为抽象类,用以生成子类。
2、UIDynamicAnimator :动力学仿真者。程序运行过程中要保证对象一直存在,用来控制所有仿真行为。
3、基本的动力学行为类:UIGravityBehavior(重力)、UICollisionBehavior(碰撞)、UIAttachmentBehavior(链接)、UISnapBehavior(吸附)、UIPushBehavior(推动)以及UIDynamicItemBehavior(元素),本例中不讨论链接、吸附、推动。
4、UICollisionBehavior的代理协议方法:-collisionBehavior: beganContactForItem: withBoundaryIdentifier: atPoint:
5、UIResponder的触摸响应事件: -touchesMoved: withEvent:
三、案例开始,实现效果:
1、一些宏定义
//小球掉落的随机X坐标 #define randomX arc4random_uniform([UIScreen mainScreen].bounds.size.width-ballWH) //随机颜色 #define randomColor [UIColor colorWithRed:arc4random_uniform(256)/255.0 green:arc4random_uniform(256)/255.0 blue:arc4random_uniform(256)/255.0 alpha:1.0] //球大小 #define ballWH 20 //计时器间隔 #define timerCount 1 //球拍宽度 #define boardW 100 //到多少球开闸(小球阈值) #define maxCount 50 //球的弹性系数 #define ballE 0.8
2、私有类扩展,全局变量声明,并遵循了碰撞检测的代理协议
@interface ZQpingPangView ()<UICollisionBehaviorDelegate> //仿真者,用来管理仿真行为 @property(nonatomic,strong) UIDynamicAnimator * animator; //仿真重力行为 @property(nonatomic,strong) UIGravityBehavior * gra ; //仿真碰撞行为 @property(nonatomic,strong) UICollisionBehavior * col; //仿真元素行为 @property(nonatomic,strong)UIDynamicItemBehavior * dyib; //小球阈值 @property(nonatomic,weak) UILabel * numLabel; //得分栏 @property(nonatomic,weak) UILabel * scoreLabel; //得分 @property(nonatomic,assign) NSInteger score; //球拍 @property(nonatomic,weak) UIImageView * board; //用于球拍边缘检测的路径 @property(nonatomic,strong) UIBezierPath * path; //记录小球数目的变量 @property(nonatomic,assign) NSInteger ballCount; @end
3、在自定义View的initWithFrame中实例化仿真者以及各个仿真行为,并且将他们赋值为全局属性
1 -(instancetype)initWithFrame:(CGRect)frame 2 { 3 self= [super initWithFrame:frame]; 4 //给视图添加大的背景 5 self.backgroundColor=[UIColor colorWithPatternImage:[UIImage imageNamed:@"BackgroundTile"]]; 6 7 //初始化小球数量为0 8 self.ballCount=0; 9 10 //创建仿真者对象 11 UIDynamicAnimator * animator = [[UIDynamicAnimator alloc]initWithReferenceView:self]; 12 self.animator =animator; 13 14 //创建重力行为,并将其添加到仿真者对象中 15 UIGravityBehavior * gra = [[UIGravityBehavior alloc]init]; 16 self.gra=gra; 17 [self.animator addBehavior:gra]; 18 19 //创建碰撞行为,并将其添加到仿真者对象中 20 UICollisionBehavior * col = [[UICollisionBehavior alloc]init]; 21 //将屏幕边缘碰撞检测开启(屏幕边缘是在仿真者对象中规定的) 22 col.translatesReferenceBoundsIntoBoundary=YES; 23 col.collisionDelegate=self; 24 self.col = col; 25 [self.animator addBehavior:col]; 26 27 //创建仿真元素行为(弹性和摩擦),这里摩擦力效果不明显 28 UIDynamicItemBehavior * dyib=[[UIDynamicItemBehavior alloc]init]; 29 //弹性 30 dyib.elasticity=ballE; 31 //摩擦力 32 dyib.friction=1; 33 self.dyib=dyib; 34 [self.animator addBehavior:dyib]; 35 36 //构图(生成中间的障碍物) 37 [self makeMainStructure]; 38 39 // 创建计时器、让小球不断生成 40 NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:timerCount target:self selector:@selector(makeBalls) userInfo:nil repeats:YES]; 41 [timer fire]; 42 43 //生成球 44 [self makeBalls]; 45 46 //生成屏幕上方的两个label,显示得分和剩余小球 47 [self makeTwoLabel]; 48 49 //生成接小球的拍子 50 [self makeBoard]; 51 52 return self; 53 }
4、生成球的方法,生成小球以后就将小球添加到各个仿真行为中去
-(void)makeBalls { CGRect frame = CGRectMake(randomX, 0, ballWH, ballWH); UIImageView * ball = [[UIImageView alloc]initWithFrame:frame]; ball.backgroundColor = randomColor; ball.layer.cornerRadius= ballWH*0.5; ball.layer.masksToBounds=YES; //计数器累加 self.ballCount +=1; [self addSubview:ball]; //添加到各个行为中 [self.gra addItem:ball]; [self.col addItem:ball]; [self.dyib addItem:ball]; //小球达到阈值,就“倾倒”出屏幕 if (self.ballCount==maxCount) { self.col.translatesReferenceBoundsIntoBoundary=NO; //计数器归零 self.ballCount =0; } else{ self.col.translatesReferenceBoundsIntoBoundary=YES; } NSInteger num =maxCount - self.ballCount % maxCount; NSString * numStr = [NSString stringWithFormat:@"即将开闸:%ld",num]; self.numLabel.text=numStr; }
5、生成屏幕中的构图:能得分的箱子,障碍物,障碍点。这些障碍物的边缘都要加入到碰撞检测当中去,而不是自身加入到碰撞检测中(这样自己也会被撞飞的)
#pragma mark - 生成构图 -(void)makeMainStructure { //生成中间的障碍物 [self makeBarrier]; //生成得分障碍物 [self makeScoreBarrier]; } #pragma mark - 生成得分障碍物 -(void)makeScoreBarrier { //得分的箱子“box1” UIImageView * box1 = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"RedeemCode"]]; box1.bounds=CGRectMake(0, 0, 25, 25); box1.center=CGPointMake(145, 250); [self addSubview:box1]; //得分的箱子“box2” UIImageView * box2 = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"RedeemCode"]]; box2.bounds=CGRectMake(0, 0, 20, 20); box2.center=CGPointMake(330, 350); [self addSubview:box2]; //通过箱子的frame,创建路径对象 UIBezierPath * path1 = [UIBezierPath bezierPathWithRect:box1.frame]; UIBezierPath * path2 = [UIBezierPath bezierPathWithRect:box2.frame]; //通过路径 ,添加边缘碰撞检测,而不是把箱子添加进检测 [self.col addBoundaryWithIdentifier:@"score1" forPath:path1]; [self.col addBoundaryWithIdentifier:@"score2" forPath:path2]; } #pragma mark - 生成中间不得分的障碍物 -(void)makeBarrier { // 创建6个箱子当做障碍物 UIImageView * box1 = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"Box1"]]; box1.bounds=CGRectMake(0, 0, 25, 25); box1.center=CGPointMake(150, 200); [self addSubview:box1]; UIImageView * box2 = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"Box1"]]; box2.bounds=CGRectMake(0, 0, 30, 30); box2.center=CGPointMake(300, 300); [self addSubview:box2]; UIImageView * box3 = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"AttachmentPoint_Mask"]]; [box3 sizeToFit]; box3.center=CGPointMake(70, 300); [self addSubview:box3]; UIImageView * box4 = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"AttachmentPoint_Mask"]]; [box4 sizeToFit]; box4.center=CGPointMake(280, 160); [self addSubview:box4]; UIImageView * box5 = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"AttachmentPoint_Mask"]]; [box5 sizeToFit]; box5.center=CGPointMake(100, 160); [self addSubview:box5]; UIImageView * box6 = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"AttachmentPoint_Mask"]]; [box6 sizeToFit]; box6.center=CGPointMake(390, 250); [self addSubview:box6]; //创建6个路径 UIBezierPath * path1 = [UIBezierPath bezierPathWithRect:box1.frame]; UIBezierPath * path2 = [UIBezierPath bezierPathWithRect:box2.frame]; UIBezierPath * path3 = [UIBezierPath bezierPathWithRect:box3.frame]; UIBezierPath * path4 = [UIBezierPath bezierPathWithRect:box4.frame]; UIBezierPath * path5 = [UIBezierPath bezierPathWithRect:box5.frame]; UIBezierPath * path6 = [UIBezierPath bezierPathWithRect:box6.frame]; //添加6个路径到碰撞检测 [self.col addBoundaryWithIdentifier:@"box1" forPath:path1]; [self.col addBoundaryWithIdentifier:@"box1" forPath:path2]; [self.col addBoundaryWithIdentifier:@"box3" forPath:path3]; [self.col addBoundaryWithIdentifier:@"box4" forPath:path4]; [self.col addBoundaryWithIdentifier:@"box5" forPath:path5]; [self.col addBoundaryWithIdentifier:@"box6" forPath:path6]; }
6、生成屏幕上方两个显示数据的label
-(void)makeTwoLabel { //创建一个记录球数的label UILabel * numLabel = [[UILabel alloc]initWithFrame:CGRectMake(280, 100, 120, 30)]; numLabel.backgroundColor=[UIColor greenColor]; [self addSubview:numLabel]; self.numLabel=numLabel; NSString * str = [NSString stringWithFormat:@"即将开闸:%d",maxCount]; self.numLabel.text=str; //创建一个记录分数的label self.score=0; UILabel * scoreLabel = [[UILabel alloc]initWithFrame:CGRectMake(20, 100, 100, 30)]; scoreLabel.backgroundColor=[UIColor yellowColor]; [self addSubview:scoreLabel]; self.scoreLabel=scoreLabel; self.scoreLabel.text=@"得分:0"; }
7、生成球拍,这里同样也要将球拍的边缘添加进碰撞检测
-(void)makeBoard { UIImageView * board = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"RedButton"]]; board.bounds=CGRectMake(0, 0, boardW, 10); board.center=CGPointMake(self.center.x, self.frame.size.height*0.75); [self addSubview:board]; //获取球拍边缘 UIBezierPath * path = [UIBezierPath bezierPathWithRect:board.frame]; //添加碰撞检测 [self.col addBoundaryWithIdentifier:@"board" forPath:path]; //做全局记录 self.board=board; self.path=path; }
8、触摸响应事件:(1)更新球拍的位置,让他能够根据手指触摸移动,并且不能移动到屏幕的上半部。
(2)更新球拍的碰撞检测的边缘,时刻保持球拍可以“击打”小球。
-(void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { //获取屏幕触摸点的一些数据 //当前点 CGPoint currentPoint = [touches.anyObject locationInView:self]; //上一刻的点 CGPoint prePoint = [touches.anyObject previousLocationInView:self]; //便宜亮 CGPoint offset = CGPointMake(currentPoint.x-prePoint.x, currentPoint.y-prePoint.y); CGFloat boardX = self.board.center.x; CGFloat boardY = self.board.center.y; //给拍子添加一个 移动上边界 //拍子在下半部,正常移动 if (self.board.center.y>= self.center.y) { boardX += offset.x; boardY += offset.y; self.board.center=CGPointMake(boardX, boardY); } else { //拍子到达边界,下一刻向下移动,则正常移动 if (offset.y>=0) { CGFloat boardX = self.board.center.x; CGFloat boardY = self.board.center.y; boardX += offset.x; boardY += offset.y; self.board.center=CGPointMake(boardX, boardY); } //拍子到达边界,下一刻向上移动,则竖直方向上不可移动(不能再向上移动了) else{ CGFloat boardX = self.board.center.x; boardX += offset.x; self.board.center=CGPointMake(boardX, boardY); } } //先移除原来的拍子的边界碰撞 [self.col removeBoundaryWithIdentifier:@"board"]; //添加拍子新的边界碰撞 self.path= [UIBezierPath bezierPathWithRect:self.board.frame]; [self.col addBoundaryWithIdentifier:@"board" forPath:self.path]; }
9、重写碰撞代理方法, 实现得分。在设定障碍物的边缘碰撞检测时,都有标记了“Identifier”,所以根据这个标识,区分不同的障碍物,可以设定不同的得分
-(void)collisionBehavior:(UICollisionBehavior *)behavior beganContactForItem:(id<UIDynamicItem>)item withBoundaryIdentifier:(id<NSCopying>)identifier atPoint:(CGPoint)p { NSString * str = (NSString *)identifier; //得5分 if ( [str isEqualToString:@"score1"]){ self.score += 5; } //得1分 else if([str isEqualToString:@"score2"]) { self.score +=1; } self.scoreLabel.text=[NSString stringWithFormat:@"得分:%ld",self.score]; }
至此,案例效果实现。
四、总结
1、UIKit动力学的引入,并不是为了替代CA或者UIView动画,在绝大多数情况下CA或者UIView动画仍然是最优方案,只有在需要引入逼真的交互设计的时候,才需要使用UIKit动力学它是作为现有交互设计和实现的一种补充。
2、使用物理仿真比较消耗性能。
3、个人感觉,物理仿真看似功能强大,还是有很多小问题以及不够灵活,比如碰撞检测的边缘添加方式不够灵活,而且容易出现小bug。拖拽和刚性附着等行为也会有小bug出现,本人功力不够,还没有很好解决。
4、本例中还遗留了一些问题,包括游戏本身交互性没有完成,只是做一个仿真行为使用的小练习,而且小球的释放问题也没处理(没有影响,就懒得弄了),球拍的击打效果,球拍的摩擦效果都没有完善,大神勿喷。。