加速计简单使用---迷宫游戏

今天通过编写一个简单的迷宫游戏,来展示如何使用iPhone的内置加速计.

游戏效果如下图所示.用户通过上下左右摇晃屏幕控制这个橙色的pacman挪动,pacman撞到屏幕边缘或者墙壁(蓝色边框方块)会反弹,撞到红色ghost会游戏失败,直到成功吃到黄色豆子便是游戏通关.整个逻辑很简单,也是我们小时候畅玩的小游戏.

整个游戏教程分为三个部分:

  1. 搭建好UI,设计出该迷宫,制作出部分walls和3个ghost.同时设置动画让三个ghosts在固定位置保持来回运动,阻碍pacman的通关;
  2. 介绍加速计来展示如何移动我们的pacman;
  3. 最后一部分,也是最重要的,就是设置pacman与屏幕四周,walls,以及ghosts之间的撞击冲突效果.

下面让我们开始吧.

为了实现游戏效果,当我们旋转iPhone时,我们不希望我们的app跟着旋转.因此,我们让我们的app仅支持横向.这里我们选择"Landscape Right".当然你也可以选择"Landscape Left",根据你个人的喜好.

第一步:搭建UI,给Ghost设置动画

一.搭建UI

  1. 我们通过storyboard来简单搭建我们的UI.为了方便,可以将我们的storyboard方向设置为横向.将背景调为黑色.

    2.将所需要的图标导入项目中.根据你自己的个人喜好布局UI.设置wall和ghost的数量和位置.这样也便决定了你设置游戏的难度.我的布局就姑且如下所示. 

接着将各个控件拖入到ViewController.m中以创建IBOutlet,方便我们超控它们.分别命名为pacman,ghost1,ghost2,ghost3,exit和wall.这些都比较简单,相信大家都会.然而这里面有一点我强调一下,就是关于wall,因为数量较多,我们将采用另一种方式,就是把这些wall对象统一和NSArray对象进行相关联.这样看起来较为清爽,也方便我们统一操作.连线时,connection选择Outlet Collection而非Outlet.

拖完后,ViewController.m中的代码如下图所示:

@property (weak, nonatomic) IBOutlet UIImageView *pacman;
@property (weak, nonatomic) IBOutlet UIImageView *ghost1;
@property (weak, nonatomic) IBOutlet UIImageView *ghost2;
@property (weak, nonatomic) IBOutlet UIImageView *ghost3;
@property (strong, nonatomic) IBOutletCollection(UIImageView) NSArray *wall;
@property (weak, nonatomic) IBOutlet UIImageView *exit;

二.导入框架

使用加速计是需要使用到CoreMotion框架的,默认情况下,我们的应用程序不包含此框架,因此需要我们手动导入.直接在TARGETS->General->Linked Frameworks and Libraries 中导入即可.同样,还要导入另一个很重要的框架:QuartzCore框架.因为我们将使用其中的CABasicAnimation类来创建基本动画.(当然,因为xcode后来UIKit中有导入框架,也不需要我们手动导入了)l

三:给Ghost设置动画

ViewController.m中导入头文件

#import <QuartzCore/CAAnimation.h>

接下来给三个ghost添加动画.

- (void)ghostAnimation {
    CGPoint origin1 = self.ghost1.center;
    CGPoint target1 = CGPointMake(self.ghost1.center.x, self.ghost1.center.y+124);
    CABasicAnimation *bounce1 = [CABasicAnimation animationWithKeyPath:@"position.y"];
    bounce1.fromValue = @(origin1.y);
    bounce1.toValue = @(target1.y);
    bounce1.duration = 2;    //周期时间
    bounce1.autoreverses = YES;    //自动反转
    bounce1.repeatCount = HUGE_VALF; 
    bounce1.fillMode = kCAFillModeForwards; //动画完成不移除
    bounce1.removedOnCompletion = NO;   //退到后台不被系统暂停
    [self.ghost1.layer addAnimation:bounce1 forKey:@"position"];

    CGPoint origin2 = self.ghost2.center;
    CGPoint target2 = CGPointMake(self.ghost2.center.x, self.ghost2.center.y+90);
    CABasicAnimation *bounce2 = [CABasicAnimation animationWithKeyPath:@"position.y"];
    bounce2.fromValue = @(origin2.y);
    bounce2.toValue = @(target2.y);
    bounce2.duration = 2.5;
    bounce2.autoreverses = YES;
    bounce2.repeatCount = HUGE_VALF;
    bounce2.fillMode = kCAFillModeForwards;
    bounce2.removedOnCompletion = NO;
    [self.ghost2.layer addAnimation:bounce2 forKey:@"position"];
    
    CGPoint origin3 = self.ghost3.center;
    CGPoint target3 = CGPointMake(self.ghost3.center.x, self.ghost3.center.y+180);
    CABasicAnimation *bounce3 = [CABasicAnimation animationWithKeyPath:@"position.y"];
    bounce3.fromValue = @(origin3.y);
    bounce3.toValue = @(target3.y);
    bounce3.duration = 3;
    bounce3.autoreverses = YES;
    bounce3.repeatCount = HUGE_VALF;
    bounce3.fillMode = kCAFillModeForwards;
    bounce3.removedOnCompletion = NO;
    [self.ghost3.layer addAnimation:bounce3 forKey:@"position"];
}

