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

前言

在上一篇文章中,咱們基本上已經把除了遊戲判贏邏輯外的全部內容都完成了,在這篇文章中,咱們將直接「模擬」現實生活中的拼圖遊戲判贏邏輯來繼續完善咱們的「黎錦拼圖」小遊戲。git

在現實生活中的拼圖遊戲,無論拼圖是多大的尺寸,最終咱們均可以隱約發現其有二維數組的影子,拼圖元素一個接着一個的排布在遊戲畫布之上,能夠理解爲二維數組被慢慢填滿。咱們在以前的文章中已經對位於畫布左右兩邊的拼圖元素分別使用 leftPuzzlesrightPuzzles 做爲存放的容器,但這兩個容器均爲一維容器,不要緊,咱們能夠從邏輯上維護。github

磁吸效果

在實現判贏邏輯以前,咱們先來完成一個可以提高玩家樂趣的小功能——「磁吸效果」,效果以下圖所示。算法

磁吸效果

該效果與咱們小時候玩耍的磁鐵自己並沒有差別,當一塊磁鐵的旁邊出現了一個鐵塊,該磁鐵會把鐵塊吸引到其身上。所以,咱們要實現的效果就是當中止移動拼圖元素時,拼圖元素會趨向離它最近的虛擬「方格」中。swift

作虛擬「方格」的切割這件事咱們並不須要真的去切割,根據上文所說,咱們只須要在邏輯上維護一個「模擬」方格便可,所以,咱們的任務就轉變成了如何在拼圖元素的拖拽事件結束時,找到距離該拼圖元素最近的虛擬「方格」。數組

大體的思路是,當拼圖元素的拖拽事件每次結束時,獲取當前拼圖元素的座標,經過該座標進行一些計算,把該座標轉換成虛擬「方格」的索引,最後再直接把拼圖元素的座標從新賦值爲該虛擬「方格」的座標,核心代碼以下所示。markdown

class ViewController: UIViewController {

    // ...
    
    override func viewDidLoad() {
        // ...
    
        bottomView.moveBegin = { puzzle in
            puzzle.panEnded = {
                for copyPuzzle in self.rightPuzzles {
                    if copyPuzzle.tag == puzzle.tag {
                        copyPuzzle.copyPuzzleCenterChange(centerPoint: puzzle.center)
                        self.adsorb()
                    }
                }
            }
            // ...
        }
        
        bottomView.moveEnd = {
            // ...
            
            self.adsorb()
        }
    }
    
    
    /// 啓動磁吸
    private func adsorb() {
        guard let tempPuzzle = self.leftPuzzles.last else { return }
        
        var tempPuzzleCenterPoint = tempPuzzle.center
        
        var tempPuzzleXIndex = CGFloat(Int(tempPuzzleCenterPoint.x / tempPuzzle.width))
        if Int(tempPuzzleCenterPoint.x) % Int(tempPuzzle.width) > 0 {
            tempPuzzleXIndex += 1
        }
        
        var tempPuzzleYIndex = CGFloat(Int(tempPuzzleCenterPoint.y / tempPuzzle.height))
        if Int(tempPuzzleCenterPoint.y) % Int(tempPuzzle.height) > 0 {
            tempPuzzleYIndex += 1
        }
        
        
        let Xedge = tempPuzzleXIndex * tempPuzzle.width
        let Yedge = tempPuzzleYIndex * tempPuzzle.height
        
        if tempPuzzleCenterPoint.x < Xedge {
            tempPuzzleCenterPoint.x = Xedge - tempPuzzle.width / 2
        }
        
        if tempPuzzleCenterPoint.y < Yedge {
            tempPuzzleCenterPoint.y = Yedge  - tempPuzzle.height / 2
        }
        
        tempPuzzle.center = tempPuzzleCenterPoint
    }
    
}
複製代碼

此時,運行工程,就能夠看到有趣的磁吸效果啦~閉包

互斥邏輯

完成磁吸效果,運行工程後,你應該會發現當畫布上有兩個相同的拼圖位於同一個位置上時,竟然重疊了,並不會「認識」到當前位置上已經被佔了。所以,咱們須要再編寫一個「互斥邏輯」來保證相同位置不容許拼圖重疊。咱們須要考慮如下兩種狀況。app

