536df207830aa7d0143f56c1c24345ae
Swift 游戏开发之黎锦拼图(二)

前言

在上篇文章中,我们完成了对拼图的元素拆分和基本拖拽的用户操作逻辑。现在我们先来补充完整当用户拖拽拼图元素时的逻辑。

在现实生活中,拼图游戏总是被「禁固」在一个确定画布上,玩家只能在这个画布中发挥自己的想象力,恢复拼图。因此,我们也需要在画布上给用户限定一个「区域」。

从之前的两篇文章中,我们知道了「黎锦拼图」中的拼图元素只能在画布的左部分进行操作,不能超出屏幕之外的范围进行操作。因此我们需要对拼图元素做一个限定。

限定拼图

为了能够较好的看到元素的边界,我们先给拼图元素加上「边界」。补充 Puzzle 里的

extension Puzzle {
    @objc
    fileprivate func pan(_ panGesture: UIPanGestureRecognizer) {
        switch panGesture.state {
        case .began:
            layer.borderColor = UIColor.white.cgColor
            layer.borderWidth = 1
        case .changed:
        case .ended:
            layer.borderWidth = 0
        default: break
        }

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

加上边界的思路比较简单,我们的目的是为了让用户在拖拽拼图元素的过程,对拼图元素能够有个比较好的边界把控。运行工程,拖拽拼图元素,拼图元素的边界已经加上啦!

拼图元素边界]

拼图元素边界]

限定拼图元素的可移动位置,可以在 Puzzle 的拖拽手势的回调方法中进行边界确认。我们先来「防止」拼图元素跨越画布的中间线。

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

        let newRightPoint = centerX + width / 2

        switch panGesture.state {
        case .began:
            layer.borderColor = UIColor.white.cgColor
            layer.borderWidth = 1
        case .changed:
            if newRightPoint > superview!.width / 2 {
                right = superview!.width / 2
            }
        case .ended:
            layer.borderWidth = 0
        default: break
        }

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

在拼图元素的拖拽回调方法里,在手势 state 枚举值的 .change 判断里,根据当前拼图元素的「最右边」位置,也就是 self.frame.origin.x + self.frame.size.width / 2 与父视图中间位置的对比,来决定出是否该拼图元素是否越界。

运行工程!发现我们再也不能把拼图元素拖到右边画布里去啦~

限定拼图

限定拼图

状态维护

经过上一个游戏「能否关个灯」的讲解,我们已经大致了解了如何通过状态去维护游戏逻辑,对于一个拼图游戏来说,能否把各个拼图元素按照一定的顺序给复原回去,决定游戏是否牲胜利。

「黎锦拼图」依然还是个 2D 游戏,细心的你一定也会发现,这个游戏本质上与「能否关个灯」这个游戏是一样的,我们都可以把游戏画布按照一定的划分规则切割出来,并通过一个二维列表与切割完成的拼图元素做映射,每次用户对拼图元素的拖拽行为结束后,都去触发一次状态的更新。最后,我们根据每次更新完成后的状态去判断出玩家是否赢得了当前游戏。

状态创建

我们的 Puzzle 类代表着拼图元素本身,拼图游戏的胜利条件是我们要把各个拼图元素按照一定顺序复原,重点在按照一定的顺序。我们可以通过给 puzzle 对象设置 tag 来做到标识每一块拼图元素。

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
                // 添加 tag
                puzzle.tag = (itemY * itemHCount) + itemX
                print(puzzle.tag)

                puzzles.append(puzzle)
                view.addSubview(puzzle)
            }
        }
    }
}

ViewController.swift 文件中的 puzzles 是用于存放所有被切割完成后的 Puzzle 实例对象,如果我们相对游戏的状态进行维护,还需要一个 contentPuzzles 用于管理被用户拖拽到画布上的拼图元素,只有当位于画布上的拼图元素按照一定顺序放置在画布上,才能赢得比赛。

为了完成以上所表达的逻辑,我们先来把「元素下图」。在画布上提供一个「功能栏」,让用户从功能栏中拖拽出拼图元素到画布上,从而完成之前已经完成的元素上图过程。

功能栏

功能栏的作用在于承载所有拼图,在 ViewController.swift 补充相关代码:

class ViewController: UIViewController {

    // ...        
    let bottomView = UIView(frame: CGRect(x: 0, y: view.height, width: view.width, height: 64 + bottomSafeAreaHeight))
    bottomView.backgroundColor = .white
    view.addSubview(bottomView)

    UIView.animate(withDuration: 0.25, delay: 0.5, options: .curveEaseIn, animations: {
        bottomView.bottom = self.view.height
    })
}

运行工程,底部功能栏加上动画后,效果还不错~

底部功能栏

底部功能栏

为了能够较好的处理底部功能栏中所承载的功能,我们需要对底部功能栏进行封装,创建一个新的类 LiBottomView

class LiBottomView: UIView {

}

现在,我们要把拼图元素都「布置」到功能栏上,采用 UICollectionView 长铺布局,也需要创建一个 LiBottomCollectionViewLiBottomCollectionViewCell

水平布局的 LiBottomCollectionView 中,我们也没有过多的动画要求,因此实现起来较为简单。

```swift
class LiBottomCollectionView: UICollectionView {

let cellIdentifier = "PJLineCollectionViewCell"
var viewModels = [Puzzle]()

override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
    super.init(frame: frame, collectionViewLayout: layout)
    initView()
}

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
}

private func initView() {
    backgroundColor = .clear
    showsHorizontalScrollIndicator = false
    isPagingEnabled = true
    dataSource = self

    register(LiBottomCollectionViewCell.self, forCellWithReuseIdentifier: "LiBottomCollectionViewCell")
}

}

extension LiBottomCollectionView: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return viewModels.count
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "LiBottomCollectionViewCell", for: indexPath) as! LiBottomCollectionViewCell
    cell.viewModel = viewModels[indexPath.row]
top Created with Sketch.