The foreword 0.

Recently, the small program “Tiaoyitiao” launched by wechat is really popular all over the country. After seeing it as a developer, I can’t help thinking: can I combine it with ARKit and play it in the AR scene? So that’s the idea. Based on my previous experience, I have this demo: Abbott Jump. Here is a brief introduction to how to make such a small game.

1. Prepare knowledge

First, we need to have some basic understanding of SceneKit and ARKit. For SceneKit, you should at least know the basic classes SCNNode, SCNGeometry, SCNAction, SCNVector3 and their common attributes and methods (see the Apple documentation). If you’re not familiar with ARKit, check out one of my previous articles: ARKit Primer.

When you’re ready, let’s get down to business.

2. The whole idea

I’ve broken down the steps of this little game into the following sub-steps:

  1. Place the squares
  2. Let the bottle jump
  3. Judging a game to fail

2.1 Placing blocks

We know that in ARKit we have a three dimensional coordinate system for the real world. By observing the “jump jump” on wechat, we can find that the next square is placed either to the left or right of the current square. For the sake of simplicity, let’s place the squares on the XZ plane of the coordinate system and randomly decide whether to extend in the x or z direction each time. The schematic diagram is as follows:

Blue represents squares that are generated in sequence, and you can see that their paths (red arrows) are parallel to the X or Z axis.

First, create a new enumeration class that lists possible directions for the next square:

Enum NextDirection: Int {case left       = 0
    case right      = 1
}
Copy the code

Then declare an array to record all the squares that have already appeared:

private var boxNodes: [SCNNode] = []
Copy the code

Finally, the method of generating blocks:

Private func generateBox(at realPosition: SCNVector3) {// Generate a squareletSCNBox(width: kBoxWidth, height: kBoxWidth / 2.0, length: kBoxWidth, chamferRadius: 0.0)letNode = SCNNode(Geometry: box) // Color the boxletMaterial = SCNMaterial () material. The diffuse. Contents. = UIColor randomColor () box. If materials = [material] / / square number is empty, Note in the initialization of the game, directly put the box position where you clickif boxNodes.isEmpty {
        node.position = realPosition
    } elseNextDirection = nextDirection (rawValue: Int(arc4random() % 2))! // Calculate the distance from the current square according to the random numberletDeltaDistance = Double(arc4random() % 25 + 25) / 100.0 deltaDistance = Double(arc4random() % 25 + 25) / 100.0if nextDirection == .left {
            node.position = SCNVector3(realPosition.x + Float(deltaDistance), realPosition.y, realPosition.z)
        } else{node.position = SCNVector3(realPosition.x, realPosition.y, realPosition.z + Float(deltaDistance))}} And add into the square array sceneView. Scene. RootNode. AddChildNode (node) boxNodes. Append (node)}Copy the code

By doing this, you can generate blocks in the game. So, when is this method called?

The first is at the beginning of the game. We click and decide where to start the game. Here we override the touchesBegan(_:_:) method (and actually touchesEnd(_:_:)), as explained below.

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {... // Add bottle funcaddConeNode() { bottleNode.position = SCNVector3(boxNodes.last! .position.x, boxNodes.last! .position.y + Float(kBoxWidth) * 0.75, boxNodes. Last! . The position. Z) sceneView. Scene. RootNode. AddChildNode (bottleNode)} / / click on the test, did you get a 3 d coordinate of feature points? func anyPositionFrom(location: CGPoint) -> (SCNVector3)? {letresults = sceneView.hitTest(location, types: .featurePoint) guard ! results.isEmptyelse {
            return nil
        }
        return SCNVector3.positionFromTransform(results[0].worldTransform)
    }
    
    letlocation = touches.first? .location(in: sceneView)
    if letposition = anyPositionFrom(location: location!) { generateBox(at: position) addConeNode() generateBox(at: boxNodes.last! .position) } ... }Copy the code

The biggest use of ARKit is probably the anyPositionFrom(_:) method here. Here, hitTest(_:_:) is used to determine whether any feature points on the screen have been touched. If so, use an extension to SCNVector3 to convert the obtained real world coordinates into virtual world coordinates. The following operations are converted to the coordinate system of the virtual world.

As you can see, when clicking on a location can successfully obtain at least one location through the click test method, that location is where we want to build/start the game. GenerateBox (_:) is then called once to generate a square at that location, and addConeNode() is added to the square, finally generating a square to which the bottle will jump.

The second place to generate a square is when a piece successfully lands on the next square, as explained below.

2.2 Let the bottle jump

As mentioned earlier, we overwrite the touchesBegan(_:_:) and touchesEnd(_:_:). In Hop, the factor that determines how far the bottle flies is the amount of time the screen is pressed. By using these two methods, one start and one end, you can get the start and end time of the press, and then you can easily get the length of a press. And then you do some function calculations with this length, and you get the distance that you’re going to travel next time. As a result, a lot of key logic can be placed in these two methods.