拼圖 A 和 B 均已在畫布上,A 往 B 的位置上移動

在這種狀況下時,咱們須要對遊戲數據源本體作作一些改造。以前咱們對添加到畫布上的拼圖元素只是單純的拿一個 array 進行 append 記錄,但這隻作到了「被添加」,並未顯式的標記出該拼圖在畫布上位置,咱們須要從數據源自己模擬出一個遊戲畫布的抽象邏輯。ide

模擬這個邏輯我使用一個二維矩陣,在 viewDidLoad 方法中初始化每個「格子」的數據爲 -1,後續在拼圖元素的 panEnded 閉包回調中執行 addSubview 上屏邏輯以後,把該拼圖對應的 tag 記錄到二維矩陣中,以此來模擬所謂的「放置」操做。工具

class ViewController: UIViewController {

    // ...
    private var finalPuzzleTags = [[Int]]()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // ...
        
        // 一行六個
        let itemHCount = 3
        let itemW = Int(view.width / CGFloat(itemHCount * 2))
        let itemH = itemW
        let itemVCount = Int(contentImageView.height / CGFloat(itemW))
        
        finalPuzzleTags = Array(repeating: Array(repeating: -1, count: itemHCount), count: itemVCount)

        // ...
    }
}
複製代碼

在「啓動磁吸」的算法中,計算出當前拼圖上屏時的座標索引後,結合該二維矩陣進行判斷和賦值,根據賦值時檢測是否有非 -1 的值,若該位置上存在非 -1 的值,則說明畫布上該位置已被其它拼圖塊佔據,被移動的拼圖塊位置被打回。

/// 啓動磁吸
private func adsorb(_ tempPuzzle: Puzzle) {
    var tempPuzzleCenterPoint = tempPuzzle.center
    
    var tempPuzzleXIndex = CGFloat(Int(tempPuzzleCenterPoint.x / tempPuzzle.width))
    if Int(tempPuzzleCenterPoint.x) % Int(tempPuzzle.width) > 0 {
        tempPuzzleXIndex += 1
    }
    
    var tempPuzzleYIndex = CGFloat(Int(tempPuzzleCenterPoint.y / tempPuzzle.height))
    if Int(tempPuzzleCenterPoint.y) % Int(tempPuzzle.height) > 0 {
        tempPuzzleYIndex += 1
    }
    
    
    let Xedge = tempPuzzleXIndex * tempPuzzle.width
    let Yedge = tempPuzzleYIndex * tempPuzzle.height
    
    if tempPuzzleCenterPoint.x < Xedge {
        tempPuzzleCenterPoint.x = Xedge - tempPuzzle.width / 2
    }
    
    if tempPuzzleCenterPoint.y < Yedge {
        tempPuzzleCenterPoint.y = Yedge  - tempPuzzle.height / 2
    }
    
    // 超出最下邊
    if (Int(tempPuzzleYIndex) > self.finalPuzzleTags.count) {
        tempPuzzle.center = tempPuzzle.beginMovedPoint
    }
    
    // 已經有的不能佔據
    if (self.finalPuzzleTags[Int(tempPuzzleYIndex - 1)][Int(tempPuzzleXIndex - 1)] == -1) {
        self.finalPuzzleTags[Int(tempPuzzleYIndex - 1)][Int(tempPuzzleXIndex - 1)] = tempPuzzle.tag
        
        
        if ((tempPuzzle.Xindex != nil) && (tempPuzzle.Yindex != nil)) {
            self.finalPuzzleTags[tempPuzzle.Xindex!][tempPuzzle.Yindex!] = -1
        }
        
        tempPuzzle.Xindex = Int(tempPuzzleYIndex - 1)
        tempPuzzle.Yindex = Int(tempPuzzleXIndex - 1)
        
        tempPuzzle.center = tempPuzzleCenterPoint
    } else {
        tempPuzzle.center = tempPuzzle.beginMovedPoint
    }
}
複製代碼

運行工程!發現兩個位於畫布上的拼圖移動時,互相不能被佔據對方的位置啦~

互斥邏輯

