使用SpriteKit做一个Frvr游戏

一,游戏怎么玩?

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

使用SpriteKit做一个Frvr游戏_第1张图片
frvr游戏

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

使用SpriteKit做一个Frvr游戏_第2张图片
frvr2

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

二,开始制作游戏之旅。

1,素材的准备。

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

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

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

使用SpriteKit做一个Frvr游戏_第3张图片
新建画布

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

使用SpriteKit做一个Frvr游戏_第4张图片
选择多边形

编辑多变形点个数

使用SpriteKit做一个Frvr游戏_第5张图片
编辑多边形点数

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

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

使用SpriteKit做一个Frvr游戏_第6张图片
六边形素材最终效果

线性渐变和圆形渐变

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

2,游戏玩法和规则。

游戏规则(百度词条)

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

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

使用SpriteKit做一个Frvr游戏_第7张图片
游戏界面

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

使用SpriteKit做一个Frvr游戏_第8张图片
待填入图形

(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;

其方向示意如图所示:

使用SpriteKit做一个Frvr游戏_第9张图片
六边形比较方向示意图

(2)要有顺序。

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

使用SpriteKit做一个Frvr游戏_第10张图片
比较顺序

“一”和“二”的两个图形里每个小六边形都有编号,其编号就是其比较顺序,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 *)touches withEvent:(UIEvent *)event;
c,- (void)touchesEnded:(NSSet *)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];

涉及单元格如图:

使用SpriteKit做一个Frvr游戏_第11张图片
比较单元格.png

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

使用SpriteKit做一个Frvr游戏_第12张图片
比较方向.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项目,这个工程里面,好好加油,我顶你哦!

你可能感兴趣的:(使用SpriteKit做一个Frvr游戏)