Swift实战之2048小游戏

上周在图书馆借了一本Swift语言实战入门,入个门玩一玩^_^正好这本书的后面有一个2048小游戏的实例,笔者跟着实战了一把。

差不多一周的时间,到今天,游戏的基本功能已基本实现,细节我已不打算继续完善,就这么整理一下过程中一些值得记录的点吧。

用的Swift版本是2.0,原书中的Swift版本会低一些,所以实践起来有些地方语法并不一样。

一、开始页面

 

在程序的第一张页面(Main.storyboard)上,只放了一个“开始游戏”按钮,点击按钮,弹出一个提示对话框,确认后,进入游戏页面。

1  @IBAction func startGame(sender: UIButton) {
2         let alerController = UIAlertController(title: "开始", message: "游戏就要开始,你准备好了吗?", preferredStyle: UIAlertControllerStyle.Alert)
3         alerController.addAction(UIAlertAction(title: "Ready Go!", style: UIAlertActionStyle.Default, handler: {
4             action in
5             self.presentViewController(MainTabViewController(), animated: true, completion: nil)
6         }))
7         self.presentViewController(alerController, animated: true, completion: nil)
8         
9     }

二、游戏页面

用一个TabViewController来实现,控制游戏页面(MainViewController)、设置页面(SettingViewController)和主题页面(KKColorListViewController)等三个页面的切换。 

 1 import UIKit
 2 
 3 class MainTabViewController: UITabBarController,KKColorListViewControllerDelegate {
 4     var viewMain = MainViewController()
 5     var viewColor = KKColorListViewController(schemeType:KKColorsSchemeType.Crayola)
 6     override func viewDidLoad() {
 7         super.viewDidLoad()
 8 
 9         // Do any additional setup after loading the view.
10         
11         viewMain.title = "2048"
12         let user=UserModel.sharedInstance().user
13         if let red = user?.red{
14             let uicolor=UIColor(red: red, green: (user?.green)!, blue: (user?.blue)!, alpha: (user?.alpha)!)
15             viewMain.view.backgroundColor=uicolor
16         }
17         
18         let viewSetting = SettingViewController()
19         viewSetting.title = "设置"
20         
21         viewColor.title="颜色"
22         viewColor.headerTitle="选择背景色"
23         viewColor.delegate=self
24         
25         let main = UINavigationController(rootViewController: viewMain)
26         let setting = UINavigationController(rootViewController: viewSetting)
27         let color = UINavigationController(rootViewController: viewColor)
28         
29         self.viewControllers = [main, setting,color]
30         self.selectedIndex = 0
31     }
32 
33     override func didReceiveMemoryWarning() {
34         super.didReceiveMemoryWarning()
35         // Dispose of any resources that can be recreated.
36     }
37     
38     func colorListController(controller: KKColorListViewController!, didSelectColor color: KKColor!) {
39         viewMain.view.backgroundColor = color.uiColor()
40         
41         UserModel.sharedInstance().saveColor(color.uiColor())
42         self.selectedIndex=0
43     }
44     
45     func colorListPickerDidComplete(controller: KKColorListViewController!) {
46         self.selectedIndex=0
47     }
48 
49 }

(一)主题页面

 其中,主题页面直接使用GitHub上的一个开源项目KKColorListViewController,选中颜色后,改变游戏页面的背景色。

这个项目可以从GitHub直接下载,但这个项目是用Objective-C写的,所以添加到Swift项目中后,需要新建一个Bridge头文件,这个头文件需要保存在项目文件夹的根目录下,而不是项目文件夹里面的源码文件夹(否则,可能需要自己配置头文件的目录)

1 #ifndef Bridging_Header_h
2 #define Bridging_Header_h
3 #import "KKColorListPicker.h"
4 
5 #endif /* Bridging_Header_h */

另外,添加到项目后,编译时还会有一些文件会报错,需要修改一些细节才能正常使用。

(1)KKColorsSchemeType.h中需添加

#import <Foundation/Foundation.h>

