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

前言

在上一篇文章中咱們瞭解了與這個遊戲相關的背景知識以及產品設計的前期流程。關於這個遊戲中須要使用到的素材爲了方便你們的學習,我都已經準備好啦!git

對於一個拼圖遊戲來講,最重要的是「拼圖元素」。想必你們小時候包括如今可能也一直在玩拼圖,拼圖遊戲的本質上跟咱們以前完成的小遊戲「可否關個燈」的核心玩法也是相似的,都是經過推斷,去逆序復原成最初的狀態github

對於拼圖遊戲自己來講,咱們徹底能夠直接經過 Sketch、PS 等繪圖軟件,繪製出一個個的「拼圖元素」,但若是咱們真的這麼作會很是很是的浪費精力,是一件費力不討好的事情。咱們能夠利用 iOS 開發中的一些「技巧」來完成對一張完整拼圖的「拆分」。swift

元素上圖

元素上圖分爲兩部分,拼圖元素的拆分和元素上圖。拼圖元素的拆分思路相對比較清晰,咱們先來實現元素上圖。windows

咱們想要把一個「元素」拖到畫布的左邊,並衍生出畫布跟隨其移動的右邊元素,仔細思考一下其實也不復雜:安全

  • 從底部功能欄中拖拽出一個元素;
  • 當把元素放置在畫布的左邊時,在畫布的右邊生成一個與之鏡像對稱的新元素;
  • 當左邊元素進行移動等操做時,順帶移動畫布右邊的元素;

咱們先來搭建遊戲的主視圖。須要用一個虛線把用戶設備界面一分爲二:markdown

class ViewController: UIViewController {

    private var lineImageView = UIImageView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .bgColor
        
        let imgView = UIImageView(frame: CGRect(x: view.width / 2, y: topSafeAreaHeight, width: 5, height: view.height - topSafeAreaHeight - bottomSafeAreaHeight))
        view.addSubview(imgView)
        UIGraphicsBeginImageContext(imgView.frame.size) // 位圖上下文繪製區域
        imgView.image?.draw(in: imgView.bounds)
        lineImageView = imgView
        
        let context:CGContext = UIGraphicsGetCurrentContext()!
        context.setLineCap(CGLineCap.square)
        context.setStrokeColor(UIColor.white.cgColor)
        context.setLineWidth(3)
        context.setLineDash(phase: 0, lengths: [10,20])
        context.move(to: CGPoint(x: 0, y: 0))
        context.addLine(to: CGPoint(x: 0, y: view.height))
        context.strokePath()
        
        imgView.image = UIGraphicsGetImageFromCurrentImageContext()
    }
}
複製代碼

咱們使用了 Core Graphics,經過開啓一個位圖上下文進行了虛線的繪製,在 iOS 中還有不少繪製虛線的方法,在此不作展開。其中,咱們爲了調用簡潔,利用 Swift 的 extension 機制對一些經常使用的例如 UIViewUIColor 等類增長了一些屬性。app

extension UIColor {
    class func rgb(_ r: CGFloat, _ g: CGFloat, _ b: CGFloat) -> UIColor {
        return UIColor(red: r / 255.0, green: g / 255.0, blue: b / 255.0, alpha: 1)
    }
    
    class func rgba(_ r: CGFloat, _ g: CGFloat, _ b: CGFloat, _ a: CGFloat) -> UIColor {
        return UIColor(red: r / 255.0, green: g / 255.0, blue: b / 255.0, alpha: a)
    }
    
    static var bgColor: UIColor {
        return rgb(29, 36, 73)
    }
}
複製代碼
extension UIView {
    // ...

    static private let PJSCREEN_SCALE = UIScreen.main.scale
    
    private func getPixintegral(pointValue: CGFloat) -> CGFloat {
        return round(pointValue * UIView.PJSCREEN_SCALE) / UIView.PJSCREEN_SCALE
    }
    
    public var x: CGFloat {
        get {
            return self.frame.origin.x
        }
        set(x) {
            self.frame = CGRect.init(
                x: getPixintegral(pointValue: x),
                y: self.y,
                width: self.width,
                height: self.height
            )
        }
    }
    
    public var y: CGFloat {
        get {
            return self.frame.origin.y
        }
        set(y) {
            self.frame = CGRect.init(
                x: self.x,
                y: getPixintegral(pointValue: y),
                width: self.width,
                height: self.height
            )
        }
    }

    // ...
}
複製代碼

對於「劉海屏」等異形屏的處理,咱們能夠經過定義幾個全局變量簡化流程。框架

/// 屏幕寬
let screenWidth = UIScreen.main.bounds.size.width
/// 屏幕高
let screentHeight = UIScreen.main.bounds.size.height
/// 底部安全距離
let bottomSafeAreaHeight = UIApplication.shared.windows.first?.safeAreaInsets.bottom ?? 0.0
///頂部的安全距離
let topSafeAreaHeight = UIApplication.shared.windows.first?.safeAreaInsets.top ?? 0.0
/// 狀態欄高度
let statusBarHeight = UIApplication.shared.statusBarFrame.height;
/// 導航欄高度
let navigationBarHeight = CGFloat(44 + topSafeAreaHeight)
複製代碼

運行工程!咱們能夠看到虛線畫出來啦~ide

虛線繪製完成

接下來咱們要完成畫布左右兩邊元素的「行爲同步」,當用戶操做位於畫布左邊的元素時,位於畫布右邊的元素也要同步。爲了保證後續「拼圖視圖」的魯棒性,咱們須要建立一個 Puzzle 類做爲「拼圖元素」。oop

class Puzzle: UIView {

    /// 是否爲「拷貝」拼圖元素
    private var isCopy = false
    
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    convenience init(frame: CGRect, isCopy: Bool) {
        self.init(frame: frame)
        self.isCopy = isCopy
        
        initView()
    }
    
    // MARK: Init
    
    private func initView() {
        backgroundColor = .red
        isUserInteractionEnabled = true
        
        if !isCopy {
            let panGesture = UIPanGestureRecognizer(target: self, action: .pan)
            self.addGestureRecognizer(panGesture)
        }
    }
}


extension Puzzle {
    @objc
    fileprivate func pan(_ panGesture: UIPanGestureRecognizer) {
        let translation = panGesture.translation(in: superview)
        center = CGPoint(x: center.x + translation.x, y: center.y + translation.y)
        panGesture.setTranslation(.zero, in: superview)
    }
}

private extension Selector {
    static let pan = #selector(Puzzle.pan(_:))
}
複製代碼

Puzzle 類中,經過便捷構造方法從外部接收一個 icCopy 變量,用於標記出當前的 Puzzle 位於畫布的左邊仍是右邊,位於畫布右邊的 Puzzle,其 isCopy 變量爲 true

Puzzle 添加了一個 UIPanGestureRecognizer 手勢識別器,用於接收用戶在屏幕上拖拽「拼圖元素」時,同步修改「拼圖元素」在畫布上的位置。在該手勢識別器內部的回調處理方法中,咱們之因此沒有去修改 Puzzlexy 座標,而是修改 center,緣由是隻修改 xy 會致使 Puzzle 在用戶每次觸摸產生移動時發生跳動,左上角老是會跳到用戶此時手指觸摸屏幕的位置上。最好咱們經過 setTranslation 把此時手勢識別器這次識別的手勢距離進行重置爲 0,讓下次手勢識別器識別手勢時產生的距離能夠從相對位置開始,不然會出現距離疊加的問題。

爲了更加 Swifty 一些,咱們對 Selector 方法選擇器寫了個 extension,再對主類寫個 extension,把全部方法選擇器須要用到的方法都寫入其中,保證主類的簡潔。

ViewController.swift 文件中,補充添加 Puzzle 類的實例化相關內容:

class ViewController: UIViewController {

    private var lineImageView = UIImageView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .bgColor
        
        let imgView = UIImageView(frame: CGRect(x: view.width / 2, y: topSafeAreaHeight, width: 5, height: view.height - topSafeAreaHeight - bottomSafeAreaHeight))
        view.addSubview(imgView)
        UIGraphicsBeginImageContext(imgView.frame.size) // 位圖上下文繪製區域
        imgView.image?.draw(in: imgView.bounds)
        lineImageView = imgView
        
        let context:CGContext = UIGraphicsGetCurrentContext()!
        context.setLineCap(CGLineCap.square)
        context.setStrokeColor(UIColor.white.cgColor)
        context.setLineWidth(3)
        context.setLineDash(phase: 0, lengths: [10,20])
        context.move(to: CGPoint(x: 0, y: 0))
        context.addLine(to: CGPoint(x: 0, y: view.height))
        context.strokePath()
        
        imgView.image = UIGraphicsGetImageFromCurrentImageContext()
        
        // 新增「拼圖元素初始化」
        let puzzle = Puzzle(frame: CGRect(x: 100, y: 100, width: 50, height: 50), isCopy: false)
        view.addSubview(puzzle)
    }
}
複製代碼

運行工程~紅色視圖能夠接收觸摸事件啦!

給拼圖添加手勢

拼圖元素拆分

在上文中,咱們已經完成元素上圖,接下來咱們須要把一張完整的圖進行切割,切割成一張張的符合咱們尺寸要求的小圖。但在切割以前,咱們須要對圖作適配,前文已經說明,咱們要作一個在 iPhone 上運行的遊戲,而 iPhone 屏幕尺寸是長比寬大的形狀,咱們只須要根據適配底圖的寬度爲屏幕寬度,並把兩者的比例乘上底圖的高度,這樣就能夠作到全尺寸適配了。但經過這種作法,在 SE 上底圖最下邊的一條線會稍微遮蓋一丟丟,不過不要緊。

class ViewController: UIViewController {

    /// 中間分割線
    private var lineImageView = UIImageView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .bgColor
        
        let imgView = UIImageView(frame: CGRect(x: view.width / 2, y: topSafeAreaHeight, width: 5, height: view.height - topSafeAreaHeight - bottomSafeAreaHeight))
        view.addSubview(imgView)
        UIGraphicsBeginImageContext(imgView.frame.size) // 位圖上下文繪製區域
        imgView.image?.draw(in: imgView.bounds)
        lineImageView = imgView
        
        let context:CGContext = UIGraphicsGetCurrentContext()!
        context.setLineCap(CGLineCap.square)
        context.setStrokeColor(UIColor.white.cgColor)
        context.setLineWidth(3)
        context.setLineDash(phase: 0, lengths: [10,20])
        context.move(to: CGPoint(x: 0, y: 0))
        context.addLine(to: CGPoint(x: 0, y: view.height))
        context.strokePath()
        
        imgView.image = UIGraphicsGetImageFromCurrentImageContext()

        // 底圖適配
        let contentImage = UIImage(named: "01")!
        let contentImageScale = view.width / contentImage.size.width
        let contentImageViewHeight = contentImage.size.height * contentImageScale
        
        let contentImageView = UIImageView(frame: CGRect(x: 0, y: topSafeAreaHeight, width: view.width, height: contentImageViewHeight))
        contentImageView.image = contentImage
        view.addSubview(contentImageView)
    }
}
複製代碼

屢次運行工程!跑不一樣的模擬器,底圖已經適配好啦~

底圖適配

對底圖進行機型的適配,如今咱們須要對已經適配完的底圖進行切割。切割這個理念自己並無什麼難以理解的地方,簡單來講:在圖中找到一個設定的區域,對該區域進行裁剪,保存裁剪的圖片。

在這裏咱們須要利用到一樣爲 Core Graphcs 框架下 CGImage 類的 cropping() 方法,該方法在 Apple 的文檔中是這麼描述的:

Create an image using the data contained within the subrectangle rect of image.

extension UIImage {
    /// 經過原圖獲取 rect 大小的圖片
    func image(with rect: CGRect) -> UIImage {
        let scale: CGFloat = 2
        let x = rect.origin.x * scale
        let y = rect.origin.y * scale
        let w = rect.size.width * scale
        let h = rect.size.height * scale
        let finalRect = CGRect(x: x, y: y, width: w, height: h)
        
        let originImageRef = self.cgImage
        let finanImageRef = originImageRef!.cropping(to: finalRect)
        let finanImage = UIImage(cgImage: finanImageRef!, scale: scale, orientation: .up)
        
        return finanImage
    }
}
複製代碼

咱們須要在經過設置一個 scale 係數在裁剪時縮放元素,底圖只作了一個二倍圖的尺寸,因此咱們的縮放係數就不從設備讀取了,直接寫死。若是咱們不乘上這個縮放係數,cropping 裁切出來的圖片像素大小爲是一倍圖的大小,在視覺上會有一種被強行放大的感覺,所以咱們須要一個縮放係數去控制。

Puzzle 作個調整,默認新建立的拼圖元素位於視圖容器的左上角。

class Puzzle: UIImageView {
    // ......
    
    convenience init(size: CGSize, isCopy: Bool) {
        self.init(frame: CGRect(x: 0, y: 0, width: size.width, height: size.height))
        self.isCopy = isCopy
        
        initView()
    }

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

接下來在 ViewController.swift 中補充完相關的切割邏輯。底圖是個徹底鏡面對稱的圖形,咱們第一步先完成畫布左右各三個拼圖,也就是一行六列「拼圖元素」,每一個「拼圖元素」的寬高相等,行數根據底圖的長度和「拼圖元素」的商值計算得出。

class ViewController: UIViewController {

    private var lineImageView = UIImageView()
    private var puzzles = [Puzzle]()
    
    override func viewDidLoad() {

        // ......
        
        // 底圖適配
        let contentImage = UIImage(named: "01")!
        let contentImageScale = view.width / contentImage.size.width
        let contentImageViewHeight = contentImage.size.height * contentImageScale
        
        let contentImageView = UIImageView(frame: CGRect(x: 0, y: topSafeAreaHeight, width: view.width, height: contentImageViewHeight))
        contentImageView.image = contentImage
        
        // 一行六個
        let itemHCount = 6
        let itemW = Int(view.width / CGFloat(itemHCount))
        let itemVCount = Int(contentImageView.height / CGFloat(itemW))
        
        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
                puzzles.append(puzzle)
                
                view.addSubview(puzzle)
            }
        }
    }
}

複製代碼

運行工程!切割好的拼圖元素出來啦!

切割元素

後記

在這篇文章中,咱們對遊戲的核心操做對象——「拼圖元素」進行了一個拆分,作到了根據不一樣的遊戲運行設備的自適應,併成功的根據適配好的原圖切割出了全部的拼圖元素。咱們完成的需求有:

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

GitHub 地址:github.com/windstormey…

相關文章
相關標籤/搜索