How to make 3D IOS Games

how to develop 3d ios games qnd 3d games ios development and how to make 3d games for ios and how to make a 3d game on ipad
Dr.KiranArora Profile Pic
Published Date:27-10-2017
Your Website URL(Optional)
SceneKit So, this is it Finally, we move from the 2D world to 3D. With SceneKit, we can make 3D games quite easily, especially since the syntax for SceneKit is quite similar to SpriteKit. When we say 3D games, we don't mean that you get to put on your 3D glasses to make the game. In 2D games, we mostly work in the x and y coordinates. In 3D games, we deal with all three axes x, y, and z. Additionally, in 3D games, we have different types of lights that we can use. Also, SceneKit has an inbuilt physics engine that will take care of forces such as gravity and will also aid collision detection. We can also use SpriteKit in SceneKit for GUI and buttons so that we can add scores and interactivity to the game. So, there is a lot to cover in this chapter. Let's get started. The topics covered in this chapter are as follows: • Creating a scene with SCNScene • Adding objects to a scene • Importing scenes from external 3D applications • Adding physics to the scene • Adding an enemy • Checking collision detection • Adding a SpriteKit overlay • Adding touch interactivity • Finishing the gameloop • Adding wall and floor parallax • Adding particles 239 SceneKit Creating a scene with SCNScene First, we create a new SceneKit project. It is very similar to creating other projects. Only this time, make sure you select SceneKit from the Game Technology drop-down list. Don't forget to select Swift for the language e fi ld. Choose iPad as the device and click on Next to create the project in the selected directory, as shown in the following screenshot: Once the project is created, open it. Click on theGameViewController class, and delete all the contents in theviewDidLoad function, delete thehandleTap function, as we will be creating a separate class, and add touch behavior. Create a new class calledGameSCNScene and import the following headers. Inherit from theSCNScene class and add aninit function that takes in a parameter called view of typeSCNView: import Foundation import UIKit import SceneKit class GameSCNScene: SCNScene let scnView: SCNView let _size:CGSize var scene: SCNScene 240 Chapter 8 required init(coder aDecoder: NSCoder) fatalError("init(coder:) has not been implemented") init(currentview view: SCNView) super.init() Also, create two new constantsscnView and_size of typeSCNView andCGSize, respectively. Also, add a variable calledscene of typeSCNScene. Since we are making a SceneKit game, we have to get the current view, which is the typeSCNView, similar to how we got the view in SpriteKit where we typecasted the current view in SpriteKit toSKView. We create a_size constant to get the current size of the view. We then create a new variablescene of typeSCNScene.SCNScene is the class used to make scenes in SceneKit, similar to how we would useSKScene to create scenes in SpriteKit. Swift would automatically ask to create therequired init function, so we might as well include it in the class. Now, move to theGameViewController class and create a global variable called gameSCNScene of typeGameSCNScene and assign it in theviewDidLoad function, as follows: class GameViewController: UIViewController var gameSCNScene:GameSCNScene override func viewDidLoad() super.viewDidLoad() let scnView = view as SCNView gameSCNScene = GameSCNScene(currentview: scnView) // UIViewController Class Great Now we can add objects in theGameSCNScene class. It is better to move all the code to a single class so that we can keep theGameSceneController class clean. 241 SceneKit In theinit function ofGameSCNScene, add the following after thesuper.init function: scnView = view _size = scnView.bounds.size // retrieve the SCNView scene = SCNScene() scnView.scene = scene scnView.allowsCameraControl = true scnView.showsStatistics = true scnView.backgroundColor = UIColor.yellowColor() Here, we first assign the current view to thescnView constant. Next, we set the _size constant to the dimensions of the current view. Next we initialize the scene variable. Then, assign the scene to the scene ofscnView. Next, enableallowCameraControls andshowStatistics. This will enable us to control the camera and move it around to have a better look at the scene. Also, with statistics enabled, we will see the performance of the game to make sure that the FPS is maintained. ThebackgroundColor property ofscnView enables us to set the color of the view. I have set it to yellow so that objects are easily visible in the scene, as shown in the following screenshot. With all this set we can run the scene. 242 Chapter 8 Well, it is not all that awesome yet. One thing to notice is that we have still not added a camera or a light, but we still see the yellow scene. This is because while we have not added anything to the scene yet, SceneKit automatically provides a default light and camera for the scene created. Adding objects to the scene Let us next add geometry to the scene. We can create some basic geometry such as spheres, boxes, cones, tori, and so on in SceneKit with ease. Let us create a sphere first and add it to the scene. Adding a sphere to the scene Create a function calledaddGeometryNode in the class and add the following code in it: func addGeometryNode() let sphereGeometry = SCNSphere(radius: 1.0) sphereGeometry.firstMaterial?.diffuse.contents = UIColor. orangeColor() let sphereNode = SCNNode(geometry: sphereGeometry) sphereNode.position = SCNVector3Make(0.0, 0.0, 0.0) scene.rootNode.addChildNode(sphereNode) For creating geometry, we use theSCNSphere class to create a sphere shape. We can also callSCNBox,SCNCone,SCNTorus, and so on to create box, cone, or torus shapes respectively. While creating the sphere, we have to provide the radius as a parameter, which will determine the size of the sphere. Although to place the shape, we have to attach it to a node so that we can place and add it to the scene. So, create a new constant calledsphereNode of typeSCNNode and pass in the sphere geometry as a parameter. For positioning the node, we have to use the SCNvector3Make property to place our object in 3D space by providing the values forx,y, andz. Finally, to add the node to the scene, we have to callscene.rootNode to add the sphereNode to scene, unlike SpriteKit where we would simply useaddChild to add objects to the scene. 243 SceneKit With the sphere added, let us run the scene. Don't forget to add self.addGeometryNode() in theinit function. We did add a sphere, so why are we getting a circle (shown in the following screenshot)? Well, the basic light source used by SceneKit just enables to us to see objects in the scene. If we want to see the actual sphere, we have to improve the light source of the scene. Adding light sources Let us create a new function calledaddLightSourceNode as follows so that we can add custom lights to our scene: func addLightSourceNode() let lightNode = SCNNode() lightNode.light = SCNLight() lightNode.light.type = SCNLightTypeOmni lightNode.position = SCNVector3(x: 10, y: 10, z: 10) scene.rootNode.addChildNode(lightNode) let ambientLightNode = SCNNode() ambientLightNode.light = SCNLight() ambientLightNode.light.type = SCNLightTypeAmbient ambientLightNode.light.color = UIColor.darkGrayColor() scene.rootNode.addChildNode(ambientLightNode) We can add some light sources to see some depth in our sphere object. Here we add two types of light source. The r fi st is an omni light. Omni lights start at a point and then the light is scattered equally in all directions. We also add an ambient light source. An ambient light is the light that is ree fl cted by other objects, such as moonlight. 244 Chapter 8 There are two more types of light sources: directional and spotlight. Spotlight is easy to understand, and we usually use it if a certain object needs to be brought to attention like a singer on a stage. Directional lights are used if you want light to go in a single direction, such as sunlight. The Sun is so far from the Earth that the light rays are almost parallel to each other when we see them. For creating a light source, we create a node calledlightNode of typeSCNNode. We then assignSCNLight to the light property oflightNode. We assign the omni light type to be the type of the light. We assign position of the light source to be at 10 in all three x, y, and z coordinates. Then, we add it to the rootnode of the scene. Next we add an ambient light to the scene. The first two steps of the process are the same as for creating any light source: 1. For the type of light we have to assignSCNLightTypeAmbient to assign an ambient type light source. Since we don't want the light source to be very strong, as it is reflected, we assign a darkGrayColor to the color. 2. Finally, we add the light source to the scene. There is no need to add the ambient light source to the scene but it will make the scene have softer shadows. You can remove the ambient light source to see the difference. Call theaddLightSourceNode function in theinit function. Now, build and run the scene to see an actual sphere with proper lighting, as shown in the following screenshot: 245 SceneKit You can place a finger on the screen and move it to rotate the cameras as we have enabled camera control. You can use two fingers to pan the camera and you can double tap to reset the camera to its original position and direction. Adding a camera to the scene Next let us add a camera to the scene, as the default camera is very close. Create a new function calledaddCameraNode to the class and add the following code in it: func addCameraNode() let cameraNode = SCNNode() = SCNCamera() cameraNode.position = SCNVector3(x: 0, y: 0, z: 15) scene.rootNode.addChildNode(cameraNode) Here, again we create an empty node calledcameraNode. We assignSCNCamera to the camera property ofcameraNode. Next we position the camera such that we keep thex andy values at zero and move the camera back in thez direction by 15 units. Then we add the camera to the rootnode of the scene. Call theaddCameraNode at the bottom of theinit function. In this scene, the origin is at the center of the scene, unlike SpriteKit where the origin of a scene is always at bottom right of the scene. Here the positive x and y are to the right and up from the center. The positive z direction is toward you. 246 Chapter 8 We didn't move the sphere back or reduce its size here. This is purely because we brought the camera backward in the scene. Let us next create a floor so that we can have a better understanding of the depth in the scene. Also, in this way, we will learn how to create floors in the scene. Adding a floor In the class, add a new function calledaddFloorNode and add the following code: func addFloorNode() var floorNode = SCNNode() floorNode.geometry = SCNFloor() floorNode.position.y = -1.0 scene.rootNode.addChildNode(floorNode) For creating a floor, we create a variable called floorNode of typeSCNNode. We then assignSCNFloor to the geometry property offloorNode. For the position, we assign they value to-1 as we want the sphere to appear above the floor. At the end, as usual, we assign thefloorNode to the root node of the scene. In the following screenshot, I have rotated the camera to show the scene in full action. Here we can see the o fl or is gray in color and the sphere is casting its ree fl ction on the o fl or, and we can also see the bright omni light at the top left of the sphere. 247 SceneKit Importing scenes from external 3D applications Although we can add objects, cameras, and lights through code, it will become very tedious and confusing when we have a lot of objects added to the scene. In SceneKit, this problem can be easily overcome by importing scenes prebuilt in other 3D applications. All 3D applications such as 3D StudioMax, Maya, Cheetah 3D, and Blender have the ability to export scenes in Collada (.dae) and Alembic (.abc) format. We can import these scenes with lighting, camera, and textured objects into SceneKit directly, without the need for setting up the scene. In this section, we will import a Collada file into the scene. In the resources folder for this chapter, you will find the monsterScene.DAE file. Drag this file into the current project. Along with the DAE file, also add the monster.png file to the project, otherwise you will see only the untextured monster mesh in the scene. 248 Chapter 8 Click on themonsterScene.DAE file. If the textured monster is not automatically loaded, drag themonster.png file from the project into the monster mesh in the preview window. Release the mouse button once you see a (+) sign while over the monster mesh. Now you will be able to see the monster properly textured. The panel on the left shows the entities in the scene. Below the entities, the scene graph is shown and the view on the right is the preview pane. Entities show all the objects in the scene and the scene graph shows the relation between these entities. If you have certain objects that are children to other objects, the scene graph will show them as a tree. For example, if you open the triangle next to CATRigHub001, you will see all the child objects under it. You can use the scene graph to move and rotate objects in the scene to fine-tune your scene. You can also add nodes, which can be accessed by code. You can see that we already have a camera and a spotlight in the scene. You can select each object and move it around using the arrow at the pivot point of the object. You can also rotate the scene to get a better view by clicking and dragging the left mouse button on the preview scene. For zooming, scroll your mouse wheel up and down. To pan, hold the Alt button on the keyboard and left-click and drag on the preview pane. One thing to note is that rotating, zooming, and panning in the preview pane won't actually move your camera. The camera is still at the same position and angle. To view from the camera, again select the Camera001 option from the drop-down list in the preview pane and the view will reset to the camera view. At the bottom of the preview window, we can either choose to see the view through the camera or spotlight, or click-and-drag to rotate the free camera. If you have more than one camera in your scene, then you will have Camera002, Camera003, and so on in the drop-down list. Below the view selection dropdown in the preview panel you also have a play button. If you click on the play button, you can look at the default animation of the monster getting played in the preview window. The preview panel is just that; it is just to aid you in having a better understanding of the objects in the scene. In no way is it a replacement for a regular 3D package such as 3DSMax, Maya, or Blender. You can create cameras, lights, and empty nodes in the scene graph, but you can't add geometry such as boxes and spheres. You can add an empty node and position it in the scene graph, and then add geometry in code and attach it to the node. 249 SceneKit Now that we have an understanding of the scene graph, let us see how we can run this scene in SceneKit. In theinit function, delete the line where we initialized the scene and add the following line instead. Also delete the objects, light, and camera we added earlier. init(currentview view:SCNView) super.init() scnView = view _size = scnView.bounds.size //retrieve the SCNView //scene = SCNScene() scene = SCNScene(named: "monsterScene.DAE") scnView.scene = scene scnView.allowsCameraControl = true scnView.showsStatistics = true scnView.backgroundColor = UIColor.yellowColor() // self.addGeometryNode() // self.addLightSourceNode() // self.addCameraNode() // self.addFloorNode() // Build and run the game to see the following screenshot: 250 Chapter 8 You will see the monster running and the yellow background that we initially assigned to the scene. While exporting the scene, if you export the animations as well, once the scene loads in SceneKit the animation starts playing automatically. Also, you will notice that we have deleted the camera and light in the scene. So, how come the default camera and the light aren't loaded in the scene? What is happening here is that while I exported the l fi e, I inserted a camera in the scene and also added a spotlight. So, when we imported the l fi e into the scene, SceneKit automatically understood that there is a camera already present, so it will use the camera as its default camera. Similarly, a spotlight is already added in the scene, which is taken as the default light source, and lighting is calculated accordingly. Adding objects and physics to the scene Let us now see how we can access each of the objects in the scene graph and add gravity to the monster. Later in this chapter, we will see how we can add a touch interface by which we will be able to make the hero character jump by applying an upward force. Accessing the hero object and adding a physics body So, create a new function calledaddColladaObjects and call anaddHero function in it. Create a global variable calledheroNode of typeSCNNode. We will use this node to access the hero object in the scene. In theaddHero function, add the following code: init(currentview view:SCNView) super.init() scnView = view _size = scnView.bounds.size //retrieve the SCNView //scene = SCNScene() scene = SCNScene(named: "monster.scnassets/monsterScene.DAE") scnView.scene = scene scnView.allowsCameraControl = true scnView.showsStatistics = true scnView.backgroundColor = UIColor.yellowColor() self.addColladaObjects() // self.addGeometryNode() 251 SceneKit // self.addLightSourceNode() // self.addCameraNode() // self.addFloorNode() func addHero() heroNode = SCNNode() var monsterNode = scene.rootNode.childNodeWithName( "CATRigHub001", recursively: false) heroNode.addChildNode(monsterNode) heroNode.position = SCNVector3Make(0, 0, 0) let collisionBox = SCNBox(width: 10.0, height: 10.0, length: 10.0, chamferRadius: 0) heroNode.physicsBody?.physicsShape = SCNPhysicsShape(geometry: collisionBox, options: nil) heroNode.physicsBody = SCNPhysicsBody.dynamicBody() heroNode.physicsBody?.mass = 20 heroNode.physicsBody?.angularVelocityFactor = SCNVector3Zero = "hero" scene.rootNode.addChildNode(heroNode) First, we call theaddColladaObjects function in theinit function, as highlighted. Then we create theaddHero function. In it we initiate theheroNode. Then, to actually move the monster, we need access to theCatRibHub001 node to move the monster. We gain access to it through theChildWithName property ofscene.rootNode. For each object that we wish to gain access to through code, we will have to use the ChildWithName property of therootNode of the scene and pass in the name of the object. If recursively is set totrue, to get said object, SceneKit will go through all the child nodes to get access to the specific node. Since the node that we are looking for is right on top, we saidfalse to save processing time. We create a temporary variable calledmonsterNode. In the next step, we add the monsterNode variable toheroNode. We then set the position of the hero node to the origin. 252 Chapter 8 ForheroNode to interact with other physics bodies in the scene, we have to assign a shape to the physics body ofheroNode. We could use the mesh of the monster, but the shape might not be calculated properly and a box is a much simpler shape than the mesh of the monster. For creating a box collider, we create a new box geometry roughly the width, height, and depth of the monster. Then, using thephysicsBody.physicsShape property of theheroNode, we assign the shape of thecollisionBox we created for it. Since we want the body to be affected by gravity, we assign the physics body type to be dynamic. Later we will see other body types. Since we want the body to be highly responsive to gravity, we assign a value of20 to themass of the body. In the next step, we set theangularVelocityFactor to0 in all three directions, as we want the body to move straight up and down when a vertical force is applied. If we don't do this, the body will flip-flop around. We also assign the namehero to the monster to check if the collided object is the hero or not. This will come in handy when we check for collision with other objects. Finally, we addheroNode to the scene. Add theaddColladaObjects to theinit function and comment or delete the self.addGeometryNode,self.addLightSourceNode,self.addCameraNode, and self.addFloorNode functions if you haven't already. Then, run the game to see the monster slowly falling through. We will create a small patch of ground right underneath the monster so that it doesn't fall down. Adding the ground Create a new function calledaddGround and add the following: func addGround() let groundBox = SCNBox(width: 10, height: 2, length: 10, chamferRadius: 0) let groundNode = SCNNode(geometry: groundBox) groundNode.position = SCNVector3Make(0, -1.01, 0) groundNode.physicsBody = SCNPhysicsBody.staticBody() groundNode.physicsBody?.restitution = 0.0 scene.rootNode.addChildNode(groundNode) 253 SceneKit We create a new constant calledgroundBox of typeSCNBox, with a width and length of10, and height of2. Chamfer is the rounding of the edges of the box. Since we didn't want any rounding of the corners, it is set to0. Next we create aSCNNode calledgroundNode and assigngroundBox to it. We place it slightly below the origin. Since the height of the box is2, we place it at–1.01 so that heroNode will be(0, 0, 0) when the monster rests on the ground. Next we assign the physics body of type static body. Also, since we don't want the hero to bounce off the ground when he falls on it, we set the restitution to0. Finally, we then add the ground to the scene's rootnode. The reason we made this body static instead of dynamic is because a dynamic body gets affected by gravity and other forces but a static one doesn't. So, in this scene, even though gravity is acting downward, the hero will fall butgroundBox won't as it is a static body. You will see that the physics syntax is very similar to SpriteKit with static bodies and dynamic bodies, gravity, and so on. And once again, similar to SpriteKit, the physics simulation is automatically turned on when we run the scene. Add theaddGround function in theaddColladaObjects functions and run the game to see the monster getting affected by gravity and stopping after coming in touch with the ground. 254 Chapter 8 Adding an enemy node To check collision in SceneKit, we can check for collision between the hero and the ground. But let us make it a little more interesting and also learn a new kind of body type: the kinematic body. For this, we will create a new box calledenemy and make it move and collide with the hero. Create a new globalSCNNode calledenemyNode as follows: let scnView: SCNView let _size:CGSize var scene: SCNScene var heroNode:SCNNode var enemyNode:SCNNode Also, create a new function calledaddEnemy to the class and add the following in it: func addEnemy() let geo = SCNBox(width: 4.0, height: 4.0, length: 4.0, chamferRadius: 0.0) geo.firstMaterial?.diffuse.contents = UIColor.yellowColor() enemyNode = SCNNode(geometry: geo) enemyNode.position = SCNVector3Make(0, 20.0 , 60.0) enemyNode.physicsBody = SCNPhysicsBody.kinematicBody() scene.rootNode.addChildNode(enemyNode) = "enemy" Nothing too fancy here Just as when adding thegroundNode, we have created a cube with all its sides four units long. We have also added a yellow color to its material. We then initializeenemyNode in the function. We position the node along the x, y, and z axes. Assign the body type as kinematic instead of static or dynamic. Then we add the body to the scene and n fi ally name the enemyNode asenemy, which we will be needing while checking for collision. Before we forget, call theaddEnemy function in theaddColladaObjects function after where we called theaddHero function. 255 SceneKit The difference between the kinematic body and other body types is that, like static, external forces cannot act on the body, but we can apply a force to a kinematic body to move it. In the case of a static body, we saw that it is not affected by gravity and even if we apply a force to it, the body just won't move. Here we won't be applying any force to move the enemy block but will simply move the object like we moved the enemy in the SpriteKit game. So, it is like making the same game, but in 3D instead of 2D, so that you can see that although we have a third dimension, the same principles of game development can be applied to both. For moving the enemy, we need anupdate function for the enemy. So, let us add it to the scene by creating anupdateEnemy function and adding the following to it: func updateEnemy() enemyNode.position.z += -0.9 if((enemyNode.position.z - 5.0) -40) var factor = arc4random_uniform(2) + 1 if( factor == 1 ) enemyNode.position = SCNVector3Make(0, 2.0 , 60.0) else enemyNode.position = SCNVector3Make(0, 15.0 , 60.0) In theupdate function, similar to how we moved the enemy in the SpriteKit game, we increment theZ position of the enemy node by 0.9. The difference being that we are moving the z direction. Once the enemy has gone beyond–40 in the z direction, we reset the position of the enemy. To create an additional challenge to the player, when the enemy resets, a random number is chosen between1 and2. If it is1, then the enemy is placed closer to the ground, otherwise it is placed at 15 units from the ground. Later, we will add a jump mechanic to the hero. So, when the enemy is closer to the ground, the hero has to jump over the enemy box, but when the enemy is spawned at a height, the hero shouldn't jump. If he jumps and hits the enemy box, then it is game over. Later we will also add a scoring mechanism to keep score. 256 Chapter 8 For updating the enemy, we actually need an update function to add theenemyUpdate function to so that the enemy moves and his position resets. So, create a function called update in the class and call theupdateEnemy function in it as follows: func update() updateEnemy() Updating objects in the scene In SceneKit, there is an update function that gets called after the scene is rendered. We will use this function to call theupdate function of our game. In the GameViewController class, add the following function: func renderer(aRenderer: SCNSceneRenderer, updateAtTime time: NSTimeInterval) gameSCNScene.update() To call therendererUpdateAtTime function, theGameViewController class needs to inherit fromSCNSceneRendererDelegate. So, where the class is created, add the following: class GameViewController: UIViewController, SCNSceneRendererDelegate Next, in theviewDidLoad function, set the current delegate to self as follows: let scnView = view as SCNView scnView.delegate = self Delegates are a part of code design patterns. Using delegates, a class can let a different class gain access and perform some of its responsibilities. HereSceneRenderer is delegating toscnView by assigning the delegate as self. TherendererUpdateAtTime function is a system function that gets called after all the objects are rendered in the scene. So, once the scene is rendered, the objects in the scene can be updated, otherwise it might result in artifacting. Now, if we build and run the game, we seeenemyBox getting updated. 257 SceneKit But there is a problem, when the box hits the hero, the hero gets knocked off his pedestal and goes yi fl ng. This is because, firstly, the hero is a dynamic body, so external forces will affect him. Secondly, though we are moving the box manually without applying any force, e we are still moving the box and there is some inertial force calculated by SceneKit, so once the box hits the hero, the energy is transferred to the hero, and it acts like an external force applied on the hero so the hero starts moving. Since we constrained the rotation of the hero usingheroNode.physicsBody?. angularVelocityFactor = SCNVector3Zero, when the box hits him, he is not rotating. If we comment or delete the line, the hero will spin because of the box hitting him. We will fix this issue when we check for collision. When the collision occurs, we will reset the position of the hero and box to their initial positions. So, let us next look at how to check for collision. Checking for contact between objects The physics engine of SceneKit has inbuilt functions that check for contact between objects when physics is enabled. A contact is triggered when two objects are just about to touch each other. 258