拼圖 A 在畫布上,拼圖 B 從底部工具欄中往拼圖 A 的位置上移動

這種狀況做爲一個你們自行去完善的地方。若是你想要拼圖 B 發現本身移動到畫布上的位置已經被佔據時,能夠先不清除底部工具欄上拼圖 B 的位置,等拼圖 B 真正被添加上畫布後再進行刪除。

這屬於產品策略,實現思路也已經說明,按照你喜歡的方式實現它吧!

完善 UI

此時咱們去完成遊戲時,發現大力神的頭出現了兩個。

兩個頭的大力神 =。=

這是由於咱們在實現「截取拼圖塊」算法時,沒有對特殊狀況作處理,只考慮了算法的可行性,沒有考慮特殊邊界。解決這個問題的思路是,在生成每行最後一個拼圖塊時,對須要「截取」的圖片寬度減少三分之一便可。

class ViewController: UIViewController {

    // ...
    
    override func viewDidLoad() {
        // ...
        
        for itemY in 0..<itemVCount {
            for itemX in 0..<itemHCount {
                let x = itemW * itemX
                let y = itemW * itemY
                
                var finalItemW = itemW
                var finalItemH = itemH
            
                // 特殊點
                if itemX == itemHCount - 1 {
                    finalItemW = itemW / 3 * 2 + 2
                }
                
                let img = contentImageView.image!.image(with: CGRect(x: x, y: y, width: finalItemW, height: finalItemH))
                let puzzle = Puzzle(size: CGSize(width: itemW, height: itemW), 

                // ...
            }
        }  
    }
}
複製代碼

此時運行工程,發現仍是有些奇怪的地方。

頭被居中了

出現這個問題時,我確實思考了一下子,一直在糾結是否是截取算法寫錯了,想着想着忽然恍惚過來!只須要修改拼圖的 contentMode 便可。

class Puzzle: UIImageView {

    // ...
    
    private func initView() {
        // 所有靠左,copyPuzzle 鏡像對稱
        contentMode = .left
        
        // ...
    }

    // ...
}
複製代碼

解決了中間線附近的問題,此時把拼圖遊戲進行到最後一行時,發現最後一行的元素又不太對勁了。

最後一行被分割了

出現這個問題的緣由沿襲以前的解決思路,把拼圖的 contentMode 換成 top 便可,但又須要作一些標識位的判斷,來決定當前拼圖的 contentModeleft 仍是 top,多餘出了一些髒代碼。

因此,咱們只須要判斷出當前是最後一行的拼圖元素時,在「磁吸算法」中把最後一行的元素往上移動 20 便可。

class ViewController: UIViewController {

    // ... 
    /// 啓動磁吸
    private func adsorb(_ tempPuzzle: Puzzle) {
        // ...
        
        if tempPuzzleCenterPoint.y < Yedge {
            // 當爲最後一列時,往上移 20
            if (Int(tempPuzzleYIndex) == finalPuzzleTags.count) {
                tempPuzzleCenterPoint.y = Yedge  - tempPuzzle.height / 2 - 20
            } else {
                tempPuzzleCenterPoint.y = Yedge  - tempPuzzle.height / 2
            }
        }
        
        // ...
    }
}
複製代碼

運行工程,從新進行遊戲!

判贏邏輯

在前幾篇文章中,咱們對本來一張大圖進行了切割,並對切割出來的各個拼圖元素按照切割順序,經過 gameIndex 標記出了其在原先大圖中的具體位置索引,隨後又打亂存儲這些切割完的拼圖元素容器中的元素位置,最終渲染到底部功能欄上的拼圖元素就變成了「隨機」產生的效果。

所以,在判贏邏輯中,咱們須要作的就是當每個拼圖元素的拖拽事件結束時,都要去判斷一次當前是否贏得了遊戲。而判贏邏輯的主要關注點在於玩家下放拼圖元素位置所表明的索引,是否與拼圖遊戲初始化時,對每一個拼圖元素設置的 tag 索引是否一致。

class ViewController: UIViewController {
    // ...
    /// 判贏算法
    private func isWin() -> Bool {
        
        var winCount = 0
        for (Vindex, HTags) in self.finalPuzzleTags.enumerated() {
            for (Hindex, tag) in HTags.enumerated() {
                let currentIndex = Vindex * 3 + Hindex
                if defaultPuzzles.count - 1 >= currentIndex {
                    if defaultPuzzles[currentIndex].tag == tag {
                        winCount += 1
                        continue
                    }
                }
                
                return false
            }
        }
        
        if winCount == defaultPuzzles.count {
            return true
        }
        return false
    }

