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…

相关文章
相关标签/搜索