怎么选一把趁手的教条键盘葡京游戏平台官方?

2048是IOS学习的Demo中深切的话题了。之前为了给后辈们讲一个关于iOS+Swift的讲座,便自个儿付出了一个。Github上倒是已经有了一个工程
austinzheng/swift-2048
,不过最后的三次commit也早已是二零一五年的时候了,有些地点应当早就落伍了啊。

从平凡的施用的话,键盘的运用功效是稍低于你的无绳电话机的。一台五六千的 索爱过了两年恐怕就会硬件落后变得不佳用,但一块好键盘至少可以陪伴您十几年一如既往坚挺。因而,在键盘上投资取得的进项相对是值得的。

那篇教程借使你已经对此Swift的大旨语法只是和Xcode的应用办法有了一个相比较清楚的回味。即便你还不打听这上头的文化的话,指出先去读书以下相关小说进行入门。

无法不必要超前申明,数字键盘带来愉悦感的多少个特点,比如手感、声音、外观,这几个都以以主观判断为根据的玄学,我只能尽只怕讲实际摆数据,真实体验唯有真正上手了才知晓哪位是你的最爱。

前言

其一课程的源代码已经放在了自己的github主页上边:
Game2048,近来并未放License,然而你可以肆意使用本文以及Github工程中的所有源代码。

话题回到项目本身。那几个种类上,我也是运用了经典了MVC架构,即Model-View-Controller。在上边讲解中,我也将着力以那个顺序来介绍代码的结构与逻辑。

什么样是游戏键盘?

根据[维基百科],多功能键盘的概念是:每种按键都由一个独门的微动开关组成的键盘。没懂?没关系,听本身渐渐给你说。

时下市面上的键盘大概可以分成多功能键盘、薄膜键盘、电容键盘。超薄键盘最早被发明出来并大方使用。之后乘机个人电脑走进千万家,万恶的资本主义为了下落资金,选拔便宜的薄膜键盘替代了游戏键盘,薄膜键盘价格低廉,所以你未来才会以为一块键盘就只值几十块钱。

<img
src=”http://upload-images.jianshu.io/upload\_images/651072-1ffc6a8958ce022d.jpeg"&gt;

多功能键盘造价高昂的缘由就是,每一种单独的键位都亟待一个独立的轴体。方今世界上最重点的产商,是源于德国的
Cherry 轴。业界良心的是,为了保险美好的导电性,Cherry轴里的金属触点使用的是金子。因而除了复杂的结构设计之外,这点点纯金也是造价高的缘故之一。

<img
src=”http://upload-images.jianshu.io/upload\_images/651072-da19d9295f62ea42.png"&gt;

预备工作

在这些片段,大家创造工程文件,并简短梳理一下工程的构造以及各类文件的成效。

为何你要求一块超薄键盘?

何以不买平板键盘?贵?听完下边的几个超薄键盘的助益大家再来说价格吧。

始建项目

地点已经宣示,我假如你已经熟谙了Xcode的操作,故那里说的概括一点。使用Xcode成立一个Single
View
Application,然后删除Storyboard相关的始末,大家将会选拔代码来营造页面。然后创立一下文书:

  • Matrix.swift:
    Model部分的代码,在此间大家构建了描述游戏中相继实体的概念模型以及处理游戏操作逻辑的算法
  • Container.swift: View部分的代码,定义了2048游乐操作的面板
  • Tile.swift:
    大家称2048戏耍中的一个格子为一个Tile,这些文件即为Tile的View
  • ColorProvider.swift:
    大家将游乐中的颜色控制部分单独了出去,使得样式的轮换尤其便民
  • Other.swift:其余的相助代码
  • Constant.swift: 某些常量定义在那里

除开,大家还选用了一部分第三方代码库,那几个库我们由此CocoaPod来安装,Podfile的情节为:

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.0'
use_frameworks!

target 'Game2048' do
    pod 'SnapKit', '~> 3.0'
    pod 'Dollar'
end

运行pod install来安装那一个看重。

首先,键位争辩

薄膜键盘的做事原理是在键盘中铺设了纵横排步的电路线,交叉的岗位就是键位,当键位按下时就接触开关,键盘的决定芯片通过检测所有的队列线,判断哪一行哪一列的点位被打开,并将那个信号传递给主机。明显,当您同时按下四个键位时,芯片并不可能分别某些点位的开关状态,也就无法将按键正确的景况传递回主机。

说回超薄键盘,由于每种键位都以一个独门的开关,键位按下就发生一个独自的信号。所以理论上,平板键盘是足以完毕同时按下整个键盘都被正确识其余,相当于「全键无冲」。

但就此说「理论上」,是因为尽管大家摆脱了按键本人的界定,键盘和主机的通讯协议又面临着新的限制。早期选择的
PS/2 协议可以将键盘暴发的装有信号都传送给主机,以往利用的 USB
协议中,键盘只可以以固定的频率定期向长机发送 8
个字节长度的数据包,首个字节预留给键盘左右两侧的 Ctrl、Alt、Shift 和
Win
Key(约等于十分有微软旗标的按键),第三个字节预留出来不行使,可以行使的就只有剩下的
6 个字节,因而使用 USB 协议的键盘最两只好同时触发 14
个按键。但鉴于第四个字节的修饰键一般不作数,由此一般的宣传语都选用「六键无冲」的布道。

乘势技术的前进,USB
由于其通用性好、占用空间小、可热插拔等居多亮点,逐步取代了 PS/2
插孔。因而真正意义上的全键无冲的键盘,已经逐步消散在历史的历程中了。

早期选用的 PS/2 接口

Model部分 – 营造起描述游戏的概念模型

第二,手感

