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),width: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,width: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 }

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     }

(三)设置页面

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

上面的几个控件都是在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 }

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 width: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),width: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 }

时间: 2024-10-05 23:58:17

Swift实战之2048小游戏的相关文章

C# 开发2048小游戏

这应该是几个月前,闲的手痒,敲了一上午代码搞出来的,随之就把它丢弃了,当时让别人玩过,提过几条更改建议,但是时至今日,我也没有进行过优化和更改(本人只会作案,不会收场,嘎嘎),下面的建议要给代码爱好的童鞋完成了. 更改建议: a.当数字超过四位数时,显示的时候有部分被它的容器TextBox遮挡了,能不能把显示的数值变小点?答案是可以的.代码里有一段通过矩阵数据填充TextBox值的操作,可以在填充时,判断下数值长度,然后修改TextBox的文字大小. b.玩游戏的时候,使用方向键移动时,焦点可能

2048小游戏(C语言版)

1 #include <climits> 2 #include <cstdio> 3 #include <cstring> 4 #include <stack> 5 #include <string> 6 #include <map> 7 #include <vector> 8 #include <cmath> 9 10 11 /* 12 玩法说明:编译运行后,输入一个整数n,表示自己想玩的游戏中是n*n的表格

canvas随笔之2048小游戏

HTML: <!DOCTYPE HTML> <html> <head> <title>2048小游戏</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=0.5, maximum-scale=2.0, u

如何在CentOS上安装一个2048小游戏

如何在centos上安装一个2048小游戏 最近在学习CentOS系统,就琢磨着玩点什么,然后我看到有人在玩2048小游戏,所有我就在想,为啥不装一个2048小游戏搞一下嘞,于是乎,我就开始工作啦 由于我个人的编程能力不强,所以我就在网上找到了一个C语言版的2048游戏小程序,我把它放到我的百度网盘上,可以供大家下载(链接:http://pan.baidu.com/s/1jIutb3g 密码:mu9z),然后我们把这个程序给复制到CentOS系统下,在进行下一步的工作.我们可以在CentOS上安

2048小游戏-JS实现(BUG调试中)

刚刚学习JS的菜鸟,游戏没有实现滑动效果.希望有前辈能指点一下······ 定义的主要方法: 1.fuzhi()生成一对随机数,然后根据这对随机数取得一个随机单元格,先判断其是否为空,不为空,对其进行赋值为2的操作:为空,则再次调用fuzhi(). 2.secai()遍历表格,根据单元格的数值改变单元格的背景颜色. 3.score()遍历单元格,计算实时总得分. 4.keyDown()主要方法,根据用户按上下左右键来进行不同的数值相加.消除动作.这一段代码写得很冗余····· 1 <!DOCTY

2048小游戏用例图

用例图主要用来描述"用户.需求.系统功能单元"之间的关系.它展示了一个外部用户能够观察到的系统功能模型图. 下面是我们2048小游戏的用例图:

Swift学习第一练——用Swift实现的FlappyBird小游戏

用Swift实现的FlappyBird小游戏 伴随着apple公司对swift的推广态度深入,swift火的很快,并且swift精简便捷的语法和强大的功能,对于使用Object-C开发iOS的开发者来说,也有必要了解学习一下swift.这篇博客跳过swift干涩的语法,直接从一个小游戏项目开始使用swift,将其中收获总结如下: FlappyBird是前段时间很火的一款小游戏,通过手指点击屏幕平衡小鸟通过障碍.我是将以前OC版的项目拿来改成了swift,所以整体的思路还是OC的开发思路. 首先,

2048小游戏(变态版哦)

近日,由于博主同学暑假有个作业是写个2048小游戏,我一听挺好玩的..然后就开始了.. 首先,2048在移动过程中的规则其实也没有特别难,但是感觉也不是一句话就能说完的.(不过玩的多--感觉总是有的) 废话不多说,其实博主同学提供了pdf描述了2048的算法. 各位筒子入坑前请先过几眼这个规则,以及其算法(当然我觉得算法第二点有点问题,后述) 那么在游戏的编写前,可以先对细枝末节做一些准备. 1.出现数字2/4的概率 int getRand() { int i = rand() % 10; if

用控制台写类窗体2048小游戏

原文地址:用控制台写类窗体2048小游戏作者:余文 2048是一个很简单的小游戏,这是我自己实现的一个版本. 这个版本有两个特色 这是一个高仿窗体程序的控制台程序. 通过使用指针将四个方向的移动简化成了一个方向的移动. 当初刚出来2048小游戏的时候,玩的不亦乐乎.之后根据游戏规则自己花了一下午时间用控制台写了一个玩,没有参考源码. 当时只实现了游戏的移动逻辑和胜负判定逻辑,界面很简单,但已经可以玩玩了. 这是当初刚写完发空间说说得瑟的时候. 今年5月有段时候比较闲,对程序做了较大更新. 实时显