First, declare a tuple that records the start and end times of pressing the screen:

private var touchTimePair: (begin: TimeInterval, end: TimeInterval) = (0, 0)
Copy the code

Then, declare a closure that calculates the distance traveled by the time difference. Here we simply do a division:

private let distanceCalculateClosure: (TimeInterval) -> CGFloat = {
    return CGFloat($0) / 4.0}Copy the code

Here are the two methods. At the beginning of pressing:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {...ifBoxnodes.isempty {same as in 2.1}else{// The game is in progress, press the screen, record the start time touchTimepair.begin = (event? .timestamp)! }}Copy the code

At the end of pressing, the end time is not only recorded, the time difference is calculated, but also the bottle is moved according to the time difference:

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {... TouchTime {pair. end = (event? .timestamp)! // Calculate the time difference between the twoletDistance = distanceCalculateClosure(touchTimepair.end - touchTimepair.begin) Var Actions = [SCNAction()]if nextDirection == .left {
        let moveAction1 = SCNAction.moveBy(x: distance, y: kJumpHeight, z: 0, duration: kMoveDuration)
        let moveAction2 = SCNAction.moveBy(x: distance, y: -kJumpHeight, z: 0, duration: kMoveDuration)
        actions = [SCNAction.rotateBy(x: 0, y: 0, z: -.pi * 2, duration: kMoveDuration * 2),
                   SCNAction.sequence([moveAction1, moveAction2])]
    } else {
        let moveAction1 = SCNAction.moveBy(x: 0, y: kJumpHeight, z: distance, duration: kMoveDuration)
        let moveAction2 = SCNAction.moveBy(x: 0, y: -kJumpHeight, z: distance, duration: kMoveDuration)
        actions = [SCNAction.rotateBy(x: .pi * 2, y: 0, z: 0, duration: kMoveDuration * 2),
                   SCNAction.sequence([moveAction1, moveAction2])]
    }
    ...
Copy the code

In order to imitate the animation effect of wechat jump, SCNAction’s Group and sequence methods are used. Group indicates that two actions are performed in parallel, and sequence indicates that two actions are performed consecutively. So the final stack looks like this:

Following the above code, we move the bottle, and after it moves, we judge whether the game has failed. And again, this is where the next cube is generated.

CompletionHandler: {[weak self] // Gets the last square, the one the bottle jumps overletboxNode = (self? .boxNodes.last!) ! // If the box does not contain a bottle, the game failsif(self? .bottleNode.isNotContainedXZ(in: boxNode))! {// Record high score, failure message, etc.}else{// If so, the game continues, generating the next block... generateBox(at: (self? .boxNodes.last! .position)!) }})}Copy the code

2.3 Judging the game failed

Since both our cubes and bottles move along axes or parallel lines, the isNotContainedXZ(in:) method described in Section 2.2 can be described as follows:

func isNotContainedXZ(in boxNode: SCNNode) -> Bool {
    let box = boxNode.geometry as! SCNBox
    let width = Float(box.width)
    ifFabs (position.x-boxNode.position.x) > width / 2.0 {return true
    }
    ifFabs (position.z-boxNode.position.z) > width / 2.0 {return true
    }
    return false
}
Copy the code

The specific meaning is to compare the absolute value of the difference between the square and the bottle’s center point on the X and Z axes. As long as any one is larger than half of the width of the square, the bottle is considered to fall outside the square range, as shown in the diagram below (red represents the bottle’s center point) :

Of course, if you want to keep it simple, you can turn all the squares into cylinders, and then you just have to figure out the relationship between the distance between the center and the radius of the cross section of the cylinder.

The general flow of the game is now complete. The first step is to generate blocks, then make the bottle move according to the length of the press, and determine if the game has failed after the movement, thus forming a closed loop of game logic.

3. Little slacks and improvements

As time is very short, in many places have done a little lazy. Such as:

  • At ARKit initialization, the orientation of the 3d coordinate system is determined. So the x and Z axes cannot be changed throughout the game.
  • The shape of the generated square is single, unlike wechat and cylinder, circular platform and so on.
  • The interface is a little ugly (after all, using native SCNGeometry)

So what can be improved in the future?

First of all, it is better to change the orientation of the coordinate axis, for example, each time the user’s current mobile phone facing position as the X-axis.

Secondly, some improvements or enhancements can be made in animation effects, aesthetics and sound effects.

Finally, if you can break the two-dimensional pattern, or even combine it with a real world object, it would be perfect.

4. Other

The project is open source as GPL V3.0 on GitHub: ARBottleJump, welcome Star/PR/Issue!

And thanks to the original version of the game: Jumping In The Bottle, Ketchapp really makes a lot of fun little games.

Making:songkuixi

Weibo:Slippery chicken

2018-01-04