preface
In the last article, we have basically completed all content except game win logic. In this article, we will directly “simulate” the real life jigsaw game win logic to continue to improve our “Li Jin jigsaw” mini-game.
In real life, no matter how big the puzzle is, we can always find the shadow of a two-dimensional array in the end. The elements of the puzzle are arranged one by one on the canvas of the game, which can be understood as the two-dimensional array is slowly filled up. In previous articles we used leftPuzzles and rightPuzzles for the left and rightPuzzles of the canvas, but both were one-dimensional and could be logically maintained.
Magnetic effect
Before implementing the win logic, let’s implement a small feature that will increase the player’s enjoyment — the “magnet effect”, as shown below.
The effect is no different from the magnets we played with as children, when an iron bar appears next to a magnet and the magnet attracts the iron bar to the magnet itself. Therefore, we want to achieve the effect that when we stop moving the puzzle element, the puzzle element will move to the nearest virtual “square”.
Do virtual “square” cut it we don’t need to really cut, according to the above, we just need to logically maintain a “simulation” grid can, therefore, our task is transformed to at the end of the jigsaw puzzle elements drag events, find the distance the recent virtual “square” puzzle elements.
Roughly idea is, when the puzzle elements of drag and drop events at the end of each time, to obtain the coordinates of the current puzzle elements, through some of the coordinates, the coordinate transformation into a virtual “square” index, and then to the coordinates of the jigsaw puzzle elements directly to the coordinates of an assignment for the virtual “grid”, the core code is shown below.
class ViewController: UIViewController {
// ...
override func viewDidLoad(a) {
// ...
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()
}
}
/// start the magnet
private func adsorb(a) {
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
}
}
Copy the code
At this point, run the project, you can see the interesting magnetic effect ~
Mutually exclusive logical
After completing the magnetization effect and running the project, you should notice that when you have two pieces of the same puzzle in the same position on the canvas, they overlap, and you won’t “realize” that the current position is occupied. Therefore, we need to write another “mutex logic” to ensure that the same position does not allow the puzzle to overlap. We need to consider two scenarios.
Puzzles A and B are both on the canvas, with A moving to B’s position
In this case, we need to make some changes to the game data source ontology. Before, we simply append an array of puzzle elements added to the canvas, but this only “added”, without explicitly marking the position of the puzzle on the canvas, we need to simulate the abstract logic of a game canvas from the data source itself.
To simulate this logic, I use a two-dimensional matrix, initialize each “grid” to -1 in the viewDidLoad method, and then record the corresponding tag of the puzzle into the two-dimensional matrix after executing the addSubview logic in the panEnded closure callback of the puzzle elements. To simulate what’s called a “place” operation.
class ViewController: UIViewController {
// ...
private var finalPuzzleTags = [[Int]] ()override func viewDidLoad(a) {
super.viewDidLoad()
// ...
// A row of six
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)
// ...}}Copy the code
In the “boot” algorithm, to calculate the current puzzles on screen after the coordinates of the index, combined with the judgment and two-dimensional matrix are assignment, according to the assignment to detect whether there is the value of the non – 1 if the position is the value of the non – 1 shows the canvas that the position has been other puzzle pieces, is the puzzle pieces of mobile location is back.
/// start the magnet
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
}
// Go beyond the bottom
if (Int(tempPuzzleYIndex) > self.finalPuzzleTags.count) {
tempPuzzle.center = tempPuzzle.beginMovedPoint
}
// What you already have cannot be occupied
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
}
}
Copy the code
Run the project! Found that two puzzle pieces on the canvas cannot occupy each other’s positions when moving
Puzzle A is on the canvas, and puzzle B moves from the bottom toolbar to the position of Puzzle A
This situation serves as a place for everyone to improve on their own. If you want puzzle B to find that the position you moved to on the canvas is already occupied, you can leave the position of Puzzle B on the bottom toolbar uncleared and wait until puzzle B is actually added to the canvas before deleting it.
This belongs to the product strategy, implementation ideas have been explained, according to the way you like to achieve it!
To improve the UI
When we went to finish the game, two of Hercules’ heads appeared.
This is because when we implement the “cut jigsaw” algorithm, we do not deal with special cases, only consider the feasibility of the algorithm, and do not consider the special boundary. The idea is to reduce the width of the image to be “captured” by a third when generating the last piece of the puzzle in each row.
class ViewController: UIViewController {
// ...
override func viewDidLoad(a) {
// ...
for itemY in 0..<itemVCount {
for itemX in 0..<itemHCount {
let x = itemW * itemX
let y = itemW * itemY
var finalItemW = itemW
var finalItemH = itemH
/ / special points
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),
// ...}}}}Copy the code
Running the project at this time, found some strange places.
When this problem appeared, I did think for a while, has been entangled in whether the interception algorithm write wrong, thinking about thinking suddenly trance come! Just change the contentMode of the puzzle.
class Puzzle: UIImageView {
// ...
private func initView(a) {
// All left, copyPuzzle mirror symmetry
contentMode = .left
// ...
}
// ...
}
Copy the code
I solved the problem near the middle line, and when I got to the last line of the puzzle, I found that the elements in the last line were not quite right again.
The reason for this problem follows the previous solution idea, change the contentMode of the puzzle to TOP, but need to do some identification bit judgment, to determine whether the contentMode of the current puzzle is left or top, some extra dirty code.
So, we just need to determine the current is the last row of the puzzle element, in the “magnet algorithm” to move the last row of the element 20 up.
class ViewController: UIViewController {
// ...
/// start the magnet
private func adsorb(_ tempPuzzle: Puzzle) {
// ...
if tempPuzzleCenterPoint.y < Yedge {
// When is the last column, move up 20
if (Int(tempPuzzleYIndex) = = finalPuzzleTags.count) {
tempPuzzleCenterPoint.y = Yedge - tempPuzzle.height / 2 - 20
} else {
tempPuzzleCenterPoint.y = Yedge - tempPuzzle.height / 2}}// ...}}Copy the code
Run the project and restart the game!
To win the logical
In the previous several articles, we cut the original large picture and marked the specific position index of each puzzle element in the original large picture according to the cutting order through gameIndex. Then we scrambled the position of the elements in the container storing these cut puzzle elements. The final rendering of the puzzle elements on the bottom feature bar becomes “random”.
Therefore, in win logic, what we need to do is to determine whether the game is currently won at the end of each drag event for each puzzle element. The main concern of the winning logic is whether the index represented by the position of the puzzle elements placed by the player is consistent with the tag index set for each puzzle element when the puzzle game is initialized.
class ViewController: UIViewController {
// ...
/// win algorithm
private func isWin(a) -> 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
}
// ...
}
Copy the code
After winning the game, we need to give the player a reward. My reward here is to see hercules in its own right, plus some particle-based animations.
private func winAnimate(a) {
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)})})}}Copy the code
When implementing the particle animation effect, since the animation is not a required part of the puzzle, I pull it out and make it a protocol that the business side can call.
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(a) {
view.layer.sublayers?.filter({ $0.isKind(of: CAEmitterLayer.self)}).first?.removeFromSuperlayer()
}
}
Copy the code
The business caller only needs to follow this protocol to invoke the particle animation method for the corresponding animation.
class ViewController: UIViewController.PJParticleAnimationable {
// ...
}
Copy the code
conclusion
So far, “Li Jin jigsaw puzzle” small game is all completed! Let’s play happily. In combination with the level design logic of the first game, we can completely separate game planning and implementation. Only one picture can complete the theme content of each level, achieving “dynamic”.
- Jigsaw material preparation;
- Element above;
- State maintenance;
- Elemental adsorption;
- The UI perfect;
- Winning logic;
- Victory dynamic effect.
GitHub address: github.com/windstormey…