博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
[swift实战入门]手把手教你编写2048(三)
阅读量:4078 次
发布时间:2019-05-25

本文共 17271 字,大约阅读时间需要 57 分钟。

上篇地址:

github地址:。

今天给大家带来2048最后一篇,之前已经实现了向游戏区域中随机插入数字块,接下来要做的,就是当我们滑动屏幕时移动及合并数字块以及插入一个新的数字块。本篇的难点就是移动时的算法问题,首先来给大家讲一下算法。

2048的算法实现其实很简单,假如我们当前数字格的格式如下:

| |4| | || | |4| ||2| |2|2||2| | | |

如果用户选择向上滑动,那么这里我们算法里要做的是,先取出第一列的4个格存为一个数组,对应坐标为[(0,1),(0,2),(0,3),(0,4)],其中对应的值为| | |2|2|,首先对数组进行去除空操作,去除之后数据为:[(0,3),(0,4)],对应值为|2|2|,之后再进行合并操作,合并时我们可以取到数组中原来两个2的坐标以及最终坐标,那么此时我们只要更新存储当前数字块状态的数组以及数字块视图,将之前两个2的地方置空,并在(0,1)处插入一个4即可,之后再继续遍历下一列做同样的操作即可。

这里用户一共有4个操作,上下左右,分别取出对应的行列一行(列)一行(列)的进行处理即可。那么接下来看代码:

首先我们定义几个枚举:

//用户操作---上下左右enum MoveDirection {    case UP,DOWN,LEFT,RIGHT}//用于存放数字块的移动状态,是否需要移动以及两个一块合并并移动等,关键数据是数组中位置以及最新的数字块的值enum TileAction{    case NOACTION(source : Int , value : Int)    case MOVE(source : Int , value : Int)    case SINGLECOMBINE(source : Int , value : Int)    case DOUBLECOMBINE(firstSource : Int , secondSource : Int , value : Int)    func getValue() -> Int {        switch self {        case let .NOACTION(_, value) : return value        case let .MOVE(_, value) : return value        case let .SINGLECOMBINE(_, value) : return value        case let .DOUBLECOMBINE(_, _, value) : return value        }    }    func getSource() -> Int {        switch self {        case let .NOACTION(source , _) : return source        case let .MOVE(source , _) : return source        case let .SINGLECOMBINE(source , _) : return source        case let .DOUBLECOMBINE(source , _ , _) : return source        }    }}//最终的移动数据封装,标注了所有需移动的块的原位置及新位置,以及块的最新值enum MoveOrder{    case SINGLEMOVEORDER(source : Int , destination : Int , value : Int , merged : Bool)    case DOUBLEMOVEORDER(firstSource : Int , secondSource : Int , destination : Int , value : Int)}

接下来就看具体算法:

func merge(group : [TileEnum]) -> [MoveOrder] {    return convert(collapse(condense(group)))}//去除空   如:| | |2|2|去掉空为:|2|2| | |func condense(group : [TileEnum]) -> [TileAction] {    var buffer = [TileAction]()    for (index , tile) in group.enumerate(){        switch tile {        //如果buffer的大小和当前group的下标一致,则表示当前数字块不需要移动        //如|2| |2| |,第一次时buffer大小和index都是0,不需要移动        //下一个2时,buffer大小为1,groupindex为2,则需要移动了        case let .Tile(value) where buffer.count == index :            buffer.append(TileAction.NOACTION(source: index, value: value))        case let .Tile(value) :            buffer.append(TileAction.MOVE(source: index, value: value))        default:            break        }    }    return buffer}//合并相同的    如:|2| | 2|2|合并为:|4|2| | |func collapse(group : [TileAction]) -> [TileAction] {    var tokenBuffer = [TileAction]()    //是否跳过下一个,如果把下一个块合并过来,则下一个数字块应该跳过    var skipNext = false    for (idx, token) in group.enumerate() {        if skipNext {            skipNext = false            continue        }        switch token {        //当前块和下一个块的值相同且当前块不需要移动,那么需要将下一个块合并到当前块来        case let .NOACTION(s, v)            where (idx < group.count-1                && v == group[idx+1].getValue()                && GameModle.quiescentTileStillQuiescent(idx, outputLength: tokenBuffer.count, originalPosition: s)):            let next = group[idx+1]            let nv = v + group[idx+1].getValue()            skipNext = true            tokenBuffer.append(TileAction.SINGLECOMBINE(source: next.getSource(), value: nv))        //当前块和下一个块的值相同,且两个块都需要移动,则将两个块移动到新的位置        case let t where (idx < group.count-1 && t.getValue() == group[idx+1].getValue()):            let next = group[idx+1]            let nv = t.getValue() + group[idx+1].getValue()            skipNext = true            tokenBuffer.append(TileAction.DOUBLECOMBINE(firstSource: t.getSource(), secondSource: next.getSource(), value: nv))        //上一步判定不需要移动,但是之前的块有合并过,所以需要移动        case let .NOACTION(s, v) where !GameModle.quiescentTileStillQuiescent(idx, outputLength: tokenBuffer.count, originalPosition: s):            tokenBuffer.append(TileAction.MOVE(source: s, value: v))        //上一步判定不需要移动,且之前的块也没有合并,则不需要移动        case let .NOACTION(s, v):            tokenBuffer.append(TileAction.NOACTION(source: s, value: v))        //上一步判定需要移动且不符合上面的条件的,则继续保持移动        case let .MOVE(s, v):            tokenBuffer.append(TileAction.MOVE(source: s, value: v))        default:            break        }    }    return tokenBuffer}class func quiescentTileStillQuiescent(inputPosition: Int, outputLength: Int, originalPosition: Int) -> Bool {    return (inputPosition == outputLength) && (originalPosition == inputPosition)}//转换为MOVEORDER便于后续处理func convert(group : [TileAction]) -> [MoveOrder] {    var buffer = [MoveOrder]()    for (idx , tileAction) in group.enumerate() {        switch tileAction {        case let .MOVE(s, v) :            //单纯的将一个块由s位置移动到idx位置,新值为v            buffer.append(MoveOrder.SINGLEMOVEORDER(source: s, destination: idx, value: v, merged: false))        case let .SINGLECOMBINE(s, v) :            //将一个块由s位置移动到idx位置,且idx位置有数字块,俩数字块进行合并,新值为v            buffer.append(MoveOrder.SINGLEMOVEORDER(source: s, destination: idx, value: v, merged: true))        case let .DOUBLECOMBINE(s, d, v) :            //将s和d两个数字块移动到idx位置并进行合并,新值为v            buffer.append(MoveOrder.DOUBLEMOVEORDER(firstSource: s, secondSource: d, destination: idx, value: v))        default:            break        }    }    return buffer}

上面代码里注释已经很详细了,这里再简单说下,**condense方法的作用就是去除空的数字块,入参就是一列的四个数字块,里面是定义了一个TileAction数组buffer,之后判断入参中不为空的则加入buffer中,其中只是做了判断数字块是否需要移动。collapse方法就是合并操作**,其实只是记录一个合并状态,如果不需要合并的就还是只判断是否需要移动,convert中则将collapse中返回的结果进行包装,表明具体的移动前和移动后的位置,以及新的值和是否需要合并。

这里算法的具体实现就做完了,下面来看下具体调用:

//提供给主控制器调用,入参为移动方向和一个需要一个是否移动过的Bool值为入参的闭包    func queenMove(direction : MoveDirection , completion : (Bool) -> ()){    let changed = performMove(direction)    completion(changed)}//移动实现func performMove(direction : MoveDirection) -> Bool {    //根据上下左右返回每列(行)的四个块的坐标    let getMoveQueen : (Int) -> [(Int , Int)] = { (idx : Int) -> [(Int , Int)] in        var buffer = Array<(Int , Int)>(count : self.dimension , repeatedValue : (0, 0))        for i in 0..
TileEnum in let (source , value) = c return self.gamebord[source , value] }) //调用算法 let moveOrders = merge(tiles) movedFlag = moveOrders.count > 0 ? true : movedFlag //对算法返回结果进行具体处理.1:更新gamebord中的数据,2:更新视图中的数字块 for order in moveOrders { switch order { //单个移动或合并的 case let .SINGLEMOVEORDER(s, d, v, m): let (sx, sy) = moveQueen[s] let (dx, dy) = moveQueen[d] if m { self.score += v } //将原位置置空,新位置设置为新的值 gamebord[sx , sy] = TileEnum.Empty gamebord[dx , dy] = TileEnum.Tile(v) //TODO 调用游戏视图更新视图中的数字块 delegate.moveOneTile((sx, sy), to: (dx, dy), value: v) //两个进行合并的 case let .DOUBLEMOVEORDER(fs , ts , d , v): let (fsx , fsy) = moveQueen[fs] let (tsx , tsy) = moveQueen[ts] let (dx , dy) = moveQueen[d] self.score += v //将原位置置空,新位置设置为新的值 gamebord[fsx , fsy] = TileEnum.Empty gamebord[tsx , tsy] = TileEnum.Empty gamebord[dx , dy] = TileEnum.Tile(v) //TODO 调用游戏视图更新视图中的数字块 delegate.moveTwoTiles((moveQueen[fs], moveQueen[ts]), to: moveQueen[d], value: v) } } } return movedFlag}