    // ...
}
複製代碼

在贏得遊戲後,咱們須要給玩家一個獎勵。在此我設計的獎勵是一覽「大力神」的本體,並附加上一些基於粒子效果的動畫。

private func winAnimate() {
    startParticleAnimation(CGPoint(x: screenWidth / 2, y: screenHeight - 10))
    
    UIView.animate(withDuration: 0.25, animations: {
        self.bottomView.top = screenHeight
    })
    
    for sv in self.view.subviews {
        sv.removeFromSuperview()
    }
    
    self.winLabel.isHidden = false
    let finalManContentView = UIImageView(frame: CGRect(x: 0, y: 0,
                                                        width: screenWidth,
                                                        height: screenHeight - 64))
    finalManContentView.image = UIImage(named: "finalManContent")
    self.view.addSubview(finalManContentView)
    
    let finalMan = UIImageView(frame: CGRect(x: 0, y: 0,
                                                width: finalManContentView.width * 0.85,
                                                height: finalManContentView.width * 0.8 * 0.85))
    finalMan.center = self.view.center
    finalMan.image = UIImage(named: "finalMan")
    self.view.addSubview(finalMan)
    
    
    UIView.animate(withDuration: 0.5, animations: {
        finalMan.transform = CGAffineTransform(rotationAngle: 0.25)
    }) { (finished) in
        UIView.animate(withDuration: 0.5, animations: {
            finalMan.transform = CGAffineTransform(rotationAngle: -0.25)
        }, completion: { (finished) in
            UIView.animate(withDuration: 0.5, animations: {
                finalMan.transform = CGAffineTransform(rotationAngle: 0)
            })
        })
    }
}
複製代碼

在執行粒子動畫的效果時,由於動畫不是拼圖的必須內容,我抽離其成爲一個協議,供業務方進行調用便可。

protocol PJParticleAnimationable {}

extension PJParticleAnimationable where Self: UIViewController {
    func startParticleAnimation(_ point : CGPoint) {
        let emitter = CAEmitterLayer()
        emitter.emitterPosition = point
        emitter.preservesDepth = true
        
        var cells = [CAEmitterCell]()
        for i in 0..<20 {
            let cell = CAEmitterCell()
            cell.velocity = 150
            cell.velocityRange = 100
            cell.scale = 0.7
            cell.scaleRange = 0.3
            cell.emissionLongitude = CGFloat(-Double.pi / 2)
            cell.emissionRange = CGFloat(Double.pi / 2)
            cell.lifetime = 3
            cell.lifetimeRange = 1.5
            cell.spin = CGFloat(Double.pi / 2)
            cell.spinRange = CGFloat(Double.pi / 2 / 2)
            cell.birthRate = 2
            cell.contents = UIImage(named: "Line\(i)")?.cgImage
            
            cells.append(cell)
        }
        emitter.emitterCells = cells
        view.layer.addSublayer(emitter)
    }

    func stopParticleAnimation() {
        view.layer.sublayers?.filter({ $0.isKind(of: CAEmitterLayer.self)}).first?.removeFromSuperlayer()
    }
}
複製代碼

業務調用方只須要遵循這個協議便可調用對於的粒子動畫方法,執行相關動畫。

class ViewController: UIViewController, PJParticleAnimationable {
    // ...
}
複製代碼

總結

至此,「黎錦拼圖」小遊戲就所有完成了!你們愉快的玩耍起來吧,結合第一個遊戲的關卡設計邏輯,咱們也能夠把遊戲策劃和實現這兩大塊內容進行徹底的分離,只須要一張圖片便可完成每個關卡的主題內容,達到了「動態化」。

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

GitHub 地址:github.com/windstormey…

相關文章
相關標籤/搜索