From e7613cf9117d2a8b2cdc87680cf21e0d6a24cbf8 Mon Sep 17 00:00:00 2001 From: Luke Schoen Date: Fri, 4 Dec 2015 18:32:46 -0300 Subject: [PATCH 1/3] Moved protocol implementations into class extensions to reduce fat controller anti-pattern --- FPSControls.xcodeproj/project.pbxproj | 16 ++++ .../GestureRecognizerDelegateExtension.swift | 27 +++++++ .../SceneRendererDelegateExtension.swift | 68 +++++++++++++++++ FPSControls/ViewController.swift | 73 +------------------ 4 files changed, 112 insertions(+), 72 deletions(-) create mode 100644 FPSControls/GestureRecognizerDelegateExtension.swift create mode 100644 FPSControls/SceneRendererDelegateExtension.swift diff --git a/FPSControls.xcodeproj/project.pbxproj b/FPSControls.xcodeproj/project.pbxproj index 7304e40..208c536 100644 --- a/FPSControls.xcodeproj/project.pbxproj +++ b/FPSControls.xcodeproj/project.pbxproj @@ -17,6 +17,8 @@ 01D1D1A71A29EEDF0067D6D3 /* Tile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D1D1A51A29EEDF0067D6D3 /* Tile.swift */; }; 01D1D1AC1A29EF320067D6D3 /* Map.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D1D1AB1A29EF320067D6D3 /* Map.swift */; }; 01D1D1AE1A29F7B00067D6D3 /* FireGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D1D1AD1A29F7B00067D6D3 /* FireGestureRecognizer.swift */; }; + 49E5D23E1C1238B600F99427 /* GestureRecognizerDelegateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E5D23D1C1238B600F99427 /* GestureRecognizerDelegateExtension.swift */; }; + 49E5D2401C12396500F99427 /* SceneRendererDelegateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E5D23F1C12396500F99427 /* SceneRendererDelegateExtension.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -32,6 +34,8 @@ 01D1D1A51A29EEDF0067D6D3 /* Tile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Tile.swift; sourceTree = ""; }; 01D1D1AB1A29EF320067D6D3 /* Map.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Map.swift; sourceTree = ""; }; 01D1D1AD1A29F7B00067D6D3 /* FireGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FireGestureRecognizer.swift; sourceTree = ""; }; + 49E5D23D1C1238B600F99427 /* GestureRecognizerDelegateExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GestureRecognizerDelegateExtension.swift; sourceTree = ""; }; + 49E5D23F1C12396500F99427 /* SceneRendererDelegateExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneRendererDelegateExtension.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -95,6 +99,7 @@ 01D1D1A91A29EF040067D6D3 /* Controllers */ = { isa = PBXGroup; children = ( + 49E5D23C1C12384200F99427 /* Extensions */, 01558A351A0291A700083EB1 /* AppDelegate.swift */, 01558A371A0291A700083EB1 /* ViewController.swift */, 01D1D1AD1A29F7B00067D6D3 /* FireGestureRecognizer.swift */, @@ -113,6 +118,15 @@ name = Resources; sourceTree = ""; }; + 49E5D23C1C12384200F99427 /* Extensions */ = { + isa = PBXGroup; + children = ( + 49E5D23D1C1238B600F99427 /* GestureRecognizerDelegateExtension.swift */, + 49E5D23F1C12396500F99427 /* SceneRendererDelegateExtension.swift */, + ); + name = Extensions; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -189,7 +203,9 @@ 01558A381A0291A700083EB1 /* ViewController.swift in Sources */, 01D1D1A61A29EEDF0067D6D3 /* Entity.swift in Sources */, 01D1D1AE1A29F7B00067D6D3 /* FireGestureRecognizer.swift in Sources */, + 49E5D2401C12396500F99427 /* SceneRendererDelegateExtension.swift in Sources */, 01D1D1A71A29EEDF0067D6D3 /* Tile.swift in Sources */, + 49E5D23E1C1238B600F99427 /* GestureRecognizerDelegateExtension.swift in Sources */, 01558A361A0291A700083EB1 /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/FPSControls/GestureRecognizerDelegateExtension.swift b/FPSControls/GestureRecognizerDelegateExtension.swift new file mode 100644 index 0000000..2132815 --- /dev/null +++ b/FPSControls/GestureRecognizerDelegateExtension.swift @@ -0,0 +1,27 @@ +// +// GestureRecognizerDelegateExtension.swift +// FPSControls +// +// Created by Luke Schoen on 4/12/2015. +// Copyright © 2015 Nick Lockwood. All rights reserved. +// + +import UIKit + +extension ViewController: UIGestureRecognizerDelegate { + + func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool { + + if gestureRecognizer == lookGesture { + return touch.locationInView(view).x > view.frame.size.width / 2 + } else if gestureRecognizer == walkGesture { + return touch.locationInView(view).x < view.frame.size.width / 2 + } + return true + } + + func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool { + + return true + } +} diff --git a/FPSControls/SceneRendererDelegateExtension.swift b/FPSControls/SceneRendererDelegateExtension.swift new file mode 100644 index 0000000..02ece81 --- /dev/null +++ b/FPSControls/SceneRendererDelegateExtension.swift @@ -0,0 +1,68 @@ +// +// SceneRendererDelegateExtension.swift +// FPSControls +// +// Created by Luke Schoen on 4/12/2015. +// Copyright © 2015 Nick Lockwood. All rights reserved. +// + +import Foundation +import SceneKit + +extension ViewController: SCNSceneRendererDelegate { + + func renderer(aRenderer: SCNSceneRenderer, updateAtTime time: NSTimeInterval) { + + //get walk gesture translation + let translation = walkGesture.translationInView(self.view) + + //create impulse vector for hero + let angle = heroNode.presentationNode.rotation.w * heroNode.presentationNode.rotation.y + var impulse = SCNVector3(x: max(-1, min(1, Float(translation.x) / 50)), y: 0, z: max(-1, min(1, Float(-translation.y) / 50))) + impulse = SCNVector3( + x: impulse.x * cos(angle) - impulse.z * sin(angle), + y: 0, + z: impulse.x * -sin(angle) - impulse.z * cos(angle) + ) + heroNode.physicsBody?.applyForce(impulse, impulse: true) + + //handle firing + let now = CFAbsoluteTimeGetCurrent() + if now - lastTappedFire < autofireTapTimeThreshold { + let fireRate = min(Double(maxRoundsPerSecond), Double(tapCount) / autofireTapTimeThreshold) + if now - lastFired > 1 / fireRate { + + //get hero direction vector + let angle = heroNode.presentationNode.rotation.w * heroNode.presentationNode.rotation.y + var direction = SCNVector3(x: -sin(angle), y: 0, z: -cos(angle)) + + //get elevation + direction = SCNVector3(x: cos(elevation) * direction.x, y: sin(elevation), z: cos(elevation) * direction.z) + + //create or recycle bullet node + let bulletNode: SCNNode = { + if self.bullets.count < self.maxBullets { + return SCNNode() + } else { + return self.bullets.removeAtIndex(0) + } + }() + bullets.append(bulletNode) + bulletNode.geometry = SCNBox(width: CGFloat(bulletRadius) * 2, height: CGFloat(bulletRadius) * 2, length: CGFloat(bulletRadius) * 2, chamferRadius: CGFloat(bulletRadius)) + bulletNode.position = SCNVector3(x: heroNode.presentationNode.position.x, y: 0.4, z: heroNode.presentationNode.position.z) + bulletNode.physicsBody = SCNPhysicsBody(type: .Dynamic, shape: SCNPhysicsShape(geometry: bulletNode.geometry!, options: nil)) + bulletNode.physicsBody?.categoryBitMask = CollisionCategory.Bullet + bulletNode.physicsBody?.collisionBitMask = CollisionCategory.All ^ CollisionCategory.Hero + bulletNode.physicsBody?.velocityFactor = SCNVector3(x: 1, y: 0.5, z: 1) + self.sceneView.scene!.rootNode.addChildNode(bulletNode) + + //apply impulse + let impulse = SCNVector3(x: direction.x * Float(bulletImpulse), y: direction.y * Float(bulletImpulse), z: direction.z * Float(bulletImpulse)) + bulletNode.physicsBody?.applyForce(impulse, impulse: true) + + //update timestamp + lastFired = now + } + } + } +} diff --git a/FPSControls/ViewController.swift b/FPSControls/ViewController.swift index 58cfedd..322beaf 100644 --- a/FPSControls/ViewController.swift +++ b/FPSControls/ViewController.swift @@ -19,7 +19,7 @@ struct CollisionCategory { static let Bullet: Int = 0b00001000 } -class ViewController: UIViewController, UIGestureRecognizerDelegate, SCNSceneRendererDelegate { +class ViewController: UIViewController { //MARK: config let autofireTapTimeThreshold = 0.2 @@ -206,21 +206,6 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate, SCNSceneRen } } - func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool { - - if gestureRecognizer == lookGesture { - return touch.locationInView(view).x > view.frame.size.width / 2 - } else if gestureRecognizer == walkGesture { - return touch.locationInView(view).x < view.frame.size.width / 2 - } - return true - } - - func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool { - - return true - } - func lookGestureRecognized(gesture: UIPanGestureRecognizer) { //get translation and convert to rotation @@ -257,60 +242,4 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate, SCNSceneRen } lastTappedFire = now } - - func renderer(aRenderer: SCNSceneRenderer, updateAtTime time: NSTimeInterval) { - - //get walk gesture translation - let translation = walkGesture.translationInView(self.view) - - //create impulse vector for hero - let angle = heroNode.presentationNode.rotation.w * heroNode.presentationNode.rotation.y - var impulse = SCNVector3(x: max(-1, min(1, Float(translation.x) / 50)), y: 0, z: max(-1, min(1, Float(-translation.y) / 50))) - impulse = SCNVector3( - x: impulse.x * cos(angle) - impulse.z * sin(angle), - y: 0, - z: impulse.x * -sin(angle) - impulse.z * cos(angle) - ) - heroNode.physicsBody?.applyForce(impulse, impulse: true) - - //handle firing - let now = CFAbsoluteTimeGetCurrent() - if now - lastTappedFire < autofireTapTimeThreshold { - let fireRate = min(Double(maxRoundsPerSecond), Double(tapCount) / autofireTapTimeThreshold) - if now - lastFired > 1 / fireRate { - - //get hero direction vector - let angle = heroNode.presentationNode.rotation.w * heroNode.presentationNode.rotation.y - var direction = SCNVector3(x: -sin(angle), y: 0, z: -cos(angle)) - - //get elevation - direction = SCNVector3(x: cos(elevation) * direction.x, y: sin(elevation), z: cos(elevation) * direction.z) - - //create or recycle bullet node - let bulletNode: SCNNode = { - if self.bullets.count < self.maxBullets { - return SCNNode() - } else { - return self.bullets.removeAtIndex(0) - } - }() - bullets.append(bulletNode) - bulletNode.geometry = SCNBox(width: CGFloat(bulletRadius) * 2, height: CGFloat(bulletRadius) * 2, length: CGFloat(bulletRadius) * 2, chamferRadius: CGFloat(bulletRadius)) - bulletNode.position = SCNVector3(x: heroNode.presentationNode.position.x, y: 0.4, z: heroNode.presentationNode.position.z) - bulletNode.physicsBody = SCNPhysicsBody(type: .Dynamic, shape: SCNPhysicsShape(geometry: bulletNode.geometry!, options: nil)) - bulletNode.physicsBody?.categoryBitMask = CollisionCategory.Bullet - bulletNode.physicsBody?.collisionBitMask = CollisionCategory.All ^ CollisionCategory.Hero - bulletNode.physicsBody?.velocityFactor = SCNVector3(x: 1, y: 0.5, z: 1) - self.sceneView.scene!.rootNode.addChildNode(bulletNode) - - //apply impulse - let impulse = SCNVector3(x: direction.x * Float(bulletImpulse), y: direction.y * Float(bulletImpulse), z: direction.z * Float(bulletImpulse)) - bulletNode.physicsBody?.applyForce(impulse, impulse: true) - - //update timestamp - lastFired = now - } - } - } } - From aa94113a9c124654ce99359bb534b8c69881141f Mon Sep 17 00:00:00 2001 From: Luke Schoen Date: Sat, 5 Dec 2015 01:56:15 -0300 Subject: [PATCH 2/3] Refactored scene using singleton pattern --- FPSControls.xcodeproj/project.pbxproj | 32 ++- .../GestureRecognizerDelegateExtension.swift | 27 -- FPSControls/GestureRecognizerExtension.swift | 85 +++++++ FPSControls/Scene.swift | 207 ++++++++++++++++ ...ion.swift => SceneRendererExtension.swift} | 18 +- FPSControls/ViewController.swift | 230 +++--------------- 6 files changed, 351 insertions(+), 248 deletions(-) delete mode 100644 FPSControls/GestureRecognizerDelegateExtension.swift create mode 100644 FPSControls/GestureRecognizerExtension.swift create mode 100644 FPSControls/Scene.swift rename FPSControls/{SceneRendererDelegateExtension.swift => SceneRendererExtension.swift} (71%) diff --git a/FPSControls.xcodeproj/project.pbxproj b/FPSControls.xcodeproj/project.pbxproj index 208c536..9a6ebdf 100644 --- a/FPSControls.xcodeproj/project.pbxproj +++ b/FPSControls.xcodeproj/project.pbxproj @@ -17,8 +17,9 @@ 01D1D1A71A29EEDF0067D6D3 /* Tile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D1D1A51A29EEDF0067D6D3 /* Tile.swift */; }; 01D1D1AC1A29EF320067D6D3 /* Map.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D1D1AB1A29EF320067D6D3 /* Map.swift */; }; 01D1D1AE1A29F7B00067D6D3 /* FireGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D1D1AD1A29F7B00067D6D3 /* FireGestureRecognizer.swift */; }; - 49E5D23E1C1238B600F99427 /* GestureRecognizerDelegateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E5D23D1C1238B600F99427 /* GestureRecognizerDelegateExtension.swift */; }; - 49E5D2401C12396500F99427 /* SceneRendererDelegateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E5D23F1C12396500F99427 /* SceneRendererDelegateExtension.swift */; }; + 49E5D23E1C1238B600F99427 /* GestureRecognizerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E5D23D1C1238B600F99427 /* GestureRecognizerExtension.swift */; }; + 49E5D2421C12417700F99427 /* Scene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E5D2411C12417700F99427 /* Scene.swift */; }; + 49E5D2491C12A00100F99427 /* SceneRendererExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E5D2481C12A00100F99427 /* SceneRendererExtension.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -34,8 +35,9 @@ 01D1D1A51A29EEDF0067D6D3 /* Tile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Tile.swift; sourceTree = ""; }; 01D1D1AB1A29EF320067D6D3 /* Map.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Map.swift; sourceTree = ""; }; 01D1D1AD1A29F7B00067D6D3 /* FireGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FireGestureRecognizer.swift; sourceTree = ""; }; - 49E5D23D1C1238B600F99427 /* GestureRecognizerDelegateExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GestureRecognizerDelegateExtension.swift; sourceTree = ""; }; - 49E5D23F1C12396500F99427 /* SceneRendererDelegateExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneRendererDelegateExtension.swift; sourceTree = ""; }; + 49E5D23D1C1238B600F99427 /* GestureRecognizerExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GestureRecognizerExtension.swift; sourceTree = ""; }; + 49E5D2411C12417700F99427 /* Scene.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Scene.swift; sourceTree = ""; }; + 49E5D2481C12A00100F99427 /* SceneRendererExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneRendererExtension.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -70,6 +72,7 @@ 01558A321A0291A700083EB1 /* FPSControls */ = { isa = PBXGroup; children = ( + 49E5D2431C12418400F99427 /* Game */, 01D1D1A81A29EEF50067D6D3 /* Model */, 01D1D1A91A29EF040067D6D3 /* Controllers */, 01D1D1AA1A29EF0E0067D6D3 /* Resources */, @@ -99,7 +102,7 @@ 01D1D1A91A29EF040067D6D3 /* Controllers */ = { isa = PBXGroup; children = ( - 49E5D23C1C12384200F99427 /* Extensions */, + 49E5D2471C12742E00F99427 /* Extensions */, 01558A351A0291A700083EB1 /* AppDelegate.swift */, 01558A371A0291A700083EB1 /* ViewController.swift */, 01D1D1AD1A29F7B00067D6D3 /* FireGestureRecognizer.swift */, @@ -118,11 +121,19 @@ name = Resources; sourceTree = ""; }; - 49E5D23C1C12384200F99427 /* Extensions */ = { + 49E5D2431C12418400F99427 /* Game */ = { isa = PBXGroup; children = ( - 49E5D23D1C1238B600F99427 /* GestureRecognizerDelegateExtension.swift */, - 49E5D23F1C12396500F99427 /* SceneRendererDelegateExtension.swift */, + 49E5D2411C12417700F99427 /* Scene.swift */, + ); + name = Game; + sourceTree = ""; + }; + 49E5D2471C12742E00F99427 /* Extensions */ = { + isa = PBXGroup; + children = ( + 49E5D23D1C1238B600F99427 /* GestureRecognizerExtension.swift */, + 49E5D2481C12A00100F99427 /* SceneRendererExtension.swift */, ); name = Extensions; sourceTree = ""; @@ -201,12 +212,13 @@ files = ( 01D1D1AC1A29EF320067D6D3 /* Map.swift in Sources */, 01558A381A0291A700083EB1 /* ViewController.swift in Sources */, + 49E5D2491C12A00100F99427 /* SceneRendererExtension.swift in Sources */, 01D1D1A61A29EEDF0067D6D3 /* Entity.swift in Sources */, 01D1D1AE1A29F7B00067D6D3 /* FireGestureRecognizer.swift in Sources */, - 49E5D2401C12396500F99427 /* SceneRendererDelegateExtension.swift in Sources */, 01D1D1A71A29EEDF0067D6D3 /* Tile.swift in Sources */, - 49E5D23E1C1238B600F99427 /* GestureRecognizerDelegateExtension.swift in Sources */, + 49E5D23E1C1238B600F99427 /* GestureRecognizerExtension.swift in Sources */, 01558A361A0291A700083EB1 /* AppDelegate.swift in Sources */, + 49E5D2421C12417700F99427 /* Scene.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/FPSControls/GestureRecognizerDelegateExtension.swift b/FPSControls/GestureRecognizerDelegateExtension.swift deleted file mode 100644 index 2132815..0000000 --- a/FPSControls/GestureRecognizerDelegateExtension.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// GestureRecognizerDelegateExtension.swift -// FPSControls -// -// Created by Luke Schoen on 4/12/2015. -// Copyright © 2015 Nick Lockwood. All rights reserved. -// - -import UIKit - -extension ViewController: UIGestureRecognizerDelegate { - - func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool { - - if gestureRecognizer == lookGesture { - return touch.locationInView(view).x > view.frame.size.width / 2 - } else if gestureRecognizer == walkGesture { - return touch.locationInView(view).x < view.frame.size.width / 2 - } - return true - } - - func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool { - - return true - } -} diff --git a/FPSControls/GestureRecognizerExtension.swift b/FPSControls/GestureRecognizerExtension.swift new file mode 100644 index 0000000..064409e --- /dev/null +++ b/FPSControls/GestureRecognizerExtension.swift @@ -0,0 +1,85 @@ +// +// GestureRecognizerExtension.swift +// FPSControls +// +// Created by Luke Schoen on 4/12/2015. +// Copyright © 2015 Nick Lockwood. All rights reserved. +// + +import UIKit +import SceneKit + +extension ViewController: UIGestureRecognizerDelegate { + + internal func setupGestureRecognizers() { + + //look gesture + lookGesture = UIPanGestureRecognizer(target: self, action: "lookGestureRecognized:") + lookGesture.delegate = self + self.sceneView.addGestureRecognizer(lookGesture) + + //walk gesture + walkGesture = UIPanGestureRecognizer(target: self, action: "walkGestureRecognized:") + walkGesture.delegate = self + self.sceneView.addGestureRecognizer(walkGesture) + + //fire gesture + fireGesture = FireGestureRecognizer(target: self, action: "fireGestureRecognized:") + fireGesture.delegate = self + self.sceneView.addGestureRecognizer(fireGesture) + } + + //implement protocol methods for conformance + func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool { + + if gestureRecognizer == lookGesture { + return touch.locationInView(self.view).x > self.view.frame.size.width / 2 + } else if gestureRecognizer == walkGesture { + return touch.locationInView(self.view).x < self.view.frame.size.width / 2 + } + return true + } + + func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool { + + return true + } + + //custom methods + func lookGestureRecognized(gesture: UIPanGestureRecognizer) { + + //get translation and convert to rotation + let translation = gesture.translationInView(self.sceneView) + let hAngle = acos(Float(translation.x) / 200) - Float(M_PI_2) + let vAngle = acos(Float(translation.y) / 200) - Float(M_PI_2) + + //rotate hero + Scene.sharedInstance.heroNode.physicsBody?.applyTorque(SCNVector4(x: 0, y: 1, z: 0, w: hAngle), impulse: true) + + //tilt camera + Scene.sharedInstance.elevation = max(Float(-M_PI_4), min(Float(M_PI_4), Scene.sharedInstance.elevation + vAngle)) + Scene.sharedInstance.camNode.rotation = SCNVector4(x: 1, y: 0, z: 0, w: Scene.sharedInstance.elevation) + + //reset translation + gesture.setTranslation(CGPointZero, inView: self.sceneView) + } + + func walkGestureRecognized(gesture: UIPanGestureRecognizer) { + + if gesture.state == UIGestureRecognizerState.Ended || gesture.state == UIGestureRecognizerState.Cancelled { + gesture.setTranslation(CGPointZero, inView: self.sceneView) + } + } + + func fireGestureRecognized(gesture: FireGestureRecognizer) { + + //update timestamp + let now = CFAbsoluteTimeGetCurrent() + if now - lastTappedFire < autofireTapTimeThreshold { + tapCount += 1 + } else { + tapCount = 1 + } + lastTappedFire = now + } +} diff --git a/FPSControls/Scene.swift b/FPSControls/Scene.swift new file mode 100644 index 0000000..6c8793c --- /dev/null +++ b/FPSControls/Scene.swift @@ -0,0 +1,207 @@ +// +// Scene.swift +// FPSControls +// +// Created by Luke Schoen on 4/12/2015. +// Copyright © 2015 Nick Lockwood. All rights reserved. +// + +import Foundation +import SceneKit + +struct CollisionCategory { + + static let None: Int = 0b00000000 + static let All: Int = 0b11111111 + static let Map: Int = 0b00000001 + static let Hero: Int = 0b00000010 + static let Monster: Int = 0b00000100 + static let Bullet: Int = 0b00001000 +} + +class Scene: SCNScene, SCNSceneRendererDelegate { + + // MARK: Properties + + internal var sceneView: SCNView? + internal var cameraNode: SCNNode? + internal var heroNode: SCNNode! + internal var camNode: SCNNode! + internal var elevation: Float = 0 + var mapNode: SCNNode! + var map: Map! + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + + override init() { + super.init() + } + + func setupSceneWithView(scnView: SCNView) { + + /** + * Setup Map, Scene View, Entities, Camera, + * Level, Floor, Ceiling, and Physics + */ + self.setupMap() + self.setupView(scnView) + self.setupEntities() + self.setupCamera() + self.setupLevel() + self.setupFloor() + self.setupCeiling() + self.setupPhysics() + } + + func setupMap() { + + map = Map(image: UIImage(named:"Map")!) + } + + func setupView(view: SCNView) { + + self.sceneView = view + } + + func setupEntities() { + + for entity in map.entities { + switch entity.type { + case .Hero: + + heroNode = SCNNode() + heroNode.physicsBody = SCNPhysicsBody(type: .Dynamic, shape: SCNPhysicsShape(geometry: SCNCylinder(radius: 0.2, height: 1), options: nil)) + heroNode.physicsBody?.angularDamping = 0.9999999 + heroNode.physicsBody?.damping = 0.9999999 + heroNode.physicsBody?.rollingFriction = 0 + heroNode.physicsBody?.friction = 0 + heroNode.physicsBody?.restitution = 0 + heroNode.physicsBody?.velocityFactor = SCNVector3(x: 1, y: 0, z: 1) + heroNode.physicsBody?.categoryBitMask = CollisionCategory.Hero + heroNode.physicsBody?.collisionBitMask = CollisionCategory.All ^ CollisionCategory.Bullet + if #available(iOS 9.0, *) { + heroNode.physicsBody?.contactTestBitMask = ~0 + } + heroNode.position = SCNVector3(x: entity.x, y: 0.5, z: entity.y) + self.rootNode.addChildNode(heroNode) + + case .Monster: + + let monsterNode = SCNNode() + monsterNode.position = SCNVector3(x: entity.x, y: 0.3, z: entity.y) + monsterNode.geometry = SCNCylinder(radius: 0.15, height: 0.6) + monsterNode.physicsBody = SCNPhysicsBody(type: .Dynamic, shape: SCNPhysicsShape(geometry: monsterNode.geometry!, options: nil)) + monsterNode.physicsBody?.categoryBitMask = CollisionCategory.Monster + monsterNode.physicsBody?.collisionBitMask = CollisionCategory.All + if #available(iOS 9.0, *) { + monsterNode.physicsBody?.contactTestBitMask = ~0 + } + self.rootNode.addChildNode(monsterNode) + } + } + } + + func setupCamera() { + + //add a camera node + camNode = SCNNode() + camNode.position = SCNVector3(x: 0, y: 0, z: 0) + heroNode.addChildNode(camNode) + + //add camera + let camera = SCNCamera() + camera.zNear = 0.01 + camera.zFar = Double(max(map.width, map.height)) + camNode.camera = camera + } + + func setupLevel() { + + //create map node + mapNode = SCNNode() + + //add walls + for tile in map.tiles { + + if tile.type == .Wall { + + //create walls + if tile.visibility.contains(.Top) { + let wallNode = SCNNode() + wallNode.geometry = SCNPlane(width: 1, height: 1) + wallNode.rotation = SCNVector4(x: 0, y: 1, z: 0, w: Float(M_PI)) + wallNode.position = SCNVector3(x: Float(tile.x) + 0.5, y: 0.5, z: Float(tile.y)) + mapNode.addChildNode(wallNode) + } + if tile.visibility.contains(.Right) { + let wallNode = SCNNode() + wallNode.geometry = SCNPlane(width: 1, height: 1) + wallNode.rotation = SCNVector4(x: 0, y: 1, z: 0, w: Float(M_PI_2)) + wallNode.position = SCNVector3(x: Float(tile.x) + 1, y: 0.5, z: Float(tile.y) + 0.5) + mapNode.addChildNode(wallNode) + } + if tile.visibility.contains(.Bottom) { + let wallNode = SCNNode() + wallNode.geometry = SCNPlane(width: 1, height: 1) + wallNode.rotation = SCNVector4(x: 0, y: 1, z: 0, w: 0) + wallNode.position = SCNVector3(x: Float(tile.x) + 0.5, y: 0.5, z: Float(tile.y) + 1) + mapNode.addChildNode(wallNode) + } + if tile.visibility.contains(.Left) { + let wallNode = SCNNode() + wallNode.geometry = SCNPlane(width: 1, height: 1) + wallNode.rotation = SCNVector4(x: 0, y: 1, z: 0, w: Float(-M_PI_2)) + wallNode.position = SCNVector3(x: Float(tile.x), y: 0.5, z: Float(tile.y) + 0.5) + mapNode.addChildNode(wallNode) + } + } + } + } + + func setupFloor() { + + //add floor + let floorNode = SCNNode() + floorNode.geometry = SCNPlane(width: CGFloat(map.width), height: CGFloat(map.height)) + floorNode.rotation = SCNVector4(x: 1, y: 0, z: 0, w: Float(-M_PI_2)) + floorNode.position = SCNVector3(x: Float(map.width)/2, y: 0, z: Float(map.height)/2) + mapNode.addChildNode(floorNode) + } + + func setupCeiling() { + + //add ceiling + let ceilingNode = SCNNode() + ceilingNode.geometry = SCNPlane(width: CGFloat(map.width), height: CGFloat(map.height)) + ceilingNode.rotation = SCNVector4(x: 1, y: 0, z: 0, w: Float(M_PI_2)) + ceilingNode.position = SCNVector3(x: Float(map.width)/2, y: 1, z: Float(map.height)/2) + mapNode.addChildNode(ceilingNode) + } + + func setupPhysics() { + + self.physicsWorld.gravity = SCNVector3(x: 0, y: -9, z: 0) + self.physicsWorld.timeStep = 1.0/360 + + //set up map physics + mapNode.physicsBody = SCNPhysicsBody(type: .Static, shape: SCNPhysicsShape(node: mapNode, options: [SCNPhysicsShapeKeepAsCompoundKey: true])) + mapNode.physicsBody?.categoryBitMask = CollisionCategory.Map + mapNode.physicsBody?.collisionBitMask = CollisionCategory.All + if #available(iOS 9.0, *) { + mapNode.physicsBody?.contactTestBitMask = ~0 + } + self.rootNode.addChildNode(mapNode) + } + + // MARK: Thread Safe Singleton Pattern + + /** + * Threadsafe Singleton declaration of static constant to + * hold the single instance of class. Supports lazy initialisation + * since Swift lazily initialises class constants and variables + * and is thread safe by the definition of let + */ + static let sharedInstance = Scene() +} diff --git a/FPSControls/SceneRendererDelegateExtension.swift b/FPSControls/SceneRendererExtension.swift similarity index 71% rename from FPSControls/SceneRendererDelegateExtension.swift rename to FPSControls/SceneRendererExtension.swift index 02ece81..1c947d6 100644 --- a/FPSControls/SceneRendererDelegateExtension.swift +++ b/FPSControls/SceneRendererExtension.swift @@ -1,8 +1,8 @@ // -// SceneRendererDelegateExtension.swift +// SceneRendererExtension.swift // FPSControls // -// Created by Luke Schoen on 4/12/2015. +// Created by Luke Schoen on 5/12/2015. // Copyright © 2015 Nick Lockwood. All rights reserved. // @@ -14,17 +14,17 @@ extension ViewController: SCNSceneRendererDelegate { func renderer(aRenderer: SCNSceneRenderer, updateAtTime time: NSTimeInterval) { //get walk gesture translation - let translation = walkGesture.translationInView(self.view) + let translation = walkGesture.translationInView(self.sceneView!) //create impulse vector for hero - let angle = heroNode.presentationNode.rotation.w * heroNode.presentationNode.rotation.y + let angle = Scene.sharedInstance.heroNode.presentationNode.rotation.w * Scene.sharedInstance.heroNode.presentationNode.rotation.y var impulse = SCNVector3(x: max(-1, min(1, Float(translation.x) / 50)), y: 0, z: max(-1, min(1, Float(-translation.y) / 50))) impulse = SCNVector3( x: impulse.x * cos(angle) - impulse.z * sin(angle), y: 0, z: impulse.x * -sin(angle) - impulse.z * cos(angle) ) - heroNode.physicsBody?.applyForce(impulse, impulse: true) + Scene.sharedInstance.heroNode.physicsBody?.applyForce(impulse, impulse: true) //handle firing let now = CFAbsoluteTimeGetCurrent() @@ -33,11 +33,11 @@ extension ViewController: SCNSceneRendererDelegate { if now - lastFired > 1 / fireRate { //get hero direction vector - let angle = heroNode.presentationNode.rotation.w * heroNode.presentationNode.rotation.y + let angle = Scene.sharedInstance.heroNode.presentationNode.rotation.w * Scene.sharedInstance.heroNode.presentationNode.rotation.y var direction = SCNVector3(x: -sin(angle), y: 0, z: -cos(angle)) //get elevation - direction = SCNVector3(x: cos(elevation) * direction.x, y: sin(elevation), z: cos(elevation) * direction.z) + direction = SCNVector3(x: cos(Scene.sharedInstance.elevation) * direction.x, y: sin(Scene.sharedInstance.elevation), z: cos(Scene.sharedInstance.elevation) * direction.z) //create or recycle bullet node let bulletNode: SCNNode = { @@ -49,12 +49,12 @@ extension ViewController: SCNSceneRendererDelegate { }() bullets.append(bulletNode) bulletNode.geometry = SCNBox(width: CGFloat(bulletRadius) * 2, height: CGFloat(bulletRadius) * 2, length: CGFloat(bulletRadius) * 2, chamferRadius: CGFloat(bulletRadius)) - bulletNode.position = SCNVector3(x: heroNode.presentationNode.position.x, y: 0.4, z: heroNode.presentationNode.position.z) + bulletNode.position = SCNVector3(x: Scene.sharedInstance.heroNode.presentationNode.position.x, y: 0.4, z: Scene.sharedInstance.heroNode.presentationNode.position.z) bulletNode.physicsBody = SCNPhysicsBody(type: .Dynamic, shape: SCNPhysicsShape(geometry: bulletNode.geometry!, options: nil)) bulletNode.physicsBody?.categoryBitMask = CollisionCategory.Bullet bulletNode.physicsBody?.collisionBitMask = CollisionCategory.All ^ CollisionCategory.Hero bulletNode.physicsBody?.velocityFactor = SCNVector3(x: 1, y: 0.5, z: 1) - self.sceneView.scene!.rootNode.addChildNode(bulletNode) + self.sceneView!.scene!.rootNode.addChildNode(bulletNode) //apply impulse let impulse = SCNVector3(x: direction.x * Float(bulletImpulse), y: direction.y * Float(bulletImpulse), z: direction.z * Float(bulletImpulse)) diff --git a/FPSControls/ViewController.swift b/FPSControls/ViewController.swift index 322beaf..c998240 100644 --- a/FPSControls/ViewController.swift +++ b/FPSControls/ViewController.swift @@ -9,189 +9,52 @@ import UIKit import SceneKit -struct CollisionCategory { - - static let None: Int = 0b00000000 - static let All: Int = 0b11111111 - static let Map: Int = 0b00000001 - static let Hero: Int = 0b00000010 - static let Monster: Int = 0b00000100 - static let Bullet: Int = 0b00001000 -} - class ViewController: UIViewController { - //MARK: config + // MARK: Properties + + @IBOutlet weak var sceneView: SCNView! + @IBOutlet weak var overlayView: UIView! + + var lookGesture: UIPanGestureRecognizer! + var walkGesture: UIPanGestureRecognizer! + var fireGesture: FireGestureRecognizer! + let autofireTapTimeThreshold = 0.2 let maxRoundsPerSecond = 30 let bulletRadius = 0.05 let bulletImpulse = 15 let maxBullets = 100 - @IBOutlet var sceneView: SCNView! - @IBOutlet var overlayView: UIView! - - var lookGesture: UIPanGestureRecognizer! - var walkGesture: UIPanGestureRecognizer! - var fireGesture: FireGestureRecognizer! - var heroNode: SCNNode! - var camNode: SCNNode! - var elevation: Float = 0 - var mapNode: SCNNode! - var map: Map! - var tapCount = 0 var lastTappedFire: NSTimeInterval = 0 var lastFired: NSTimeInterval = 0 var bullets = [SCNNode]() - + override func viewDidLoad() { super.viewDidLoad() - //generate map - map = Map(image: UIImage(named:"Map")!) - - //create a new scene - let scene = SCNScene() - scene.physicsWorld.gravity = SCNVector3(x: 0, y: -9, z: 0) - scene.physicsWorld.timeStep = 1.0/360 - - //add entities - for entity in map.entities { - switch entity.type { - case .Hero: - - heroNode = SCNNode() - heroNode.physicsBody = SCNPhysicsBody(type: .Dynamic, shape: SCNPhysicsShape(geometry: SCNCylinder(radius: 0.2, height: 1), options: nil)) - heroNode.physicsBody?.angularDamping = 0.9999999 - heroNode.physicsBody?.damping = 0.9999999 - heroNode.physicsBody?.rollingFriction = 0 - heroNode.physicsBody?.friction = 0 - heroNode.physicsBody?.restitution = 0 - heroNode.physicsBody?.velocityFactor = SCNVector3(x: 1, y: 0, z: 1) - heroNode.physicsBody?.categoryBitMask = CollisionCategory.Hero - heroNode.physicsBody?.collisionBitMask = CollisionCategory.All ^ CollisionCategory.Bullet - if #available(iOS 9.0, *) { - heroNode.physicsBody?.contactTestBitMask = ~0 - } - heroNode.position = SCNVector3(x: entity.x, y: 0.5, z: entity.y) - scene.rootNode.addChildNode(heroNode) - - case .Monster: - - let monsterNode = SCNNode() - monsterNode.position = SCNVector3(x: entity.x, y: 0.3, z: entity.y) - monsterNode.geometry = SCNCylinder(radius: 0.15, height: 0.6) - monsterNode.physicsBody = SCNPhysicsBody(type: .Dynamic, shape: SCNPhysicsShape(geometry: monsterNode.geometry!, options: nil)) - monsterNode.physicsBody?.categoryBitMask = CollisionCategory.Monster - monsterNode.physicsBody?.collisionBitMask = CollisionCategory.All - if #available(iOS 9.0, *) { - monsterNode.physicsBody?.contactTestBitMask = ~0 - } - scene.rootNode.addChildNode(monsterNode) - } - } - - //add a camera node - camNode = SCNNode() - camNode.position = SCNVector3(x: 0, y: 0, z: 0) - heroNode.addChildNode(camNode) - - //add camera - let camera = SCNCamera() - camera.zNear = 0.01 - camera.zFar = Double(max(map.width, map.height)) - camNode.camera = camera - - //create map node - mapNode = SCNNode() - - //add walls - for tile in map.tiles { - - if tile.type == .Wall { - - //create walls - if tile.visibility.contains(.Top) { - let wallNode = SCNNode() - wallNode.geometry = SCNPlane(width: 1, height: 1) - wallNode.rotation = SCNVector4(x: 0, y: 1, z: 0, w: Float(M_PI)) - wallNode.position = SCNVector3(x: Float(tile.x) + 0.5, y: 0.5, z: Float(tile.y)) - mapNode.addChildNode(wallNode) - } - if tile.visibility.contains(.Right) { - let wallNode = SCNNode() - wallNode.geometry = SCNPlane(width: 1, height: 1) - wallNode.rotation = SCNVector4(x: 0, y: 1, z: 0, w: Float(M_PI_2)) - wallNode.position = SCNVector3(x: Float(tile.x) + 1, y: 0.5, z: Float(tile.y) + 0.5) - mapNode.addChildNode(wallNode) - } - if tile.visibility.contains(.Bottom) { - let wallNode = SCNNode() - wallNode.geometry = SCNPlane(width: 1, height: 1) - wallNode.rotation = SCNVector4(x: 0, y: 1, z: 0, w: 0) - wallNode.position = SCNVector3(x: Float(tile.x) + 0.5, y: 0.5, z: Float(tile.y) + 1) - mapNode.addChildNode(wallNode) - } - if tile.visibility.contains(.Left) { - let wallNode = SCNNode() - wallNode.geometry = SCNPlane(width: 1, height: 1) - wallNode.rotation = SCNVector4(x: 0, y: 1, z: 0, w: Float(-M_PI_2)) - wallNode.position = SCNVector3(x: Float(tile.x), y: 0.5, z: Float(tile.y) + 0.5) - mapNode.addChildNode(wallNode) - } - } - } - - //add floor - let floorNode = SCNNode() - floorNode.geometry = SCNPlane(width: CGFloat(map.width), height: CGFloat(map.height)) - floorNode.rotation = SCNVector4(x: 1, y: 0, z: 0, w: Float(-M_PI_2)) - floorNode.position = SCNVector3(x: Float(map.width)/2, y: 0, z: Float(map.height)/2) - mapNode.addChildNode(floorNode) + self.setupGame() + } - //add ceiling - let ceilingNode = SCNNode() - ceilingNode.geometry = SCNPlane(width: CGFloat(map.width), height: CGFloat(map.height)) - ceilingNode.rotation = SCNVector4(x: 1, y: 0, z: 0, w: Float(M_PI_2)) - ceilingNode.position = SCNVector3(x: Float(map.width)/2, y: 1, z: Float(map.height)/2) - mapNode.addChildNode(ceilingNode) + /** + * Setup game by creating and configuring singleton instance + * of scene, assigning it to scene property of the Interface Builder + * outlet of the SceneKit view, assigning the current View Controller + * as the delegate of the SceneKit view implementing the + * relevant protocols, showing statistics including FPS and timing info + * and setting up gesture recognisers for looking, walking, and firing. + */ + func setupGame() { - //set up map physics - mapNode.physicsBody = SCNPhysicsBody(type: .Static, shape: SCNPhysicsShape(node: mapNode, options: [SCNPhysicsShapeKeepAsCompoundKey: true])) - mapNode.physicsBody?.categoryBitMask = CollisionCategory.Map - mapNode.physicsBody?.collisionBitMask = CollisionCategory.All - if #available(iOS 9.0, *) { - mapNode.physicsBody?.contactTestBitMask = ~0 - } - scene.rootNode.addChildNode(mapNode) - - //set the scene to the view - sceneView.scene = scene - sceneView.delegate = self - - //show statistics such as fps and timing information - sceneView.showsStatistics = true - - //configure the view - sceneView.backgroundColor = UIColor.blackColor() - - //look gesture - lookGesture = UIPanGestureRecognizer(target: self, action: "lookGestureRecognized:") - lookGesture.delegate = self - view.addGestureRecognizer(lookGesture) - - //walk gesture - walkGesture = UIPanGestureRecognizer(target: self, action: "walkGestureRecognized:") - walkGesture.delegate = self - view.addGestureRecognizer(walkGesture) - - //fire gesture - fireGesture = FireGestureRecognizer(target: self, action: "fireGestureRecognized:") - fireGesture.delegate = self - view.addGestureRecognizer(fireGesture) + Scene.sharedInstance.setupSceneWithView(self.sceneView!) + self.sceneView!.scene = Scene.sharedInstance + self.sceneView!.delegate = self + self.sceneView!.showsStatistics = true + self.sceneView!.backgroundColor = UIColor.blackColor() + self.setupGestureRecognizers() } - + override func viewDidAppear(animated: Bool) { UIView.animateWithDuration(0.5) { @@ -205,41 +68,4 @@ class ViewController: UIViewController { self.overlayView.alpha = 0 } } - - func lookGestureRecognized(gesture: UIPanGestureRecognizer) { - - //get translation and convert to rotation - let translation = gesture.translationInView(self.view) - let hAngle = acos(Float(translation.x) / 200) - Float(M_PI_2) - let vAngle = acos(Float(translation.y) / 200) - Float(M_PI_2) - - //rotate hero - heroNode.physicsBody?.applyTorque(SCNVector4(x: 0, y: 1, z: 0, w: hAngle), impulse: true) - - //tilt camera - elevation = max(Float(-M_PI_4), min(Float(M_PI_4), elevation + vAngle)) - camNode.rotation = SCNVector4(x: 1, y: 0, z: 0, w: elevation) - - //reset translation - gesture.setTranslation(CGPointZero, inView: self.view) - } - - func walkGestureRecognized(gesture: UIPanGestureRecognizer) { - - if gesture.state == UIGestureRecognizerState.Ended || gesture.state == UIGestureRecognizerState.Cancelled { - gesture.setTranslation(CGPointZero, inView: self.view) - } - } - - func fireGestureRecognized(gesture: FireGestureRecognizer) { - - //update timestamp - let now = CFAbsoluteTimeGetCurrent() - if now - lastTappedFire < autofireTapTimeThreshold { - tapCount += 1 - } else { - tapCount = 1 - } - lastTappedFire = now - } } From ba020c9f54579fa76032aaefe4fbad7ce6d2526a Mon Sep 17 00:00:00 2001 From: Luke Schoen Date: Sat, 5 Dec 2015 02:12:31 -0300 Subject: [PATCH 3/3] Updated release notes in Readme and updated version in product info --- FPSControls/Info.plist | 2 +- README.md | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/FPSControls/Info.plist b/FPSControls/Info.plist index 91b8fbc..c09c764 100644 --- a/FPSControls/Info.plist +++ b/FPSControls/Info.plist @@ -19,7 +19,7 @@ CFBundleSignature ???? CFBundleVersion - 1 + 2 LSRequiresIPhoneOS UILaunchStoryboardName diff --git a/README.md b/README.md index a6e7f26..ea9f114 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,11 @@ Run it on a real device instead. You should get 60fps on an iPhone 6, or compara Release notes --------------- +Version 1.0.2 + +- New: [2015-12-05] @ltfschoen Added Scene Class for creating Singleton Pattern instance +- New: [2015-12-05] @ltfschoen Refactored View Controller into Scene Class and Extensions + Version 1.0.1 - Updated for Swift 2 / Xcode 7 and iOS 9 compatibility