(2)KKColorListViewController中(void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath方法,将最后一句注释掉。否则每次选完颜色,程序就会关闭当前的MainTabViewController而回到开始游戏页面。

1 - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
2 {
3     KKColor *color = self.colors[indexPath.row];
4     if (self.delegate) {
5         [self.delegate colorListController:self didSelectColor:color];
6     }
7 //    [self dismissViewControllerAnimated:YES completion:nil];
8 }

(二)游戏页面

 

1、ScoreView

游戏页面,上方有两个自定义的UIView,用于动态显示游戏分数。

  

 1 import UIKit
 2 
 3 enum ScoreType{
 4     case Common
 5     case Best
 6 }
 7 
 8 protocol ScoreViewProtocol{
 9     func changeScore(value s:Int)
10 }
11 
12 class ScoreView: UIView, ScoreViewProtocol {
13 
14     var label:UILabel!
15     let defaultFrame = CGRectMake(0, 0, 100, 30)
16     var stype:String!
17     
18     var score:Int = 0 {
19         didSet{
20             label.text = "(stype):(score)"
21         }
22     }
23     
24     init(stype: ScoreType){
25         super.init(frame: defaultFrame)
26         self.stype = (stype == ScoreType.Common ? "分数":"最高分")
27         
28         backgroundColor = UIColor.orangeColor()
29         label = UILabel(frame: defaultFrame)
30         label.textAlignment = NSTextAlignment.Center
31         label.font = UIFont(name: "微软雅黑", size: 16)
32         label.textColor = UIColor.whiteColor()
33         
34         self.addSubview(label)
35         
36         //布局约束
37         //必须将该属性值设置为false,否则自己设置的约束和AutoresizingMask生成的约束有冲突,运行时会产生异常
38         self.translatesAutoresizingMaskIntoConstraints = false
39         //宽度约束
40         self.widthAnchor.constraintEqualToConstant(100).active=true
41         //高度约束
42         self.heightAnchor.constraintEqualToConstant(30).active=true
43     }
44 
45     required init?(coder aDecoder: NSCoder) {
46         super.init(coder: aDecoder)
47     }
48     
49     func changeScore(value s: Int) {
50         self.score = s
51     }
52 
53 }

上面的ScoreView首先由一个ScoreType来选择显示“分数”还是“最高分”,然后有一个changeScore的方法,可以改变Score属性值,改变该值得时候同时改变Label显示的数字。

值得一提的是,在该UIView中,添加了布局约束,方便我们把该UIView添加到页面时控制它的布局。在iOS9.0中,多了一个NSLayoutAnchor类,用它来完成布局约束,比原来低版本用的NSLayoutConstraint要更方便一些。

2、按钮

 

游戏页面下方是两个按钮,重置清空本次游戏的数字,生成则产生一个数字,这两个按钮主要用于调试。

按钮是在一个ViewFactory的工厂类中生产的,同样生产时,添加了一些布局约束。

 1 class func createButton(title:String,action:Selector,sender:UIViewController) -> UIButton{
 2         let button = UIButton(frame: CGRectZero)
 3         button.backgroundColor=UIColor.orangeColor()
 4         button.setTitle(title, forState: .Normal)
 5         button.titleLabel!.textColor=UIColor.whiteColor()
 6         button.titleLabel!.font=UIFont.systemFontOfSize(14)
 7         
 8         //布局约束
 9         button.translatesAutoresizingMaskIntoConstraints = false
10         button.widthAnchor.constraintEqualToConstant(100).active=true
11         button.heightAnchor.constraintEqualToConstant(30).active=true
12         
13         button.addTarget(sender, action: action, forControlEvents: UIControlEvents.TouchUpInside)
14         return button
15     }

3、游戏区域(游戏地图)

一个5X5的矩阵,首先在所有位置上放置灰色的方块UIView。

 1 var backgrounds:Array<UIView>! //所有方块的背景
 2    func setupGameMap(){
 3         let margins = self.view.layoutMarginsGuide
 4         
 5         for row in 0..<self.dimension {
 6             for col in 0..<self.dimension {
 7                 //放置灰色的方块在对应的矩阵位置上
 8                 let background = UIView(frame: CGRectMake(0, 0, self.width, self.width))
 9                 background.backgroundColor = UIColor.darkGrayColor()
10                 background.translatesAutoresizingMaskIntoConstraints = false
11                 background.widthAnchor.constraintEqualToConstant(self.width).active = true
12                 background.heightAnchor.constraintEqualToConstant(self.width).active = true
13                 self.view.addSubview(background)
14                 self.backgrounds.append(background)
15                 
16                 //布局约束
17                 background.translatesAutoresizingMaskIntoConstraints=false
18                 background.widthAnchor.constraintEqualToConstant(self.width).active=true
19                 background.heightAnchor.constraintEqualToConstant(self.width).active=true
20                 
21                 //用代码进行布局约束
22                 var centerXConstant:CGFloat
23                 var centerYConstant:CGFloat
24                 if self.dimension%2 == 1 {
25                     centerXConstant = (self.padding+self.width) * CGFloat(col-self.dimension/2)
26                     centerYConstant = (self.padding+self.width) * CGFloat(row-self.dimension/2)
27                 }else{
28                     centerXConstant = (self.padding+self.width) * CGFloat(Double(col-self.dimension/2)+0.5)
29                     centerYConstant = (self.padding+self.width) * CGFloat(Double(row-self.dimension/2)+0.5)
30                 }
31                 
32                 background.centerXAnchor.constraintEqualToAnchor(margins.centerXAnchor, constant: centerXConstant).active=true
33                 background.centerYAnchor.constraintEqualToAnchor(margins.centerYAnchor, constant: centerYConstant).active=true
34 
35             }
36         }
37     }

然后,添加数字时,再在相应位置上放置数字方块TileView。

 1 var tiles = [NSIndexPath:TileView]() //存储当前的有数字的方块
 2 
 3 //插入一个数字方块
 4     func insertTile(pos:(Int,Int),value:Int){
 5         let (row,col)=pos
 6         
 7         let x=50 + CGFloat(col) * (self.width+self.padding)
 8         let y=150 + CGFloat(row) * (self.width+self.padding)
 9         
10         let tile=TileView(pos:CGPointMake(x,y),self.width,value:value)
11       
12         self.view.addSubview(tile)
13         self.view.bringSubviewToFront(tile)
14         
15         let index = NSIndexPath(forRow: row, inSection: col)
16         
17         tiles[index] = tile
18         
19         //布局约束
20         var centerXConstant:CGFloat
21         var centerYConstant:CGFloat
22         if self.dimension%2 == 1 {
23             centerXConstant = (self.padding+self.width) * CGFloat(col-self.dimension/2)
24             centerYConstant = (self.padding+self.width) * CGFloat(row-self.dimension/2)
25         }else{
26             centerXConstant = (self.padding+self.width) * CGFloat(Double(col-self.dimension/2)+0.5)
27             centerYConstant = (self.padding+self.width) * CGFloat(Double(row-self.dimension/2)+0.5)
28         }
29         let margins=self.view.layoutMarginsGuide
30         tile.centerXAnchor.constraintEqualToAnchor(margins.centerXAnchor, constant: centerXConstant).active=true
31         tile.centerYAnchor.constraintEqualToAnchor(margins.centerYAnchor, constant: centerYConstant).active=true
32     }

上面用了一个数字tiles来保存插入的TileView,以便清除数字时,可以把对应的TileView从界面中移除。

1     //移除一个数字方块
2     func clearTile(row:Int,col:Int){
3         
4         let index=NSIndexPath(forRow: row, inSection: col)
5         let tile=tiles[index]!
6         tile.removeFromSuperview()
7         tiles.removeValueForKey(index)
8         
9     }

TileView的实现代码如下:

 1 import Foundation
 2 import UIKit
 3 
 4 
 5 class TileView : UIView {
 6     let colorMap=[
 7         2:UIColor.redColor(),
 8         4:UIColor.orangeColor(),
 9         8:UIColor.lightTextColor(),
10         16:UIColor.greenColor(),
11         32:UIColor.brownColor(),
12         64:UIColor.blackColor(),
13         128:UIColor.purpleColor(),
14         256:UIColor.lightGrayColor(),
15         512:UIColor.cyanColor(),
16         1024:UIColor.magentaColor(),
17         2048:UIColor.blackColor()
18     ]
19     
20     var value:Int{
21         didSet{
22             backgroundColor=colorMap[value]
23             numberLabel.text="(value)"
24         }
25     }
26     
27     var numberLabel:UILabel!
28     
29     init(pos:CGPoint,CGFloat,value:Int){
30         self.value=value
31         
32         numberLabel=UILabel(frame: CGRectMake(0, 0, width, width))
33         numberLabel.textColor=UIColor.whiteColor()
34         numberLabel.textAlignment=NSTextAlignment.Center
35         numberLabel.minimumScaleFactor=0.5
36         numberLabel.font=UIFont(name: "微软雅黑", size: 20)
37         numberLabel.text="(value)"
38         
39         super.init(frame: CGRectMake(pos.x, pos.y,width , width))
40         addSubview(numberLabel)
41         backgroundColor=colorMap[value]
42         
43         //代码约束
44         self.translatesAutoresizingMaskIntoConstraints = false
45         self.widthAnchor.constraintEqualToConstant(width).active=true
46         self.heightAnchor.constraintEqualToConstant(width).active=true
47         
48     }
49 
50     required init?(coder aDecoder: NSCoder) {
51         self.value = 2
52         super.init(coder: aDecoder)
53     }
54     
55     
56 }
View Code

4、游戏模型

(1)游戏数据存储

采用一个自定义的矩阵数据结构,存储游戏数据。

//自定义矩阵,对应2048的游戏面板
struct Matrix {
    let rows:Int,columns:Int
    var grid:[Int]
    init(rows:Int,columns:Int){
        self.rows=rows
        self.columns=columns
        grid = Array<Int>(count: rows * columns, repeatedValue: 0)
    }
    
    func indexIsValidForRow(row:Int,column:Int)->Bool{
        return (row >= 0) && (row < rows) && (column >= 0) && (column < columns)
    }
    
    subscript(row:Int,column:Int)->Int{
        get{
            assert(indexIsValidForRow(row, column: column),"超出范围")
            return grid[(row * columns)+column]
        }
        set{
            assert(indexIsValidForRow(row, column: column),"超出范围")
            grid[(row * columns)+column]=newValue
        }
    }
    
//    相等函数,判断两个Matrix是否相等
    func isEqualTo(matrix:Matrix)->Bool{
        if rows != matrix.rows{
            return false
        }
        if columns != matrix.columns{
            return false
        }
        for i in 0..<rows{
            for j in 0..<columns{
                if self[i,j] != matrix[i,j]{
                    return false
                }
            }
        }
        return true
    }
}

游戏模型中,包含tiles和维度两个属性,用维度来对tiles这个Matrix进行初始化,同时,提供一些方法。

 1 class GameModel{
 2     var dimension:Int = 0{
 3         didSet{
 4             self.tiles = Matrix(rows: self.dimension, columns: self.dimension)
 5         }
 6     }
 7     var tiles:Matrix    
 8     init(dimension:Int){
 9         self.dimension=dimension
10         self.tiles = Matrix(rows: self.dimension, columns: self.dimension)
11     }
12     
13     func emptyPosition()->[Int]{
14         var emptytiles=Array<Int>()
15         for row in 0..<self.dimension{
16             for col in 0..<self.dimension{
17                 let val=tiles[row,col]
18                 if val==0 {
19                     emptytiles.append(tiles[row,col])
20                 }
21             }
22             
23         }
24         return emptytiles
25     }
26     
27     func isFull()->Bool{
28         if emptyPosition().count==0 {
29             return true
30         }
31         return false
32     }
33 //    打印矩阵,调试时使用
34     func printTiles(){
35         for i in 0..<self.dimension{
36             print(tiles.grid[(i*self.dimension)...((i+1)*self.dimension-1)])
37         }
38     }
39     
40 // 设置某个位置的值
41     func setPosition(row:Int,col:Int,value:Int)->Bool{
42         assert(row>=0 && row<dimension)
43         assert(col>=0 && col<dimension)
44         
45         print("值更新之前:")
46         printTiles()
47         
48         tiles[row,col]=value
49         
50         print("值更新之后:tiles[(row),(col)]=(tiles[row,col])")
51         printTiles()
52         return true
53     }
54 // 清除数据
55     func clearAll(){
56         tiles=Matrix(rows: self.dimension, columns: self.dimension)
57     }
58     
59 }
(2)游戏数据变更

 产生一个数字:

 1     //生成新的数字,生成按钮的响应方法
 2     func genNumber(){
 3 ////        随机产生数字2和4,几率为1:4
 4 //        let randv=Int(arc4random_uniform(5))
 5 //        var seed:Int = 2
 6 //        if randv==1 {
 7 //            seed = 4
 8 //        }
 9 
10         let col=Int(arc4random_uniform(UInt32(dimension)))
11         let row=Int(arc4random_uniform(UInt32(dimension)))
12         
13         if gameModel.isFull(){
14             print("位置已经满了")
15             return
16         }
17         
18         if gameModel.tiles[row, col]>0 {
19             genNumber()
20             return
21         }
22         
23         let seed=2 //原书的程序,按照一定的几率比来生成2或4。此处改成一直生成2.
24         gameModel.setPosition(row, col: col, value: seed)
25         insertTile((row,col), value: seed)
26         
27         // 生成数字后,判断一下游戏是否结束
28         checkGameOver()
29     }

每次生成一个数字后,判断一下游戏是否结束。如果矩阵的所有位置都有数字,并且相邻位置的数字都不相同,则本轮游戏结束,弹出一个提示框,重新开始游戏。

 1     //检查游戏是否结束
 2     func checkGameOver(){
 3         if self.gameModel.isFull(){
 4             for row in 0..<self.dimension{
 5                 for col in 0..<self.dimension{
 6                     let val = gameModel.tiles[row,col]
 7                     let left:Int? = (col-1)>=0 ? gameModel.tiles[row,col-1] : nil
 8                     let right:Int? = (col+1)<self.dimension ? gameModel.tiles[row,col+1] : nil
 9                     let up:Int? = (row-1)>=0 ? gameModel.tiles[row-1,col] : nil
10                     let down:Int? = (row+1)<self.dimension ? gameModel.tiles[row+1,col] : nil
11                     if (val==left) || (val==right) || (val==up) || (val==down){
12                         return
13                     }
14                     
15                 }
16             }
17             
18             let alerController = UIAlertController(title: "游戏结束", message: "本轮游戏结束,重新开始吧!", preferredStyle: UIAlertControllerStyle.Alert)
19             alerController.addAction(UIAlertAction(title: "Ready Go!", style: UIAlertActionStyle.Default, handler: {
20                 action in
21                 self.resetGameMap()
22             }))
23             self.presentViewController(alerController, animated: true, completion: nil)
24             return
25         }
26     }

5、游戏效果实现

 

首先,是添加手势识别器,相当于对手势处理方法进行注册。

 1     //添加滑动的手势识别处理
 2     func setupSwipeGestures(){
 3         let upSwipe=UISwipeGestureRecognizer(target: self, action: Selector("swipeUp"))
 4         upSwipe.numberOfTouchesRequired=1
 5         upSwipe.direction=UISwipeGestureRecognizerDirection.Up
 6         self.view.addGestureRecognizer(upSwipe)
 7         
 8         let downSwipe=UISwipeGestureRecognizer(target: self, action: Selector("swipeDown"))
 9         downSwipe.numberOfTouchesRequired=1
10         downSwipe.direction=UISwipeGestureRecognizerDirection.Down
11         self.view.addGestureRecognizer(downSwipe)
12         
13         let leftSwipe=UISwipeGestureRecognizer(target: self, action: Selector("swipeLeft"))
14         leftSwipe.numberOfTouchesRequired=1
15         leftSwipe.direction=UISwipeGestureRecognizerDirection.Left
16         self.view.addGestureRecognizer(leftSwipe)
17         
18         let rightSwipe=UISwipeGestureRecognizer(target: self, action: Selector("swipeRight"))
19         rightSwipe.numberOfTouchesRequired=1
20         rightSwipe.direction=UISwipeGestureRecognizerDirection.Right
21         self.view.addGestureRecognizer(rightSwipe)
22         
23     }

然后,就是具体的处理方法,上下左右四个方向的滑动,会引起对应的方向上的数字重排和合并。原书中的处理是先将数字重排,然后进行合并。这里采用自己的逻辑方法,用递归的方式,对于值为0的位置,向后检索不为0的数字放入其中,并考虑是否要和相邻位置进行合并。

 1 var tileVals:Matrix //合并数字时所用的游戏数据复本
 2 
 3     //向上滑动
 4     func swipeUp(){
 5         tileVals = gameModel.tiles
 6         
 7         func merge(row row:Int,col:Int){
 8             if row == self.dimension-1{
 9                 return
10             }
11             
12             for i in (row+1)..<self.dimension {
13                 let valNew = tileVals[i,col]
14                 if valNew>0 {
15                     let val = tileVals[row, col]
16                     
17                     if val == 0 {
18                         tileVals[row, col] = valNew
19                         tileVals[i, col] = 0
20                         merge(row: row, col: col)
21                     }else if val == valNew{
22                         tileVals[row, col] = valNew<<1
23                         tileVals[i, col] = 0
24                         if row==0 {
25                             merge(row: row, col: col)
26                         }else{
27                             merge(row: row-1, col: col)
28                         }
29 //                        若产生合并,则加分
30                         changeScore(valNew)
31                     }else{
32                         merge(row: row+1, col: col)
33                     }
34                     
35                     break
36                 }
37             }
38         }
39         
40         for col in 0..<self.dimension{
41             merge(row: 0, col: col)
42         }
43         
44 //        如果合并后的结果与原来相同,则不做任何操作
45         if tileVals.isEqualTo(gameModel.tiles){
46             return
47         }
48 //        显示合并后的结果,并产生新的数字
49         refresh()
50         genNumber()
51     }

如果过程中产生合并,则进行加分。

1     //加分
2     func changeScore(baseNum:Int){
3         self.scoreView.score += baseNum * 2
4         
5     }

处理时,用了一个临时的Matrix变量tileVals来作处理,以便与未处理的数据进行比较,决定是否要进行后续的操作。如果处理后的数据与处理前不一致,则需要刷新页面中的游戏数据。

 1     //刷新页面
 2     func refresh(){
 3         for i in 0..<self.dimension{
 4             for j in 0..<self.dimension{
 5                 let val = gameModel.tiles[i,j]
 6                 let valNew = tileVals[i,j]
 7                 if valNew != val{
 8                     gameModel.setPosition(i, col: j, value: valNew)
 9                     if valNew>0{
10                         if val>0{
11                             clearTile(i, col: j)
12                         }
13                         insertTile((i,j) , value: valNew)
14                         
15                     }else{
16                         clearTile(i, col: j)
17                     }
18                 }
19                 
20             }
21         }
22     }

其他三个方向上的处理方法类似:

  1     //向下滑动
  2     
  3     func swipeDown(){
  4         tileVals = gameModel.tiles
  5         
  6         func merge(row row:Int,col:Int){
  7             if row == 0{
  8                 return
  9             }
 10             
 11             for var i=row-1;i>=0;i-- {
 12                 let valNew=tileVals[i,col]
 13                 if valNew>0 {
 14                     let val=tileVals[row,col]
 15                     
 16                     if val == 0 {
 17                         tileVals[row, col] = valNew
 18                         tileVals[i, col] = 0
 19                         merge(row: row, col: col)
 20                     }else if val == valNew{
 21                         tileVals[row, col] = valNew<<1
 22                         tileVals[i, col] = 0
 23 
 24                         if row==self.dimension-1 {
 25                             merge(row: row, col: col)
 26                         }else{
 27                             merge(row: row+1, col: col)
 28                         }
 29 //                        若产生合并,则加分
 30                         changeScore(valNew)
 31                     }else{
 32                         merge(row: row-1, col: col)
 33                     }
 34                     
 35                     break
 36                 }
 37             }
 38         }
 39         
 40         for col in 0..<self.dimension{
 41             merge(row: self.dimension-1, col: col)
 42         }
 43 //        如果合并后的结果与原来相同,则不做任何操作
 44         if tileVals.isEqualTo(gameModel.tiles){
 45             return
 46         }
 47         
 48         refresh()
 49         genNumber()
 50     }
 51     
 52     //向左滑动
 53     func swipeLeft(){
 54         tileVals = gameModel.tiles
 55         func merge(row row:Int,col:Int){
 56             if col == self.dimension-1{
 57                 return
 58             }
 59             
 60             for i in (col+1)..<self.dimension {
 61                 let valNew=tileVals[row,i]
 62                 if valNew>0 {
 63                     let val=tileVals[row,col]
 64                     
 65                     if val == 0 {
 66                         tileVals[row, col] = valNew
 67                         tileVals[row, i] = 0
 68                         merge(row: row, col: col)
 69                     }else if val == valNew{
 70                         tileVals[row, col] = valNew<<1
 71                         tileVals[row, i] = 0
 72                         if col==0 {
 73                             merge(row: row, col: col)
 74                         }else{
 75                             merge(row: row, col: col-1)
 76                         }
 77 //                        若产生合并,则加分
 78                         changeScore(valNew)
 79                     }else{
 80                         merge(row: row, col: col+1)
 81                     }
 82                     
 83                     break
 84                 }
 85             }
 86         }
 87         
 88         for row in 0..<self.dimension{
 89             merge(row: row, col: 0)
 90         }
 91         
 92 //        如果合并后的结果与原来相同,则不做任何操作
 93         if tileVals.isEqualTo(gameModel.tiles){
 94             return
 95         }
 96         refresh()
 97         genNumber()
 98     }
 99     
100     //向右滑动
101     func swipeRight(){
102         tileVals = gameModel.tiles
103         func merge(row row:Int,col:Int){
104             if col == 0{
105                 return
106             }
107             
108             for var i=col-1;i>=0;i-- {
109                 let valNew=tileVals[row, i]
110                 if valNew>0 {
111                     let val=tileVals[row, col]
112                     
113                     if val == 0 {
114                         tileVals[row, col] = valNew
115                         tileVals[row, i] = 0
116                         merge(row: row, col: col)
117                     }else if val == valNew{
118                         tileVals[row, col] = valNew<<1
119                         tileVals[row, i] = 0
120                         if col==self.dimension-1 {
121                             merge(row: row, col: col)
122                         }else{
123                             merge(row: row, col: col+1)
124                         }
125 //                        若产生合并,则加分
126                         changeScore(valNew)
127                     }else{
128                         merge(row: row, col: col-1)
129                     }
130                     
131                     break
132                 }
133             }
134         }
135         
136         for row in 0..<self.dimension{
137             merge(row: row, col: self.dimension-1)
138         }
139         
140 //        如果合并后的结果与原来相同,则不做任何操作
141         if tileVals.isEqualTo(gameModel.tiles){
142             return
143         }
144         refresh()
145         genNumber()
146         
147     }
View Code

(三)设置页面

设置页面可以设置游戏矩阵的维度。上面的阈值,游戏中数字超过该阈值可以视作通关,不过这个功能并没有实现,因为到后面对这个功能无甚兴趣~(≧▽≦)/~。

上面的几个控件都是在ViewFactory生产的,大多是一些常用控件,UISegmentedControl我倒是第一次用到,功能上有点像RadioButton,只是外观不一样。

ViewFactory的代码如下:

 1 import UIKit
 2 
 3 class ViewFactory {
 4     class func getDefaultFrame() -> CGRect {
 5         let defaultFrame = CGRectMake(0, 0, 100, 30)
 6         return defaultFrame
 7     }
 8     
 9     class func createControl(type:String, title:[String],action:Selector,sender:AnyObject)->UIView{
10         switch(type){
11             case "label": return ViewFactory.createLabel(title[0])
12             case "button": return ViewFactory.createButton(title[0], action: action, sender: sender as! UIViewController)
13             case "text":return ViewFactory.createTextField(title[0], action: action, sender: sender as! UITextFieldDelegate)
14             case "segment":return ViewFactory.createSegment(title, action: action, sender: sender as! UIViewController)
15         default: return ViewFactory.createLabel(title[0])
16         }
17     }
18     
19     class func createLabel(title:String) -> UILabel{
20         let label = UILabel()
21         label.textColor = UIColor.blackColor()
22         label.backgroundColor = UIColor.whiteColor()
23         label.text = title
24         label.frame = CGRectZero
25         label.font = UIFont(name: "HelveticaNeue-Bold", size: 16)
26         label.translatesAutoresizingMaskIntoConstraints = false
27         
28         return label
29     }
30     
31     class func createButton(title:String,action:Selector,sender:UIViewController) -> UIButton{
32         let button = UIButton(frame: CGRectZero)
33         button.backgroundColor=UIColor.orangeColor()
34         button.setTitle(title, forState: .Normal)
35         button.titleLabel!.textColor=UIColor.whiteColor()
36         button.titleLabel!.font=UIFont.systemFontOfSize(14)
37         
38         //布局约束
39         button.translatesAutoresizingMaskIntoConstraints = false
40         button.widthAnchor.constraintEqualToConstant(100).active=true
41         button.heightAnchor.constraintEqualToConstant(30).active=true
42         
43         button.addTarget(sender, action: action, forControlEvents: UIControlEvents.TouchUpInside)
44         return button
45     }
46     
47     class func createTextField(value:String,action:Selector,sender:UITextFieldDelegate) -> UITextField{
48         let textField=UITextField(frame: ViewFactory.getDefaultFrame())
49         textField.backgroundColor=UIColor.clearColor()
50         textField.textColor=UIColor.blackColor()
51         textField.text=value
52         textField.borderStyle=UITextBorderStyle.RoundedRect
53         textField.adjustsFontSizeToFitWidth=true
54         textField.delegate=sender
55         
56         textField.translatesAutoresizingMaskIntoConstraints = false
57         return textField
58     }
59     
60     class func createSegment(items:[String],action:Selector,sender:UIViewController)->UISegmentedControl {
61         let segment=UISegmentedControl(items: items)
62         segment.frame=ViewFactory.getDefaultFrame()
63         segment.momentary=false
64         segment.addTarget(sender, action: action, forControlEvents: UIControlEvents.ValueChanged)
65         
66         segment.translatesAutoresizingMaskIntoConstraints = false
67         return segment
68         
69     }
70 }
View Code

SettingViewController的实现:

 1 import UIKit
 2 
 3 class SettingViewController: UIViewController,UITextFieldDelegate {
 4     init(){
 5         super.init(nibName: nil, bundle: nil)
 6     }
 7 
 8     required init?(coder aDecoder: NSCoder) {
 9         super.init(coder: aDecoder)
10     }
11     
12     var main:MainViewController?
13     override func viewDidLoad() {
14         super.viewDidLoad()
15         self.view.backgroundColor=UIColor.whiteColor()
16         
17         let views = (self.parentViewController?.parentViewController as! MainTabViewController).viewControllers
18         main = ((views?[0] as? UINavigationController)?.childViewControllers)?[0] as? MainViewController
19         setupControls()
20         // Do any additional setup after loading the view.
21     }
22 
23     override func didReceiveMemoryWarning() {
24         super.didReceiveMemoryWarning()
25         // Dispose of any resources that can be recreated.
26     }
27     
28     var segDimension:UISegmentedControl?
29     func setupControls(){
30         let segVals = [3,4,5]
31         let textNum = ViewFactory.createTextField("", action: Selector("numChanged:"), sender: self)
32         textNum.returnKeyType=UIReturnKeyType.Done
33         
34         self.view.addSubview(textNum)
35         
36         let labelNum = ViewFactory.createLabel("阈值:")
37         self.view.addSubview(labelNum)
38         
39         segDimension = ViewFactory.createSegment(["3X3","4X4","5X5"], action: "dimensionChanged:", sender: self)
40         self.view.addSubview(segDimension!)
41         segDimension!.selectedSegmentIndex=segVals.indexOf((main?.dimension)!)!
42         
43         let labelDm=ViewFactory.createLabel("维度:")
44         self.view.addSubview(labelDm)
45         
46         //布局约束
47         let margins=self.view.layoutMarginsGuide
48         labelNum.widthAnchor.constraintEqualToConstant(60).active=true
49         labelNum.heightAnchor.constraintEqualToConstant(30).active=true
50         labelNum.leadingAnchor.constraintEqualToAnchor(margins.leadingAnchor, constant: 20).active=true
51         labelNum.topAnchor.constraintEqualToAnchor(margins.topAnchor, constant: 100).active=true
52         
53         textNum.widthAnchor.constraintEqualToConstant(200).active=true
54         textNum.heightAnchor.constraintEqualToConstant(30).active=true
55         textNum.leadingAnchor.constraintEqualToAnchor(labelNum.trailingAnchor).active=true
56         textNum.topAnchor.constraintEqualToAnchor(labelNum.topAnchor).active=true
57         
58         labelDm.widthAnchor.constraintEqualToConstant(60).active=true
59         labelDm.heightAnchor.constraintEqualToConstant(30).active=true
60         labelDm.leadingAnchor.constraintEqualToAnchor(labelNum.leadingAnchor).active=true
61         labelDm.topAnchor.constraintEqualToAnchor(textNum.topAnchor, constant: 100).active=true
62         
63         segDimension?.widthAnchor.constraintEqualToConstant(200).active=true
64         segDimension?.heightAnchor.constraintEqualToConstant(30).active=true
65         segDimension?.leadingAnchor.constraintEqualToAnchor(labelDm.trailingAnchor).active=true
66         segDimension?.topAnchor.constraintEqualToAnchor(labelDm.topAnchor).active=true
67         
68         
69     }
70     
71     //
72     func numChanged(textField:UITextField)->Bool{
73         textField.resignFirstResponder()
74         //改变游戏页面
75         if (textField.text != "(main?.maxnumber)"){
76             let num = Int(textField.text!)
77             main?.maxnumber = num!
78             
79         }
80         //存储到本地数据库
81         UserModel.sharedInstance().saveMaxnum((main?.maxnumber)!)
82         return true
83     }
84     
85     func dimensionChanged(sender:SettingViewController){
86         let segVals = [3,4,5]
87         //改变游戏页面
88         main?.dimension = segVals[segDimension!.selectedSegmentIndex]
89         main?.resetGameMap()
90         //存储到本地数据库
91         UserModel.sharedInstance().saveDimesion((main?.dimension)!)
92     }
93 
94 }

设置页面不仅要对游戏页面的刷新,同时也对本地数据库的游戏参数进行变更。

1、游戏界面刷新

 1     //游戏面板重绘
 2     func resetGameMap(){
 3         UserModel.sharedInstance().saveBestScore(self.scoreView.score)
 4         for view in self.view.subviews{
 5             view.removeFromSuperview()
 6         }
 7         backgrounds.removeAll()
 8         tiles.removeAll()
 9         gameModel.clearAll()
10         gameModel.dimension=self.dimension
11         setupScoreLables()
12         setupGameMap()
13         setupButton()
14         genNumber()
15         
16     }

刷新界面时,先保存当前分数,然后再对界面进行重绘。

2、游戏参数保存

原书使用SQLite作为本地数据库,笔者个人觉得SQLite的使用挺麻烦的(偶是个没有用过SQLite的菜鸟O(∩_∩)O~),无意间找到了一个相对使用更为方便的数据库Realm。

官网地址:https://realm.io,在首页上就可以下载Objective-C、Swift或Android等三个版本的Realm。首页下方有Realm的介绍,标题就是这么一句:

Realm is a replacement for SQLite & Core Data

Realm的中文资料不是很多,但因为其使用方便,现有资料也已经足够。这里有一篇关于Realm的教程:Realm数据库基础教程,具体配置可以参考,不过由于版本差异可能会有些不一样的地方。

笔者下载的版本是realm-swift-0.95.2,下载后自动解压。

(1)在iOS/swift-2.0中找到Realm.framework和RealmSwift.framework,把它们拖到项目中,一定要确保勾选了 Copy Items if needed 选项。

(2)将链接库配置如下:

下面是基于Realm的数据模型。UserMode使用单例模式保证数据库的唯一性,并提供方法来对Realm数据库中的user的属性值进行改写。

  1 import Foundation
  2 import UIKit
  3 import RealmSwift
  4 import Realm
  5 
  6 // 继承Object的用户数据模型
  7 // 属性包括用户id、游戏维度、通过数值、背景颜色参数、不同维度对应的最高分数
  8 class UserObject:Object{
  9     // 所有属性必须明确数据类型并且初始化
 10     dynamic var userid:String = ""
 11     dynamic var dimension:Int = 0
 12     dynamic var maxnum:Int = 0
 13     
 14     dynamic var red:CGFloat = 0.0
 15     dynamic var green:CGFloat = 0.0
 16     dynamic var blue:CGFloat = 0.0
 17     dynamic var alpha:CGFloat = 0.0
 18     
 19     dynamic var bestScoreForD3:Int = 0
 20     dynamic var bestScoreForD4:Int = 0
 21     dynamic var bestScoreForD5:Int = 0
 22 
 23     // 自定义的初始化方法
 24     init(userid:String,dimension:Int,maxnum:Int,backgroundColor:UIColor){
 25         
 26         let cicolor = CIColor(color:backgroundColor)
 27         red = cicolor.red
 28         green = cicolor.green
 29         blue = cicolor.blue
 30         alpha = cicolor.alpha
 31         
 32         self.dimension = dimension
 33         self.maxnum = maxnum
 34         self.userid = userid
 35         
 36         super.init()
 37     }
 38 
 39     required init() {
 40         super.init()
 41     }
 42     
 43     override init(realm: RLMRealm, schema: RLMObjectSchema) {
 44         super.init(realm: realm, schema: schema)
 45     }
 46     
 47     // 将userid作为primaryKey,这个功能不是必要的,因为userid都是同一个。
 48     // 只是看了Realm的文档后,试着添加进来而已
 49     override static func primaryKey() -> String?{
 50         return "userid"
 51     }
 52 }
 53 
 54                  
 55 class UserModel
 56 {
 57     // 产生userid
 58     class func get_uuid()->String{
 59         let userid = NSUserDefaults.standardUserDefaults().stringForKey("swift2048user")
 60         if userid == nil {
 61             let uuid_ref=CFUUIDCreate(nil)
 62             let uuid_string_ref=CFUUIDCreateString(nil, uuid_ref)
 63             let uuid:String = uuid_string_ref as String
 64             NSUserDefaults.standardUserDefaults().setObject(uuid, forKey: "swift2048user")
 65             return uuid
 66         }
 67         return userid!
 68     }
 69     
 70     var realm:Realm!
 71     let userid:String
 72     var user:UserObject?
 73     required init(){
 74         //------ Realm 配置 --------------
 75         // 这里涉及到Realm的Migrations。
 76         // 因为我对UserObject的架构进行更改过,必须配置,才能使数据库中的旧的UserObject更新。
 77         var config = Realm.Configuration()
 78         
 79         // schemaVersion,这可以看做UserObject的版本
 80         // 原始值为0,这是第二次更改,所以要赋值为2
 81         config.schemaVersion = 2
 82         config.migrationBlock = {
 83             migration,oldSchemaVersion in
 84             if oldSchemaVersion < 2{
 85                 
 86             }
 87         }
 88         Realm.Configuration.defaultConfiguration = config
 89         //------ Realm 配置 Over --------------
 90         
 91         
 92         realm = try! Realm()
 93         userid = UserModel.get_uuid()
 94         let objects = realm.objects(UserObject).filter("userid == '(userid)'") //检索对象
 95         //如果用户不存在
 96         if objects.count == 0 {
 97             realm.beginWrite()
 98             //根据默认的数据创建游戏
 99             user = UserObject(userid: userid, dimension: 3, maxnum: 0, backgroundColor: UIColor.darkGrayColor())
100             realm.add(user!)
101             realm.commitWrite()
102         }else{
103             user = objects.first
104         }
105     }
106     
107 //    单例模式
108     struct Static {
109         static var instance:UserModel? = nil
110         static var token: dispatch_once_t = 0
111     }
112     class func sharedInstance()->UserModel {
113         dispatch_once(&Static.token){
114             print("UserModel - Dispatch once")
115             Static.instance = self.init()
116         }
117         return Static.instance!
118     }
119     
120 //    返回属性值,调试可用
121     func getUserdata()->[String:String]{
122         let dic:[String:String] = ["maxnum":"(self.user?.maxnum)",
123             "dimension":"(self.user?.dimension)",
124             "red":"(self.user?.red)",
125             "green":"(self.user?.green)",
126             "blue":"(self.user?.blue)",
127             "alpha":"(self.user?.alpha)"]
128         return dic
129         
130     }
131     
132 //    更改属性值
133     func saveDimesion(dimension:Int){
134         realm.write({
135             self.user?.dimension = dimension
136         })
137         
138     }
139     
140     func saveMaxnum(maxnum:Int){
141         realm.write({
142             self.user?.maxnum = maxnum
143         })
144     }
145     
146     func saveColor(backgroundColor:UIColor){
147         realm.write({
148             let cicolor = CIColor(color: backgroundColor)
149             self.user?.red = cicolor.red
150             self.user?.green = cicolor.green
151             self.user?.blue = cicolor.alpha
152         })
153     }
154     
155     func saveBestScore(score:Int){
156         realm.write({
157             if (self.user?.dimension==3) && (score>self.user?.bestScoreForD3){
158                 self.user?.bestScoreForD3 = score
159                 return
160             }
161             
162             if (self.user?.dimension==4) && (score>self.user?.bestScoreForD4){
163                 self.user?.bestScoreForD4 = score
164                 return
165             }
166             
167             if (self.user?.dimension==5) && (score>self.user?.bestScoreForD5){
168                 self.user?.bestScoreForD5 = score
169                 return
170             }
171         })
172     }
173 }

 至此,整个项目的完成过程介绍完毕。

关于游戏成品:功能不算丰富,细节不算完美,个别地方只是在原书的基础上做了更改,如有兴趣,可以自行完善。

最后,附上MainViewController的完整代码。

  1 import UIKit
  2 import RealmSwift
  3 
  4 class MainViewController: UIViewController {
  5 
  6   
  7     var dimension:Int = 4  //矩阵的维度,即游戏面板的大小
  8     var maxnumber:Int = 2048  //游戏通关阈值,该功能没有实现
  9     var gameModel:GameModel  //游戏数据模型
 10     
 11     var CGFloat = 50  //方块的宽度,用于布局
 12     var padding:CGFloat = 6 //方块之间的间隔宽度,用于布局
 13     
 14     var backgrounds:Array<UIView>! //所有方块的背景
 15     
 16     var tiles = [NSIndexPath:TileView]() //存储当前的有数字的方块
 17     var tileVals:Matrix //合并数字时所用的游戏数据复本
 18     
 19     
 20     init(){
 21         self.dimension = (UserModel.sharedInstance().user?.dimension)!  //从数据库中取出维度
 22         self.backgrounds = Array<UIView> ()
 23         self.gameModel = GameModel(dimension: self.dimension)
 24         self.tileVals = Matrix(rows: self.dimension, columns: self.dimension)
 25         
 26         print("dimension:(self.dimension)")
 27         super.init(nibName: nil, bundle: nil)
 28     }
 29 
 30     required init?(coder aDecoder: NSCoder) {
 31         self.gameModel = GameModel(dimension: dimension)
 32         self.tileVals = Matrix(rows: dimension, columns: dimension)
 33         super.init(coder: aDecoder)
 34     }
 35     
 36     
 37     override func viewDidLoad() {
 38         super.viewDidLoad()
 39         
 40         // Do any additional setup after loading the view.
 41         self.view.backgroundColor = UIColor.whiteColor()
 42         setupScoreLables()
 43         setupGameMap()
 44         setupButton()
 45         setupSwipeGestures()
 46         
 47         genNumber()
 48         
 49     }
 50     
 51     override func didReceiveMemoryWarning() {
 52         super.didReceiveMemoryWarning()
 53         // Dispose of any resources that can be recreated.
 54     }
 55     
 56 //---------------------------初始化游戏页面-------------------------
 57     func setupGameMap(){
 58         let margins = self.view.layoutMarginsGuide
 59         
 60         for row in 0..<self.dimension {
 61             for col in 0..<self.dimension {
 62                 //放置灰色的方块在对应的矩阵位置上
 63                 let background = UIView(frame: CGRectMake(0, 0, self.width, self.width))
 64                 background.backgroundColor = UIColor.darkGrayColor()
 65                 background.translatesAutoresizingMaskIntoConstraints = false
 66                 background.widthAnchor.constraintEqualToConstant(self.width).active = true
 67                 background.heightAnchor.constraintEqualToConstant(self.width).active = true
 68                 self.view.addSubview(background)
 69                 self.backgrounds.append(background)
 70                 
 71                 //布局约束
 72                 background.translatesAutoresizingMaskIntoConstraints=false
 73                 background.widthAnchor.constraintEqualToConstant(self.width).active=true
 74                 background.heightAnchor.constraintEqualToConstant(self.width).active=true
 75                 
 76                 //用代码进行布局约束
 77                 var centerXConstant:CGFloat
 78                 var centerYConstant:CGFloat
 79                 if self.dimension%2 == 1 {
 80                     centerXConstant = (self.padding+self.width) * CGFloat(col-self.dimension/2)
 81                     centerYConstant = (self.padding+self.width) * CGFloat(row-self.dimension/2)
 82                 }else{
 83                     centerXConstant = (self.padding+self.width) * CGFloat(Double(col-self.dimension/2)+0.5)
 84                     centerYConstant = (self.padding+self.width) * CGFloat(Double(row-self.dimension/2)+0.5)
 85                 }
 86                 
 87                 background.centerXAnchor.constraintEqualToAnchor(margins.centerXAnchor, constant: centerXConstant).active=true
 88                 background.centerYAnchor.constraintEqualToAnchor(margins.centerYAnchor, constant: centerYConstant).active=true
 89 
 90             }
 91         }
 92     }
 93     
 94     let scoreView = ScoreView(stype:ScoreType.Common)
 95     let bestScoreView = ScoreView(stype: ScoreType.Best)
 96     func setupScoreLables(){
 97         
 98         scoreView.changeScore(value: 0)
 99         self.view.addSubview(scoreView)
100         
101         let val=(self.dimension==3) ? (UserModel.sharedInstance().user?.bestScoreForD3)! : ((self.dimension==4) ? (UserModel.sharedInstance().user?.bestScoreForD4)! : (UserModel.sharedInstance().user?.bestScoreForD5)!)
102         bestScoreView.changeScore(value: val)
103         self.view.addSubview(bestScoreView)
104         
105         //布局约束
106         let margins = self.view.layoutMarginsGuide
107         scoreView.trailingAnchor.constraintEqualToAnchor(margins.centerXAnchor, constant: -30).active=true
108         scoreView.topAnchor.constraintEqualToAnchor(margins.topAnchor, constant: 100).active=true
109         //
110         bestScoreView.leadingAnchor.constraintEqualToAnchor(margins.centerXAnchor, constant: 30).active=true
111         bestScoreView.topAnchor.constraintEqualToAnchor(scoreView.topAnchor).active=true
112         
113     }
114     
115     func setupButton(){
116         let clrBtn=ViewFactory.createButton("重置", action: Selector("clearNumber"), sender: self)
117         self.view.addSubview(clrBtn)
118         
119         let genBtn=ViewFactory.createButton("生成", action: Selector("genNumber"), sender: self)
120         self.view.addSubview(genBtn)
121         
122         
123         //布局约束
124         let margins = self.view.layoutMarginsGuide
125         //
126         clrBtn.trailingAnchor.constraintEqualToAnchor(margins.centerXAnchor, constant: -30).active = true
127         clrBtn.bottomAnchor.constraintEqualToAnchor(margins.bottomAnchor, constant:-100).active=true
128         //
129         genBtn.leadingAnchor.constraintEqualToAnchor(margins.centerXAnchor, constant: 30).active = true
130         genBtn.bottomAnchor.constraintEqualToAnchor(margins.bottomAnchor, constant:-100).active=true
131         //
132     }
133     
134 //---------------------------游戏逻辑实现-------------------------
135     
136     //游戏面板重绘
137     func resetGameMap(){
138         UserModel.sharedInstance().saveBestScore(self.scoreView.score)
139         for view in self.view.subviews{
140             view.removeFromSuperview()
141         }
142         backgrounds.removeAll()
143         tiles.removeAll()
144         gameModel.clearAll()
145         gameModel.dimension=self.dimension
146         setupScoreLables()
147         setupGameMap()
148         setupButton()
149         genNumber()
150         
151     }
152     
153     // 清除所有数字,重置按钮的响应方法
154     func clearNumber(){
155         gameModel.clearAll()
156         for (_,tile) in tiles{
157             tile.removeFromSuperview()
158         }
159         tiles.removeAll()
160         
161     }
162     
163     //生成新的数字,生成按钮的响应方法
164     func genNumber(){
165 ////        随机产生数字2和4,几率为1:4
166 //        let randv=Int(arc4random_uniform(5))
167 //        var seed:Int = 2
168 //        if randv==1 {
169 //            seed = 4
170 //        }
171 
172         let col=Int(arc4random_uniform(UInt32(dimension)))
173         let row=Int(arc4random_uniform(UInt32(dimension)))
174         
175         if gameModel.isFull(){
176             print("位置已经满了")
177             return
178         }
179         
180         if gameModel.tiles[row, col]>0 {
181             genNumber()
182             return
183         }
184         
185         let seed=2 //原书的程序,按照一定的几率比来生成2或4。此处改成一直生成2.
186         gameModel.setPosition(row, col: col, value: seed)
187         insertTile((row,col), value: seed)
188         
189         // 生成数字后,判断一下游戏是否结束
190         checkGameOver()
191     }
192     
193     //插入一个数字方块
194     func insertTile(pos:(Int,Int),value:Int){
195         let (row,col)=pos
196         
197         let x=50 + CGFloat(col) * (self.width+self.padding)
198         let y=150 + CGFloat(row) * (self.width+self.padding)
199         
200         let tile=TileView(pos:CGPointMake(x,y),self.width,value:value)
201       
202         self.view.addSubview(tile)
203         self.view.bringSubviewToFront(tile)
204         
205         let index = NSIndexPath(forRow: row, inSection: col)
206         
207         tiles[index] = tile
208         
209         //布局约束
210         var centerXConstant:CGFloat
211         var centerYConstant:CGFloat
212         if self.dimension%2 == 1 {
213             centerXConstant = (self.padding+self.width) * CGFloat(col-self.dimension/2)
214             centerYConstant = (self.padding+self.width) * CGFloat(row-self.dimension/2)
215         }else{
216             centerXConstant = (self.padding+self.width) * CGFloat(Double(col-self.dimension/2)+0.5)
217             centerYConstant = (self.padding+self.width) * CGFloat(Double(row-self.dimension/2)+0.5)
218         }
219         let margins=self.view.layoutMarginsGuide
220         tile.centerXAnchor.constraintEqualToAnchor(margins.centerXAnchor, constant: centerXConstant).active=true
221         tile.centerYAnchor.constraintEqualToAnchor(margins.centerYAnchor, constant: centerYConstant).active=true
222         //原书中的动画效果
223 //        tile.layer.setAffineTransform(CGAffineTransformMakeScale(0.1, 0.1))
224         
225 //        UIView.animateWithDuration(0.5, delay: 0.1, options: UIViewAnimationOptions.TransitionNone, animations: { () -> Void in
226 //            tile.layer.setAffineTransform(CGAffineTransformMakeRotation(90))
227 //            }, completion: { (Bool) -> Void in
228 //                UIView.animateWithDuration(0.5, animations: { () -> Void in
229 //                    tile.layer.setAffineTransform(CGAffineTransformIdentity)
230 //                })
231 //        })
232         
233 //        tile.alpha=0
234 //        UIView.animateWithDuration(0.5, delay: 0.01, options: UIViewAnimationOptions.CurveEaseInOut, animations: { () -> Void in
235 //            }) { (Bool) -> Void in
236 //                UIView.animateWithDuration(1, animations: { () -> Void in
237 //                    tile.alpha = 1
238 //                })
239 //        }
240         
241 //        UIView.beginAnimations("animation", context: nil)
242 //        UIView.setAnimationDuration(2)
243 //        UIView.setAnimationCurve(UIViewAnimationCurve.EaseInOut)
244 //        UIView.setAnimationTransition(UIViewAnimationTransition.FlipFromLeft, forView: self.view, cache: false)
245 //        UIView.commitAnimations()
246     }
247     
248     //移除一个数字方块
249     func clearTile(row:Int,col:Int){
250         
251         let index=NSIndexPath(forRow: row, inSection: col)
252         let tile=tiles[index]!
253         tile.removeFromSuperview()
254         tiles.removeValueForKey(index)
255         
256     }
257     
258     //添加滑动的手势识别处理
259     func setupSwipeGestures(){
260         let upSwipe=UISwipeGestureRecognizer(target: self, action: Selector("swipeUp"))
261         upSwipe.numberOfTouchesRequired=1
262         upSwipe.direction=UISwipeGestureRecognizerDirection.Up
263         self.view.addGestureRecognizer(upSwipe)
264         
265         let downSwipe=UISwipeGestureRecognizer(target: self, action: Selector("swipeDown"))
266         downSwipe.numberOfTouchesRequired=1
267         downSwipe.direction=UISwipeGestureRecognizerDirection.Down
268         self.view.addGestureRecognizer(downSwipe)
269         
270         let leftSwipe=UISwipeGestureRecognizer(target: self, action: Selector("swipeLeft"))
271         leftSwipe.numberOfTouchesRequired=1
272         leftSwipe.direction=UISwipeGestureRecognizerDirection.Left
273         self.view.addGestureRecognizer(leftSwipe)
274         
275         let rightSwipe=UISwipeGestureRecognizer(target: self, action: Selector("swipeRight"))
276         rightSwipe.numberOfTouchesRequired=1
277         rightSwipe.direction=UISwipeGestureRecognizerDirection.Right
278         self.view.addGestureRecognizer(rightSwipe)
279         
280     }
281     
282     //向上滑动
283     func swipeUp(){
284         tileVals = gameModel.tiles
285         
286         func merge(row row:Int,col:Int){
287             if row == self.dimension-1{
288                 return
289             }
290             
291             for i in (row+1)..<self.dimension {
292                 let valNew = tileVals[i,col]
293                 if valNew>0 {
294                     let val = tileVals[row, col]
295                     
296                     if val == 0 {
297                         tileVals[row, col] = valNew
298                         tileVals[i, col] = 0
299                         merge(row: row, col: col)
300                     }else if val == valNew{
301                         tileVals[row, col] = valNew<<1
302                         tileVals[i, col] = 0
303                         if row==0 {
304                             merge(row: row, col: col)
305                         }else{
306                             merge(row: row-1, col: col)
307                         }
308 //                        若产生合并,则加分
309                         changeScore(valNew)
310                     }else{
311                         merge(row: row+1, col: col)
312                     }
313                     
314                     break
315                 }
316             }
317         }
318         
319         for col in 0..<self.dimension{
320             merge(row: 0, col: col)
321         }
322         
323 //        如果合并后的结果与原来相同,则不做任何操作
324         if tileVals.isEqualTo(gameModel.tiles){
325             return
326         }
327 //        显示合并后的结果,并产生新的数字
328         refresh()
329         genNumber()
330     }
331     
332     //向下滑动
333     
334     func swipeDown(){
335         tileVals = gameModel.tiles
336         
337         func merge(row row:Int,col:Int){
338             if row == 0{
339                 return
340             }
341             
342             for var i=row-1;i>=0;i-- {
343                 let valNew=tileVals[i,col]
344                 if valNew>0 {
345                     let val=tileVals[row,col]
346                     
347                     if val == 0 {
348                         tileVals[row, col] = valNew
349                         tileVals[i, col] = 0
350                         merge(row: row, col: col)
351                     }else if val == valNew{
352                         tileVals[row, col] = valNew<<1
353                         tileVals[i, col] = 0
354 
355                         if row==self.dimension-1 {
356                             merge(row: row, col: col)
357                         }else{
358                             merge(row: row+1, col: col)
359                         }
360 //                        若产生合并,则加分
361                         changeScore(valNew)
362                     }else{
363                         merge(row: row-1, col: col)
364                     }
365                     
366                     break
367                 }
368             }
369         }
370         
371         for col in 0..<self.dimension{
372             merge(row: self.dimension-1, col: col)
373         }
374 //        如果合并后的结果与原来相同,则不做任何操作
375         if tileVals.isEqualTo(gameModel.tiles){
376             return
377         }
378         
379         refresh()
380         genNumber()
381     }
382     
383     //向左滑动
384     func swipeLeft(){
385         tileVals = gameModel.tiles
386         func merge(row row:Int,col:Int){
387             if col == self.dimension-1{
388                 return
389             }
390             
391             for i in (col+1)..<self.dimension {
392                 let valNew=tileVals[row,i]
393                 if valNew>0 {
394                     let val=tileVals[row,col]
395                     
396                     if val == 0 {
397                         tileVals[row, col] = valNew
398                         tileVals[row, i] = 0
399                         merge(row: row, col: col)
400                     }else if val == valNew{
401                         tileVals[row, col] = valNew<<1
402                         tileVals[row, i] = 0
403                         if col==0 {
404                             merge(row: row, col: col)
405                         }else{
406                             merge(row: row, col: col-1)
407                         }
408 //                        若产生合并,则加分
409                         changeScore(valNew)
410                     }else{
411                         merge(row: row, col: col+1)
412                     }
413                     
414                     break
415                 }
416             }
417         }
418         
419         for row in 0..<self.dimension{
420             merge(row: row, col: 0)
421         }
422         
423 //        如果合并后的结果与原来相同,则不做任何操作
424         if tileVals.isEqualTo(gameModel.tiles){
425             return
426         }
427         refresh()
428         genNumber()
429     }
430     
431     //向右滑动
432     func swipeRight(){
433         tileVals = gameModel.tiles
434         func merge(row row:Int,col:Int){
435             if col == 0{
436                 return
437             }
438             
439             for var i=col-1;i>=0;i-- {
440                 let valNew=tileVals[row, i]
441                 if valNew>0 {
442                     let val=tileVals[row, col]
443                     
444                     if val == 0 {
445                         tileVals[row, col] = valNew
446                         tileVals[row, i] = 0
447                         merge(row: row, col: col)
448                     }else if val == valNew{
449                         tileVals[row, col] = valNew<<1
450                         tileVals[row, i] = 0
451                         if col==self.dimension-1 {
452                             merge(row: row, col: col)
453                         }else{
454                             merge(row: row, col: col+1)
455                         }
456 //                        若产生合并,则加分
457                         changeScore(valNew)
458                     }else{
459                         merge(row: row, col: col-1)
460                     }
461                     
462                     break
463                 }
464             }
465         }
466         
467         for row in 0..<self.dimension{
468             merge(row: row, col: self.dimension-1)
469         }
470         
471 //        如果合并后的结果与原来相同,则不做任何操作
472         if tileVals.isEqualTo(gameModel.tiles){
473             return
474         }
475         refresh()
476         genNumber()
477         
478     }
479     
480     //刷新页面
481     func refresh(){
482         for i in 0..<self.dimension{
483             for j in 0..<self.dimension{
484                 let val = gameModel.tiles[i,j]
485                 let valNew = tileVals[i,j]
486                 if valNew != val{
487                     gameModel.setPosition(i, col: j, value: valNew)
488                     if valNew>0{
489                         if val>0{
490                             clearTile(i, col: j)
491                         }
492                         insertTile((i,j) , value: valNew)
493                         
494                     }else{
495                         clearTile(i, col: j)
496                     }
497                 }
498                 
499             }
500         }
501     }
502     
503     //加分
504     func changeScore(baseNum:Int){
505         self.scoreView.score += baseNum * 2
506         
507     }
508     
509     //检查游戏是否结束
510     func checkGameOver(){
511         if self.gameModel.isFull(){
512             for row in 0..<self.dimension{
513                 for col in 0..<self.dimension{
514                     let val = gameModel.tiles[row,col]
515                     let left:Int? = (col-1)>=0 ? gameModel.tiles[row,col-1] : nil
516                     let right:Int? = (col+1)<self.dimension ? gameModel.tiles[row,col+1] : nil
517                     let up:Int? = (row-1)>=0 ? gameModel.tiles[row-1,col] : nil
518                     let down:Int? = (row+1)<self.dimension ? gameModel.tiles[row+1,col] : nil
519                     if (val==left) || (val==right) || (val==up) || (val==down){
520                         return
521                     }
522                     
523                 }
524             }
525             
526             let alerController = UIAlertController(title: "游戏结束", message: "本轮游戏结束,重新开始吧!", preferredStyle: UIAlertControllerStyle.Alert)
527             alerController.addAction(UIAlertAction(title: "Ready Go!", style: UIAlertActionStyle.Default, handler: {
528                 action in
529                 self.resetGameMap()
530             }))
531             self.presentViewController(alerController, animated: true, completion: nil)
532             return
533         }
534     }
535     
536 }
View Code
原文地址:https://www.cnblogs.com/tt2015-sz/p/4843858.html