Swift 遊戲開發之黎錦拼圖(三)

前言

在上一篇文章中,咱們完成了對「黎錦拼圖」遊戲底部功能欄的 UI 和邏輯,而且也能給把拼圖元素從底部功能欄中「拖拽」到遊戲畫布上。如今,咱們須要先來補充完整拼圖元素的邊界。git

補充完整拼圖元素限定邊界

經過前幾篇文章的講解相比你們對這個遊戲的規則已經很是清晰了,也明白了拼圖元素只能在畫布之中進行移動,但在上一篇文章中,咱們只對位於畫布左邊的拼圖元素作了不讓其「越過」中間線的限定,而且只能是當拼圖元素成功加載到遊戲畫布上時才執行判斷。github

咱們想要完成的效果是,拼圖元素從底部功能欄拖拽出來時就須要給其補上其在畫布上的其它位置限定,而不是「停留」在畫布上,用戶再去拖拽時才執行邊界判斷。swift

咱們先來完成當拼圖元素停留在遊戲畫布上時,用戶繼續拖拽拼圖元素時,補充完其邊界限定。markdown

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 變量的取值來動態修改。app

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 添加了一個長按手勢,經過這個長按手勢傳遞出手勢的三種狀態給父視圖進行處理。dom

與 CollectionViewCell 相關的父視圖處理邏輯修改成:ide

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)
        }
    }
}
複製代碼

在移動長按手勢添加到屏幕視圖中的拼圖元素,咱們一樣在手勢改變的狀態回調處理方法中,對當前回調傳遞出來的值進行限定。運行工程,發現從功能欄拖拽出來的拼圖元素已經具有邊界限定啦~oop

限定拼圖元素全部邊界

狀態維護

底部功能欄隨機化

想要去維護「黎錦拼圖」遊戲的當前狀態,咱們須要先把當前遊戲畫布上的內容與某個數據源進行關聯管理。在開展這部分工做以前,咱們先來把位於功能欄中的拼圖元素位置進行打亂,不然就不必進行狀態維護了,直接從底部功能欄的第一個一直拖拽到元素到畫布上直到最後位於功能欄的最後一個拼圖元素,遊戲就完成了,這樣當然是有問題的。佈局

