ios编程技术收集swift项目学习与开发swift

开源项目Swift-2048学习、分析

2015-11-01  本文已影响1946人  疯狂的剁椒鱼头

开源项目Swift-2048学习、分析

[TOC]

这篇博客写了什么?

刚开始使用swift编写ios程序,花了两三天的时间看了下《The Swift Programming Language》,看了就忘了o(╯□╰)o,于是干脆从一个项目入手,一边开发一边学习。github上面找了一个挺有名的开源项目[swift-2048][id],经过一天半的"刻苦"学习,终于略有小成。
[id]: https://github.com/austinzheng/swift-2048 "gitHub"

项目结构

除去xocde7自动生成的一些文件外,austinzheng(2048项目作者),一共就使用7个.swift文件完成了整个项目,让本人第一感觉这个项目挺简单的。接下来就简单的介绍一下每一个文件的大概用处。


代码分析

以文件为单位,对2048项目进行一个简单的分析。

TileView.swift

2048项目里面较为简单的一个文件。TileView也就是2048中可以移动的方块

AccessoryViews.swift

前面项目结构以及分析了,这个文件主要负责获得分数的显示.这个文件也十分的简单,里面主要的类就是 ScoreView它遵守了ScoreViewProtocol协议。因为简单,所以不过多解释.

AppearanceProvider.swift

一个辅助用的,主要用于TileView颜色的控制,简单不多解释.(没有这个文件提供的功能,项目一样可以跑,只是丑点)

GameboardView.swift

一个稍稍复杂的文件,代表游戏面板的view,也就是下面这个黑框框(十分明显的九宫格布局)。当然还实现了一些对TileView的移动、插入等操作。接下来只解释一些主要属性。

NumberTileGame.swift

这个文件就是本项目最主要的一个视图控制器的实现。处理着绝大部分的逻辑。

    func xPositionToCenterView(v: UIView) -> CGFloat {
        let viewWidth = v.bounds.size.width
        let tentativeX = 0.5*(vcWidth - viewWidth)
        return tentativeX >= 0 ? tentativeX : 0
    }       

这个方法还不算复杂,其目的是:计算出能使传入参数v这个view,在控制器中居中显示的x的值。

        func yPositionForViewAtPosition(order: Int, views: [UIView]) -> CGFloat {
        ...
    //所有控件高度之和(包括间距),views.map({ $0.bounds.size.height })将所有view的高度取出,然后通过.reduce对所有高度进行求和。
      let totalHeight = CGFloat(views.count - 1)*viewPadding + views.map({ $0.bounds.size.height }).reduce(verticalViewOffset, combine: { $0 + $1 })
      // 这便是计算出来的views整体的起点y
      let viewsTop = 0.5*(vcHeight - totalHeight) >= 0 ? 0.5*(vcHeight - totalHeight) : 0

      // 然后根据order,数值0,代表第一个view也就是最上面的;数值1就是第二个view(本项目一共只有2个view所以也就是最下面的view)计算出任意一个view的y值
      var acc: CGFloat = 0
      for i in 0..<order {
        acc += viewPadding + views[i].bounds.size.height
      }
      return viewsTop + acc
    }

yPositionForViewAtPosition就比较复杂了。前面已经说明这两个方法是为了让两个view居中显示。yPositionForViewAtPosition就是为了找到能让任意一个view垂直居中的y值。因为在垂直面上有多个view(这里是2个),所以单独凭借一个view是无法计算的,必须把所有的view都传进来,再根据所有view的高度和计算出scoreView应该距离顶部的位置或者GameboardView距离底部的位置。

scoreVIewGameboardView创建完后,调用insertTileAtRandomLocation插入2个tileVIew结束。

GameModel

终于来到2048核心所在!先来简单的看一下所包含的属性。

    struct SquareGameboard<T> {
    
  let dimension : Int  // 面板大小
  var boardArray : [T] // 这里是存储TileObject类型的数组

  init(dimension d: Int, initialValue: T) {
    dimension = d
    boardArray = [T](count:d*d, repeatedValue:initialValue)
  }
    //下标脚本,这样能够快速访问到boardArray任意一个元素(gameboard[0][1])
  subscript(row: Int, col: Int) -> T {
    get {
      assert(row >= 0 && row < dimension)
      assert(col >= 0 && col < dimension)
      return boardArray[row*dimension + col]
    }
    set {
      assert(row >= 0 && row < dimension)
      assert(col >= 0 && col < dimension)
      boardArray[row*dimension + col] = newValue
    }
  }
    ...
}