可以看到,上面调用我们之前写的算法,以及将gamebord中存储内容更新了(gamebord存储的是当前各个位置的数字块状态,前两篇有介绍),接下来需要更新游戏视图中的数字块,接下来在GamebordView.swift中添加如下代码:

//从from位置移动一个块到to位置,并赋予新的值valuefunc moveOneTiles(from : (Int , Int)  , to : (Int , Int) , value : Int) {    let (fx , fy) = from    let (tx , ty) = to    let fromKey = NSIndexPath(forRow: fx , inSection: fy)    let toKey = NSIndexPath(forRow: tx, inSection: ty)    //取出from位置和to位置的数字块    guard let tile = tiles[fromKey] else{        assert(false, "not exists tile")    }    let endTile = tiles[toKey]    //将from位置的数字块的位置定到to位置    var changeFrame = tile.frame    changeFrame.origin.x = tilePadding + CGFloat(tx)*(tilePadding + tileWidth)    changeFrame.origin.y = tilePadding + CGFloat(ty)*(tilePadding + tileWidth)    tiles.removeValueForKey(fromKey)    tiles[toKey] = tile    // 动画以及给新位置的数字块赋值    let shouldPop = endTile != nil    UIView.animateWithDuration(perSquareSlideDuration,                               delay: 0.0,                               options: UIViewAnimationOptions.BeginFromCurrentState,                               animations: {                                tile.frame = changeFrame        },                               completion: { (finished: Bool) -> Void in                                //对新位置的数字块赋值                                tile.value = value                                endTile?.removeFromSuperview()                                if !shouldPop || !finished {                                    return                                }                                tile.layer.setAffineTransform(CGAffineTransformMakeScale(self.tileMergeStartScale, self.tileMergeStartScale))                                UIView.animateWithDuration(self.tileMergeExpandTime,                                    animations: {                                        tile.layer.setAffineTransform(CGAffineTransformMakeScale(self.tilePopMaxScale, self.tilePopMaxScale))                                    },                                    completion: { finished in                                        UIView.animateWithDuration(self.tileMergeContractTime) {                                            tile.layer.setAffineTransform(CGAffineTransformIdentity)                                        }                                })    })}//将from里两个位置的数字块移动到to位置,并赋予新的值,原理同上func moveTwoTiles(from: ((Int, Int), (Int, Int)), to: (Int, Int), value: Int) {    assert(positionIsValid(from.0) && positionIsValid(from.1) && positionIsValid(to))    let (fromRowA, fromColA) = from.0    let (fromRowB, fromColB) = from.1    let (toRow, toCol) = to    let fromKeyA = NSIndexPath(forRow: fromRowA, inSection: fromColA)    let fromKeyB = NSIndexPath(forRow: fromRowB, inSection: fromColB)    let toKey = NSIndexPath(forRow: toRow, inSection: toCol)    guard let tileA = tiles[fromKeyA] else {        assert(false, "placeholder error")    }    guard let tileB = tiles[fromKeyB] else {        assert(false, "placeholder error")    }    var finalFrame = tileA.frame    finalFrame.origin.x = tilePadding + CGFloat(toRow)*(tileWidth + tilePadding)    finalFrame.origin.y = tilePadding + CGFloat(toCol)*(tileWidth + tilePadding)    let oldTile = tiles[toKey]      oldTile?.removeFromSuperview()    tiles.removeValueForKey(fromKeyA)    tiles.removeValueForKey(fromKeyB)    tiles[toKey] = tileA    UIView.animateWithDuration(perSquareSlideDuration,                               delay: 0.0,                               options: UIViewAnimationOptions.BeginFromCurrentState,                               animations: {                                tileA.frame = finalFrame                                tileB.frame = finalFrame        },                               completion: { finished in                                //赋值                                tileA.value = value                                tileB.removeFromSuperview()                                if !finished {                                    return                                }                                tileA.layer.setAffineTransform(CGAffineTransformMakeScale(self.tileMergeStartScale, self.tileMergeStartScale))                                UIView.animateWithDuration(self.tileMergeExpandTime,                                    animations: {                                        tileA.layer.setAffineTransform(CGAffineTransformMakeScale(self.tilePopMaxScale, self.tilePopMaxScale))                                    },                                    completion: { finished in                                        UIView.animateWithDuration(self.tileMergeContractTime) {                                            tileA.layer.setAffineTransform(CGAffineTransformIdentity)                                        }                                })    })}func positionIsValid(pos: (Int, Int)) -> Bool {    let (x, y) = pos    return (x >= 0 && x < dimension && y >= 0 && y < dimension)}

