D8805030d828cae0c691121d3d3c5ccb
Swift 游戏开发之黎锦拼图(一)

前言

在上一篇文章中我们了解了与这个游戏相关的背景知识以及产品设计的前期流程。关于这个游戏中需要使用到的素材为了方便大家的学习,我都已经准备好啦!

对于一个拼图游戏来说,最重要的是「拼图元素」。想必大家小时候包括现在可能也一直在玩拼图,拼图游戏的本质上跟我们之前完成的小游戏「能否关个灯」的核心玩法也是类似的,都是通过推断,去逆序复原成最初的状态

对于拼图游戏本身来说,我们完全可以直接通过 Sketch、PS 等绘图软件,绘制出一个个的「拼图元素」,但如果我们真的这么做会非常非常的浪费精力,是一件费力不讨好的事情。我们可以利用 iOS 开发中的一些「技巧」来完成对一张完整拼图的「拆分」。

元素上图

元素上图分为两部分,拼图元素的拆分和元素上图。拼图元素的拆分思路相对比较清晰,我们先来实现元素上图。

我们想要把一个「元素」拖到画布的左边,并衍生出画布跟随其移动的右边元素,仔细思考一下其实也不复杂:

  • 从底部功能栏中拖拽出一个元素;
  • 当把元素放置在画布的左边时,在画布的右边生成一个与之镜像对称的新元素;
  • 当左边元素进行移动等操作时,顺带移动画布右边的元素;

我们先来搭建游戏的主视图。需要用一个虚线把用户设备界面一分为二:

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 等类增加了一些属性。

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)

运行工程!我们可以看到虚线画出来啦~

虚线绘制完成

虚线绘制完成

接下来我们要完成画布左右两边元素的「行为同步」,当用户操作位于画布左边的元素时,位于画布右边的元素也要同步。为了保证后续「拼图视图」的鲁棒性,我们需要创建一个 Puzzle 类作为「拼图元素」。

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 类的实例化相关内容:

```swift

top Created with Sketch.