附上gameboardGameboardView关系图一张

接着继续往下走,下面的代码就是把取出来准备进行移动计算的每一列(行),根据其(Int, Int)类型的数据,取出gameboard对应的每一项TIleObject

      // coords数组存放的顺序也就是移动的顺序
      let tiles = coords.map() { (c: (Int, Int)) -> TileObject in
        let (x, y) = c
        return self.gameboard[x, y]
      }

提供一个思考方式

每当获得tiles这个需要的移动的逻辑tiles,便会开始执行合并操作--调用merge方法.这个方法返回bool类型,只有发生移动/合并操作才会返回true.读到这里,笔者推荐先去看看下文介绍的merge方法的实现再继续看下面的解释。

读到这里,笔者默认你已经读完下面解释merge三个步骤

任然接着算法第三步返回的例子,返回值我还记得是【SingleMoveOrder(1, 0,4,ture),SingleMoveOrder(2,1,2,false)】
接着对返回的数据(操作)进行处理,项目使用的是forin的循环。请看代码注释(先感叹一下,好巧妙的映射方式)

      for object in orders {
        switch object {
        case let MoveOrder.SingleMoveOrder(s, d, v, wasMerge):
          // 是不是已经忘记coords里面是什么了?同样在map映射解释那有写哦
          let (sx, sy) = coords[s]//针对我们的例子这里的值是[(1,3),1,2),1,1),1,0)]。  我们算出来都是2个SingleMoveOrder类型,这里分析合并情况即s=1,所以取出来的是(1,2)
          let (dx, dy) = coords[d] //这里取出来的是(1,3)
          if wasMerge {
            score += v    //这个是总得分,这里会触发属性观察器从而调用代理
          }
          gameboard[sx, sy] = TileObject.Empty  // 设置(跟新)逻辑面板状态
          gameboard[dx, dy] = TileObject.Tile(v)// 设置(跟新)逻辑面板状态
          //到这里,逻辑面板已经移动完毕,接下来就是改变UI了,所以调用下面的方法。这个方法在GameboardView.swift中实现.
          delegate.moveOneTile(coords[s], to: coords[d], value: v)
            
        case let MoveOrder.DoubleMoveOrder(s1, s2, d, v):
          // Perform a simultaneous two-tile move
          let (s1x, s1y) = coords[s1]
          let (s2x, s2y) = coords[s2]
          let (dx, dy) = coords[d]
          score += v
          gameboard[s1x, s1y] = TileObject.Empty
          gameboard[s2x, s2y] = TileObject.Empty
          gameboard[dx, dy] = TileObject.Tile(v)
          delegate.moveTwoTiles((coords[s1], coords[s2]), to: coords[d], value: v)
        }
      }
      

SingleMoveOrderDoubleMoveOrder处理上几乎一致,所以继续分析。到这里,整个项目几乎已经分析完。项目中最难理解的一个是合并算法,另外一个笔者便认为是项目作者设计的一种巧妙的映射方式,最后附上全部映射关系图一张.

