preface

In the last article we learned about the background of the game and the initial process of product design. About this game need to use the material for the convenience of everyone’s study, I have prepared!

The most important thing for a jigsaw puzzle is the “jigsaw elements”. You must have been playing jigsaw puzzles when you were young, including now. The essence of jigsaw puzzles is similar to the core gameplay of the mini-game “Can I turn off the light”, which is to reverse order and restore the original state through inference.

For the puzzle game itself, we can draw the “puzzle elements” one by one directly through Sketch, PS and other drawing software, but if we really do so, it will be a waste of energy, and it is a thankless thing. There are some “tricks” in iOS development that can be used to “break” a complete puzzle.

The element above,

The element diagram is divided into two parts, the resolution of the puzzle elements and the element diagram. The separation of puzzle elements is relatively clear, we first to achieve the elements above.

We want to drag an “element” to the left of the canvas and derive the element on the right that the canvas moves with, which isn’t too complicated if you think about it:

  • Drag an element from the bottom function bar.
  • When an element is placed to the left of the canvas, a new element is created on the right side of the canvas that mirrors it.
  • When the left element is moved, move the right element of the canvas.

Let’s start by building the main view of the game. A dotted line is required to split the user device interface in two:

class ViewController: UIViewController {

    private var lineImageView = UIImageView(a)override func viewDidLoad(a) {
        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) // Bitmap context draws the region
        imgView.image?.draw(in: imgView.bounds)
        lineImageView = imgView
        
        let context:CGContext = UIGraphicsGetCurrentContext(a)!
        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()}}Copy the code

We used Core Graphics to draw dashed lines by enabling a bitmap context. There are many other ways to draw dashed lines in iOS, so we won’t expand them here. Among them, for the sake of simplicity, we use Swift extension mechanism to add some attributes to some commonly used classes such as UIView and UIColor.

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)}}Copy the code
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
            )
        }
    }

    // ...
}
Copy the code

We can define several global variables to simplify the process of processing irregular screens such as “bangs screen”.

/ / / the screen width
let screenWidth = UIScreen.main.bounds.size.width
/ / / screen
let screentHeight = UIScreen.main.bounds.size.height
/// bottom safe distance
let bottomSafeAreaHeight = UIApplication.shared.windows.first?.safeAreaInsets.bottom ?? 0.0
/// The top of the safe distance
let topSafeAreaHeight = UIApplication.shared.windows.first?.safeAreaInsets.top ?? 0.0
/// Status bar height
let statusBarHeight = UIApplication.shared.statusBarFrame.height;
/// Navigation bar height
let navigationBarHeight = CGFloat(44 + topSafeAreaHeight)
Copy the code

Run the project! We can see the dotted line here

The next step is to “synchronize behavior” of the elements on the left and right sides of the canvas. As the user manipulates the elements on the left side of the canvas, the elements on the right side of the canvas also synchronize. To ensure the robustness of the subsequent jigsaw view, we need to create a Puzzle class as a “jigsaw element.”

class Puzzle: UIView {

    /// whether to "copy" puzzle elements
    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(a) {
        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(_:))}Copy the code

In the Puzzle class, an icCopy variable is received externally via the convenient constructor that marks whether the current Puzzle is on the left or right side of the canvas. Puzzle on the right side of the canvas has an isCopy variable of true.

Added a UIPanGestureRecognizer gesture recognizer to Puzzle that synchronously changes the position of the “Puzzle element” on the canvas when the user drags the “Puzzle element” across the screen. In the callback processing method inside the gesture recognizer, we did not change the X and Y coordinates of Puzzle, but changed the center, because changing only X and Y would cause Puzzle to jump every time the user moved by touching. The top left corner always jumps to where the user’s finger is touching the screen. It is better to reset the gesture distance recognized by the gesture recognizer to 0 through setTranslation, so that the distance generated by the gesture recognizer next time can start from the relative position, otherwise there will be the problem of distance superposition.

To make it Swifty, we’ll write an extension to the Selector method Selector, and then an extension to the main class. We’ll write all the methods that the method Selector needs to use to keep the main class simple.

Add the instantiation of the Puzzle class to the viewController.swift file:

class ViewController: UIViewController {

    private var lineImageView = UIImageView(a)override func viewDidLoad(a) {
        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) // Bitmap context draws the region
        imgView.image?.draw(in: imgView.bounds)
        lineImageView = imgView
        
        let context:CGContext = UIGraphicsGetCurrentContext(a)!
        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(a)// Add "jigsaw element initialization"
        let puzzle = Puzzle(frame: CGRect(x: 100, y: 100, width: 50, height: 50), isCopy: false)
        view.addSubview(puzzle)
    }
}
Copy the code

Red view can receive touch events now!

Jigsaw element resolution

In the above article, we have completed the element diagram, next we need to cut a complete picture into smaller pictures that meet our size requirements. But before cutting, we need to do to figure adaptation, was handled in the specification, we need to do a run on the iPhone’s game, and the shape of the iPhone screen size is longer than wide, we only need to according to fit the width of the reproduction for screen width, and the proportion of the two on the height of the base, so that you can do full size adaptation. But by doing this, the bottom line on the SE will cover up a little bit, but that’s okay.

class ViewController: UIViewController {

    /// Middle divider
    private var lineImageView = UIImageView(a)override func viewDidLoad(a) {
        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) // Bitmap context draws the region
        imgView.image?.draw(in: imgView.bounds)
        lineImageView = imgView
        
        let context:CGContext = UIGraphicsGetCurrentContext(a)!
        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(a)// Base map fit
        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)
    }
}
Copy the code

Run the project multiple times! Run different emulators, the base map has been adapted

To adapt the base map, now we need to cut the base map that has been adapted. There’s nothing difficult about the idea of cutting itself. It’s simple: Find a specific area in the image, crop that area, and save the cropped image.

Here we need to use the cropping() method of the CGImage class in the Core Graphcs framework, which is described in the Apple documentation as follows:

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

extension UIImage {
    // Get the rect size image from the original image
    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
    }
}
Copy the code

We need to scale the elements when clipping by setting a scale factor. The base image is only a double image size, so our scale factor is not read from the device and written to death. If we don’t multiply the zoom factor, the pixel size of the cropping picture is twice that of the graph, which makes it visually feel like being forcibly enlarged, so we need a zoom factor to control it.

Make a change to Puzzle so that by default newly created Puzzle elements are in the upper left corner of the view container.

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()
    }

    / /...
}
Copy the code

Next, add the relevant cutting logic in viewController.swift. The base graph is a completely mirror symmetrical graph. The first step is to complete three puzzles on the left and right of the canvas, namely six columns of “puzzle elements” in one row. Each “puzzle element” has the same width and height, and the number of lines is calculated according to the length of the base graph and the quotient of “puzzle elements”.

class ViewController: UIViewController {

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

        / /...
        
        // Base map fit
        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
        
        // A row of six
        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)
            }
        }
    }
}

Copy the code

Run the project! The cut puzzle pieces are out!

Afterword.

In this article, we split the core operation object of the game — “puzzle elements”, achieved adaptive according to different game running devices, and successfully cut out all the puzzle elements according to the adapted original picture. We have completed the following requirements:

  • Jigsaw material preparation;
  • Element above;
  • State maintenance;
  • Elemental adsorption;
  • The UI perfect;
  • Winning logic;
  • Victory dynamic effect;

GitHub address: github.com/windstormey…