在viewDidLoad中调用下该方法.好的,这个时候我们可以跑一下我们的代码了.如果一切无误的话,你会看到整个迷宫界面,并且三个ghost在上下跳动,是不是有点意思了?

好了,下面就让我们使用加速计来操纵我们的pacman.

第二步:使用加速计来移动pacman

一.声明属性

 iPhone的内置加速计为我们iOS开发人员提供了大量创造有趣游戏的机会.我们经常见到通过倾斜iPhone来控制游戏角色的app,比如狂野飞车等,当然那个就相当复杂了.现在我们通过这个简单的迷宫游戏来熟悉下系统提供的加速计如何使用.首先我们需要声明一些属性.在此之前导入必要的头文件

#import <CoreMotion/CoreMotion.h>
@property (nonatomic,assign) CGPoint currentPoint;  //pacman当前位置
@property (nonatomic,assign) CGPoint previousPoint; //pacman移动的位置
@property (nonatomic,assign) CGFloat pacmenXVelocity;   //速度的x分量(速度是矢量的)
@property (nonatomic,assign) CGFloat pacmanYVelocity;   //速度的y分量(速度是矢量的)
@property (nonatomic,assign) CGFloat angle; //pac当前角度 为了看起来更真实,我们设置pacman的旋转
@property (nonatomic,assign) CMAcceleration acceleration;   //加速度计测量出的当前加速度
@property (nonatomic,strong) CMMotionManager *motionManager;    //是一个队列,可以帮助我们接收和处理从加速度计发送的数据
@property (nonatomic,strong) NSOperationQueue *queue;
@property (nonatomic,strong) NSDate *lastUpdateTime;    //允许我们控制加速度计上次调用以来的时间

额外---加速计(Accelerometer)的介绍

1-加速计简介

  • 1.加速计的作用

    • 用于检测设备的运动(比如摇晃)
      • 检测某一个方向上力的作用.它用来测量加速力,无论是由重力还是运动引起的加速力都可以.所以换言之,加速计可以测量移动速度并且能够感知到它的握持角度.如果您还想了解下iPhone加速计如何工作的更多信息,可以查看此视频.
  • 2.加速计的经典应用场景

    • 摇一摇
    • 计步器
  • 3.加速计的原理

    • 检测设备在x轴、y轴、z轴上的加速度
      • 哪一个方向有力的作用,哪一个方向就运动了
        • 根据加速度的数值,就可以判断出在各个方向上的作用力度
  • 4.在iOS4之前加速度计是由UIAccelerometer类来负责采集数据,现在一般都是用CoreMotion来处理加速度

  • 5.需要注意的是:加速计的坐标系不是iPhone屏幕的坐标系,而是大家在上学时期所熟知的笛卡尔坐标系

2-加速计的坐标

home键在下时,Y轴是笛卡尔坐标系。home建在右,X轴是笛卡尔坐标系。屏幕朝上时,z是笛卡尔坐标系.这里说的是iPhone朝地的方向,因为加速器是根据重力来检测的.(具体很难讲清楚,大家一会可以在项目中打印rotationRate.x , rotationRate.y, rotationRate.z来体验下)