想要打亂底部功能欄中的元素佈局,咱們須要從功能欄的數據源下手。優化

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 的代碼爲:

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()`
        }

        // ...
    }
}
複製代碼

運行工程,發現咱們已經解決掉了當前的全部 bug!

標記元素

對於一個拼圖遊戲來講,此時咱們的底部功能欄基本邏輯上已經完成,但從用戶角度出發,這個遊戲真的是太難了,由於我不知道哪一個拼圖元素應該放在哪裏,咱們還須要給用戶提供一個「提示」,用於告知每一個拼圖元素的放置順序。

class LiBottomCollectionViewCell: UICollectionViewCell {
    // ...

    override init(frame: CGRect) {
        super.init(frame: frame)

        // ...
        
        img.contentMode = .scaleAspectFit
        img.frame = CGRect(x: 0, y: 0, width: width, height: height)
        addSubview(img)

        
        tipLabel = UILabel(frame: CGRect(x: width - 10, y: top - 10, width: 17, height: 17))
        tipLabel.font = UIFont.systemFont(ofSize: 11)
        tipLabel.backgroundColor = UIColor.rgb(80, 80, 80)
        tipLabel.textColor = .white
        tipLabel.textAlignment = .center
        tipLabel.layer.cornerRadius = tipLabel.width / 2
        tipLabel.layer.masksToBounds = true
        addSubview(tipLabel)

        // ...
    }

    // ...
    
    private func setViewModel() {
        img.image = viewModel?.image
        tipLabel.text = "\(gameIndex!)"
    }
}

// ...
複製代碼

運行工程,拼圖元素的標記加上啦!

標記元素

左右映像

如今咱們已經完成了遊戲畫布左邊的大部分邏輯,如今來補充遊戲畫布的右邊邏輯。咱們須要建立出一個與遊戲畫布左邊鏡像對稱的拼圖元素。

左邊的拼圖元素與右邊的拼圖元素要保持位置上的徹底一致,而且還要保證其爲鏡像對稱。在個人 WWDC19 獎學金申請項目中,我採起了一個偷懶的作法,用戶必須先把拼圖元素放到遊戲畫布的左邊,觸發長按手勢的結束狀態事件後,再移動該拼圖元素才能在遊戲畫布的右邊看到 copy 的拼圖元素。這種作法只能說是能用,距離「優雅」還差點東西。

咱們想要作到的效果是,當用戶在底部功能欄中長按選擇一個拼圖元素,該拼圖元素在底部功能欄所屬的區域內移動時不會觸發生成 copy 的拼圖元素在遊戲畫布的右邊,一旦向上移動出了底部功能欄的區域,copy 的拼圖元素即出現。咱們須要當玩家在底部功能欄選擇拼圖元素的同時,生成 copy 的拼圖元素。

爲了防止 copy 拼圖元素在生成時出如今遊戲畫布的尷尬位置上,咱們 Puzzle 類的初始化方法作一些改動。

class Puzzle: UIImageView {
    
    convenience init(size: CGSize, isCopy: Bool) {
        // 剛開始先頂出去
        self.init(frame: CGRect(x: -1000, y: -1000, width: size.width, height: size.height))
        self.isCopy = isCopy
        
        initView()
    }

    private func initView() {
        contentMode = .scaleAspectFit
        
        if !isCopy {
            // ...
        } else {
            // 若是是 copy 拼圖元素,則鏡像翻轉
            transform = CGAffineTransform(scaleX: -1, y: 1)
        }
    }
}
複製代碼

ViewController.swift 文件中,咱們須要聲明一個用於暫時配合從底部功能欄上圖拼圖元素的 copy 拼圖元素,配合其進行移動。等到用戶肯定上圖拼圖元素的位置後,觸發長按手勢結束狀態的事件,再把這個用於配合移動的 copy 拼圖元素移除,從新建立一個「肯定」的拼圖元素在遊戲畫布的右邊。

class ViewController: UIViewController {
    // ...

    private var copyPuzzles = [Puzzle]()
    // 用於配合移動的 `copy` 拼圖元素
    private var tempCopyPuzzle: Puzzle?

    // ...

    override func viewDidLoad() {
        // ...

        bottomView.moveBegin = {
            self.tempCopyPuzzle = Puzzle(size: $0.frame.size, isCopy: true)
            self.tempCopyPuzzle?.image = $0.image
            self.tempCopyPuzzle?.tag = $0.tag
            // 當接收到底部功能欄回調出的長按手勢事件,即建立 `copy` 拼圖元素
            self.view.addSubview(self.tempCopyPuzzle!)
        }
        
        bottomView.moveChanged = {
            guard let tempPuzzle = self.tempCopyPuzzle else { return }
            
            // 超出底部功能欄位置後才顯示
            if $0.y < self.bottomView.top {
                // 計算的重點
                tempPuzzle.center = CGPoint(x: self.view.width - $0.x, y: $0.y)
            }
        }
        
        bottomView.moveEnd = {
            guard let tempPuzzle = self.tempCopyPuzzle else { return }
            // 長按手勢完成,先移除
            tempPuzzle.removeFromSuperview()
            
            let copyPuzzle = Puzzle(size: $0.frame.size, isCopy: true)
            copyPuzzle.center = tempPuzzle.center
            copyPuzzle.image = tempPuzzle.image
            // 再添加「肯定」的 `copy` 拼圖元素
            self.view.addSubview(copyPuzzle)
            self.copyPuzzles.append(copyPuzzle)
        }
    }

    // ...
}
複製代碼

此時運行工程,你會發現只能從底部功能欄中把拼圖元素上圖時才能觸發 copy 元素的移動,當結束長按手勢後,卻再也沒法觸發了(今後之後,位於遊戲畫布左邊的拼圖元素爲 leftPuzzle,位於遊戲畫布右邊的拼圖元素爲 rightPuzzle)。這是由於 leftPuzzle 的移動手勢沒有傳遞給 rightPuzzle,咱們須要對 Puzzle 類作一點改動。

class Puzzle: UIImageView {
    var longTapChange: ((CGPoint) -> ())?
    
    // ...

    /// 移動 `rightPuzzle`
    func copyPuzzleCenterChange(centerPoint: CGPoint) {
        if !isCopy { return }
        
        center = CGPoint(x: screenWidth - centerPoint.x, y: centerPoint.y)
    }
}

extension Puzzle {
    @objc
    fileprivate func pan(_ panGesture: UIPanGestureRecognizer) {
        // ...
        
        center = CGPoint(x: center.x + translation.x, y: center.y + translation.y)
        panGesture.setTranslation(.zero, in: superview)
        
        // 傳遞出該長按手勢的移動位置
        longTapChange?(center)
    }
}
複製代碼

ViewController.swift 中修改。

class ViewController: UIViewController {
    // ...

    override func viewDidLoad() {
         bottomView.moveBegin = { puzzle in
            // 把 `leftPuzzle` 添加到遊戲畫布的時機遷移到 `ViewController` 中去作 
            self.view.addSubview(puzzle)
            self.leftPuzzles.append(puzzle)
            puzzle.updateEdge()
            
            // 搜索與 `leftPuzzle` 相等的 `rightPuzzle`,並把移動距離傳遞進去
            puzzle.longTapChange = {
                for copyPuzzle in self.rightPuzzles {
                    if copyPuzzle.tag == puzzle.tag {
                        copyPuzzle.copyPuzzleCenterChange(centerPoint: $0)
                    }
                }
            }

            // ...
        }
        
        bottomView.moveChanged = {
            guard let tempPuzzle = self.tempCopyPuzzle else { return }
            
            // 超出底部功能欄位置後才顯示
            if $0.y < self.bottomView.top {
                // 封裝了 `rightPuzzle` 移動方法
                tempPuzzle.copyPuzzleCenterChange(centerPoint: $0)
            }
            
        }
        
        bottomView.moveEnd = {
            // ...

            // 把 tag 傳入
            copyPuzzle.tag = tempPuzzle.tag

            // ...
        }
    }

    // ...
}
複製代碼

運行工程,發現咱們已經能夠左右鏡像啦!

左右映像

後記

在這篇文章中,咱們完善了拼圖元素從底部功能欄上圖這一環節的全部邏輯,在下一篇文章中,咱們將着重關注「黎錦拼圖」的核心玩法邏輯。目前,咱們完成的需求有:

  • 拼圖素材準備;
  • 元素上圖;
  • 狀態維護;
  • 元素吸附;
  • UI 完善;
  • 判贏邏輯;
  • 勝利動效。

GitHub 地址:github.com/windstormey…

相關文章
相關標籤/搜索