上面方法更新了游戏视图中的数字块状态。那么接下来我们在主控制器中调用queenMove就可以运行游戏看移动效果了,在NumbertailGameController.swift的NumbertailGameController类中添加如下代码:

//注册监听器,监听当前视图里的手指滑动操作,上下左右分别对应下面的四个方法func setupSwipeConttoller() {    let upSwipe = UISwipeGestureRecognizer(target: self , action: #selector(NumbertailGameController.upCommand(_:)))    upSwipe.numberOfTouchesRequired = 1    upSwipe.direction = UISwipeGestureRecognizerDirection.Up    view.addGestureRecognizer(upSwipe)    let downSwipe = UISwipeGestureRecognizer(target: self , action: #selector(NumbertailGameController.downCommand(_:)))    downSwipe.numberOfTouchesRequired = 1    downSwipe.direction = UISwipeGestureRecognizerDirection.Down    view.addGestureRecognizer(downSwipe)    let leftSwipe = UISwipeGestureRecognizer(target: self , action: #selector(NumbertailGameController.leftCommand(_:)))    leftSwipe.numberOfTouchesRequired = 1    leftSwipe.direction = UISwipeGestureRecognizerDirection.Left    view.addGestureRecognizer(leftSwipe)    let rightSwipe = UISwipeGestureRecognizer(target: self , action: #selector(NumbertailGameController.rightCommand(_:)))    rightSwipe.numberOfTouchesRequired = 1    rightSwipe.direction = UISwipeGestureRecognizerDirection.Right    view.addGestureRecognizer(rightSwipe)}//向上滑动的方法,调用queenMove,传入MoveDirection.UPfunc upCommand(r : UIGestureRecognizer) {    let m = gameModle!    m.queenMove(MoveDirection.UP , completion: { (changed : Bool) -> () in        if  changed {            self.followUp()        }    })}//向下滑动的方法,调用queenMove,传入MoveDirection.DOWNfunc downCommand(r : UIGestureRecognizer) {    let m = gameModle!    m.queenMove(MoveDirection.DOWN , completion: { (changed : Bool) -> () in        if  changed {            self.followUp()        }    })}//向左滑动的方法,调用queenMove,传入MoveDirection.LEFTfunc leftCommand(r : UIGestureRecognizer) {    let m = gameModle!    m.queenMove(MoveDirection.LEFT , completion: { (changed : Bool) -> () in        if  changed {            self.followUp()        }    })}//向右滑动的方法,调用queenMove,传入MoveDirection.RIGHTfunc rightCommand(r : UIGestureRecognizer) {    let m = gameModle!    m.queenMove(MoveDirection.RIGHT , completion: { (changed : Bool) -> () in        if  changed {            self.followUp()        }    })}//移动之后需要判断用户的输赢情况,如果赢了则弹框提示,给一个重玩和取消按钮func followUp() {    assert(gameModle != nil)    let m = gameModle!    let (userWon, _) = m.userHasWon()    if userWon {        let winAlertView = UIAlertController(title: "結果", message: "你贏了", preferredStyle: UIAlertControllerStyle.Alert)        let resetAction = UIAlertAction(title: "重置", style: UIAlertActionStyle.Default, handler: {(u : UIAlertAction) -> () in            self.reset()        })        winAlertView.addAction(resetAction)        let cancleAction = UIAlertAction(title: "取消", style: UIAlertActionStyle.Default, handler: nil)        winAlertView.addAction(cancleAction)        self.presentViewController(winAlertView, animated: true, completion: nil)        return    }    //如果没有赢则需要插入一个新的数字块    let randomVal = Int(arc4random_uniform(10))    m.insertRandomPositoinTile(randomVal == 1 ? 4 : 2)    //插入数字块后判断是否输了,输了则弹框提示    if m.userHasLost() {        NSLog("You lost...")        let lostAlertView = UIAlertController(title: "結果", message: "你輸了", preferredStyle: UIAlertControllerStyle.Alert)        let resetAction = UIAlertAction(title: "重置", style: UIAlertActionStyle.Default, handler: {(u : UIAlertAction) -> () in            self.reset()        })        lostAlertView.addAction(resetAction)        let cancleAction = UIAlertAction(title: "取消", style: UIAlertActionStyle.Default, handler: nil)        lostAlertView.addAction(cancleAction)        self.presentViewController(lostAlertView, animated: true, completion: nil)    }}