iOS Core Motion框架允许我们开发人员从设备硬件中获取运动数据并且处理这些数据.这里的硬件设备包括加速计,陀螺仪,磁力计,计步器等.有兴趣的童鞋可以私自去深挖其中的奥秘.其中CMMotionManager类负责管理运动数据.通过使用该类,我们可以定期获取加速计检测到的数据,我们待会将有用到.

了解了加速计的原理后,下面让我们来继续我们的迷宫游戏项目.

二.使用CMMotionManager来获取运动数据

获取运动数据会调用一个专门采样的方法,这里我们来设定一下采样的频率.采样频率越高,那么pacman运动就会更精确,当然也会更耗电些.那么我们来设定一个值每秒钟采样60次吧,这样pacman的运动看起来会相当平滑.我们为此定义一个宏.

#define kUpdateInterval (1.0f/60.f)

下面让我们来初始化加速计,设置pacman的运动

- (void)pacmanAnimation {
    self.lastUpdateTime = [NSDate date];    //记录当前时间
    self.currentPoint = CGPointMake(0, 144);    //设置pacman初始位置
    self.motionManager = [[CMMotionManager alloc] init];    //创建运动管理器
    self.queue = [[NSOperationQueue alloc] init];    //队列一般尽量使用全局变量
    self.motionManager.accelerometerUpdateInterval = kUpdateInterval;//设置采样间隔,每秒钟60次,这样,咱们的pacman应该运动起来足够平滑了吧.

    //开始采样的方法
    [self.motionManager startAccelerometerUpdatesToQueue:self.queue withHandler:^(CMAccelerometerData * _Nullable accelerometerData, NSError * _Nullable error) {
        //记录下当前的加速度.有兴趣的童鞋可以打印看看x,y,z值感受下.
        [self setAcceleration:accelerometerData.acceleration];
        //        NSLog(@"x: %f, y: %f, z: %f", self.acceleration.x , self.acceleration.y, self.acceleration.z);
        //回到主线程更新UI
        [self performSelectorOnMainThread:@selector(updateLocation) withObject:nil waitUntilDone:NO];
    }];
}

三.移动吃豆子

目前我们已经完成了不断采样pacman加速度的数据了,接下来我们将使用这些数据来不断更新pacman的新位置.让我们来实现updateLocation方法.

- (void)updateLocation {
    //获取两次采样之间的时间间隔. 前面加个负号是为了保证值为正数.
    NSTimeInterval secondSinceLastDraw = - ([self.lastUpdateTime timeIntervalSinceNow]);
    //获取y方向的速度分量.因为屏幕是横屏,所以self.pacmanYVelocity要通过self.acceleration.x来进行叠加,这里童鞋们要注意的.又因为我们之前选择横屏的方向为:Landscape Right,所以是通过减号来保证y方向速度的增量,如果童鞋们当时选的是Landscape Left,那么就要改成加号了.所以大家一定要注意这个细节.具体原因跟上面提到的笛卡尔坐标系有很大关联.
    self.pacmanYVelocity = self.pacmanYVelocity - (self.acceleration.x*secondSinceLastDraw);
    //x方向的速度分量的计算原理同上.  这样计算的目的是实现在重力加速度作用下,实现速度越来越快的效果,更有现实感.
    self.pacmenXVelocity = self.pacmenXVelocity - (self.acceleration.y*secondSinceLastDraw);
    //上面得出每次采样时当前的速度后,这里就可以计算出每两次采样时间间隔内pacman在x,y方向上挪动的距离.500是自己设置的一个参数.确定pacman移动的速度,这个值越大,pacman移动得越快.个人感觉设定这个值已经可以了.大家可以根据自己的喜好擅自更改
    CGFloat xDelta = secondSinceLastDraw*self.pacmenXVelocity*500;
    CGFloat yDelta = secondSinceLastDraw*self.pacmanYVelocity*500;
//大伙们可以看看打印出来的结果进行分析.
//    NSLog(@"secondSinceLastDraw:%lf---x:%lf---pacmanYVelocity:%lf---yDelta:%lf",secondSinceLastDraw,self.acceleration.x,self.pacmanYVelocity,yDelta);
    //每次采样后更新pacman的位置
    self.currentPoint = CGPointMake(self.currentPoint.x+xDelta, self.currentPoint.y+yDelta);
    //移动pacman
    [self movePacman];
    //pacman做旋转操作
    [self rotateThePacman];
    //把当前的时间记录下来作为上一次更新的时间
    self.lastUpdateTime = [NSDate date];
}

上面更新pacman位置的方法我在注释中已经描述的比较清楚了,这里就不再次描述.值得注意的是,因为速度是矢量值,所以我们要分别计算它的x矢量和y矢量,当然如果是3d的话是还存在z矢量的,因为我们这个是平面小游戏,就不考虑z矢量了.另外有童鞋可能会问:既然已经把采样间隔时间设置为每秒60次了,还要再次计算两次采样之间的时间间隔呢?那是因为这只是一个近似值.出于某些原因,我们可能会不定期的获取数据,所以我们还是计算自上次调用以来经过的时间较好,这样比较准确.

最后,我们也要使pacman挪到相应的位置的.

- (void)movePacman {

    CGRect frame = self.pacman.frame;
    frame.origin.x = self.currentPoint.x;
    frame.origin.y = self.currentPoint.y;
    self.pacman.frame = frame;
    //保存最新的位置
    self.previousPoint = self.currentPoint;
}

到了这里,大家可以跑一跑代码体验下pacman随着屏幕的摇晃而运动,是不是开始有点感觉了?但还是有一点不尽人意的地方,那就是pacman运动起来自己却是静态的且不切实际,我们希望看到他在迷宫中移动时也能保持旋转.

下面让我们来实现pacman的旋转.

- (void)rotateThePacman {
    CGFloat newAngle = (self.pacmenXVelocity + self.pacmanYVelocity)*M_PI*4;
    self.angle += newAngle*kUpdateInterval;
    CABasicAnimation *rotate = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
    rotate.fromValue = @(0);
    rotate.toValue = @(self.angle);
    rotate.duration = kUpdateInterval;
    rotate.repeatCount = 1;
    rotate.removedOnCompletion = NO;
    rotate.fillMode = kCAFillModeForwards;
    [self.pacman.layer addAnimation:rotate forKey:@"10"];
    
    self.lastAngle = self.angle;
}

角度的旋转真的有点棘手,它涉及到一些数学知识.我目前还没有想到很好的办法,只能姑且通过self.angle += newAngle*kUpdateInterval使旧值向新值过渡,实现pacman的平滑旋转.如果要想实现pacman的开口处始终朝着前进的方向,这需要花费点时间是琢磨计算公式,你们可以去尝试一下.

现在可以跑跑代码感受一下.

第三步:设置碰撞效果

以上情况,pacman可以穿透任何walls和ghosts还有屏幕等障碍物,还属于半生不熟的状态,现在我们来给pacman设置障碍,与障碍物接触时产生碰撞效果.包括以下四种情况:

  1. 与屏幕边界碰撞,pacman不离开屏幕,而是反弹;

  2. 当pacman绕过所有障碍并吃掉黄豆时,那么玩家就赢得比赛;

  3. 当与ghost碰撞时,则游戏失败;

  4. 当和wall碰撞时,那么也是反弹.

一.与屏幕边界的碰撞

这个比较容易,只要检查pacman是否在屏幕内即可.

- (void)collisionWithBoundaries {
    if (self.currentPoint.x<0) {
        _currentPoint.x = 0;
        self.pacmenXVelocity = -(self.pacmenXVelocity/2.0);
    }
    if (self.currentPoint.y<0) {
        _currentPoint.y = 0;
        self.pacmanYVelocity = -(self.pacmanYVelocity/2.0);
    }
    if (self.currentPoint.x>self.view.bounds.size.width-self.pacman.bounds.size.width) {
        _currentPoint.x = self.view.bounds.size.width-self.pacman.bounds.size.width;
        //反转速度矢量的方向,同时让速度减半,模仿现实世界的效果
        self.pacmenXVelocity = -(self.pacmenXVelocity/2.0);
    }
    if (self.currentPoint.y>self.view.bounds.size.height-self.pacman.bounds.size.height) {
        _currentPoint.y = self.view.bounds.size.height-self.pacman.bounds.size.height;
        //反转速度矢量的方向,同时让速度减半,模仿现实世界的效果
        self.pacmanYVelocity = -(self.pacmanYVelocity/2.0);
    }
}

在movePacman方法的开头处就调用此方法.