商讨多功能键盘不可防止的话题就是,手感。数字键盘和薄膜键盘放在一起比手感,就好比苍井空(日文名:蒼井そら)和凯拉·奈特莉……(苍先生还德艺双馨呢

薄膜键盘通过键位下方的胶膜提供回弹力,压力是线性相关的,而且下方的薄膜触点寿命短,因而一再地采纳两7个月就会变得软塌塌无力。

机械轴的压力转移曲线图

超薄键盘通过很是的宏图,使得敲击可以取得非线性的压力转移和段落感,因而在叩击时方可拿到清晰明确、直接便捷的力回馈。那样的利益一来是可以幸免误触,二来就是可以痛快分享啪啪啪的敲击声。码农的性生存少但可以在此地拿到多少补偿嘛。

中央数据表示 – Matrix

2048娱乐中,大家根本须求处理的是一个矩形的数据结构,为了便于的存储和拍卖数据,大家创制一个名为Matrix的结构体:

struct Matrix {
      private let dimension: Int
      private var elements: [Int]

      /// 初始化函数,创建一个Matrix结构体
    ///
    /// - Parameters:
    ///   - d: 游戏中矩阵的维数,一般是4
    ///   - initialValue: 被创建的矩阵中每个元素的初始值
    init(dimension d: Int, initialValue: Int = 0) {
        dimension = d
        elements = [Int](repeating: initialValue, count: d * d)
    }

    func getDimension() -> Int {
        return dimension
    }

    func asArray() -> [Int] {
        return elements
    }
}

其考虑并不复杂,Matrix里面包裹的依旧是一个一维数组。为了让这几个Matrix可见就如Matlab等次第中的矩阵一样可以用二元数的主意访问,大家给它助长如下的代码:

subscript(row: Int, col: Int) -> Int {
        get {
            assert(row >= 0 && row < dimension)
            assert(col >= 0 && col < dimension)
            return elements[row * dimension + col]
        }

        set {
            assert(row >= 0 && row < dimension)
            assert(col >= 0 && col < dimension)
            elements[row * dimension + col] = newValue
        }
    }

再就是为了传递参数的惠及,大家将二元数定义成一个特定的门类,方便参数传递。Matrix外部加上

typealias MatrixCoordinate = (row: Int, col: Int)
// 定了一个特殊的二元数作为空坐标
let kNullMatrixCoordinate = MatrixCoordinate(row: -1, col: -1)

然后在Matrix中加上:

subscript(index: MatrixCoordinate) -> Int {
    get {
        let (row, col) = index
        return self[row, col]
    }

    set {
        let (row, col) = index
        self[row, col] = newValue
    }
}

终极我们还给Matrix加上一些实用的工具函数,用于查询和插入

    /// 将矩阵的所有元素置零
    mutating func clearAll() {
        for index in 0 ..< (dimension * dimension) {
            elements[index] = kZeroTileValue
        }
    }


    /// 将元素的值插入到矩阵的指定位置,注意这个函数只能给原来为空的位置赋值
    ///
    /// - Parameters:
    ///   - position: 坐标
    ///   - value: 插入的值
    mutating func insert(at position: MatrixCoordinate, with value: Int) {
        if isEmpty(at: position) {
            self[position] = value
        } else {
            assertionFailure()
        }
    }


    /// 矩阵指定位置是否为空(为空即是指此处为0)
    ///
    /// - Parameter position: 指定位置
    /// - Returns: 是否为空
    func isEmpty(at position: MatrixCoordinate) -> Bool {
          // kZeroTileValue定义在Constant.swift里面,为0
        return self[position] == kZeroTileValue
    }


    /// 获取矩阵中所有为空的位置
    ///
    /// - Returns: 列表形式的坐标集合
    func getEmptyTiles() -> [MatrixCoordinate] {
        var buffer: [MatrixCoordinate] = []
        for row in 0..<dimension {
            for col in 0..<dimension {
                let pos = MatrixCoordinate(row: row, col: col)
                if isEmpty(at: pos) {
                    buffer.append(pos)
                }
            }
        }
        return buffer
    }


    /// 矩阵中元素的最大值
    var max: Int {
        get {
            return elements.max()!
        }
    }


    /// 矩阵中所有元素的和
    var total: Int {
        return $.reduce(elements, initial: 0, combine: { $0 + $1 })
    }

迄今,大家已毕了对游戏数量表达的肤浅,即将2048中的4*4矩阵用Matrix来表示,并在那个结构体上定义了有利于的走访格局和工具函数。

第三,寿命

本人的第一块平板键盘是 2012 年购买的 Cherry G80-3000
青轴,到明天依然如新。

我的首先块多功能键盘,于今还在服役

机械轴的平分利用次数大概是 2000 万次—5000 万次。瞅着数字可能没什么概念?

假如你天天要求敲敲 10000 次键盘,并且均匀分布在 26
个假名上,则平均每一种键每日被打击约 385 次,这意味着你可以选拔大致 51948
天,大概 140 年。

哼,什么一颗钻石永流传。一块好键盘才是国粹的首选嘛。

汇总,即使数字键盘的价格相比高,但跟它的人品相比较,真的高呢?

Model层对外封装结构

然后大家定义Model要求对Controller暴露的操作接口。定义一个新的GameModel类。

class GameModel {
    private var matrix: Matrix
    var dimension: Int {
        get {
            return matrix.getDimension()
        }
    }
    let winningThreshold: Int

    /// 分数
    var score: Int {
        return matrix.total
    }

    init (dimension: Int = 4, winningThreshold threshold: Int = 2048) {
        matrix = Matrix(dimension: dimension)
        winningThreshold = threshold
    }
}

设想2048的游戏规则,Model层应该对上层提供如下那么些接口:

  • 在一个自由空地点插入一个新的格子
  • 在指定地方插入一个指定值
  • 判断用户是还是不是胜利
  • 判定用户是还是不是业已失利
  • 对用户的光景左右滑动操作做出响应
  • 重置游戏

// 在一个指定位置插入一个指定的值
func insertTile(at position: MatrixCoordinate, with value: Int) {
}
// 在一个随机空位置插入一个随机的值,按照一般的规则,随机的插入2或者4,其中2的概率要远大于4
func insertTilesAtRandonPosition(with value: Int) -> Int {
}
// 用户是否已经胜利
func userHasWon() -> Bool {
}
// 用户是否已经失败
func userHasLost() -> Bool {
}
// 响应用户操作,注意这里我们引入了新的MoveCommand和MoveAction的概念,这个我们会在后面详细解释
func perform(move command: MoveCommand) -> [MoveAction] {
}

上面大家来逐个表明各种协会的效果和落实。

什么挑选适合本身的教条键盘?

见状此间您还一向不怒而退出可能点开评论喷我,表明你是由衷想买键盘。那么怎么选键盘才能买到适合自身的吧?

在指定地方插入指定值

以此接口已毕分外简单,因为大家早就在Matrix类中落到实处了看似的接口。故在那里大家只需求调用对应的函数即可。

func insertTile(at position: MatrixCoordinate, with value: Int) {
    matrix.insert(at: position, with: value)
}

报表来源:Wikipedia

广大的轴有黑轴、红轴、青轴、茶轴,以及各式停产了仍旧生产较少的异型轴,在上面那张表格中,大家须求重点关怀的就是压力和段落感。

压力表示触发按键时急需的能力,压力越大,手感越硬朗。

段落感表示的是接触按键时,按下任何键程的中途就会有一种恍若开关被按下的手感。

我对那三种主流轴的下结论就是:

  • 青轴:段落感最分明,敲击声也最大,是游戏键盘的代表轴。适合打字使用。
  • 黑轴:直上直下,没有确定性的段落感,噪音也较小。适合游戏使用。
  • 茶轴:各地点都相比较平均的轴,轻微段落感,噪音较小。兼顾游戏和打字。
  • 红轴:最青春的主流轴,手感类似黑轴,但压力更小。兼顾游戏和打字。

故此,想要一块多功能键盘来炫耀和体会心旷神怡手感,我引进您拔取青轴,在办公使用请务必小心被人浇咖啡;倘诺不想在办公吸引仇恨,茶轴则是你的一流选项,低调但中庸,适合长日子敲击。倘诺是一日游玩家,我推荐您采纳黑轴大概红轴,二者的界别紧要在于压力大小,可以依照自身的软妹指数选取。

除了,原厂还有白轴和奶轴,曾停下生产一段时间,最近又过来了供应,但产量极少,并且优先要求自身键盘使用。还有各类各种国产轴,那么些本身就不推荐你购买了……

在一个无限制空位插入一个即兴的值

处于程序设计中函数应当有限援助短小精悍的口径,为了落到实处这些效应,大家增添几个工具函数:

// 这个函数会返回插入的位置,返回的格式为matrix内部一维数组定义下的index
func insertTilesAtRandonPosition(with value: Int) -> Int {
        let emptyTiles = matrix.getEmptyTiles()
        if emptyTiles.isEmpty {
            return -1
        }
        let randomIdx = Int(arc4random_uniform(UInt32(emptyTiles.count - 1)))
        let result = emptyTiles[randomIdx]
        insertTile(at: emptyTiles[randomIdx], with: value)
        return coordinateToIndex(result)
}

func coordinateToIndex(_ coordincate: MatrixCoordinate) -> Int {
        let (row, col) = coordincate
        return row * dimension + col
}

// 工具函数,按照预定的概率生成2或者4
func getValueForInsert() -> Int {
        if uniformFromZeroToOne() < chanceToDisplayFour {
            return 4
        } else {
            return 2
        }
    }

    func uniformFromZeroToOne() -> Double {
        return Double(arc4random()) / Double(UINT32_MAX)
    }

键帽

说完轴体,说说键帽。键帽就是我们敲击键盘时,和手指亲密接触的小玩意儿。可以拆下来清洗(洁癖的精神世界拿到知足与升华)。

那么键帽又有怎么着讲究呢?常见的键帽根据材质首要有二种:ABS、POM 和 PBT。

ABS 最广泛,你懂的,最广大就是开支低。ABS
的毛病至极无人不晓——简单打油,使用一八个月就会油光焕发,拿酒精擦都擦不根本(摊手

由此,游戏键盘一般都会利用 POM 可能 PBT 材质的键帽。POM
材料质量坚硬,耐用性和抗打油性都出色好,常见于 切莉 的原厂键盘中。PBT
则是一种更优质的塑料,坚硬度比 POM
更高,耐用抗打油,抗氧化抗腐蚀成效也不行不错。从手感上来说,PBT
的手感相比粗糙,摩擦感更强一些,POM 的外表比较细腻。

判定用户是还是不是胜利

此间只需求看清matrix中的最大值是还是不是达标了给定的阈值即可:

func userHasWon() -> Bool {
    return matrix.max >= winningThreshold
}

字母刻印

字母刻印就是键盘上的字母印刷的方法。将来相比较常用的字符印刷形式主要为正刻、同刻、侧刻、无刻。

正刻和同刻类似,都以直接印刷到键帽正面,不一致是同刻是阴刻,字符会凹陷进去;侧刻就是把字符印刷在键帽的侧面;无刻就是一心不作任何印刷。

作为我个人的见解,如若得以流畅的拔取盲打作为一般输入,那么本人强力推荐您购买一套无刻的键帽。终究真的是赏心悦目嘛。

判定用户是还是不是曾经破产

那么些逻辑要相对复杂一点,用户失利时,即用户无论怎么操作矩阵都不会暴发变化。依照规则,用户战败应当满足上边多个条件

  1. 装有的格子都早就填满
  2. 随机一个格子和其附近格子都心有余而力不足统一
    这一进度可以形成上面的代码。代码的逻辑并不至极复杂,可以透过阅读源代码进行驾驭。

    /// 用户是已经获胜
    func userHasWon() -> Bool {
        return matrix.max >= winningThreshold
    }


    // 用户已经失败
    func userHasLost() -> Bool {
        return !isPotentialMoveAvaialbe()
    }


    /// 用户是否还有可以移动的步骤
    func isPotentialMoveAvaialbe() -> Bool {
        var result: Bool = false
        for row in 0..<dimension {
            for col in 0..<dimension {
                result = result || isTileMovable(at: MatrixCoordinate(row: row, col: col))
                if result {
                    break
                }
            }
        }
        return result
    }


    /// 指定的格子是否还可以移动
    func isTileMovable(at tileCoordincate: MatrixCoordinate) -> Bool {
        let val = matrix[tileCoordincate]
        if val == kZeroTileValue {
            return true
        }
        let neighbors = getNeightbors(around: tileCoordincate)
        var result: Bool = false
        for index: MatrixCoordinate in neighbors {
            let fetchedVal = matrix[index]
            result = result || (fetchedVal == val) || fetchedVal == kZeroTileValue
            if result {
                break
            }
        }
        return result
    }

    /// 获取一个格子的相邻格子
    func getNeightbors(around tileCoordincate: MatrixCoordinate) -> [MatrixCoordinate] {
        let (row, col) = tileCoordincate
        var result: [MatrixCoordinate] = []
        if row - 1 > 0 {
            result.append(MatrixCoordinate(row: row - 1, col: col))
        }
        if row + 1 < dimension {
            result.append(MatrixCoordinate(row: row + 1, col: col))
        }
        if col - 1 > 0 {
            result.append(MatrixCoordinate(row: row, col: col - 1))
        }
        if col + 1 < dimension {
            result.append(MatrixCoordinate(row: row, col: col + 1))
        }
        return result
    }

剁手路线

好了,后边说那么多,上边的部分就实在进入剁手的环节,数字键盘这么好,不买永远感受不到。

试探 | WASD Cherry 试轴器

眼下介绍了如此多轴,到底哪一款真正适合您依然要亲自上手试试。WASD
的那款试轴器就是一个很好的工具。用完了将来,摆在电脑旁也是一个美美的摆件。

试水 | Ducky 2108s

Ducky
来自宝岛湖北,江湖人称「魔力鸭」。那把来自广西的鸭子在键盘界中算不上高端,但手感中规中矩。凭借特出的性价比在人世中掀起了不小的腥风血雨。

入门 | Cherry G80-3000

现阶段市面上常见的教条键盘,大多数使用的都是由德意志 Cherry 公司供应的 MX
机械轴,由此 Cherry也被江湖人亲切地喻为「原厂」。德意志联邦共和国人创立业的用功和认真都可以在原厂键盘上感受到,尽管你不爱好原厂古朴的安插性,也很值得购买一把来珍藏,手指轻抚在按键上的快感是其余键盘不能够相比的。

原厂键盘我引进那款 G80-3000
青轴,亚马逊(Amazon)对它的推荐语是「爽快清脆的段落感如冬日般舒畅(Jennifer)」,但实则即使在宿舍或许办公室中利用,却很不难被群殴成狂躁的伏季。就算这把键盘的造型依旧上个世纪的样式,但那丝毫不影响它变成最热销的游戏键盘之一。
本篇小说就是行使那把键盘打出去的,洋洋洒洒 2000
多字,除去中途才思堵塞,其余时候都如行云流水般满面红光。极度适合新手入坑时作为第一块键盘。

旁门 | Logitech G710+

游玩玩家一定对雷蛇那个牌子不生疏,作为江湖中专攻一路的高手,多年来为电子竞赛提供了丰硕可相信的硬件。我推荐的那款茶轴键盘回弹明确,同时还有两个可编程的按键和传媒控制按键,可以提供更高兴的游乐体验。适合平常玩游戏比较多的雄鹰入手。

进阶 | Poker II

Poker II 是邻里选手,由一个境内的键盘咳嗽友创造的品牌 KBC
研发,那些品牌齐全是 The Keyboard to Cheer you
up,一般那种长的名字都会比较拉风,因而即使 Poker II
采纳了奇特的键盘布局,也仍旧俘获了很多键盘爱好者的心。

我推荐的 Poker II
与第一代相比,底部带钢板,手感富饶,回弹力非常振奋。不过键位布局比较独特,须求自然时间适应,盲打已成肌肉纪念的请小心出手。

高阶 | FILCO Majestouch 2 红轴

FILCO
也是一方霸主,来自日本,武术分外细腻优雅。纵然修炼的内功也是原厂轴,但利用的键帽会比较别致,造型也特别精细雅观。

FILCO 门中的悍将分外多,我引进那把 NINJA 87 Majestouch 2
红轴,敲击感明确、回弹有力,红轴的键盘手感虽不如青轴茶轴那般清爽,但分外适合长期输入并且混合使用的须要。

入魔 | QwerkyWriter

行使原厂青轴,打字机式超薄键盘,完美复刻了观念打字机的形态。除了略微昂贵的价位,完全找不到理由不买啊!

重置游戏

重置游戏只须要把matrix中的数值清空即可

    func clearAll() {
        matrix.clearAll()
    }

题外话

只怕你觉得花了一千多大洋买了一块键盘就是骨灰玩家了,嗯哼哼其实远远没有。就单拿键帽来说,除了文中介绍的三种主流塑料材质之外,还有金属键;而字符印刷的不二法门则越是铺天盖地,激光蚀刻、UV覆膜、热升华、二色等等,造价也是一个比一个高,配一套下来都够你再买一把键盘了。除了键帽可以烧,还有背光灯、手托、蓝牙5.0、定制轴、定制模具……同理可得,那就是一个深不见底的大坑。入坑有危害,剁手请慎重。

但正所谓「穷玩车,富玩表,屌丝玩电脑」,数码产品再怎么烧,也花不了多少个钱的哇。

对用户的前后左右滑行操作做出响应
娱乐逻辑分析

其一有些关联到的就是游戏逻辑的中央了。通过对游戏规则的发现2048标题有如下的特点:

  • 向某一个样子滑动时,沿该方向的逐一列之间相互独立,故可以将五遍滑动暴发的二维格子移动合并难题,转化成为多少个独立求解的一维格子队列的运动和统一难点。
  • 不一样的滑行方向,其实逻辑规则是在转动操作下是等价的。
    综上所述,我们可以将游戏中针对用户操作方向做出响应统计matrix矩阵的新值这样一个二维标题,分解为多少个线性难题的三结合。例如,若某一个操作之后matrix所代表的游艺中格子分布为:
    2 | 2 | 0 |4
    4 | 0 | 2 | 0
    0 | 0 | 0 | 0
    0 | 0 | 0 | 0
    此时用户向左滑动,则求解新的矩阵数值分布能够解释为七个子难题:即[2,
    2, 0 4], [4, 0, 2, 0], [0, 0, 0, 0], [0, 0, 0,
    0]。而且,由于旋转等价性,我们可以将顺序方向的滑行全部都表达为一维的,向左合并的子难题。为了更形象的认证那或多或少,仍旧参照上边给出的事例。若用户向上滑动,则足以分解为[2,
    4, 0, 0], [2, 0, 0, 0], [0, 2, 0, 0], [4, 0, 0,
    0]两个问题的。

完了了上述难点的架空和简化将来,大家来重点分析一维的,向左合并的简化难点。那些题材的求解,可以分解成二种操作:一是从左到右,移除非零数字之间的零,大家称之为condense;二是将紧邻的极度数字进行合并,大家称之为collapse。一般只必要condense
— collapse两步即可,少数场合下须要最后额外开展四遍condense,例如[2, 2,
2, 2],collapse落成之后拿到[4, 0, 4,
0],需求再拓展一回condense才能成为[4, 4, 0, 0]。

在编程的时候,condense是一个丰富便于完成的操作。我们只需求将待处理的数组中的非零成分依照原先的一一放到新数组里面就能够了。

用户操作的意味和达成

在前一部分的辨析中我们发现,分化倾向的滑动,都足以解释为多少个一维题材,只是不一样的滑动方向下,一维难题的表达格局,以及将各样解出的结果还原为二维矩阵的主意不一样。而一维题材的求解方法是平等的。那种特点适合于选取多态的筹划格局。即大家定义一个基类MoveCommand,在内部落到实处一维标题求解的算法,而把一维难点的领到和回复的算法放在各样滑动方向对应的子类中达成:

/// 移动指令,代表用户在屏幕上的一次滑动
class MoveCommand {
    /**
     * 我们使用了多态来处理不同的滑动指令。
     * 为了解决2048这个发生在二维空间的问题,我们需要将问题进行降维。下面以四维情况为例来说明。
     * 
     * 无论用户想那个方向滑动,格子的变化,总是沿着用户滑动的方向进行,即格子其他处于同一用户滑动方向直线上格子发生交互(合并),而与其他
     * 平行的直线上的格子无关。那么我们可以在用户滑动发生时,将矩阵按照用户滑动方向划分成多个组,然后在每组中独立的解决一维的合并问题。例如
     * 下面的矩阵情形
     *  |0  |0  |2  |2  |
     *  |0  |0  |2  |2  |
     *  |0  |0  |2  |2  |
     *  |0  |0  |2  |2  |

     * 当用户向左侧滑动是,可以将上面的矩阵拆解成|0  |0  |2  |2  |的一维问题进行求解。
     * 而且容易发现,对于用户的不同滑动方向,只是一维问题分解的方式不同,求解一维问题的方法是一致的。我们用多态来实现这种复用。
     */

    // 还原
    func getCoordinate(forIndex index: Int, withOffset offset: Int, dimension: Int) -> MatrixCoordinate {
        fatalError("Not implemented")
    }

    // 提取一维问题
    func getOneLine(forDimension dimension: Int, at index: Int) -> [MatrixCoordinate] {
        fatalError("Not implemented")
    }

    // condense
    func getMovableTiles(from line: [Int]) -> [MovableTile] {
        var buffer: [MovableTile] = []
        for (idx, val) in line.enumerated() {
            if val > 0 {
                buffer.append(MovableTile(src: idx, val: val, trg: buffer.count))
            }
        }
        return buffer
    }

    // collapse
    func collapse(_ tiles: [MovableTile]) -> [MovableTile] {
        var result: [MovableTile] = []
        var skipNext: Bool = false
        for (idx, tile) in tiles.enumerated() {
            if skipNext {
                skipNext = false
                continue
            }
            if idx == tiles.count - 1 {
                var collapsed = tile
                collapsed.trg = result.count
                result.append(collapsed)
                break
            }

            let nextTile = tiles[idx + 1]
            if nextTile.val == tile.val {
                result.append(MovableTile(src: tile.src, val: tile.val + nextTile.val, trg: result.count, src2: nextTile.src))
                skipNext = true
            } else {
                var collapsed = tile
                collapsed.trg = result.count
                result.append(collapsed)
            }
        }
        return result
    }
}

在下边的代码中,大家引入了MovableTile那个类。其成效是描述格子在两遍滑动操作中的变化进度。

/// 矩阵变化过程中描述每一个格子的数据结构,可以记录格子的移动,合并,消失,以及值的改变
struct MovableTile {

    /// 源位置
    var src: Int

    /// 取值
    var val: Int

    /// 目标位置
    var trg: Int = -1


    /// 如果此值非负,则意味着这个结构体描述了一个合并过程,并且这个src2代表参与合并的另一个格子,为默认值-1时,则意味着只是单纯的格子移动,没有发生合并
    var src2: Int = -1

    init (src: Int, val: Int, trg: Int = -1, src2: Int = -1) {
        self.src = src
        self.val = val
        self.trg = trg
        self.src2 = src2
    }


    /// 这个格子是否实际发生了移动。
    ///
    /// - Returns: 是否需要移动
    func needMove() -> Bool {
        return src != trg || src2 >= 0
    }
}

接下去,大家必要贯彻不一样滑动方向对应的子类,其促成逻辑万分直观,读者可以自身精晓一下:

class UpMoveCommand: MoveCommand {
    override func getOneLine(forDimension dimension: Int, at index: Int) -> [MatrixCoordinate] {
        return (0..<dimension).map({ MatrixCoordinate(row: $0, col: index) })
    }

    override func getCoordinate(forIndex index: Int, withOffset offset: Int, dimension: Int) -> MatrixCoordinate {
        return MatrixCoordinate(row: offset, col: index)
    }
}

class DownMoveCommand: UpMoveCommand {
    override func getOneLine(forDimension dimension: Int, at index: Int) -> [MatrixCoordinate] {
        return super.getOneLine(forDimension: dimension, at: index).reversed()
    }

    override func getCoordinate(forIndex index: Int, withOffset offset: Int, dimension: Int) -> MatrixCoordinate {
        return MatrixCoordinate(row: dimension - 1 - offset, col: index)
    }
}

class LeftMoveCommand: MoveCommand {
    override func getOneLine(forDimension dimension: Int, at index: Int) -> [MatrixCoordinate] {
        return (0..<dimension).map({ MatrixCoordinate(row: index, col: $0) })
    }

    override func getCoordinate(forIndex index: Int, withOffset offset: Int, dimension: Int) -> MatrixCoordinate {
        return MatrixCoordinate(row: index, col: offset)

    }
}

class RightMoveCommand: LeftMoveCommand {
    override func getOneLine(forDimension dimension: Int, at index: Int) -> [MatrixCoordinate] {
        return super.getOneLine(forDimension: dimension, at: index).reversed()
    }

    override func getCoordinate(forIndex index: Int, withOffset offset: Int, dimension: Int) -> MatrixCoordinate {
        return MatrixCoordinate(row: index , col: dimension - 1 - offset)
    }
}
实现GameModel中的接口

有了上述准备,大家得以出手完成GameModel中的接口了。把下部的函数添加到GameModel

    /// 执行一个移动命令
    func perform(move command: MoveCommand) -> [MoveAction] {
        // 最后生成的可供UI解析的移动命令
        var actions: [MoveAction] = []
        var newMatrix = matrix
        newMatrix.clearAll()
        // 逐行或者逐列进行遍历(具体取决于滑动方向)
        (0..<matrix.getDimension()).forEach { (index) in
            // 提取出一维问题,注意这里提取的是列或者行中所有格子的坐标
            let tiles = command.getOneLine(forDimension: matrix.getDimension(), at: index)
            // 取出各个格子中的值
            let tilesVals = tiles.map({ matrix[$0] })
            // 进行condense-collapse-condense操作
            let movables = command.collapse(command.getMovableTiles(from: tilesVals))
            // 将movable tiles转化成move action
            for move in movables {
                let trg = command.getCoordinate(forIndex: index, withOffset: move.trg, dimension: matrix.getDimension())
                newMatrix[trg] = move.val
                if !move.needMove() {
                    continue
                }
                let src = command.getCoordinate(forIndex: index, withOffset: move.src, dimension: matrix.getDimension())
                if move.src != move.trg {
                    let action = MoveAction(src: src, trg: trg, val: -1)
                    actions.append(action)
                }
                if move.src2 >= 0 {
                    let src2 = command.getCoordinate(forIndex: index, withOffset: move.src2, dimension: matrix.getDimension())
                    actions.append(MoveAction(src: src2, trg: trg, val: -1))
                    actions.append(MoveAction(src: kNullMatrixCoordinate, trg: trg, val: move.val))
                }
            }
        }
        // 应用计算完之后的结果
        self.matrix = newMatrix
        newMatrix.printSelf()
        // 将需要UI执行的变化返回
        return actions
    }

此处大家又引入了一个新的类MoveAction,那一个类其实是对MovableTile的一个疏理。在目前大家提到了,当MovableTile可以描述在滑行进程中实际格子的变动。诚然,单个格子的位移大家得以一向运用MovableTile个中的数据操纵UI,不过在发生合并是快要麻烦很多了。出于那么些原因大家引入了新的MoveAction,并且保险各个MoveAction只对应UI中的一个格子的一个移动。其定义如下:

struct MoveAction {
    var src: MatrixCoordinate
    var trg: MatrixCoordinate
    var val: Int

    init(src: MatrixCoordinate, trg: MatrixCoordinate, val: Int) {
        self.src = src
        self.trg = trg
        self.val = val
    }
}

专注这么些中和MovableTile的一个要害不一致时撤废了src2其一特性。
对此由一个MovableTile意味着的五个格子的合并进程(即src2不为-1),我们当然地将其解说为多少个子动作,分别是七个单纯移动和一个新的格子现身。对于唯有移动而值不发生变化的格子,大家将其MoveActionval安装成-1,对于新出现的格子,大家将其src设置成-1。当然,假使被联合的五个格子其中有一个并未运动,那么就只会扭转一个格子移动和一个新格子爆发的MoveAction

View部分

View部分相对相比简单,终归唯有一个页面。View部分只涉嫌到五个类,分别是ContainerTileView(格子)。

TileView

格子相比简单,除了背景以外只须要展示一个数字。我在此处运用了SnapKit这么些AutoLayout库,大家可以在github上阅读以下表达。也十分推荐大家在协调的Project中利用这么些库。

class TileView: UIView {
    // 显示数字
    var valLbl: UILabel!

    // 在矩阵中的位置,row * dimension + col
    var loc: Int = -1

    // 颜色配置
    var color: ColorProvider!

    // 数值
    var val: Int = 0 {
        didSet {
            valLbl.text = "\(val)"
            backgroundColor = color.colorForValue(val)
            valLbl.textColor = color.textColorForVal(val)
        }
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        configureValLbl()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError()
    }

    func configureBackground() {
        layer.cornerRadius = 2
    }

    func configureValLbl() {
        valLbl = UILabel()
        valLbl.font = UIFont.systemFont(ofSize: 25, weight: UIFontWeightBold)
        valLbl.textColor = .black
        valLbl.textAlignment = .center

        addSubview(valLbl)

        valLbl.snp.makeConstraints { (make) in
            make.edges.equalTo(self)
        }
    }
}

其一相比简单,就不多说了。

Container

Container选取了相对相比越发的规划格局,使得大家在活动格子的时候的代码操作会相比简单。总的来说,以UIStackView为着力,在方形的UIStackView容器内,放入五个横条状的UIStackView,再在其次级UIStackView内放置方格。注意,那里放入的方格并非之后用户操作移动的带数值的方格,而是空白的,没有数字突显的”placeholder
tile”,其意义是符号方格地点。当我们需求把一个带数字的格子移动到某个地方时,就把其与该岗位的placeholder使用Autolayout对齐起来。
本着下边的描述,诸位可以参照上面的代码来领会一下。

class Container: UIViewController {

    var data: GameModel
    var color: ColorProvider

    let tileInterval: CGFloat = 5
    let horizontalMargin: CGFloat = 20
    let tileCornerRadius: CGFloat = 4
    let boardCornerRadius: CGFloat = 8

    let panDistanceUpperThreshold: CGFloat = 20
    let panDistanceLowerThreshold: CGFloat = 10

    var board: UIStackView!
    var tileMatrx: [UIView] = []
    var foreGroundTiles: [Int: TileView] = [:]
    var scoreLbl: UILabel!
    var restartBtn: UIButton!

    var needsToBeRemoved: [UIView] = []

    init(dimension: Int, winningThreshold: Int) {
        data = GameModel(dimension: dimension, winningThreshold: winningThreshold)
        color = DefaultColorProvider()
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        configureBoard()
        configureTileMatrix()
        configureScoreLbl()
        configureGestureRecognizers()
        configureRestartBtn()

        restart()
    }

    func configureRestartBtn() {
        restartBtn = UIButton()
        restartBtn.addTarget(self, action: #selector(restart), for: .touchUpInside)
        view.addSubview(restartBtn)
        restartBtn.setTitle("Restart", for: .normal)
        restartBtn.setTitleColor(.white, for: .normal)
        restartBtn.backgroundColor = color.tileBackgroundColor()
        restartBtn.layer.cornerRadius = 6
        restartBtn.snp.makeConstraints { (make) in
            make.right.equalTo(board)
            make.top.equalTo(view).offset(20)
            make.width.equalTo(70)
            make.height.equalTo(30)
        }
    }

    func configureScoreLbl() {
        scoreLbl = UILabel()
        scoreLbl.textColor = .black
        scoreLbl.font = UIFont.systemFont(ofSize: 24, weight: UIFontWeightBold)
        scoreLbl.text = "0"
        view.addSubview(scoreLbl)
        scoreLbl.snp.makeConstraints { (make) in
            make.centerX.equalTo(view)
            make.bottom.equalTo(board.snp.top).offset(-20)
        }
    }

    func configureBoard() {
        board = UIStackView()
        view.addSubview(board)
//        board.backgroundColor = color.boardBackgroundColor()
        board.alignment = .center
        board.distribution = .fillEqually
        board.axis = .vertical
        board.spacing = tileInterval

        board.snp.makeConstraints { (make) in
            make.left.equalTo(view).offset(horizontalMargin)
            make.right.equalTo(view).offset(-horizontalMargin)
            make.height.equalTo(board.snp.width)
            make.centerY.equalTo(view)
        }

        let boardBackground = UIView()
        boardBackground.backgroundColor = color.boardBackgroundColor()
        board.addSubview(boardBackground)
        boardBackground.layer.cornerRadius = boardCornerRadius
        boardBackground.snp.makeConstraints { (make) in
            make.edges.equalTo(board).inset(-tileInterval)
        }
    }

    func configureTileMatrix() {
        for _ in 0..<getDimension() {
            let stack = UIStackView()
            board.addArrangedSubview(stack)
            configureHorizontalStackViews(stack)
            for _ in 0..<getDimension() {
                let tile = createTilePlaceholder()
                stack.addArrangedSubview(tile)
                tile.snp.makeConstraints({ (make) in
                    make.height.equalTo(tile.snp.width)
                })
                tileMatrx.append(tile)
            }
        }
    }

    func configureHorizontalStackViews(_ stackView: UIStackView) {
        stackView.backgroundColor = .clear
        stackView.spacing = tileInterval
        stackView.axis = .horizontal
        stackView.alignment = .center
        stackView.snp.makeConstraints { (make) in
            make.left.equalTo(board)
            make.right.equalTo(board)
        }
    }

    func createTilePlaceholder() -> UIView {
        let tile = UIView()
        tile.backgroundColor = color.tileBackgroundColor()
        tile.layer.cornerRadius = tileCornerRadius
        return tile
    }

    func getDimension() -> Int {
        return data.dimension
    }

    func updateScore() {
        scoreLbl.text = "Score: \(data.score)"
    }

    // 创建手势识别器,用来识别用户的滑动操作
    func configureGestureRecognizers() {
        createGestureRecognizer(withDirections: [.up, .down, .right, .left]).forEach({ view.addGestureRecognizer($0) })
    }

    func createGestureRecognizer(withDirections directions: [UISwipeGestureRecognizerDirection]) -> [UIGestureRecognizer]{
        return directions.map({ (dir) -> UIGestureRecognizer in
            let swipe = UISwipeGestureRecognizer(target: self, action: #selector(swiped(_:)))
            swipe.direction = dir
            return swipe
        })
    }

    func swiped(_ swipe: UISwipeGestureRecognizer) {
        let move: MoveCommand
        switch swipe.direction {
        case UISwipeGestureRecognizerDirection.up:
            move = UpMoveCommand()
        case UISwipeGestureRecognizerDirection.down:
            move = DownMoveCommand()
        case UISwipeGestureRecognizerDirection.left:
            move = LeftMoveCommand()
        case UISwipeGestureRecognizerDirection.right:
            move = RightMoveCommand()
        default:
            fatalError()
        }
        let result = data.perform(move: move)
        print(result)
        self.move(withActions: result)
    }

    func move(withActions actions: [MoveAction]) {
        if actions.count == 0 {
            if data.userHasLost() {
                restart()
            }
            return
        }

        actions.filter({ $0.val < 0 }).forEach({ moveTile(from: data.coordinateToIndex($0.src), to: data.coordinateToIndex($0.trg)) })
        UIView.animate(withDuration: 0.1, animations: {
            self.view.layoutIfNeeded()
        })

        actions.filter({ $0.val >= 0 }).forEach({ showNewTile(at: data.coordinateToIndex($0.trg), withVal: $0.val) })


        DispatchQueue.main.asyncAfter(deadline: .now() + 0.21) {
            self.removeViewsNeededToBeRemoved()
            self.addNewRandomTile(animated: true)
            self.updateScore()
        }
    }

    func removeViewsNeededToBeRemoved() {
        for view in needsToBeRemoved {
            view.removeFromSuperview()
        }
        needsToBeRemoved.removeAll()
    }

    func moveTile(from idx1: Int, to idx2: Int) {
        guard let tileFrom = foreGroundTiles[idx1] else {
            assertionFailure()
            return
        }

        let trgTilePh = tileMatrx[idx2]
        tileFrom.snp.remakeConstraints { (make) in
            make.edges.equalTo(trgTilePh)
        }

        foreGroundTiles[idx1] = nil
        if let oldView = foreGroundTiles[idx2] {
            needsToBeRemoved.append(oldView)
        }
        foreGroundTiles[idx2] = tileFrom
    }

    func showNewTile(at idx: Int, withVal val: Int) {
        let tile = createNewTile()
        tile.val = val
        if let oldView = foreGroundTiles[idx] {
            needsToBeRemoved.append(oldView)
        }
        foreGroundTiles[idx] = tile

        let trgTilePh = tileMatrx[idx]

        view.addSubview(tile)
        tile.snp.makeConstraints { (make) in
            make.edges.equalTo(trgTilePh)
        }
        UIView.animate(withDuration: 0.1, delay: 0.05, animations: {
            tile.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
            }) { (_) in
                UIView.animate(withDuration: 0.05, animations: {
                    tile.transform = .identity
                })
        }
    }

    // MARK: - Game logic

    func restart() {
        data.clearAll()
        for (_, tile) in foreGroundTiles {
            tile.removeFromSuperview()
        }
        foreGroundTiles.removeAll()

        addNewRandomTile()
        addNewRandomTile()

        updateScore()
    }

    func addNewRandomTile(animated: Bool = false) {
        let val = data.getValueForInsert()
        let idx = data.insertTilesAtRandonPosition(with: val)
        if idx < 0 {
            return
        }
        let tile = createNewTile()
        tile.val = val
        assert(foreGroundTiles[idx] == nil)
        foreGroundTiles[idx] = tile

        let placeHolder = tileMatrx[idx]
        tile.snp.makeConstraints { (make) in
            make.edges.equalTo(placeHolder)
        }

        if animated {
            tile.transform = CGAffineTransform(scaleX: 0.2, y: 0.2)
            UIView.animate(withDuration: 0.2, animations: { 
                tile.transform = .identity
            })
        }
    }

    func createNewTile() -> TileView{
        let tile = TileView()
        tile.color = color
        view.addSubview(tile)
        tile.layer.cornerRadius = tileCornerRadius

        return tile
    }
}

在上面的代码中大家还引入了一部分操纵按钮,比如重新开头,这一部分并不困难,相信你能领悟。不过,里面关于逻辑控制的代码,大概要求特别说圣元下。其中最为主题的函数为move(withAction:)函数,大家把这些函数以及其调用的函数单独拎出来证雀巢下。

    // 解析一次滑动产生的`MoveAction`操作列表
    func move(withActions actions: [MoveAction]) {
        // 列表为空,那么有可能是用户已经无路可以走了
        if actions.count == 0 {
            if data.userHasLost() {
                // 这里我们是直接自动重新开始游戏了,你也可以选择弹出提示框告诉用户已经失败
                restart()
            }
            return
        }

        // `val`字段小于0的MoveAction是指纯粹的移动。将这些指令筛选出来,进行移动操作
        actions.filter({ $0.val < 0 }).forEach({ moveTile(from: data.coordinateToIndex($0.src), to: data.coordinateToIndex($0.trg)) })
        // 驱动移动动画
        UIView.animate(withDuration: 0.1, animations: {
            self.view.layoutIfNeeded()
        })

        // `val`字段非负的MoveAction是指合并后新的格子的生成。将这些指令筛选出来,并构造新的Tile
        actions.filter({ $0.val >= 0 }).forEach({ showNewTile(at: data.coordinateToIndex($0.trg), withVal: $0.val) })


        // 稍微等待一段很短的时间以后,在空格处插入一个新的格子,并且更新分数
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.21) {
            // 注意在上面的操作之后,有一些格子需要移除,主要是合并的格子,在新的格子产生以后需要将原来的两个格子去掉
            self.removeViewsNeededToBeRemoved()
            self.addNewRandomTile(animated: true)
            self.updateScore()
        }
    }

    func removeViewsNeededToBeRemoved() {
        // 需要被移除的格子被暂存在了`needsToBeRemoved`队列中
        for view in needsToBeRemoved {
            view.removeFromSuperview()
        }
        needsToBeRemoved.removeAll()
    }

    // 处理格子的移动
    func moveTile(from idx1: Int, to idx2: Int) {
        // `foreGroundTiles`是我们建立的一个由位置到格子的索引表
        guard let tileFrom = foreGroundTiles[idx1] else {
            assertionFailure()
            return
        }

        // `tileMatrix`是placeholder的索引表
        let trgTilePh = tileMatrx[idx2]

        // 移动格子
        tileFrom.snp.remakeConstraints { (make) in
            make.edges.equalTo(trgTilePh)
        }

        // 更新`foreGroundTiles`索引表
        foreGroundTiles[idx1] = nil
        // 注意,这里是为了保证在目标位置在一次操作完成后总是最多只有一个格子。
        // 设想在一次合并过程中,两个格子会一起移动到同一个目标位置,那么第二次
        // 移动执行时,会把前一个移动到这里的格子标记为需要移除
        if let oldView = foreGroundTiles[idx2] {
            needsToBeRemoved.append(oldView)
        }
        foreGroundTiles[idx2] = tileFrom
    }

    // 生成新的格子
    func showNewTile(at idx: Int, withVal val: Int) {
        let tile = createNewTile()
        tile.val = val
        // 和上面moveTile(from:to:)末尾的注释接起来。新的格子生成后,会把之前第二个移动到这里的格子标记为
        // 需要移除
        if let oldView = foreGroundTiles[idx] {
            needsToBeRemoved.append(oldView)
        }
        foreGroundTiles[idx] = tile

        let trgTilePh = tileMatrx[idx]

        view.addSubview(tile)
        // 移动格子
        tile.snp.makeConstraints { (make) in
            make.edges.equalTo(trgTilePh)
        }
        // 动画
        UIView.animate(withDuration: 0.1, delay: 0.05, animations: {
            tile.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
            }) { (_) in
                UIView.animate(withDuration: 0.05, animations: {
                    tile.transform = .identity
                })
        }
    }
ColorProvider

那就相比简单了,间接贴代码吧,大家都能看懂的吧。

extension UIColor {
    static func RGB(r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) -> UIColor {
        return UIColor(red: r / 255, green: g / 255, blue: b / 255, alpha: a / 100)
    }

    static func RGB(r: CGFloat, g: CGFloat, b: CGFloat) -> UIColor {
        return UIColor.RGB(r: r, g: g, b: b, a: 100)
    }
}


protocol ColorProvider {
    func colorForValue(_ val: Int) -> UIColor
    func boardBackgroundColor() -> UIColor
    func tileBackgroundColor() -> UIColor
    func textColorForVal(_ val: Int) -> UIColor
}

class DefaultColorProvider: ColorProvider {
    private var colorMap: [Int: UIColor] = [
        2: UIColor.RGB(r: 240, g: 240, b: 240),
        4: UIColor.RGB(r: 237, g: 224, b: 200),
        8: UIColor.RGB(r: 242, g: 177, b: 121),
        16: UIColor.RGB(r: 245, g: 149, b: 99),
        32: UIColor.RGB(r: 246, g: 124, b: 95),
        64: UIColor.RGB(r: 246, g: 94, b: 59)
    ]

    func colorForValue(_ val: Int) -> UIColor {
        if let result = colorMap[val] {
            return result
        } else {
//            fatalError()
            return UIColor.red
        }
    }

    func textColorForVal(_ val: Int) -> UIColor {
        if val >= 256 {
            return UIColor.white
        } else {
            return UIColor.black
        }
    }

    func tileBackgroundColor() -> UIColor {
        return UIColor.RGB(r: 204, g: 192, b: 180)
    }

    func boardBackgroundColor() -> UIColor {
        return UIColor.RGB(r: 185, g: 171, b: 160)
    }
}

启动APP

剩下的干活是把在AppDelegate.swift文件之中添加适当的代码来启动大家的APP了:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        self.window = UIWindow(frame: UIScreen.main.bounds)
        // Override point for customization after application launch.
        self.window!.backgroundColor = UIColor.white
        let container = Container(dimension: 4, winningThreshold: 2048)
        self.window?.rootViewController = container
        self.window!.makeKeyAndVisible()
        return true
    }

统计一下

这篇blog工程量可不小啊,里面肯定有成百上千欠缺的地方,大家遭逢哪些难题在说东道西里指出,我会尽快回答。