F20b4e5cd29465227b8b5b79a2634567
Swift 游戏开发之黎锦拼图(三)

前言

在上一篇文章中,我们完成了对「黎锦拼图」游戏底部功能栏的 UI 和逻辑,并且也能给把拼图元素从底部功能栏中「拖拽」到游戏画布上。现在,我们需要先来补充完整拼图元素的边界。

补充完整拼图元素限定边界

通过前几篇文章的讲解相比大家对这个游戏的规则已经非常清晰了,也明白了拼图元素只能在画布之中进行移动,但在上一篇文章中,我们只对位于画布左边的拼图元素做了不让其「越过」中间线的限定,并且只能是当拼图元素成功加载到游戏画布上时才执行判断。

我们想要完成的效果是,拼图元素从底部功能栏拖拽出来时就需要给其补上其在画布上的其它位置限定,而不是「停留」在画布上,用户再去拖拽时才执行边界判断。

我们先来完成当拼图元素停留在游戏画布上时,用户继续拖拽拼图元素时,补充完其边界限定。

extension Puzzle {
    @objc
    fileprivate func pan(_ panGesture: UIPanGestureRecognizer) {
        let translation = panGesture.translation(in: superview)

        switch panGesture.state {
        case .began:
            layer.borderColor = UIColor.white.cgColor
            layer.borderWidth = 1
        case .changed:
            if right > rightPoint {
                right = rightPoint
            }
            if left < leftaPoint {
                left = leftaPoint
            }
            if top < topPoint {
                top = topPoint
            }
            if bottom > bottomPoint {
                bottom = bottomPoint
            }

        case .ended:
            layer.borderWidth = 0
        default: break
        }

        center = CGPoint(x: center.x + translation.x, y: center.y + translation.y)
        panGesture.setTranslation(.zero, in: superview)
    }
}

通过几个边界变量值来根据拼图元素的 isCopy 变量的取值来动态修改。

class Puzzle: UIImageView {

    /// 是否为「拷贝」拼图元素
    private var isCopy = false
    private var rightPoint: CGFloat = 0
    private var leftaPoint: CGFloat = 0
    private var topPoint: CGFloat = 0
    private var bottomPoint: CGFloat = 0

    // ......

    func updateEdge() {
        if superview != nil {
            if !isCopy {
                topPoint = topSafeAreaHeight
                bottomPoint = superview!.bottom - bottomSafeAreaHeight
                rightPoint = superview!.width / 2
                leftaPoint = 0
            }
        } else {
            if superview != nil {
                topPoint = superview!.top
                bottomPoint = superview!.bottom
                rightPoint = superview!.width
                leftaPoint = superview!.width / 2
            }
        }
    }
}

Puzzle 对象实例化被 addSubview 到其它父视图时,我们可以调用 updateEdge 更新拼图元素与父视图强关联的边界值。用户从底部功能栏拖拽出一个元素到画布上时,通过之前文章中的代码我们可以知道,实际上是给 CollectionViewCell 添加了一个长按手势,通过这个长按手势传递出手势的三种状态给父视图进行处理。

与 CollectionViewCell 相关的父视图处理逻辑修改为:

class LiBottomView: UIView {
    // ......

    private var rightPoint: CGFloat = 0
    private var leftaPoint: CGFloat = 0
    private var topPoint: CGFloat = 0
    private var bottomPoint: CGFloat = 0

    // ......

    private func initView() {
        // ......

        collectionView!.longTapChange = {
            guard let tempPuzzle = self.tempPuzzle else { return }
            tempPuzzle.center = CGPoint(x: $0.x, y: $0.y + self.top)

            if tempPuzzle.right > self.rightPoint {
                tempPuzzle.right = self.rightPoint
            }
            if tempPuzzle.left < self.leftaPoint {
                tempPuzzle.left = self.leftaPoint
            }
            if tempPuzzle.top < self.topPoint {
                tempPuzzle.top = self.topPoint
            }
            if tempPuzzle.bottom > self.bottomPoint {
                tempPuzzle.bottom = self.bottomPoint
            }
        }
        collectionView!.longTapEnded = {
            self.moveEnd?($0)
        }
    }
}

在移动长按手势添加到屏幕视图中的拼图元素,我们同样在手势改变的状态回调处理方法中,对当前回调传递出来的值进行限定。运行工程,发现从功能栏拖拽出来的拼图元素已经具备边界限定啦~