二.与exit的碰撞

撞到黄豆后,就提示恭喜赢得比赛.并且停止motionManager的运动采样方法.

- (void)collisionWithExit {
    if (CGRectIntersectsRect(self.pacman.frame, self.exit.frame)) {
        //停止加速计采样
        [self.motionManager stopAccelerometerUpdates];
        
        UIAlertController *ac = [UIAlertController alertControllerWithTitle:@"Congratulations" message:@"You've won the game!" preferredStyle:UIAlertControllerStyleAlert];
        UIAlertAction *action = [UIAlertAction actionWithTitle:@"ok" style:UIAlertActionStyleDefault handler:nil];
        [ac addAction:action];
        [self presentViewController:ac animated:YES completion:nil];
    }
}

Core Graphics提供了CGRectIntersectsRect来帮助我们检查一个view的帧是否与另一个指定view的帧重叠.

三.与ghost的碰撞

撞到ghost后,那么就提示游戏失败,并且回到起点.

- (void)collisionWithGhosts {
    CALayer *ghostLayer1 = self.ghost1.layer.presentationLayer;
    CALayer *ghostLayer2 = self.ghost2.layer.presentationLayer;
    CALayer *ghostLayer3 = self.ghost3.layer.presentationLayer;
    
    if (CGRectIntersectsRect(self.pacman.frame, ghostLayer1.frame)||CGRectIntersectsRect(self.pacman.frame, ghostLayer2.frame)||CGRectIntersectsRect(self.pacman.frame, ghostLayer3.frame)) {
        UIAlertController *ac = [UIAlertController alertControllerWithTitle:@"Oops" message:@"Mission Failed" preferredStyle:UIAlertControllerStyleAlert];
        UIAlertAction *action = [UIAlertAction actionWithTitle:@"ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
            self.currentPoint = CGPointMake(0, 144);
        }];
        [ac addAction:action];
        [self presentViewController:ac animated:YES completion:nil];
    }
}

四.与walls的碰撞

这个可能比前面几个碰撞稍微复杂一点点,因为我们这里的wall是方形的,所以回涉及到时撞墙的左右还是上下,那么加速度的方向将有不同的取择.

- (void)collisionWithWalls {
    CGRect frame = self.pacman.frame;
    frame.origin.x = self.currentPoint.x;
    frame.origin.y = self.currentPoint.y;
    for (UIImageView *image in self.wall) {
        if (CGRectIntersectsRect(frame, image.frame)) {
            CGPoint pacmanCenter = CGPointMake(frame.origin.x+frame.size.width/2.f, frame.origin.y+frame.size.height/2.f);
            CGPoint imageCenter = CGPointMake(image.frame.origin.x+frame.size.width/2.f, image.frame.origin.y+frame.size.height/2.f);
            CGFloat angleX = pacmanCenter.x - imageCenter.x;
            CGFloat angleY = pacmanCenter.y - imageCenter.y;
            //判断x分量和y分量的大小,如果x>y,则说明撞到了wall的左右边缘,那么就将速度的x分量反转并减半;如果x<y,说明撞到了wall的上下边缘,那么就将速度的y分量反转并减半.
            if (fabs(angleX)>fabs(angleY)) {
                _currentPoint.x = self.previousPoint.x;
                self.pacmenXVelocity = -self.pacmenXVelocity/2.f;
            }
            else {
                _currentPoint.y = self.previousPoint.y;
                self.pacmanYVelocity = -self.pacmanYVelocity/2.f;
            }
        }
    }
}

将这几个方法在movePacman方法中调用.

- (void)movePacman {
    [self collisionWithBoundaries];
    [self collisionWithExit];
    [self collisionWithGhosts];
    [self collisionWithWalls];
    CGRect frame = self.pacman.frame;
    frame.origin.x = self.currentPoint.x;
    frame.origin.y = self.currentPoint.y;
    self.pacman.frame = frame;
    self.previousPoint = self.currentPoint;
}

到了这一步了,那么就恭喜你一款简易的迷宫游戏完成了.跑下代码体验一下吧.当然了这个游戏很不完美,存在着很多错误,其实这个小项目的目的是来初步的了解iOS系统自带的速度计的使用.大家相互学习吧.欢迎你们可以在此基础上制作出一个更高级别的游戏.

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值