Xcode 9. If you do not have Xcode yet or do not have an Apple Developer account registered with Xcode, go to this link, sign in with your apple ID to create a free Apple Developer account https://developer.apple.com/xcode/
To run the AR app on your phone, you must have an iPhone with iOS 11.3 +
If you run into trouble with Xcode, iOS, or MacOS versions, contact me (@Jen). If you do not have the correct versions installed, you may get an error when trying to run the app on your phone that says "cannot find support files"
Keep in mind that if you have a Mac but do not have a compatible device, I will be bringing my iPad so everyone who develops the app will be able to test it out
IMPORTANT NOTE: If you want to copy and paste code, please check in the shared google drive for myViewController.txt file!
Let's get started!
Open Xcode and click "Create a new Xcode project"
Next, choose "Augmented Reality App" for your project template
You will then be taken to the options page where you will be able to choose a name for your AR application. I chose "AR Basketball" for my Project Name. Make sure that all the fields are filled out like this:
Ensure that "Include Unit Tests" and "Include UI Tests" are unchecked as we will not be needing them. We will be coding in Swift and using Apple's SceneKit for creating content.
After clicking "Next", you will be asked to save your Xcode project - save the project wherever you would like on your computer.
The first thing we are going to do is go into the storyboard by clicking "Main.storyboard" in the dropdown menu on the left side. We will now be taken to a page presenting the "ARSCNView". This is the main view in which all virtual objects are rendered onto. This is what makes Augmented Reality possible. However, this view does not allow us to add buttons, so we are going to delete it, for now.
Under "View Controller Scene" -> "View Controller", locate "Scene View" and delete it by pressing the delete button.
To ensure that we can add buttons later on, we need to add "UIView" to our scene. In the bottom right corner, search for "UIView" and select "View" and drag it onto the scene.
Next, using the same search bar, search for the "ARSCNView" that we recently deleted and add that to your scene and stretch it to cover the entire area.
Now, we need to add constraints to define rules for the layout of our application. In the bottom right, to the left of the search bar we used in the step above, add four constraints by clicking the red lines surrounding the box, all set to "0", and then click "Add 4 constraints".
Typically, the sceneView is connected to the main view, but since we deleted the main view, that connection was also deleted. In order to connect the sceneView to the main view, we need to right click the "View Controller" in the dropdown menu under "View Controller Scene". Click the plus sign by the "sceneView" and drag it to the main view like this:
To get the basketball hoop download the files labeled "Basketball" in the folder Charles created on our shared Google Drive. Once downloaded, right click the folder "art.scnassets" in the left-side dropdown menu and click "Add Files to 'art.scnassets'" and select only the hoop file.
Now, to get the basketball skin, locate "Assets.xcassets" below the "art.scnassets" folder, and select the files "basketballSkin" and "deleteIcon" and drag them into the "Assets.xcassets" folder.
It's time to code! We need to load the hoop model that we just imported into our scene. Go into "ViewController.swift"
Under the "viewDidLoad()" function, create a new function called "addBackboard()"
See code below. The following is an explanation.
We need to access the hoop element in the hoop.scn file. If you click "hoop.scn", you can see there is a "backboard" element and a "net" element. To access the backboard element, we must first access the hoop.scn, and then the backboard.
SCNScene() takes in one parameter, which is the location of the scene you want to load. Similarly, to access the childNode of this element, we put in the name of the element we want, "backboard", and set recursively to "false" since the backboard is a root element of "hoop.scn"
To set the position of the backboard, we need to set the position of a 3D Vector, SCNVector3 where x:0 means that the hoop will be centered from the device, y:0.5 means that the hoop will be 50cm above the device, and z: -3 is 3m in front of the device. You can change these settings if you want!
To add to the scene, we addChildNode() to the sceneView.
NOTE: COMMENT OUT currentNode = backboardNode. That is for later on in the tutorial. In the viewDidLoad() function, find the line "let scene = SCNScene(named: "art.scnassets/ship.scn")!" and change it to "let scene = SCNScene()" and under the line "sceneView.scene = scene", call your function "addBackboard()".
Checkpoint 1: Basketball Hoop
Try building and running your app on your device, you should see your AR basketball hoop! To run on your phone, plug your device into your computer and to the right of the run button in the upper left corner, find your device and click run. If you have any errors, contact me (@Jen).
See code to the left, the following is an explanation: The next step is adding in the ball. We need to implement a Gesture Recognizer so that we can shoot the ball whenever we tap the screen. Under the addBackboard() call in viewDidLoad(), add a call to the function that we are about to create, registerGestureRecognizer()
Above the addBackboard() function, write a new function, registerGestureRecognizer().
First we need to create the tap gesture recognizer. The target will be self since it references the ViewController, which is the main screen. The action is the action that will be performed after we touch the screen. The #selector(handleTap) will call the function handleTap when the screen is tapped (we will define this function shortly). We now need to add the UITapGestureRecognizer, tap, to the scene.
Now, we need to create the handleTap function. Put this function right under the registerGestureRecognizer() function.
Code Explanation for code below:
We wrote @objc by this function because #selector is part of the objective C framework, so in order for the UITapGestureRecognizer to recognize the handleTap function, we need "@obj" in front of the function. The function takes in a gestureRecognizer. First, we need to access the scene and locate the point of view of the scene (the center point) (see variable sceneView and centerPoint).
Next, we need to access the orientation and location of the camera by accessing the transform matrix (the orientation of the camera is backwards, which is why we must make the x,y,z points negative). From this, we can get the position of the camera which is where we want to place the ball. Once we have the camera position, we need to create the ball.
The ball is a SCNSphere with a radius of 0.15 (if you want a bigger ball, change this!). To make the ball look like a basketball, we need to add the material (basketballSkin.png) to the ball. The UIImage function takes in the name of the material file. Since the file is in the "Assets.xcassets" folder, we do not need to include the path. material.diffuse property deals with how light is rendered on the material and the contents property deals with the actual parents of the material. Now, to attach the material to the ball, we need to add our material variable to the ball (ball.materials = [material]).
In order to set the position of an object, that object must be a node. This is why we create the ballNode variable with the geometry of the ball. As mentioned before, we want to set the ball's position to the position of the camera, which is just the center of the screen (ballNode.position = cameraPosition)
Now, we need to add the ball to the scene by adding a childNode to the sceneView.
At this point, if you build and run the app, you would be able to move the camera around and tap the screen and a ball would be placed in the center of the screen. This is not the functionality we want, however, which is why we must add a Physics Body to the ball so that instead of being placed, the ball will be shot.
To create a physics body, we first must create a physicsShape. The first argument of SCNPhysicsShape is the node, which is the ballNode. For the second argument, options, we pass in "nil" because we do not need to use the additional options. We then create the physicsBody for the ball. The first argument of SCNPhysicsBody is the type which we want to be ".dynamic" so that we can apply forces to the physicsBody (so that if it is hit, it will move). We want to do this so that when the ball hits the backboard or the rim, it will bounce off. We now need to connect the ballNode to the physics body (ballNode.physicsBody = physicsBody). Next, to apply a force to the physicsBody, we use the applyForce function and pass in the SCNVector3 with the cameraPosition with asImpulse equal to true.
If you were to build and run now, the ball would fall to the ground when you tapped the screen. This is why we must add a forceVector. We give this forceVector a value of 6 arbitrarily (you can change this if you want a different force). We multiply each x,y,z position in the applyForce function to apply this force.
At this point, if you build and run, the ball will shoot each time you tap the screen, but will go straight through the backboard. This is because we have not yet added a PhysicsBody to the backboard! In addBackboard(), add a physics body to the backboard. We pass in the backboardNode as the node in SCNPhysicsShape and for now put "options: nil" (this will be changed). For the SCNPhysicsBody, we want the type to be ".static" since the backboard never moves when it is hit (this makes it unmovable) and pass in the newly created physicsShape as the shape. Then we set the backboard physicsBody to the physicsBody we just created.
If you ran the game now, the ball would bounce off the backboard, but would roll off the hoop. The reason for this is because Xcode interprets the backboard and the rim as the same object. It would be impossible to score like this!
To fix this, go back to your SCNPhysicsShape for the backboard and add in "[SCNPhysicsShape.Option.type: SCNPhysicsShape.ShapeType.concavePolyhedron]" in the options parameter. This will define the shapes in the hoop object as separate, and will recognize the rim as a circle.
Checkpoint 2: Ball and Hoop!
At this point, when you build and run, you should have a functional ball and hoop. But we are not done yet! We are going to make the backboard move a little bit left and right to make the game more challenging.
Under the addBackboard() function, create a new function horizontalAction() that takes in a node. We then create the left movement, leftAction. This will be a SCNAction that uses the move function to move "by:" a certain distance in a "duration:" amount of time. The code below will move the hoop to the left and to the right by 1m in 3 seconds. Feel free to adjust this depending on how fast or slow/ how far you would like the hoop to move.
We must place the left and right actions in a sequence so that the hoop moves first to the left and then to the right instead of trying to simultaneously move in both directions. The SCNAction.sequence function takes in an array of SCNActions, so we pass in the leftAction and then the rightAction so that it will execute left first and then right. We then run the action by calling runAction on the node. We use the repeat function to repeat the sequence 4 times.
Then, call the function horizontalAction() at the end of the addBackboard() function. If you build and run, the hoop will move left, right, left, right, left, right, left, right, and then stop!
Using what we just learned, just for fun, we're going to make a function to move the hoop in a circle. Remember to call your function roundAction(node: backboardNode) in addBackboard() and comment out the horizontalAction call!
If you build and run, the hoop will move in a clockwise motion! We will now create the user interface. Go back to the main storyboard (Main.storyboard) and search in the bottom right search bar for "Button". Drag a button onto the scene and drag to make it have the dimensions W: 70, H: 30 pixels. Go to the "Add Constraints" bar that we used earlier and check the boxes "Width" and "Height" and add the two constraints.
Click the button you just created and copy and paste so that there are two more buttons. Hold down shift and select the three buttons. We are now going to embed the buttons in a stack. Select the button "Embed in Stack" near "Add Constraints". The buttons should now be aligned. While the "Stack View" you just created is highlighted, click the button, "Alignment Constraints" in between the "Add Constraints" and "Embed in Stack" buttons and check the box "Horizontally in Container" and click "Add 1 Constraint". Now, to move these buttons down a bit, add another constraint to the "Stack View" by clicking "Add Constraints" and changing the bottom constraint to 25 and add the constraint. Change the Spacing to 12 by clicking the "Show the Attributes Inspector" button on the right side.
We now want to change the text in the buttons. Select the first button and change the Text from Button to "<->" and increase the font size from 15 to 17. Change the color from Blue to "Snow". Do this for all of the buttons with the middle button being "Stop" and the right button being "<~>"
Your Main.storyboard should look like this
We now need to connect the buttons to the actions we created earlier. Select the "Assistant Editor" icon in the top right
Scroll down to the bottom of the ViewController script. Then right-click the right button and drag it onto the script (while holding down the right-click). Change the connection type to Action and name the right button "startRoundAction". Do the same for the left and middle buttons, naming them "startHorizontalAction" and "stopAllActions", respectively.
We must now create a global variable in order to call the roundAction and horizontalAction functions (both take in a SCNNode, but we do not have access to the backboardNode currently). At the top of the script, after this line: "@IBOutlet var sceneView: ARSCNView!", add in the line: "var currentNode: SCNNode!"
In addBackboard(), remove the roundAction function call and instead write currentNode = backboardNode at the end of the function. Now we can call our functions in the button action functions, calling currentNode as our SCNNode.
Final Checkpoint: Buttons, Ball, and Moving Hoop!
Congratulations, you're all done! I hope you enjoyed creating your own AR Basketball game. Please remember to fill out the survey (posted on Slack) for feedback about your experience building AR with Apple's ARKit and SceneKit.