46f3e18be7786614f585786083d3701b
Swift 游戏开发之黎锦拼图(四)

前言

在上一篇文章中,我们基本上已经把除了游戏判赢逻辑外的所有内容都完成了,在这篇文章中,我们将直接「模拟」现实生活中的拼图游戏判赢逻辑来继续完善我们的「黎锦拼图」小游戏。

在现实生活中的拼图游戏,不管拼图是多大的尺寸,最终我们都可以隐约发现其有二维数组的影子,拼图元素一个接着一个的排布在游戏画布之上,可以理解为二维数组被慢慢填满。我们在之前的文章中已经对位于画布左右两边的拼图元素分别使用 leftPuzzlesrightPuzzles 作为存放的容器,但这两个容器均为一维容器,没关系,我们可以从逻辑上维护。

磁吸效果

在实现判赢逻辑之前,我们先来完成一个能够提升玩家乐趣的小功能——「磁吸效果」,效果如下图所示。

磁吸效果

磁吸效果

该效果与我们小时候玩耍的磁铁本身并无差异,当一块磁铁的旁边出现了一个铁块,该磁铁会把铁块吸引到其身上。因此,我们要实现的效果就是当停止移动拼图元素时,拼图元素会趋向离它最近的虚拟「方格」中。

做虚拟「方格」的切割这件事我们并不需要真的去切割,根据上文所说,我们只需要在逻辑上维护一个「模拟」方格即可,因此,我们的任务就转变成了如何在拼图元素的拖拽事件结束时,找到距离该拼图元素最近的虚拟「方格」。

大致的思路是,当拼图元素的拖拽事件每次结束时,获取当前拼图元素的坐标,通过该坐标进行一些计算,把该坐标转换成虚拟「方格」的索引,最后再直接把拼图元素的坐标重新赋值为该虚拟「方格」的坐标,核心代码如下所示。

class ViewController: UIViewController {

    // ...

    override func viewDidLoad() {
        // ...

        bottomView.moveBegin = { puzzle in
            puzzle.panEnded = {
                for copyPuzzle in self.rightPuzzles {
                    if copyPuzzle.tag == puzzle.tag {
                        copyPuzzle.copyPuzzleCenterChange(centerPoint: puzzle.center)
                        self.adsorb()
                    }
                }
            }
            // ...
        }

        bottomView.moveEnd = {
            // ...

            self.adsorb()
        }
    }


    /// 启动磁吸
    private func adsorb() {
        guard let tempPuzzle = self.leftPuzzles.last else { return }

        var tempPuzzleCenterPoint = tempPuzzle.center

        var tempPuzzleXIndex = CGFloat(Int(tempPuzzleCenterPoint.x / tempPuzzle.width))
        if Int(tempPuzzleCenterPoint.x) % Int(tempPuzzle.width) > 0 {
            tempPuzzleXIndex += 1
        }

        var tempPuzzleYIndex = CGFloat(Int(tempPuzzleCenterPoint.y / tempPuzzle.height))
        if Int(tempPuzzleCenterPoint.y) % Int(tempPuzzle.height) > 0 {
            tempPuzzleYIndex += 1
        }


        let Xedge = tempPuzzleXIndex * tempPuzzle.width
        let Yedge = tempPuzzleYIndex * tempPuzzle.height

        if tempPuzzleCenterPoint.x < Xedge {
            tempPuzzleCenterPoint.x = Xedge - tempPuzzle.width / 2
        }

        if tempPuzzleCenterPoint.y < Yedge {
            tempPuzzleCenterPoint.y = Yedge  - tempPuzzle.height / 2
        }

        tempPuzzle.center = tempPuzzleCenterPoint
    }

}

此时,运行工程,就可以看到有趣的磁吸效果啦~

互斥逻辑

完成磁吸效果,运行工程后,你应该会发现当画布上有两个相同的拼图位于同一个位置上时,居然重叠了,并不会「认识」到当前位置上已经被占了。因此,我们需要再编写一个「互斥逻辑」来保证相同位置不允许拼图重叠。我们需要考虑以下两种情况。

拼图 A 和 B 均已在画布上,A 往 B 的位置上移动

在这种情况下时,我们需要对游戏数据源本体做做一些改造。之前我们对添加到画布上的拼图元素只是单纯的拿一个 array 进行 append 记录,但这只做到了「被添加」,并未显式的标记出该拼图在画布上位置,我们需要从数据源本身模拟出一个游戏画布的抽象逻辑。

模拟这个逻辑我使用一个二维矩阵,在 viewDidLoad 方法中初始化每一个「格子」的数据为 -1,后续在拼图元素的 panEnded 闭包回调中执行 addSubview 上屏逻辑之后,把该拼图对应的 tag 记录到二维矩阵中,以此来模拟所谓的「放置」操作。

