使用SpriteKit做一个Frvr游戏

作者: 小武的技术渔场 | 来源:发表于2016-04-13 09:57 被阅读645次

    一,游戏怎么玩?

    好吧,我又回来了,之前利用SpriteKit游戏引擎做过一个十字消除的游戏。对于不会其它引擎的人来说,SpriteKit的优点就是比较简单迅速,快速开发些有趣的小游戏。前些天,玩了一个很好玩的在线游戏Hex Frvr,是使用Html5做的,在AppStore上有可以下载玩到,在线游戏的地址点这里

    frvr游戏

    从这里下载完整的Demo工程,在Xcode里面直接打开运行吧。玩起来的效果是这样的:

    frvr2

    Ok,东西就是这么个东东,本文就详细描述下demo里面的内容,用SpriteKit来实现吧。

    二,开始制作游戏之旅。

    1,素材的准备。

    好吧,之前做十字消游戏的时候,使用程序生成方块的素材。这次素材是六边形,程序这块本想研究下怎么生成的,不过学习使人进步,我最近研究了下Mac OS下的一个设计神器“Sketch”,然我由此走上了设计师的路(尼玛,老板你怎么不请美工啊)。

    ok,简单使用下Sketch,画个六边形。如果不想画的哥们,请直接拿Demo工程里面的素材,不谢不谢!

    Sketch绘制六边形的方法:
    (1)绘制六边形。
    a,打开Sketch,新建一个画布(A)

    新建画布

    b,插入多边形,默认是5边形,先画一个。在右边检查器里选6,增加一个点,就生成了6变形。
    注意:按着Shift键拉动,调整成正六边形。

    选择多边形

    编辑多变形点个数

    编辑多边形点数

    (2)上颜色加增加立体效果。
    a,上颜色。
    在右侧检查器的Fill栏,选择喜欢颜色,然后Blending模式选择Normal。就有基本的底色了。

    b,增加图像的立体感。
    增加增加线性渐变:Fill栏里,添加线性渐变,Blending模式选择Overlay。
    圆形渐变:Fill栏里,添加圆形渐变,Blending模式选择Overlay。
    效果特效自己可以随便设置,我都是以最上面那个点为渐变起点,最底下的点为渐变终点,强调一下立体的感觉。
    最终效果如图所示:

    六边形素材最终效果

    线性渐变和圆形渐变

    c,导出素材
    点击export,按照提示到处png格式图片。

    2,游戏玩法和规则。

    游戏规则(百度词条)

    “游戏的主界面是一个大六边形棋盘[2] ,六边形的每一边又由5个小六边形组成,构成共有61格的棋盘空间。玩家拖动系统自动输出的各种六边形组合置于大六边形的空白处,使之排列成完整的一行或多行并且消除得分,而连续消除会有额外Combo加分。直到棋盘上再也无法摆下任意一个六边形组合的时候,游戏就会失败。”

    OK,归纳成编写程序的输入,我们要实现下面的东东:
    (1)61个小六边形组成的蜂巢状棋盘。
    如图的游戏主界面所示,主界面是如游戏名Hex所代表的蜂窝状六边形组合成的棋盘,共有61格,为了显示方便,我都边上了号码。

    游戏界面

    (2)24种有4个小六边形组成的不同形状+1个单六边形。
    如图2.6所示,4个小六边形所排列组合成的不同形状,就是我们要填入到棋盘格子里面的图形和形状。

    待填入图形

    (3)将(2)中的不同形状填入(1)中的棋盘,在横向,左斜方向和右斜方向有占满的六边形格子就消除。
    (4)每回合有三个(2)中的形状,填入一个补充一个。如果三个形状都无法再填入61格子的棋盘中,游戏结束。
    其游戏过程可以参见文章开头的动图显示。

    3,游戏实现数据结构及算法说明。

    (1)要有方向。

    一个六边形有六个方向,因此如果要在棋盘中进行比较和消除,必须比较每一个小六边形单元六个方向的情况。很显然,我们将六边形的六个方向做一个编号:

    //LeftTop:0
    //RightTop:1
    //Right:2
    //RightBottom:3
    //LeftBottom:4
    //Left:5
    
    typedef enum : NSInteger {
        SUDNone = -1,
        SUDTopLeft = 0,
        SUDTopRight,
        SUDRight,
        SUDBottomRight,
        SUDBottomLeft,
        SUDLeft,
    } ShapeUnitDirector;
    
    

    其方向示意如图所示:

    六边形比较方向示意图

    (2)要有顺序。

    如何检查2.(2)中的25种形状是否可以放入2.(1)中的棋盘呢?嗯,好了,计算机要一个一个的比较,没有你聪明。不过好在它的速度飞快!
    但是你要告诉电脑怎么弄。首先比较是有顺序的,对比较的不同的形状,必须规定一个比较顺序。为其中每一个单元的六边形编一个序号,表示比较的顺序。这个顺序要是一个连续的,可达的路径,计算机比较的时候能有来有回。路径可以用上面规定的方向来表示。如图选了两个形状,来说明他们的比较顺序。

    比较顺序

    “一”和“二”的两个图形里每个小六边形都有编号,其编号就是其比较顺序,1非常重要,是比较的起始点。
    “一”图形里,1是起始比较点,比较1后,就要比较2,2位于1的LeftBottom方位,所以要将LeftBottom记住。3位于2的RightBottom位置,4位于3的RightTop方位。所以按照1到4的顺序,比较路径就是[LeftBottom,RightBottom,RightTop],红色箭头所示。
    同理,“二”的图形里,比较路径就是[Right,RightBottom,LeftBottom]。
    注意:这个形状比较顺序非常重要,要理解清楚。

    (3)要会比较。

    a,第一种比较是,将图形放置到棋盘上后,看图形里的每一个六边形是否能放置到棋盘上去。在程序里面其实算法很简单,就是遍历图形里每一个小六边形,看其所在位置下的棋盘格子是否是空的,如果全都是空的就可以放上去了。
    b,每一次成功放置了形状后,都会补充一个新的形状。此时,要判定游戏失败条件,即游戏是否可以进行下去。将现有的没有放进棋盘的三个形状,迭代的对棋盘里的每一个位置进行一下比较,看是否能放得进去。如果三个形状都放不进棋盘,那么游戏结束了。

    注意,这里的比较,就要使用到刚才定义的比较顺序了,如“一”里面的[LeftBottom,RightBottom,RightTop],因为你知道棋盘格每一个的位置,棋盘六个方向的位置也可以通过数据结构来记录,但是你需要知道放置的图形,它的比较路径和位置信息,才能够很好的便利,所以才会定义比较路径和比较起始点,有了这两个元素,比较才能进行。

    PS:其实还有别的方法,仅需要图形的起始点,把起始点移到棋盘的每一格,按照比较a里的位置判断方法比较。好了,其实比较简单啦!理解下就好。

    4,编程实现。

    SpriteKit的用法和Cocos2dx比较像,也是将实体精灵Sprite以树形结构组织,你来规定Sprite节点的交互和动画,达到游戏的结果。在iOS9系统中加入了很多的新功能,实在值得好好研究,不过这里我们用的比较简单。大家也可以看看我之前写的《使用SpriteKit游戏引擎,做一个十字消游戏》。里面有SpriteKit的普及和基础知识,我们在这里就不对SpriteKit引擎进行过多的讲解。

    游戏界面:
    (1)设计游戏界面布局。
    第一,由于是蜂巢状的六边形,每一行的个数先是增加,后来又递减,而且位置又不太相同。所以需要记录棋盘每行的单元格个数。计算出横向和纵向的距离,再进行添加。
    第二,需要记录蜂巢状六边形的六个方向的单元格编号,方便比较的时候搜索。我在这里使用了一个JSON文件,将每一个单元格的信息写入里面,初始化的时候读入,生成相关的信息数据结构。其结构如下,serialNum号就是单元格编号,如图2.5所示。adjacent就是邻接的单元点编号,-1代表该方向没有单元格。

    "unitInfos" : [
       {
          "x" : 0,
          "y" : 0,
          "serialNum" : 0,
          "adjacent" : "-1 -1 1 6 5 -1"
       },
    

    添加棋盘的代码参见Demo代码里的GameScene.m的如下代码:

    - (void)addPlayground
    {
        // 1  初始化相关数据结构
        SKSpriteNode *node;
        self.unitNodeArray = [[NSMutableArray alloc] init];
        
        self.unitTexture = [SKTexture textureWithImageNamed:@"6kuai_gray.png"];
        self.unitWidth = self.unitTexture.size.width;
        self.unitHight= self.unitTexture.size.height;
        
        //2 生成每行的单元格个数,并设置起始点。
        NSArray *arrayNumber = @[@5,@6,@7,@8,@9,@8,@7,@6,@5];
        CGPoint startPoint = CGPointMake(CGRectGetMidX(self.frame) -2*self.unitWidth, CGRectGetHeight(self.frame)-150 );
        
      // 3 两层循环,摆放棋盘单元格,并填入从JSON中读取的信息,放入userdata字段。
        int index = 0;
        int nodeCount = 0;
        for (NSNumber *lineNumber in arrayNumber) {
            int count = lineNumber.intValue;
            for (int i = 0; i < count; i++) {
                //3.1 生成单元格节点
                node = [SKSpriteNode spriteNodeWithTexture:self.unitTexture];
    
                //3.2 摆放位置
                if (index <= 4) {
                    [node setPosition:CGPointMake(startPoint.x-XDISTANCE*index +i*self.unitWidth, startPoint.y-YDISTANCE*index)];
                }
                else
                {
                    [node setPosition:CGPointMake(startPoint.x - XDISTANCE*((PLAYGROUNDLINE-1)-index) + i*self.unitWidth, startPoint.y - YDISTANCE*index)];
                }
                
                // 3.3 读取单元格信息,并填入userData
                ShapeUnitInfo *unitInfo = [_unitInfoArray objectAtIndex:nodeCount];
                unitInfo.unitPosition = node.position;
                node.userData = [[NSMutableDictionary alloc] init];
                [node.userData setValue:unitInfo forKey:@"unitInfo"];
                [node setName:@"unitShape"];
                
                // 3.4 加入数字标签
                SKLabelNode *label = [SKLabelNode labelNodeWithText:[NSString stringWithFormat:@"%d",nodeCount]];
                label.position = CGPointMake(0, 0);
                label.fontColor = [UIColor blackColor];
                label.fontSize = 18;
                label.zPosition = 2;
                [node addChild:label];
                
                //3.5 添加节点入GameScene
                [self addChild:node];
                [self.unitNodeArray addObject:node];
                nodeCount++;
            }
            index++;
        }
    }
    
    - (void)addPlayground
    {
        // 1  初始化相关数据结构
        SKSpriteNode *node;
        self.unitNodeArray = [[NSMutableArray alloc] init];
        
        self.unitTexture = [SKTexture textureWithImageNamed:@"6kuai_gray.png"];
        self.unitWidth = self.unitTexture.size.width;
        self.unitHight= self.unitTexture.size.height;
        
        //2 生成每行的单元格个数,并设置起始点。
        NSArray *arrayNumber = @[@5,@6,@7,@8,@9,@8,@7,@6,@5];
        CGPoint startPoint = CGPointMake(CGRectGetMidX(self.frame) -2*self.unitWidth, CGRectGetHeight(self.frame)-150 );
        
      // 3 两层循环,摆放棋盘单元格,并填入从JSON中读取的信息,放入userdata字段。
        int index = 0;
        int nodeCount = 0;
        for (NSNumber *lineNumber in arrayNumber) {
            int count = lineNumber.intValue;
            for (int i = 0; i < count; i++) {
                //3.1 生成单元格节点
                node = [SKSpriteNode spriteNodeWithTexture:self.unitTexture];
     
                //3.2 摆放位置
                if (index <= 4) {
                    [node setPosition:CGPointMake(startPoint.x-XDISTANCE*index +i*self.unitWidth, startPoint.y-YDISTANCE*index)];
                }
                else
                {
                    [node setPosition:CGPointMake(startPoint.x - XDISTANCE*((PLAYGROUNDLINE-1)-index) + i*self.unitWidth, startPoint.y - YDISTANCE*index)];
                }
                
                // 3.3 读取单元格信息,并填入userData
                ShapeUnitInfo *unitInfo = [_unitInfoArray objectAtIndex:nodeCount];
                unitInfo.unitPosition = node.position;
                node.userData = [[NSMutableDictionary alloc] init];
                [node.userData setValue:unitInfo forKey:@"unitInfo"];
                [node setName:@"unitShape"];
                
                // 3.4 加入数字标签
                SKLabelNode *label = [SKLabelNode labelNodeWithText:[NSString stringWithFormat:@"%d",nodeCount]];
                label.position = CGPointMake(0, 0);
                label.fontColor = [UIColor blackColor];
                label.fontSize = 18;
                label.zPosition = 2;
                [node addChild:label];
                
                //3.5 添加节点入GameScene
                [self addChild:node];
                [self.unitNodeArray addObject:node];
                nodeCount++;
            }
            index++;
        }
    }
    

    (2)设置游戏相关初始化数据。
    这里就是读入JSON文件,并将分数置0。

    - (void)unitInfoInit
    {
        // 1 初始化信息存入的数据容器,一个NSArray
        if (_unitNodeArray != nil) {
            return;
        }
        
        _unitInfoArray = [[NSMutableArray alloc] init];
        
        // 2  读入JSON文件
        NSString *bundleDir = [[NSBundle mainBundle] bundlePath];
        NSString *path = [bundleDir stringByAppendingPathComponent:@"unitInfo.json"];
        
        NSURL *url = [NSURL fileURLWithPath:path];
        NSData *data = [NSData dataWithContentsOfURL:url];
        
        NSError *error = nil;
        
        // 3 解析JSON文件,并存入Arrary容器
        NSDictionary *jsonDic = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
        
        NSArray *unitInfos = [jsonDic objectForKey:@"unitInfos"];
        
        if (unitInfos != nil) {
            for (NSDictionary *unitInfoDic in unitInfos) {
                ShapeUnitInfo *unitInfo = [[ShapeUnitInfo alloc] init];
                int x = ((NSNumber *)[unitInfoDic objectForKey:@"x"]).intValue;
                int y = ((NSNumber *)[unitInfoDic objectForKey:@"y"]).intValue;
                int sn = ((NSNumber *)[unitInfoDic objectForKey:@"serialNum"]).intValue;
                NSString *adjacentString = (NSString *)[unitInfoDic objectForKey:@"adjacent"];
                NSArray *adjacents = [adjacentString componentsSeparatedByString:@" "];
                unitInfo.unitLocation = CGPointMake(x, y);
                unitInfo.serialNumber = sn;
                
                [unitInfo.adjacentArray addObjectsFromArray:adjacents];
                
                [_unitInfoArray addObject:unitInfo];
            }
        }
    }
    
    

    (3)三个备选容器添加.
    在棋盘下面的位置添加三个备选容器,如图2.5种的2所标示的位置。

     - (void)addShapeFrame
    {
       //1 初始化存储容器
        SKSpriteNode *node;
        _shapePosArray = [[NSMutableArray alloc] initWithCapacity:3];
        _shapeArray = [[NSMutableArray alloc] initWithCapacity:3];
     
       // 2 生成被选位置节点,并加入到Scene中去
        for (int i = 0; i < 3; i++) {
            node = [[SKSpriteNode alloc] init];
            node.size = CGSizeMake(100, 100);
            node.position= CGPointMake(CGRectGetMidX(self.frame) + (i - 1)*120, 220);
            node.name = [NSString stringWithFormat:@"shapeFrame_%d",i];
            [self addChild:node];
            
            [_shapePosArray addObject:[NSValue valueWithCGPoint:node.position]];
        }
        
        // 3 调用生成被选图形的接口,填充入这些位置节点。
        [self shapeFill];
    }
    
    

    (4)随机生成填充形状。

    上面(3)中代码的最后一步,在GameScene里面调用shapeFill方法来,填充三个备选容器。实际上是调用RandomShapeMgr.h中的RandomShapeMgr的单例对象,生成如图2.6所示的25种不同的待填入形状。重要的是将其编号,和比较队列写好,放入一个队列对象。RandomShapeMgr里的代码里面的posInfoInit方法可以研究下,节点的位置队列和比较队列如何生成好保存。

    游戏交互
    实际上SpriteKit里面的交互和iOS应用里的交互一脉相承。由于有点击,拖动图形,放下图形等操作,所以使用如下几个方法:

    a,-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
    b,- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
    c,- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;

    分别在点击开始,中途和结束时进行程序处理完成主要核心交互,如下三个功能:
    (1)点击形状,识别形状,并移动。
    点击形状,调用方法a。在方法a中,根据节点Name值,判断是否时需要保存处理的形状节点。如是的话,使用 _handleNode来持有。因为屏幕大小限制,平时待选的图形仅仅只有单元格大小的1/2,因此点击持有形状后,会执行动画,将待选形状扩大一倍。处理逻辑:

    if ([node.name isEqual:@"shape"]) {
    //            NSLog(@"shape");
                _handleNode = node;
                [_handleNode runAction:[SKAction scaleTo:2 duration:0.4] completion:^{
                }];
                
                break;
            }
    

    移动持有图形,调用方法b,如果_handleNode有值的话,实时更新_handleNode的位置Position值。

    (2)放置形状,并判断是否能够放置入棋盘。
    在方法c中,判定是否能够放入棋盘。就是判断图形里的节点是否能否放在它下面的那个单元格中。处理逻辑:

    if (_handleNode != nil) {
            
            // 1 获取放入形状的所有小六边形,并获取其颜色纹理texuture
            NSArray *handleShapeNodes = [_handleNode children];
            SKTexture *texture = [(SKSpriteNode *)[[_handleNode children] firstObject] texture];;
            
           // 2 遍历所有的小六边形,获取其位置,并察看该位置下是否存在Unit单元格,如果存在单元格,察看是否是被占状态。如果所有的小六边形下,都有未被占用的单元格,那么就可以放置在棋盘上了;反之,返回形状待选区域。
            NSUInteger index = 0;
            NSUInteger ocuppiedCount = 0;
            NSMutableArray *tempArray = [[NSMutableArray alloc] init];
            // 2.1 遍历小六边形
            for (SKSpriteNode *child in handleShapeNodes) {
                index++;
                // 2.2 获取小六边形位置
                CGPoint childLocation = CGPointMake(child.position.x*2 +_handleNode.position.x, child.position.y*2+_handleNode.position.y);
     
                // 2.3 获取该位置下的所有节点
                NSArray *shapeNodes = [self nodesAtPoint:childLocation];
     
               // 2.4 看该节点下,是否存在违背占用的单元格
                for (SKNode *shapeNode in shapeNodes) {
                    if ([self isShapeUnit:(SKSpriteNode *)shapeNode] && ![self isUnitOcuppied:(SKSpriteNode *)shapeNode]) {
                        ocuppiedCount++;
                        [tempArray addObject:shapeNode];
                    }
                }
            }
           
            // 2.5 如果所有六边形都用空白的Unit可以占,那么就可以放入。
            if ( index == ocuppiedCount ) {
                // 2.6 执行占用,并调用shapeFill补充shape
                for (SKSpriteNode *unitNode in tempArray) {
                    [unitNode setTexture:texture];
                    ShapeUnitInfo *unitInfo = [unitNode.userData objectForKey:@"unitInfo"];
                    unitInfo.occupy = YES;
    //                NSLog(@"set occupy");
                }
                [_shapeArray removeObject:_handleNode];
                
                [_handleNode removeFromParent];
                [self shapeFill];
            }
            else {
                // 2.7  否则就将shape移动回待选区域。
                NSUInteger index = [_shapeArray indexOfObject:_handleNode];
                CGPoint location = [(NSValue *)[_shapePosArray objectAtIndex:index] CGPointValue];
                SKAction *scale = [SKAction scaleTo:1 duration:0.3];
                SKAction *move = [SKAction moveTo:location duration:0.3];
                SKAction *group = [SKAction group:@[scale,move]];
                group.timingMode = SKActionTimingEaseOut;
                [_handleNode runAction:group];
            }
     
            
            _handleNode = nil;
    
    

    (3)消除判断,消除积分增加。
    在方法c中,还需要进行消除判断,填入形状后,是否会在横,左斜和右斜方向存在填满一行的情况,如果有就需要进行消除,并积分。

    // 检查消除并积分
     [self resultDealElimination];
    

    使用数组记录下Top和Bottom行的单元格编号,并记录每一行开头的单元格编号:

    // 1 每一行开头的单元格编号
        NSArray *compareIndexRow = @[@0,@5,@11,@18,@26,@35,@43,@50,@56];
     
       // 2 Top行所有元素的编号
        NSArray *compareIndexTopSlash = @[@0,@1,@2,@3,@4];
     
        // 3 Bottom行所有元素的编号
        NSArray *compareIndexBottomSlash = @[@56,@57,@58,@59,@60];
    

    涉及单元格如图:

    比较单元格.png

    比较方向如下图所示,1是横向,2是Top斜,3是Bottom斜:

    比较方向.png

    比较方向按六边形方向定义比较,具体见代码。

    (4)游戏结束判断
    将三个待填入的图形分别比较放在棋盘里进行

    // 调用检查是否能Continue
     [self checkContinue];
    

    比较方案如上述所描述,按照填入图形的比较序列,逐个对每个单元格进行比对,如果还存在可以填入的位置,游戏就可以继续,如果不存在,游戏就结束。核心比较代码:

    - (BOOL)isOccupByShape:(SKSpriteNode *)shapeNode atUnit:(SKSpriteNode *)unitNode
    {
       // 1 获取该形状Shape的比较序列
        NSArray *comSeqArray = (NSArray *)[shapeNode.userData objectForKey:@"shapeCompOrder"];
        
       // 2 以读入的单元格为起始比较单元格,按照比较序列进行比较。
        SKSpriteNode *tempNode = unitNode;
        ShapeUnitInfo *nodeInfo = (ShapeUnitInfo *)[tempNode.userData objectForKey:@"unitInfo"];
        if ([nodeInfo isOccupied]) {
            return YES;
        }
        
        for (NSNumber *index in comSeqArray) {
            
            NSInteger nodeIndex = [(NSNumber *)[nodeInfo.adjacentArray objectAtIndex:[index unsignedIntegerValue]] integerValue];
            if(-1 == nodeIndex) {
                return YES;
            }
            
            tempNode = (SKSpriteNode *)[_unitNodeArray objectAtIndex:nodeIndex];
            nodeInfo = (ShapeUnitInfo *)[tempNode.userData objectForKey:@"unitInfo"];
            if ([nodeInfo isOccupied]) {
                return YES;
            }
        }
        
        return NO;
    }
    
    

    三,游戏效果,何去何从。

    好了,从这里下载完整的Demo工程,在Xcode里面运行打开吧。执行效果文章开始所示。
    好了,其实还有很多的功能可以添加和细化。比如添加很多的动画效果,增加积分机制和加入社交化分享,广告条。很多功能可以添加,有兴趣的哥们,就在GitHub的Frvr项目,这个工程里面,好好加油,我顶你哦!

    相关文章

      网友评论

      • 捞月亮的猴子:这个应该是可以普通vc跳转到游戏吧?
      • 张小然:感谢分享了,正在做一个html5版本的,给了很好的思路。
      • 60ec62911b00:html5怎么做,有教程吗?

      本文标题:使用SpriteKit做一个Frvr游戏

      本文链接:https://www.haomeiwen.com/subject/mkvwlttx.html