├── .gitignore ├── Base.lproj └── LaunchScreen.storyboard ├── README.md ├── art.scnassets ├── fire.scnp ├── grass.png ├── ringTexture.png ├── ship.scn ├── skybox.png ├── smoke.png ├── smoke.scnp ├── spark.png └── texture.png ├── assets.xcassets └── AppIcon.appiconset │ ├── AppIcon-29@2x.png │ ├── AppIcon-29@3x.png │ ├── AppIcon-40@2x.png │ ├── AppIcon-40@3x.png │ ├── AppIcon-60@2x.png │ ├── AppIcon-60@3x.png │ └── Contents.json ├── partI ├── GameLevel.swift ├── GameViewController.swift ├── Info.plist └── Player.swift ├── partII ├── GameLevel.swift ├── GameViewController.swift ├── Info.plist └── Player.swift ├── partIII ├── GameLevel.swift ├── GameSettings.swift ├── GameViewController.swift ├── HUD.swift ├── Info.plist ├── Player.swift └── Ring.swift ├── partIV ├── GameLevel.swift ├── GameObject.swift ├── GameSettings.swift ├── GameViewController.swift ├── HUD.swift ├── Handicap.swift ├── Info.plist ├── Player.swift └── Ring.swift ├── partV ├── GameLevel.swift ├── GameObject.swift ├── GameSettings.swift ├── GameViewController.swift ├── HUD.swift ├── Handicap.swift ├── Info.plist ├── Player.swift └── Ring.swift ├── partVI ├── Bullet.swift ├── Enemy.swift ├── GameLevel.swift ├── GameObject.swift ├── GameSettings.swift ├── GameSound.swift ├── GameViewController.swift ├── HUD.swift ├── Handicap.swift ├── Info.plist ├── Plane.swift ├── Player.swift └── Ring.swift ├── screenshots ├── screenshot0.png ├── screenshot1.png ├── screenshot2.png ├── screenshot3.png ├── screenshot4.png ├── screenshot5.png └── screenshot6.png ├── shared ├── AppDelegate.swift ├── RB+CGPoint.swift ├── RB+SCNVector.swift ├── RB+SKAction.swift ├── RB+UIColor.swift ├── RBLog.swift ├── RBPerlinNoiseGenerator.swift ├── RBRandom.swift ├── RBTerrain.swift └── RBUtility.swift ├── sounds ├── bonus.wav ├── explosion.wav └── fire.wav └── tutorial.xcodeproj ├── project.pbxproj └── project.xcworkspace └── contents.xcworkspacedata /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/swift,macos 3 | 4 | ### macOS ### 5 | *.DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | 9 | # Icon must end with two \r 10 | Icon 11 | 12 | # Thumbnails 13 | ._* 14 | 15 | # Files that might appear in the root of a volume 16 | .DocumentRevisions-V100 17 | .fseventsd 18 | .Spotlight-V100 19 | .TemporaryItems 20 | .Trashes 21 | .VolumeIcon.icns 22 | .com.apple.timemachine.donotpresent 23 | 24 | # Directories potentially created on remote AFP share 25 | .AppleDB 26 | .AppleDesktop 27 | Network Trash Folder 28 | Temporary Items 29 | .apdisk 30 | 31 | ### Swift ### 32 | # Xcode 33 | # 34 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 35 | 36 | ## Build generated 37 | build/ 38 | DerivedData/ 39 | 40 | ## Various settings 41 | *.pbxuser 42 | !default.pbxuser 43 | *.mode1v3 44 | !default.mode1v3 45 | *.mode2v3 46 | !default.mode2v3 47 | *.perspectivev3 48 | !default.perspectivev3 49 | xcuserdata/ 50 | 51 | ## Other 52 | *.moved-aside 53 | *.xccheckout 54 | *.xcscmblueprint 55 | 56 | ## Obj-C/Swift specific 57 | *.hmap 58 | *.ipa 59 | *.dSYM.zip 60 | *.dSYM 61 | 62 | ## Playgrounds 63 | timeline.xctimeline 64 | playground.xcworkspace 65 | 66 | # Swift Package Manager 67 | # 68 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 69 | # Packages/ 70 | # Package.pins 71 | .build/ 72 | 73 | # CocoaPods - Refactored to standalone file 74 | 75 | # Carthage - Refactored to standalone file 76 | 77 | # fastlane 78 | # 79 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 80 | # screenshots whenever they are needed. 81 | # For more information about the recommended setup visit: 82 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 83 | 84 | fastlane/report.xml 85 | fastlane/Preview.html 86 | fastlane/screenshots 87 | fastlane/test_output 88 | 89 | # End of https://www.gitignore.io/api/swift,macos 90 | -------------------------------------------------------------------------------- /Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Update February 2024**:: I wrote the first part of this tutorial almost eight years ago. 2 | SceneKit is available on Vision OS but is no longer used for immersive 3D rendering. 3 | Instead, you have to use RealityKit. But since this information is still very useful, 4 | I have created a little [E-Book](https://visionos.substack.com) that contains the entire SceneKit Tutorial. 5 | 6 | 7 | ### The Tutorial 8 | 9 | The code is part of my SceneKit Tutorial Serie: From Zero to Hero. 10 | 11 | ![Screenshot Part 6](/screenshots/screenshot0.png) 12 | 13 | 14 | ### Tutorial 15 | - Chapter 1 - Building a Terrain 16 | - Chapter 2 - Create a real player game object 17 | - Chapter 3 - Add life to your terrain 18 | - Chapter 4 - Implement a Game Loop 19 | - Chapter 5 - Fly smoothly with CoreMotion 20 | - Chapter 6 - Finish the game (The missing parts) 21 | 22 | *Remarks*: 23 | - *Code is now updated to be Swift 4 compliant* 24 | - *It's imortant to know that the code is not intented to learn Swift, but to understand learning game development concepts* 25 | - *Therefore some things (like unwrapping optionals) are just here to make things simpler* 26 | 27 | 28 | ### The Game 29 | This tutorial is a companion work I do parallel on the development of my next upcoming 3D Game which uses just SceneKit and it's written entirely with Swift 4. 30 | 31 | 32 | ### For who is this Tutorial? 33 | It's not for the absolute beginner or novice, because I assume things like Swift notation, trigonometric know-how etc. 34 | On the other hand you don't have to learn all at the begin. Like the terrain class **RBTerrain**. 35 | You can use it just like it is and read the code later again when you have more know-how. 36 | The most important is that you understand the principles. 37 | 38 | 39 | ### Introduction 40 | It's important for me to start directly with a good and solid base which can be used for real games later. 41 | So please don't create a project using the Xcode Project wizard but start directly with the Source on GitHub. 42 | Also I don't want to build a step by step tutorial which shows each line, but always cover a chapter of the Game and introduce 43 | a new component and show the how and why :) 44 | 45 | 46 | ### Screenshots 47 | 48 | Tutorial Part 5 49 | ![Screenshot Part 5](/screenshots/screenshot5.png) 50 | 51 | Tutorial Part 4 52 | ![Screenshot Part 4](/screenshots/screenshot4.png) 53 | 54 | Tutorial Part 3 55 | ![Screenshot Part 3](/screenshots/screenshot3.png) 56 | 57 | Tutorial Part 2 58 | ![Screenshot Part 2](/screenshots/screenshot2.png) 59 | 60 | Tutorial Part 1 61 | ![Screenshot Part 1](/screenshots/screenshot1.png) 62 | 63 | Follow me on [Substack](https://visionos.substack.com) to get the next tutorial: 64 | [Vision OS: Create an Immersive App with RealityKit](https://visionos.substack.com) 65 | 66 | 67 | **Important**: 68 | *Feel free to use this code in every way you want, but please consider also 69 | to give something back to the community.* 70 | 71 | *I don't own the license rights for the assets used in this tutorials, 72 | so before you use them for something else then self-learning, please check by yourself the license behind 73 | or even better replace it with your own art. Thank you!* 74 | -------------------------------------------------------------------------------- /art.scnassets/fire.scnp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rogerboesch/SceneKitTutorial/7fbe31110fc6a4bba43dbdde79cf26844e63ed11/art.scnassets/fire.scnp -------------------------------------------------------------------------------- /art.scnassets/grass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rogerboesch/SceneKitTutorial/7fbe31110fc6a4bba43dbdde79cf26844e63ed11/art.scnassets/grass.png -------------------------------------------------------------------------------- /art.scnassets/ringTexture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rogerboesch/SceneKitTutorial/7fbe31110fc6a4bba43dbdde79cf26844e63ed11/art.scnassets/ringTexture.png -------------------------------------------------------------------------------- /art.scnassets/ship.scn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rogerboesch/SceneKitTutorial/7fbe31110fc6a4bba43dbdde79cf26844e63ed11/art.scnassets/ship.scn -------------------------------------------------------------------------------- /art.scnassets/skybox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rogerboesch/SceneKitTutorial/7fbe31110fc6a4bba43dbdde79cf26844e63ed11/art.scnassets/skybox.png -------------------------------------------------------------------------------- /art.scnassets/smoke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rogerboesch/SceneKitTutorial/7fbe31110fc6a4bba43dbdde79cf26844e63ed11/art.scnassets/smoke.png -------------------------------------------------------------------------------- /art.scnassets/smoke.scnp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rogerboesch/SceneKitTutorial/7fbe31110fc6a4bba43dbdde79cf26844e63ed11/art.scnassets/smoke.scnp -------------------------------------------------------------------------------- /art.scnassets/spark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rogerboesch/SceneKitTutorial/7fbe31110fc6a4bba43dbdde79cf26844e63ed11/art.scnassets/spark.png -------------------------------------------------------------------------------- /art.scnassets/texture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rogerboesch/SceneKitTutorial/7fbe31110fc6a4bba43dbdde79cf26844e63ed11/art.scnassets/texture.png -------------------------------------------------------------------------------- /assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rogerboesch/SceneKitTutorial/7fbe31110fc6a4bba43dbdde79cf26844e63ed11/assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png -------------------------------------------------------------------------------- /assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rogerboesch/SceneKitTutorial/7fbe31110fc6a4bba43dbdde79cf26844e63ed11/assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png -------------------------------------------------------------------------------- /assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rogerboesch/SceneKitTutorial/7fbe31110fc6a4bba43dbdde79cf26844e63ed11/assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png -------------------------------------------------------------------------------- /assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rogerboesch/SceneKitTutorial/7fbe31110fc6a4bba43dbdde79cf26844e63ed11/assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png -------------------------------------------------------------------------------- /assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rogerboesch/SceneKitTutorial/7fbe31110fc6a4bba43dbdde79cf26844e63ed11/assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png -------------------------------------------------------------------------------- /assets.xcassets/AppIcon.appiconset/AppIcon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rogerboesch/SceneKitTutorial/7fbe31110fc6a4bba43dbdde79cf26844e63ed11/assets.xcassets/AppIcon.appiconset/AppIcon-60@3x.png -------------------------------------------------------------------------------- /assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "size" : "29x29", 15 | "idiom" : "iphone", 16 | "filename" : "AppIcon-29@2x.png", 17 | "scale" : "2x" 18 | }, 19 | { 20 | "size" : "29x29", 21 | "idiom" : "iphone", 22 | "filename" : "AppIcon-29@3x.png", 23 | "scale" : "3x" 24 | }, 25 | { 26 | "size" : "40x40", 27 | "idiom" : "iphone", 28 | "filename" : "AppIcon-40@2x.png", 29 | "scale" : "2x" 30 | }, 31 | { 32 | "size" : "40x40", 33 | "idiom" : "iphone", 34 | "filename" : "AppIcon-40@3x.png", 35 | "scale" : "3x" 36 | }, 37 | { 38 | "size" : "60x60", 39 | "idiom" : "iphone", 40 | "filename" : "AppIcon-60@2x.png", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "size" : "60x60", 45 | "idiom" : "iphone", 46 | "filename" : "AppIcon-60@3x.png", 47 | "scale" : "3x" 48 | }, 49 | { 50 | "idiom" : "ios-marketing", 51 | "size" : "1024x1024", 52 | "scale" : "1x" 53 | } 54 | ], 55 | "info" : { 56 | "version" : 1, 57 | "author" : "xcode" 58 | } 59 | } -------------------------------------------------------------------------------- /partI/GameLevel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameLevel.swift 3 | // 4 | // Part 1 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 12/07/16. 8 | // Copyright © 2016 Roger Boesch. All rights reserved. 9 | // 10 | 11 | import UIKit 12 | import SceneKit 13 | 14 | // ----------------------------------------------------------------------------- 15 | 16 | class GameLevel: SCNScene, SCNSceneRendererDelegate, SCNPhysicsContactDelegate { 17 | private let levelWidth = 320 18 | private let levelLength = 320 19 | 20 | private var _terrain: RBTerrain? 21 | private var _player: Player? 22 | 23 | // ------------------------------------------------------------------------- 24 | // MARK: - Place objects 25 | 26 | private func addPlayer() { 27 | _player = Player() 28 | _player!.position = SCNVector3(160, 3, 0) 29 | self.rootNode.addChildNode(_player!) 30 | 31 | let moveAction = SCNAction.moveBy(x: 0, y: 0, z: 200, duration: 20) 32 | _player!.runAction(moveAction) 33 | } 34 | 35 | // ------------------------------------------------------------------------- 36 | 37 | private func addTerrain() { 38 | // Create terrain 39 | _terrain = RBTerrain(width: levelWidth, length: levelLength, scale: 128) 40 | 41 | let generator = RBPerlinNoiseGenerator(seed: nil) 42 | _terrain?.formula = {(x: Int32, y: Int32) in 43 | return generator.valueFor(x: x, y: y) 44 | } 45 | 46 | _terrain!.create(withColor: UIColor.green) 47 | _terrain!.position = SCNVector3Make(0, 0, 0) 48 | self.rootNode.addChildNode(_terrain!) 49 | } 50 | 51 | // ------------------------------------------------------------------------- 52 | // MARK: - Initialisation 53 | 54 | func create() { 55 | addTerrain() 56 | addPlayer() 57 | } 58 | 59 | // ------------------------------------------------------------------------- 60 | 61 | override init() { 62 | super.init() 63 | } 64 | 65 | // ------------------------------------------------------------------------- 66 | 67 | required init(coder: NSCoder) { 68 | fatalError("Not yet implemented") 69 | } 70 | 71 | // ------------------------------------------------------------------------- 72 | 73 | } 74 | -------------------------------------------------------------------------------- /partI/GameViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameViewController.swift 3 | // 4 | // Part 1 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 12/07/16. 8 | // Copyright © 2016 Roger Boesch. All rights reserved. 9 | // 10 | 11 | import UIKit 12 | import SceneKit 13 | 14 | class GameViewController: UIViewController { 15 | 16 | private var _sceneView: SCNView! 17 | private var _level: GameLevel! 18 | 19 | // ------------------------------------------------------------------------- 20 | // MARK: - ViewController life cycle 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | 25 | _level = GameLevel() 26 | _level.create() 27 | 28 | _sceneView = SCNView() 29 | _sceneView.scene = _level 30 | _sceneView.allowsCameraControl = false 31 | _sceneView.showsStatistics = true 32 | _sceneView.backgroundColor = UIColor.black 33 | _sceneView!.debugOptions = .showWireframe 34 | self.view = _sceneView 35 | } 36 | 37 | // ------------------------------------------------------------------------- 38 | 39 | override func didReceiveMemoryWarning() { 40 | super.didReceiveMemoryWarning() 41 | } 42 | 43 | // ------------------------------------------------------------------------- 44 | 45 | } 46 | -------------------------------------------------------------------------------- /partI/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | Game! 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UIStatusBarHidden 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationLandscapeLeft 34 | UIInterfaceOrientationLandscapeRight 35 | 36 | UILaunchStoryboardName 37 | LaunchScreen 38 | 39 | 40 | -------------------------------------------------------------------------------- /partI/Player.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Player.swift 3 | // 4 | // Part 1 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 12/07/16. 8 | // Copyright © 2016 Roger Boesch. All rights reserved. 9 | // 10 | 11 | import SceneKit 12 | 13 | // ----------------------------------------------------------------------------- 14 | 15 | class Player : SCNNode { 16 | private let lookAtForwardPosition = SCNVector3Make(0.0, 0.0, 1.0) 17 | private let cameraFowardPosition = SCNVector3(x: 0.8, y: 1, z: -0.5) 18 | 19 | private var _lookAtNode: SCNNode? 20 | private var _cameraNode: SCNNode? 21 | private var _playerNode: SCNNode? 22 | 23 | // ------------------------------------------------------------------------- 24 | // MARK: - Initialisation 25 | 26 | override init() { 27 | super.init() 28 | 29 | // Create player node 30 | let cubeGeometry = SCNBox(width: 0.5, height: 0.5, length: 0.5, chamferRadius: 0.0) 31 | _playerNode = SCNNode(geometry: cubeGeometry) 32 | _playerNode?.isHidden = true 33 | addChildNode(_playerNode!) 34 | 35 | let colorMaterial = SCNMaterial() 36 | cubeGeometry.materials = [colorMaterial] 37 | 38 | // Look at Node 39 | _lookAtNode = SCNNode() 40 | _lookAtNode!.position = lookAtForwardPosition 41 | addChildNode(_lookAtNode!) 42 | 43 | // Camera Node 44 | _cameraNode = SCNNode() 45 | _cameraNode!.camera = SCNCamera() 46 | _cameraNode!.position = cameraFowardPosition 47 | _cameraNode!.camera!.zNear = 0.1 48 | _cameraNode!.camera!.zFar = 200 49 | self.addChildNode(_cameraNode!) 50 | 51 | // Link them 52 | let constraint1 = SCNLookAtConstraint(target: _lookAtNode) 53 | constraint1.isGimbalLockEnabled = true 54 | _cameraNode!.constraints = [constraint1] 55 | 56 | // Create a spotlight at the player 57 | let spotLight = SCNLight() 58 | spotLight.type = SCNLight.LightType.spot 59 | spotLight.spotInnerAngle = 40.0 60 | spotLight.spotOuterAngle = 80.0 61 | spotLight.castsShadow = true 62 | spotLight.color = UIColor.white 63 | let spotLightNode = SCNNode() 64 | spotLightNode.light = spotLight 65 | spotLightNode.position = SCNVector3(x: 1.0, y: 5.0, z: -2.0) 66 | self.addChildNode(spotLightNode) 67 | 68 | // Linnk it 69 | let constraint2 = SCNLookAtConstraint(target: self) 70 | constraint2.isGimbalLockEnabled = true 71 | spotLightNode.constraints = [constraint2] 72 | 73 | // Create additional omni light 74 | let lightNode = SCNNode() 75 | lightNode.light = SCNLight() 76 | lightNode.light!.type = SCNLight.LightType.omni 77 | lightNode.light!.color = UIColor.darkGray 78 | lightNode.position = SCNVector3(x: 0, y: 10.00, z: -2) 79 | self.addChildNode(lightNode) 80 | } 81 | 82 | // ------------------------------------------------------------------------- 83 | 84 | required init(coder: NSCoder) { 85 | fatalError("Not yet implemented") 86 | } 87 | 88 | // ------------------------------------------------------------------------- 89 | 90 | } 91 | -------------------------------------------------------------------------------- /partII/GameLevel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameLevel.swift 3 | // 4 | // Part 2 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 12/10/16. 8 | // Copyright © 2016 Roger Boesch. All rights reserved. 9 | // 10 | 11 | import UIKit 12 | import SceneKit 13 | 14 | // ----------------------------------------------------------------------------- 15 | 16 | class GameLevel: SCNScene, SCNSceneRendererDelegate, SCNPhysicsContactDelegate { 17 | private let levelWidth = 320 18 | private let levelLength = 320 19 | 20 | private var _terrain: RBTerrain? 21 | private var _player: Player? 22 | 23 | // ------------------------------------------------------------------------- 24 | // MARK: - Input handling 25 | 26 | func swipeLeft() { 27 | _player!.moveLeft() 28 | } 29 | 30 | // ------------------------------------------------------------------------- 31 | 32 | func swipeRight() { 33 | _player!.moveRight() 34 | } 35 | 36 | // ------------------------------------------------------------------------- 37 | // MARK: - Place objects 38 | 39 | private func addPlayer() { 40 | _player = Player() 41 | _player!.position = SCNVector3(160, 3, 0) 42 | self.rootNode.addChildNode(_player!) 43 | 44 | let moveAction = SCNAction.moveBy(x: 0, y: 0, z: 200, duration: 20) 45 | _player!.runAction(moveAction) 46 | } 47 | 48 | // ------------------------------------------------------------------------- 49 | 50 | private func addTerrain() { 51 | // Create terrain 52 | _terrain = RBTerrain(width: levelWidth, length: levelLength, scale: 128) 53 | 54 | let generator = RBPerlinNoiseGenerator(seed: nil) 55 | _terrain?.formula = {(x: Int32, y: Int32) in 56 | return generator.valueFor(x: x, y: y) 57 | } 58 | 59 | _terrain!.create(withColor: UIColor.green) 60 | _terrain!.position = SCNVector3Make(0, 0, 0) 61 | self.rootNode.addChildNode(_terrain!) 62 | } 63 | 64 | // ------------------------------------------------------------------------- 65 | // MARK: - Initialisation 66 | 67 | func create() { 68 | addTerrain() 69 | addPlayer() 70 | } 71 | 72 | // ------------------------------------------------------------------------- 73 | 74 | override init() { 75 | super.init() 76 | } 77 | 78 | // ------------------------------------------------------------------------- 79 | 80 | required init(coder: NSCoder) { 81 | fatalError("Not yet implemented") 82 | } 83 | 84 | // ------------------------------------------------------------------------- 85 | 86 | } 87 | -------------------------------------------------------------------------------- /partII/GameViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameViewController.swift 3 | // 4 | // Part 2 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 12/07/16. 8 | // Copyright © 2016 Roger Boesch. All rights reserved. 9 | // 10 | 11 | import UIKit 12 | import SceneKit 13 | 14 | class GameViewController: UIViewController { 15 | 16 | private var _sceneView: SCNView! 17 | private var _level: GameLevel! 18 | 19 | // ------------------------------------------------------------------------- 20 | // MARK: - Swipe gestures 21 | 22 | @objc private func handleSwipe(_ gestureRecognize: UISwipeGestureRecognizer) { 23 | if (gestureRecognize.direction == .left) { 24 | _level!.swipeLeft() 25 | } 26 | else if (gestureRecognize.direction == .right) { 27 | _level!.swipeRight() 28 | } 29 | } 30 | 31 | // ------------------------------------------------------------------------- 32 | // MARK: - ViewController life cycle 33 | 34 | override func viewDidLoad() { 35 | super.viewDidLoad() 36 | 37 | _level = GameLevel() 38 | _level.create() 39 | 40 | _sceneView = SCNView() 41 | _sceneView.scene = _level 42 | _sceneView.allowsCameraControl = false 43 | _sceneView.showsStatistics = true 44 | _sceneView.backgroundColor = UIColor.black 45 | self.view = _sceneView 46 | 47 | let swipeLeftGesture = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipe)) 48 | swipeLeftGesture.direction = .left 49 | _sceneView!.addGestureRecognizer(swipeLeftGesture) 50 | 51 | let swipeRightGesture = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipe)) 52 | swipeRightGesture.direction = .right 53 | _sceneView!.addGestureRecognizer(swipeRightGesture) 54 | } 55 | 56 | // ------------------------------------------------------------------------- 57 | 58 | override func didReceiveMemoryWarning() { 59 | super.didReceiveMemoryWarning() 60 | } 61 | 62 | // ------------------------------------------------------------------------- 63 | 64 | } 65 | -------------------------------------------------------------------------------- /partII/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIRequiredDeviceCapabilities 24 | 25 | armv7 26 | 27 | UIStatusBarHidden 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationLandscapeLeft 32 | UIInterfaceOrientationLandscapeRight 33 | 34 | UILaunchStoryboardName 35 | LaunchScreen 36 | 37 | 38 | -------------------------------------------------------------------------------- /partII/Player.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Player.swift 3 | // 4 | // Part 2 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 12/07/16. 8 | // Copyright © 2016 Roger Boesch. All rights reserved. 9 | // 10 | 11 | import SceneKit 12 | 13 | // ----------------------------------------------------------------------------- 14 | 15 | class Player : SCNNode { 16 | private let lookAtForwardPosition = SCNVector3Make(0.0, -1.0, 6.0) 17 | private let cameraFowardPosition = SCNVector3(x: 5, y: 1.0, z: -5) 18 | 19 | private var _lookAtNode: SCNNode? 20 | private var _cameraNode: SCNNode? 21 | private var _playerNode: SCNNode? 22 | 23 | 24 | // ------------------------------------------------------------------------- 25 | // MARK: - Camera adjustment 26 | 27 | private func toggleCamera() { 28 | var position = _cameraNode!.position 29 | 30 | if position.x < 0 { 31 | position.x = 5.0 32 | } 33 | else { 34 | position.x = -5.0 35 | } 36 | 37 | SCNTransaction.begin() 38 | SCNTransaction.animationDuration = 1.0 39 | 40 | _cameraNode?.position = position 41 | 42 | SCNTransaction.commit() 43 | } 44 | 45 | // ------------------------------------------------------------------------- 46 | // MARK: - Plane movements 47 | 48 | func moveLeft() { 49 | let moveAction = SCNAction.moveBy(x: 2.0, y: 0.0, z: 0, duration: 0.5) 50 | self.runAction(moveAction, forKey: "moveLeftRight") 51 | 52 | let rotateAction1 = SCNAction.rotateBy(x: 0, y: 0, z: -degreesToRadians(value: 15.0), duration: 0.25) 53 | let rotateAction2 = SCNAction.rotateBy(x: 0, y: 0, z: degreesToRadians(value: 15.0), duration: 0.25) 54 | 55 | _playerNode!.runAction(SCNAction.sequence([rotateAction1, rotateAction2])) 56 | 57 | toggleCamera() 58 | } 59 | 60 | // ------------------------------------------------------------------------- 61 | 62 | func moveRight() { 63 | let moveAction = SCNAction.moveBy(x: -2.0, y: 0.0, z: 0, duration: 0.5) 64 | self.runAction(moveAction, forKey: "moveLeftRight") 65 | 66 | let rotateAction1 = SCNAction.rotateBy(x: 0, y: 0, z: degreesToRadians(value: 15.0), duration: 0.25) 67 | let rotateAction2 = SCNAction.rotateBy(x: 0, y: 0, z: -degreesToRadians(value: 15.0), duration: 0.25) 68 | 69 | _playerNode!.runAction(SCNAction.sequence([rotateAction1, rotateAction2])) 70 | 71 | toggleCamera() 72 | } 73 | 74 | // ------------------------------------------------------------------------- 75 | // MARK: - Initialisation 76 | 77 | override init() { 78 | super.init() 79 | 80 | // Create player node 81 | let scene = SCNScene(named: "art.scnassets/ship.scn") 82 | if (scene == nil) { 83 | fatalError("Scene not loaded") 84 | } 85 | 86 | _playerNode = scene!.rootNode.childNode(withName: "ship", recursively: true) 87 | if (_playerNode == nil) { 88 | fatalError("Ship node not found") 89 | } 90 | 91 | _playerNode!.scale = SCNVector3(x: 0.25, y: 0.25, z: 0.25) 92 | self.addChildNode(_playerNode!) 93 | 94 | // Look at Node 95 | _lookAtNode = SCNNode() 96 | _lookAtNode!.position = lookAtForwardPosition 97 | addChildNode(_lookAtNode!) 98 | 99 | // Camera Node 100 | _cameraNode = SCNNode() 101 | _cameraNode!.camera = SCNCamera() 102 | _cameraNode!.position = cameraFowardPosition 103 | _cameraNode!.camera!.zNear = 0.1 104 | _cameraNode!.camera!.zFar = 200 105 | self.addChildNode(_cameraNode!) 106 | 107 | // Link them 108 | let constraint1 = SCNLookAtConstraint(target: _lookAtNode) 109 | constraint1.isGimbalLockEnabled = true 110 | _cameraNode!.constraints = [constraint1] 111 | 112 | // Create a spotlight at the player 113 | let spotLight = SCNLight() 114 | spotLight.type = SCNLight.LightType.spot 115 | spotLight.spotInnerAngle = 40.0 116 | spotLight.spotOuterAngle = 80.0 117 | spotLight.castsShadow = true 118 | spotLight.color = UIColor.white 119 | let spotLightNode = SCNNode() 120 | spotLightNode.light = spotLight 121 | spotLightNode.position = SCNVector3(x: 1.0, y: 5.0, z: -2.0) 122 | self.addChildNode(spotLightNode) 123 | 124 | // Linnk it 125 | let constraint2 = SCNLookAtConstraint(target: self) 126 | constraint2.isGimbalLockEnabled = true 127 | spotLightNode.constraints = [constraint2] 128 | 129 | // Create additional omni light 130 | let lightNode = SCNNode() 131 | lightNode.light = SCNLight() 132 | lightNode.light!.type = SCNLight.LightType.omni 133 | lightNode.light!.color = UIColor.darkGray 134 | lightNode.position = SCNVector3(x: 0, y: 10.00, z: -2) 135 | self.addChildNode(lightNode) 136 | } 137 | 138 | // ------------------------------------------------------------------------- 139 | 140 | required init(coder: NSCoder) { 141 | fatalError("Not yet implemented") 142 | } 143 | 144 | // ------------------------------------------------------------------------- 145 | 146 | } 147 | -------------------------------------------------------------------------------- /partIII/GameLevel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameLevel.swift 3 | // 4 | // Part 3 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 12/10/16. 8 | // Copyright © 2016 Roger Boesch. All rights reserved. 9 | // 10 | 11 | import UIKit 12 | import SceneKit 13 | 14 | // ----------------------------------------------------------------------------- 15 | 16 | class GameLevel: SCNScene, SCNSceneRendererDelegate, SCNPhysicsContactDelegate { 17 | private let levelWidth = 320 18 | private let levelLength = 640 19 | 20 | private var _terrain: RBTerrain? 21 | private var _player: Player? 22 | 23 | // Part 3: Number od rings and touched rings saved here 24 | private let numberOfRings = 10 25 | private var touchedRings = 0 26 | 27 | // Part 3: Reference to the HUD 28 | private var _hud: HUD? 29 | 30 | // ------------------------------------------------------------------------- 31 | // MARK: - Properties 32 | 33 | var hud: HUD? { 34 | get { 35 | return _hud 36 | } 37 | set(value) { 38 | _hud = value 39 | } 40 | } 41 | 42 | // ------------------------------------------------------------------------- 43 | // MARK: - Input handling 44 | 45 | func swipeLeft() { 46 | _player!.moveLeft() 47 | } 48 | 49 | // ------------------------------------------------------------------------- 50 | 51 | func swipeRight() { 52 | _player!.moveRight() 53 | } 54 | 55 | // ------------------------------------------------------------------------- 56 | // MARK: - Physics delegate 57 | 58 | func collision(withRing ring: Ring) { 59 | // Part 3: Collision handling based on physics 60 | if ring.isHidden { 61 | return 62 | } 63 | 64 | debugPrint("Collision width \(ring)") 65 | 66 | ring.isHidden = true 67 | _player!.roll() 68 | 69 | touchedRings += 1 70 | 71 | _hud?.points = touchedRings 72 | } 73 | 74 | // ------------------------------------------------------------------------- 75 | 76 | func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) { 77 | // Part 3: Physics delegate get called when objects collide 78 | if let ring = contact.nodeB.parent as? Ring { 79 | collision(withRing: ring) 80 | } 81 | } 82 | 83 | // ------------------------------------------------------------------------- 84 | // MARK: - Place objects 85 | 86 | private func addRings() { 87 | // Part 3: Add rings to the game level 88 | let space = levelLength / (numberOfRings+1) 89 | 90 | for i in 1...numberOfRings { 91 | let ring = Ring() 92 | 93 | var x: CGFloat = 160 94 | let rnd = RBRandom.integer(1, 3) 95 | if rnd == 1 { 96 | x = x - Player.moveOffset 97 | } 98 | else if rnd == 3 { 99 | x = x + Player.moveOffset 100 | } 101 | 102 | ring.position = SCNVector3(Int(x), 3, (i*space)) 103 | self.rootNode.addChildNode(ring) 104 | } 105 | } 106 | 107 | // ------------------------------------------------------------------------- 108 | 109 | private func addPlayer() { 110 | _player = Player() 111 | _player!.position = SCNVector3(160, 4, 0) 112 | self.rootNode.addChildNode(_player!) 113 | 114 | let moveAction = SCNAction.moveBy(x: 0, y: 0, z: CGFloat(levelLength)-10, duration: 60) 115 | _player!.runAction(moveAction) 116 | } 117 | 118 | // ------------------------------------------------------------------------- 119 | 120 | private func addTerrain() { 121 | // Create terrain 122 | _terrain = RBTerrain(width: levelWidth, length: levelLength, scale: 128) 123 | 124 | let generator = RBPerlinNoiseGenerator(seed: nil) 125 | _terrain?.formula = {(x: Int32, y: Int32) in 126 | return generator.valueFor(x: x, y: y) 127 | } 128 | 129 | _terrain!.create(withColor: UIColor.green) 130 | _terrain!.position = SCNVector3Make(0, 0, 0) 131 | self.rootNode.addChildNode(_terrain!) 132 | } 133 | 134 | // ------------------------------------------------------------------------- 135 | // MARK: - Initialisation 136 | 137 | func create() { 138 | addTerrain() 139 | addPlayer() 140 | addRings() 141 | } 142 | 143 | // ------------------------------------------------------------------------- 144 | 145 | override init() { 146 | super.init() 147 | 148 | self.physicsWorld.contactDelegate = self 149 | } 150 | 151 | // ------------------------------------------------------------------------- 152 | 153 | required init(coder: NSCoder) { 154 | fatalError("Not yet implemented") 155 | } 156 | 157 | // ------------------------------------------------------------------------- 158 | 159 | } 160 | -------------------------------------------------------------------------------- /partIII/GameSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameSettings.swift 3 | // 4 | // Part 3 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 12/10/16. 8 | // Copyright © 2016 Roger Boesch. All rights reserved. 9 | // 10 | // Structs are a very good way to structure the different application settings. 11 | // Like that they are easy to read and changeable at one place 12 | // 13 | 14 | import Foundation 15 | 16 | struct Game { 17 | 18 | struct Physics { 19 | 20 | // Category bits used for physics handling 21 | struct Categories { 22 | static let player: Int = 0b00000001 23 | static let ring: Int = 0b00000010 24 | } 25 | 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /partIII/GameViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameViewController.swift 3 | // 4 | // Part 3 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 12/07/16. 8 | // Copyright © 2016 Roger Boesch. All rights reserved. 9 | // 10 | 11 | import UIKit 12 | import SceneKit 13 | 14 | class GameViewController: UIViewController { 15 | 16 | private var _sceneView: SCNView! 17 | private var _level: GameLevel! 18 | 19 | // ------------------------------------------------------------------------- 20 | // MARK: - Swipe gestures 21 | 22 | @objc private func handleSwipe(_ gestureRecognize: UISwipeGestureRecognizer) { 23 | if (gestureRecognize.direction == .left) { 24 | _level!.swipeLeft() 25 | } 26 | else if (gestureRecognize.direction == .right) { 27 | _level!.swipeRight() 28 | } 29 | } 30 | 31 | // ------------------------------------------------------------------------- 32 | // MARK: - ViewController life cycle 33 | 34 | override func viewWillAppear(_ animated: Bool) { 35 | super.viewWillAppear(animated) 36 | 37 | // Part 3: HUD is created and assigned to view and game level 38 | let hud = HUD(size: self.view.bounds.size) 39 | _level.hud = hud 40 | _sceneView.overlaySKScene = hud.scene 41 | } 42 | 43 | // ------------------------------------------------------------------------- 44 | 45 | override func viewDidLoad() { 46 | super.viewDidLoad() 47 | 48 | _level = GameLevel() 49 | _level.create() 50 | 51 | _sceneView = SCNView() 52 | _sceneView.scene = _level 53 | _sceneView.allowsCameraControl = false 54 | _sceneView.showsStatistics = true 55 | _sceneView.backgroundColor = UIColor.black 56 | self.view = _sceneView 57 | 58 | let swipeLeftGesture = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipe)) 59 | swipeLeftGesture.direction = .left 60 | _sceneView!.addGestureRecognizer(swipeLeftGesture) 61 | 62 | let swipeRightGesture = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipe)) 63 | swipeRightGesture.direction = .right 64 | _sceneView!.addGestureRecognizer(swipeRightGesture) 65 | } 66 | 67 | // ------------------------------------------------------------------------- 68 | 69 | override func didReceiveMemoryWarning() { 70 | super.didReceiveMemoryWarning() 71 | } 72 | 73 | // ------------------------------------------------------------------------- 74 | 75 | } 76 | -------------------------------------------------------------------------------- /partIII/HUD.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HUD.swift 3 | // 4 | // Part 3 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 11.11.17. 8 | // Copyright © 2017 Roger Boesch. All rights reserved. 9 | // 10 | // This class I have newly introduced in Part 3. It's purpose 11 | // is to show information, like points, time etc. to the user. 12 | // At the moment the number of catched rings. 13 | // 14 | 15 | import SpriteKit 16 | 17 | class HUD { 18 | private var _scene: SKScene! 19 | private let _points = SKLabelNode(text: "0 RINGS") 20 | 21 | // ------------------------------------------------------------------------- 22 | // MARK: - Properties 23 | 24 | var scene: SKScene { 25 | get { 26 | return _scene 27 | } 28 | } 29 | 30 | // ------------------------------------------------------------------------- 31 | 32 | var points: Int { 33 | get { 34 | return 0 35 | } 36 | set(value) { 37 | _points.text = String(format: "%d RINGS", value) 38 | } 39 | } 40 | 41 | // ------------------------------------------------------------------------- 42 | // MARK: - Initialisation 43 | 44 | init(size: CGSize) { 45 | _scene = SKScene(size: size) 46 | 47 | _points.position = CGPoint(x: size.width/2, y: size.height-50) 48 | _points.horizontalAlignmentMode = .center 49 | _points.fontName = "MarkerFelt-Wide" 50 | _points.fontSize = 30 51 | _points.fontColor = UIColor.white 52 | _scene.addChild(_points) 53 | } 54 | 55 | // ------------------------------------------------------------------------- 56 | 57 | required init?(coder aDecoder: NSCoder) { 58 | fatalError("init?(coder aDecoder: NSCoder) not implemented") 59 | } 60 | 61 | // ------------------------------------------------------------------------- 62 | } 63 | 64 | -------------------------------------------------------------------------------- /partIII/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UIStatusBarHidden 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationLandscapeLeft 34 | UIInterfaceOrientationLandscapeRight 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /partIII/Player.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Player.swift 3 | // 4 | // Part 3 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 12/07/16. 8 | // Copyright © 2016 Roger Boesch. All rights reserved. 9 | // 10 | 11 | import SceneKit 12 | 13 | // ----------------------------------------------------------------------------- 14 | 15 | class Player : SCNNode { 16 | public static let moveOffset: CGFloat = 10 17 | 18 | private let lookAtForwardPosition = SCNVector3Make(0.0, -1.0, 6.0) 19 | private let cameraFowardPosition = SCNVector3(x: 0, y: 1.0, z: -5) 20 | 21 | private var _lookAtNode: SCNNode? 22 | private var _cameraNode: SCNNode? 23 | private var _playerNode: SCNNode? 24 | 25 | // ------------------------------------------------------------------------- 26 | // MARK: - Effects 27 | 28 | func roll() { 29 | // Part 3: An easy effect we use, whenever we fly trough a ring 30 | let rotateAction = SCNAction.rotateBy(x: 0, y: 0, z: -degreesToRadians(value: 360.0), duration: 0.5) 31 | _playerNode!.runAction(rotateAction) 32 | } 33 | 34 | // ------------------------------------------------------------------------- 35 | // MARK: - Camera adjustment 36 | 37 | private func toggleCamera() { 38 | var position = _cameraNode!.position 39 | 40 | if position.x < 0 { 41 | position.x = 0.5 42 | } 43 | else { 44 | position.x = -0.5 45 | } 46 | 47 | SCNTransaction.begin() 48 | SCNTransaction.animationDuration = 1.0 49 | 50 | _cameraNode?.position = position 51 | 52 | SCNTransaction.commit() 53 | } 54 | 55 | // ------------------------------------------------------------------------- 56 | // MARK: - Plane movements 57 | 58 | func moveLeft() { 59 | let moveAction = SCNAction.moveBy(x: Player.moveOffset, y: 0.0, z: 0, duration: 0.5) 60 | self.runAction(moveAction, forKey: "moveLeftRight") 61 | 62 | let rotateAction1 = SCNAction.rotateBy(x: 0, y: 0, z: -degreesToRadians(value: 15.0), duration: 0.25) 63 | let rotateAction2 = SCNAction.rotateBy(x: 0, y: 0, z: degreesToRadians(value: 15.0), duration: 0.25) 64 | 65 | _playerNode!.runAction(SCNAction.sequence([rotateAction1, rotateAction2])) 66 | 67 | toggleCamera() 68 | } 69 | 70 | // ------------------------------------------------------------------------- 71 | 72 | func moveRight() { 73 | let moveAction = SCNAction.moveBy(x: -Player.moveOffset, y: 0.0, z: 0, duration: 0.5) 74 | self.runAction(moveAction, forKey: "moveLeftRight") 75 | 76 | let rotateAction1 = SCNAction.rotateBy(x: 0, y: 0, z: degreesToRadians(value: 15.0), duration: 0.25) 77 | let rotateAction2 = SCNAction.rotateBy(x: 0, y: 0, z: -degreesToRadians(value: 15.0), duration: 0.25) 78 | 79 | _playerNode!.runAction(SCNAction.sequence([rotateAction1, rotateAction2])) 80 | 81 | toggleCamera() 82 | } 83 | 84 | // ------------------------------------------------------------------------- 85 | // MARK: - Initialisation 86 | 87 | override init() { 88 | super.init() 89 | 90 | // Create player node 91 | let scene = SCNScene(named: "art.scnassets/ship.scn") 92 | if (scene == nil) { 93 | fatalError("Scene not loaded") 94 | } 95 | 96 | _playerNode = scene!.rootNode.childNode(withName: "ship", recursively: true) 97 | _playerNode?.name = "player" 98 | 99 | if (_playerNode == nil) { 100 | fatalError("Ship node not found") 101 | } 102 | 103 | _playerNode!.scale = SCNVector3(x: 0.25, y: 0.25, z: 0.25) 104 | self.addChildNode(_playerNode!) 105 | 106 | // Contact box 107 | // Part 3: Instead of use the plane itself we add a collision node to the player object 108 | let boxMaterial = SCNMaterial() 109 | boxMaterial.diffuse.contents = UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 0.0) 110 | 111 | let box = SCNBox(width: 2.0, height: 1.0, length: 1.0, chamferRadius: 0.0) 112 | box.materials = [boxMaterial] 113 | let contactBox = SCNNode(geometry: box) 114 | contactBox.name = "player" 115 | contactBox.physicsBody = SCNPhysicsBody(type: .kinematic, shape: nil) 116 | contactBox.physicsBody?.categoryBitMask = Game.Physics.Categories.player 117 | contactBox.physicsBody!.contactTestBitMask = Game.Physics.Categories.ring 118 | self.addChildNode(contactBox) 119 | 120 | // Look at Node 121 | _lookAtNode = SCNNode() 122 | _lookAtNode!.position = lookAtForwardPosition 123 | addChildNode(_lookAtNode!) 124 | 125 | // Camera Node 126 | _cameraNode = SCNNode() 127 | _cameraNode!.camera = SCNCamera() 128 | _cameraNode!.position = cameraFowardPosition 129 | _cameraNode!.camera!.zNear = 0.1 130 | _cameraNode!.camera!.zFar = 200 131 | self.addChildNode(_cameraNode!) 132 | 133 | // Link them 134 | let constraint1 = SCNLookAtConstraint(target: _lookAtNode) 135 | constraint1.isGimbalLockEnabled = true 136 | _cameraNode!.constraints = [constraint1] 137 | 138 | // Create a spotlight at the player 139 | let spotLight = SCNLight() 140 | spotLight.type = SCNLight.LightType.spot 141 | spotLight.spotInnerAngle = 40.0 142 | spotLight.spotOuterAngle = 80.0 143 | spotLight.castsShadow = true 144 | spotLight.color = UIColor.white 145 | let spotLightNode = SCNNode() 146 | spotLightNode.light = spotLight 147 | spotLightNode.position = SCNVector3(x: 1.0, y: 5.0, z: -2.0) 148 | self.addChildNode(spotLightNode) 149 | 150 | // Link it 151 | let constraint2 = SCNLookAtConstraint(target: self) 152 | constraint2.isGimbalLockEnabled = true 153 | spotLightNode.constraints = [constraint2] 154 | 155 | // Create additional omni light 156 | let lightNode = SCNNode() 157 | lightNode.light = SCNLight() 158 | lightNode.light!.type = SCNLight.LightType.omni 159 | lightNode.light!.color = UIColor.darkGray 160 | lightNode.position = SCNVector3(x: 0, y: 10.00, z: -2) 161 | self.addChildNode(lightNode) 162 | } 163 | 164 | // ------------------------------------------------------------------------- 165 | 166 | required init(coder: NSCoder) { 167 | fatalError("Not yet implemented") 168 | } 169 | 170 | // ------------------------------------------------------------------------- 171 | 172 | } 173 | -------------------------------------------------------------------------------- /partIII/Ring.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Ring.swift 3 | // 4 | // Part 3 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 13/12/17. 8 | // Copyright © 2017 Roger Boesch. All rights reserved. 9 | // 10 | // Rings are the first game elements (besides the player) introduced in this tutorial. 11 | // The goal is simple: Th eplayer must fly trough this rings and get one point for each. 12 | // 13 | 14 | import SceneKit 15 | 16 | // ----------------------------------------------------------------------------- 17 | 18 | class Ring : SCNNode { 19 | 20 | // ------------------------------------------------------------------------- 21 | // MARK: - Initialisation 22 | 23 | override init() { 24 | super.init() 25 | 26 | let ringMaterial = SCNMaterial() 27 | ringMaterial.diffuse.contents = UIImage(named: "art.scnassets/ringTexture") 28 | ringMaterial.diffuse.wrapS = .repeat 29 | ringMaterial.diffuse.wrapT = .repeat 30 | ringMaterial.diffuse.contentsTransform = SCNMatrix4MakeScale(3, 1, 1) 31 | 32 | let ring = SCNTorus(ringRadius: 5.0, pipeRadius: 0.5) 33 | ring.materials = [ringMaterial] 34 | let ringNode = SCNNode(geometry: ring) 35 | self.addChildNode(ringNode) 36 | 37 | ringNode.eulerAngles = SCNVector3(degreesToRadians(value: 90), 0, 0) 38 | 39 | let action = SCNAction.rotateBy(x: 0, y: 0, z: degreesToRadians(value: 360), duration: 3.0) 40 | ringNode.runAction(SCNAction.repeatForever(action)) 41 | 42 | // Contact box 43 | let boxMaterial = SCNMaterial() 44 | boxMaterial.diffuse.contents = UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 0.0) 45 | 46 | let box = SCNBox(width: 10.0, height: 10.0, length: 1.0, chamferRadius: 0.0) 47 | box.materials = [boxMaterial] 48 | let contactBox = SCNNode(geometry: box) 49 | contactBox.name = "ring" 50 | contactBox.physicsBody = SCNPhysicsBody(type: .kinematic, shape: nil) 51 | contactBox.physicsBody?.categoryBitMask = Game.Physics.Categories.ring 52 | self.addChildNode(contactBox) 53 | } 54 | 55 | // ------------------------------------------------------------------------- 56 | 57 | required init(coder: NSCoder) { 58 | fatalError("Not yet implemented") 59 | } 60 | 61 | // ------------------------------------------------------------------------- 62 | 63 | } 64 | 65 | -------------------------------------------------------------------------------- /partIV/GameLevel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameLevel.swift 3 | // 4 | // Part 4 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 12/10/16. 8 | // Copyright © 2016 Roger Boesch. All rights reserved. 9 | // 10 | // New in Part 4: 11 | // The game has become a game loop. A main concept used in any pro game 12 | // While you can create SceneKit games without this, it open's some b iug 13 | // advantages which are covered in more detail in the tutorial 14 | 15 | import UIKit 16 | import SceneKit 17 | 18 | enum GameState { 19 | case initialized, play, win, loose, stopped 20 | } 21 | 22 | // ----------------------------------------------------------------------------- 23 | 24 | class GameLevel: SCNScene, SCNPhysicsContactDelegate { 25 | private let levelWidth = 320 26 | private let levelLength = 640 27 | 28 | // New in Part 4: A list of all game objects 29 | private var _gameObjects = Array() 30 | 31 | private var _terrain: RBTerrain? 32 | private var _player: Player? 33 | private var _hud: HUD? 34 | 35 | private let _numberOfRings = 10 36 | private var _touchedRings = 0 37 | 38 | // New in Part 4: We catch now also the number of missed rings 39 | private var _missedRings = 0 40 | 41 | // New in Part 4: The game becomes differenr states 42 | private var _state = GameState.initialized 43 | 44 | // ------------------------------------------------------------------------- 45 | // MARK: - Properties 46 | 47 | var hud: HUD? { 48 | get { 49 | return _hud 50 | } 51 | set(value) { 52 | _hud = value 53 | } 54 | } 55 | 56 | // ------------------------------------------------------------------------- 57 | 58 | var state: GameState { 59 | get { 60 | return _state 61 | } 62 | set(value) { 63 | rbDebug("State of level changed from \(_state) to \(value)") 64 | _state = value 65 | } 66 | } 67 | 68 | // ------------------------------------------------------------------------- 69 | // MARK: - Input handling 70 | 71 | func swipeLeft() { 72 | _player!.moveLeft() 73 | } 74 | 75 | // ------------------------------------------------------------------------- 76 | 77 | func swipeRight() { 78 | _player!.moveRight() 79 | } 80 | 81 | // ------------------------------------------------------------------------- 82 | // MARK: - Actions 83 | 84 | func flyTrough(_ ring: Ring) { 85 | _touchedRings += 1 86 | _hud?.rings = _touchedRings 87 | } 88 | 89 | // ------------------------------------------------------------------------- 90 | 91 | func touchedHandicap(_ handicap: Handicap) { 92 | _hud?.message("GAME OVER", information: "- Touch to restart - ") 93 | 94 | self.state = .loose 95 | } 96 | 97 | // ------------------------------------------------------------------------- 98 | // MARK: - Game loop 99 | 100 | func update(atTime time: TimeInterval) { 101 | // New in Part 4: The game loop (see tutorial) 102 | if self.state != .play { 103 | return 104 | } 105 | 106 | var missedRings = 0 107 | 108 | for object in _gameObjects { 109 | object.update(atTime: time, level: self) 110 | 111 | // Check which rings are behind the player but still 'alive' 112 | if let ring = object as? Ring { 113 | if (ring.presentation.position.z + 5.0) < _player!.presentation.position.z { 114 | if ring.state == .alive { 115 | missedRings += 1 116 | } 117 | } 118 | } 119 | } 120 | 121 | if missedRings > _missedRings { 122 | _missedRings = missedRings 123 | _hud?.missedRings = _missedRings 124 | } 125 | 126 | // Test for end of game 127 | if _missedRings + _touchedRings == _numberOfRings { 128 | if _missedRings < 3 { 129 | _hud?.message("YOU WIN", information: "- Touch to restart - ") 130 | } 131 | else { 132 | _hud?.message("TRY TO IMPROVE", information: "- Touch to restart - ") 133 | } 134 | 135 | self.state = .win 136 | } 137 | } 138 | 139 | // ------------------------------------------------------------------------- 140 | // MARK: - Physics delegate 141 | 142 | func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) { 143 | // Part 4: Call collision handler of the object (see GameObject) 144 | if let gameObjectA = contact.nodeA.parent as? GameObject, let gameObjectB = contact.nodeB.parent as? GameObject { 145 | gameObjectA.collision(with: gameObjectB, level: self) 146 | gameObjectB.collision(with: gameObjectA, level: self) 147 | } 148 | } 149 | 150 | // ------------------------------------------------------------------------- 151 | // MARK: - Place objects 152 | 153 | private func addRings() { 154 | // Part 3: Add rings to the game level 155 | let space = levelLength / (_numberOfRings+1) 156 | 157 | for i in 1..._numberOfRings { 158 | let ring = Ring() 159 | 160 | var x: CGFloat = 160 161 | let rnd = RBRandom.integer(1, 3) 162 | if rnd == 1 { 163 | x = x - Game.Player.moveOffset 164 | } 165 | else if rnd == 3 { 166 | x = x + Game.Player.moveOffset 167 | } 168 | 169 | ring.position = SCNVector3(Int(x), 3, (i*space)) 170 | self.rootNode.addChildNode(ring) 171 | 172 | _gameObjects.append(ring) 173 | } 174 | } 175 | 176 | // ------------------------------------------------------------------------- 177 | 178 | private func addHandicap(x: CGFloat, z: CGFloat) { 179 | let handicap = Handicap() 180 | handicap.position = SCNVector3(x, handicap.height/2, z) 181 | self.rootNode.addChildNode(handicap) 182 | 183 | _gameObjects.append(handicap) 184 | } 185 | 186 | // ------------------------------------------------------------------------- 187 | 188 | private func addHandicaps() { 189 | // Part 4: Add handicaps to the game level 190 | let space: CGFloat = CGFloat(levelLength / (_numberOfRings+1)) 191 | 192 | for i in 1..._numberOfRings-1 { 193 | var x: CGFloat = 160 194 | let rnd = RBRandom.integer(1, 3) 195 | 196 | if rnd == 1 { 197 | x = x - Game.Player.moveOffset 198 | 199 | addHandicap(x: x-RBRandom.cgFloat(10, 50), z: CGFloat(i)*space + space/2.0) 200 | addHandicap(x: x+Game.Player.moveOffset+RBRandom.cgFloat(10, 50), z: CGFloat(i)*space + space/2.0) 201 | } 202 | else if rnd == 3 { 203 | x = x + Game.Player.moveOffset 204 | 205 | addHandicap(x: x+RBRandom.cgFloat(10, 50), z: CGFloat(i)*space + space/2.0) 206 | addHandicap(x: x-Game.Player.moveOffset-RBRandom.cgFloat(10, 50), z: CGFloat(i)*space + space/2.0) 207 | } 208 | 209 | addHandicap(x: x, z: CGFloat(i)*space + space/2.0) 210 | } 211 | } 212 | 213 | // ------------------------------------------------------------------------- 214 | 215 | private func addPlayer() { 216 | _player = Player() 217 | _player!.state = .alive 218 | 219 | _player!.position = SCNVector3(160, 4, 0) 220 | self.rootNode.addChildNode(_player!) 221 | 222 | let moveAction = SCNAction.moveBy(x: 0, y: 0, z: CGFloat(levelLength)-10, duration: 60) 223 | _player!.runAction(moveAction) 224 | 225 | _gameObjects.append(_player!) 226 | } 227 | 228 | // ------------------------------------------------------------------------- 229 | 230 | private func addTerrain() { 231 | // Create terrain 232 | _terrain = RBTerrain(width: levelWidth, length: levelLength, scale: 128) 233 | 234 | let generator = RBPerlinNoiseGenerator(seed: nil) 235 | _terrain?.formula = {(x: Int32, y: Int32) in 236 | return generator.valueFor(x: x, y: y) 237 | } 238 | 239 | _terrain!.create(withColor: UIColor.green) 240 | _terrain!.position = SCNVector3Make(0, 0, 0) 241 | self.rootNode.addChildNode(_terrain!) 242 | } 243 | 244 | // ------------------------------------------------------------------------- 245 | // MARK: - Stop 246 | 247 | func stop() { 248 | // New in Part 4: Stop all! 249 | for object in _gameObjects { 250 | object.stop() 251 | } 252 | 253 | self.physicsWorld.contactDelegate = nil 254 | self.hud = nil 255 | self.state = .stopped 256 | } 257 | 258 | // ------------------------------------------------------------------------- 259 | // MARK: - Initialisation 260 | 261 | func create() { 262 | // New in Part 4: A skybox is used to show a game's background 263 | self.background.contents = UIImage(named: "art.scnassets/skybox") 264 | 265 | addTerrain() 266 | addRings() 267 | addHandicaps() 268 | addPlayer() 269 | 270 | self.state = .play 271 | } 272 | 273 | // ------------------------------------------------------------------------- 274 | 275 | override init() { 276 | super.init() 277 | 278 | self.physicsWorld.contactDelegate = self 279 | } 280 | 281 | // ------------------------------------------------------------------------- 282 | 283 | required init(coder: NSCoder) { 284 | fatalError("Not yet implemented") 285 | } 286 | 287 | // ------------------------------------------------------------------------- 288 | 289 | } 290 | -------------------------------------------------------------------------------- /partIV/GameObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameObject.swift 3 | // 4 | // Part 4 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 31.10.17. 8 | // Copyright © 2017 Roger Boesch. All rights reserved. 9 | // 10 | // New in Part 4: This is a bigger change. While the game becomes more and more 11 | // various game objects, it's common to encapsulate general things 12 | // in a base class. 13 | // The most important is the state which is described in detail in the tutorial. 14 | 15 | import SceneKit 16 | 17 | enum GameObjecState { 18 | case initialized, alive, died, stopped 19 | } 20 | 21 | class GameObject : SCNNode { 22 | private static var count: Int = 0 23 | static var level: GameLevel? 24 | 25 | private var _internalID: Int = 0 26 | private var _tag = 0 27 | private var _state = GameObjecState.initialized 28 | 29 | // ------------------------------------------------------------------------- 30 | // MARK: - Propertiues 31 | 32 | override var description: String { 33 | get { 34 | return "game object \(self.id)" 35 | } 36 | } 37 | 38 | // ------------------------------------------------------------------------- 39 | 40 | var id: Int { 41 | return _internalID 42 | } 43 | 44 | // ------------------------------------------------------------------------- 45 | 46 | var tag: Int { 47 | get { 48 | return _tag 49 | } 50 | set(value) { 51 | _tag = value 52 | } 53 | } 54 | 55 | // ------------------------------------------------------------------------- 56 | 57 | var state: GameObjecState { 58 | get { 59 | return _state 60 | } 61 | set(value) { 62 | rbDebug("State of \(self) changed from \(_state) to \(value)") 63 | _state = value 64 | } 65 | } 66 | 67 | // ------------------------------------------------------------------------- 68 | // MARK: - Actions 69 | 70 | func hit() {} // Object get hit by another object 71 | 72 | // ------------------------------------------------------------------------- 73 | 74 | func stop() { 75 | // Stop object (release all) 76 | self.state = .stopped 77 | stopAllActions(self) 78 | } 79 | 80 | // ------------------------------------------------------------------------- 81 | // MARK: - Game loop 82 | 83 | func update(atTime time: TimeInterval, level: GameLevel) {} 84 | 85 | // ------------------------------------------------------------------------- 86 | // MARK: - Collision handling 87 | 88 | func collision(with object: GameObject, level: GameLevel) {} 89 | 90 | // ------------------------------------------------------------------------- 91 | // MARK: - Helper methods 92 | 93 | func stopAllActions(_ node: SCNNode) { 94 | // It's important to stop all actions before we remove a game object 95 | // Otherwise they continue to run and result in difficult side effects. 96 | node.removeAllActions() 97 | 98 | for child in node.childNodes { 99 | child.removeAllActions() 100 | stopAllActions(child) 101 | } 102 | } 103 | 104 | // ------------------------------------------------------------------------- 105 | // MARK: - Initialisation 106 | 107 | override init() { 108 | super.init() 109 | 110 | GameObject.count += 1 111 | _internalID = GameObject.count 112 | } 113 | 114 | // ------------------------------------------------------------------------- 115 | 116 | required init?(coder aDecoder: NSCoder) { 117 | fatalError("init(coder aDecoder: NSCoder) is not implemented") 118 | } 119 | 120 | // ------------------------------------------------------------------------- 121 | } 122 | 123 | 124 | -------------------------------------------------------------------------------- /partIV/GameSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameSettings.swift 3 | // 4 | // Part 4 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 12/10/16. 8 | // Copyright © 2016 Roger Boesch. All rights reserved. 9 | // 10 | // Structs are a very good way to structure the different application settings. 11 | // Like that they are easy to read and changeable at one place 12 | // 13 | // New in Part 4: Move all constants here 14 | 15 | import UIKit 16 | 17 | struct Game { 18 | 19 | struct Player { 20 | static let moveOffset: CGFloat = 15 21 | } 22 | 23 | struct Physics { 24 | 25 | // Category bits used for physics handling 26 | struct Categories { 27 | static let player: Int = 0b00000001 28 | static let ring: Int = 0b00000010 29 | static let enemy: Int = 0b00000100 30 | } 31 | 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /partIV/GameViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameViewController.swift 3 | // 4 | // Part 4 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 12/07/16. 8 | // Copyright © 2016 Roger Boesch. All rights reserved. 9 | // 10 | 11 | import UIKit 12 | import SceneKit 13 | import SpriteKit 14 | 15 | class GameViewController: UIViewController, SCNSceneRendererDelegate { 16 | private var _sceneView: SCNView! 17 | private var _level: GameLevel! 18 | private var _hud: HUD! 19 | 20 | // ------------------------------------------------------------------------- 21 | // MARK: - Properties 22 | 23 | var sceneView: SCNView { 24 | return _sceneView 25 | } 26 | 27 | // ------------------------------------------------------------------------- 28 | 29 | var hud: HUD { 30 | return _hud 31 | } 32 | 33 | // ------------------------------------------------------------------------- 34 | // MARK: - Render delegate (New in Part 4) 35 | 36 | func renderer(_ renderer: SCNSceneRenderer, didSimulatePhysicsAtTime time: TimeInterval) { 37 | if _level != nil { 38 | _level.update(atTime: time) 39 | } 40 | 41 | renderer.loops = true 42 | } 43 | 44 | // ------------------------------------------------------------------------- 45 | // MARK: - Gesture recognoizers 46 | 47 | @objc private func handleTap(_ gestureRecognize: UITapGestureRecognizer) { 48 | // New in Part 4: A tap is used to restart the level (see tutorial) 49 | if _level.state == .loose || _level.state == .win { 50 | _level.stop() 51 | _level = nil 52 | 53 | DispatchQueue.main.async { 54 | // Create things in main thread 55 | 56 | let level = GameLevel() 57 | level.create() 58 | 59 | level.hud = self.hud 60 | self.hud.reset() 61 | 62 | self.sceneView.scene = level 63 | self._level = level 64 | } 65 | 66 | } 67 | } 68 | 69 | // ------------------------------------------------------------------------- 70 | 71 | @objc private func handleSwipe(_ gestureRecognize: UISwipeGestureRecognizer) { 72 | if (gestureRecognize.direction == .left) { 73 | _level!.swipeLeft() 74 | } 75 | else if (gestureRecognize.direction == .right) { 76 | _level!.swipeRight() 77 | } 78 | } 79 | 80 | // ------------------------------------------------------------------------- 81 | // MARK: - ViewController life cycle 82 | 83 | override func viewWillAppear(_ animated: Bool) { 84 | super.viewWillAppear(animated) 85 | 86 | // Part 3: HUD is created and assigned to view and game level 87 | _hud = HUD(size: self.view.bounds.size) 88 | _level.hud = _hud 89 | _sceneView.overlaySKScene = _hud.scene 90 | } 91 | 92 | // ------------------------------------------------------------------------- 93 | 94 | override func viewDidLoad() { 95 | super.viewDidLoad() 96 | 97 | _level = GameLevel() 98 | _level.create() 99 | 100 | _sceneView = SCNView() 101 | _sceneView.scene = _level 102 | _sceneView.allowsCameraControl = false 103 | _sceneView.showsStatistics = false 104 | _sceneView.backgroundColor = UIColor.black 105 | _sceneView.delegate = self 106 | 107 | self.view = _sceneView 108 | 109 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap)) 110 | _sceneView!.addGestureRecognizer(tapGesture) 111 | 112 | let swipeLeftGesture = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipe)) 113 | swipeLeftGesture.direction = .left 114 | _sceneView!.addGestureRecognizer(swipeLeftGesture) 115 | 116 | let swipeRightGesture = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipe)) 117 | swipeRightGesture.direction = .right 118 | _sceneView!.addGestureRecognizer(swipeRightGesture) 119 | } 120 | 121 | // ------------------------------------------------------------------------- 122 | 123 | override func didReceiveMemoryWarning() { 124 | super.didReceiveMemoryWarning() 125 | } 126 | 127 | // ------------------------------------------------------------------------- 128 | 129 | } 130 | -------------------------------------------------------------------------------- /partIV/HUD.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HUD.swift 3 | // 4 | // Part 4 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 11.11.17. 8 | // Copyright © 2017 Roger Boesch. All rights reserved. 9 | // 10 | 11 | import SpriteKit 12 | 13 | class HUD { 14 | private var _scene: SKScene! 15 | private let _rings = SKLabelNode(text: "") 16 | private let _missedRings = SKLabelNode(text: "") 17 | private let _message = SKLabelNode(text: "") 18 | private let _info = SKLabelNode(text: "") 19 | 20 | // ------------------------------------------------------------------------- 21 | // MARK: - Properties 22 | 23 | var scene: SKScene { 24 | get { 25 | return _scene 26 | } 27 | } 28 | 29 | // ------------------------------------------------------------------------- 30 | 31 | var rings: Int { 32 | get { 33 | return 0 34 | } 35 | set(value) { 36 | if value == 1 { 37 | _rings.text = String(format: "%d RING", value) 38 | } 39 | else { 40 | _rings.text = String(format: "%d RINGS", value) 41 | } 42 | 43 | // New in Part 4: Animated HUD informations (check RB+SKAction.swift for details) 44 | let scaling: CGFloat = 3 45 | let action = SKAction.zoomWithNode(_rings, amount: CGPoint.make(scaling, scaling), oscillations: 1, duration: 0.5) 46 | _rings.run(action) 47 | } 48 | } 49 | 50 | // ------------------------------------------------------------------------- 51 | 52 | var missedRings: Int { 53 | get { 54 | return 0 55 | } 56 | set(value) { 57 | _missedRings.text = String(format: "%d MISSED", value) 58 | 59 | if value > 0 { 60 | _missedRings.fontColor = UIColor.red 61 | } 62 | else { 63 | _missedRings.fontColor = UIColor.white 64 | } 65 | 66 | let scaling: CGFloat = 3 67 | let action = SKAction.zoomWithNode(_missedRings, amount: CGPoint.make(scaling, scaling), oscillations: 1, duration: 0.5) 68 | _missedRings.run(action) 69 | } 70 | } 71 | 72 | // ------------------------------------------------------------------------- 73 | 74 | func message(_ str: String, information: String? = nil) { 75 | // New in Part 4: Used for game over and win messages 76 | _message.text = str 77 | _message.isHidden = false 78 | 79 | let scaling: CGFloat = 10 80 | let action = SKAction.zoomWithNode(_message, amount: CGPoint.make(scaling, scaling), oscillations: 1, duration: 0.5) 81 | _message.run(action) 82 | 83 | if information != nil { 84 | info(information!) 85 | } 86 | } 87 | 88 | // ------------------------------------------------------------------------- 89 | 90 | func info(_ str: String) { 91 | // New in Part 4: Uses for additional info when show messages 92 | 93 | _info.text = str 94 | _info.isHidden = false 95 | 96 | let scaling: CGFloat = 2 97 | let action = SKAction.zoomWithNode(_info, amount: CGPoint.make(scaling, scaling), oscillations: 1, duration: 0.5) 98 | _info.run(action) 99 | } 100 | 101 | // ------------------------------------------------------------------------- 102 | 103 | func reset() { 104 | // New in Part 4: Reset is needed whenever start the level 105 | 106 | _message.text = "" 107 | _message.isHidden = true 108 | 109 | _info.text = "" 110 | _info.isHidden = true 111 | 112 | _rings.text = "0 RINGS" 113 | _missedRings.text = "0 MISSED" 114 | } 115 | 116 | // ------------------------------------------------------------------------- 117 | // MARK: - Initialisation 118 | 119 | init(size: CGSize) { 120 | _scene = SKScene(size: size) 121 | 122 | _rings.position = CGPoint(x: 40, y: size.height-50) 123 | _rings.horizontalAlignmentMode = .left 124 | _rings.fontName = "MarkerFelt-Wide" 125 | _rings.fontSize = 30 126 | _rings.fontColor = UIColor.white 127 | _scene.addChild(_rings) 128 | 129 | _missedRings.position = CGPoint(x: size.width-40, y: size.height-50) 130 | _missedRings.horizontalAlignmentMode = .right 131 | _missedRings.fontName = "MarkerFelt-Wide" 132 | _missedRings.fontSize = 30 133 | _missedRings.fontColor = UIColor.white 134 | _scene.addChild(_missedRings) 135 | 136 | _message.position = CGPoint(x: size.width/2, y: size.height/2) 137 | _message.horizontalAlignmentMode = .center 138 | _message.fontName = "MarkerFelt-Wide" 139 | _message.fontSize = 60 140 | _message.fontColor = UIColor.white 141 | _message.isHidden = true 142 | _scene.addChild(_message) 143 | 144 | _info.position = CGPoint(x: size.width/2, y: size.height/2-40) 145 | _info.horizontalAlignmentMode = .center 146 | _info.fontName = "MarkerFelt-Wide" 147 | _info.fontSize = 20 148 | _info.fontColor = UIColor.white 149 | _info.isHidden = true 150 | _scene.addChild(_info) 151 | 152 | reset() 153 | } 154 | 155 | // ------------------------------------------------------------------------- 156 | 157 | required init?(coder aDecoder: NSCoder) { 158 | fatalError("init?(coder aDecoder: NSCoder) not implemented") 159 | } 160 | 161 | // ------------------------------------------------------------------------- 162 | } 163 | -------------------------------------------------------------------------------- /partIV/Handicap.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Handicap.swift 3 | // 4 | // Part 4 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 13/12/17. 8 | // Copyright © 2017 Roger Boesch. All rights reserved. 9 | // 10 | // New in Part 4: So far we just had rings to fly trough. Now we introduce 11 | // handicaps which the plane should not touch. Otherwise it crashes and the 12 | // game is over. 13 | 14 | import SceneKit 15 | 16 | // ----------------------------------------------------------------------------- 17 | 18 | class Handicap : GameObject { 19 | private var _node: SCNNode! 20 | private var _height: CGFloat = 0 21 | 22 | // ------------------------------------------------------------------------- 23 | // MARK: - Propertiues 24 | 25 | override var description: String { 26 | get { 27 | return "handicap \(self.id)" 28 | } 29 | } 30 | 31 | // ------------------------------------------------------------------------- 32 | 33 | var height: CGFloat { 34 | get { 35 | return _height 36 | } 37 | } 38 | 39 | // ------------------------------------------------------------------------- 40 | // MARK: - Actions 41 | 42 | override func hit() { 43 | if self.state != .alive { 44 | return 45 | } 46 | 47 | self.state = .died 48 | 49 | let action1 = SCNAction.moveBy(x: 0, y: -3, z: 0, duration: 0.15) 50 | _node.runAction(action1) 51 | 52 | let action2 = SCNAction.rotateBy(x: degreesToRadians(value: 30), y: 0, z: degreesToRadians(value: 15), duration: 0.3) 53 | _node.runAction(action2) 54 | 55 | if let emitter = SCNParticleSystem(named: "art.scnassets/fire.scnp", inDirectory: nil) { 56 | self.addParticleSystem(emitter) 57 | } 58 | } 59 | 60 | // ------------------------------------------------------------------------- 61 | // MARK: - Initialisation 62 | 63 | override init() { 64 | super.init() 65 | 66 | // Use some randomness in height, width and color 67 | let material = SCNMaterial() 68 | material.diffuse.contents = UIColor.random(list: UIGreenColorList) 69 | 70 | let width = RBRandom.cgFloat(4.0, 9.0) 71 | _height = RBRandom.cgFloat(15.0, 25) 72 | 73 | var geometry: SCNGeometry! 74 | let rnd = RBRandom.integer(1, 3) 75 | if rnd == 1 { 76 | geometry = SCNBox(width: width, height: _height, length: 2.0, chamferRadius: 0.0) 77 | } 78 | else if rnd == 2 { 79 | geometry = SCNCylinder(radius: width, height: _height) 80 | } 81 | else { 82 | geometry = SCNCone(topRadius: 0.0, bottomRadius: width, height: _height) 83 | } 84 | 85 | geometry.materials = [material] 86 | 87 | _node = SCNNode(geometry: geometry) 88 | _node.name = "handicap" 89 | _node.physicsBody = SCNPhysicsBody(type: .kinematic, shape: nil) 90 | _node.physicsBody?.categoryBitMask = Game.Physics.Categories.enemy 91 | self.addChildNode(_node) 92 | 93 | self.state = .alive 94 | } 95 | 96 | // ------------------------------------------------------------------------- 97 | 98 | required init(coder: NSCoder) { 99 | fatalError("Not yet implemented") 100 | } 101 | 102 | // ------------------------------------------------------------------------- 103 | 104 | } 105 | 106 | 107 | -------------------------------------------------------------------------------- /partIV/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UIStatusBarHidden 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationLandscapeLeft 34 | UIInterfaceOrientationLandscapeRight 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /partIV/Player.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Player.swift 3 | // 4 | // Part 4 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 12/07/16. 8 | // Copyright © 2016 Roger Boesch. All rights reserved. 9 | // 10 | // Part 4: Now the player objects is derrived from GameObject instead of SCNNode 11 | // 12 | 13 | import SceneKit 14 | 15 | // ----------------------------------------------------------------------------- 16 | 17 | class Player : GameObject { 18 | private let lookAtForwardPosition = SCNVector3Make(0.0, -1.0, 6.0) 19 | private let cameraFowardPosition = SCNVector3(x: 0, y: 1.0, z: -5) 20 | 21 | private var _lookAtNode: SCNNode? 22 | private var _cameraNode: SCNNode? 23 | private var _playerNode: SCNNode? 24 | 25 | // ------------------------------------------------------------------------- 26 | // MARK: - Propertiues 27 | 28 | override var description: String { 29 | get { 30 | return "player \(self.id)" 31 | } 32 | } 33 | 34 | // ------------------------------------------------------------------------- 35 | // MARK: - Collision handling 36 | 37 | override func collision(with object: GameObject, level: GameLevel) { 38 | if let ring = object as? Ring { 39 | if ring.state != .alive { 40 | return 41 | } 42 | 43 | level.flyTrough(ring) 44 | ring.hit() 45 | 46 | self.roll() 47 | } 48 | else if let handicap = object as? Handicap { 49 | level.touchedHandicap(handicap) 50 | handicap.hit() 51 | 52 | self.die() 53 | } 54 | } 55 | 56 | // ------------------------------------------------------------------------- 57 | // MARK: - Effects 58 | 59 | func die() { 60 | if self.state != .alive { 61 | return 62 | } 63 | 64 | self.state = .died 65 | _playerNode?.isHidden = true 66 | 67 | self.removeAllActions() 68 | _playerNode?.removeAllActions() 69 | } 70 | 71 | // ------------------------------------------------------------------------- 72 | 73 | func roll() { 74 | // Part 3: An easy effect we use, whenever we fly trough a ring 75 | let rotateAction = SCNAction.rotateBy(x: 0, y: 0, z: -degreesToRadians(value: 360.0), duration: 0.5) 76 | _playerNode!.runAction(rotateAction) 77 | } 78 | 79 | // ------------------------------------------------------------------------- 80 | // MARK: - Camera adjustment 81 | 82 | private func toggleCamera() { 83 | var position = _cameraNode!.position 84 | 85 | if position.x < 0 { 86 | position.x = 0.5 87 | } 88 | else { 89 | position.x = -0.5 90 | } 91 | 92 | SCNTransaction.begin() 93 | SCNTransaction.animationDuration = 1.0 94 | 95 | _cameraNode?.position = position 96 | 97 | SCNTransaction.commit() 98 | } 99 | 100 | // ------------------------------------------------------------------------- 101 | // MARK: - Plane movements 102 | 103 | func moveLeft() { 104 | let moveAction = SCNAction.moveBy(x: Game.Player.moveOffset, y: 0.0, z: 0, duration: 0.5) 105 | self.runAction(moveAction, forKey: "moveLeftRight") 106 | 107 | let rotateAction1 = SCNAction.rotateBy(x: 0, y: 0, z: -degreesToRadians(value: 15.0), duration: 0.25) 108 | let rotateAction2 = SCNAction.rotateBy(x: 0, y: 0, z: degreesToRadians(value: 15.0), duration: 0.25) 109 | 110 | _playerNode!.runAction(SCNAction.sequence([rotateAction1, rotateAction2])) 111 | 112 | toggleCamera() 113 | } 114 | 115 | // ------------------------------------------------------------------------- 116 | 117 | func moveRight() { 118 | let moveAction = SCNAction.moveBy(x: -Game.Player.moveOffset, y: 0.0, z: 0, duration: 0.5) 119 | self.runAction(moveAction, forKey: "moveLeftRight") 120 | 121 | let rotateAction1 = SCNAction.rotateBy(x: 0, y: 0, z: degreesToRadians(value: 15.0), duration: 0.25) 122 | let rotateAction2 = SCNAction.rotateBy(x: 0, y: 0, z: -degreesToRadians(value: 15.0), duration: 0.25) 123 | 124 | _playerNode!.runAction(SCNAction.sequence([rotateAction1, rotateAction2])) 125 | 126 | toggleCamera() 127 | } 128 | 129 | // ------------------------------------------------------------------------- 130 | // MARK: - Initialisation 131 | 132 | override init() { 133 | super.init() 134 | 135 | // Create player node 136 | let scene = SCNScene(named: "art.scnassets/ship.scn") 137 | if (scene == nil) { 138 | fatalError("Scene not loaded") 139 | } 140 | 141 | _playerNode = scene!.rootNode.childNode(withName: "ship", recursively: true) 142 | _playerNode?.name = "player" 143 | 144 | if (_playerNode == nil) { 145 | fatalError("Ship node not found") 146 | } 147 | 148 | _playerNode!.scale = SCNVector3(x: 0.25, y: 0.25, z: 0.25) 149 | self.addChildNode(_playerNode!) 150 | 151 | // Contact box 152 | // Part 3: Instead of use the plane itself we add a collision node to the player object 153 | let boxMaterial = SCNMaterial() 154 | boxMaterial.diffuse.contents = UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 0.0) 155 | 156 | let box = SCNBox(width: 2.0, height: 1.0, length: 1.0, chamferRadius: 0.0) 157 | box.materials = [boxMaterial] 158 | let contactBox = SCNNode(geometry: box) 159 | contactBox.name = "player" 160 | contactBox.physicsBody = SCNPhysicsBody(type: .kinematic, shape: nil) 161 | contactBox.physicsBody?.categoryBitMask = Game.Physics.Categories.player 162 | contactBox.physicsBody!.contactTestBitMask = Game.Physics.Categories.ring | Game.Physics.Categories.enemy 163 | self.addChildNode(contactBox) 164 | 165 | // Look at Node 166 | _lookAtNode = SCNNode() 167 | _lookAtNode!.position = lookAtForwardPosition 168 | addChildNode(_lookAtNode!) 169 | 170 | // Camera Node 171 | _cameraNode = SCNNode() 172 | _cameraNode!.camera = SCNCamera() 173 | _cameraNode!.position = cameraFowardPosition 174 | _cameraNode!.camera!.zNear = 0.1 175 | _cameraNode!.camera!.zFar = 200 176 | self.addChildNode(_cameraNode!) 177 | 178 | // Link them 179 | let constraint1 = SCNLookAtConstraint(target: _lookAtNode) 180 | constraint1.isGimbalLockEnabled = true 181 | _cameraNode!.constraints = [constraint1] 182 | 183 | // Create a spotlight at the player 184 | let spotLight = SCNLight() 185 | spotLight.type = SCNLight.LightType.spot 186 | spotLight.spotInnerAngle = 40.0 187 | spotLight.spotOuterAngle = 80.0 188 | spotLight.castsShadow = true 189 | spotLight.color = UIColor.white 190 | let spotLightNode = SCNNode() 191 | spotLightNode.light = spotLight 192 | spotLightNode.position = SCNVector3(x: 1.0, y: 5.0, z: -2.0) 193 | self.addChildNode(spotLightNode) 194 | 195 | // Link it 196 | let constraint2 = SCNLookAtConstraint(target: self) 197 | constraint2.isGimbalLockEnabled = true 198 | spotLightNode.constraints = [constraint2] 199 | 200 | // Create additional omni light 201 | let lightNode = SCNNode() 202 | lightNode.light = SCNLight() 203 | lightNode.light!.type = SCNLight.LightType.omni 204 | lightNode.light!.color = UIColor.darkGray 205 | lightNode.position = SCNVector3(x: 0, y: 10.00, z: -2) 206 | self.addChildNode(lightNode) 207 | } 208 | 209 | // ------------------------------------------------------------------------- 210 | 211 | required init(coder: NSCoder) { 212 | fatalError("Not yet implemented") 213 | } 214 | 215 | // ------------------------------------------------------------------------- 216 | 217 | } 218 | -------------------------------------------------------------------------------- /partIV/Ring.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Ring.swift 3 | // 4 | // Part 4 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 13/12/17. 8 | // Copyright © 2017 Roger Boesch. All rights reserved. 9 | // 10 | // Part 4: Now the ring object is derrived from GameObject instead of SCNNode 11 | // 12 | import SceneKit 13 | 14 | // ----------------------------------------------------------------------------- 15 | 16 | class Ring : GameObject { 17 | 18 | // ------------------------------------------------------------------------- 19 | // MARK: - Propertiues 20 | 21 | override var description: String { 22 | get { 23 | return "ring \(self.id)" 24 | } 25 | } 26 | 27 | // ------------------------------------------------------------------------- 28 | // MARK: - Actions 29 | 30 | override func hit() { 31 | self.state = .died 32 | } 33 | 34 | // ------------------------------------------------------------------------- 35 | // MARK: - Initialisation 36 | 37 | override init() { 38 | super.init() 39 | 40 | let ringMaterial = SCNMaterial() 41 | ringMaterial.diffuse.contents = UIImage(named: "art.scnassets/ringTexture") 42 | ringMaterial.diffuse.wrapS = .repeat 43 | ringMaterial.diffuse.wrapT = .repeat 44 | ringMaterial.diffuse.contentsTransform = SCNMatrix4MakeScale(3, 1, 1) 45 | 46 | let ring = SCNTorus(ringRadius: 5.0, pipeRadius: 0.5) 47 | ring.materials = [ringMaterial] 48 | let ringNode = SCNNode(geometry: ring) 49 | self.addChildNode(ringNode) 50 | 51 | ringNode.eulerAngles = SCNVector3(degreesToRadians(value: 90), 0, 0) 52 | 53 | let action = SCNAction.rotateBy(x: 0, y: 0, z: degreesToRadians(value: 360), duration: 3.0) 54 | ringNode.runAction(SCNAction.repeatForever(action)) 55 | 56 | // Contact box 57 | let boxMaterial = SCNMaterial() 58 | boxMaterial.diffuse.contents = UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 0.0) 59 | 60 | let box = SCNBox(width: 10.0, height: 10.0, length: 1.0, chamferRadius: 0.0) 61 | box.materials = [boxMaterial] 62 | let contactBox = SCNNode(geometry: box) 63 | contactBox.name = "ring" 64 | contactBox.physicsBody = SCNPhysicsBody(type: .kinematic, shape: nil) 65 | contactBox.physicsBody?.categoryBitMask = Game.Physics.Categories.ring 66 | self.addChildNode(contactBox) 67 | 68 | self.state = .alive 69 | } 70 | 71 | // ------------------------------------------------------------------------- 72 | 73 | required init(coder: NSCoder) { 74 | fatalError("Not yet implemented") 75 | } 76 | 77 | // ------------------------------------------------------------------------- 78 | 79 | } 80 | 81 | -------------------------------------------------------------------------------- /partV/GameObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameObject.swift 3 | // Part 5 of the SceneKit Tutorial Series 'From Zero to Hero' at: 4 | // https://rogerboesch.github.io/ 5 | // 6 | // Created by Roger Boesch on 31.10.17. 7 | // Copyright © 2017 Roger Boesch. All rights reserved. 8 | // 9 | // New in Part 4: This is a bigger change. While the game becomes more and more 10 | // various game objects, it's common to encapsulate general things 11 | // in a base class. 12 | // The most important is the state which is described in detail in the tutorial. 13 | 14 | import SceneKit 15 | 16 | enum GameObjecState { 17 | case initialized, alive, died, stopped 18 | } 19 | 20 | class GameObject : SCNNode { 21 | private static var count: Int = 0 22 | static var level: GameLevel? 23 | 24 | private var _internalID: Int = 0 25 | private var _tag = 0 26 | private var _state = GameObjecState.initialized 27 | 28 | // ------------------------------------------------------------------------- 29 | // MARK: - Propertiues 30 | 31 | override var description: String { 32 | get { 33 | return "game object \(self.id)" 34 | } 35 | } 36 | 37 | // ------------------------------------------------------------------------- 38 | 39 | var id: Int { 40 | return _internalID 41 | } 42 | 43 | // ------------------------------------------------------------------------- 44 | 45 | var tag: Int { 46 | get { 47 | return _tag 48 | } 49 | set(value) { 50 | _tag = value 51 | } 52 | } 53 | 54 | // ------------------------------------------------------------------------- 55 | 56 | var state: GameObjecState { 57 | get { 58 | return _state 59 | } 60 | set(value) { 61 | rbDebug("State of \(self) changed from \(_state) to \(value)") 62 | _state = value 63 | } 64 | } 65 | 66 | // ------------------------------------------------------------------------- 67 | // MARK: - Actions 68 | 69 | func hit() {} // Object get hit by another object 70 | 71 | // ------------------------------------------------------------------------- 72 | 73 | func stop() { 74 | // Stop object (release all) 75 | self.state = .stopped 76 | stopAllActions(self) 77 | } 78 | 79 | // ------------------------------------------------------------------------- 80 | // MARK: - Game loop 81 | 82 | func update(atTime time: TimeInterval, level: GameLevel) {} 83 | 84 | // ------------------------------------------------------------------------- 85 | // MARK: - Collision handling 86 | 87 | func collision(with object: GameObject, level: GameLevel) {} 88 | 89 | // ------------------------------------------------------------------------- 90 | // MARK: - Helper methods 91 | 92 | func stopAllActions(_ node: SCNNode) { 93 | // It's important to stop all actions before we remove a game object 94 | // Otherwise they continue to run and result in difficult side effects. 95 | node.removeAllActions() 96 | 97 | for child in node.childNodes { 98 | child.removeAllActions() 99 | stopAllActions(child) 100 | } 101 | } 102 | 103 | // ------------------------------------------------------------------------- 104 | // MARK: - Initialisation 105 | 106 | override init() { 107 | super.init() 108 | 109 | GameObject.count += 1 110 | _internalID = GameObject.count 111 | } 112 | 113 | // ------------------------------------------------------------------------- 114 | 115 | required init?(coder aDecoder: NSCoder) { 116 | fatalError("init(coder aDecoder: NSCoder) is not implemented") 117 | } 118 | 119 | // ------------------------------------------------------------------------- 120 | } 121 | 122 | 123 | -------------------------------------------------------------------------------- /partV/GameSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameSettings.swift 3 | // 4 | // Part 5 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 12/10/16. 8 | // Copyright © 2016 Roger Boesch. All rights reserved. 9 | // 10 | // Structs are a very good way to structure the different application settings. 11 | // Like that they are easy to read and changeable at one place 12 | // 13 | 14 | import UIKit 15 | 16 | struct Game { 17 | 18 | struct Level { 19 | static let numberOfRings: Int = 10 // Number of rings in level 20 | 21 | static let width: CGFloat = 320 // Terrain width 22 | static let length: CGFloat = 840 // Terraiun length 23 | static let start: CGFloat = 100 // Start of player 24 | 25 | struct Fog { 26 | static let start: CGFloat = 20 // Begin of fog 27 | static let end: CGFloat = 300 // End of fog 28 | } 29 | } 30 | 31 | struct Player { 32 | static let actionTime: TimeInterval = 10.0 // Time used for move actions 33 | static let leftRightMoveDistance: CGFloat = 6.0 // Left/Right distance take in time 34 | static let upDownMoveDistance: CGFloat = 4.0 // Up/Down distance take in time 35 | static let speedDistance: CGFloat = 100.0 // Forward distance take in time 36 | 37 | static let upDownAngle: CGFloat = 5.0 // Angle when fly up or down 38 | static let leftRightAngle: CGFloat = 30.0 // Angle when fly left or right 39 | 40 | static let minimumHeight: Float = 4.0 // Minimum height 41 | static let maximumHeight: Float = 30.0 // Maximum height 42 | static let maximumLeft: Float = 360.0 // Maximum left 43 | static let maximumRight: Float = 30.0 // Maximum right 44 | } 45 | 46 | struct Objects { 47 | static let offset: CGFloat = 15 // Space between objects 48 | } 49 | 50 | struct Physics { 51 | 52 | // Category bits used for physics handling 53 | struct Categories { 54 | static let player: Int = 0b00000001 55 | static let ring: Int = 0b00000010 56 | static let enemy: Int = 0b00000100 57 | } 58 | } 59 | 60 | struct Motion { 61 | static let threshold: Double = 0.2 // Minium threshold of accelerometer 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /partV/GameViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameViewController.swift 3 | // 4 | // Part 5 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 12/07/16. 8 | // Copyright © 2016 Roger Boesch. All rights reserved. 9 | // 10 | 11 | import UIKit 12 | import SceneKit 13 | import SpriteKit 14 | import CoreMotion 15 | import GameController 16 | 17 | class GameViewController: UIViewController, SCNSceneRendererDelegate { 18 | private var _sceneView: SCNView! 19 | private var _level: GameLevel! 20 | private var _hud: HUD! 21 | 22 | // New in Part 5: Use CoreMotion to fly the plane 23 | private var _motionManager = CMMotionManager() 24 | private var _startAttitude: CMAttitude? // Start attitude 25 | private var _currentAttitude: CMAttitude? // Current attitude 26 | 27 | // ------------------------------------------------------------------------- 28 | // MARK: - Properties 29 | 30 | var sceneView: SCNView { 31 | return _sceneView 32 | } 33 | 34 | // ------------------------------------------------------------------------- 35 | 36 | var hud: HUD { 37 | return _hud 38 | } 39 | 40 | // ------------------------------------------------------------------------- 41 | // MARK: - Render delegate (New in Part 4) 42 | 43 | func renderer(_ renderer: SCNSceneRenderer, didSimulatePhysicsAtTime time: TimeInterval) { 44 | guard _level != nil else { return } 45 | 46 | _level.update(atTime: time) 47 | renderer.loops = true 48 | } 49 | 50 | // ------------------------------------------------------------------------- 51 | // MARK: - Gesture recognoizers 52 | 53 | @objc private func handleTap(_ gestureRecognize: UITapGestureRecognizer) { 54 | // New in Part 4: A tap is used to restart the level (see tutorial) 55 | if _level.state == .loose || _level.state == .win { 56 | _level.stop() 57 | _level = nil 58 | 59 | DispatchQueue.main.async { 60 | // Create things in main thread 61 | 62 | let level = GameLevel() 63 | level.create() 64 | 65 | level.hud = self.hud 66 | self.hud.reset() 67 | 68 | self.sceneView.scene = level 69 | self._level = level 70 | 71 | self.hud.message("READY?", information: "- Touch screen to start -") 72 | } 73 | } 74 | // New in Part 5: A tap is used to start the level (see tutorial) 75 | else if _level.state == .ready { 76 | _startAttitude = _currentAttitude 77 | _level.start() 78 | } 79 | } 80 | 81 | // ------------------------------------------------------------------------- 82 | 83 | @objc private func handleSwipe(_ gestureRecognize: UISwipeGestureRecognizer) { 84 | if _level.state != .play { 85 | return 86 | } 87 | 88 | if (gestureRecognize.direction == .left) { 89 | _level!.swipeLeft() 90 | } 91 | else if (gestureRecognize.direction == .right) { 92 | _level!.swipeRight() 93 | } 94 | else if (gestureRecognize.direction == .down) { 95 | _level!.swipeDown() 96 | } 97 | else if (gestureRecognize.direction == .up) { 98 | _level!.swipeUp() 99 | } 100 | } 101 | 102 | // ------------------------------------------------------------------------- 103 | // MARK: - Motion handling 104 | 105 | private func motionDidChange(data: CMDeviceMotion) { 106 | _currentAttitude = data.attitude 107 | 108 | guard _level != nil, _level?.state == .play else { return } 109 | 110 | // Up/Down 111 | let diff1 = _startAttitude!.roll - _currentAttitude!.roll 112 | 113 | if (diff1 >= Game.Motion.threshold) { 114 | _level!.motionMoveUp() 115 | } 116 | else if (diff1 <= -Game.Motion.threshold) { 117 | _level!.motionMoveDown() 118 | } 119 | else { 120 | _level!.motionStopMovingUpDown() 121 | } 122 | 123 | let diff2 = _startAttitude!.pitch - _currentAttitude!.pitch 124 | 125 | if (diff2 >= Game.Motion.threshold) { 126 | _level!.motionMoveLeft() 127 | } 128 | else if (diff2 <= -Game.Motion.threshold) { 129 | _level!.motionMoveRight() 130 | } 131 | else { 132 | _level!.motionStopMovingLeftRight() 133 | } 134 | } 135 | 136 | // ------------------------------------------------------------------------- 137 | 138 | private func setupMotionHandler() { 139 | if (GCController.controllers().count == 0 && _motionManager.isAccelerometerAvailable) { 140 | _motionManager.accelerometerUpdateInterval = 1/60.0 141 | 142 | _motionManager.startDeviceMotionUpdates(to: OperationQueue.main, withHandler: {(data, error) in 143 | self.motionDidChange(data: data!) 144 | }) 145 | } 146 | } 147 | 148 | // ------------------------------------------------------------------------- 149 | // MARK: - ViewController life cycle 150 | 151 | override func viewWillAppear(_ animated: Bool) { 152 | super.viewWillAppear(animated) 153 | 154 | // Part 3: HUD is created and assigned to view and game level 155 | _hud = HUD(size: self.view.bounds.size) 156 | _level.hud = _hud 157 | _sceneView.overlaySKScene = _hud.scene 158 | 159 | self.hud.message("READY?", information: "- Touch screen to start -") 160 | } 161 | 162 | // ------------------------------------------------------------------------- 163 | 164 | override func viewDidLoad() { 165 | super.viewDidLoad() 166 | 167 | _level = GameLevel() 168 | _level.create() 169 | 170 | _sceneView = SCNView() 171 | _sceneView.scene = _level 172 | _sceneView.allowsCameraControl = false 173 | _sceneView.showsStatistics = false 174 | _sceneView.backgroundColor = UIColor.black 175 | _sceneView.delegate = self 176 | 177 | self.view = _sceneView 178 | 179 | setupMotionHandler() 180 | 181 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap)) 182 | _sceneView!.addGestureRecognizer(tapGesture) 183 | 184 | let swipeLeftGesture = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipe)) 185 | swipeLeftGesture.direction = .left 186 | _sceneView!.addGestureRecognizer(swipeLeftGesture) 187 | 188 | let swipeRightGesture = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipe)) 189 | swipeRightGesture.direction = .right 190 | _sceneView!.addGestureRecognizer(swipeRightGesture) 191 | 192 | let swipeDownGesture = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipe)) 193 | swipeDownGesture.direction = .down 194 | _sceneView!.addGestureRecognizer(swipeDownGesture) 195 | 196 | let swipeUpGesture = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipe)) 197 | swipeUpGesture.direction = .up 198 | _sceneView!.addGestureRecognizer(swipeUpGesture) 199 | } 200 | 201 | // ------------------------------------------------------------------------- 202 | 203 | override func didReceiveMemoryWarning() { 204 | super.didReceiveMemoryWarning() 205 | } 206 | 207 | // ------------------------------------------------------------------------- 208 | 209 | } 210 | -------------------------------------------------------------------------------- /partV/HUD.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HUD.swift 3 | // 4 | // Part 5 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 11.11.17. 8 | // Copyright © 2017 Roger Boesch. All rights reserved. 9 | // 10 | 11 | import SpriteKit 12 | 13 | class HUD { 14 | private var _scene: SKScene! 15 | private let _rings = SKLabelNode(text: "") 16 | private let _missedRings = SKLabelNode(text: "") 17 | private let _message = SKLabelNode(text: "") 18 | private let _info = SKLabelNode(text: "") 19 | 20 | // ------------------------------------------------------------------------- 21 | // MARK: - Properties 22 | 23 | var scene: SKScene { 24 | get { 25 | return _scene 26 | } 27 | } 28 | 29 | // ------------------------------------------------------------------------- 30 | 31 | var rings: Int { 32 | get { 33 | return 0 34 | } 35 | set(value) { 36 | if value == 1 { 37 | _rings.text = String(format: "%d RING", value) 38 | } 39 | else { 40 | _rings.text = String(format: "%d RINGS", value) 41 | } 42 | 43 | // New in Part 4: Animated HUD informations (check RB+SKAction.swift for details) 44 | let scaling: CGFloat = 3 45 | let action = SKAction.zoomWithNode(_rings, amount: CGPoint.make(scaling, scaling), oscillations: 1, duration: 0.5) 46 | _rings.run(action) 47 | } 48 | } 49 | 50 | // ------------------------------------------------------------------------- 51 | 52 | var missedRings: Int { 53 | get { 54 | return 0 55 | } 56 | set(value) { 57 | _missedRings.text = String(format: "%d MISSED", value) 58 | 59 | if value > 0 { 60 | _missedRings.isHidden = false 61 | _missedRings.fontColor = UIColor.red 62 | } 63 | else { 64 | _missedRings.isHidden = true 65 | } 66 | 67 | let scaling: CGFloat = 3 68 | let action = SKAction.zoomWithNode(_missedRings, amount: CGPoint.make(scaling, scaling), oscillations: 1, duration: 0.5) 69 | _missedRings.run(action) 70 | } 71 | } 72 | 73 | // ------------------------------------------------------------------------- 74 | 75 | func message(_ str: String, information: String? = nil) { 76 | // New in Part 4: Used for game over and win messages 77 | _message.text = str 78 | _message.isHidden = false 79 | 80 | let scaling: CGFloat = 10 81 | let action = SKAction.zoomWithNode(_message, amount: CGPoint.make(scaling, scaling), oscillations: 1, duration: 0.5) 82 | _message.run(action) 83 | 84 | if information != nil { 85 | info(information!) 86 | } 87 | } 88 | 89 | // ------------------------------------------------------------------------- 90 | 91 | func info(_ str: String) { 92 | // New in Part 4: Uses for additional info when show messages 93 | 94 | _info.text = str 95 | _info.isHidden = false 96 | 97 | let scaling: CGFloat = 2 98 | let action = SKAction.zoomWithNode(_info, amount: CGPoint.make(scaling, scaling), oscillations: 1, duration: 0.5) 99 | _info.run(action) 100 | } 101 | 102 | // ------------------------------------------------------------------------- 103 | 104 | func reset() { 105 | // New in Part 4: Reset is needed whenever start the level 106 | 107 | _message.text = "" 108 | _message.isHidden = true 109 | 110 | _info.text = "" 111 | _info.isHidden = true 112 | 113 | rings = 0 114 | missedRings = 0 115 | } 116 | 117 | // ------------------------------------------------------------------------- 118 | // MARK: - Initialisation 119 | 120 | init(size: CGSize) { 121 | _scene = SKScene(size: size) 122 | 123 | _rings.position = CGPoint(x: 40, y: size.height-50) 124 | _rings.horizontalAlignmentMode = .left 125 | _rings.fontName = "MarkerFelt-Wide" 126 | _rings.fontSize = 30 127 | _rings.fontColor = UIColor.white 128 | _scene.addChild(_rings) 129 | 130 | _missedRings.position = CGPoint(x: size.width-40, y: size.height-50) 131 | _missedRings.horizontalAlignmentMode = .right 132 | _missedRings.fontName = "MarkerFelt-Wide" 133 | _missedRings.fontSize = 30 134 | _missedRings.fontColor = UIColor.white 135 | _scene.addChild(_missedRings) 136 | 137 | _message.position = CGPoint(x: size.width/2, y: size.height/2) 138 | _message.horizontalAlignmentMode = .center 139 | _message.fontName = "MarkerFelt-Wide" 140 | _message.fontSize = 60 141 | _message.fontColor = UIColor.white 142 | _message.isHidden = true 143 | _scene.addChild(_message) 144 | 145 | _info.position = CGPoint(x: size.width/2, y: size.height/2-40) 146 | _info.horizontalAlignmentMode = .center 147 | _info.fontName = "MarkerFelt-Wide" 148 | _info.fontSize = 20 149 | _info.fontColor = UIColor.white 150 | _info.isHidden = true 151 | _scene.addChild(_info) 152 | 153 | reset() 154 | } 155 | 156 | // ------------------------------------------------------------------------- 157 | 158 | required init?(coder aDecoder: NSCoder) { 159 | fatalError("init?(coder aDecoder: NSCoder) not implemented") 160 | } 161 | 162 | // ------------------------------------------------------------------------- 163 | } 164 | -------------------------------------------------------------------------------- /partV/Handicap.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Handicap.swift 3 | // 4 | // Part 5 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 13/12/17. 8 | // Copyright © 2017 Roger Boesch. All rights reserved. 9 | // 10 | 11 | import SceneKit 12 | 13 | // ----------------------------------------------------------------------------- 14 | 15 | class Handicap : GameObject { 16 | private var _node: SCNNode! 17 | private var _height: CGFloat = 0 18 | 19 | // ------------------------------------------------------------------------- 20 | // MARK: - Propertiues 21 | 22 | override var description: String { 23 | get { 24 | return "handicap \(self.id)" 25 | } 26 | } 27 | 28 | // ------------------------------------------------------------------------- 29 | 30 | var height: CGFloat { 31 | get { 32 | return _height 33 | } 34 | } 35 | 36 | // ------------------------------------------------------------------------- 37 | // MARK: - Actions 38 | 39 | override func hit() { 40 | if self.state != .alive { 41 | return 42 | } 43 | 44 | self.state = .died 45 | 46 | let action1 = SCNAction.moveBy(x: 0, y: -3, z: 0, duration: 0.15) 47 | _node.runAction(action1) 48 | 49 | let action2 = SCNAction.rotateBy(x: degreesToRadians(value: 30), y: 0, z: degreesToRadians(value: 15), duration: 0.3) 50 | _node.runAction(action2) 51 | 52 | if let emitter = SCNParticleSystem(named: "art.scnassets/fire.scnp", inDirectory: nil) { 53 | self.addParticleSystem(emitter) 54 | } 55 | } 56 | 57 | // ------------------------------------------------------------------------- 58 | // MARK: - Initialisation 59 | 60 | override init() { 61 | super.init() 62 | 63 | // Use some randomness in height, width and color 64 | let material = SCNMaterial() 65 | material.diffuse.contents = UIColor.random(list: UIGreenColorList) 66 | 67 | let width = RBRandom.cgFloat(4.0, 9.0) 68 | _height = RBRandom.cgFloat(15.0, 25) 69 | 70 | var geometry: SCNGeometry! 71 | let rnd = RBRandom.integer(1, 3) 72 | if rnd == 1 { 73 | geometry = SCNBox(width: width, height: _height, length: 2.0, chamferRadius: 0.0) 74 | } 75 | else if rnd == 2 { 76 | geometry = SCNCylinder(radius: width, height: _height) 77 | } 78 | else { 79 | geometry = SCNCone(topRadius: 0.0, bottomRadius: width, height: _height) 80 | } 81 | 82 | geometry.materials = [material] 83 | 84 | _node = SCNNode(geometry: geometry) 85 | _node.name = "handicap" 86 | _node.physicsBody = SCNPhysicsBody(type: .kinematic, shape: nil) 87 | _node.physicsBody?.categoryBitMask = Game.Physics.Categories.enemy 88 | self.addChildNode(_node) 89 | 90 | self.state = .alive 91 | } 92 | 93 | // ------------------------------------------------------------------------- 94 | 95 | required init(coder: NSCoder) { 96 | fatalError("Not yet implemented") 97 | } 98 | 99 | // ------------------------------------------------------------------------- 100 | 101 | } 102 | 103 | 104 | -------------------------------------------------------------------------------- /partV/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UIStatusBarHidden 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationLandscapeLeft 34 | UIInterfaceOrientationLandscapeRight 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /partV/Ring.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Ring.swift 3 | // 4 | // Part 5 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 13/12/17. 8 | // Copyright © 2017 Roger Boesch. All rights reserved. 9 | // 10 | 11 | import SceneKit 12 | 13 | // ----------------------------------------------------------------------------- 14 | 15 | class Ring : GameObject { 16 | private var _number: SCNNode! 17 | 18 | // ------------------------------------------------------------------------- 19 | // MARK: - Propertiues 20 | 21 | override var description: String { 22 | get { 23 | return "ring \(self.id)" 24 | } 25 | } 26 | 27 | // ------------------------------------------------------------------------- 28 | // MARK: - Actions 29 | 30 | override func hit() { 31 | self.state = .died 32 | 33 | _number.isHidden = false 34 | _number.removeAllActions() 35 | } 36 | 37 | // ------------------------------------------------------------------------- 38 | // MARK: - Initialisation 39 | 40 | init(number: Int) { 41 | super.init() 42 | 43 | let ringMaterial = SCNMaterial() 44 | ringMaterial.diffuse.contents = UIImage(named: "art.scnassets/ringTexture") 45 | ringMaterial.diffuse.wrapS = .repeat 46 | ringMaterial.diffuse.wrapT = .repeat 47 | ringMaterial.diffuse.contentsTransform = SCNMatrix4MakeScale(3, 1, 1) 48 | 49 | let ring = SCNTorus(ringRadius: 5.0, pipeRadius: 0.5) 50 | ring.materials = [ringMaterial] 51 | let ringNode = SCNNode(geometry: ring) 52 | self.addChildNode(ringNode) 53 | 54 | ringNode.eulerAngles = SCNVector3(degreesToRadians(value: 90), 0, 0) 55 | 56 | let action1 = SCNAction.rotateBy(x: 0, y: 0, z: degreesToRadians(value: 360), duration: 3.0) 57 | ringNode.runAction(SCNAction.repeatForever(action1)) 58 | 59 | // Contact box 60 | let boxMaterial = SCNMaterial() 61 | boxMaterial.diffuse.contents = UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 0.0) 62 | 63 | let box = SCNBox(width: 10.0, height: 10.0, length: 1.0, chamferRadius: 0.0) 64 | box.materials = [boxMaterial] 65 | let contactBox = SCNNode(geometry: box) 66 | contactBox.name = "ring" 67 | contactBox.physicsBody = SCNPhysicsBody(type: .kinematic, shape: nil) 68 | contactBox.physicsBody?.categoryBitMask = Game.Physics.Categories.ring 69 | self.addChildNode(contactBox) 70 | 71 | // New in Part 5: Add a number to the ring 72 | let numberMaterial = SCNMaterial() 73 | numberMaterial.diffuse.contents = UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0) 74 | 75 | let text = SCNText(string: "\(number)", extrusionDepth: 0.01) 76 | text.materials = [numberMaterial] 77 | _number = SCNNode(geometry: text) 78 | _number.eulerAngles = SCNVector3(0, degreesToRadians(value: 180), 0) 79 | _number.scale = SCNVector3(0.7, 0.7, 0.7) 80 | _number.position = SCNVector3(2.0, 4.5, -0.7) 81 | self.addChildNode(_number) 82 | 83 | let action2 = SCNAction.scale(to: 0.7, duration: 1.0) 84 | let action3 = SCNAction.scale(to: 1.0, duration: 1.0) 85 | let action4 = SCNAction.sequence([action2, action3]) 86 | _number.runAction(SCNAction.repeatForever(action4)) 87 | 88 | self.state = .alive 89 | } 90 | 91 | // ------------------------------------------------------------------------- 92 | 93 | required init(coder: NSCoder) { 94 | fatalError("Not yet implemented") 95 | } 96 | 97 | // ------------------------------------------------------------------------- 98 | 99 | } 100 | 101 | -------------------------------------------------------------------------------- /partVI/Bullet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bullet.swift 3 | // 4 | // Part 6 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 13/12/17. 8 | // Copyright © 2017 Roger Boesch. All rights reserved. 9 | // 10 | 11 | import UIKit 12 | import SceneKit 13 | 14 | class Bullet : GameObject { 15 | private static let speedDistance: CGFloat = 80.0 // Distance taken in time 16 | private static let lifeTime: TimeInterval = 3.0 // Life time of a bullet in s 17 | 18 | private var _enemy = false // Bullet show by player/enemy 19 | private var _speed: CGFloat = 2.0 // Speed of th ebullet 20 | 21 | private var _bulletNode: SCNNode! 22 | 23 | // ------------------------------------------------------------------------- 24 | // MARK: - Getter/Setter 25 | 26 | var enemy: Bool { 27 | get { 28 | return _enemy 29 | } 30 | set(value) { 31 | _enemy = value 32 | 33 | if (_enemy) { 34 | _bulletNode.physicsBody = SCNPhysicsBody(type: .kinematic, shape: nil) 35 | _bulletNode.physicsBody!.categoryBitMask = Game.Physics.Categories.bullet 36 | _bulletNode.physicsBody!.contactTestBitMask = Game.Physics.Categories.player 37 | } 38 | else { 39 | _bulletNode.physicsBody = SCNPhysicsBody(type: .kinematic, shape: nil) 40 | _bulletNode.physicsBody!.categoryBitMask = Game.Physics.Categories.bullet 41 | _bulletNode.physicsBody!.contactTestBitMask = Game.Physics.Categories.enemy 42 | } 43 | } 44 | } 45 | 46 | // ------------------------------------------------------------------------- 47 | 48 | override var description: String { 49 | get { 50 | if (self.enemy) { 51 | return "enemy bullet \(self.id)" 52 | } 53 | else { 54 | return "player bullet \(self.id)" 55 | } 56 | } 57 | } 58 | 59 | // ------------------------------------------------------------------------- 60 | // MARK: - Fire bullet 61 | 62 | func fire(direction: PlaneDirection, sideDistance: CGFloat, fallDistance: CGFloat, speed: CGFloat = 2.0) { 63 | var moveAction: SCNAction? 64 | 65 | self.state = .alive 66 | 67 | _speed = speed 68 | 69 | switch (direction) { 70 | case .up: 71 | moveAction = SCNAction.moveBy(x: sideDistance, y: fallDistance, z: Bullet.speedDistance, duration: TimeInterval(10/_speed)) 72 | case .down: 73 | moveAction = SCNAction.moveBy(x: -sideDistance, y: fallDistance, z: -Bullet.speedDistance, duration: TimeInterval(10/_speed)) 74 | case .left: 75 | moveAction = SCNAction.moveBy(x: Bullet.speedDistance, y: fallDistance, z: 0, duration: TimeInterval(10/_speed)) 76 | case .right: 77 | moveAction = SCNAction.moveBy(x: -Bullet.speedDistance, y: fallDistance, z: 0, duration: TimeInterval(10/_speed)) 78 | 79 | default: 80 | return 81 | } 82 | 83 | self.runAction(SCNAction.repeatForever(moveAction!), forKey: "fire") 84 | 85 | let delay = SCNAction.wait(duration: Bullet.lifeTime) 86 | let action2 = SCNAction.run({ _ in 87 | rbDebug("Lifetime is over for \(self)") 88 | 89 | self.isHidden = true 90 | self.state = .died 91 | self.removeAllActions() 92 | }) 93 | 94 | self.runAction(SCNAction.sequence([delay, action2]), forKey: "lifetime") 95 | } 96 | 97 | // ------------------------------------------------------------------------- 98 | // MARK: - Collision handling 99 | 100 | override func collision(with object: GameObject, level: GameLevel) { 101 | // Collision with player and enemies are handled in their classes 102 | // Here the bullets are set themself to destroyed 103 | if (self.state == .died) { 104 | return 105 | } 106 | 107 | if let enemy = object as? Enemy { 108 | // Check for player bullet 109 | if !self.enemy { 110 | rbDebug("\(self) collided with \(object)") 111 | 112 | GameSound.explosion(enemy) 113 | enemy.hit() 114 | 115 | level.addPoints(enemy.points) 116 | 117 | } 118 | } 119 | else if let player = object as? Player { 120 | // Check for enemy bullet 121 | if self.enemy { 122 | rbDebug("\(self) collided with \(object)") 123 | 124 | GameSound.explosion(player) 125 | player.hit() 126 | } 127 | } 128 | else if let handicap = object as? Handicap { 129 | // Check for player bullet 130 | if !self.enemy { 131 | rbDebug("\(self) collided with \(object)") 132 | 133 | GameSound.explosion(handicap) 134 | handicap.hit() 135 | 136 | level.addPoints(handicap.points) 137 | } 138 | } 139 | 140 | // Destroy myself 141 | self.isHidden = true 142 | self.state = .died 143 | 144 | self.removeAllActions() 145 | } 146 | 147 | // ------------------------------------------------------------------------- 148 | // MARK: - Initialisation 149 | 150 | init(enemy: Bool) { 151 | super.init() 152 | 153 | let material = SCNMaterial() 154 | material.diffuse.contents = UIColor.white 155 | let geometry = SCNBox(width: 0.2, height: 0.2, length: 0.2, chamferRadius: 0.0) 156 | 157 | _bulletNode = SCNNode(geometry: geometry) 158 | 159 | self.addChildNode(_bulletNode) 160 | 161 | self.enemy = enemy 162 | 163 | rbDebug("Create \(self)") 164 | } 165 | 166 | // ------------------------------------------------------------------------- 167 | 168 | required init(coder: NSCoder) { 169 | fatalError("Not yet implemented") 170 | } 171 | 172 | // ------------------------------------------------------------------------- 173 | 174 | } 175 | 176 | -------------------------------------------------------------------------------- /partVI/Enemy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Player.swift 3 | // 4 | // Part 6 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 12/07/16. 8 | // Copyright © 2016 Roger Boesch. All rights reserved. 9 | // 10 | // Part 6: Created a enemy plane class 11 | // 12 | 13 | import SceneKit 14 | 15 | // ----------------------------------------------------------------------------- 16 | 17 | class Enemy : Plane { 18 | 19 | // ------------------------------------------------------------------------- 20 | // MARK: - Propertiues 21 | 22 | override var description: String { 23 | get { 24 | return "enemy \(self.id)" 25 | } 26 | } 27 | 28 | // ------------------------------------------------------------------------- 29 | // MARK: - Actions 30 | 31 | override func hit() { 32 | if let emitter = SCNParticleSystem(named: "art.scnassets/smoke.scnp", inDirectory: nil) { 33 | self.addParticleSystem(emitter) 34 | } 35 | 36 | moveDown() 37 | moveDown() 38 | 39 | SCNTransaction.begin() 40 | SCNTransaction.animationDuration = 1.0 41 | 42 | self.modelNode?.eulerAngles = SCNVector3(degreesToRadians(value: 40.0), degreesToRadians(value: 0.0), 0) 43 | 44 | SCNTransaction.commit() 45 | } 46 | 47 | // ------------------------------------------------------------------------- 48 | 49 | override func start() { 50 | super.start() 51 | moveUp() 52 | } 53 | 54 | // ------------------------------------------------------------------------- 55 | 56 | func fire(_ level: GameLevel) { 57 | if self.numberOfBullets == 0 { 58 | rbDebug("\(self) has no bullets anymore") 59 | return 60 | } 61 | 62 | var position = self.position 63 | position.z = position.z + 1.0 64 | 65 | GameSound.fire(self) 66 | 67 | // Create bullet and fire 68 | self.numberOfBullets -= 1 69 | let bullet = level.fireBullet(enemy: true, position: position, sideDistance: 0, fallDistance: 0) 70 | 71 | rbDebug("\(self) has fired bullet \(bullet)") 72 | } 73 | 74 | // ------------------------------------------------------------------------- 75 | // MARK: - Game loop 76 | 77 | override func update(atTime time: TimeInterval, level: GameLevel) { 78 | super.update(atTime: time, level: level) 79 | 80 | if self.playerIsInDistance(level.player) && self.numberOfBullets > 0 { 81 | self.fire(level) 82 | } 83 | } 84 | 85 | // ------------------------------------------------------------------------- 86 | // MARK: - Helper methods 87 | 88 | private func playerIsInDistance(_ player: Player?) -> Bool { 89 | if player == nil { 90 | return false 91 | } 92 | 93 | if self.position.distance(to: player!.position) <= Game.Plane.fireDistance { 94 | return true 95 | } 96 | 97 | return false 98 | } 99 | 100 | 101 | // ------------------------------------------------------------------------- 102 | // MARK: - Initialisation 103 | 104 | override init() { 105 | super.init() 106 | 107 | // Set physics 108 | self.collissionNode!.name = "enemy-plane" 109 | self.collissionNode!.physicsBody?.categoryBitMask = Game.Physics.Categories.enemy 110 | 111 | self.flip = true 112 | 113 | self.state = .alive 114 | self.points = Game.Points.enemy 115 | self.numberOfBullets = Game.Bullets.enemy 116 | } 117 | 118 | // ------------------------------------------------------------------------- 119 | 120 | required init(coder: NSCoder) { 121 | fatalError("Not yet implemented") 122 | } 123 | 124 | // ------------------------------------------------------------------------- 125 | 126 | } 127 | 128 | -------------------------------------------------------------------------------- /partVI/GameObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameObject.swift 3 | // Part 6 of the SceneKit Tutorial Series 'From Zero to Hero' at: 4 | // https://rogerboesch.github.io/ 5 | // 6 | // Created by Roger Boesch on 31.10.17. 7 | // Copyright © 2017 Roger Boesch. All rights reserved. 8 | // 9 | 10 | import SceneKit 11 | 12 | enum GameObjecState { 13 | case initialized, alive, died, stopped 14 | } 15 | 16 | class GameObject : SCNNode { 17 | private static var count: Int = 0 18 | static var level: GameLevel? 19 | 20 | private var _internalID: Int = 0 21 | private var _tag = 0 22 | private var _state = GameObjecState.initialized 23 | 24 | private var _points = 0 25 | 26 | // ------------------------------------------------------------------------- 27 | // MARK: - Propertiues 28 | 29 | override var description: String { 30 | get { 31 | return "game object \(self.id)" 32 | } 33 | } 34 | 35 | // ------------------------------------------------------------------------- 36 | 37 | var id: Int { 38 | return _internalID 39 | } 40 | 41 | // ------------------------------------------------------------------------- 42 | 43 | var tag: Int { 44 | get { 45 | return _tag 46 | } 47 | set(value) { 48 | _tag = value 49 | } 50 | } 51 | 52 | // ------------------------------------------------------------------------- 53 | 54 | var state: GameObjecState { 55 | get { 56 | return _state 57 | } 58 | set(value) { 59 | rbDebug("State of \(self) changed from \(_state) to \(value)") 60 | _state = value 61 | } 62 | } 63 | 64 | // ------------------------------------------------------------------------- 65 | 66 | var points: Int { 67 | get { 68 | return _points 69 | } 70 | set(value) { 71 | _points = value 72 | } 73 | } 74 | 75 | // ------------------------------------------------------------------------- 76 | // MARK: - Actions 77 | 78 | func hit() {} // Object get hit by another object 79 | 80 | // ------------------------------------------------------------------------- 81 | 82 | func start() { } 83 | 84 | func stop() { 85 | // Stop object (release all) 86 | self.state = .stopped 87 | stopAllActions(self) 88 | } 89 | 90 | // ------------------------------------------------------------------------- 91 | // MARK: - Game loop 92 | 93 | func update(atTime time: TimeInterval, level: GameLevel) {} 94 | 95 | // ------------------------------------------------------------------------- 96 | // MARK: - Collision handling 97 | 98 | func collision(with object: GameObject, level: GameLevel) {} 99 | 100 | // ------------------------------------------------------------------------- 101 | // MARK: - Helper methods 102 | 103 | func stopAllActions(_ node: SCNNode) { 104 | // It's important to stop all actions before we remove a game object 105 | // Otherwise they continue to run and result in difficult side effects. 106 | node.removeAllActions() 107 | 108 | for child in node.childNodes { 109 | child.removeAllActions() 110 | stopAllActions(child) 111 | } 112 | } 113 | 114 | // ------------------------------------------------------------------------- 115 | // MARK: - Initialisation 116 | 117 | override init() { 118 | super.init() 119 | 120 | GameObject.count += 1 121 | _internalID = GameObject.count 122 | } 123 | 124 | // ------------------------------------------------------------------------- 125 | 126 | required init?(coder aDecoder: NSCoder) { 127 | fatalError("init(coder aDecoder: NSCoder) is not implemented") 128 | } 129 | 130 | // ------------------------------------------------------------------------- 131 | } 132 | 133 | 134 | -------------------------------------------------------------------------------- /partVI/GameSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameSettings.swift 3 | // 4 | // Part 6 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 12/10/16. 8 | // Copyright © 2016 Roger Boesch. All rights reserved. 9 | // 10 | 11 | import UIKit 12 | 13 | struct Game { 14 | 15 | struct Points { 16 | static let ring: Int = 200 // Points get when fly trough a ring 17 | static let enemy: Int = 100 // Points get when shoot a enemy plane 18 | static let handicap: Int = 50 // Points get when shoot a handicap 19 | } 20 | 21 | struct Bullets { 22 | static let player: Int = Int.max // Number of bullets player has initially 23 | static let enemy: Int = 1 // Number of bullets an enemy plane has initially 24 | } 25 | 26 | struct Level { 27 | static let numberOfRings: Int = 10 // Number of rings in level 28 | static let numberOfEnemies: Int = 20 // Number of enemy planes in level 29 | 30 | static let width: CGFloat = 640 // Terrain width 31 | static let length: CGFloat = 960 // Terraiun length 32 | static let start: CGFloat = 100 // Start of player 33 | 34 | struct Fog { 35 | static let start: CGFloat = 20 // Begin of fog 36 | static let end: CGFloat = 300 // End of fog 37 | } 38 | } 39 | 40 | struct Plane { 41 | static let fireDistance: CGFloat = 70.0 // Fire distance (to player) for enemy 42 | 43 | static let actionTime: TimeInterval = 10.0 // Time used for move actions 44 | static let leftRightMoveDistance: CGFloat = 6.0 // Left/Right distance take in time 45 | static let upDownMoveDistance: CGFloat = 4.0 // Up/Down distance take in time 46 | static let speedDistance: CGFloat = 100.0 // Forward distance take in time 47 | 48 | static let upDownAngle: CGFloat = 5.0 // Angle when fly up or down 49 | static let leftRightAngle: CGFloat = 30.0 // Angle when fly left or right 50 | 51 | static let crashHeight: Float = 1.5 // Minimum height when crashed 52 | static let minimumHeight: Float = 4.0 // Minimum height 53 | static let maximumHeight: Float = 30.0 // Maximum height 54 | static let maximumLeft: Float = 360.0 // Maximum left 55 | static let maximumRight: Float = 30.0 // Maximum right 56 | } 57 | 58 | struct Objects { 59 | static let offset: CGFloat = 15 // Space between objects 60 | } 61 | 62 | struct Physics { 63 | 64 | // Category bits used for physics handling 65 | struct Categories { 66 | static let player: Int = 0b00000001 67 | static let ring: Int = 0b00000010 68 | static let enemy: Int = 0b00000100 69 | static let bullet: Int = 0b00001000 70 | } 71 | } 72 | 73 | struct Motion { 74 | static let threshold: Double = 0.2 // Minium threshold of accelerometer 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /partVI/GameSound.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameSound.swift 3 | // 4 | // Part 6 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 13/12/17. 8 | // Copyright © 2017 Roger Boesch. All rights reserved. 9 | // 10 | 11 | import Foundation 12 | import SceneKit 13 | import AudioToolbox.AudioServices 14 | 15 | class GameSound { 16 | private static let explosion = SCNAudioSource(fileNamed: "sounds/explosion.wav") 17 | private static let fire = SCNAudioSource(fileNamed: "sounds/fire.wav") 18 | private static let bonus = SCNAudioSource(fileNamed: "sounds/bonus.wav") 19 | 20 | // ------------------------------------------------------------------------- 21 | // MARK: - Vibrate 22 | 23 | static func vibrate() { 24 | rbDebug("GameSound: vibrate") 25 | AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) 26 | } 27 | 28 | // ------------------------------------------------------------------------- 29 | 30 | private static func play(_ name: String, source: SCNAudioSource, node: SCNNode) { 31 | let _ = GameAudioPlayer(name: name, source: source, node: node) 32 | } 33 | 34 | // ------------------------------------------------------------------------- 35 | // MARK: - Sound effects 36 | 37 | static func explosion(_ node: SCNNode) { 38 | guard explosion != nil else { return } 39 | 40 | GameSound.play("explosion", source: GameSound.explosion!, node: node) 41 | GameSound.vibrate() 42 | } 43 | 44 | // ------------------------------------------------------------------------- 45 | 46 | static func fire(_ node: SCNNode) { 47 | guard fire != nil else { return } 48 | 49 | GameSound.play("fire", source: GameSound.fire!, node: node) 50 | GameSound.vibrate() 51 | } 52 | 53 | // ------------------------------------------------------------------------- 54 | 55 | static func bonus(_ node: SCNNode) { 56 | guard bonus != nil else { return } 57 | 58 | GameSound.play("bonus", source: GameSound.bonus!, node: node) 59 | } 60 | 61 | // ------------------------------------------------------------------------- 62 | 63 | } 64 | 65 | class GameAudioPlayer : SCNAudioPlayer { 66 | private var _node: SCNNode! 67 | 68 | init(name: String, source: SCNAudioSource, node: SCNNode) { 69 | super.init(source: source) 70 | 71 | node.addAudioPlayer(self) 72 | _node = node 73 | 74 | rbDebug("GameAudioPlayer: play \(name)") 75 | 76 | self.didFinishPlayback = { 77 | rbDebug("GameAudioPlayer: stopped \(name)") 78 | 79 | self._node.removeAudioPlayer(self) 80 | } 81 | } 82 | 83 | } 84 | 85 | -------------------------------------------------------------------------------- /partVI/GameViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameViewController.swift 3 | // 4 | // Part 6 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 12/07/16. 8 | // Copyright © 2016 Roger Boesch. All rights reserved. 9 | // 10 | 11 | import UIKit 12 | import SceneKit 13 | import SpriteKit 14 | import CoreMotion 15 | import GameController 16 | 17 | class GameViewController: UIViewController, SCNSceneRendererDelegate { 18 | private var _sceneView: SCNView! 19 | private var _level: GameLevel! 20 | private var _hud: HUD! 21 | 22 | // New in Part 5: Use CoreMotion to fly the plane 23 | private var _motionManager = CMMotionManager() 24 | private var _startAttitude: CMAttitude? // Start attitude 25 | private var _currentAttitude: CMAttitude? // Current attitude 26 | 27 | // ------------------------------------------------------------------------- 28 | // MARK: - Properties 29 | 30 | var sceneView: SCNView { 31 | return _sceneView 32 | } 33 | 34 | // ------------------------------------------------------------------------- 35 | 36 | var hud: HUD { 37 | return _hud 38 | } 39 | 40 | // ------------------------------------------------------------------------- 41 | // MARK: - Render delegate (New in Part 4) 42 | 43 | func renderer(_ renderer: SCNSceneRenderer, didSimulatePhysicsAtTime time: TimeInterval) { 44 | guard _level != nil else { return } 45 | 46 | _level.update(atTime: time) 47 | renderer.loops = true 48 | } 49 | 50 | // ------------------------------------------------------------------------- 51 | // MARK: - Gesture recognoizers 52 | 53 | @objc private func handleTap(_ gestureRecognize: UITapGestureRecognizer) { 54 | // New in Part 4: A tap is used to restart the level (see tutorial) 55 | if _level.state == .loose || _level.state == .win { 56 | _level.stop() 57 | _level = nil 58 | 59 | DispatchQueue.main.async { 60 | // Create things in main thread 61 | 62 | let level = GameLevel() 63 | level.create() 64 | 65 | level.hud = self.hud 66 | self.hud.reset() 67 | 68 | self.sceneView.scene = level 69 | self._level = level 70 | 71 | self.hud.message("READY?", information: "- Touch screen to start -") 72 | } 73 | } 74 | // New in Part 5: A tap is used to start the level (see tutorial) 75 | else if _level.state == .ready { 76 | _startAttitude = _currentAttitude 77 | _level.start() 78 | } 79 | // New in Part 6: A tap is used to shot fire (see tutorial) 80 | else if _level.state == .play { 81 | _level.fire() 82 | } 83 | } 84 | 85 | // ------------------------------------------------------------------------- 86 | 87 | @objc private func handleSwipe(_ gestureRecognize: UISwipeGestureRecognizer) { 88 | if _level.state != .play { 89 | return 90 | } 91 | 92 | if (gestureRecognize.direction == .left) { 93 | _level!.swipeLeft() 94 | } 95 | else if (gestureRecognize.direction == .right) { 96 | _level!.swipeRight() 97 | } 98 | else if (gestureRecognize.direction == .down) { 99 | _level!.swipeDown() 100 | } 101 | else if (gestureRecognize.direction == .up) { 102 | _level!.swipeUp() 103 | } 104 | } 105 | 106 | // ------------------------------------------------------------------------- 107 | // MARK: - Motion handling 108 | 109 | private func motionDidChange(data: CMDeviceMotion) { 110 | _currentAttitude = data.attitude 111 | 112 | guard _level != nil, _level?.state == .play else { return } 113 | 114 | // Up/Down 115 | let diff1 = _startAttitude!.roll - _currentAttitude!.roll 116 | 117 | if (diff1 >= Game.Motion.threshold) { 118 | _level!.motionMoveUp() 119 | } 120 | else if (diff1 <= -Game.Motion.threshold) { 121 | _level!.motionMoveDown() 122 | } 123 | else { 124 | _level!.motionStopMovingUpDown() 125 | } 126 | 127 | let diff2 = _startAttitude!.pitch - _currentAttitude!.pitch 128 | 129 | if (diff2 >= Game.Motion.threshold) { 130 | _level!.motionMoveLeft() 131 | } 132 | else if (diff2 <= -Game.Motion.threshold) { 133 | _level!.motionMoveRight() 134 | } 135 | else { 136 | _level!.motionStopMovingLeftRight() 137 | } 138 | } 139 | 140 | // ------------------------------------------------------------------------- 141 | 142 | private func setupMotionHandler() { 143 | if (GCController.controllers().count == 0 && _motionManager.isAccelerometerAvailable) { 144 | _motionManager.accelerometerUpdateInterval = 1/60.0 145 | 146 | _motionManager.startDeviceMotionUpdates(to: OperationQueue.main, withHandler: {(data, error) in 147 | self.motionDidChange(data: data!) 148 | }) 149 | } 150 | } 151 | 152 | // ------------------------------------------------------------------------- 153 | // MARK: - ViewController life cycle 154 | 155 | override func viewWillAppear(_ animated: Bool) { 156 | super.viewWillAppear(animated) 157 | 158 | // Part 3: HUD is created and assigned to view and game level 159 | _hud = HUD(size: self.view.bounds.size) 160 | _level.hud = _hud 161 | _sceneView.overlaySKScene = _hud.scene 162 | 163 | self.hud.message("READY?", information: "- Touch screen to start -") 164 | } 165 | 166 | // ------------------------------------------------------------------------- 167 | 168 | override func viewDidLoad() { 169 | super.viewDidLoad() 170 | 171 | _level = GameLevel() 172 | _level.create() 173 | 174 | _sceneView = SCNView() 175 | _sceneView.scene = _level 176 | _sceneView.allowsCameraControl = false 177 | _sceneView.showsStatistics = false 178 | _sceneView.backgroundColor = UIColor.black 179 | _sceneView.delegate = self 180 | 181 | self.view = _sceneView 182 | 183 | setupMotionHandler() 184 | 185 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap)) 186 | _sceneView!.addGestureRecognizer(tapGesture) 187 | 188 | let swipeLeftGesture = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipe)) 189 | swipeLeftGesture.direction = .left 190 | _sceneView!.addGestureRecognizer(swipeLeftGesture) 191 | 192 | let swipeRightGesture = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipe)) 193 | swipeRightGesture.direction = .right 194 | _sceneView!.addGestureRecognizer(swipeRightGesture) 195 | 196 | let swipeDownGesture = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipe)) 197 | swipeDownGesture.direction = .down 198 | _sceneView!.addGestureRecognizer(swipeDownGesture) 199 | 200 | let swipeUpGesture = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipe)) 201 | swipeUpGesture.direction = .up 202 | _sceneView!.addGestureRecognizer(swipeUpGesture) 203 | } 204 | 205 | // ------------------------------------------------------------------------- 206 | 207 | override func didReceiveMemoryWarning() { 208 | super.didReceiveMemoryWarning() 209 | } 210 | 211 | // ------------------------------------------------------------------------- 212 | 213 | } 214 | -------------------------------------------------------------------------------- /partVI/HUD.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HUD.swift 3 | // 4 | // Part 6 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 11.11.17. 8 | // Copyright © 2017 Roger Boesch. All rights reserved. 9 | // 10 | 11 | import SpriteKit 12 | 13 | class HUD { 14 | private var _scene: SKScene! 15 | private let _points = SKLabelNode(text: "") 16 | private let _rings = SKLabelNode(text: "") 17 | private let _missedRings = SKLabelNode(text: "") 18 | private let _message = SKLabelNode(text: "") 19 | private let _info = SKLabelNode(text: "") 20 | 21 | private var _totalPoints = 0 22 | 23 | // ------------------------------------------------------------------------- 24 | // MARK: - Properties 25 | 26 | var scene: SKScene { 27 | get { 28 | return _scene 29 | } 30 | } 31 | 32 | // ------------------------------------------------------------------------- 33 | 34 | var points: Int { 35 | get { 36 | return _totalPoints 37 | } 38 | set(value) { 39 | _totalPoints = value 40 | _points.text = String(format: "%04d", _totalPoints) 41 | } 42 | } 43 | 44 | // ------------------------------------------------------------------------- 45 | 46 | var rings: Int { 47 | get { 48 | return 0 49 | } 50 | set(value) { 51 | if value == 1 { 52 | _rings.text = String(format: "%d RING", value) 53 | } 54 | else { 55 | _rings.text = String(format: "%d RINGS", value) 56 | } 57 | 58 | // New in Part 4: Animated HUD informations (check RB+SKAction.swift for details) 59 | let scaling: CGFloat = 3 60 | let action = SKAction.zoomWithNode(_rings, amount: CGPoint.make(scaling, scaling), oscillations: 1, duration: 0.5) 61 | _rings.run(action) 62 | } 63 | } 64 | 65 | // ------------------------------------------------------------------------- 66 | 67 | var missedRings: Int { 68 | get { 69 | return 0 70 | } 71 | set(value) { 72 | _missedRings.text = String(format: "%d MISSED", value) 73 | 74 | if value > 0 { 75 | _missedRings.isHidden = false 76 | _missedRings.fontColor = UIColor.red 77 | } 78 | else { 79 | _missedRings.isHidden = true 80 | } 81 | 82 | let scaling: CGFloat = 3 83 | let action = SKAction.zoomWithNode(_missedRings, amount: CGPoint.make(scaling, scaling), oscillations: 1, duration: 0.5) 84 | _missedRings.run(action) 85 | } 86 | } 87 | 88 | // ------------------------------------------------------------------------- 89 | // MARK: - Points handling 90 | 91 | private func changePoints(_ pointsToAdd: Int, total: Int, count: Int) { 92 | _totalPoints += pointsToAdd 93 | points = _totalPoints 94 | 95 | let scaling: CGFloat = 1.5 96 | 97 | let action = SKAction.zoomWithNode(_points, amount: CGPoint.make(scaling, scaling), oscillations: 1, duration: 0.01) 98 | _points.run(action) 99 | 100 | if count < total { 101 | Run.after(0.01, { 102 | self.changePoints(1, total: total, count: count+1) 103 | }) 104 | } 105 | } 106 | 107 | // ------------------------------------------------------------------------- 108 | 109 | func addPoints(_ points: Int) { 110 | self.changePoints(1, total: points, count: 1) 111 | } 112 | 113 | // ------------------------------------------------------------------------- 114 | // MARK: - Message handling 115 | 116 | func message(_ str: String, information: String? = nil) { 117 | // New in Part 4: Used for game over and win messages 118 | _message.text = str 119 | _message.isHidden = false 120 | 121 | let scaling: CGFloat = 10 122 | let action = SKAction.zoomWithNode(_message, amount: CGPoint.make(scaling, scaling), oscillations: 1, duration: 0.5) 123 | _message.run(action) 124 | 125 | if information != nil { 126 | info(information!) 127 | } 128 | } 129 | 130 | // ------------------------------------------------------------------------- 131 | 132 | func info(_ str: String) { 133 | // New in Part 4: Uses for additional info when show messages 134 | 135 | _info.text = str 136 | _info.isHidden = false 137 | 138 | let scaling: CGFloat = 2 139 | let action = SKAction.zoomWithNode(_info, amount: CGPoint.make(scaling, scaling), oscillations: 1, duration: 0.5) 140 | _info.run(action) 141 | } 142 | 143 | // ------------------------------------------------------------------------- 144 | 145 | func reset() { 146 | // New in Part 4: Reset is needed whenever start the level 147 | 148 | _message.text = "" 149 | _message.isHidden = true 150 | 151 | _info.text = "" 152 | _info.isHidden = true 153 | 154 | rings = 0 155 | missedRings = 0 156 | points = 0 157 | } 158 | 159 | // ------------------------------------------------------------------------- 160 | // MARK: - Initialisation 161 | 162 | init(size: CGSize) { 163 | _scene = SKScene(size: size) 164 | 165 | _points.position = CGPoint(x: size.width/2, y: size.height-50) 166 | _points.horizontalAlignmentMode = .center 167 | _points.fontName = "MarkerFelt-Wide" 168 | _points.fontSize = 30 169 | _points.fontColor = UIColor.white 170 | _scene.addChild(_points) 171 | 172 | _rings.position = CGPoint(x: 40, y: size.height-50) 173 | _rings.horizontalAlignmentMode = .left 174 | _rings.fontName = "MarkerFelt-Wide" 175 | _rings.fontSize = 30 176 | _rings.fontColor = UIColor.white 177 | _scene.addChild(_rings) 178 | 179 | _missedRings.position = CGPoint(x: size.width-40, y: size.height-50) 180 | _missedRings.horizontalAlignmentMode = .right 181 | _missedRings.fontName = "MarkerFelt-Wide" 182 | _missedRings.fontSize = 30 183 | _missedRings.fontColor = UIColor.white 184 | _scene.addChild(_missedRings) 185 | 186 | _message.position = CGPoint(x: size.width/2, y: size.height/2) 187 | _message.horizontalAlignmentMode = .center 188 | _message.fontName = "MarkerFelt-Wide" 189 | _message.fontSize = 60 190 | _message.fontColor = UIColor.white 191 | _message.isHidden = true 192 | _scene.addChild(_message) 193 | 194 | _info.position = CGPoint(x: size.width/2, y: size.height/2-40) 195 | _info.horizontalAlignmentMode = .center 196 | _info.fontName = "MarkerFelt-Wide" 197 | _info.fontSize = 20 198 | _info.fontColor = UIColor.white 199 | _info.isHidden = true 200 | _scene.addChild(_info) 201 | 202 | reset() 203 | } 204 | 205 | // ------------------------------------------------------------------------- 206 | 207 | required init?(coder aDecoder: NSCoder) { 208 | fatalError("init?(coder aDecoder: NSCoder) not implemented") 209 | } 210 | 211 | // ------------------------------------------------------------------------- 212 | } 213 | -------------------------------------------------------------------------------- /partVI/Handicap.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Handicap.swift 3 | // 4 | // Part 6 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 13/12/17. 8 | // Copyright © 2017 Roger Boesch. All rights reserved. 9 | // 10 | 11 | import SceneKit 12 | 13 | // ----------------------------------------------------------------------------- 14 | 15 | class Handicap : GameObject { 16 | private var _node: SCNNode! 17 | private var _height: CGFloat = 0 18 | 19 | // ------------------------------------------------------------------------- 20 | // MARK: - Propertiues 21 | 22 | override var description: String { 23 | get { 24 | return "handicap \(self.id)" 25 | } 26 | } 27 | 28 | // ------------------------------------------------------------------------- 29 | 30 | var height: CGFloat { 31 | get { 32 | return _height 33 | } 34 | } 35 | 36 | // ------------------------------------------------------------------------- 37 | // MARK: - Actions 38 | 39 | override func hit() { 40 | if self.state != .alive { 41 | return 42 | } 43 | 44 | self.state = .died 45 | 46 | let action1 = SCNAction.moveBy(x: 0, y: -4, z: 0, duration: 0.15) 47 | _node.runAction(action1) 48 | 49 | let action2 = SCNAction.rotateBy(x: degreesToRadians(value: 30), y: 0, z: degreesToRadians(value: 15), duration: 0.3) 50 | _node.runAction(action2) 51 | 52 | if let emitter = SCNParticleSystem(named: "art.scnassets/fire.scnp", inDirectory: nil) { 53 | self.addParticleSystem(emitter) 54 | } 55 | } 56 | 57 | // ------------------------------------------------------------------------- 58 | // MARK: - Initialisation 59 | 60 | override init() { 61 | super.init() 62 | 63 | let material = SCNMaterial() 64 | material.diffuse.contents = UIColor.random(list: UIGreenColorList) 65 | material.specular.contents = UIColor.white 66 | material.shininess = 1.0 67 | 68 | // Use some randomness in height, width and color 69 | let width = RBRandom.cgFloat(4.0, 9.0) 70 | _height = RBRandom.cgFloat(15.0, 25) 71 | 72 | var geometry: SCNGeometry! 73 | let rnd = RBRandom.integer(1, 3) 74 | if rnd == 1 { 75 | geometry = SCNBox(width: width, height: _height, length: 2.0, chamferRadius: 0.0) 76 | } 77 | else if rnd == 2 { 78 | geometry = SCNCylinder(radius: width, height: _height) 79 | } 80 | else { 81 | geometry = SCNCone(topRadius: 0.0, bottomRadius: width, height: _height) 82 | } 83 | 84 | geometry.materials = [material] 85 | 86 | _node = SCNNode(geometry: geometry) 87 | _node.name = "handicap" 88 | _node.physicsBody = SCNPhysicsBody(type: .kinematic, shape: nil) 89 | _node.physicsBody?.categoryBitMask = Game.Physics.Categories.enemy 90 | self.addChildNode(_node) 91 | 92 | self.state = .alive 93 | self.points = Game.Points.handicap 94 | } 95 | 96 | // ------------------------------------------------------------------------- 97 | 98 | required init(coder: NSCoder) { 99 | fatalError("Not yet implemented") 100 | } 101 | 102 | // ------------------------------------------------------------------------- 103 | 104 | } 105 | 106 | 107 | -------------------------------------------------------------------------------- /partVI/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UIStatusBarHidden 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationLandscapeLeft 34 | UIInterfaceOrientationLandscapeRight 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /partVI/Player.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Player.swift 3 | // 4 | // Part 6 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 12/07/16. 8 | // Copyright © 2016 Roger Boesch. All rights reserved. 9 | // 10 | 11 | import SceneKit 12 | 13 | // ----------------------------------------------------------------------------- 14 | 15 | class Player : Plane { 16 | private let lookAtForwardPosition = SCNVector3Make(0.0, -1.0, 6.0) 17 | private let cameraFowardPosition = SCNVector3(x: 0, y: 1.0, z: -5) 18 | 19 | private var _lookAtNode: SCNNode? 20 | private var _cameraNode: SCNNode? 21 | 22 | private var _crashed = false 23 | 24 | // ------------------------------------------------------------------------- 25 | // MARK: - Propertiues 26 | 27 | override var description: String { 28 | get { 29 | return "player \(self.id)" 30 | } 31 | } 32 | 33 | // ------------------------------------------------------------------------- 34 | // MARK: - Actions 35 | 36 | func fire(_ level: GameLevel) { 37 | if self.numberOfBullets == 0 { 38 | rbDebug("\(self) has no bullets anymore") 39 | return 40 | } 41 | 42 | var position = self.position 43 | position.z = position.z + 1.0 44 | 45 | GameSound.fire(self) 46 | 47 | var y: CGFloat = 0 48 | if self.upDownDirection == .up { 49 | y = 20 50 | } 51 | else if self.upDownDirection == .down { 52 | y = -20 53 | } 54 | 55 | var x: CGFloat = 0 56 | if self.leftRightDirection == .left { 57 | x = 40 58 | } 59 | else if self.leftRightDirection == .right { 60 | x = -40 61 | } 62 | 63 | // Create bullet and fire 64 | self.numberOfBullets -= 1 65 | let bullet = level.fireBullet(enemy: false, position: position, sideDistance: x, fallDistance: y) 66 | 67 | rbDebug("\(self) has fired bullet \(bullet)") 68 | } 69 | 70 | // ------------------------------------------------------------------------- 71 | 72 | override func hit() { 73 | if _crashed { 74 | return 75 | } 76 | 77 | if let emitter = SCNParticleSystem(named: "art.scnassets/smoke.scnp", inDirectory: nil) { 78 | self.addParticleSystem(emitter) 79 | } 80 | 81 | _crashed = true 82 | 83 | moveDown() 84 | moveDown() 85 | 86 | SCNTransaction.begin() 87 | SCNTransaction.animationDuration = 1.0 88 | 89 | self.modelNode?.eulerAngles = SCNVector3(degreesToRadians(value: 40.0), degreesToRadians(value: 0.0), 0) 90 | 91 | SCNTransaction.commit() 92 | } 93 | 94 | // ------------------------------------------------------------------------- 95 | // MARK: - Game loop 96 | 97 | override func update(atTime time: TimeInterval, level: GameLevel) { 98 | if !_crashed { 99 | super.update(atTime: time, level: level) 100 | return 101 | } 102 | 103 | if (self.position.y <= Game.Plane.crashHeight) { 104 | self.die() 105 | self.modelNode?.isHidden = false 106 | 107 | GameSound.explosion(self) 108 | 109 | level.crashed() 110 | } 111 | } 112 | 113 | // ------------------------------------------------------------------------- 114 | // MARK: - Collision handling 115 | 116 | override func collision(with object: GameObject, level: GameLevel) { 117 | if self.state != .alive { 118 | return 119 | } 120 | 121 | if let ring = object as? Ring { 122 | rbDebug("\(self) collided with \(object)") 123 | 124 | if ring.state != .alive { 125 | return 126 | } 127 | 128 | GameSound.bonus(self) 129 | 130 | level.flyTrough(ring) 131 | ring.hit() 132 | } 133 | else if let handicap = object as? Handicap { 134 | if self.state != .alive { 135 | return 136 | } 137 | 138 | rbDebug("\(self) collided with \(object)") 139 | 140 | GameSound.explosion(self) 141 | 142 | level.touchedHandicap(handicap) 143 | handicap.hit() 144 | 145 | self.die() 146 | } 147 | else if let enemy = object as? Enemy { 148 | if enemy.state != .alive { 149 | return 150 | } 151 | 152 | rbDebug("\(self) collided with \(object)") 153 | 154 | GameSound.explosion(self) 155 | 156 | enemy.hit() 157 | self.hit() 158 | } 159 | } 160 | 161 | // ------------------------------------------------------------------------- 162 | // MARK: - Camera adjustment 163 | 164 | private func adjustCamera() { 165 | // New in Part 5: move the camera according to the fly direction 166 | var position = _cameraNode!.position 167 | 168 | if (self.leftRightDirection == .left) { 169 | position.x = 1.0 170 | } 171 | else if (self.leftRightDirection == .right) { 172 | position.x = -1.0 173 | } 174 | else if (self.leftRightDirection == .none) { 175 | position.x = 0.1 176 | } 177 | 178 | SCNTransaction.begin() 179 | SCNTransaction.animationDuration = 1.0 180 | 181 | _cameraNode?.position = position 182 | 183 | SCNTransaction.commit() 184 | } 185 | 186 | // ------------------------------------------------------------------------- 187 | // MARK: - New in Part 5: Move Actions 188 | 189 | override func moveUp() { 190 | if _crashed { 191 | return 192 | } 193 | 194 | let oldDirection = self.upDownDirection 195 | 196 | super.moveUp() 197 | 198 | if oldDirection != self.upDownDirection { 199 | adjustCamera() 200 | } 201 | } 202 | 203 | // ------------------------------------------------------------------------- 204 | 205 | override func moveDown() { 206 | let oldDirection = self.upDownDirection 207 | 208 | super.moveDown() 209 | 210 | if oldDirection != self.upDownDirection { 211 | adjustCamera() 212 | } 213 | } 214 | 215 | // ------------------------------------------------------------------------- 216 | 217 | override func stopMovingUpDown() { 218 | if _crashed { 219 | return 220 | } 221 | 222 | let oldDirection = self.upDownDirection 223 | 224 | super.stopMovingUpDown() 225 | 226 | if oldDirection != self.upDownDirection { 227 | adjustCamera() 228 | } 229 | } 230 | 231 | // ------------------------------------------------------------------------- 232 | 233 | override func moveLeft() { 234 | let oldDirection = self.leftRightDirection 235 | 236 | super.moveLeft() 237 | 238 | if oldDirection != self.leftRightDirection { 239 | adjustCamera() 240 | } 241 | } 242 | 243 | // ------------------------------------------------------------------------- 244 | 245 | override func moveRight() { 246 | let oldDirection = self.leftRightDirection 247 | 248 | super.moveRight() 249 | 250 | if oldDirection != self.leftRightDirection { 251 | adjustCamera() 252 | } 253 | } 254 | 255 | // ------------------------------------------------------------------------- 256 | 257 | override func stopMovingLeftRight() { 258 | let oldDirection = self.leftRightDirection 259 | 260 | super.stopMovingLeftRight() 261 | 262 | if oldDirection != self.leftRightDirection { 263 | adjustCamera() 264 | } 265 | } 266 | 267 | // ------------------------------------------------------------------------- 268 | // MARK: - Initialisation 269 | 270 | override init() { 271 | super.init() 272 | 273 | // Set physics 274 | self.collissionNode!.name = "player" 275 | self.collissionNode!.physicsBody?.categoryBitMask = Game.Physics.Categories.player 276 | self.collissionNode!.physicsBody!.contactTestBitMask = Game.Physics.Categories.ring | Game.Physics.Categories.enemy 277 | 278 | // Look at Node 279 | _lookAtNode = SCNNode() 280 | _lookAtNode!.position = lookAtForwardPosition 281 | addChildNode(_lookAtNode!) 282 | 283 | // Camera 284 | let camera = SCNCamera() 285 | camera.zNear = 0.1 286 | camera.zFar = 600 287 | 288 | _cameraNode = SCNNode() 289 | _cameraNode!.camera = camera 290 | _cameraNode!.position = cameraFowardPosition 291 | self.addChildNode(_cameraNode!) 292 | 293 | // Link them 294 | let constraint1 = SCNLookAtConstraint(target: _lookAtNode) 295 | constraint1.isGimbalLockEnabled = true 296 | _cameraNode!.constraints = [constraint1] 297 | 298 | // Create a spotlight at the player 299 | let spotLight = SCNLight() 300 | spotLight.type = SCNLight.LightType.spot 301 | spotLight.spotInnerAngle = 40.0 302 | spotLight.spotOuterAngle = 80.0 303 | spotLight.castsShadow = true 304 | spotLight.color = UIColor.white 305 | let spotLightNode = SCNNode() 306 | spotLightNode.light = spotLight 307 | spotLightNode.position = SCNVector3(x: 0.0, y: 25.0, z: -1.0) 308 | self.addChildNode(spotLightNode) 309 | 310 | // Link it 311 | let constraint2 = SCNLookAtConstraint(target: self) 312 | constraint2.isGimbalLockEnabled = true 313 | spotLightNode.constraints = [constraint2] 314 | 315 | // Create additional omni light 316 | let lightNode = SCNNode() 317 | lightNode.light = SCNLight() 318 | lightNode.light!.type = SCNLight.LightType.omni 319 | lightNode.light!.color = UIColor.darkGray 320 | lightNode.position = SCNVector3(x: 0, y: 100.00, z: -2) 321 | self.addChildNode(lightNode) 322 | 323 | self.state = .alive 324 | self.numberOfBullets = Game.Bullets.player 325 | } 326 | 327 | // ------------------------------------------------------------------------- 328 | 329 | required init(coder: NSCoder) { 330 | fatalError("Not yet implemented") 331 | } 332 | 333 | // ------------------------------------------------------------------------- 334 | 335 | } 336 | -------------------------------------------------------------------------------- /partVI/Ring.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Ring.swift 3 | // 4 | // Part 6 of the SceneKit Tutorial Series 'From Zero to Hero' at: 5 | // https://rogerboesch.github.io/ 6 | // 7 | // Created by Roger Boesch on 13/12/17. 8 | // Copyright © 2017 Roger Boesch. All rights reserved. 9 | // 10 | 11 | import SceneKit 12 | 13 | // ----------------------------------------------------------------------------- 14 | 15 | class Ring : GameObject { 16 | private var _number: SCNNode! 17 | 18 | // ------------------------------------------------------------------------- 19 | // MARK: - Propertiues 20 | 21 | override var description: String { 22 | get { 23 | return "ring \(self.id)" 24 | } 25 | } 26 | 27 | // ------------------------------------------------------------------------- 28 | // MARK: - Actions 29 | 30 | override func hit() { 31 | self.state = .died 32 | 33 | _number.isHidden = false 34 | _number.removeAllActions() 35 | } 36 | 37 | // ------------------------------------------------------------------------- 38 | // MARK: - Initialisation 39 | 40 | init(number: Int) { 41 | super.init() 42 | 43 | let ringMaterial = SCNMaterial() 44 | ringMaterial.diffuse.contents = UIImage(named: "art.scnassets/ringTexture") 45 | ringMaterial.diffuse.wrapS = .repeat 46 | ringMaterial.diffuse.wrapT = .repeat 47 | ringMaterial.diffuse.contentsTransform = SCNMatrix4MakeScale(3, 1, 1) 48 | 49 | let ring = SCNTorus(ringRadius: 5.0, pipeRadius: 0.5) 50 | ring.materials = [ringMaterial] 51 | let ringNode = SCNNode(geometry: ring) 52 | self.addChildNode(ringNode) 53 | 54 | ringNode.eulerAngles = SCNVector3(degreesToRadians(value: 90), 0, 0) 55 | 56 | let action1 = SCNAction.rotateBy(x: 0, y: 0, z: degreesToRadians(value: 360), duration: 3.0) 57 | ringNode.runAction(SCNAction.repeatForever(action1)) 58 | 59 | // Contact box 60 | let boxMaterial = SCNMaterial() 61 | boxMaterial.diffuse.contents = UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 0.0) 62 | 63 | let box = SCNBox(width: 10.0, height: 10.0, length: 1.0, chamferRadius: 0.0) 64 | box.materials = [boxMaterial] 65 | let contactBox = SCNNode(geometry: box) 66 | contactBox.name = "ring" 67 | contactBox.physicsBody = SCNPhysicsBody(type: .kinematic, shape: nil) 68 | contactBox.physicsBody?.categoryBitMask = Game.Physics.Categories.ring 69 | self.addChildNode(contactBox) 70 | 71 | // New in Part 5: Add a number to the ring 72 | let numberMaterial = SCNMaterial() 73 | numberMaterial.diffuse.contents = UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0) 74 | 75 | let text = SCNText(string: "\(number)", extrusionDepth: 0.01) 76 | text.materials = [numberMaterial] 77 | _number = SCNNode(geometry: text) 78 | _number.eulerAngles = SCNVector3(0, degreesToRadians(value: 180), 0) 79 | _number.scale = SCNVector3(0.7, 0.7, 0.7) 80 | _number.position = SCNVector3(2.0, 4.5, -0.7) 81 | self.addChildNode(_number) 82 | 83 | let action2 = SCNAction.scale(to: 0.7, duration: 1.0) 84 | let action3 = SCNAction.scale(to: 1.0, duration: 1.0) 85 | let action4 = SCNAction.sequence([action2, action3]) 86 | _number.runAction(SCNAction.repeatForever(action4)) 87 | 88 | self.state = .alive 89 | self.points = Game.Points.ring 90 | } 91 | 92 | // ------------------------------------------------------------------------- 93 | 94 | required init(coder: NSCoder) { 95 | fatalError("Not yet implemented") 96 | } 97 | 98 | // ------------------------------------------------------------------------- 99 | 100 | } 101 | 102 | -------------------------------------------------------------------------------- /screenshots/screenshot0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rogerboesch/SceneKitTutorial/7fbe31110fc6a4bba43dbdde79cf26844e63ed11/screenshots/screenshot0.png -------------------------------------------------------------------------------- /screenshots/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rogerboesch/SceneKitTutorial/7fbe31110fc6a4bba43dbdde79cf26844e63ed11/screenshots/screenshot1.png -------------------------------------------------------------------------------- /screenshots/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rogerboesch/SceneKitTutorial/7fbe31110fc6a4bba43dbdde79cf26844e63ed11/screenshots/screenshot2.png -------------------------------------------------------------------------------- /screenshots/screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rogerboesch/SceneKitTutorial/7fbe31110fc6a4bba43dbdde79cf26844e63ed11/screenshots/screenshot3.png -------------------------------------------------------------------------------- /screenshots/screenshot4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rogerboesch/SceneKitTutorial/7fbe31110fc6a4bba43dbdde79cf26844e63ed11/screenshots/screenshot4.png -------------------------------------------------------------------------------- /screenshots/screenshot5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rogerboesch/SceneKitTutorial/7fbe31110fc6a4bba43dbdde79cf26844e63ed11/screenshots/screenshot5.png -------------------------------------------------------------------------------- /screenshots/screenshot6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rogerboesch/SceneKitTutorial/7fbe31110fc6a4bba43dbdde79cf26844e63ed11/screenshots/screenshot6.png -------------------------------------------------------------------------------- /shared/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // 4 | // Created by Roger Boesch on 12/07/16. 5 | // Copyright © 2016 Roger Boesch. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | @UIApplicationMain 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | private var _window: UIWindow? 14 | private var _gameViewController: GameViewController? 15 | 16 | // ------------------------------------------------------------------------------ 17 | 18 | func applicationDidFinishLaunching(_ application: UIApplication) { 19 | _gameViewController = GameViewController() 20 | 21 | _window = UIWindow(frame: UIScreen.main.bounds) 22 | _window?.rootViewController = _gameViewController 23 | _window?.makeKeyAndVisible() 24 | } 25 | 26 | // ------------------------------------------------------------------------------ 27 | 28 | func applicationWillResignActive(_ application: UIApplication) {} 29 | func applicationDidEnterBackground(_ application: UIApplication) {} 30 | func applicationWillEnterForeground(_ application: UIApplication) {} 31 | func applicationDidBecomeActive(_ application: UIApplication) {} 32 | func applicationWillTerminate(_ application: UIApplication) {} 33 | 34 | // ------------------------------------------------------------------------------ 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /shared/RB+CGPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RB+CGPoint.swift 3 | // CGRect extensions 4 | // 5 | // Created by Roger Boesch on 01/01/16. 6 | // Copyright © 2016 Roger Boesch All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | // ----------------------------------------------------------------------------- 16 | // MARK: - Make a CGPoint 17 | 18 | extension CGPoint { 19 | 20 | static func make(_ x: CGFloat, _ y: CGFloat) -> CGPoint { 21 | return CGPoint(x: x, y: y) 22 | } 23 | 24 | } 25 | 26 | // ----------------------------------------------------------------------------- 27 | 28 | public extension CGPoint { 29 | 30 | // ------------------------------------------------------------------------------ 31 | 32 | public init(vector: CGVector) { 33 | self.init(x: vector.dx, y: vector.dy) 34 | } 35 | 36 | // ------------------------------------------------------------------------------ 37 | 38 | public init(angle: CGFloat) { 39 | self.init(x: cos(angle), y: sin(angle)) 40 | } 41 | 42 | // ------------------------------------------------------------------------------ 43 | 44 | public mutating func offset(_ dx: CGFloat, dy: CGFloat) -> CGPoint { 45 | x += dx 46 | y += dy 47 | 48 | return self 49 | } 50 | 51 | // ------------------------------------------------------------------------------ 52 | 53 | public func length() -> CGFloat { 54 | return sqrt(x*x + y*y) 55 | } 56 | 57 | // ------------------------------------------------------------------------------ 58 | 59 | public func lengthSquared() -> CGFloat { 60 | return x*x + y*y 61 | } 62 | 63 | // ------------------------------------------------------------------------------ 64 | 65 | func normalized() -> CGPoint { 66 | let len = length() 67 | 68 | return len>0 ? self / len : CGPoint.zero 69 | } 70 | 71 | // ------------------------------------------------------------------------------ 72 | 73 | public mutating func normalize() -> CGPoint { 74 | self = normalized() 75 | 76 | return self 77 | } 78 | 79 | // ------------------------------------------------------------------------------ 80 | 81 | public func distanceTo(_ point: CGPoint) -> CGFloat { 82 | return (self - point).length() 83 | } 84 | 85 | // ------------------------------------------------------------------------------ 86 | 87 | public var angle: CGFloat { 88 | return atan2(y, x) 89 | } 90 | 91 | // ------------------------------------------------------------------------------ 92 | 93 | } 94 | 95 | // ------------------------------------------------------------------------------ 96 | 97 | public func + (left: CGPoint, right: CGPoint) -> CGPoint { 98 | return CGPoint(x: left.x + right.x, y: left.y + right.y) 99 | } 100 | 101 | // ------------------------------------------------------------------------------ 102 | 103 | public func += (left: inout CGPoint, right: CGPoint) { 104 | left = left + right 105 | } 106 | 107 | // ------------------------------------------------------------------------------ 108 | 109 | public func + (left: CGPoint, right: CGVector) -> CGPoint { 110 | return CGPoint(x: left.x + right.dx, y: left.y + right.dy) 111 | } 112 | 113 | // ------------------------------------------------------------------------------ 114 | 115 | public func += (left: inout CGPoint, right: CGVector) { 116 | left = left + right 117 | } 118 | 119 | // ------------------------------------------------------------------------------ 120 | 121 | public func - (left: CGPoint, right: CGPoint) -> CGPoint { 122 | return CGPoint(x: left.x - right.x, y: left.y - right.y) 123 | } 124 | 125 | // ------------------------------------------------------------------------------ 126 | 127 | public func -= (left: inout CGPoint, right: CGPoint) { 128 | left = left - right 129 | } 130 | 131 | // ------------------------------------------------------------------------------ 132 | 133 | public func - (left: CGPoint, right: CGVector) -> CGPoint { 134 | return CGPoint(x: left.x - right.dx, y: left.y - right.dy) 135 | } 136 | 137 | // ------------------------------------------------------------------------------ 138 | 139 | public func -= (left: inout CGPoint, right: CGVector) { 140 | left = left - right 141 | } 142 | 143 | // ------------------------------------------------------------------------------ 144 | 145 | public func * (left: CGPoint, right: CGPoint) -> CGPoint { 146 | return CGPoint(x: left.x * right.x, y: left.y * right.y) 147 | } 148 | 149 | // ------------------------------------------------------------------------------ 150 | 151 | public func *= (left: inout CGPoint, right: CGPoint) { 152 | left = left * right 153 | } 154 | 155 | // ------------------------------------------------------------------------------ 156 | 157 | public func * (point: CGPoint, scalar: CGFloat) -> CGPoint { 158 | return CGPoint(x: point.x * scalar, y: point.y * scalar) 159 | } 160 | 161 | // ------------------------------------------------------------------------------ 162 | 163 | public func *= (point: inout CGPoint, scalar: CGFloat) { 164 | point = point * scalar 165 | } 166 | 167 | // ------------------------------------------------------------------------------ 168 | 169 | public func * (left: CGPoint, right: CGVector) -> CGPoint { 170 | return CGPoint(x: left.x * right.dx, y: left.y * right.dy) 171 | } 172 | 173 | // ------------------------------------------------------------------------------ 174 | 175 | public func *= (left: inout CGPoint, right: CGVector) { 176 | left = left * right 177 | } 178 | 179 | // ------------------------------------------------------------------------------ 180 | 181 | public func / (left: CGPoint, right: CGPoint) -> CGPoint { 182 | return CGPoint(x: left.x / right.x, y: left.y / right.y) 183 | } 184 | 185 | // ------------------------------------------------------------------------------ 186 | 187 | public func /= (left: inout CGPoint, right: CGPoint) { 188 | left = left / right 189 | } 190 | 191 | // ------------------------------------------------------------------------------ 192 | 193 | public func / (point: CGPoint, scalar: CGFloat) -> CGPoint { 194 | return CGPoint(x: point.x / scalar, y: point.y / scalar) 195 | } 196 | 197 | // ------------------------------------------------------------------------------ 198 | 199 | public func /= (point: inout CGPoint, scalar: CGFloat) { 200 | point = point / scalar 201 | } 202 | 203 | // ------------------------------------------------------------------------------ 204 | 205 | public func / (left: CGPoint, right: CGVector) -> CGPoint { 206 | return CGPoint(x: left.x / right.dx, y: left.y / right.dy) 207 | } 208 | 209 | // ------------------------------------------------------------------------------ 210 | 211 | public func /= (left: inout CGPoint, right: CGVector) { 212 | left = left / right 213 | } 214 | 215 | // ------------------------------------------------------------------------------ 216 | 217 | public func lerp(_ start: CGPoint, end: CGPoint, t: CGFloat) -> CGPoint { 218 | return CGPoint(x: start.x + (end.x - start.x)*t, y: start.y + (end.y - start.y)*t) 219 | } 220 | 221 | // ------------------------------------------------------------------------------ 222 | -------------------------------------------------------------------------------- /shared/RB+SCNVector.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RB+SCNVector.swift 3 | // SCNVector3 extension 4 | // 5 | // Created by Roger Boesch on 1/09/17. 6 | // Copyright © 2017 Roger Boesch. All rights reserved. 7 | // 8 | 9 | import SceneKit 10 | 11 | extension SCNVector3 { 12 | 13 | func distance(to destination: SCNVector3) -> CGFloat { 14 | let dx = destination.x - x 15 | let dy = destination.y - y 16 | let dz = destination.z - z 17 | 18 | return CGFloat(sqrt(dx*dx + dy*dy + dz*dz)) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /shared/RB+SKAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKAction+Extensions.swift 3 | // SpriteKit action extensions 4 | // 5 | // Created by Roger Boesch on 19/09/13. 6 | // Copyright © 2015 Roger Boesch. All rights reserved. 7 | // 8 | 9 | import SpriteKit 10 | 11 | // ----------------------------------------------------------------------------- 12 | // MARK: - Timing functions 13 | 14 | func SKTTimingFunctionLinear(_ t: CGFloat) -> CGFloat { 15 | return t 16 | } 17 | 18 | func SKTCreateShakeFunction(_ oscillations: Int) -> (CGFloat) -> CGFloat { 19 | return { t in -pow(2.0, -10.0 * t) * sin(t * pi * CGFloat(oscillations) * 2.0) + 1.0 } 20 | } 21 | 22 | // ----------------------------------------------------------------------------- 23 | // MARK: - Effects 24 | 25 | class SKTEffect { 26 | 27 | unowned var node: SKNode 28 | var duration: TimeInterval 29 | open var timingFunction: ((CGFloat) -> CGFloat)? 30 | 31 | init(node: SKNode, duration: TimeInterval) { 32 | self.node = node 33 | self.duration = duration 34 | 35 | timingFunction = SKTTimingFunctionLinear 36 | } 37 | 38 | func update(_ t: CGFloat) { } 39 | } 40 | 41 | class SKTScaleEffect: SKTEffect { 42 | var startScale: CGPoint 43 | var delta: CGPoint 44 | var previousScale: CGPoint 45 | 46 | init(node: SKNode, duration: TimeInterval, startScale: CGPoint, endScale: CGPoint) { 47 | previousScale = CGPoint(x: node.xScale, y: node.yScale) 48 | self.startScale = startScale 49 | delta = endScale - startScale 50 | 51 | super.init(node: node, duration: duration) 52 | } 53 | 54 | override func update(_ t: CGFloat) { 55 | let newScale = startScale + delta*t 56 | let diff = newScale / previousScale 57 | previousScale = newScale 58 | node.xScale *= diff.x 59 | node.yScale *= diff.y 60 | } 61 | } 62 | 63 | // ----------------------------------------------------------------------------- 64 | // MARK: - Actions 65 | 66 | extension SKAction { 67 | 68 | class func actionWithEffect(_ effect: SKTEffect) -> SKAction { 69 | return SKAction.customAction(withDuration: effect.duration) { node, elapsedTime in 70 | var t = elapsedTime / CGFloat(effect.duration) 71 | 72 | if let timingFunction = effect.timingFunction { 73 | t = timingFunction(t) 74 | } 75 | 76 | effect.update(t) 77 | } 78 | } 79 | 80 | 81 | class func zoomWithNode(_ node: SKNode, amount: CGPoint, oscillations: Int, duration: TimeInterval) -> SKAction { 82 | let oldScale = CGPoint(x: node.xScale, y: node.yScale) 83 | let newScale = oldScale * amount 84 | 85 | let effect = SKTScaleEffect(node: node, duration: duration, startScale: newScale, endScale: oldScale) 86 | effect.timingFunction = SKTCreateShakeFunction(oscillations) 87 | 88 | return SKAction.actionWithEffect(effect) 89 | } 90 | 91 | // ------------------------------------------------------------------------- 92 | 93 | } 94 | -------------------------------------------------------------------------------- /shared/RB+UIColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RB+UIColor.swift 3 | // UIColor extensions 4 | // 5 | // Created by Roger Boesch on 01/11/15. 6 | // Copyright © 2016 Roger Boesch All rights reserved. 7 | // 8 | 9 | import SceneKit 10 | 11 | // ----------------------------------------------------------------------------- 12 | 13 | let UIRedColorList:[UIColor] = [ 14 | UIColor(hex: "#800000"), 15 | UIColor(hex: "#AA0000"), 16 | UIColor(hex: "#D40000"), 17 | UIColor(hex: "#FF0000"), 18 | UIColor(hex: "#FF2A2A") 19 | ] 20 | 21 | // ----------------------------------------------------------------------------- 22 | 23 | let UIPurpleColorList:[UIColor] = [ 24 | UIColor(hex: "#660067"), 25 | UIColor(hex: "#810080"), 26 | UIColor(hex: "#BE2AED"), 27 | UIColor(hex: "#D997FF"), 28 | UIColor(hex: "#F0BCFF") 29 | ] 30 | 31 | // ----------------------------------------------------------------------------- 32 | 33 | let UINeonColorList:[UIColor] = [ 34 | UIColor(hex: "#FF15AC"), 35 | UIColor(hex: "#FF6900"), 36 | UIColor(hex: "#FFFF01"), 37 | UIColor(hex: "#00FF01"), 38 | UIColor(hex: "#01E6f0") 39 | ] 40 | 41 | // ----------------------------------------------------------------------------- 42 | 43 | let UIGreenColorList:[UIColor] = [ 44 | UIColor(hex: "#075907"), 45 | UIColor(hex: "#097609"), 46 | UIColor(hex: "#70AF1A"), 47 | UIColor(hex: "#B9D40B"), 48 | UIColor(hex: "#E5EB0B") 49 | ] 50 | 51 | // ----------------------------------------------------------------------------- 52 | 53 | let UIGrayColorList:[UIColor] = [ 54 | UIColor(hex: "#C0CBCB"), 55 | UIColor(hex: "#CAC6BF"), 56 | UIColor(hex: "#B4ACA1"), 57 | UIColor(hex: "#E5E4dE"), 58 | UIColor(hex: "#C5C1B7"), 59 | UIColor(hex: "#78797D") 60 | ] 61 | 62 | // ----------------------------------------------------------------------------- 63 | // MARK: - Hex support 64 | 65 | extension UIColor { 66 | 67 | public convenience init(hex: String) { 68 | var str = hex 69 | 70 | if str.hasPrefix("#") { 71 | let index = str.index(str.startIndex, offsetBy: 1) 72 | str = String(str[index...]) 73 | } 74 | 75 | if (str.count == 6) { 76 | str = "\(str)ff" 77 | } 78 | 79 | if str.count == 8 { 80 | let scanner = Scanner(string: String(str)) 81 | var hexNumber: UInt64 = 0 82 | 83 | if scanner.scanHexInt64(&hexNumber) { 84 | let r = (hexNumber & 0xff000000) >> 24 85 | let g = (hexNumber & 0x00ff0000) >> 16 86 | let b = (hexNumber & 0x0000ff00) >> 8 87 | let a = (hexNumber & 0x000000ff) 88 | 89 | self.init(red: CGFloat(r)/255.0, green: CGFloat(g)/255.0, blue: CGFloat(b)/255.0, alpha: CGFloat(a)/255.0) 90 | return 91 | } 92 | } 93 | 94 | self.init(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0) 95 | } 96 | } 97 | 98 | // ----------------------------------------------------------------------------- 99 | // MARK: - Random colors 100 | 101 | extension UIColor { 102 | 103 | public static func random(list: [UIColor]) -> UIColor { 104 | let maxValue = list.count 105 | let rand = RBRandom.integer(0, maxValue-1) 106 | 107 | return list[rand] 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /shared/RBLog.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RBLog.swift 3 | // (Simple-) Logging functionality 4 | // 5 | // Created by Roger Boesch on 02/04/16. 6 | // Copyright © 2016 Roger Boesch All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SceneKit 11 | 12 | enum RBLogSeverity : Int { 13 | case debug = 0 14 | case info = 1 15 | case warning = 2 16 | case error = 3 17 | case none = 4 18 | } 19 | 20 | public class RBLog: NSObject { 21 | static var _severity = RBLogSeverity.debug 22 | 23 | // ----------------------------------------------------------------------------- 24 | // MARK: - Properties 25 | 26 | static var severity: RBLogSeverity { 27 | get { 28 | return _severity 29 | } 30 | set(value) { 31 | _severity = value 32 | } 33 | } 34 | 35 | // ------------------------------------------------------------------------- 36 | // MARK: - Logging severity 37 | 38 | static func error(message: String) { 39 | if (RBLogSeverity.error.rawValue >= RBLog.severity.rawValue) { 40 | RBLog.log(message: message, severity: "⛔️") 41 | } 42 | } 43 | 44 | // ------------------------------------------------------------------------- 45 | 46 | static func warning(message: String) { 47 | if (RBLogSeverity.warning.rawValue >= RBLog.severity.rawValue) { 48 | RBLog.log(message: message, severity: "⚠️") 49 | } 50 | } 51 | 52 | // ------------------------------------------------------------------------- 53 | 54 | static func info(message: String) { 55 | if (RBLogSeverity.info.rawValue >= RBLog.severity.rawValue) { 56 | RBLog.log(message: message, severity: "▷") 57 | } 58 | } 59 | 60 | // ------------------------------------------------------------------------- 61 | 62 | static func debug(message: String) { 63 | if (RBLogSeverity.debug.rawValue >= RBLog.severity.rawValue) { 64 | RBLog.log(message: message, severity: "→") 65 | } 66 | } 67 | 68 | // ------------------------------------------------------------------------- 69 | // MARK: - Write logs 70 | 71 | private static func log(message: String, severity: String) { 72 | RBLog.write(message: "\(severity) \(message)") 73 | } 74 | 75 | // ------------------------------------------------------------------------- 76 | 77 | public static func write(message: String) { 78 | print(message) 79 | } 80 | 81 | // ------------------------------------------------------------------------- 82 | 83 | } 84 | 85 | // ----------------------------------------------------------------------------- 86 | // MARK: - Short functions 87 | 88 | func rbError(_ message: String, _ args: CVarArg...) { 89 | let str = String(format: message, arguments: args) 90 | RBLog.error(message: str) 91 | } 92 | 93 | // ----------------------------------------------------------------------------- 94 | 95 | func rbWarning(_ message: String, _ args: CVarArg...) { 96 | let str = String(format: message, arguments: args) 97 | RBLog.warning(message: str) 98 | } 99 | 100 | // ----------------------------------------------------------------------------- 101 | 102 | func rbInfo(_ message: String, _ args: CVarArg...) { 103 | let str = String(format: message, arguments: args) 104 | RBLog.info(message: str) 105 | } 106 | 107 | // ----------------------------------------------------------------------------- 108 | 109 | func rbDebug(_ message: String, _ args: CVarArg...) { 110 | let str = String(format: message, arguments: args) 111 | RBLog.debug(message: str) 112 | } 113 | 114 | // ----------------------------------------------------------------------------- 115 | -------------------------------------------------------------------------------- /shared/RBPerlinNoiseGenerator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RBPerlinNoiseGenerator.swift 3 | // Perlin noise generator (used for terrain class) 4 | // 5 | // Created by Roger Boesch on 12/07/16. 6 | // Based on Obj-C code created by Steven Troughton-Smith on 24/12/11. 7 | // 8 | 9 | import UIKit 10 | 11 | class RBPerlinNoiseGenerator { 12 | private static let noiseX = 1619 13 | private static let noiseY = 31337 14 | private static let noiseSeed = 1013 15 | 16 | private var _seed: Int = 1 17 | 18 | // ------------------------------------------------------------------------- 19 | 20 | private func interpolate(a: Double, b: Double, x: Double) ->Double { 21 | let ft: Double = x * Double.pi 22 | let f: Double = (1.0-cos(ft)) * 0.5 23 | 24 | return a*(1.0-f)+b*f 25 | } 26 | 27 | // ------------------------------------------------------------------------- 28 | 29 | private func findNoise(x: Double, y: Double) ->Double { 30 | var n = (RBPerlinNoiseGenerator.noiseX*Int(x) + 31 | RBPerlinNoiseGenerator.noiseY*Int(y) + 32 | RBPerlinNoiseGenerator.noiseSeed * _seed) & 0x7fffffff 33 | 34 | n = (n >> 13) ^ n 35 | n = (n &* (n &* n &* 60493 + 19990303) + 1376312589) & 0x7fffffff 36 | 37 | return 1.0 - Double(n)/1073741824 38 | } 39 | 40 | // ------------------------------------------------------------------------- 41 | 42 | private func noise(x: Double, y: Double) ->Double { 43 | let floorX: Double = Double(Int(x)) 44 | let floorY: Double = Double(Int(y)) 45 | 46 | let s = findNoise(x:floorX, y:floorY) 47 | let t = findNoise(x:floorX+1, y:floorY) 48 | let u = findNoise(x:floorX, y:floorY+1) 49 | let v = findNoise(x:floorX+1, y:floorY+1) 50 | 51 | let i1 = interpolate(a:s, b:t, x:x-floorX) 52 | let i2 = interpolate(a:u, b:v, x:x-floorX) 53 | 54 | return interpolate(a:i1, b:i2, x:y-floorY) 55 | } 56 | 57 | // ------------------------------------------------------------------------- 58 | // MARK: - Calculate a noise value for x,y 59 | 60 | func valueFor(x: Int32, y: Int32) ->Double { 61 | let octaves = 2 62 | let p: Double = 1/2 63 | let zoom: Double = 6 64 | var getnoise: Double = 0 65 | 66 | for a in 0.. 255) { 76 | value = 255 77 | } 78 | else if (value < 0) { 79 | value = 0 80 | } 81 | 82 | return value 83 | } 84 | 85 | // ------------------------------------------------------------------------- 86 | // MARK: - Initialisation 87 | 88 | init(seed: Int? = nil) { 89 | if (seed == nil) { 90 | _seed = Int(arc4random()) % Int(INT32_MAX) 91 | } 92 | else { 93 | _seed = seed! 94 | } 95 | } 96 | 97 | // ------------------------------------------------------------------------- 98 | 99 | } 100 | -------------------------------------------------------------------------------- /shared/RBRandom.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RBRandom.swift 3 | // Random number extensions 4 | // 5 | // Created by Roger Boesch on 13/01/17. 6 | // Copyright © 2017 Roger Boesch. All rights reserved. 7 | // 8 | // This class I have newly introduced in Part 3. It's mainly and abstraction 9 | // class for the classes and methods available in GameKit. It allows a better 10 | // random handling then those available in system library. 11 | 12 | import Foundation 13 | import GameKit 14 | 15 | extension Int { 16 | static func random(maxValue: Int) -> Int { 17 | let rand = Int(arc4random_uniform(UInt32(maxValue))) 18 | return rand 19 | } 20 | } 21 | 22 | class RBRandom { 23 | private let source = GKMersenneTwisterRandomSource() 24 | 25 | // ------------------------------------------------------------------------- 26 | // MARK: - Get random numbers 27 | 28 | class func boolean() -> Bool { 29 | if RBRandom.sharedInstance.integer(0, 1) == 1 { 30 | return true 31 | } 32 | 33 | return false 34 | } 35 | 36 | // ------------------------------------------------------------------------- 37 | 38 | class func integer(_ from: Int, _ to: Int) -> Int { 39 | return RBRandom.sharedInstance.integer(from, to) 40 | } 41 | 42 | // ------------------------------------------------------------------------- 43 | 44 | class func timeInterval(_ from: Int, _ to: Int) -> TimeInterval { 45 | return TimeInterval(RBRandom.sharedInstance.integer(from, to)) 46 | } 47 | 48 | // ------------------------------------------------------------------------- 49 | 50 | class func cgFloat(_ from: CGFloat, _ to: CGFloat) -> CGFloat { 51 | return CGFloat(RBRandom.sharedInstance.integer(Int(from), Int(to))) 52 | } 53 | 54 | // ------------------------------------------------------------------------- 55 | 56 | private func integer(_ from: Int, _ to: Int) -> Int { 57 | let rd = GKRandomDistribution(randomSource: self.source, lowestValue: from, highestValue: to) 58 | let number = rd.nextInt() 59 | 60 | return number 61 | } 62 | 63 | // ------------------------------------------------------------------------- 64 | // MARK: - Initialisation 65 | 66 | init() { 67 | source.seed = UInt64(CFAbsoluteTimeGetCurrent()) 68 | } 69 | 70 | // ------------------------------------------------------------------------- 71 | 72 | private static let sharedInstance : RBRandom = { 73 | let instance = RBRandom() 74 | return instance 75 | }() 76 | 77 | // ------------------------------------------------------------------------- 78 | 79 | } 80 | 81 | -------------------------------------------------------------------------------- /shared/RBTerrain.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RBTerrain.swift 3 | // Terrain creation 4 | // 5 | // Created by Roger Boesch on 12/07/16. 6 | // Inspired by Obj-C code created by Steven Troughton-Smith on 24/12/11. 7 | // 8 | 9 | import Foundation 10 | import SceneKit 11 | 12 | typealias RBTerrainFormula = ((Int32, Int32) -> (Double)) 13 | 14 | // ----------------------------------------------------------------------------- 15 | 16 | class RBTerrain: SCNNode { 17 | private var _heightScale = 256 18 | private var _terrainWidth = 32 19 | private var _terrainLength = 32 20 | private var _terrainGeometry: SCNGeometry? 21 | private var _texture: UIImage? 22 | private var _color = UIColor.white 23 | 24 | var formula: RBTerrainFormula? 25 | 26 | // ------------------------------------------------------------------------- 27 | // MARK: - Properties 28 | 29 | var length: Int { 30 | get { 31 | return _terrainLength 32 | } 33 | } 34 | 35 | // ------------------------------------------------------------------------- 36 | 37 | var width: Int { 38 | get { 39 | return _terrainLength 40 | } 41 | } 42 | 43 | // ------------------------------------------------------------------------- 44 | 45 | var texture: UIImage? { 46 | get { 47 | return _texture 48 | } 49 | set(value) { 50 | _texture = value 51 | 52 | if (_terrainGeometry != nil && _texture != nil) { 53 | let material = SCNMaterial() 54 | material.diffuse.contents = _texture! 55 | material.isLitPerPixel = true 56 | material.diffuse.magnificationFilter = .none 57 | material.diffuse.wrapS = .repeat 58 | material.diffuse.wrapT = .repeat 59 | material.diffuse.contentsTransform = SCNMatrix4MakeScale(Float(_terrainWidth*2), Float(_terrainLength*2), 1) 60 | 61 | _terrainGeometry!.firstMaterial = material 62 | _terrainGeometry!.firstMaterial!.isDoubleSided = true 63 | } 64 | } 65 | } 66 | 67 | // ------------------------------------------------------------------------- 68 | 69 | var color: UIColor { 70 | get { 71 | return _color 72 | } 73 | set(value) { 74 | _color = value 75 | 76 | if (_terrainGeometry != nil) { 77 | let material = SCNMaterial() 78 | material.diffuse.contents = _color 79 | material.isLitPerPixel = true 80 | 81 | _terrainGeometry!.firstMaterial = material 82 | _terrainGeometry!.firstMaterial!.isDoubleSided = true 83 | } 84 | } 85 | } 86 | 87 | // ------------------------------------------------------------------------- 88 | // MARK: - Terrain formula 89 | 90 | func valueFor(x: Int32, y: Int32) ->Double { 91 | if (formula == nil) { 92 | return 0.0 93 | } 94 | 95 | return formula!(x, y) 96 | } 97 | 98 | // ------------------------------------------------------------------------- 99 | // MARK: - Geometry creation 100 | 101 | private func createGeometry() ->SCNGeometry { 102 | let cint: CInt = 0 103 | let sizeOfCInt = MemoryLayout.size(ofValue: cint) 104 | let float: Float = 0.0 105 | let sizeOfFloat = MemoryLayout.size(ofValue: float) 106 | let vec2: vector_float2 = vector2(0, 0) 107 | let sizeOfVecFloat = MemoryLayout.size(ofValue: vec2) 108 | 109 | let w: CGFloat = CGFloat(_terrainWidth) 110 | let h: CGFloat = CGFloat(_terrainLength) 111 | let scale: Double = Double(_heightScale) 112 | 113 | var sources = [SCNGeometrySource]() 114 | var elements = [SCNGeometryElement]() 115 | 116 | let maxElements: Int = _terrainWidth * _terrainLength * 4 117 | var vertices = [SCNVector3](repeating:SCNVector3Zero, count:maxElements) 118 | var normals = [SCNVector3](repeating:SCNVector3Zero, count:maxElements) 119 | var uvList: [vector_float2] = [] 120 | 121 | var vertexCount = 0 122 | let factor: CGFloat = 0.5 123 | 124 | for y in 0...Int(h-2) { 125 | for x in 0...Int(w-1) { 126 | let topLeftZ = valueFor(x: Int32(x), y: Int32(y+1)) / scale 127 | let topRightZ = valueFor(x: Int32(x+1), y: Int32(y+1)) / scale 128 | let bottomLeftZ = valueFor(x: Int32(x), y: Int32(y)) / scale 129 | let bottomRightZ = valueFor(x: Int32(x+1), y: Int32(y)) / scale 130 | 131 | let topLeft = SCNVector3Make(Float(x)-Float(factor), Float(topLeftZ), Float(y)+Float(factor)) 132 | let topRight = SCNVector3Make(Float(x)+Float(factor), Float(topRightZ), Float(y)+Float(factor)) 133 | let bottomLeft = SCNVector3Make(Float(x)-Float(factor), Float(bottomLeftZ), Float(y)-Float(factor)) 134 | let bottomRight = SCNVector3Make(Float(x)+Float(factor), Float(bottomRightZ), Float(y)-Float(factor)) 135 | 136 | vertices[vertexCount] = bottomLeft 137 | vertices[vertexCount+1] = topLeft 138 | vertices[vertexCount+2] = topRight 139 | vertices[vertexCount+3] = bottomRight 140 | 141 | let xf = CGFloat(x) 142 | let yf = CGFloat(y) 143 | 144 | uvList.append(vector_float2(Float(xf/w), Float(yf/h))) 145 | uvList.append(vector_float2(Float(xf/w), Float((yf+factor)/h))) 146 | uvList.append(vector_float2(Float((xf+factor)/w), Float((yf+factor)/h))) 147 | uvList.append(vector_float2(Float((xf+factor)/w), Float(yf/h))) 148 | 149 | vertexCount += 4 150 | } 151 | } 152 | 153 | let source = SCNGeometrySource(vertices: vertices) 154 | sources.append(source) 155 | 156 | let geometryData = NSMutableData() 157 | 158 | var geometry: CInt = 0 159 | while (geometry < CInt(vertexCount)) { 160 | let bytes: [CInt] = [geometry, geometry+2, geometry+3, geometry, geometry+1, geometry+2] 161 | geometryData.append(bytes, length: sizeOfCInt*6) 162 | geometry += 4 163 | } 164 | 165 | let element = SCNGeometryElement(data: geometryData as Data, primitiveType: .triangles, primitiveCount: vertexCount/2, bytesPerIndex: sizeOfCInt) 166 | elements.append(element) 167 | 168 | for normalIndex in 0...vertexCount-1 { 169 | normals[normalIndex] = SCNVector3Make(0, 0, -1) 170 | } 171 | sources.append(SCNGeometrySource(normals: normals)) 172 | 173 | let uvData = NSData(bytes: uvList, length: uvList.count * sizeOfVecFloat) 174 | let uvSource = SCNGeometrySource(data: uvData as Data, semantic: SCNGeometrySource.Semantic.texcoord, vectorCount: uvList.count, usesFloatComponents: true, componentsPerVector: 2, bytesPerComponent: sizeOfFloat, dataOffset: 0, dataStride: sizeOfVecFloat) 175 | sources.append(uvSource) 176 | 177 | _terrainGeometry = SCNGeometry(sources: sources, elements: elements) 178 | 179 | let material = SCNMaterial() 180 | material.isLitPerPixel = true 181 | material.diffuse.magnificationFilter = .none 182 | material.diffuse.wrapS = .repeat 183 | material.diffuse.wrapT = .repeat 184 | material.diffuse.contentsTransform = SCNMatrix4MakeScale(Float(_terrainWidth*2), Float(_terrainLength*2), 1) 185 | material.diffuse.intensity = 1.0 186 | 187 | _terrainGeometry!.firstMaterial = material 188 | _terrainGeometry!.firstMaterial!.isDoubleSided = true 189 | 190 | return _terrainGeometry! 191 | } 192 | 193 | // ------------------------------------------------------------------------- 194 | // MARK: - Create terrain 195 | 196 | func create(withImage image: UIImage?) { 197 | let terrainNode = SCNNode(geometry: createGeometry()) 198 | self.addChildNode(terrainNode) 199 | 200 | if (image != nil) { 201 | self.texture = image 202 | } 203 | else { 204 | self.color = UIColor.green 205 | } 206 | } 207 | 208 | // ------------------------------------------------------------------------- 209 | 210 | func create(withColor color: UIColor) { 211 | let terrainNode = SCNNode(geometry: createGeometry()) 212 | self.addChildNode(terrainNode) 213 | 214 | self.color = color 215 | } 216 | 217 | // ------------------------------------------------------------------------- 218 | // MARK: - Initialisation 219 | 220 | init(width: Int, length: Int, scale: Int) { 221 | super.init() 222 | 223 | _terrainWidth = width 224 | _terrainLength = length 225 | _heightScale = scale 226 | } 227 | 228 | // ------------------------------------------------------------------------- 229 | 230 | required init(coder: NSCoder) { 231 | fatalError("Not yet implemented") 232 | } 233 | 234 | // ------------------------------------------------------------------------- 235 | } 236 | -------------------------------------------------------------------------------- /shared/RBUtility.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RBUtility.swift 3 | // General utilities 4 | // 5 | // Created by Roger Boesch on 19/09/15. 6 | // Copyright © 2015 Roger Boesch. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | let pi = CGFloat(Double.pi) 12 | 13 | // ----------------------------------------------------------------------------- 14 | // MARK: - Deg/Rad 15 | 16 | func degreesToRadians(value: CGFloat) -> CGFloat { 17 | return value * pi / 180.0 18 | } 19 | 20 | // ----------------------------------------------------------------------------- 21 | 22 | func radiansToDegrees(value: CGFloat) -> CGFloat { 23 | return value * 180.0 / pi 24 | } 25 | 26 | // ----------------------------------------------------------------------------- 27 | 28 | typealias RunClosure = () -> () 29 | 30 | public class Run { 31 | 32 | static func after(_ timeInterval: TimeInterval, _ closure: @escaping RunClosure) { 33 | let when = DispatchTime.now() + timeInterval 34 | DispatchQueue.main.asyncAfter(deadline: when) { 35 | closure() 36 | } 37 | } 38 | 39 | } 40 | 41 | // ----------------------------------------------------------------------------- 42 | -------------------------------------------------------------------------------- /sounds/bonus.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rogerboesch/SceneKitTutorial/7fbe31110fc6a4bba43dbdde79cf26844e63ed11/sounds/bonus.wav -------------------------------------------------------------------------------- /sounds/explosion.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rogerboesch/SceneKitTutorial/7fbe31110fc6a4bba43dbdde79cf26844e63ed11/sounds/explosion.wav -------------------------------------------------------------------------------- /sounds/fire.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rogerboesch/SceneKitTutorial/7fbe31110fc6a4bba43dbdde79cf26844e63ed11/sounds/fire.wav -------------------------------------------------------------------------------- /tutorial.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | --------------------------------------------------------------------------------