class ViewController: UIViewController {

    // ...
    private var finalPuzzleTags = [[Int]]()

    override func viewDidLoad() {
        super.viewDidLoad()
        // ...

        // 一行六个
        let itemHCount = 3
        let itemW = Int(view.width / CGFloat(itemHCount * 2))
        let itemH = itemW
        let itemVCount = Int(contentImageView.height / CGFloat(itemW))

        finalPuzzleTags = Array(repeating: Array(repeating: -1, count: itemHCount), count: itemVCount)

        // ...
    }
}

在「启动磁吸」的算法中,计算出当前拼图上屏时的坐标索引后,结合该二维矩阵进行判断和赋值,根据赋值时检测是否有非 -1 的值,若该位置上存在非 -1 的值,则说明画布上该位置已被其它拼图块占据,被移动的拼图块位置被打回。

/// 启动磁吸
private func adsorb(_ tempPuzzle: Puzzle) {
    var tempPuzzleCenterPoint = tempPuzzle.center

    var tempPuzzleXIndex = CGFloat(Int(tempPuzzleCenterPoint.x / tempPuzzle.width))
    if Int(tempPuzzleCenterPoint.x) % Int(tempPuzzle.width) > 0 {
        tempPuzzleXIndex += 1
    }

    var tempPuzzleYIndex = CGFloat(Int(tempPuzzleCenterPoint.y / tempPuzzle.height))
    if Int(tempPuzzleCenterPoint.y) % Int(tempPuzzle.height) > 0 {
        tempPuzzleYIndex += 1
    }


    let Xedge = tempPuzzleXIndex * tempPuzzle.width
    let Yedge = tempPuzzleYIndex * tempPuzzle.height

    if tempPuzzleCenterPoint.x < Xedge {
        tempPuzzleCenterPoint.x = Xedge - tempPuzzle.width / 2
    }

    if tempPuzzleCenterPoint.y < Yedge {
        tempPuzzleCenterPoint.y = Yedge  - tempPuzzle.height / 2
    }

    // 超出最下边
    if (Int(tempPuzzleYIndex) > self.finalPuzzleTags.count) {
        tempPuzzle.center = tempPuzzle.beginMovedPoint
    }

    // 已经有的不能占据
    if (self.finalPuzzleTags[Int(tempPuzzleYIndex - 1)][Int(tempPuzzleXIndex - 1)] == -1) {
        self.finalPuzzleTags[Int(tempPuzzleYIndex - 1)][Int(tempPuzzleXIndex - 1)] = tempPuzzle.tag


        if ((tempPuzzle.Xindex != nil) && (tempPuzzle.Yindex != nil)) {
            self.finalPuzzleTags[tempPuzzle.Xindex!][tempPuzzle.Yindex!] = -1
        }

        tempPuzzle.Xindex = Int(tempPuzzleYIndex - 1)
        tempPuzzle.Yindex = Int(tempPuzzleXIndex - 1)

        tempPuzzle.center = tempPuzzleCenterPoint
    } else {
        tempPuzzle.center = tempPuzzle.beginMovedPoint
    }
}

运行工程!发现两个位于画布上的拼图移动时,互相不能被占据对方的位置啦~

互斥逻辑

互斥逻辑

拼图 A 在画布上,拼图 B 从底部工具栏中往拼图 A 的位置上移动

这种情况作为一个大家自行去完善的地方。如果你想要拼图 B 发现自己移动到画布上的位置已经被占据时,可以先不清除底部工具栏上拼图 B 的位置,等拼图 B 真正被添加上画布后再进行删除。

这属于产品策略,实现思路也已经说明,按照你喜欢的方式实现它吧!

完善 UI

此时我们去完成游戏时,发现大力神的头出现了两个。

两个头的大力神 =。=

两个头的大力神 =。=

这是因为我们在实现「截取拼图块」算法时,没有对特殊情况做处理,只考虑了算法的可行性,没有考虑特殊边界。解决这个问题的思路是,在生成每行最后一个拼图块时,对需要「截取」的图片宽度减小三分之一即可。

```swift
class ViewController: UIViewController {

// ...

override func viewDidLoad() {
    // ...

    for itemY in 0..<itemVCount {
        for itemX in 0..<itemHCount {
            let x = itemW * itemX
            let y = itemW * itemY

            var finalItemW = itemW
            var finalItemH = itemH

            // 特殊点
            if itemX == itemHCount - 1 {
                finalItemW = itemW / 3 * 2 + 2
            }

            let img = contentImageView.image!.image(with: CGRect(x: x, y: y, width: finalItemW, height: finalItemH))
            let puzzle = Puzzle(size: CGSize(width: itemW, height: itemW),
top Created with Sketch.