银河网投[网址]www.308877.com-官网首页

您的位置:银河网投 > 游戏破解器 > 怎样开发一个2048游戏

怎样开发一个2048游戏

2019-11-09 02:30
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] {
}

下边大家来各个表明各样组织的成效和得以完结。

筹算职业

在此个有些,我们创建工程文件,并简短梳理一下工程的布局甚至各样文件的功力。

那篇教程倘让你已经对此斯维夫特的中坚语法只是和Xcode的运用方式有了一个比较清楚的心得。假如你还不打听那上头的文化的话,提议先去读书以下相关作品实行入门。

View部分

View部分绝比较较轻易,究竟独有二个页面。View部分只关乎到多个类,分别是ContainerTileView(格子)。

启动APP

余下的劳作是把在AppDelegate.swift文件之中增多适合的数量的代码来运行大家的应用程式了:

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
    }
对客商的内外左右滑行操作做出响应

2048是IOS学习的德姆o中久久的话题了。以前为了给后辈们讲三个关于iOS+斯威夫特的讲座,便自身开支了叁个。Github上倒是已经有了壹个工程 austinzheng/swift-2048 ,不过最后的贰次commit也早已经是2015年的时候了,有个别地点应当早已落伍了吧。

总括一下

那篇blog工程量可非常的大啊,里面肯定有超级多劣势之处,大家遇到什么难点在信心胡说里提出,笔者会尽快回答。

创设项目

上边已经宣称,笔者假如你曾经深谙了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来设置那几个正视。

判断顾客是还是不是胜利

此地只要求推断matrix中的最大值是还是不是达到了给定的阈值就可以:

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

格子较易,除了背景以外只须求出示三个数字。作者在这里边运用了Snap基特这些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)
        }
    }
}

以此比较轻易,就相当少说了。

娱乐逻辑深入分析

本条片段涉及到的正是31日游逻辑的宗旨了。通过对游戏法则的开掘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是三个要命便利达成的操作。大家只供给将待管理的数组中的非零成分依照原先的相继放到新数组里面就能够了。

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)
    }
}
骨干数据表示 - 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 })
    }

于今,大家完结了对游戏数量表明的肤浅,将要204第88中学的4*4矩阵用Matrix来代表,并在此个结构体上定义了便利的拜谒格局和工具函数。

认清顾客是还是不是早就倒闭

以此逻辑要相对复杂一点,客商退步时,即顾客无论怎么操作矩阵都不会发生变化。依据准绳,用户失利应当满足上面三个原则

  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
    }
在一个专擅空位插入二个私行的值

高居程序设计中等学园函授数应当保持巧夺天工的条件,为了落到实处那一个功用,大家扩张多少个工具函数:

// 这个函数会返回插入的位置,返回的格式为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)
    }
重新初始化游戏

重新恢复生机设置游戏只须求把matrix中的数值清空就可以

    func clearAll() {
        matrix.clearAll()
    }
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:)函数,我们把这些函数以致其调用的函数单独拎出来证圣元(Nutrilon卡塔尔下。

    // 解析一次滑动产生的`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
                })
        }
    }
实现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

在内定地方插入钦赐值

本条接口实现特别轻便,因为大家已经在Matrix类中落到实处了看似的接口。故在那地大家只须求调用对应的函数就能够。

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

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

前言

以此科指标源代码已经位于了自家的github主页上边: Game2048,近期不曾放License,但是你能够随意使用本文以致Github工程中的全体源代码。

话题回到项目小编。那一个类型上,作者也是接受了卓绝了MVC架构,即Model-View-Controller。在底下疏解中,小编也将中央以那几个顺序来介绍代码的布局与逻辑。

客户操作的象征和促成

在前风度翩翩部分的深入分析中大家发掘,差异趋向的滑动,都得以分解为多少个生龙活虎维难点,只是差异的滑动方向下,黄金年代维难点的分解格局,甚至将逐风华正茂解出的结果还原为二维矩阵的法子不相同。而生龙活虎维难点的求解方法是千篇少年老成律的。这种特征切合于选拔多态的布署艺术。即我们定义三个基类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)
    }
}

本文由银河网投发布于游戏破解器,转载请注明出处:怎样开发一个2048游戏

关键词: