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

前言

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

在现实生活中,拼图游戏老是被「禁固」在一个肯定画布上,玩家只能在这个画布中发挥本身的想象力,恢复拼图。所以,咱们也须要在画布上给用户限定一个「区域」。github

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

限定拼图

为了可以较好的看到元素的边界,咱们先给拼图元素加上「边界」。补充 Puzzle 里的markdown

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)
    }
}
复制代码

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

![拼图元素边界]](i.loli.net/2019/09/08/…)app

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

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 与父视图中间位置的对比,来决定出是否该拼图元素是否越界。oop

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

限定拼图

状态维护

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

「黎锦拼图」依然仍是个 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 中,咱们也没有过多的动画要求,所以实现起来较为简单。

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]
        return cell
    }
}
复制代码

在新建的 LiBottomCollectionViewCell 补充代码。

class LiBottomCollectionViewCell: UICollectionViewCell {
    var img = UIImageView()
    
    var viewModel: Puzzle? {
        didSet { setViewModel() }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        layer.borderWidth = 1
        layer.borderColor = UIColor.darkGray.cgColor
        layer.shadowColor = UIColor.black.cgColor
        layer.shadowRadius = 10
        layer.shadowOffset = CGSize.zero
        layer.shadowOpacity = 1
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setViewModel() {
        img.contentMode = .scaleAspectFit
        img.image = viewModel?.image
        img.frame = CGRect(x: 0, y: 0, width: width, height: height)
        if !subviews.contains(img) {
            addSubview(img)
        }
    }
}
复制代码

运行工程~能够看到咱们的底部功能栏已经把拼图元素都布局好啦~

完整的底部功能栏 UI

功能栏上图

当咱们把一个拼图元素从功能栏中拖拽到画布上时,原先位于功能栏上的拼图元素须要被移除,咱们先来实现拼图元素在功能栏上的移除功能。

底部功能栏上的拼图元素数据源来自 LiBottomCollectionViewviewModels,当该数据源被赋值时会调用 reloadDate() 方法刷新页面,所以咱们只须要经过某个「方法」移除在数据源中的拼图元素便可。

给底部功能栏的 Cell 添加上长按手势,在长按手势识别器的回调方法中,传递出当前 Cell 的数据源,经过 LiBottomCollectionView 操做主数据源进行删除,再执行 reloadData() 方法便可。

class LiBottomCollectionViewCell: UICollectionViewCell {
    var longTapBegan: ((Int) -> ())?
    var longTapChange: ((CGPoint) -> ())?
    var longTapEnded: ((Int) -> ())?
    var index: Int?

    // ...

    override init(frame: CGRect) {
        // ... 
        
        let longTapGesture = UILongPressGestureRecognizer(target: self, action: .longTap)
        addGestureRecognizer(longTapGesture)
    }

    // ...
}

extension LiBottomCollectionViewCell {
    @objc
    fileprivate func longTap(_ longTapGesture: UILongPressGestureRecognizer) {
        guard let index = index else { return }
        
        switch longTapGesture.state {
        case .began:
            longTapBegan?(index)
        case .changed:
            let translation = longTapGesture.location(in: superview)
            let point = CGPoint(x: translation.x, y: translation.y)
            longTapChange?(point)
        case .ended:
            longTapEnded?(index)
        default: break
        }
    }
}
复制代码

在 Cell 的长按手势识别器回调方法中,咱们分别对手势的三个状态 .began.changed.ended 进行了处理。在 .began 手势状态中,经过 longTapBegan() 闭包把当前 Cell 的索引传递出去给父视图,在 .changed 手势状态中,经过 longTapChange() 闭包把用户在当前视图上操做的坐标转化成与父视图一致的坐标,在 .ended 方法中一样把当前视图的索引传递出去。

在父视图 LiBottomCollectionView 中,修改 cellForRow 方法:

// ...


extension LiBottomCollectionView: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "LiBottomCollectionViewCell", for: indexPath) as! LiBottomCollectionViewCell
        cell.viewModel = viewModels[indexPath.row]
        cell.index = 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)
        }
        cell.longTapChange = {
            self.longTapChange?($0)
        }
        cell.longTapEnded = {
            self.longTapEnded?(self.viewModels[$0])
            self.viewModels.remove(at: $0)
            self.reloadData()
        }
        
        return cell
    }
}
复制代码

cellForRow 方法中,对 cell 的闭包 longTapEnded() 执行了移除视图操做,达到当用户在底部功能栏对拼图元素执行长按手势操做后,再「释放」长按手势时,从底部功能栏中移除该拼图元素的效果。剩下的两个 cell 的闭包经过 collectionView 再传递到了对应的父视图中。

LiBottomView 这一层级的视图进行拼图元素的上图操做。

class LiBottomView: UIView {
    // ...
    
    private func initView() {
        // ... 
    
        collectionView!.longTapBegan = {
            let center = $1
            let tempPuzzle = Puzzle(size: $0.frame.size, isCopy: false)
            tempPuzzle.image = $0.image
            tempPuzzle.center = center
            tempPuzzle.y += self.top
            self.tempPuzzle = tempPuzzle
            
            self.superview!.addSubview(tempPuzzle)
        }
        collectionView!.longTapChange = {
            guard let tempPuzzle = self.tempPuzzle else { return }
            tempPuzzle.center = CGPoint(x: $0.x, y: $0.y + self.top)
        }
    }
}
复制代码

longTapBegan() 方法中新建一个 Puzzle 拼图元素,注意此时不能直接使用传递出来的 puzzle 对象,不然会由于引用关系而致使后续一些奇怪的问题产生。

longTapChange() 方法中维护由在 LiBottomCollectionViewCell 中所触发的长按手势事件回调出的 CGPoint。在实现从底部功能栏上图这一环节中,很容易会想到在 longTapBegan() 新建 Puzzle 对象时,给该新建对象再绑上一个 UIPanGesture 拖拽手势,但其实仔细一想,UILongPressGestureRecognizer 是继承于 UIGestureRecognizer 类的,UIGestureRecognizer 中维护了一套与用户手势相关识别流程,不论是轻扫、拖拽仍是长按,本质上也都是经过在必定时间间隔点判断用户手势的移动距离和趋势来决定出具体是哪一个手势类型,所以咱们直接使用 UILongPressGestureRecognizer 便可。

运行工程~在底部功能栏里选中一个你喜欢的拼图,长按它!激发手势,慢慢的拖拽到画笔上,好好感觉一下吧~

拼图元素上图

后记

在这篇文章中,咱们主要关注了底部功能栏的逻辑实现,让底部功能栏具有初步「功能」的做用,并完善了上一篇文章中「元素上图」的需求,使其更加完整,目前,咱们完成的需求有:

  • 拼图素材准备;
  • 元素上图;
  • 状态维护;
  • 元素吸附;
  • UI 完善;
  • 判赢逻辑;
  • 胜利动效。

GitHub 地址:github.com/windstormey…

相关文章
相关标签/搜索