//这种情况应用在tile需要移动且需要合并的情况,需要移动的状态包括第一步计算出来的与前面发生过合并导致的
let next = group[idx+1]
let nv = t.getValue() + group[idx+1].getValue()
skipNext = true
tokenBuffer.append(ActionToken.DoubleCombine(source: t.getSource(), second: next.getSource(), value: nv))
case let .NoAction(s, v) where !GameModel.quiescentTileStillQuiescent(idx, outputLength: tokenBuffer.count, originalPosition: s):
//这种情况应用于需要移动但不需要合并,而需要移动是因为前面进行过合并操作造成的
tokenBuffer.append(ActionToken.Move(source: s, value: v))
case let .NoAction(s, v):
//不需要移动且不合并
tokenBuffer.append(ActionToken.NoAction(source: s, value: v))
case let .Move(s, v):
// 仅仅需要移动
tokenBuffer.append(ActionToken.Move(source: s, value: v))
default:
break
}
}
return tokenBuffer
}


    总结一下,合并/移动的分类

    1 tile需要合并但不需要移动的情况,这个种情况在一列/行*tiles*中最多存在一次。   因为因为一旦需要合并,后面的tile都需要进行移动操作。哪怕是在第一步计算中是不需   要移动的。列如[2][2][8][2]---->[4][ ][8][2]--->[4][8][2][],这种情况经   过算法第一步后全是*NoAction*,但是在算法第二步因为前2个[2]会发生合并,所以导   致第二个位置会空从而导致原本NoAction的[8][2]也要移动,所以一旦有发生合并,后   面的都**必须进行移动**.

    2 tile需要移动且需要合并的情况,需要移动的状态包括第一步计算出来的与前面发生过  合并导致的。比如[2][2][4][4]-->[4][ ][4][4]--->[4][8][][]
    
    3 需要移动但不需要合并,而需要移动是因为前面进行过合并操作造成的(参考1)

    4 不需要移动且不合并:列如[2][4][8][16]

    5  仅仅需要移动,比如[2][ ][4][8] -->[2][4][8]

    最后解释一下为什么在第一种分类下是*SingleCombine*而第二种是   *DoubleCombine*。两个类型的不同就在于*DoubleCombine*多了一个*second:*参 数。至于为什么这样?不妨回想一下上面1情况与2情况的分别。没错,区别在与我说的 *tile*是否需要移动,郑重强调:**只要发生合并操作,绝对是需要进行tile的移动 的!绝对需要移动**,是不是感觉奇怪,明明前面说不需要移动。对,我说tile不需要移  动是指*for*循环中**token**代表的当前的那个tile,但是合并是两个tile的事情,    所以*let next = group[idx+1]*取出了下一个tile,而这个利用*idx+1*取出来   的tile是**一定得需要移动的**,而*token*代表的那个tile不一定需要移动,所以: *SingleCombine*是指只要移动一个*tile*的情况,而*DoubleCombine*是值2个需  要合并的*tile*均需要移动!(其实在分析**condense**(算法第一步)的时候已经强 调,算法的步骤是**先移动再合并**)。任然回到算法第一步最后那个例子
    经过第一步计算传入的group内容是【NoAtion(0,2),NoAtion(1,2),NoAtion(2,2)】
    经过第二步计算出来的返回值tokenBuffer是【SingleCombine(1,4),Move(2,2)】

* convert

    这是最后一步了,这里又多出了一个*MoveOrder*枚举,这个枚举把需要进行的操作再度    简化就分为*SingleMoveOrder*与*DoubleMoveOrder*两种操作类别,十分显然 *DoubleMoveOrder*对应的是前面需要进行两个tile移动的且这两个需要合并的操作(    对应算法第二步分类中的2)。*SingleMoveOrder*是单一一个tile的移动操作。啥?你说少了一种合并情况?tile需要合并当不需要移动的操作被吃了?我就问了:算法第二步分类中的1是不是也需要移动一块被合并的tile(用*let next = group[idx+1]*取出的那块)?,这就对了,*SingleMoveOrder*有个参数*wasMerge:*代表的就是需不需要合并。所以*SingleMoveOrder*对应算法第二步中分类的1、3、5。至于4,人家都说了不移动不合并,就让人家好好原地待着。接着例子来,我们看看最后返回的是什么
    经过第二步计算传入的group是【SingleCombine(1,4),Move(2,2)】
    经过第三步计算出返回值moveBuffer是【SingleMoveOrder(1, 0,4,ture),SingleMoveOrder(2,1,2,false)】
    看到这里算法分析基本结束,可以返回去继续看**performMove**方法,看后续操作.
    

## 总结
到这里应该就告一段落了,虽然还有些代码没有分析,但那些是不太重要的东西了。整个项目可以说不难,比较适合初学者,关键是要理解作者设置的映射关系与合并算法,。想要彻底的了解这个2048,最好就是自己从头到尾从零开始写一个。先从搭建界面开始,一步一步慢慢的来。笔者2048项目花了1天办时间重写,而写这篇文章却花了将近三天。如果有时间。可能会继续写关于2048的博文,应该是一步一步的去记录实现2048的步骤。
上一篇 下一篇

猜你喜欢

热点阅读