限定拼图元素所有边界

限定拼图元素所有边界

状态维护

底部功能栏随机化

想要去维护「黎锦拼图」游戏的当前状态,我们需要先把当前游戏画布上的内容与某个数据源进行关联管理。在开展这部分工作之前,我们先来把位于功能栏中的拼图元素位置进行打乱,否则就没必要进行状态维护了,直接从底部功能栏的第一个一直拖拽到元素到画布上直到最后位于功能栏的最后一个拼图元素,游戏就完成了,这样固然是有问题的。

想要打乱底部功能栏中的元素布局,我们需要从功能栏的数据源下手。

class ViewController: UIViewController {

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

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

                let img = contentImageView.image!.image(with: CGRect(x: x, y: y, width: itemW, height: itemW))
                let puzzle = Puzzle(size: CGSize(width: itemW, height: itemW), isCopy: false)
                puzzle.image = img
                puzzle.tag = (itemY * itemHCount) + itemX
                puzzles.append(puzzle)
            }
        }

        // 随机化
        for i in 1..<puzzles.count {
            let index = Int(arc4random()) % i
            if index != i {
                puzzles.swapAt(i, index)
            }
        }
    }
}

生成完拼图元素时,我们对拼图元素的数据源进行一个简单的交换即可。使用上述这种方法进行随机化有些冗余,大家可以优化这段代码。

修复两个 bug

细心的你应该能够从之前文章的几个动图中看出一点端倪,当我们从底部功能栏中「长按」并「拖拽」拼图元素上图时,会发现上图和功能栏中被删掉的拼图元素不对。

上图的拼图元素不对是因为之前我们直接把代表着拼图元素本身「位置」的 index 索引当成了拼图元素 Cell 在 CollectionView 中的位置索引,用于 remove 操作。所以,我们还需要给拼图元素 Cell 增加一个游戏索引 gameIndex ,代表其在游戏中的位置索引,使用 cellIndex 代表其在功能栏 CollectionView 中的位置索引。修改后的 LiBottomCollectionViewCell 代码如下:

class LiBottomCollectionViewCell: UICollectionViewCell {
    // ...

    var cellIndex: Int?
    var gameIndex: Int?

    // ...
}

// ...

extension LiBottomCollectionViewCell {
    @objc
    fileprivate func longTap(_ longTapGesture: UILongPressGestureRecognizer) {
        guard let cellIndex = cellIndex else { return }

        switch longTapGesture.state {
        case .began:
            longTapBegan?(cellIndex)
        case .changed:
            var translation = longTapGesture.location(in: superview)

            let itemCount = 5
            if cellIndex > itemCount {
                translation.x = translation.x - CGFloat(cellIndex / itemCount * Int(screenWidth))
            }

            let point = CGPoint(x: translation.x, y: translation.y)
            longTapChange?(point)
        case .ended:
            longTapEnded?(cellIndex)
        default: break
        }
    }
}

// ...

在修复这个 bug 的同时,我还发现了当用户滑动功能栏到下一页时,上图的拼图元素都不能动了,反复确认了一番后,其实功能栏只要是非第一页的拼图元素都会出现这个问题。

LiBottomCollectionViewCell 的长按回调事件中打印出 .change 的 x 坐标值,发现非第一页的元素上图后转换的 x 坐标的对比是与功能栏页数为对比的,滑到非第一页时,会加上滑动过每页的宽度,因此,我们的解决思路就是算出当前用户滑动过去了几页,并乘上这个每页的宽度,用拼图元素当前的转换后的 x 坐标减去它。

修改第二个 bug。拼图元素上图后功能栏删除掉的元素与上图的元素不一致。查了一会儿后发现其实这个问题是因为之前的注释没把对应的逻辑带上,导致多 reloadData 一次,修改 LiBottomCollectionView 的代码为:

```swift
extension LiBottomCollectionView: UICollectionViewDataSource {
// ...

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    // ...

    cell.cellIndex = indexPath.row
    cell.gameIndex = viewModels[indexPath.row].tag
    cell.longTapBegan = { [weak self] index in
        guard let self = self else { return }
        guard self.viewModels.count != 0 else { return }
        self.longTapBegan?(self.viewModels[index], cell.center)
        // --------
        // 原先这里有个 `self.reloadData()`
    }
top Created with Sketch.