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…

相关文章
相关标签/搜索