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…

相關文章
相關標籤/搜索