上面代码中的userHasLost和userHasWon方法需要在GameModel中进行判断,这里是通过gameModle进行调用的,接下来看下具体的判断代码:

//如果gamebord中有超过我们定的最大分数threshold的,则用户赢了func userHasWon() -> (Bool, (Int, Int)?) {    for i in 0..
= threshold { return (true, (i, j)) } } } return (false, nil)}//当前gamebord已经满了且两两间的值都不同,则用户输了 func userHasLost() -> Bool { guard getEmptyPosition().isEmpty else { return false } for i in 0..
Bool { let (x, y) = location guard y != dimension - 1 else { return false } if case let .Tile(v) = gamebord[x, y+1] { return v == value } return false}func tileToRightHasSameValue(location: (Int, Int), _ value: Int) -> Bool { let (x, y) = location guard x != dimension - 1 else { return false } if case let .Tile(v) = gamebord[x+1, y] { return v == value } return false}

接下来将之前的setupSwipeConttoller方法放入游戏初始化代码中则可以运行游戏了,在NumbertailGameController类的init方法中添加调用:

init(dimension d : Int , threshold t : Int) {    //此处省略之前代码    setupSwipeConttoller()}

接下来就可以运行游戏了,其他的都是些边边角角的优化了,reset方法什么的,大家可以在github中把代码下下来看就行,这里就不多做介绍了。

这里再讲一点就是之前说的将面板中的数字换成文字,其实很简单,就在TileView中定义一个字典Dictionary<Int,String>,放如值如[2:”我”,4:”的”],在给数字块赋值的时候根据原本的值取出对应的文字赋到数字块上即可。


我的博客:blog.scarlettbai.com

我的微信公众号:读书健身编程

你可能感兴趣的文章
我想先用三个或者五个激光测距做无人机的室内定位和避障
查看>>
pixhawk也可以用Airsim仿真
查看>>
《无人机电机与电调技术》可以看看
查看>>
我发现七月在线的GAAS课程基本都讲到了
查看>>
电机堵转
查看>>
一个真正好的无人机应该是需要自己慢慢去调参的,别人的默认参数是可以飞但是可能达不到perfect的效果。
查看>>
carzepony也在想往FreeRTOS上迁移
查看>>
可以买个好点的电烙铁
查看>>
ACfly调参记录(包括ACfly-F330和ACfly-T265)
查看>>
一定记得每飞几次或者隔一天要把螺丝和浆帽拧一次,确实会松的
查看>>
《多旋翼无人飞行器嵌入式飞控开发指南》里基于FreeRTOS的无人机软件框架
查看>>
我感觉无人机借助于激光雷达实现定点悬停的效果应该好于光流才是
查看>>
思岚A1的SDK其实很好读懂,每个函数清晰明了,可以直接调用
查看>>
六角铜柱的型号
查看>>
pixhawk无GPS时可以在定高或者自稳模式下解锁起飞(见过多次别人说到)
查看>>
pixhawk(PX4)的一些论坛网站(包括中文版的PX4用户手册和PX4开发手册)
查看>>
串级 PID 为什么外环输出是内环的期望?(和我之前对串级PID的总结一样)
查看>>
APM/Pixhawk飞行日志分析入门(苍穹四轴)
查看>>
我刚刚才完全清楚GPS模块的那根杆子是怎么固定安装好的
查看>>
去github里面找找也没有别人无人机+SLAM的工程
查看>>