Swift 实现俄罗斯方块详细思路解析(附完整项目)
一:写在开发前
俄罗斯方块,是一款我们小时候都玩过的小游戏,我自己也是看着书上的思路,学着用 Swift 来写这个小游戏,在写这个游戏的过程中,除了一些位置的计算,数据模型和理解 Swift 语言之外,最好知道UIKIt框架中的 Quartz2D 这个知识点。是我在简书上面找的,是关于 Quartz2D 这个知识点的,看它我觉得也就够学习。经过这两天的整理,充分觉得在写这些之前,一定要理清楚思路,你可能会花很多时间在它上面,你要知道了,怎么写就变的反而简单了。
二:具体开发思路及主要代码
我在博客的最下面附上了完整的代码,大家可以在Git上下载到它,你要也使用Git,就顺便给我个小星星吧 O(∩_∩)O哈哈~。。
1》游戏界面的布局设计
这个里面的Label 和 Button 就不多费口舌了,这不是我们的重点,看看这个效果我们也就一笔带过了吧!重点是我们使用的上面说的利用 Quartz2D 这个知识画出来表格。它单看就是一个 N * M 的表格,在它里面就要运行我们的俄罗斯小方块,在下面的代码里面也会详细的说明它的制作。
下面是我们绘制上面网格视图的方法,下面所有代码方法里面的有些参数是定义成全局变量的,大家可以下载完整版的代码去看看。在代码中也加了许多的注释,相信都能看的明白的。
// MARK: 绘制俄罗斯方库网格的方法 func creatcells(rows:Int,cols:Int,cellwidth:Int,cellHeight:Int) -> Void { // 开始创建路径 CGContextBeginPath(CTX) // 绘制横向网格对应的路径 for i in 0...TETRIS_Row { CGContextMoveToPoint(CTX, 0, CGFloat(i * CELL_Size)) CGContextAddLineToPoint(CTX, CGFloat(TETRIS_Cols * CELL_Size), CGFloat(i * CELL_Size)) } // 绘制纵向的网格对应路径 for i in 0...TETRIS_Cols { CGContextMoveToPoint(CTX, CGFloat(i * CELL_Size),0) CGContextAddLineToPoint(CTX, CGFloat(i * CELL_Size), CGFloat(TETRIS_Row * CELL_Size)) } // 关闭 CGContextClosePath(CTX) // 设置笔触颜色 CGContextSetStrokeColorWithColor(CTX, UIColor(red: 0.9 , green: 0.9 , blue: 0.9,alpha: 1).CGColor) // 设置效线条粗细 CGContextSetLineWidth(CTX, CGFloat(STROKE_Width)) // 绘制线条 CGContextStrokePath(CTX) }
2》小游戏的数据模型
1: 游戏的游戏界面是一个 N * M 的网格,每一张网格显示一张图片,但对于我们来说,我门就得用一个二维数组来定义,纪录每一块的行和列!来保存游戏的状态。我们在最开始把每一个小块的游状态都初始化为 0 ,看下面代码。
// 定义用于纪录方块游戏状态的二维数组 var tetris_status = [[Int]]() // MARK初始化游戏状态 func initTetrisStatus() -> Void { let tmpRow = Array.init(count: TETRIS_Cols, repeatedValue: NO_Block) tetris_status = Array.init(count: TETRIS_Row, repeatedValue: tmpRow) }
2: 游戏的过程中有一只处于“下落”状态的四个方块,这四个方块我们也会是要纪录,才可以做它的旋转、向左、向右等等的处理。我们就用一个数组包含着四个方块,那具体到这四个方块呢?我们就用一个结构体去体现你这四个方块它的 X、Y值和颜色。
struct Block { var X:Int var Y:Int var Color:Int var description:String { return "Block[X=\(X),Y=\(Y),Color=\(Color)]" } }
3:在俄罗斯方块这个游戏中,你也肯定得知道有哪些方块的组合可以下落,这也是一个数据源!你也得定义好,在每次要下落的时候你就随机取出这个而数据源里面的数据,让它随机的出现下落。这些工作也就是你要在初始化上面要纪录的四个正在下落的方块数组的时候做的事了,下面是这些个组合的数据源。
// 几种可能的组合方块 self.blockArr = [ // 第一种可能出现的组合 Z [ Block(X:TETRIS_Cols/2 - 1,Y:0,Color:1), Block(X:TETRIS_Cols/2,Y:0,Color:1), Block(X:TETRIS_Cols/2,Y:1,Color:1), Block(X:TETRIS_Cols/2 + 1,Y:1,Color:1) ], // 第二种可能出现的组合 反Z [ Block(X:TETRIS_Cols/2 + 1,Y:0,Color:2), Block(X:TETRIS_Cols/2,Y:0,Color:2), Block(X:TETRIS_Cols/2,Y:1,Color:2), Block(X:TETRIS_Cols/2 - 1,Y:1,Color:2) ], // 第三种可能出现的组合 田 [ Block(X:TETRIS_Cols/2 - 1,Y:0,Color:3), Block(X:TETRIS_Cols/2,Y:0,Color:3), Block(X:TETRIS_Cols/2 - 1,Y:1,Color:3), Block(X:TETRIS_Cols/2 ,Y:1,Color:3) ], // 第四种可能出现的组合 L [ Block(X:TETRIS_Cols/2 - 1,Y:0,Color:4), Block(X:TETRIS_Cols/2 - 1,Y:1,Color:4), Block(X:TETRIS_Cols/2 - 1,Y:2,Color:4), Block(X:TETRIS_Cols/2 ,Y:2,Color:4) ], // 第五种可能出现的组合 J [ Block(X:TETRIS_Cols/2,Y:0,Color:5), Block(X:TETRIS_Cols/2,Y:1,Color:5), Block(X:TETRIS_Cols/2,Y:2,Color:5), Block(X:TETRIS_Cols/2 - 1,Y:2,Color:5) ], // 第六种可能出现的组合 —— [ Block(X:TETRIS_Cols/2,Y:0,Color:6), Block(X:TETRIS_Cols/2,Y:1,Color:6), Block(X:TETRIS_Cols/2,Y:2,Color:6), Block(X:TETRIS_Cols/2,Y:3,Color:6) ], // 第七种可能出现的组合 土缺一 [ Block(X:TETRIS_Cols/2,Y:0,Color:7), Block(X:TETRIS_Cols/2-1,Y:1,Color:7), Block(X:TETRIS_Cols/2,Y:1,Color:7), Block(X:TETRIS_Cols/2 + 1,Y:1,Color:7) ], ]
随机取出下落
// 定义纪录 “正在下掉的四个方块” 位置 var currentFall = [Block]() func initBlock() -> Void { // 生成一个在 0 - blockArr.count 之间的随机数 let rand = Int(arc4random()) % blockArr.count // 随机取出 blockArr 数组中的某个元素为正在下掉的方块组合 currentFall = blockArr[rand] }
3》 游戏逻辑处理
1:下落
前面我们提到过有用数组纪录正在下落的四个方块的状态,我们梳理一下“下落”状态的逻辑关系。如果在下落的状态,你只需要把这四个正在下落的方块的 Y 值加 1 即可! 但是得注意什么情况下它不能再下落了。。
(1):如果方块组合中任意一个方块已经到达了最底下就不能再下落了。
(2) :如果方库组合中任意一个方块的下面有了方块就不能再下落了。
下落的实现思路就是,如果有方块可以下落,那么就把方块组合原来所在位置的颜色清楚,然后把组合中的每一个方块的 Y 属性加1 ,最后把当前方块的所在位置加上相应的颜色,下面是思路实现的代码。
// MARK:控制方块组合向下移动 func movedown () -> Void { // 定义能否向下掉落的 标签 var canDown = true // 遍历每一块方块,判断它是否能向下掉落 for i in 0..<currentFall.count { // 第一种情况,如果位置到行数最底下了,不能再下落 if currentFall[i].Y >= TETRIS_Row - 1 { canDown = false break } // 第二种情况,如果他的下面有了方块,不能再下落 if tetris_status[currentFall[i].Y + 1][currentFall[i].X] != NO_Block { canDown = false break } } // 如果能向下掉落 if canDown { self.drawBlock()// for i in 0..<currentFall.count { let cur = currentFall[i] // 设置填充颜色 CGContextSetFillColorWithColor(CTX, UIColor.whiteColor().CGColor) CGContextFillRect(CTX, CGRectMake(CGFloat(cur.X * CELL_Size + STROKE_Width), CGFloat(cur.Y * CELL_Size + STROKE_Width), CGFloat(CELL_Size - STROKE_Width * 2),CGFloat(CELL_Size - STROKE_Width * 2))) } // 遍历每一个方块。控制每一个方块的 有坐标都 加 1 for i in 0..<currentFall.count { currentFall[i].Y += 1 } // 将下移后的每一个方块的背景涂色称该方块的颜色 for i in 0..<currentFall.count { let cur = currentFall[i] // print(cur.X , cur.Y) CGContextSetFillColorWithColor(CTX, colors[cur.Color]) CGContextFillRect(CTX, CGRectMake(CGFloat(cur.X * CELL_Size + STROKE_Width), CGFloat(cur.Y * CELL_Size + STROKE_Width), CGFloat(CELL_Size - STROKE_Width * 2),CGFloat(CELL_Size - STROKE_Width * 2))) } } // 不能向下掉落 else { // 遍历每个方块,把每个方块的值纪录到 for i in 0..<currentFall.count { let cur = currentFall[i] // 小于2表示已经到最上面,游戏要结束了 if cur.Y < 2 { // 计时器失效 curTimer?.invalidate() // 提示游戏结束 self.delegate.UpdateGameState() } // 把每个方块当前所在的位置赋值为当前方块的颜色值 tetris_status[cur.Y][cur.X] = cur .Color } // 判断是否有可消除的行 lineFull() // 开始一组新的方块 initBlock() } // 获取缓存区的图片 image = UIGraphicsGetImageFromCurrentImageContext() // 通知重绘 self.setNeedsDisplay() }
里面的代理更新UI(及分数和速度)我们就不多说了,说说 drawBlock() 这个方法,它是来绘制了我们在所有的方块,相当于把我们的互数据模型给全都可视化;
//MARK: 绘制俄罗斯方块的状态 func drawBlock() -> Void { for i in 0..<TETRIS_Row { for j in 0..<TETRIS_Cols { if tetris_status[i][j] != NO_Block { // 设置填充颜色 CGContextSetFillColorWithColor(CTX, colors[tetris_status[i][j]]) CGContextFillRect(CTX, CGRectMake(CGFloat(j * CELL_Size + STROKE_Width),CGFloat(i * CELL_Size + STROKE_Width) , CGFloat(CELL_Size - STROKE_Width * 2), CGFloat(CELL_Size - STROKE_Width * 2))) } else { // 设置填充颜色 CGContextSetFillColorWithColor(CTX, UIColor.whiteColor().CGColor) CGContextFillRect(CTX, CGRectMake(CGFloat(j * CELL_Size + STROKE_Width),CGFloat(i * CELL_Size + STROKE_Width) , CGFloat(CELL_Size - STROKE_Width * 2), CGFloat(CELL_Size - STROKE_Width * 2))) } } } }
2:判断这行是否已满
上面是让它下落了,里面有调用判断一行是否已满,其实这里的逻辑就是遍历每一行每一个方块,给你的每一行都加一个状态,这里是 true ,判断你该行的每一个方块的状态是不是初始化时候的 0 ,要是,那说明是缺方块的,这行没有满,跳出。。要是都不是,那就说明这行都满了。。就可以进行消除这行的后续操作了。增加积分,消除相应的行等,下面是它的代码。
// MARK: 判断是否有一行已满 func lineFull() -> Void{ // 遍历每一行 for i in 0..<TETRIS_Row { var flag = true // 遍历每一行的每一个单元 for j in 0..<TETRIS_Cols { if tetris_status[i][j] == NO_Block { flag = false break } } // 如果当前行已经全部有了方块 if flag { // 当前积分增加 100 curScore += 100 // 代理更新当前积分 self.delegate.UpdateScore(curScore) if curScore >= curSpeed * curSpeed * 500{ curSpeed += 1 // 代理更新当前速度 self.delegate.UpdateSpeed(curSpeed) curTimer?.invalidate() curTimer = NSTimer.scheduledTimerWithTimeInterval(BASE_Speed/Double(curSpeed), target: self, selector: #selector(self.movedown), userInfo: nil, repeats: true) } } // 把所有的整体下移一行 for var j = i; j < 0 ; j -= 1 { for k in 0..<TETRIS_Cols { tetris_status[j][k] = tetris_status[j-1][k] } } // 播放消除的音乐 // if !disBackGroundMusicPlayer.play() { // // disBackGroundMusicPlayer.play() // } } }
3.左移处理
它的处理方式和上面的下落的逻辑是一样的,也就是两点,到了最左边和左边有了两类型的情况,代码如下。
//MARK: 定义左边移动的方法 func moveLeft () -> Void { // 定义左边移动的标签 var canLeft = true for i in 0..<currentFall.count { if currentFall[i].X <= 0 { canLeft = false break } // 左变位置的前边一块 if tetris_status[currentFall[i].Y][currentFall[i].X - 1] != NO_Block { canLeft = false break } } // 如果可以左移 if canLeft { self.drawBlock() // 将左移前的的每一个方块背景涂成白底 for i in 0..<currentFall.count { let cur = currentFall[i] CGContextSetFillColorWithColor(CTX, UIColor.whiteColor() .CGColor) CGContextFillRect(CTX, CGRectMake(CGFloat(cur.X * CELL_Size + STROKE_Width), CGFloat(cur.Y * CELL_Size + STROKE_Width), CGFloat(CELL_Size - STROKE_Width * 2), CGFloat(CELL_Size - STROKE_Width * 2))) } // 左移正字啊下掉的方块 for i in 0..<currentFall.count { currentFall[i].X -= 1 } // 将左移后的的每一个方块背景涂成对应的颜色 for i in 0..<currentFall.count { let cur = currentFall[i] CGContextSetFillColorWithColor(CTX,colors[cur.Color]) CGContextFillRect(CTX, CGRectMake(CGFloat(cur.X * CELL_Size + STROKE_Width), CGFloat(cur.Y * CELL_Size + STROKE_Width), CGFloat(CELL_Size - STROKE_Width * 2), CGFloat(CELL_Size - STROKE_Width * 2))) } // 获取缓冲区的图片 image = UIGraphicsGetImageFromCurrentImageContext() // 通知重新绘制 self.setNeedsDisplay() } }
4.右移处理
右边移动的处理情况几乎就和左边的完全相同了,见代码
// MARK: 定义右边移动的方法 func moveRight () -> Void { // 能否右移动的标签 var canRight = true for i in 0..<currentFall.count { // 如果已经到最右边就不能再移动 if currentFall[i].X >= TETRIS_Cols - 1 { canRight = false break } // 如果右边有方块,就不能再移动 if tetris_status[currentFall[i].Y][currentFall[i].X + 1] != NO_Block { canRight = false break } } // 如果能右边移动 if canRight { self.drawBlock() // 将香油移动的每个方块涂白色 for i in 0..<currentFall.count { let cur = currentFall[i] CGContextSetFillColorWithColor(CTX, UIColor.whiteColor().CGColor) CGContextFillRect(CTX, CGRectMake(CGFloat(cur.X * CELL_Size + STROKE_Width), CGFloat(cur.Y * CELL_Size + STROKE_Width), CGFloat(CELL_Size - STROKE_Width * 2), CGFloat(CELL_Size - STROKE_Width * 2))) } } // 右边移动正在下落的所有的方块 for i in 0..<currentFall.count { currentFall[i].X += 1 } // 有以后将每个方块的颜色背景图成各自方块对应的颜色 for i in 0..<currentFall.count { let cur = currentFall[i] // 设置填充颜色 CGContextSetFillColorWithColor(CTX, colors[cur.Color]) // 绘制矩形 CGContextFillRect(CTX, CGRectMake(CGFloat(cur.X * CELL_Size + STROKE_Width), CGFloat(cur.Y * CELL_Size + STROKE_Width), CGFloat(CELL_Size - STROKE_Width * 2), CGFloat(CELL_Size - STROKE_Width * 2))) image = UIGraphicsGetImageFromCurrentImageContext() // 通知重新绘制 self.setNeedsDisplay() } }
5.旋转处理
旋转处理,就得用点数学知识了,你画一个坐标轴,试着把一个点顺时针或者逆时针旋转九十度,你再写出旋转后的坐标。其实清楚了这点也就OK了,我们是按逆时针旋转处理的,四个方块,就按照第三个作为它的旋转轴心。
// MARK: 定义旋转的方法 func rotate () -> Void { // 定义是否能旋转的标签 var canRotate = true for i in 0..<currentFall.count { let preX = currentFall[i].X let preY = currentFall[i].Y // 始终以第三块作为旋转的中心 // 当 i == 2的时候,说明是旋转的中心 if i != 2 { // 计算方块旋转后的X,Y坐标 let afterRotateX = currentFall[2].X + preY - currentFall[2].Y let afterRotateY = currentFall[2].Y + currentFall[2].X - preX // 如果旋转后的x,y坐标越界,或者旋转后的位置已有别的方块,表示不能旋转 if afterRotateX < 0 || afterRotateX > TETRIS_Cols - 1 || afterRotateY < 0 || afterRotateY > TETRIS_Row - 1 || tetris_status[afterRotateY][afterRotateX] != NO_Block { canRotate = false break } } } // 如果能旋转 if canRotate { self.drawBlock() for i in 0..<currentFall.count { let cur = currentFall[i] // 设置填充颜色 CGContextSetFillColorWithColor(CTX, UIColor.whiteColor().CGColor) // 绘制矩形 CGContextFillRect(CTX, CGRectMake(CGFloat(cur.X * CELL_Size + STROKE_Width), CGFloat(cur.Y * CELL_Size + STROKE_Width), CGFloat(CELL_Size - STROKE_Width * 2), CGFloat(CELL_Size - STROKE_Width * 2))) } for i in 0..<currentFall.count { let preX = currentFall[i].X let preY = currentFall[i].Y // 始终第三个作为旋转中心 if i != 2 { currentFall[i].X = currentFall[2].X + preY - currentFall[2].Y currentFall[i].Y = currentFall[2].Y + currentFall[2].X - preX } } for i in 0..<currentFall.count { let cur = currentFall[i] CGContextSetFillColorWithColor(CTX, colors[cur.Color]) // 绘制矩形 CGContextFillRect(CTX, CGRectMake(CGFloat(cur.X * CELL_Size + STROKE_Width), CGFloat(cur.Y * CELL_Size + STROKE_Width), CGFloat(CELL_Size - STROKE_Width * 2), CGFloat(CELL_Size - STROKE_Width * 2))) } // 获取缓存区的图片 image = UIGraphicsGetImageFromCurrentImageContext() // 通知重新绘制 self.setNeedsDisplay() } }
三:启动游戏
做完了上面的工作,你就可以启动你的游戏了,你的做的工作就有下面这些;
重置游戏积分,将积分设置为 0
重置下落的速度,也将它设置为0
初始化俄罗斯方块的状态,将它们的值全都初始化为 0
生成一组在下落的方块组
启动计时器,控制下落的方块
// MARK:开始游戏 func startGame() { self.curSpeed = 1 self.delegate.UpdateSpeed(self.curSpeed) self.curScore = 0 self.delegate.UpdateScore(self.curScore) // 初始化游戏状态 self.initTetrisStatus() // 初始化四个正在下落的方块 self.initBlock() // 定时器控制下落 curTimer = NSTimer.scheduledTimerWithTimeInterval(BASE_Speed/Double(curSpeed), target: self, selector: #selector(self.movedown), userInfo: nil, repeats: true) }
PS:一张游戏运行图片
四:写在开发后
差不多到这里也就结束了,但里面有一个BUG,有些时候会发生一个数组的越界导致的崩溃,这个问题有时间在好好看一下,自己写的里面可能还有我不知道的问题,也没做大量的测试,感兴趣的朋友可以自己好好完善一下,比如试试暂停,重新开始这些功能的。。反正肯定还有写的不好的地方,有问题大家可以发消息随时交流!!
写完了,说点无聊的,说说自己😄,其实在大学的时候,我打死也不可能相信自己将来会走上编程这条路,一个大一连C语言都挂科不懂得人。现在想想真的就是着实蛋疼。要是那时候上帝给我说一句,你将来要会是一个开发软件的,我一定觉得是上帝疯了。可转眼工作也一年多了,慢慢的,我喜欢上了自己做的事,至少我自己觉得挺好的。工作第一,但也得给自己充充电,每天敲着代码 PS:还有加着班,但心里踏实。没有为碌碌无为,荒废一天又一天感到不安!难道有什么比你心里踏实更重要的么,当然你要是有鸿鹄之志,额~~~你还是得充电呀,,O(∩_∩)O哈哈~
最后就是完整代码。。Git地址给大家。。。点击下载 Swift俄罗斯方块完整代码