├── .gitignore ├── .jazzy.yml ├── .swiftlint.yml ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── LICENSE ├── Package.swift ├── README.md ├── Readme ├── package.png └── tank-demo.webp ├── Sources └── UnityKit │ ├── Components │ ├── AudioSource.swift │ ├── BoxCollider.swift │ ├── Camera.swift │ ├── Collider.swift │ ├── Light.swift │ ├── MeshCollider.swift │ ├── MeshFilter.swift │ ├── ParticleSystem.swift │ ├── PlaneCollider.swift │ ├── Renderer.swift │ ├── Rigidbody.swift │ ├── Transform.swift │ └── Vehicle.swift │ ├── Element │ ├── AudioClip.swift │ ├── AudioEngine.swift │ ├── GameObject.swift │ ├── Layer.swift │ ├── Material.swift │ ├── Mesh.swift │ ├── Scene.swift │ └── Tag.swift │ ├── Engine │ ├── Behaviour.swift │ ├── Component.swift │ ├── Instantiable.swift │ ├── MonoBehaviour.swift │ └── Object.swift │ ├── Extension │ ├── ActionExtension.swift │ ├── ColorExtension.swift │ ├── FloatingPointExtension.swift │ ├── GameObjectExtension.swift │ ├── GameObjectPhysicsBody.swift │ ├── GameObjectSCNActionable.swift │ ├── GameObjectSearch.swift │ ├── GeometryExtension.swift │ ├── MathExtension.swift │ ├── ObjectSearchExtension.swift │ ├── SCNNodeExtension.swift │ ├── Size.swift │ ├── UIImageExtension.swift │ ├── Vector2.swift │ ├── Vector3.swift │ └── Vector4Quaternion.swift │ ├── Protocol │ ├── Getteable.swift │ └── Identifiable.swift │ ├── Static │ ├── Debug.swift │ ├── Input.swift │ ├── Physics.swift │ ├── Screen.swift │ └── Time.swift │ ├── UI │ ├── Components │ │ ├── Canvas.swift │ │ ├── Image.swift │ │ └── Slider.swift │ ├── Elements │ │ ├── CanvasObject.swift │ │ └── View.swift │ └── Engine │ │ ├── UI.swift │ │ └── UIBehaviour.swift │ └── Utilities │ ├── Ease.swift │ ├── File.swift │ └── Volume.swift └── Templates └── UnityScript File.xctemplate ├── TemplateIcon.png ├── TemplateIcon@2x.png ├── TemplateInfo.plist └── ___FILEBASENAME___.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | Documentation/ 9 | 10 | ## Various settings 11 | *.pbxuser 12 | !default.pbxuser 13 | *.mode1v3 14 | !default.mode1v3 15 | *.mode2v3 16 | !default.mode2v3 17 | *.perspectivev3 18 | !default.perspectivev3 19 | xcuserdata/ 20 | 21 | ## Other 22 | *.moved-aside 23 | *.xcuserstate 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Pods 32 | /pods 33 | 34 | Pods/ 35 | Pods -------------------------------------------------------------------------------- /.jazzy.yml: -------------------------------------------------------------------------------- 1 | theme: fullwidth 2 | skip_undocumented: true 3 | readme: README.md 4 | xcodebuild_arguments: 5 | - -workspace 6 | - UnityKit.xcworkspace 7 | - -scheme 8 | - UnityKit 9 | author: Salt and Pepper Code 10 | 11 | #jazzy --config .jazzy.yml --output Documentation --clean -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - todo 3 | - multiple_closures_with_trailing_closure 4 | - nesting 5 | - identifier_name 6 | - type_name 7 | - operator_usage_whitespace # This has a few bugs that are annoying 8 | - function_parameter_count # We are too far gone... revisit again in near future 9 | - line_length 10 | - vertical_parameter_alignment 11 | - unused_optional_binding 12 | - shorthand_operator 13 | opt_in_rules: 14 | - opening_brace 15 | - vertical_whitespace 16 | - vertical_whitespace_closing_braces 17 | - vertical_whitespace_opening_braces 18 | 19 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Kevin Malkic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "UnityKit", 7 | platforms: [.iOS("15.0")], 8 | products: [ 9 | .library(name: "UnityKit", targets: ["UnityKit"]), 10 | ], 11 | dependencies: [ 12 | 13 | ], 14 | targets: [ 15 | .target(name: "UnityKit", dependencies: [], path: "Sources"), 16 | ] 17 | ) 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UnityKit 2 | 3 | The idea behind UnityKit is to replicate Unity3D coding patterns. My intention was not to improve the system but to allow any developer that is familiar with Unity already and to be able to follow C# tutorials and write it in Swift. 4 | It uses SceneKit, UnityKit is a layer on top of it to ease the use with function that will make your life easier. 5 | The advantage of having this, is that you can have multiple scene running at the same time, something that Unity3D can't do on iOS. 6 | 7 | ## Requirements 8 | - iOS 11.0+ 9 | - Xcode 11.1+ 10 | - Swift 5.0+ 11 | 12 | ## Tank Demo Example 13 | ![Example](Readme/tank-demo.webp) 14 | ### Summary 15 | Originally recorded at Unite Boston 2015, this series will teach you how to create a 1 player shooter game. You'll learn about simple game mechanics, integrating world and screen space UI, as well as game architecture and audio mixing. 16 | 17 | [Original Tutorial in C#](https://learn.unity.com/project/tanks-tutorial) 18 | 19 | 💥 [Tank Demo in Swift](https://github.com/salt-pepper-code/TankDemo) 20 | 21 | ## Installations 22 | ### Carthage 23 | Add UnityKit to your Cartfile: 24 | ``` 25 | github "salt-pepper-code/unitykit" "master" 26 | ``` 27 | 28 | And then run: 29 | ``` 30 | carthage update 31 | ``` 32 | In your application targets “General” tab under the “Linked Frameworks and Libraries” section, drag and drop UnityKit.framework from the Carthage/Build/iOS directory that `carthage update` produced. 33 | 34 | ### Swift Package Manager 35 | ``` swift 36 | // swift-tools-version:5.1 37 | 38 | import PackageDescription 39 | 40 | let package = Package( 41 | name: "UnityKit", 42 | platforms: [.iOS(.v11)], 43 | products: [ 44 | .library(name: "UnityKit", targets: ["UnityKit"]), 45 | ], 46 | dependencies: [ 47 | 48 | ], 49 | targets: [ 50 | .target(name: "UnityKit", dependencies: [], path: "Sources"), 51 | ] 52 | ) 53 | ``` 54 | And then import wherever needed: ```import UnityKit``` 55 | 56 | #### Adding it to an existent iOS Project via Swift Package Manager 57 | 58 | 1. Using Xcode 11 go to File > Swift Packages > Add Package Dependency 59 | 2. Paste the project URL: https://github.com/salt-pepper-code/UnityKit.git 60 | 3. Click on next and select the project target 61 | ![Example](Readme/package.png) 62 | 63 | If you have doubts, please, check the following links: 64 | 65 | [How to use](https://developer.apple.com/videos/play/wwdc2019/408/) 66 | 67 | [Creating Swift Packages](https://developer.apple.com/videos/play/wwdc2019/410/) 68 | 69 | After successfully retrieved the package and added it to your project, just import `UnityKit` and you can get the full benefits of it. 70 | 71 | ## Usage 72 | 73 | ### Preparing the view and scene: 74 | Simple way 75 | ``` swift 76 | class GameViewController: UIViewController { 77 | override func loadView() { 78 | self.view = UI.View.makeView() 79 | } 80 | 81 | var sceneView: UI.View { 82 | return self.view as? UI.View ?? UI.View(frame: .zero) 83 | } 84 | ... 85 | ``` 86 | 87 | There is many way to load a scene, please check the `makeView` documentation for the different parameters. 88 | Usually it is best to prepare your own Scene.scn to have pre-configurated environment and 3d objects inside. 89 | ``` swift 90 | class GameViewController: UIViewController { 91 | override func loadView() { 92 | self.view = UI.View.makeView(sceneName: "Scene.scn", options: UI.View.Options(showsStatistics: true)) 93 | } 94 | 95 | var sceneView: UI.View { 96 | return self.view as? UI.View ?? UI.View(frame: .zero) 97 | } 98 | ... 99 | ``` 100 | 101 | ### Scene 102 | To access the scene you can retrieve it from the view. However by default the allocation type of the scene is singleton so you can simply access it with `Scene.shared`. 103 | ``` swift 104 | guard let scene = Scene.shared else { return } 105 | // or 106 | guard let scene = sceneView.sceneHolder else { return } 107 | ``` 108 | 109 | ### GameObject 110 | Just like Unity you can create empty GameObjects 111 | ``` swift 112 | let gameManager = GameObject(name: "GameManager") 113 | ``` 114 | Or create a GameObject with predefined shapes. 115 | ``` swift 116 | let cube = GameObject.createPrimitive(.cube(width: 20, height: 20, length: 20, chamferRadius: 0, name: "MyCube")) 117 | scene.addGameObject(cube) 118 | ``` 119 | ``` swift 120 | // List of different primitives: 121 | public enum PrimitiveType { 122 | case sphere(radius: Float, name: String?) 123 | case capsule(capRadius: Float, height: Float, name: String?) 124 | case cylinder(radius: Float, height: Float, name: String?) 125 | case cube(width: Float, height: Float, length: Float, chamferRadius: Float, name: String?) 126 | case plane(width: Float, height: Float, name: String?) 127 | case floor(width: Float, length: Float, name: String?) 128 | } 129 | ``` 130 | ⚠️ You will note that GameObject still reference SCNNode but it is not recommended to play with it directly. 131 | To access the geometry please use the `MeshFilter` 132 | 133 | ### Components / MonoBehaviour 134 | As you should already know Unity uses script components to handle various behaviour of the GameObject. So I won't describe more on this point. 135 | Just like Unity3D you can only add component by its type and not an instance of it. 136 | All Component should inherit from `MonoBehaviour` 137 | ``` swift 138 | class GameManager: MonoBehaviour { } 139 | ``` 140 | ``` swift 141 | let gameManager = GameObject(name: "GameManager") 142 | gameManager.addComponent(GameManager.self) 143 | scene.addGameObject(gameManager) 144 | ``` 145 | 146 | #### MonoBehaviour life cycles 147 | ``` swift 148 | /// Awake is called when the script instance is being loaded. 149 | open func awake() { } 150 | 151 | /// Start is called on the frame when a script is enabled just before any of the Update methods are called the first time. 152 | open func start() { } 153 | 154 | /// preUpdate is called every frame, if the Object is enabled on willRenderScene. 155 | open func preUpdate() { } 156 | 157 | internal func internalUpdate() { } 158 | 159 | /// Update is called every frame, if the Object is enabled on didRenderScene. 160 | open func update() { } 161 | 162 | /// fixedUpdate is called every simulated physics frame, if the Object is enabled on didSimulatePhysicsAtTime. 163 | open func fixedUpdate() { } 164 | ``` 165 | Events: 166 | ``` swift 167 | open func onEnable() { } 168 | 169 | open func onDisable() { } 170 | 171 | open func onCollisionEnter(_ collision: Collision) { } 172 | 173 | open func onCollisionExit(_ collision: Collision) { } 174 | 175 | open func onTriggerEnter(_ collider: Collider) { } 176 | 177 | open func onTriggerExit(_ collider: Collider) { } 178 | ``` 179 | 180 | ### Threading / Coroutine 181 | ``` swift 182 | public func startCoroutine(_ coroutine: CoroutineClosure, thread: CoroutineThread = .background) { ... 183 | public func queueCoroutine(_ coroutine: Coroutine, thread: CoroutineThread = .main) { ... 184 | ``` 185 | 186 | ### Audio 187 | ``` swift 188 | if let clip = AudioClip(fileName: "BackgroundMusic.wav", playType: .loop) { 189 | gameManager.addComponent(AudioSource.self) 190 | .configure { 191 | $0.clip = clip 192 | $0.volume = 0.3 193 | $0.play() 194 | } 195 | } 196 | ``` 197 | 198 | ### Debugging 199 | UnityKit as it's own Debug class that prettify the logs, and you can control what is displayed. 200 | ``` swift 201 | Debug.set(enable: .all)` 202 | ``` 203 | ``` swift 204 | all 205 | debug 206 | info 207 | warning 208 | error 209 | none 210 | ``` 211 | 212 | ## Credits 213 | 214 | Kevin Malkic - Salt and Pepper Code Ltd 215 | 216 | ## License 217 | 218 | UnityKit is released under the MIT license. See LICENSE for details. 219 | -------------------------------------------------------------------------------- /Readme/package.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salt-pepper-code/UnityKit/89df21948b87aa75421faa0a265ae8d2fdd4f137/Readme/package.png -------------------------------------------------------------------------------- /Readme/tank-demo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salt-pepper-code/UnityKit/89df21948b87aa75421faa0a265ae8d2fdd4f137/Readme/tank-demo.webp -------------------------------------------------------------------------------- /Sources/UnityKit/Components/AudioSource.swift: -------------------------------------------------------------------------------- 1 | import AVKit 2 | 3 | /** 4 | A representation of audio sources in 3D. 5 | */ 6 | public class AudioSource: Component { 7 | internal override var order: ComponentOrder { 8 | .other 9 | } 10 | private var soundPlayer = AVAudioPlayerNode() 11 | 12 | public var volume: Float = 1 { 13 | didSet { 14 | soundPlayer.volume = volume 15 | } 16 | } 17 | 18 | public var customPosition: Vector3? { 19 | didSet { 20 | soundPlayer.position = customPosition?.toAVAudio3DPoint() ?? soundPlayer.position 21 | } 22 | } 23 | 24 | public var clip: AudioClip? { 25 | didSet { 26 | let engine = AudioEngine.sharedInstance 27 | 28 | if let oldValue = oldValue { 29 | engine.disconnectNodeOutput(soundPlayer, bus: oldValue.bus) 30 | } 31 | 32 | guard let clip = clip, 33 | let buffer = clip.buffer 34 | else { return } 35 | 36 | engine.connect(soundPlayer, to: engine.environment, fromBus: 0, toBus: clip.bus, format: engine.format) 37 | engine.startEngine() 38 | 39 | switch clip.playType { 40 | case .loop: 41 | soundPlayer.scheduleBuffer(buffer, at: nil, options: .loops, completionHandler: nil) 42 | case .playOnce: 43 | soundPlayer.scheduleBuffer(buffer, at: nil) 44 | } 45 | } 46 | } 47 | 48 | public override func awake() { 49 | let engine = AudioEngine.sharedInstance 50 | engine.attach(soundPlayer) 51 | } 52 | 53 | /** 54 | Configurable block that passes and returns itself. 55 | 56 | - parameters: 57 | - configurationBlock: block that passes itself. 58 | 59 | - returns: itself 60 | */ 61 | @discardableResult public func configure(_ configurationBlock: (AudioSource) -> Void) -> AudioSource { 62 | configurationBlock(self) 63 | return self 64 | } 65 | 66 | @discardableResult public func set(volume: Float) -> AudioSource { 67 | self.volume = volume 68 | return self 69 | } 70 | 71 | @discardableResult public func set(clip: AudioClip) -> AudioSource { 72 | self.clip = clip 73 | return self 74 | } 75 | 76 | @discardableResult public func set(customPosition: Vector3) -> AudioSource { 77 | self.customPosition = customPosition 78 | return self 79 | } 80 | 81 | public override func onDestroy() { 82 | stop() 83 | let engine = AudioEngine.sharedInstance 84 | if let clip = clip { 85 | engine.disconnectNodeOutput(soundPlayer, bus: clip.bus) 86 | } 87 | engine.detach(soundPlayer) 88 | } 89 | 90 | public func play() { 91 | soundPlayer.play() 92 | } 93 | 94 | public func stop() { 95 | soundPlayer.stop() 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Sources/UnityKit/Components/BoxCollider.swift: -------------------------------------------------------------------------------- 1 | import SceneKit 2 | 3 | /** 4 | A box-shaped primitive collider. 5 | */ 6 | public final class BoxCollider: Collider { 7 | private(set) public var size: Vector3Nullable? 8 | private(set) public var center: Vector3Nullable? 9 | 10 | @discardableResult public func set(size: Vector3Nullable?) -> BoxCollider { 11 | self.size = size 12 | return self 13 | } 14 | 15 | @discardableResult public func set(center: Vector3Nullable?) -> BoxCollider { 16 | self.center = center 17 | return self 18 | } 19 | 20 | /** 21 | Configurable block that passes and returns itself. 22 | 23 | - parameters: 24 | - configurationBlock: block that passes itself. 25 | 26 | - returns: itself 27 | */ 28 | @discardableResult public func configure(_ configurationBlock: (BoxCollider) -> Void) -> BoxCollider { 29 | configurationBlock(self) 30 | return self 31 | } 32 | 33 | override func constructBody() { 34 | guard let gameObject = gameObject, 35 | let name = gameObject.name 36 | else { return } 37 | 38 | var boundingBox = gameObject.node.boundingBox 39 | let boundingCenter = Volume.boundingCenter(boundingBox) 40 | 41 | if let x = size?.x { 42 | boundingBox.min.x = boundingCenter.x - (x / 2) 43 | boundingBox.max.x = boundingCenter.x + (x / 2) 44 | } 45 | if let y = size?.y { 46 | boundingBox.min.y = boundingCenter.y - (y / 2) 47 | boundingBox.max.y = boundingCenter.y + (y / 2) 48 | } 49 | if let z = size?.z { 50 | boundingBox.min.z = boundingCenter.z - (z / 2) 51 | boundingBox.max.z = boundingCenter.z + (z / 2) 52 | } 53 | 54 | if let center = center { 55 | boundingBox = Volume.moveCenter(boundingBox, center: center) 56 | } 57 | 58 | let vertices = [ 59 | Vector3(boundingBox.min.x, boundingBox.min.y, boundingBox.min.z), 60 | Vector3(boundingBox.min.x, boundingBox.max.y, boundingBox.min.z), 61 | Vector3(boundingBox.max.x, boundingBox.max.y, boundingBox.min.z), 62 | Vector3(boundingBox.max.x, boundingBox.min.y, boundingBox.min.z), 63 | Vector3(boundingBox.max.x, boundingBox.min.y, boundingBox.max.z), 64 | Vector3(boundingBox.max.x, boundingBox.max.y, boundingBox.max.z), 65 | Vector3(boundingBox.min.x, boundingBox.max.y, boundingBox.max.z), 66 | Vector3(boundingBox.min.x, boundingBox.min.y, boundingBox.max.z) 67 | ] 68 | 69 | let indices: [Int16] = [ 70 | 0, 1, 2, 71 | 0, 2, 3, 72 | 3, 2, 5, 73 | 3, 5, 4, 74 | 5, 2, 1, 75 | 5, 1, 6, 76 | 3, 4, 7, 77 | 3, 7, 0, 78 | 0, 7, 6, 79 | 0, 6, 1, 80 | 4, 5, 6, 81 | 4, 6, 7 82 | ] 83 | 84 | let normals = [ 85 | Vector3(0, 0, 1), 86 | Vector3(0, 0, 1), 87 | Vector3(0, 0, 1), 88 | Vector3(0, 0, 1), 89 | Vector3(0, 0, 1), 90 | Vector3(0, 0, 1), 91 | Vector3(0, 0, 1), 92 | Vector3(0, 0, 1) 93 | ] 94 | 95 | let vertexData = Data(bytes: vertices, count: vertices.count * MemoryLayout.size) 96 | let vertexSource = SCNGeometrySource( 97 | data: vertexData, 98 | semantic: .vertex, 99 | vectorCount: vertices.count, 100 | usesFloatComponents: true, 101 | componentsPerVector: 3, 102 | bytesPerComponent: MemoryLayout.size, 103 | dataOffset: 0, 104 | dataStride: MemoryLayout.size 105 | ) 106 | 107 | let normalData = Data(bytes: normals, count: normals.count * MemoryLayout.size) 108 | let normalSource = SCNGeometrySource( 109 | data: normalData, 110 | semantic: .normal, 111 | vectorCount: normals.count, 112 | usesFloatComponents: true, 113 | componentsPerVector: 3, 114 | bytesPerComponent: MemoryLayout.size, 115 | dataOffset: 0, 116 | dataStride: MemoryLayout.size 117 | ) 118 | 119 | let indexData = Data(bytes: indices, count: indices.count * MemoryLayout.size) 120 | let element = SCNGeometryElement( 121 | data: indexData, 122 | primitiveType: .triangles, 123 | primitiveCount: indices.count / 3, 124 | bytesPerIndex: MemoryLayout.size 125 | ) 126 | 127 | let geometry = SCNGeometry(sources: [vertexSource, normalSource], elements: [element]) 128 | geometry.name = name + "BoxCollider" 129 | 130 | physicsShape = SCNPhysicsShape( 131 | geometry: geometry, 132 | options: [.type: SCNPhysicsShape.ShapeType.boundingBox, 133 | .scale: gameObject.transform.localScale.x] 134 | ) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Sources/UnityKit/Components/Camera.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SceneKit 3 | 4 | /** 5 | A Camera is a device through which the player views the world. 6 | A world space point is defined in global coordinates (for example, Transform.position). 7 | */ 8 | public final class Camera: Component { 9 | override internal var order: ComponentOrder { 10 | .priority 11 | } 12 | private var hFieldOfView: CGFloat = 60 13 | 14 | internal(set) public var scnCamera = SCNCamera() { 15 | didSet { 16 | cullingMask = GameObject.Layer.all 17 | calculateFieldOfViews() 18 | } 19 | } 20 | 21 | /** 22 | Determines the receiver's field of view (in degree). Animatable. 23 | 24 | **Defaults to 60°.** 25 | */ 26 | public var fieldOfView: CGFloat { 27 | get { 28 | guard orthographic 29 | else { return 0 } 30 | 31 | return scnCamera.fieldOfView 32 | } 33 | set { 34 | hFieldOfView = newValue 35 | 36 | guard orthographic 37 | else { return } 38 | 39 | scnCamera.fieldOfView = newValue 40 | scnCamera.projectionDirection = .horizontal 41 | } 42 | } 43 | 44 | /** 45 | Determines the receiver's near value. Animatable. 46 | 47 | The near value determines the minimal distance between the camera and a visible surface. If a surface is closer to the camera than this minimal distance, then the surface is clipped. The near value must be different than zero. 48 | 49 | **Defaults to 1.** 50 | */ 51 | public var zNear: Double { 52 | get { 53 | return scnCamera.zNear 54 | } 55 | set { 56 | scnCamera.zNear = newValue 57 | } 58 | } 59 | 60 | /** 61 | Determines the receiver's far value. Animatable. 62 | 63 | The far value determines the maximal distance between the camera and a visible surface. If a surface is further from the camera than this maximal distance, then the surface is clipped. 64 | 65 | **Defaults to 100.** 66 | */ 67 | public var zFar: Double { 68 | get { 69 | return scnCamera.zFar 70 | } 71 | set { 72 | scnCamera.zFar = newValue 73 | } 74 | } 75 | 76 | /** 77 | Determines whether the receiver uses an orthographic projection or not. 78 | 79 | **Defaults to false.** 80 | */ 81 | public var orthographic: Bool { 82 | get { 83 | return scnCamera.usesOrthographicProjection 84 | } 85 | set { 86 | scnCamera.usesOrthographicProjection = newValue 87 | } 88 | } 89 | 90 | /** 91 | Determines the receiver's orthographic scale value. Animatable. 92 | 93 | This setting determines the size of the camera's visible area. This is only enabled when usesOrthographicProjection is set to true. 94 | 95 | **Defaults to 1.** 96 | */ 97 | public var orthographicSize: Double { 98 | get { 99 | return scnCamera.orthographicScale 100 | } 101 | set { 102 | scnCamera.orthographicScale = newValue 103 | } 104 | } 105 | 106 | /** 107 | Determines if the receiver has a high dynamic range. 108 | 109 | **Defaults to false.** 110 | */ 111 | public var allowHDR: Bool { 112 | get { 113 | return scnCamera.wantsHDR 114 | } 115 | set { 116 | scnCamera.wantsHDR = newValue 117 | } 118 | } 119 | 120 | /** 121 | This is used to render parts of the scene selectively. 122 | 123 | - important: If the GameObject's layerMask AND the camera's cullingMask is zero then the game object will be invisible from this camera. 124 | See [Layer](GameObject/Layer.html) for more information. 125 | */ 126 | public var cullingMask: GameObject.Layer { 127 | get { 128 | return GameObject.Layer(rawValue: scnCamera.categoryBitMask) 129 | } 130 | set { 131 | scnCamera.categoryBitMask = newValue.rawValue 132 | gameObject?.node.categoryBitMask = newValue.rawValue 133 | } 134 | } 135 | 136 | /** 137 | The game object this component is attached to. A component is always attached to a game object. 138 | */ 139 | public override var gameObject: GameObject? { 140 | didSet { 141 | guard let node = gameObject?.node, 142 | node.camera != scnCamera 143 | else { return } 144 | 145 | node.camera.map { scnCamera = $0 } 146 | calculateFieldOfViews() 147 | } 148 | } 149 | 150 | /** 151 | The game object that it's follows. 152 | */ 153 | private(set) public var target: GameObject? 154 | 155 | /// Create a new instance 156 | public required init() { 157 | super.init() 158 | self.cullingMask = GameObject.Layer.all 159 | calculateFieldOfViews() 160 | } 161 | 162 | /** 163 | Configurable block that passes and returns itself. 164 | 165 | - parameters: 166 | - configurationBlock: block that passes itself. 167 | 168 | - returns: itself 169 | */ 170 | @discardableResult public func configure(_ configurationBlock: (Camera) -> Void) -> Camera { 171 | configurationBlock(self) 172 | return self 173 | } 174 | 175 | public override func awake() { 176 | guard let node = gameObject?.node, 177 | node.camera == nil 178 | else { return } 179 | 180 | node.camera = scnCamera 181 | } 182 | 183 | internal func calculateFieldOfViews() { 184 | fieldOfView = hFieldOfView 185 | } 186 | 187 | /** 188 | The primary Camera in the Scene. Returns nil if there is no such camera in the Scene. This property uses GameObject.find(.tag(.mainCamera)) internally and doesn't cache the result. 189 | It is advised to cache the return value of Camera.main if it is used multiple times per frame. 190 | - parameters: 191 | - scene: Current scene. 192 | 193 | - returns: The first enabled camera tagged "MainCamera". 194 | */ 195 | public static func main(in scene: Scene? = Scene.shared) -> Camera? { 196 | guard let scene = scene 197 | else { return nil } 198 | 199 | return GameObject.find(.tag(.mainCamera), in: scene)?.getComponent(Camera.self) 200 | } 201 | 202 | /** 203 | Follow a target gameObject. 204 | - parameters: 205 | - target: Target gameObject to be followed 206 | - distanceRange: minimum distance and maximum distance. If nil will keep current distance as constraints. 207 | */ 208 | public func followTarget(target: GameObject?, distanceRange: (minimum: Float, maximum: Float)? = nil) { 209 | self.target = target 210 | 211 | guard let target = target, 212 | let gameObject = gameObject 213 | else { return } 214 | 215 | let targetConstraint = SCNLookAtConstraint(target: target.node) 216 | targetConstraint.isGimbalLockEnabled = true 217 | 218 | guard let distanceRange = distanceRange else { 219 | gameObject.node.constraints = [targetConstraint] 220 | return 221 | } 222 | 223 | let constraint = distanceConstraint(gameObject: gameObject, target: target, distanceRange: distanceRange) 224 | gameObject.node.constraints = [targetConstraint, constraint] 225 | } 226 | 227 | private func distanceConstraint(gameObject: GameObject, target: GameObject, distanceRange: (minimum: Float, maximum: Float)) -> SCNConstraint { 228 | let distanceConstraint = SCNDistanceConstraint(target: target.node) 229 | distanceConstraint.minimumDistance = CGFloat(distanceRange.minimum) 230 | distanceConstraint.maximumDistance = CGFloat(distanceRange.maximum) 231 | return distanceConstraint 232 | } 233 | 234 | /** 235 | Look at target gameObject. 236 | - parameters: 237 | - target: Target gameObject to be followed 238 | - duration: Duration of animation. If nil it will be instant. 239 | */ 240 | public func lookAt(_ gameObject: GameObject, duration: TimeInterval? = nil) { 241 | lookAt(gameObject.transform, duration: duration) 242 | } 243 | 244 | /** 245 | Look at target transform. 246 | - parameters: 247 | - target: Target transform to be followed 248 | - duration: Duration of animation. If nil it will be instant. 249 | */ 250 | public func lookAt(_ target: Transform, duration: TimeInterval? = nil) { 251 | gameObject?.node.constraints = nil 252 | guard let duration = duration else { 253 | transform?.lookAt(target) 254 | return 255 | } 256 | SCNTransaction.begin() 257 | SCNTransaction.animationDuration = duration 258 | SCNTransaction.animationTimingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) 259 | transform?.lookAt(target) 260 | SCNTransaction.commit() 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /Sources/UnityKit/Components/Collider.swift: -------------------------------------------------------------------------------- 1 | import SceneKit 2 | 3 | public typealias Collision = SCNPhysicsContact 4 | 5 | /** 6 | A base class of all colliders. 7 | */ 8 | public class Collider: Component, Instantiable { 9 | override internal var order: ComponentOrder { 10 | .collider 11 | } 12 | /** 13 | Clones the object original and returns the clone. 14 | */ 15 | open func instantiate(gameObject: GameObject) -> Self { 16 | let clone = type(of: self).init() 17 | clone.collideWithLayer = collideWithLayer 18 | clone.contactWithLayer = contactWithLayer 19 | return clone 20 | } 21 | 22 | internal var physicsShape: SCNPhysicsShape? 23 | 24 | /** 25 | Layers that this gameObject will collide with. 26 | */ 27 | public var collideWithLayer: GameObject.Layer? { 28 | didSet { 29 | gameObject?.updateBitMask() 30 | } 31 | } 32 | 33 | /** 34 | Tell if it will trigger on contact. 35 | */ 36 | public var isTrigger: Bool = false { 37 | didSet { 38 | if isTrigger, contactWithLayer == nil { 39 | contactWithLayer = collideWithLayer 40 | } 41 | gameObject?.updateBitMask() 42 | } 43 | } 44 | 45 | /** 46 | Layers that this gameObject will contact with. 47 | */ 48 | public var contactWithLayer: GameObject.Layer? { 49 | didSet { 50 | isTrigger = contactWithLayer != nil 51 | } 52 | } 53 | 54 | private func getAllPhysicsShapes() -> [SCNPhysicsShape]? { 55 | guard let gameObject = gameObject 56 | else { return nil } 57 | 58 | return gameObject.getComponents(Collider.self) 59 | .compactMap { collider -> SCNPhysicsShape? in collider.physicsShape } 60 | } 61 | 62 | internal func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: Collision) { 63 | guard isTrigger, 64 | let gameObject = gameObject, 65 | contact.nodeA.name == gameObject.name || contact.nodeB.name == gameObject.name 66 | else { return } 67 | 68 | for component in gameObject.components { 69 | guard let monoBehaviour = component as? MonoBehaviour 70 | else { continue } 71 | 72 | monoBehaviour.onCollisionEnter(contact) 73 | monoBehaviour.onTriggerEnter(self) 74 | } 75 | } 76 | 77 | internal func physicsWorld(_ world: SCNPhysicsWorld, didEnd contact: Collision) { 78 | guard isTrigger, 79 | let gameObject = gameObject, 80 | contact.nodeA.name == gameObject.name || contact.nodeB.name == gameObject.name 81 | else { return } 82 | 83 | for component in gameObject.components { 84 | guard let monoBehaviour = component as? MonoBehaviour 85 | else { continue } 86 | 87 | monoBehaviour.onCollisionExit(contact) 88 | monoBehaviour.onTriggerExit(self) 89 | } 90 | } 91 | 92 | public override func start() { 93 | constructBody() 94 | gameObject?.updatePhysicsBody() 95 | } 96 | 97 | public override func onDestroy() { 98 | } 99 | 100 | internal func constructBody() { 101 | fatalError("Can't use Collider as a component, please use subclasses") 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Sources/UnityKit/Components/Light.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SceneKit 3 | 4 | /** 5 | Script interface for light components. 6 | */ 7 | public final class Light: Component { 8 | override internal var order: ComponentOrder { 9 | .priority 10 | } 11 | internal(set) public var scnLight = SCNLight() 12 | 13 | /** 14 | Specifies the receiver's type. 15 | 16 | **Defaults to SCNLightTypeOmni on iOS 8 and later, and on macOS 10.10 and later (otherwise defaults to SCNLightTypeAmbient).** 17 | */ 18 | public var type: SCNLight.LightType { 19 | get { return scnLight.type } 20 | set { scnLight.type = newValue } 21 | } 22 | 23 | /** 24 | Specifies the receiver's color (NSColor or CGColorRef). Animatable. Defaults to white. 25 | 26 | **The initial value is a NSColor. The renderer multiplies the light's color is by the color derived from the light's temperature.** 27 | */ 28 | public var color: Any { 29 | get { return scnLight.color } 30 | set { scnLight.color = newValue } 31 | } 32 | 33 | /** 34 | Specifies the receiver's temperature. 35 | 36 | **This specifies the temperature of the light in Kelvin. The renderer multiplies the light's color by the color derived from the light's temperature. Defaults to 6500 (pure white). Animatable.** 37 | */ 38 | public var temperature: CGFloat { 39 | get { return scnLight.temperature } 40 | set { scnLight.temperature = newValue } 41 | } 42 | 43 | /** 44 | Specifies the receiver's intensity. 45 | 46 | **This intensity is used to modulate the light color. When used with a physically-based material, this corresponds to the luminous flux of the light, expressed in lumens (lm). Defaults to 1000. Animatable.** 47 | */ 48 | public var intensity: CGFloat { 49 | get { return scnLight.intensity } 50 | set { scnLight.intensity = newValue } 51 | } 52 | 53 | /** 54 | Determines the name of the receiver. 55 | */ 56 | public override var name: String? { 57 | didSet { 58 | scnLight.name = name 59 | } 60 | } 61 | 62 | /** 63 | Determines whether the receiver casts a shadow. Defaults to NO. 64 | 65 | **Shadows are only supported by spot and directional lights.** 66 | */ 67 | public var castsShadow: Bool { 68 | get { return scnLight.castsShadow } 69 | set { scnLight.castsShadow = newValue } 70 | } 71 | 72 | /** 73 | Specifies the color (CGColorRef or NSColor) of the shadow casted by the receiver. Defaults to black. Animatable. 74 | 75 | **On iOS 9 or earlier and macOS 10.11 or earlier, this defaults to black 50% transparent.** 76 | */ 77 | public var shadowColor: Any { 78 | get { return scnLight.shadowColor } 79 | set { scnLight.shadowColor = newValue } 80 | } 81 | 82 | /** 83 | Specifies the sample radius used to render the receiver’s shadow. Default value is 3.0. Animatable. 84 | */ 85 | public var shadowRadius: CGFloat { 86 | get { return scnLight.shadowRadius } 87 | set { scnLight.shadowRadius = newValue } 88 | } 89 | 90 | /** 91 | Specifies the size of the shadow map. 92 | 93 | **The larger the shadow map is the more precise the shadows are but the slower the computation is. If set to {0,0} the size of the shadow map is automatically chosen. Defaults to {0,0}.** 94 | */ 95 | public var shadowMapSize: CGSize { 96 | get { return scnLight.shadowMapSize } 97 | set { scnLight.shadowMapSize = newValue } 98 | } 99 | 100 | /** 101 | Specifies the number of sample per fragment to compute the shadow map. Defaults to 0. 102 | 103 | **On macOS 10.11 or earlier, the shadowSampleCount defaults to 16. On iOS 9 or earlier it defaults to 1.0. 104 | On macOS 10.12, iOS 10 and greater, when the shadowSampleCount is set to 0, a default sample count is chosen depending on the platform.** 105 | */ 106 | public var shadowSampleCount: Int { 107 | get { return scnLight.shadowSampleCount } 108 | set { scnLight.shadowSampleCount = newValue } 109 | } 110 | 111 | /** 112 | Specified the mode to use to cast shadows. See above for the available modes and their description. Defaults to SCNShadowModeForward. 113 | */ 114 | public var shadowMode: SCNShadowMode { 115 | get { return scnLight.shadowMode } 116 | set { scnLight.shadowMode = newValue } 117 | } 118 | 119 | /** 120 | Specifies the correction to apply to the shadow map to correct acne artefacts. It is multiplied by an implementation-specific value to create a constant depth offset. Defaults to 1.0 121 | */ 122 | 123 | public var shadowBias: CGFloat { 124 | get { return scnLight.shadowBias } 125 | set { scnLight.shadowBias = newValue } 126 | } 127 | /** 128 | Specifies if the shadow map projection should be done automatically or manually by the user. Defaults to YES. 129 | */ 130 | public var automaticallyAdjustsShadowProjection: Bool { 131 | get { return scnLight.automaticallyAdjustsShadowProjection } 132 | set { scnLight.automaticallyAdjustsShadowProjection = newValue } 133 | } 134 | 135 | /** 136 | Specifies the maximum distance from the viewpoint from which the shadows for the receiver light won't be computed. Defaults to 100.0. 137 | */ 138 | public var maximumShadowDistance: CGFloat { 139 | get { return scnLight.maximumShadowDistance } 140 | set { scnLight.maximumShadowDistance = newValue } 141 | } 142 | 143 | /** 144 | Render only back faces of the shadow caster when enabled. Defaults to NO. 145 | This is a behavior change from previous releases. 146 | */ 147 | public var forcesBackFaceCasters: Bool { 148 | get { return scnLight.forcesBackFaceCasters } 149 | set { scnLight.forcesBackFaceCasters = newValue } 150 | } 151 | 152 | /** 153 | Use the sample distribution of the main rendering to better fit the shadow frusta. Defaults to NO. 154 | */ 155 | public var sampleDistributedShadowMaps: Bool { 156 | get { return scnLight.sampleDistributedShadowMaps } 157 | set { scnLight.sampleDistributedShadowMaps = newValue } 158 | } 159 | 160 | /** 161 | Specifies the number of distinct shadow maps that will be computed for the receiver light. Defaults to 1. Maximum is 4. 162 | */ 163 | public var shadowCascadeCount: Int { 164 | get { return scnLight.shadowCascadeCount } 165 | set { scnLight.shadowCascadeCount = newValue } 166 | } 167 | 168 | /** 169 | Specifies a factor to interpolate between linear splitting (0) and logarithmic splitting (1). Defaults to 0.15. 170 | */ 171 | public var shadowCascadeSplittingFactor: CGFloat { 172 | get { return scnLight.shadowCascadeSplittingFactor } 173 | set { scnLight.shadowCascadeSplittingFactor = newValue } 174 | } 175 | 176 | /** 177 | Specifies the orthographic scale used to render from the directional light into the shadow map. Defaults to 1. 178 | 179 | **This is only applicable for directional lights.** 180 | */ 181 | public var orthographicScale: CGFloat { 182 | get { return scnLight.orthographicScale } 183 | set { scnLight.orthographicScale = newValue } 184 | } 185 | 186 | public var zRange: ClosedRange { 187 | get { return scnLight.zNear...scnLight.zFar } 188 | set { 189 | scnLight.zNear = newValue.lowerBound 190 | scnLight.zFar = newValue.upperBound 191 | } 192 | } 193 | 194 | /** 195 | The distance at which the attenuation starts and ends (Omni or Spot light types only). Animatable. Defaults to 0. 196 | */ 197 | public var attenuationDistance: ClosedRange { 198 | get { return scnLight.attenuationStartDistance...scnLight.attenuationEndDistance } 199 | set { 200 | scnLight.attenuationStartDistance = newValue.lowerBound 201 | scnLight.attenuationEndDistance = newValue.upperBound 202 | } 203 | } 204 | 205 | /** 206 | Specifies the attenuation between the start and end attenuation distances. 0 means a constant attenuation, 1 a linear attenuation and 2 a quadratic attenuation, but any positive value will work (Omni or Spot light types only). Animatable. Defaults to 2. 207 | */ 208 | public var attenuationFalloffExponent: CGFloat { 209 | get { return scnLight.attenuationFalloffExponent } 210 | set { scnLight.attenuationFalloffExponent = newValue } 211 | } 212 | 213 | /** 214 | The angle in degrees between the spot direction and the lit element below/after which the lighting is at full strength. Animatable. Inner defaults to 0. Outer defaults to 45 degrees. 215 | */ 216 | public var spotAngle: (inner: CGFloat, outer: CGFloat) { 217 | get { return (scnLight.spotInnerAngle, scnLight.spotOuterAngle) } 218 | set { 219 | scnLight.spotInnerAngle = newValue.inner 220 | scnLight.spotOuterAngle = newValue.outer 221 | } 222 | } 223 | 224 | /** 225 | Determines the node categories that will be lit by the receiver. Defaults to all bit set. 226 | */ 227 | public var categoryBitMask: Int { 228 | get { return scnLight.categoryBitMask } 229 | set { scnLight.categoryBitMask = newValue } 230 | } 231 | 232 | public override var gameObject: GameObject? { 233 | didSet { 234 | guard let node = gameObject?.node, 235 | node.light != scnLight 236 | else { return } 237 | 238 | node.light.map { scnLight = $0 } 239 | } 240 | } 241 | 242 | /** 243 | Configurable block that passes and returns itself. 244 | 245 | - parameters: 246 | - configurationBlock: block that passes itself. 247 | 248 | - returns: itself 249 | */ 250 | @discardableResult public func configure(_ configurationBlock: (Light) -> Void) -> Light { 251 | configurationBlock(self) 252 | return self 253 | } 254 | 255 | public override func awake() { 256 | guard let node = gameObject?.node, 257 | node.light == nil 258 | else { return } 259 | 260 | node.light = scnLight 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /Sources/UnityKit/Components/MeshCollider.swift: -------------------------------------------------------------------------------- 1 | import SceneKit 2 | 3 | /** 4 | A mesh collider allows you to do collision detection between meshes and primitives. 5 | */ 6 | public final class MeshCollider: Collider { 7 | private(set) public var mesh: Mesh? 8 | 9 | /** 10 | Configurable block that passes and returns itself. 11 | 12 | - parameters: 13 | - configurationBlock: block that passes itself. 14 | 15 | - returns: itself 16 | */ 17 | @discardableResult public func configure(_ configurationBlock: (MeshCollider) -> Void) -> MeshCollider { 18 | configurationBlock(self) 19 | return self 20 | } 21 | 22 | /// <#Description#> 23 | /// 24 | /// - Parameter mesh: <#mesh description#> 25 | /// - Returns: <#return value description#> 26 | @discardableResult public func set(mesh: Mesh?) -> MeshCollider { 27 | self.mesh = mesh 28 | return self 29 | } 30 | 31 | /// <#Description#> 32 | override func constructBody() { 33 | guard let gameObject = gameObject, 34 | let name = gameObject.name 35 | else { return } 36 | 37 | if let geometry = mesh?.geometry { 38 | geometry.name = name + "MeshCollider" 39 | 40 | let shape = SCNPhysicsShape( 41 | geometry: geometry, 42 | options: [.type: SCNPhysicsShape.ShapeType.convexHull, 43 | .scale: gameObject.transform.localScale.x] 44 | ) 45 | physicsShape = shape 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/UnityKit/Components/MeshFilter.swift: -------------------------------------------------------------------------------- 1 | /** 2 | A class to access the Mesh of the mesh filter. 3 | */ 4 | public final class MeshFilter: Component { 5 | override internal var order: ComponentOrder { 6 | .priority 7 | } 8 | /** 9 | Returns the instantiated Mesh assigned to the mesh filter. 10 | */ 11 | public var mesh: Mesh? 12 | } 13 | -------------------------------------------------------------------------------- /Sources/UnityKit/Components/ParticleSystem.swift: -------------------------------------------------------------------------------- 1 | import SceneKit 2 | 3 | /** 4 | Script interface for Particle Systems. 5 | */ 6 | public class ParticleSystem: Component { 7 | internal override var order: ComponentOrder { 8 | .other 9 | } 10 | public var scnParticleSystem: SCNParticleSystem? 11 | 12 | public override func onDestroy() { 13 | guard let particule = scnParticleSystem 14 | else { return } 15 | 16 | particule.reset() 17 | scnParticleSystem = nil 18 | gameObject?.node.removeParticleSystem(particule) 19 | } 20 | 21 | @discardableResult public func load( 22 | fileName: String, 23 | bundle: Bundle = Bundle.main, 24 | loops: Bool 25 | ) -> ParticleSystem { 26 | guard let modelUrl = searchPathForResource(for: fileName, extension: nil, bundle: bundle) 27 | else { return self } 28 | 29 | var path = modelUrl.relativePath 30 | .replacingOccurrences(of: bundle.bundlePath, with: "") 31 | .replacingOccurrences(of: modelUrl.lastPathComponent, with: "") 32 | 33 | if path.first == "/" { 34 | path.removeFirst() 35 | } 36 | 37 | guard let particule = SCNParticleSystem(named: modelUrl.lastPathComponent, inDirectory: path) 38 | else { return self } 39 | 40 | particule.colliderNodes = [] 41 | particule.loops = loops 42 | scnParticleSystem = particule 43 | gameObject?.node.addParticleSystem(particule) 44 | 45 | return self 46 | } 47 | 48 | @discardableResult public func execute(_ block: (SCNParticleSystem?) -> Void) -> ParticleSystem { 49 | block(scnParticleSystem) 50 | return self 51 | } 52 | 53 | @discardableResult public func executeAfter(milliseconds: Int, block: @escaping (SCNParticleSystem?) -> Void) -> ParticleSystem { 54 | DispatchQueue.main.asyncAfter(deadline: .now() + DispatchTimeInterval.milliseconds(milliseconds)) { [weak scnParticleSystem] in 55 | block(scnParticleSystem) 56 | } 57 | return self 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/UnityKit/Components/PlaneCollider.swift: -------------------------------------------------------------------------------- 1 | import SceneKit 2 | 3 | /** 4 | Representation of a plane in 3D space. 5 | */ 6 | public final class PlaneCollider: Collider { 7 | /** 8 | Configurable block that passes and returns itself. 9 | 10 | - parameters: 11 | - configurationBlock: block that passes itself. 12 | 13 | - returns: itself 14 | */ 15 | @discardableResult public func configure(_ configurationBlock: (PlaneCollider) -> Void) -> PlaneCollider { 16 | configurationBlock(self) 17 | return self 18 | } 19 | 20 | override func constructBody() { 21 | guard let gameObject = gameObject, 22 | let name = gameObject.name 23 | else { return } 24 | 25 | let boundingBox = gameObject.node.boundingBox 26 | let vertices = [ 27 | Vector3(boundingBox.min.x, boundingBox.max.y, boundingBox.min.z), //1 //0 28 | Vector3(boundingBox.max.x, boundingBox.max.y, boundingBox.min.z), //2 //1 29 | Vector3(boundingBox.max.x, boundingBox.max.y, boundingBox.max.z), //5 //2 30 | Vector3(boundingBox.min.x, boundingBox.max.y, boundingBox.max.z) //6 //3 31 | ] 32 | 33 | let indices: [Int16] = [ 34 | 2, 1, 0, 35 | 3, 2, 0 36 | ] 37 | 38 | let normals = [ 39 | Vector3(0, 1, 1), 40 | Vector3(0, 1, 1), 41 | Vector3(0, 1, 1), 42 | Vector3(0, 1, 1) 43 | ] 44 | 45 | let vertexData = Data(bytes: vertices, count: vertices.count * MemoryLayout.size) 46 | let vertexSource = SCNGeometrySource( 47 | data: vertexData, 48 | semantic: .vertex, 49 | vectorCount: vertices.count, 50 | usesFloatComponents: true, 51 | componentsPerVector: 3, 52 | bytesPerComponent: MemoryLayout.size, 53 | dataOffset: 0, 54 | dataStride: MemoryLayout.size 55 | ) 56 | 57 | let normalData = Data(bytes: normals, count: normals.count * MemoryLayout.size) 58 | let normalSource = SCNGeometrySource( 59 | data: normalData, 60 | semantic: .normal, 61 | vectorCount: normals.count, 62 | usesFloatComponents: true, 63 | componentsPerVector: 3, 64 | bytesPerComponent: MemoryLayout.size, 65 | dataOffset: 0, 66 | dataStride: MemoryLayout.size 67 | ) 68 | 69 | let indexData = Data(bytes: indices, count: indices.count * MemoryLayout.size) 70 | let element = SCNGeometryElement( 71 | data: indexData, 72 | primitiveType: .triangles, 73 | primitiveCount: indices.count / 3, 74 | bytesPerIndex: MemoryLayout.size 75 | ) 76 | 77 | let geometry = SCNGeometry(sources: [vertexSource, normalSource], elements: [element]) 78 | geometry.name = name + "PlaneCollider" 79 | 80 | physicsShape = SCNPhysicsShape( 81 | geometry: geometry, 82 | options: [.scale: gameObject.transform.localScale.x] 83 | ) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/UnityKit/Components/Renderer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SceneKit 3 | 4 | /** 5 | General functionality for all renderers. 6 | */ 7 | public final class Renderer: Component { 8 | override internal var order: ComponentOrder { 9 | .renderer 10 | } 11 | /** 12 | Returns all the instantiated materials of this object. 13 | */ 14 | public var materials = [Material]() { 15 | didSet { 16 | gameObject?.node.geometry?.materials = materials.map { material -> SCNMaterial in material.scnMaterial } 17 | } 18 | } 19 | 20 | /** 21 | Returns the first instantiated Material assigned to the renderer. 22 | */ 23 | public var material: Material? { 24 | get { 25 | return materials.first 26 | } 27 | set { 28 | if let newMaterial = newValue { 29 | materials = [newMaterial] 30 | } 31 | } 32 | } 33 | 34 | /** 35 | Does this object cast shadows? 36 | */ 37 | public var shadowCasting: Bool { 38 | get { 39 | guard let gameObject = gameObject 40 | else { return false } 41 | 42 | return gameObject.node.castsShadow 43 | } 44 | set { 45 | gameObject?.node.castsShadow = newValue 46 | } 47 | } 48 | 49 | /** 50 | Renderer's order within a sorting layer. 51 | */ 52 | public var sortingOrder: Int { 53 | get { 54 | guard let gameObject = gameObject 55 | else { return 0 } 56 | 57 | return gameObject.node.renderingOrder 58 | } 59 | set { 60 | gameObject?.node.renderingOrder = newValue 61 | } 62 | } 63 | 64 | /** 65 | Configurable block that passes and returns itself. 66 | 67 | - parameters: 68 | - configurationBlock: block that passes itself. 69 | 70 | - returns: itself 71 | */ 72 | @discardableResult public func configure(_ configurationBlock: (Renderer) -> Void) -> Renderer { 73 | configurationBlock(self) 74 | return self 75 | } 76 | 77 | public override func awake() { 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/UnityKit/Components/Rigidbody.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SceneKit 3 | 4 | /** 5 | Controls which degrees of freedom are allowed for the simulation of this Rigidbody. 6 | */ 7 | public struct RigidbodyConstraints: OptionSet { 8 | public let rawValue: Int 9 | 10 | public init(rawValue: Int) { 11 | self.rawValue = rawValue 12 | } 13 | 14 | public static let none = RigidbodyConstraints(rawValue: 1 << 0) 15 | public static let freezePositionX = RigidbodyConstraints(rawValue: 1 << 1) 16 | public static let freezePositionY = RigidbodyConstraints(rawValue: 1 << 2) 17 | public static let freezePositionZ = RigidbodyConstraints(rawValue: 1 << 3) 18 | public static let freezeRotationX = RigidbodyConstraints(rawValue: 1 << 4) 19 | public static let freezeRotationY = RigidbodyConstraints(rawValue: 1 << 5) 20 | public static let freezeRotationZ = RigidbodyConstraints(rawValue: 1 << 6) 21 | public static let freezePosition: RigidbodyConstraints = [.freezePositionX, .freezePositionY, .freezePositionZ] 22 | public static let freezeRotation: RigidbodyConstraints = [.freezeRotationX, .freezeRotationY, .freezeRotationZ] 23 | public static let freezeAll: RigidbodyConstraints = [.freezePosition, .freezeRotation] 24 | } 25 | 26 | /** 27 | Control of an object's position through physics simulation. 28 | 29 | Adding a Rigidbody component to an object will put its motion under the control of Unity's physics engine. Even without adding any code, a Rigidbody object will be pulled downward by gravity and will react to collisions with incoming objects if the right Collider component is also present. 30 | 31 | The Rigidbody also has a scripting API that lets you apply forces to the object and control it in a physically realistic way. For example, a car's behaviour can be specified in terms of the forces applied by the wheels. Given this information, the physics engine can handle most other aspects of the car's motion, so it will accelerate realistically and respond correctly to collisions. 32 | 33 | In a script, the FixedUpdate function is recommended as the place to apply forces and change Rigidbody settings (as opposed to Update, which is used for most other frame update tasks). The reason for this is that physics updates are carried out in measured time steps that don't coincide with the frame update. FixedUpdate is called immediately before each physics update and so any changes made there will be processed directly. 34 | 35 | A common problem when starting out with Rigidbodies is that the game physics appears to run in "slow motion". This is actually due to the scale used for your models. The default gravity settings assume that one world unit corresponds to one metre of distance. With non-physical games, it doesn't make much difference if your models are all 100 units long but when using physics, they will be treated as very large objects. If a large scale is used for objects that are supposed to be small, they will appear to fall very slowly - the physics engine thinks they are very large objects falling over very large distances. With this in mind, be sure to keep your objects more or less at their scale in real life (so a car should be about 4 units = 4 metres, for example). 36 | */ 37 | public final class Rigidbody: Component, Instantiable { 38 | override internal var order: ComponentOrder { 39 | return .rigidbody 40 | } 41 | public func instantiate(gameObject: GameObject) -> Rigidbody { 42 | let clone = type(of: self).init() 43 | clone.isStatic = isStatic 44 | clone.isKinematic = isKinematic 45 | clone.useGravity = useGravity 46 | clone.gameObject = gameObject 47 | clone.constraints = constraints 48 | return clone 49 | } 50 | 51 | public var constraints: RigidbodyConstraints = .none { 52 | didSet { 53 | func freezeAxe(_ value: Float) -> Float { 54 | if value < -.pi/2 { return -.pi } else if value > .pi/2 { return .pi } else { return 0 } 55 | } 56 | 57 | if constraints.contains(.freezePositionX) || 58 | constraints.contains(.freezePositionY) || 59 | constraints.contains(.freezePositionZ) { 60 | var factor = Vector3.one 61 | if constraints.contains(.freezePositionX) { 62 | factor.x = 0 63 | } 64 | if self.constraints.contains(.freezePositionY) { 65 | factor.y = 0 66 | } 67 | if self.constraints.contains(.freezePositionZ) { 68 | factor.z = 0 69 | } 70 | set(property: .velocityFactor(factor)) 71 | } 72 | 73 | if constraints.contains(.freezeRotationX) || 74 | constraints.contains(.freezeRotationY) || 75 | constraints.contains(.freezeRotationZ) { 76 | var factor = Vector3.one 77 | if constraints.contains(.freezeRotationX) { 78 | factor.x = 0 79 | } 80 | if self.constraints.contains(.freezeRotationY) { 81 | factor.y = 0 82 | } 83 | if self.constraints.contains(.freezeRotationZ) { 84 | factor.z = 0 85 | } 86 | set(property: .angularVelocityFactor(factor)) 87 | } 88 | } 89 | } 90 | 91 | public var position: Vector3 { 92 | transform?.position ?? .zero 93 | } 94 | 95 | public var localPosition: Vector3 { 96 | transform?.localPosition ?? .zero 97 | } 98 | 99 | public var localRotation: Quaternion { 100 | transform?.localRotation ?? .zero 101 | } 102 | 103 | public var useGravity: Bool = true { 104 | didSet { 105 | gameObject?.node.physicsBody?.isAffectedByGravity = useGravity 106 | } 107 | } 108 | 109 | public var isStatic: Bool = false { 110 | didSet { 111 | if isStatic { 112 | gameObject?.node.movabilityHint = .fixed 113 | } else { 114 | gameObject?.node.movabilityHint = .movable 115 | } 116 | } 117 | } 118 | 119 | public var isKinematic: Bool = true 120 | 121 | public enum Properties { 122 | public enum Setter { 123 | case mass(Float) 124 | case restitution(Float) 125 | case friction(Float) 126 | case rollingFriction(Float) 127 | case damping(Float) 128 | case angularDamping(Float) 129 | case velocity(Vector3) 130 | case angularVelocity(Vector4) 131 | case velocityFactor(Vector3) 132 | case angularVelocityFactor(Vector3) 133 | case allowsResting(Bool) 134 | } 135 | 136 | public enum Getter: Int { 137 | case mass 138 | case restitution 139 | case friction 140 | case rollingFriction 141 | case damping 142 | case angularDamping 143 | case velocity 144 | case angularVelocity 145 | case velocityFactor 146 | case angularVelocityFactor 147 | case allowsResting 148 | } 149 | } 150 | 151 | internal var properties = [Properties.Getter: Any]() 152 | 153 | public func set(property: Properties.Setter) { 154 | let physicsBody = gameObject?.node.physicsBody 155 | 156 | switch property { 157 | case let .mass(value): 158 | properties[.mass] = value.toCGFloat() 159 | physicsBody?.mass = value.toCGFloat() 160 | case let .restitution(value): 161 | properties[.restitution] = value.toCGFloat() 162 | physicsBody?.restitution = value.toCGFloat() 163 | case let .friction(value): 164 | properties[.friction] = value.toCGFloat() 165 | physicsBody?.friction = value.toCGFloat() 166 | case let .rollingFriction(value): 167 | properties[.rollingFriction] = value.toCGFloat() 168 | physicsBody?.rollingFriction = value.toCGFloat() 169 | case let .damping(value): 170 | properties[.damping] = value.toCGFloat() 171 | physicsBody?.damping = value.toCGFloat() 172 | case let .angularDamping(value): 173 | properties[.angularDamping] = value.toCGFloat() 174 | physicsBody?.angularDamping = value.toCGFloat() 175 | case let .velocity(value): 176 | properties[.velocity] = value 177 | physicsBody?.velocity = value 178 | case let .angularVelocity(value): 179 | properties[.angularVelocity] = value 180 | physicsBody?.angularVelocity = value 181 | case let .velocityFactor(value): 182 | properties[.velocityFactor] = value 183 | physicsBody?.velocityFactor = value 184 | case let .angularVelocityFactor(value): 185 | properties[.angularVelocityFactor] = value 186 | physicsBody?.angularVelocityFactor = value 187 | case let .allowsResting(value): 188 | properties[.allowsResting] = value 189 | physicsBody?.allowsResting = value 190 | } 191 | } 192 | 193 | public func get(property: Properties.Getter) -> T? where T: Getteable { 194 | let physicsBody = gameObject?.node.physicsBody 195 | 196 | switch property { 197 | case .mass: 198 | return properties[.mass] as? T ?? physicsBody?.mass.toFloat() as? T 199 | case .restitution: 200 | return properties[.restitution] as? T ?? physicsBody?.restitution.toFloat() as? T 201 | case .friction: 202 | return properties[.friction] as? T ?? physicsBody?.friction.toFloat() as? T 203 | case .rollingFriction: 204 | return properties[.rollingFriction] as? T ?? physicsBody?.rollingFriction.toFloat() as? T 205 | case .damping: 206 | return properties[.damping] as? T ?? physicsBody?.damping.toFloat() as? T 207 | case .angularDamping: 208 | return properties[.angularDamping] as? T ?? physicsBody?.angularDamping.toFloat() as? T 209 | case .velocity: 210 | return properties[.velocity] as? T ?? physicsBody?.velocity as? T 211 | case .angularVelocity: 212 | return properties[.angularVelocity] as? T ?? physicsBody?.angularVelocity as? T 213 | case .velocityFactor: 214 | return properties[.velocityFactor] as? T ?? physicsBody?.velocityFactor as? T 215 | case .angularVelocityFactor: 216 | return properties[.angularVelocityFactor] as? T ?? physicsBody?.angularVelocityFactor as? T 217 | case .allowsResting: 218 | return properties[.allowsResting] as? T ?? physicsBody?.allowsResting as? T 219 | } 220 | } 221 | 222 | /** 223 | Configurable block that passes and returns itself. 224 | 225 | - parameters: 226 | - configurationBlock: block that passes itself. 227 | 228 | - returns: itself 229 | */ 230 | @discardableResult public func configure(_ configurationBlock: (Rigidbody) -> Void) -> Rigidbody { 231 | configurationBlock(self) 232 | return self 233 | } 234 | 235 | public override func onDestroy() { 236 | gameObject?.node.physicsBody = nil 237 | } 238 | 239 | public override func start() { 240 | if let _ = getComponent(Collider.self) { 241 | return 242 | } 243 | gameObject?.updatePhysicsBody() 244 | } 245 | 246 | public func movePosition(_ position: Vector3) { 247 | guard let transform = gameObject?.transform 248 | else { return } 249 | 250 | transform.position = position 251 | } 252 | 253 | public func moveRotation(_ to: Vector3) { 254 | guard let transform = gameObject?.transform 255 | else { return } 256 | 257 | transform.localEulerAngles = to 258 | } 259 | 260 | public func addForce(_ direction: Vector3) { 261 | guard let physicsBody = gameObject?.node.physicsBody 262 | else { return } 263 | 264 | physicsBody.applyForce(direction, asImpulse: true) 265 | } 266 | 267 | public func addTorque(_ torque: Vector4, asImpulse: Bool) { 268 | guard let physicsBody = gameObject?.node.physicsBody 269 | else { return } 270 | 271 | physicsBody.applyTorque(torque, asImpulse: asImpulse) 272 | } 273 | 274 | public func addExplosionForce( 275 | explosionForce: Float, 276 | explosionPosition: Vector3, 277 | explosionRadius: Float, 278 | replacePosition: Vector3Nullable? = nil 279 | ) { 280 | guard let gameObject = gameObject, 281 | let transform = gameObject.transform, 282 | let physicsBody = gameObject.node.physicsBody 283 | else { return } 284 | 285 | var from = explosionPosition 286 | var to = transform.position 287 | 288 | replacePosition?.x.map { from.x = $0; to.x = $0 } 289 | replacePosition?.y.map { from.y = $0; to.y = $0 } 290 | replacePosition?.z.map { from.z = $0; to.z = $0 } 291 | 292 | let heading = to - from 293 | let distance = heading.magnitude() 294 | var direction = (heading / distance).normalized() 295 | 296 | direction *= explosionForce / distance 297 | 298 | physicsBody.applyForce(direction, asImpulse: true) 299 | } 300 | 301 | public func clearAllForces() { 302 | guard let physicsBody = gameObject?.node.physicsBody 303 | else { return } 304 | 305 | physicsBody.clearAllForces() 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /Sources/UnityKit/Components/Transform.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SceneKit 3 | 4 | /** 5 | Position, rotation and scale of an object. 6 | 7 | Every object in a scene has a Transform. It's used to store and manipulate the position, rotation and scale of the object. Every Transform can have a parent, which allows you to apply position, rotation and scale hierarchically. 8 | ### Usage Example: ### 9 | ```` 10 | public class ExampleClass: MonoBehaviour { 11 | void Example() { 12 | transform?.children?.forEach { 13 | $0.position += Vector3.up * 10.0 14 | } 15 | } 16 | } 17 | ```` 18 | */ 19 | public final class Transform: Component { 20 | override internal var order: ComponentOrder { 21 | .transform 22 | } 23 | 24 | public required init() { 25 | super.init() 26 | } 27 | 28 | public init(_ gameObject: GameObject) { 29 | super.init() 30 | self.gameObject = gameObject 31 | } 32 | 33 | /// The children of the transform. 34 | public var children: [Transform]? { return gameObject?.getChildren().map { $0.transform } } 35 | /// The parent of the transform. 36 | public var parent: Transform? { return gameObject?.parent?.transform } 37 | /// The number of children the Transform has. 38 | public var childCount: Int { return gameObject?.getChildren().count ?? 0 } 39 | 40 | /// The blue axis of the transform in world space. 41 | public var forward: Vector3 { 42 | guard let node = gameObject?.node 43 | else { return .zero } 44 | 45 | return Vector3(hasOrIsPartOfPhysicsBody() ? node.presentation.simdWorldFront : node.simdWorldFront) 46 | } 47 | 48 | /// The negative blue axis of the transform in world space. 49 | public var back: Vector3 { 50 | return forward.negated() 51 | } 52 | 53 | /// The green axis of the transform in world space. 54 | public var up: Vector3 { 55 | guard let node = gameObject?.node 56 | else { return .zero } 57 | 58 | return Vector3(hasOrIsPartOfPhysicsBody() ? node.presentation.simdWorldUp : node.simdWorldUp) 59 | } 60 | 61 | /// The negative green axis of the transform in world space. 62 | public var bottom: Vector3 { 63 | return up.negated() 64 | } 65 | 66 | /// The red axis of the transform in world space. 67 | public var right: Vector3 { 68 | guard let node = gameObject?.node 69 | else { return .zero } 70 | 71 | return Vector3(hasOrIsPartOfPhysicsBody() ? node.presentation.simdWorldRight : node.simdWorldRight) 72 | } 73 | 74 | /// The negative red axis of the transform in world space. 75 | public var left: Vector3 { 76 | return right.negated() 77 | } 78 | 79 | /// The global scale of the object (Read Only). 80 | public var lossyScale: Vector3 { 81 | guard let parent = gameObject?.parent 82 | else { return localScale } 83 | 84 | return parent.transform.lossyScale * localScale 85 | } 86 | 87 | private func hasOrIsPartOfPhysicsBody() -> Bool { 88 | guard let gameObject = gameObject 89 | else { return false } 90 | 91 | guard let parent = gameObject.parent 92 | else { return gameObject.node.physicsBody != nil } 93 | 94 | return gameObject.node.physicsBody != nil || parent.transform.hasOrIsPartOfPhysicsBody() 95 | } 96 | 97 | /** 98 | The position of the transform in world space. 99 | 100 | The position member can be accessed by the Game code. Setting this value can be used to animate the GameObject. The example below makes an attached sphere bounce by updating the position. This bouncing slowly comes to an end. The position can also be use to determine where in 3D space the transform. 101 | */ 102 | public var position: Vector3 { 103 | get { 104 | guard let node = gameObject?.node 105 | else { return .zero } 106 | 107 | return hasOrIsPartOfPhysicsBody() ? node.presentation.worldPosition : node.worldPosition 108 | } 109 | set { 110 | guard let node = gameObject?.node 111 | else { return } 112 | 113 | node.worldPosition = newValue 114 | } 115 | } 116 | 117 | /// The orientation of the transform in world space stored as a Quaternion. 118 | public var orientation: Quaternion { 119 | get { 120 | guard let node = gameObject?.node 121 | else { return .zero } 122 | 123 | return hasOrIsPartOfPhysicsBody() ? node.presentation.worldOrientation : node.worldOrientation 124 | } 125 | set { 126 | gameObject?.node.worldOrientation = newValue 127 | } 128 | } 129 | 130 | /// The orientation of the transform relative to the parent transform's orientation. 131 | public var localOrientation: Quaternion { 132 | get { 133 | guard let node = gameObject?.node 134 | else { return .zero } 135 | 136 | return hasOrIsPartOfPhysicsBody() ? node.presentation.orientation : node.orientation 137 | } 138 | set { 139 | gameObject?.node.orientation = newValue 140 | } 141 | } 142 | 143 | /// Position of the transform relative to the parent transform. 144 | public var localPosition: Vector3 { 145 | get { 146 | guard let node = gameObject?.node 147 | else { return .zero } 148 | 149 | return hasOrIsPartOfPhysicsBody() ? node.presentation.position : node.position 150 | } 151 | set { 152 | gameObject?.node.position = newValue 153 | } 154 | } 155 | 156 | /// The rotation of the transform relative to the parent transform's rotation. 157 | public var localRotation: Vector4 { 158 | get { 159 | guard let node = gameObject?.node 160 | else { return .zero } 161 | 162 | return hasOrIsPartOfPhysicsBody() ? node.presentation.rotation : node.rotation 163 | } 164 | set { 165 | gameObject?.node.rotation = newValue 166 | } 167 | } 168 | 169 | /// The rotation as Euler angles in degrees. 170 | public var localEulerAngles: Vector3 { 171 | get { 172 | guard let node = gameObject?.node 173 | else { return .zero } 174 | 175 | return hasOrIsPartOfPhysicsBody() ? node.presentation.orientation.toEuler().radiansToDegrees() : node.orientation.toEuler().radiansToDegrees() 176 | } 177 | set { 178 | gameObject?.node.eulerAngles = newValue.degreesToRadians() 179 | } 180 | } 181 | 182 | /// The scale of the transform relative to the parent. 183 | public var localScale: Vector3 { 184 | get { 185 | guard let node = gameObject?.node 186 | else { return .zero } 187 | 188 | return hasOrIsPartOfPhysicsBody() ? node.presentation.scale : node.scale 189 | } 190 | set { 191 | gameObject?.node.scale = newValue 192 | } 193 | } 194 | 195 | /// Rotates the transform so the forward vector points at /target/'s current position. 196 | /// 197 | /// - Parameter target: Object to point towards. 198 | public func lookAt(_ target: Transform) { 199 | if let constraints = gameObject?.node.constraints, constraints.count > 0 { 200 | Debug.warning("remove constraints on node before using lookAt") 201 | return 202 | } 203 | 204 | gameObject?.node.look(at: target.position) 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /Sources/UnityKit/Components/Vehicle.swift: -------------------------------------------------------------------------------- 1 | import SceneKit 2 | 3 | /** 4 | Wheel component. 5 | */ 6 | public class Wheel { 7 | public struct Parameters { 8 | public let nodeName: String 9 | public var suspensionStiffness: Float? 10 | public var suspensionCompression: Float? 11 | public var suspensionDamping: Float? 12 | public var maximumSuspensionTravel: Float? 13 | public var frictionSlip: Float? 14 | public var maximumSuspensionForce: Float? 15 | public var connectionPosition: Vector3? 16 | public var steeringAxis: Vector3? 17 | public var axle: Vector3? 18 | public var radius: Float? 19 | public var suspensionRestLength: Float? 20 | 21 | public init(nodeName: String) { 22 | self.nodeName = nodeName 23 | } 24 | } 25 | 26 | public let gameObject: GameObject 27 | public var parameters: Parameters 28 | 29 | public init(gameObject: GameObject, parameter: Parameters) { 30 | self.gameObject = gameObject 31 | self.parameters = parameter 32 | } 33 | 34 | fileprivate func scnWheel() -> SCNPhysicsVehicleWheel { 35 | let wheel = SCNPhysicsVehicleWheel(node: gameObject.node) 36 | parameters.suspensionStiffness.map { wheel.suspensionStiffness = $0.toCGFloat() } 37 | parameters.suspensionCompression.map { wheel.suspensionCompression = $0.toCGFloat() } 38 | parameters.suspensionDamping.map { wheel.suspensionDamping = $0.toCGFloat() } 39 | parameters.maximumSuspensionTravel.map { wheel.maximumSuspensionTravel = $0.toCGFloat() } 40 | parameters.frictionSlip.map { wheel.frictionSlip = $0.toCGFloat() } 41 | parameters.maximumSuspensionForce.map { wheel.maximumSuspensionForce = $0.toCGFloat() } 42 | parameters.connectionPosition.map { wheel.connectionPosition = $0 } 43 | parameters.steeringAxis.map { wheel.steeringAxis = $0 } 44 | parameters.axle.map { wheel.axle = $0 } 45 | parameters.radius.map { wheel.radius = $0.toCGFloat() } 46 | parameters.suspensionRestLength.map { wheel.suspensionRestLength = $0.toCGFloat() } 47 | return wheel 48 | } 49 | } 50 | 51 | /** 52 | The Vehicles module implements vehicle physics simulation through the Wheel component. 53 | */ 54 | public class Vehicle: Component { 55 | override internal var order: ComponentOrder { 56 | return .vehicle 57 | } 58 | private(set) public var wheels = [Wheel]() 59 | private var parameters: [Wheel.Parameters]? 60 | private var vehicle: SCNPhysicsVehicle? 61 | private var physicsWorld: SCNPhysicsWorld? 62 | public var speedInKilometersPerHour: Float { 63 | guard let vehicle = vehicle 64 | else { return 0 } 65 | return vehicle.speedInKilometersPerHour.toFloat() 66 | } 67 | 68 | /** 69 | Configurable block that passes and returns itself. 70 | 71 | - parameters: 72 | - configurationBlock: block that passes itself. 73 | 74 | - returns: itself 75 | */ 76 | @discardableResult public func configure(_ configurationBlock: (Vehicle) -> Void) -> Vehicle { 77 | configurationBlock(self) 78 | return self 79 | } 80 | 81 | @discardableResult public func set(wheels parameters: [Wheel.Parameters], physicsWorld: SCNPhysicsWorld) -> Vehicle { 82 | self.physicsWorld = physicsWorld 83 | self.parameters = parameters 84 | 85 | guard let gameObject = gameObject, 86 | let physicsBody = gameObject.node.physicsBody 87 | else { return self } 88 | 89 | wheels = parameters.compactMap { parameter -> Wheel? in 90 | guard let wheel = GameObject.find(.name(.exact(parameter.nodeName)), in: gameObject) 91 | else { return nil } 92 | 93 | return Wheel(gameObject: wheel, parameter: parameter) 94 | } 95 | 96 | let vehicle = SCNPhysicsVehicle( 97 | chassisBody: physicsBody, 98 | wheels: wheels.map { wheel -> SCNPhysicsVehicleWheel in return wheel.scnWheel() } 99 | ) 100 | physicsWorld.addBehavior(vehicle) 101 | 102 | self.vehicle = vehicle 103 | 104 | return self 105 | } 106 | 107 | public override func onDestroy() { 108 | guard let physicsWorld = physicsWorld, 109 | let vehicle = vehicle 110 | else { return } 111 | 112 | physicsWorld.removeBehavior(vehicle) 113 | } 114 | 115 | public override func start() { 116 | if let physicsWorld = physicsWorld, 117 | let parameters = parameters, 118 | vehicle == nil { 119 | set(wheels: parameters, physicsWorld: physicsWorld) 120 | } 121 | } 122 | 123 | private func wheelStride(_ vehicle: SCNPhysicsVehicle, forWheelAt index: Int?) -> StrideThrough? { 124 | guard vehicle.wheels.count > 0 125 | else { return nil } 126 | 127 | if let index = index { 128 | return stride(from: index, through: index, by: 1) 129 | } 130 | 131 | return stride(from: 0, through: vehicle.wheels.count - 1, by: 1) 132 | } 133 | 134 | public func applyEngineForce(_ value: Float, forWheelAt index: Int? = nil) { 135 | guard let vehicle = vehicle, 136 | let stride = wheelStride(vehicle, forWheelAt: index) 137 | else { return } 138 | 139 | DispatchQueue.main.async { [weak vehicle] () -> Void in 140 | for i in stride { 141 | vehicle?.applyEngineForce(value.toCGFloat(), forWheelAt: i) 142 | } 143 | } 144 | } 145 | 146 | public func applySteeringAngle(_ value: Degree, forWheelAt index: Int? = nil) { 147 | guard let vehicle = vehicle, 148 | let stride = wheelStride(vehicle, forWheelAt: index) 149 | else { return } 150 | 151 | DispatchQueue.main.async { [weak vehicle] () -> Void in 152 | for i in stride { 153 | vehicle?.setSteeringAngle(value.toCGFloat(), forWheelAt: i) 154 | } 155 | } 156 | } 157 | 158 | public func applyBrakingForce(_ value: Float, forWheelAt index: Int? = nil) { 159 | guard let vehicle = vehicle, 160 | let stride = wheelStride(vehicle, forWheelAt: index) 161 | else { return } 162 | 163 | DispatchQueue.main.async { [weak vehicle] () -> Void in 164 | for i in stride { 165 | vehicle?.applyBrakingForce(value.toCGFloat(), forWheelAt: i) 166 | } 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /Sources/UnityKit/Element/AudioClip.swift: -------------------------------------------------------------------------------- 1 | import AVKit 2 | 3 | public class AudioClip { 4 | public enum PlayType { 5 | case playOnce 6 | case loop 7 | } 8 | 9 | public var playType: PlayType = .playOnce 10 | private(set) internal var bus: Int = 0 11 | private(set) internal var buffer: AVAudioPCMBuffer? 12 | public let filename: String 13 | 14 | public init?(fileName: String, playType: PlayType = .playOnce, bundle: Bundle = Bundle.main) { 15 | guard let audioUrl = searchPathForResource(for: fileName, extension: nil, bundle: bundle) 16 | else { return nil } 17 | 18 | do { 19 | let file = try AVAudioFile(forReading: audioUrl) 20 | guard let format = AudioEngine.sharedInstance.format, // AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: file.fileFormat.sampleRate, channels: file.fileFormat.channelCount, interleaved: false), 21 | let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: UInt32(file.length)) 22 | else { return nil } 23 | 24 | try file.read(into: buffer) 25 | 26 | let engine = AudioEngine.sharedInstance 27 | self.bus = engine.environment.nextAvailableInputBus 28 | self.buffer = buffer 29 | self.filename = fileName 30 | self.playType = playType 31 | } catch { 32 | return nil 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/UnityKit/Element/AudioEngine.swift: -------------------------------------------------------------------------------- 1 | import AVKit 2 | 3 | internal class AudioEngine: AVAudioEngine { 4 | internal static let sharedInstance = AudioEngine() 5 | internal var environment = AVAudioEnvironmentNode() 6 | internal var format: AVAudioFormat? 7 | 8 | override init() { 9 | super.init() 10 | 11 | let sessionInstance = AVAudioSession.sharedInstance() 12 | let hardwareSampleRate = environment.outputFormat(forBus: 0).sampleRate 13 | let maxChannels = sessionInstance.maximumOutputNumberOfChannels 14 | format = AVAudioFormat(standardFormatWithSampleRate: hardwareSampleRate, channels: AVAudioChannelCount(maxChannels)) 15 | 16 | do { 17 | try sessionInstance.setCategory(AVAudioSession.Category(rawValue: convertFromAVAudioSessionCategory(AVAudioSession.Category.playback))) 18 | try sessionInstance.setPreferredOutputNumberOfChannels(min(8, maxChannels)) 19 | try sessionInstance.setActive(true) 20 | } catch {} 21 | 22 | attach(environment) 23 | connect(environment, to: outputNode, format: format) 24 | } 25 | 26 | internal func startEngine() { 27 | guard !isRunning 28 | else { return } 29 | 30 | prepare() 31 | 32 | do { try start() } catch {} 33 | } 34 | } 35 | 36 | // Helper function inserted by Swift 4.2 migrator. 37 | private func convertFromAVAudioSessionCategory(_ input: AVAudioSession.Category) -> String { 38 | return input.rawValue 39 | } 40 | -------------------------------------------------------------------------------- /Sources/UnityKit/Element/GameObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SceneKit 3 | 4 | public class GameObject: Object { 5 | internal var task: DispatchWorkItem? 6 | 7 | public override var name: String? { 8 | didSet { 9 | node.name = name 10 | } 11 | } 12 | 13 | public var layer: Layer { 14 | get { 15 | return Layer(rawValue: node.categoryBitMask) 16 | } 17 | set { 18 | guard node.camera == nil, 19 | node.light == nil 20 | else { return } 21 | 22 | node.categoryBitMask = newValue.rawValue 23 | children.forEach { $0.layer = newValue } 24 | } 25 | } 26 | 27 | public var tag: Tag = .untagged 28 | 29 | public var node: SCNNode 30 | 31 | /*! 32 | @property ignoreUpdates 33 | @abstract Specifies if the receiver's will cascade the update calls. 34 | @discussion By manually changing this value can improve considerably performance by skipping update calls. 35 | */ 36 | public var ignoreUpdates = true { 37 | didSet { 38 | if !ignoreUpdates { 39 | parent?.ignoreUpdates = false 40 | } 41 | } 42 | } 43 | 44 | private(set) public var transform: Transform! 45 | private(set) public var renderer: Renderer? 46 | 47 | private(set) internal var children = [GameObject]() 48 | 49 | private(set) public weak var parent: GameObject? 50 | private(set) public weak var scene: Scene? { 51 | didSet { 52 | guard oldValue != scene 53 | else { return } 54 | 55 | if let parent = parent, let rootGameObject = oldValue?.rootGameObject, parent == rootGameObject { 56 | scene?.rootGameObject.addChild(self) 57 | } 58 | 59 | movedToScene() 60 | } 61 | } 62 | 63 | private var didAwake: Bool = false 64 | private var didStart: Bool = false 65 | private var waitNextUpdate: Bool = true { 66 | didSet { 67 | children.forEach { $0.waitNextUpdate = waitNextUpdate } 68 | } 69 | } 70 | 71 | public var activeInHierarchy: Bool { 72 | if let parent = parent { 73 | return activeSelf && parent.activeInHierarchy 74 | } 75 | return activeSelf 76 | } 77 | 78 | private(set) public var activeSelf: Bool { 79 | get { 80 | return !node.isHidden 81 | } 82 | set { 83 | node.isHidden = !newValue 84 | 85 | for component in components { 86 | guard let behaviour = component as? Behaviour 87 | else { continue } 88 | 89 | behaviour.enabled = newValue 90 | } 91 | } 92 | } 93 | 94 | public var enabled: Bool { 95 | get { 96 | return activeSelf 97 | } 98 | set { 99 | activeSelf = newValue 100 | } 101 | } 102 | 103 | public var boundingBox: BoundingBox { 104 | return node.boundingBox 105 | } 106 | 107 | public var boundingSphere: BoundingSphere { 108 | return node.boundingSphere 109 | } 110 | 111 | public convenience init?( 112 | fileName: String, 113 | nodeName: String?, 114 | bundle: Bundle = Bundle.main 115 | ) { 116 | guard let modelUrl = searchPathForResource(for: fileName, extension: nil, bundle: bundle) 117 | else { return nil } 118 | 119 | self.init(modelUrl: modelUrl, nodeName: nodeName) 120 | } 121 | 122 | public convenience init?( 123 | modelPath: String, 124 | nodeName: String?, 125 | bundle: Bundle = Bundle.main 126 | ) { 127 | guard let modelUrl = bundle.url(forResource: modelPath, withExtension: nil) 128 | else { return nil } 129 | 130 | self.init(modelUrl: modelUrl, nodeName: nodeName) 131 | } 132 | 133 | public convenience init?(modelUrl: URL, nodeName: String?) { 134 | guard let referenceNode = SCNReferenceNode(url: modelUrl) 135 | else { return nil } 136 | 137 | referenceNode.load() 138 | 139 | if let nodeName = nodeName { 140 | guard let node = referenceNode.childNodes.filter({ $0.name == nodeName }).first 141 | else { return nil } 142 | 143 | self.init(node) 144 | } else { 145 | self.init(referenceNode) 146 | } 147 | } 148 | 149 | public convenience init(name: String) { 150 | let node = SCNNode() 151 | node.name = name 152 | self.init(node) 153 | } 154 | 155 | public init(_ node: SCNNode) { 156 | self.node = node 157 | super.init() 158 | initialize() 159 | awake() 160 | } 161 | 162 | internal func initialize() { 163 | self.name = node.name ?? "No name" 164 | self.layer = .`default` 165 | self.transform = addComponent(external: false, type: Transform.self) 166 | 167 | if let geometry = node.geometry { 168 | let meshFilter = addComponent(external: false, type: MeshFilter.self) 169 | meshFilter.mesh = Mesh(geometry) 170 | 171 | self.renderer = addComponent(external: false, type: Renderer.self) 172 | 173 | self.renderer?.materials = geometry.materials.map { Material($0) } 174 | } 175 | 176 | if let camera = node.camera { 177 | addComponent(external: false, type: Camera.self).scnCamera = camera 178 | } 179 | 180 | if let light = node.light { 181 | addComponent(external: false, type: Light.self).scnLight = light 182 | } 183 | 184 | GameObject.convertAllChildToGameObjects(self) 185 | } 186 | 187 | /// Create a new instance 188 | public required init() { 189 | self.node = SCNNode() 190 | super.init() 191 | self.transform = addComponent(external: false, type: Transform.self) 192 | awake() 193 | } 194 | 195 | public override func destroy() { 196 | super.destroy() 197 | parent?.removeChild(self) 198 | } 199 | 200 | public func instantiate() -> GameObject { 201 | let cloneNode = node.deepClone() 202 | let clone = GameObject(cloneNode) 203 | 204 | if let name = name { 205 | clone.name = "\(name) Clone" 206 | } 207 | 208 | clone.tag = tag 209 | clone.layer = layer 210 | 211 | components.forEach { 212 | if let component = $0 as? Component & Instantiable { 213 | clone.addComponent(component.instantiate(gameObject: clone), gameObject: clone) 214 | } 215 | } 216 | return clone 217 | } 218 | 219 | internal func shouldIgnoreUpdates() -> Bool { 220 | return components.first(where: { !$0.ignoreUpdates }) == nil 221 | } 222 | 223 | internal func setScene(_ scene: Scene) { 224 | self.scene = scene 225 | children.forEach { $0.setScene(scene) } 226 | } 227 | 228 | internal override func movedToScene() { 229 | components.forEach { $0.movedToScene() } 230 | } 231 | 232 | public func setActive(_ active: Bool) { 233 | self.activeSelf = active 234 | } 235 | 236 | //Update 237 | 238 | public override func awake() { 239 | guard !didAwake 240 | else { return } 241 | 242 | didAwake = true 243 | components.forEach { $0.awake() } 244 | children.forEach { $0.awake() } 245 | } 246 | 247 | public override func start() { 248 | guard didAwake, 249 | !didStart, 250 | activeSelf 251 | else { return } 252 | 253 | guard !waitNextUpdate else { 254 | waitNextUpdate = false 255 | return 256 | } 257 | 258 | didStart = true 259 | components.forEach { $0.start() } 260 | children.forEach { $0.start() } 261 | setActive(true) 262 | } 263 | 264 | override func internalUpdate() { 265 | guard didAwake, 266 | didStart, 267 | activeSelf 268 | else { return } 269 | 270 | components 271 | .compactMap { $0 as? MonoBehaviour } 272 | .filter { $0.enabled } 273 | .forEach { $0.internalUpdate() } 274 | 275 | children 276 | .filter { !$0.ignoreUpdates } 277 | .forEach { $0.internalUpdate() } 278 | } 279 | 280 | public override func preUpdate() { 281 | guard didAwake, 282 | didStart, 283 | activeSelf 284 | else { return } 285 | 286 | components 287 | .filter { 288 | if !$0.implementsPreUpdate { return false } 289 | if let behaviour = $0 as? Behaviour { return behaviour.enabled } 290 | return true 291 | } 292 | .forEach { $0.preUpdate() } 293 | 294 | children 295 | .filter { !$0.ignoreUpdates } 296 | .forEach { $0.preUpdate() } 297 | } 298 | 299 | public override func update() { 300 | guard didAwake, 301 | activeSelf 302 | else { return } 303 | 304 | guard didStart else { 305 | start() 306 | return 307 | } 308 | 309 | components 310 | .filter { 311 | if !$0.implementsUpdate { return false } 312 | if let behaviour = $0 as? Behaviour { return behaviour.enabled } 313 | return true 314 | } 315 | .forEach { $0.update() } 316 | 317 | children 318 | .filter { !$0.ignoreUpdates || !$0.didStart } 319 | .forEach { $0.update() } 320 | } 321 | 322 | public override func fixedUpdate() { 323 | guard didAwake, 324 | didStart, 325 | activeSelf 326 | else { return } 327 | 328 | components 329 | .filter { 330 | if !$0.implementsFixedUpdate { return false } 331 | if let behaviour = $0 as? Behaviour { return behaviour.enabled } 332 | return true 333 | } 334 | .forEach { $0.fixedUpdate() } 335 | 336 | children 337 | .filter { !$0.ignoreUpdates } 338 | .forEach { $0.fixedUpdate() } 339 | } 340 | 341 | public func removeFromParent() { 342 | parent?.removeChild(self) 343 | } 344 | 345 | // Component 346 | 347 | @discardableResult internal override func addComponent( 348 | _ component: T, 349 | gameObject: GameObject? 350 | ) -> T { 351 | return super.addComponent(component, gameObject: gameObject) 352 | } 353 | 354 | @discardableResult public override func addComponent(_ type: T.Type) -> T { 355 | let component = super.addComponent(external: true, type: type, gameObject: self) 356 | self.ignoreUpdates = shouldIgnoreUpdates() 357 | return component 358 | } 359 | 360 | @discardableResult internal override func addComponent( 361 | external: Bool = true, 362 | type: T.Type, 363 | gameObject: GameObject? = nil 364 | ) -> T { 365 | return super.addComponent(external: external, type: type, gameObject: gameObject ?? self) 366 | } 367 | 368 | public func getComponentInChild(_ type: T.Type) -> T? { 369 | for child in children { 370 | if let component = child.getComponent(type) { 371 | return component 372 | } 373 | } 374 | for child in children { 375 | if let component = child.getComponentInChild(type) { 376 | return component 377 | } 378 | } 379 | return nil 380 | } 381 | 382 | public func getComponentsInChild(_ type: T.Type) -> [T] { 383 | return children.flatMap { (child) -> [T] in 384 | child.getComponents(type) + child.getComponentsInChild(type) 385 | } 386 | } 387 | 388 | // Child 389 | 390 | public func addToScene(_ scene: Scene) { 391 | setScene(scene) 392 | parent = scene.rootGameObject 393 | 394 | scene.rootGameObject.addChild(self) 395 | } 396 | 397 | public func addChild(_ child: GameObject) { 398 | if let scene = scene { 399 | child.setScene(scene) 400 | } 401 | child.parent = self 402 | if !child.ignoreUpdates { 403 | self.ignoreUpdates = false 404 | } 405 | if children.first(where: { $0 == child }) == nil { 406 | children.append(child) 407 | if child.node.parent != node { 408 | node.addChildNode(child.node) 409 | } 410 | } 411 | } 412 | 413 | public func getChildren() -> [GameObject] { 414 | return children 415 | } 416 | 417 | internal func getChildNodes() -> [SCNNode] { 418 | return node.childNodes 419 | } 420 | 421 | public func getChild(_ index: Int) -> GameObject? { 422 | return children[index] 423 | } 424 | 425 | public func removeChild(_ child: GameObject) { 426 | if let index = children.firstIndex(where: { $0 == child }) { 427 | if let gameObject = getChild(index) { 428 | gameObject.node.removeFromParentNode() 429 | } 430 | children.remove(at: index) 431 | } 432 | } 433 | } 434 | -------------------------------------------------------------------------------- /Sources/UnityKit/Element/Layer.swift: -------------------------------------------------------------------------------- 1 | extension GameObject { 2 | public struct Layer: OptionSet { 3 | public let rawValue: Int 4 | 5 | public init(rawValue: Int) { 6 | self.rawValue = rawValue 7 | } 8 | 9 | public static let `default` = Layer(rawValue: 1 << 0) 10 | public static let ground = Layer(rawValue: 1 << 1) 11 | public static let player = Layer(rawValue: 1 << 2) 12 | public static let environment = Layer(rawValue: 1 << 3) 13 | public static let projectile = Layer(rawValue: 1 << 4) 14 | 15 | public static var all: Layer { 16 | return layers.values.dropFirst().reduce(`default`) { prev, layer -> Layer in 17 | [prev, layer] 18 | } 19 | } 20 | 21 | private(set) public static var layers = ["default": `default`, "ground": ground, "player": player, "environment": environment, "projectile": projectile] 22 | 23 | public static func layer(for name: String) -> Layer { 24 | return layers[name] ?? `default` 25 | } 26 | 27 | public static func name(for layer: Layer) -> String { 28 | guard let index = layers.firstIndex(where: { _, value -> Bool in value == layer }) else { return "" } 29 | return layers.keys[index] 30 | } 31 | 32 | @discardableResult internal static func addLayer(with name: String) -> Layer { 33 | if let layer = layers[name] { 34 | return layer 35 | } 36 | 37 | let rawValue = 1 << layers.count 38 | let layer = Layer(rawValue: rawValue) 39 | layers[name] = layer 40 | 41 | return layer 42 | } 43 | 44 | public func isPart(of bitMaskRawValue: Int) -> Bool { 45 | return Layer(rawValue: bitMaskRawValue).contains(self) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/UnityKit/Element/Material.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SceneKit 3 | 4 | public final class Material: Object { 5 | public enum BasicColorShading: String { 6 | case diffuse = "_Color" 7 | case specular = "_SpecColor" 8 | case emission = "_EmissionColor" 9 | case reflective = "_ReflectColor" 10 | case unknown = "" 11 | } 12 | 13 | public enum BasicTextureShading: String { 14 | case diffuse = "_MainText" 15 | case normal = "_BumpMap" 16 | case reflective = "_Cube" 17 | case unknown = "" 18 | } 19 | 20 | public let scnMaterial: SCNMaterial 21 | 22 | public var color: Color? { 23 | get { 24 | return getColor(.diffuse) 25 | } 26 | set { 27 | setColor(.diffuse, color: newValue) 28 | } 29 | } 30 | 31 | public var mainTexture: UIImage? { 32 | get { 33 | return getTexture(.diffuse) 34 | } 35 | set { 36 | setTexture(.diffuse, image: newValue) 37 | } 38 | } 39 | 40 | public var doubleSidedGI: Bool { 41 | get { 42 | return scnMaterial.isDoubleSided 43 | } 44 | set { 45 | scnMaterial.isDoubleSided = newValue 46 | } 47 | } 48 | 49 | /// Create a new instance 50 | public required init() { 51 | self.scnMaterial = SCNMaterial() 52 | self.scnMaterial.lightingModel = .phong 53 | 54 | super.init() 55 | } 56 | 57 | public required init(_ color: Color, lightingModel: SCNMaterial.LightingModel = .phong) { 58 | self.scnMaterial = SCNMaterial() 59 | self.scnMaterial.lightingModel = lightingModel 60 | 61 | super.init() 62 | 63 | self.setColor(.diffuse, color: color) 64 | } 65 | 66 | public init(_ scnMaterial: SCNMaterial) { 67 | self.scnMaterial = scnMaterial 68 | 69 | super.init() 70 | } 71 | 72 | public init(_ lightingModel: SCNMaterial.LightingModel) { 73 | self.scnMaterial = SCNMaterial() 74 | self.scnMaterial.lightingModel = lightingModel 75 | 76 | super.init() 77 | } 78 | 79 | //Color 80 | 81 | public func getColor(_ name: BasicColorShading) -> Color? { 82 | switch name { 83 | case .diffuse: 84 | return scnMaterial.diffuse.contents as? Color 85 | case .specular: 86 | return scnMaterial.specular.contents as? Color 87 | case .emission: 88 | return scnMaterial.emission.contents as? Color 89 | case .reflective: 90 | return scnMaterial.reflective.contents as? Color 91 | default: 92 | return nil 93 | } 94 | } 95 | 96 | public func setColor(_ name: BasicColorShading, color: Color?) { 97 | switch name { 98 | case .diffuse: 99 | scnMaterial.diffuse.contents = color 100 | case .specular: 101 | scnMaterial.specular.contents = color 102 | case .emission: 103 | scnMaterial.emission.contents = color 104 | case .reflective: 105 | scnMaterial.reflective.contents = color 106 | case .unknown: 107 | break 108 | } 109 | } 110 | 111 | //Texture 112 | 113 | public func getTexture(_ name: BasicTextureShading) -> UIImage? { 114 | switch name { 115 | case .diffuse: 116 | return scnMaterial.diffuse.contents as? UIImage 117 | case .normal: 118 | return scnMaterial.normal.contents as? UIImage 119 | case .reflective: 120 | return scnMaterial.reflective.contents as? UIImage 121 | default: 122 | return nil 123 | } 124 | } 125 | 126 | public func setTexture(_ name: BasicTextureShading, image: UIImage?) { 127 | switch name { 128 | case .diffuse: 129 | scnMaterial.diffuse.contents = color 130 | case .normal: 131 | scnMaterial.normal.contents = color 132 | case .reflective: 133 | scnMaterial.reflective.contents = color 134 | case .unknown: 135 | break 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Sources/UnityKit/Element/Mesh.swift: -------------------------------------------------------------------------------- 1 | import SceneKit 2 | 3 | public final class Mesh: Object { 4 | public var geometry: SCNGeometry 5 | 6 | @available(*, unavailable) 7 | public required init() { 8 | fatalError("init() has not been implemented") 9 | } 10 | 11 | /// Create a new instance 12 | /// 13 | /// - Parameter geometry: A geometry to be managed 14 | public required init(_ geometry: SCNGeometry) { 15 | self.geometry = geometry 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/UnityKit/Element/Scene.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SceneKit 3 | 4 | public typealias SceneLoadingOptions = [SCNSceneSource.LoadingOption: Any] 5 | 6 | open class Scene: Identifiable, Equatable { 7 | public enum Allocation { 8 | case instantiate 9 | case singleton 10 | } 11 | 12 | private var gameObjectCount: Int = 0 13 | private var ignoreUpdatesCount: Int = 0 14 | private var lastTimeStamp: TimeInterval? 15 | public let scnScene: SCNScene 16 | public let rootGameObject: GameObject 17 | internal let shadowCastingAllowed: Bool 18 | public let id: String 19 | 20 | private(set) public static var shared: Scene? 21 | 22 | public convenience init?( 23 | sceneName: String, 24 | options: SceneLoadingOptions? = nil, 25 | bundle: Bundle = Bundle.main, 26 | allocation: Allocation, 27 | shadowCastingAllowed: Bool = true 28 | ) { 29 | guard let sceneUrl = searchPathForResource(for: sceneName, extension: nil, bundle: bundle) 30 | else { return nil } 31 | 32 | self.init( 33 | sceneUrl: sceneUrl, 34 | options: options, 35 | allocation: allocation, 36 | shadowCastingAllowed: shadowCastingAllowed 37 | ) 38 | } 39 | 40 | public convenience init?( 41 | scenePath: String, 42 | options: SceneLoadingOptions? = nil, 43 | bundle: Bundle = Bundle.main, 44 | allocation: Allocation, 45 | shadowCastingAllowed: Bool = true 46 | ) { 47 | guard let sceneUrl = bundle.url(forResource: scenePath, withExtension: nil) 48 | else { return nil } 49 | 50 | self.init( 51 | sceneUrl: sceneUrl, 52 | options: options, 53 | allocation: allocation, 54 | shadowCastingAllowed: shadowCastingAllowed 55 | ) 56 | } 57 | 58 | public convenience init?( 59 | sceneUrl: URL, 60 | options: SceneLoadingOptions? = nil, 61 | allocation: Allocation, 62 | shadowCastingAllowed: Bool = true 63 | ) { 64 | guard let scene = try? SCNScene(url: sceneUrl, options: options) 65 | else { return nil } 66 | 67 | self.init( 68 | scene, 69 | allocation: allocation, 70 | shadowCastingAllowed: shadowCastingAllowed 71 | ) 72 | } 73 | 74 | public init( 75 | _ scene: SCNScene? = nil, 76 | allocation: Allocation, 77 | shadowCastingAllowed: Bool = true 78 | ) { 79 | self.shadowCastingAllowed = shadowCastingAllowed 80 | 81 | self.id = UUID().uuidString 82 | 83 | self.scnScene = scene ?? SCNScene() 84 | 85 | self.rootGameObject = GameObject(self.scnScene.rootNode) 86 | 87 | self.rootGameObject.setScene(self) 88 | 89 | if let camera = GameObject.find(.camera(.any), in: self) { 90 | camera.tag = .mainCamera 91 | camera.name = camera.tag.name 92 | } 93 | 94 | if Camera.main(in: self) == nil { 95 | let cameraObject = GameObject() 96 | 97 | let cameraComponent = cameraObject.addComponent(Camera.self) 98 | cameraObject.node.camera = cameraComponent.scnCamera 99 | 100 | cameraObject.tag = .mainCamera 101 | cameraObject.name = cameraObject.tag.name 102 | 103 | self.rootGameObject.addChild(cameraObject) 104 | 105 | cameraObject.transform.position = Vector3(0, 10, 20) 106 | } 107 | 108 | if shadowCastingAllowed == false { 109 | self.disableCastsShadow(gameObject: self.rootGameObject) 110 | } 111 | 112 | switch allocation { 113 | case .singleton: 114 | Scene.shared = self 115 | case .instantiate: 116 | Scene.shared = nil 117 | } 118 | } 119 | 120 | internal func disableCastsShadow(gameObject: GameObject) { 121 | gameObject.getChildren().forEach { 122 | $0.node.castsShadow = false 123 | $0.node.light?.castsShadow = false 124 | disableCastsShadow(gameObject: $0) 125 | } 126 | } 127 | // 128 | 129 | public func getInstanceID() -> String { 130 | id 131 | } 132 | 133 | internal func preUpdate(updateAtTime time: TimeInterval) { 134 | guard let _ = lastTimeStamp else { return } 135 | 136 | rootGameObject.preUpdate() 137 | } 138 | 139 | internal func update(updateAtTime time: TimeInterval) { 140 | guard let lastTimeStamp = lastTimeStamp else { 141 | self.lastTimeStamp = time 142 | rootGameObject.start() 143 | return 144 | } 145 | 146 | Time.deltaTime = time - lastTimeStamp 147 | rootGameObject.update() 148 | rootGameObject.internalUpdate() 149 | self.lastTimeStamp = time 150 | } 151 | 152 | internal func fixedUpdate(updateAtTime time: TimeInterval) { 153 | guard let _ = lastTimeStamp else { return } 154 | rootGameObject.fixedUpdate() 155 | } 156 | 157 | // 158 | 159 | public func clearScene() { 160 | let copy = rootGameObject.getChildren() 161 | copy.forEach { destroy($0) } 162 | } 163 | 164 | public func addGameObject(_ gameObject: GameObject) { 165 | gameObject.addToScene(self) 166 | if shadowCastingAllowed == false { 167 | gameObject.node.castsShadow = false 168 | } 169 | } 170 | 171 | public func find(_ type: GameObject.SearchType) -> GameObject? { 172 | return GameObject.find(type, in: self) 173 | } 174 | 175 | public func findGameObjects(_ type: GameObject.SearchType) -> [GameObject] { 176 | return GameObject.findGameObjects(type, in: self) 177 | } 178 | } 179 | 180 | // Debug 181 | extension Scene { 182 | public func printGameObjectsIgnoreUpdates() { 183 | gameObjectCount = 0 184 | ignoreUpdatesCount = 0 185 | printGameObjectsIgnoreUpdates(for: rootGameObject) 186 | Debug.log("ignoreUpdates count: \(ignoreUpdatesCount) / \(gameObjectCount)") 187 | } 188 | 189 | private func printGameObjectsIgnoreUpdates(for gameObject: GameObject) { 190 | gameObject.getChildren().forEach { 191 | gameObjectCount += 1 192 | if $0.ignoreUpdates { 193 | ignoreUpdatesCount += 1 194 | } 195 | Debug.log("\($0.name ?? "No name") -> ignoreUpdates: \($0.ignoreUpdates)") 196 | printGameObjectsIgnoreUpdates(for: $0) 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /Sources/UnityKit/Element/Tag.swift: -------------------------------------------------------------------------------- 1 | extension GameObject { 2 | public enum Tag: Hashable { 3 | case untagged 4 | case mainCamera 5 | case custom(String) 6 | 7 | public var name: String { 8 | switch self { 9 | case .untagged: 10 | return "Untagged" 11 | case .mainCamera: 12 | return "MainCamera" 13 | case let .custom(name): 14 | return name 15 | } 16 | } 17 | 18 | public func hash(into hasher: inout Hasher) { 19 | switch self { 20 | case .untagged: hasher.combine(0) 21 | case .mainCamera: hasher.combine(1) 22 | case .custom(let name): hasher.combine(name) 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/UnityKit/Engine/Behaviour.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | open class Behaviour: Component { 4 | public var enabled: Bool = false { 5 | didSet { 6 | guard enabled != oldValue else { return } 7 | 8 | if enabled { 9 | onEnable() 10 | } else { 11 | onDisable() 12 | } 13 | } 14 | } 15 | 16 | internal func enableChanged() { 17 | } 18 | 19 | open func onEnable() { 20 | } 21 | 22 | open func onDisable() { 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/UnityKit/Engine/Component.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal enum ComponentOrder: Int { 4 | case transform 5 | case priority 6 | case renderer 7 | case rigidbody 8 | case collider 9 | case vehicle 10 | case other 11 | case monoBehaviour 12 | } 13 | 14 | /** 15 | Base class for everything attached to GameObjects. 16 | 17 | - notes: 18 | Note that your code will never directly create a Component. Instead, you write script code (subclass from MonoBehaviour), and attach the script to a GameObject. See [MonoBehaviour](MonoBehaviour.html). 19 | */ 20 | open class Component: Object, Hashable { 21 | /** 22 | The game object this component is attached to. A component is always attached to a game object. 23 | */ 24 | internal(set) public weak var gameObject: GameObject? 25 | internal var implementsPreUpdate = true 26 | internal var implementsUpdate = true 27 | internal var implementsFixedUpdate = true 28 | internal var order: ComponentOrder { 29 | return .other 30 | } 31 | public var ignoreUpdates: Bool { 32 | return true 33 | } 34 | 35 | public var transform: Transform? { 36 | return gameObject?.transform 37 | } 38 | 39 | public func hash(into hasher: inout Hasher) { 40 | hasher.combine(ObjectIdentifier(self)) 41 | } 42 | 43 | /// Create a new instance 44 | public required init() { 45 | super.init() 46 | } 47 | 48 | open override func preUpdate() { 49 | implementsPreUpdate = false 50 | } 51 | 52 | open override func update() { 53 | implementsUpdate = false 54 | } 55 | 56 | open override func fixedUpdate() { 57 | implementsFixedUpdate = false 58 | } 59 | 60 | open override func destroy() { 61 | gameObject?.removeComponent(self) 62 | } 63 | 64 | open func onDestroy() { 65 | } 66 | 67 | public func remove() { 68 | gameObject?.removeComponent(self) 69 | } 70 | 71 | public override func removeComponent(_ component: Component) { 72 | gameObject?.removeComponent(component) 73 | } 74 | 75 | public override func removeComponentsOfType(_ type: Component.Type) { 76 | gameObject?.removeComponentsOfType(type) 77 | } 78 | 79 | open override func getComponent(_ type: T.Type) -> T? { 80 | return gameObject?.getComponent(type) 81 | } 82 | 83 | open override func getComponents(_ type: T.Type) -> [T] { 84 | return gameObject?.getComponents(type) ?? [] 85 | } 86 | 87 | @discardableResult open override func addComponent(_ type: T.Type) -> T { 88 | return (gameObject ?? GameObject()).addComponent(type) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/UnityKit/Engine/Instantiable.swift: -------------------------------------------------------------------------------- 1 | public protocol Instantiable { 2 | func instantiate(gameObject: GameObject) -> Self 3 | } 4 | -------------------------------------------------------------------------------- /Sources/UnityKit/Engine/MonoBehaviour.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public typealias CoroutineCondition = (TimeInterval) -> Bool 4 | public typealias CoroutineClosure = () -> Void 5 | public typealias Coroutine = (execute: CoroutineClosure, exitCondition: CoroutineCondition?) 6 | private typealias CoroutineInfo = (coroutine: Coroutine, thread: CoroutineThread) 7 | 8 | public enum CoroutineThread { 9 | case main 10 | case background 11 | } 12 | 13 | open class MonoBehaviour: Behaviour, Instantiable { 14 | internal override var order: ComponentOrder { 15 | return .monoBehaviour 16 | } 17 | private var queuedCoroutineInfo = [CoroutineInfo]() 18 | private var currentCoroutineInfo: CoroutineInfo? 19 | private var timePassed: TimeInterval = 0 20 | public override var ignoreUpdates: Bool { 21 | return false 22 | } 23 | 24 | open override func destroy() { 25 | currentCoroutineInfo = nil 26 | queuedCoroutineInfo.removeAll() 27 | super.destroy() 28 | } 29 | 30 | open func instantiate(gameObject: GameObject) -> Self { 31 | return type(of: self).init() 32 | } 33 | 34 | open override func onEnable() { 35 | } 36 | 37 | open override func onDisable() { 38 | } 39 | 40 | open func onCollisionEnter(_ collision: Collision) { 41 | } 42 | 43 | open func onCollisionExit(_ collision: Collision) { 44 | } 45 | 46 | open func onTriggerEnter(_ collider: Collider) { 47 | } 48 | 49 | open func onTriggerExit(_ collider: Collider) { 50 | } 51 | 52 | override func internalUpdate() { 53 | guard let coroutineInfo = currentCoroutineInfo 54 | else { return } 55 | 56 | timePassed += Time.deltaTime 57 | 58 | let exit: Bool 59 | if let exitCondition = coroutineInfo.coroutine.exitCondition { 60 | exit = exitCondition(timePassed) 61 | } else { 62 | exit = true 63 | } 64 | guard exit else { return } 65 | 66 | queuedCoroutineInfo.removeFirst() 67 | currentCoroutineInfo = nil 68 | 69 | guard let next = nextCoroutineInfo() else { return } 70 | 71 | executeCoroutine(next) 72 | } 73 | 74 | public func startCoroutine(_ coroutine: CoroutineClosure, thread: CoroutineThread = .background) { 75 | coroutine() 76 | } 77 | 78 | public func queueCoroutine(_ coroutine: Coroutine, thread: CoroutineThread = .main) { 79 | queuedCoroutineInfo.append((coroutine: coroutine, thread: thread)) 80 | if queuedCoroutineInfo.count == 1, 81 | let next = nextCoroutineInfo() { 82 | executeCoroutine(next) 83 | } 84 | } 85 | 86 | private func nextCoroutineInfo() -> CoroutineInfo? { 87 | guard let first = queuedCoroutineInfo.first 88 | else { return nil } 89 | return first 90 | } 91 | 92 | private func executeCoroutine(_ coroutineInfo: CoroutineInfo) { 93 | timePassed = 0 94 | currentCoroutineInfo = coroutineInfo 95 | switch coroutineInfo.thread { 96 | case .main: 97 | coroutineInfo.coroutine.execute() 98 | case .background: 99 | DispatchQueue.global(qos: .background).async { 100 | coroutineInfo.coroutine.execute() 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Sources/UnityKit/Engine/Object.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public func destroy(_ gameObject: GameObject) { 4 | Object.destroy(gameObject) 5 | } 6 | 7 | open class Object: Identifiable, Equatable { 8 | private static var cache = [Component: [Component]]() 9 | /** 10 | Determines the name of the receiver. 11 | */ 12 | open var name: String? 13 | 14 | private(set) internal var components = [Component]() 15 | public let id: String 16 | 17 | /// Create a new instance 18 | public required init() { 19 | self.id = UUID().uuidString 20 | } 21 | 22 | /// Returns the instance id of the object. 23 | public func getInstanceID() -> String { 24 | id 25 | } 26 | 27 | /// Removes a gameobject, component or asset. 28 | public class func destroy(_ gameObject: GameObject) { 29 | gameObject.destroy() 30 | } 31 | 32 | /// Removes a gameobject, component or asset. 33 | open func destroy() { 34 | removeAllComponents() 35 | } 36 | 37 | /// Awake is called when the script instance is being loaded. 38 | open func awake() { 39 | } 40 | 41 | /// Start is called on the frame when a script is enabled just before any of the Update methods are called the first time. 42 | open func start() { 43 | } 44 | 45 | /// preUpdate is called every frame, if the Object is enabled on willRenderScene. 46 | open func preUpdate() { 47 | } 48 | 49 | internal func internalUpdate() { 50 | } 51 | 52 | /// Update is called every frame, if the Object is enabled on didRenderScene. 53 | open func update() { 54 | } 55 | 56 | /// fixedUpdate is called every simulated physics frame, if the Object is enabled on didSimulatePhysicsAtTime. 57 | open func fixedUpdate() { 58 | } 59 | 60 | internal func movedToScene() { 61 | } 62 | 63 | internal func removeAllComponents() { 64 | components.forEach { $0.remove() } 65 | } 66 | 67 | /// Remove a component that matches the type. 68 | public func removeComponentsOfType(_ type: Component.Type) { 69 | while let index = components.firstIndex(where: { $0.self === type }) { 70 | components[index].remove() 71 | } 72 | } 73 | 74 | /// Remove a component instance. 75 | public func removeComponent(_ component: Component) { 76 | if let index = components.firstIndex(where: { $0 == component }) { 77 | components[index].onDestroy() 78 | components.remove(at: index) 79 | Object.removeCache(component) 80 | } 81 | } 82 | 83 | /// Returns the component of Type type if the game object has one attached, null if it doesn't. 84 | open func getComponent(_ type: T.Type) -> T? { 85 | return components.first { $0 is T } as? T 86 | } 87 | 88 | /// Returns all components of Type type in the GameObject. 89 | open func getComponents(_ type: T.Type) -> [T] { 90 | return components.compactMap { $0 as? T } 91 | } 92 | 93 | /// Add a component to this GameObject. 94 | @discardableResult open func addComponent(_ type: T.Type) -> T { 95 | return addComponent(external: true, type: type) 96 | } 97 | 98 | @discardableResult internal func addComponent(external: Bool = true, type: T.Type, gameObject: GameObject? = nil) -> T { 99 | if external && (T.self === Renderer.self || T.self === Transform.self || T.self === MeshFilter.self || T.self === UI.Canvas.self) { 100 | fatalError("Can't manually add Renderer, Transform, MeshFilter or Canvas") 101 | } 102 | return addComponent(T(), gameObject: gameObject) 103 | } 104 | 105 | @discardableResult internal func addComponent(_ component: T, gameObject: GameObject? = nil) -> T { 106 | components.append(component) 107 | components.sort { $0.order.rawValue <= $1.order.rawValue } 108 | component.gameObject = gameObject 109 | component.awake() 110 | if let behaviour = component as? Behaviour { 111 | behaviour.enabled = true 112 | } 113 | Object.addCache(component) 114 | return component 115 | } 116 | 117 | internal class func addCache(_ component: T) { 118 | let key = Object.cache.keys.first(where: { $0 is T }) 119 | if let key = key, var components = Object.cache[key] { 120 | components.append(component) 121 | Object.cache[key] = components 122 | } else { 123 | Object.cache[T.init()] = [component] 124 | } 125 | } 126 | 127 | internal class func removeCache(_ component: T) { 128 | let keys = Object.cache.keys.filter ({ $0 is T }) 129 | guard keys.count > 0 else { return } 130 | keys.forEach { key in 131 | var components = Object.cache[key] 132 | if let index = components?.firstIndex(where: { $0 == component }) { 133 | components?.remove(at: index) 134 | Object.cache[key] = components 135 | return 136 | } 137 | } 138 | } 139 | 140 | internal class func cache(_ type: T.Type) -> [T]? { 141 | let keys = Object.cache.keys.filter { $0 is T } 142 | guard keys.count > 0 else { return nil } 143 | let result: [T] = keys.reduce([T]()) { (prev, key) -> [T] in 144 | if let cache = Object.cache[key]?.compactMap({ $0 as? T }) { 145 | return prev + cache 146 | } 147 | return prev 148 | } 149 | return result 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /Sources/UnityKit/Extension/ActionExtension.swift: -------------------------------------------------------------------------------- 1 | import SceneKit 2 | 3 | public typealias Action = SCNAction 4 | public typealias ActionTimingFunction = SCNActionTimingFunction 5 | 6 | extension Action { 7 | public func set(ease: Ease) -> Action { 8 | self.timingFunction = ease.timingFunction() 9 | return self 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/UnityKit/Extension/ColorExtension.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public typealias Color = UIColor 4 | 5 | public extension Color { 6 | var components: (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) { 7 | guard let c = self.cgColor.components 8 | else { return (red: 1, green: 1, blue: 1, alpha: 1) } 9 | 10 | return (red: c[0], green: c[1], blue: c[2], alpha: c[3]) 11 | } 12 | 13 | convenience init(hex: String) { 14 | let r, g, b, a: CGFloat 15 | 16 | guard hex.hasPrefix("#") else { 17 | self.init(red: 1, green: 0, blue: 0, alpha: 1) 18 | return 19 | } 20 | let start = hex.index(hex.startIndex, offsetBy: 1) 21 | let hexColor = String(hex[start...]) 22 | 23 | let scanner = Scanner(string: hexColor) 24 | var hexNumber: UInt64 = 0 25 | 26 | guard scanner.scanHexInt64(&hexNumber) else { 27 | self.init(red: 1, green: 0, blue: 0, alpha: 1) 28 | return 29 | } 30 | 31 | r = CGFloat((hexNumber & 0xff000000) >> 24) / 255 32 | g = CGFloat((hexNumber & 0x00ff0000) >> 16) / 255 33 | b = CGFloat((hexNumber & 0x0000ff00) >> 8) / 255 34 | a = hexColor.count == 8 ? CGFloat(hexNumber & 0x000000ff) / 255 : 1 35 | 36 | self.init(red: r, green: g, blue: b, alpha: a) 37 | return 38 | } 39 | 40 | func toHexString() -> String { 41 | var r: CGFloat = 0 42 | var g: CGFloat = 0 43 | var b: CGFloat = 0 44 | var a: CGFloat = 0 45 | getRed(&r, green: &g, blue: &b, alpha: &a) 46 | let rgb: Int = (Int)(r*255)<<16 | (Int)(g*255)<<8 | (Int)(b*255)<<0 47 | return String(format: "#%06x", rgb) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/UnityKit/Extension/FloatingPointExtension.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | import Foundation 3 | 4 | extension CGFloat { 5 | public func toFloat() -> Float { 6 | return Float(self) 7 | } 8 | 9 | public func toDouble() -> Double { 10 | return Double(self) 11 | } 12 | } 13 | 14 | extension Float { 15 | public func toDouble() -> Double { 16 | return Double(self) 17 | } 18 | 19 | public func toCGFloat() -> CGFloat { 20 | return CGFloat(self) 21 | } 22 | } 23 | 24 | extension Double { 25 | public func toFloat() -> Float { 26 | return Float(self) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/UnityKit/Extension/GameObjectExtension.swift: -------------------------------------------------------------------------------- 1 | import SceneKit 2 | 3 | extension GameObject { 4 | internal static func convertAllChildToGameObjects(_ gameObject: GameObject) { 5 | gameObject.layer = .`default` 6 | gameObject.getChildNodes().forEach { 7 | gameObject.addChild(GameObject($0)) 8 | } 9 | } 10 | 11 | public static func instantiate(original: GameObject, addToScene: Bool = true) -> GameObject { 12 | let gameObject = original.instantiate() 13 | 14 | if addToScene { 15 | let scene = original.scene ?? Scene.shared 16 | scene?.addGameObject(gameObject) 17 | } 18 | 19 | return gameObject 20 | } 21 | 22 | public static func instantiate(original: GameObject, parent: Transform) -> GameObject { 23 | let gameObject = original.instantiate() 24 | 25 | parent.gameObject?.addChild(gameObject) 26 | 27 | return gameObject 28 | } 29 | 30 | public func setColor(_ color: Color, lightingModel: SCNMaterial.LightingModel = .phong) -> GameObject { 31 | renderer?.material = Material(color, lightingModel: lightingModel) 32 | 33 | return self 34 | } 35 | 36 | public func setOpacity(_ opacity: Float, lightingModel: SCNMaterial.LightingModel = .phong) -> GameObject { 37 | node.opacity = opacity.toCGFloat() 38 | 39 | return self 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/UnityKit/Extension/GameObjectPhysicsBody.swift: -------------------------------------------------------------------------------- 1 | import SceneKit 2 | 3 | extension GameObject { 4 | private func getAllPhysicsShapes() -> [SCNPhysicsShape]? { 5 | return getComponents(Collider.self) 6 | .compactMap { collider -> SCNPhysicsShape? in collider.physicsShape } 7 | } 8 | 9 | private func getCollisionLayer() -> GameObject.Layer? { 10 | let layers = getComponents(Collider.self) 11 | .compactMap { collider -> GameObject.Layer? in collider.collideWithLayer } 12 | 13 | let result = layers.reduce(Layer(rawValue: 0)) { prev, layer -> Layer in 14 | guard prev != layer 15 | else { return layer } 16 | 17 | let new: Layer = [prev, layer] 18 | return new 19 | } 20 | 21 | return result.rawValue == 0 ? nil : result 22 | } 23 | 24 | private func getContactLayer() -> GameObject.Layer? { 25 | let layers = getComponents(Collider.self) 26 | .compactMap { collider -> GameObject.Layer? in collider.contactWithLayer } 27 | 28 | let result = layers.reduce(Layer(rawValue: 0)) { prev, layer -> Layer in 29 | guard prev != layer 30 | else { return layer } 31 | 32 | let new: Layer = [prev, layer] 33 | return new 34 | } 35 | 36 | return result.rawValue == 0 ? nil : result 37 | } 38 | 39 | internal func updateBitMask(_ physicsBody: SCNPhysicsBody? = nil) { 40 | let body: SCNPhysicsBody? 41 | if let physicsBody = physicsBody { 42 | body = physicsBody 43 | } else if let physicsBody = node.physicsBody { 44 | body = physicsBody 45 | } else { 46 | body = nil 47 | } 48 | 49 | guard let physicsBody = body, 50 | layer != .ground 51 | else { return } 52 | 53 | getCollisionLayer().map { physicsBody.collisionBitMask = $0.rawValue } 54 | getContactLayer().map { physicsBody.contactTestBitMask = $0.rawValue } 55 | } 56 | 57 | internal func updatePhysicsBody() { 58 | var physicsShape: SCNPhysicsShape? 59 | var useGravity: Bool 60 | var bodyType: SCNPhysicsBodyType = .kinematic 61 | 62 | if let physicsShapes = getAllPhysicsShapes() { 63 | if physicsShapes.count > 1 { 64 | physicsShape = SCNPhysicsShape(shapes: physicsShapes, transforms: nil) 65 | } else if let first = physicsShapes.first { 66 | physicsShape = first 67 | } 68 | } 69 | 70 | if let rigidBody = getComponent(Rigidbody.self) { 71 | useGravity = rigidBody.useGravity 72 | bodyType = rigidBody.isStatic ? .`static` : rigidBody.isKinematic ? .kinematic : .dynamic 73 | } else { 74 | useGravity = false 75 | bodyType = .dynamic 76 | } 77 | 78 | let physicsBody = SCNPhysicsBody(type: bodyType, shape: physicsShape) 79 | 80 | physicsBody.categoryBitMask = layer.rawValue 81 | physicsBody.isAffectedByGravity = useGravity 82 | 83 | updateBitMask(physicsBody) 84 | 85 | let rigidBody = getComponent(Rigidbody.self) 86 | 87 | rigidBody?.get(property: .mass).map { physicsBody.mass = $0 } 88 | rigidBody?.get(property: .restitution).map { physicsBody.restitution = $0 } 89 | rigidBody?.get(property: .friction).map { physicsBody.friction = $0 } 90 | rigidBody?.get(property: .rollingFriction).map { physicsBody.rollingFriction = $0 } 91 | rigidBody?.get(property: .damping).map { physicsBody.damping = $0 } 92 | rigidBody?.get(property: .angularDamping).map { physicsBody.angularDamping = $0 } 93 | rigidBody?.get(property: .velocity).map { physicsBody.velocity = $0 } 94 | rigidBody?.get(property: .angularVelocity).map { physicsBody.angularVelocity = $0 } 95 | rigidBody?.get(property: .velocityFactor).map { physicsBody.velocityFactor = $0 } 96 | rigidBody?.get(property: .angularVelocityFactor).map { physicsBody.angularVelocityFactor = $0 } 97 | rigidBody?.get(property: .allowsResting).map { physicsBody.allowsResting = $0 } 98 | 99 | node.physicsBody = nil 100 | node.physicsBody = physicsBody 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Sources/UnityKit/Extension/GameObjectSCNActionable.swift: -------------------------------------------------------------------------------- 1 | import SceneKit 2 | 3 | extension GameObject { 4 | public func runAction(_ action: SCNAction) { 5 | node.runAction(action) 6 | } 7 | 8 | public func runAction(_ action: SCNAction, completionHandler block: (() -> Void)? = nil) { 9 | node.runAction(action, completionHandler: block) 10 | } 11 | 12 | public func runAction(_ action: SCNAction, forKey key: String?) { 13 | node.runAction(action, forKey: key) 14 | } 15 | 16 | public func runAction(_ action: SCNAction, forKey key: String?, completionHandler block: (() -> Void)? = nil) { 17 | node.runAction(action, forKey: key, completionHandler: block) 18 | } 19 | 20 | public var hasActions: Bool { 21 | return node.hasActions 22 | } 23 | 24 | public func action(forKey key: String) -> SCNAction? { 25 | return node.action(forKey: key) 26 | } 27 | 28 | public func removeAction(forKey key: String) { 29 | node.removeAction(forKey: key) 30 | } 31 | 32 | public func removeAllActions() { 33 | node.removeAllActions() 34 | } 35 | 36 | public var actionKeys: [String] { 37 | return node.actionKeys 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/UnityKit/Extension/GameObjectSearch.swift: -------------------------------------------------------------------------------- 1 | extension GameObject { 2 | public enum SearchType { 3 | public enum Name { 4 | case contains(String) 5 | case startWith(String) 6 | case exact(String) 7 | case any 8 | } 9 | case name(Name) 10 | case tag(Tag) 11 | case layer(Layer) 12 | case nameAndTag(Name, Tag) 13 | case camera(Name) 14 | case light(Name) 15 | } 16 | 17 | private static func compare(_ compareType: SearchType.Name, to other: String) -> Bool { 18 | switch compareType { 19 | case let .contains(name): 20 | return other.contains(name) 21 | 22 | case let .startWith(name): 23 | return other.starts(with: name) 24 | 25 | case let .exact(name): 26 | return other == name 27 | 28 | case .any: 29 | return true 30 | } 31 | } 32 | 33 | private static func compare(_ type: SearchType, gameObject: GameObject) -> Bool { 34 | switch type { 35 | case let .name(compareType): 36 | guard let name = gameObject.name, 37 | GameObject.compare(compareType, to: name) 38 | else { break } 39 | 40 | return true 41 | 42 | case let .tag(tag) where gameObject.tag == tag: 43 | return true 44 | 45 | case let .nameAndTag(compareType, tag): 46 | 47 | guard let name = gameObject.name, 48 | GameObject.compare(compareType, to: name), 49 | gameObject.tag == tag 50 | else { break } 51 | 52 | return true 53 | 54 | case let .layer(layer) where gameObject.layer == layer: 55 | return true 56 | 57 | case let .camera(compareType): 58 | 59 | guard let _ = gameObject.node.camera, 60 | let name = gameObject.name, 61 | GameObject.compare(compareType, to: name) 62 | else { break } 63 | 64 | return true 65 | 66 | case let .light(compareType): 67 | 68 | guard let _ = gameObject.node.light, 69 | let name = gameObject.name, 70 | GameObject.compare(compareType, to: name) 71 | else { break } 72 | 73 | return true 74 | 75 | default: 76 | break 77 | } 78 | 79 | return false 80 | } 81 | 82 | public static func find(_ type: SearchType, in scene: Scene? = Scene.shared) -> GameObject? { 83 | guard let scene = scene else { return nil } 84 | return GameObject.find(type, in: scene.rootGameObject) 85 | } 86 | 87 | public static func find(_ type: SearchType, in gameObject: GameObject) -> GameObject? { 88 | for child in gameObject.getChildren() { 89 | if GameObject.compare(type, gameObject: child) { 90 | return child 91 | } 92 | } 93 | for child in gameObject.getChildren() { 94 | if let found = GameObject.find(type, in: child) { 95 | return found 96 | } 97 | } 98 | return nil 99 | } 100 | 101 | public static func findGameObjects(_ type: SearchType, in scene: Scene? = Scene.shared) -> [GameObject] { 102 | guard let scene = scene else { return [] } 103 | return GameObject.findGameObjects(type, in: scene.rootGameObject) 104 | } 105 | 106 | public static func findGameObjects(_ type: SearchType, in gameObject: GameObject) -> [GameObject] { 107 | return gameObject.getChildren() 108 | .map { (child) -> [GameObject] in 109 | if GameObject.compare(type, gameObject: child) { 110 | return [child] + GameObject.findGameObjects(type, in: child) 111 | } 112 | return GameObject.findGameObjects(type, in: child) 113 | } 114 | .reduce([], { (current, next) -> [GameObject] in 115 | current + next 116 | }) 117 | } 118 | 119 | public static func getComponents(_ type: T.Type, in scene: Scene? = Scene.shared) -> [T] { 120 | guard let scene = scene else { return [] } 121 | return GameObject.getComponents(type, in: scene.rootGameObject) 122 | } 123 | 124 | public static func getComponents(_ type: T.Type, in gameObject: GameObject) -> [T] { 125 | return gameObject.getChildren() 126 | .map { (child) -> [T] in 127 | return child.getComponents(type) + GameObject.getComponents(type, in: child) 128 | }.reduce([], { (current, next) -> [T] in 129 | current + next 130 | }) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Sources/UnityKit/Extension/GeometryExtension.swift: -------------------------------------------------------------------------------- 1 | import SceneKit 2 | 3 | public enum PrimitiveType { 4 | case sphere( 5 | radius: Float, 6 | name: String? = nil 7 | ) 8 | case capsule( 9 | capRadius: Float, 10 | height: Float, 11 | name: String? = nil 12 | ) 13 | case cylinder( 14 | radius: Float, 15 | height: Float, 16 | name: String? = nil 17 | ) 18 | case cube( 19 | width: Float, 20 | height: Float, 21 | length: Float, 22 | chamferRadius: Float, 23 | name: String? = nil 24 | ) 25 | case plane( 26 | width: Float, 27 | height: Float, 28 | name: String? = nil 29 | ) 30 | case floor( 31 | width: Float, 32 | length: Float, 33 | name: String? = nil 34 | ) 35 | 36 | var name: String { 37 | switch self { 38 | case let .sphere(_, n): 39 | return n ?? "Sphere" 40 | 41 | case let .capsule(_, _, n): 42 | return n ?? "Capsule" 43 | 44 | case let .cylinder(_, _, n): 45 | return n ?? "Cylinder" 46 | 47 | case let .cube(_, _, _, _, n): 48 | return n ?? "Cube" 49 | 50 | case let .plane(_, _, n): 51 | return n ?? "Plane" 52 | 53 | case let .floor(_, _, n): 54 | return n ?? "Floor" 55 | } 56 | } 57 | } 58 | 59 | extension GameObject { 60 | public static func createPrimitive(_ type: PrimitiveType) -> GameObject { 61 | let geometry = SCNGeometry.createPrimitive(type) 62 | geometry.firstMaterial?.lightingModel = .phong 63 | 64 | let gameObject = GameObject(SCNNode(geometry: geometry)) 65 | 66 | gameObject.name = type.name 67 | 68 | return gameObject 69 | } 70 | } 71 | 72 | extension SCNGeometry { 73 | internal static func createPrimitive(_ type: PrimitiveType) -> SCNGeometry { 74 | let geometry: SCNGeometry 75 | 76 | switch type { 77 | case let .sphere(rad, _): 78 | geometry = SCNSphere(radius: rad.toCGFloat()) 79 | 80 | case let .capsule(rad, y, _): 81 | geometry = SCNCapsule(capRadius: rad.toCGFloat(), height: y.toCGFloat()) 82 | 83 | case let .cylinder(rad, y, _): 84 | geometry = SCNCylinder(radius: rad.toCGFloat(), height: y.toCGFloat()) 85 | 86 | case let .cube(x, y, z, rad, _): 87 | geometry = SCNBox(width: x.toCGFloat(), height: y.toCGFloat(), length: z.toCGFloat(), chamferRadius: rad.toCGFloat()) 88 | 89 | case let .plane(x, y, _): 90 | geometry = SCNPlane(width: x.toCGFloat(), height: y.toCGFloat()) 91 | 92 | case let .floor(x, z, _): 93 | let floor = SCNFloor() 94 | floor.width = x.toCGFloat() 95 | floor.length = z.toCGFloat() 96 | geometry = floor 97 | } 98 | 99 | return geometry 100 | } 101 | 102 | internal static func vertices(source: SCNGeometrySource) -> [Vector3] { 103 | guard let value = source.data.withUnsafeBytes({ 104 | $0.baseAddress?.assumingMemoryBound(to: UInt8.self) 105 | }) else { return [] } 106 | 107 | let rawPointer = UnsafeRawPointer(value) 108 | let strides = stride(from: source.dataOffset, 109 | to: source.dataOffset + source.dataStride * source.vectorCount, 110 | by: source.dataStride) 111 | 112 | return strides.map { byteOffset -> Vector3 in 113 | Vector3(rawPointer.load(fromByteOffset: byteOffset, as: SIMD3.self)) 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Sources/UnityKit/Extension/MathExtension.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | import Foundation 3 | 4 | public typealias Degree = Float 5 | 6 | extension Degree { 7 | public static func clamp(_ angle: Degree) -> Degree { 8 | var angle = angle 9 | while angle >= 360 { 10 | angle -= 360 11 | } 12 | while angle < 0 { 13 | angle += 360 14 | } 15 | return angle 16 | } 17 | 18 | public func clampDegree() -> Degree { 19 | return .clamp(self) 20 | } 21 | 22 | public static func differenceAngle(x: Degree, y: Degree) -> Degree { 23 | var arg = fmod(y - x, 360) 24 | if arg < 0 { 25 | arg = arg + 360 26 | } 27 | if arg > 180 { 28 | arg = arg - 360 29 | } 30 | return -arg 31 | } 32 | 33 | public func differenceAngle(_ other: Degree) -> Degree { 34 | return Degree.differenceAngle(x: self, y: other) 35 | } 36 | } 37 | 38 | extension FloatingPoint { 39 | public static func degToRad() -> Self { return .pi / 180 } 40 | public static func radToDeg() -> Self { return 180 / .pi } 41 | 42 | public var degreesToRadians: Self { return self * Self.degToRad() } 43 | public var radiansToDegrees: Self { return self * Self.radToDeg() } 44 | 45 | public mutating func clamp(_ range: ClosedRange) { 46 | self = Self.clamp(self, range: range) 47 | } 48 | 49 | public static func clamp(_ value: Self, range: ClosedRange) -> Self { 50 | return max(min(value, range.upperBound), range.lowerBound) 51 | } 52 | 53 | public func clamp01() -> Self { 54 | return Self.clamp(self, range: 0...1) 55 | } 56 | } 57 | 58 | extension Double { 59 | public static func lerp(from v0: Double, to v1: Double, time t: TimeInterval) -> Double { 60 | return (1 - t) * v0 + t * v1 61 | } 62 | } 63 | 64 | extension Float { 65 | public static func lerp(from v0: Float, to v1: Float, time t: TimeInterval) -> Float { 66 | return Float(1 - t) * v0 + Float(t) * v1 67 | } 68 | } 69 | 70 | extension CGFloat { 71 | public static func lerp(from v0: CGFloat, to v1: CGFloat, time t: TimeInterval) -> CGFloat { 72 | return CGFloat(1 - t) * v0 + CGFloat(t) * v1 73 | } 74 | } 75 | 76 | extension Color { 77 | public static func lerp(from v0: Color, to v1: Color, time t: TimeInterval) -> Color { 78 | let comp0 = v0.components 79 | let comp1 = v1.components 80 | return Color( 81 | red: .lerp(from: comp0.red, to: comp1.red, time: t), 82 | green: .lerp(from: comp0.green, to: comp1.green, time: t), 83 | blue: .lerp(from: comp0.blue, to: comp1.blue, time: t), 84 | alpha: .lerp(from: comp0.alpha, to: comp1.alpha, time: t) 85 | ) 86 | } 87 | } 88 | 89 | public func normalize(_ value: Float, in range: ClosedRange) -> Float { 90 | return (value - range.lowerBound) / (range.upperBound - range.lowerBound) 91 | } 92 | 93 | public func interpolate(from start: Float, to end: Float, alpha: Float) -> Float { 94 | return (1 - alpha) * start + alpha * end 95 | } 96 | -------------------------------------------------------------------------------- /Sources/UnityKit/Extension/ObjectSearchExtension.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension Object { 4 | public static func findObjectOfType(_ type: T.Type, in scene: Scene? = Scene.shared) -> T? { 5 | guard let scene = scene 6 | else { return nil } 7 | 8 | if let component = scene.rootGameObject.getComponent(type) { return component } 9 | if let component = scene.rootGameObject.getComponentInChild(type) { return component } 10 | return nil 11 | } 12 | 13 | public static func findObjectsOfType(_ type: T.Type, in scene: Scene? = Scene.shared) -> [T] { 14 | guard let scene = scene 15 | else { return [] } 16 | if let cache = Object.cache(type) { 17 | return cache 18 | } 19 | return scene.rootGameObject.getComponents(type) + scene.rootGameObject.getComponentsInChild(type) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/UnityKit/Extension/SCNNodeExtension.swift: -------------------------------------------------------------------------------- 1 | import SceneKit 2 | 3 | extension SCNNode { 4 | func deepClone(_ node: SCNNode? = nil) -> SCNNode { 5 | let refNode = (node ?? self) 6 | let clone = refNode.clone() 7 | if let geometry = clone.geometry?.copy() as? SCNGeometry { 8 | clone.geometry = geometry 9 | } 10 | let copy = clone.childNodes 11 | copy.forEach { 12 | $0.name = clone.name 13 | clone.addChildNode(deepClone($0)) 14 | $0.removeFromParentNode() 15 | } 16 | return clone 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/UnityKit/Extension/Size.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public struct Size { 4 | public let width: Float 5 | public let height: Float 6 | 7 | public static let zero = Size(0, 0) 8 | 9 | public init(_ width: Float, _ height: Float) { 10 | self.width = width 11 | self.height = height 12 | } 13 | 14 | public init(width: Float, height: Float) { 15 | self.width = width 16 | self.height = height 17 | } 18 | } 19 | 20 | extension Size { 21 | public func toCGSize() -> CGSize { 22 | return CGSize(width: width.toCGFloat(), height: height.toCGFloat()) 23 | } 24 | } 25 | 26 | extension CGSize { 27 | public func toSize() -> Size { 28 | return Size(width: width.toFloat(), height: height.toFloat()) 29 | } 30 | } 31 | 32 | extension Size: Equatable { 33 | public static func == (left: Size, right: Size) -> Bool { 34 | return left.width == right.width && left.height == right.height 35 | } 36 | } 37 | 38 | /// Multiplies the width and height fields of a Size with a scalar value 39 | /// 40 | /// - Parameters: 41 | /// - size: A size 42 | /// - scalar: A float 43 | /// - Returns: the result as a new Size. 44 | public func * (size: Size, scalar: Float) -> Size { 45 | return Size(size.width * scalar, size.height * scalar) 46 | } 47 | 48 | /// Multiplies the width and height fields of a Size with a scalar value 49 | /// 50 | /// - Parameters: 51 | /// - scalar: A float 52 | /// - size: A size 53 | /// - Returns: the result as a new Size. 54 | public func * (scalar: Float, size: Size) -> Size { 55 | return Size(size.width * scalar, size.height * scalar) 56 | } 57 | -------------------------------------------------------------------------------- /Sources/UnityKit/Extension/UIImageExtension.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIImage { 4 | func resize(to size: CGSize) -> UIImage { 5 | UIGraphicsBeginImageContextWithOptions(size, false, 0) 6 | draw(in: CGRect(origin: .zero, size: size)) 7 | 8 | guard let result = UIGraphicsGetImageFromCurrentImageContext() 9 | else { return self } 10 | 11 | return result 12 | } 13 | 14 | func replaceColor(with color: UIColor) -> UIImage { 15 | let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) 16 | UIGraphicsBeginImageContextWithOptions(rect.size, false, scale) 17 | 18 | guard let c = UIGraphicsGetCurrentContext() 19 | else { return self } 20 | 21 | draw(in: rect) 22 | 23 | c.setFillColor(color.cgColor) 24 | c.setBlendMode(.sourceIn) 25 | c.fill(rect) 26 | 27 | guard let result = UIGraphicsGetImageFromCurrentImageContext() 28 | else { return self } 29 | 30 | return result 31 | } 32 | 33 | func fill( 34 | fromAngle: Degree, 35 | toAngle: Degree, 36 | fillOrigin: UI.Image.FillOrigin, 37 | clockwise: Bool 38 | ) -> UIImage { 39 | UIGraphicsBeginImageContextWithOptions(self.size, false, scale) 40 | 41 | guard let ctx = UIGraphicsGetCurrentContext() 42 | else { return self } 43 | 44 | let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) 45 | let center = CGPoint(x: size.width / 2, y: size.height / 2) 46 | let nextPoint: CGPoint 47 | 48 | var from = fromAngle 49 | var to = toAngle 50 | 51 | switch fillOrigin { 52 | case .right: 53 | nextPoint = CGPoint(x: size.width, y: center.y) 54 | case .bottom: 55 | nextPoint = CGPoint(x: center.x, y: 0) 56 | from += 90 57 | to += 90 58 | case .left: 59 | nextPoint = CGPoint(x: 0, y: center.y) 60 | from += 180 61 | to += 180 62 | case .top: 63 | nextPoint = CGPoint(x: center.x, y: size.height) 64 | from += 270 65 | to += 270 66 | } 67 | 68 | let path = CGMutablePath() 69 | path.move(to: center) 70 | path.addLine(to: nextPoint) 71 | path.addArc( 72 | center: center, radius: size.height / 2, 73 | startAngle: from.toCGFloat().degreesToRadians, 74 | endAngle: to.toCGFloat().degreesToRadians, 75 | clockwise: clockwise 76 | ) 77 | path.addLine(to: center) 78 | 79 | ctx.addPath(path) 80 | ctx.closePath() 81 | ctx.clip() 82 | 83 | draw(in: rect) 84 | 85 | guard let result = UIGraphicsGetImageFromCurrentImageContext() 86 | else { return self } 87 | 88 | return result 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/UnityKit/Extension/Vector2.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public struct Vector2 { 4 | public let x: Float 5 | public let y: Float 6 | 7 | public init(_ x: Float, _ y: Float) { 8 | self.x = x 9 | self.y = y 10 | } 11 | } 12 | 13 | extension Vector2 { 14 | public func toCGPoint() -> CGPoint { 15 | return CGPoint(x: self.x.toCGFloat(), y: self.y.toCGFloat()) 16 | } 17 | } 18 | 19 | extension CGSize { 20 | public func toVector2() -> Vector2 { 21 | return Vector2(self.width.toFloat(), self.height.toFloat()) 22 | } 23 | } 24 | 25 | extension CGPoint { 26 | public func toVector2() -> Vector2 { 27 | return Vector2(self.x.toFloat(), self.y.toFloat()) 28 | } 29 | } 30 | 31 | extension Vector2: Equatable { 32 | public static func == (left: Vector2, right: Vector2) -> Bool { 33 | return left.x == right.x && left.y == right.y 34 | } 35 | } 36 | 37 | extension Vector2 { 38 | public static var zero: Vector2 { 39 | return Vector2(0, 0) 40 | } 41 | 42 | public static var one: Vector2 { 43 | return Vector2(1, 1) 44 | } 45 | 46 | /** 47 | * Returns the length (magnitude) of the vector described by the Vector2 48 | */ 49 | public func length() -> Float { 50 | return Vector2.length(self) 51 | } 52 | 53 | public static func length(_ vector: Vector2) -> Float { 54 | return sqrtf(vector.x * vector.x + vector.y * vector.y) 55 | } 56 | 57 | /** 58 | * Returns the distance between a and b. 59 | */ 60 | public func distance(_ vector: Vector2) -> Float { 61 | return Vector2.distance(self, vector) 62 | } 63 | 64 | public static func distance(_ a: Vector2, _ b: Vector2) -> Float { 65 | return (b - a).length() 66 | } 67 | } 68 | 69 | /** 70 | * Adds two Vector2 vectors and returns the result as a new Vector2. 71 | */ 72 | public func + (left: Vector2, right: Vector2) -> Vector2 { 73 | return Vector2(left.x + right.x, left.y + right.y) 74 | } 75 | 76 | /** 77 | * Increments a Vector2 with the value of another. 78 | */ 79 | public func += ( left: inout Vector2, right: Vector2) { 80 | left = left + right 81 | } 82 | 83 | /** 84 | * Subtracts two Vector2 vectors and returns the result as a new Vector2. 85 | */ 86 | public func - (left: Vector2, right: Vector2) -> Vector2 { 87 | return Vector2(left.x - right.x, left.y - right.y) 88 | } 89 | 90 | /** 91 | * Decrements a Vector2 with the value of another. 92 | */ 93 | public func -= ( left: inout Vector2, right: Vector2) { 94 | left = left - right 95 | } 96 | 97 | /** 98 | * Multiplies two Vector2 vectors and returns the result as a new Vector2. 99 | */ 100 | public func * (left: Vector2, right: Vector2) -> Vector2 { 101 | return Vector2(left.x * right.x, left.y * right.y) 102 | } 103 | 104 | /** 105 | * Multiplies a Vector2 with another. 106 | */ 107 | public func *= (left: inout Vector2, right: Vector2) { 108 | left = left * right 109 | } 110 | 111 | /** 112 | * Multiplies the x, y and z fields of a Vector2 with the same scalar value and 113 | * returns the result as a new Vector2. 114 | */ 115 | public func * (vector: Vector2, scalar: Float) -> Vector2 { 116 | return Vector2(vector.x * scalar, vector.y * scalar) 117 | } 118 | public func * (scalar: Float, vector: Vector2) -> Vector2 { 119 | return Vector2(vector.x * scalar, vector.y * scalar) 120 | } 121 | 122 | /** 123 | * Multiplies the x and y fields of a Vector2 with the same scalar value. 124 | */ 125 | public func *= (vector: inout Vector2, scalar: Float) { 126 | vector = vector * scalar 127 | } 128 | 129 | /** 130 | * Divides two Vector2 vectors abd returns the result as a new Vector2 131 | */ 132 | public func / (left: Vector2, right: Vector2) -> Vector2 { 133 | return Vector2(left.x / right.x, left.y / right.y) 134 | } 135 | 136 | /** 137 | * Divides a Vector2 by another. 138 | */ 139 | public func /= ( left: inout Vector2, right: Vector2) { 140 | left = left / right 141 | } 142 | 143 | /** 144 | * Divides the x, y and z fields of a Vector2 by the same scalar value and 145 | * returns the result as a new Vector2. 146 | */ 147 | public func / (vector: Vector2, scalar: Float) -> Vector2 { 148 | return Vector2(vector.x / scalar, vector.y / scalar) 149 | } 150 | 151 | /** 152 | * Divides the x, y and z of a Vector2 by the same scalar value. 153 | */ 154 | public func /= ( vector: inout Vector2, scalar: Float) { 155 | vector = vector / scalar 156 | } 157 | -------------------------------------------------------------------------------- /Sources/UnityKit/Extension/Vector3.swift: -------------------------------------------------------------------------------- 1 | import SceneKit 2 | import AVKit 3 | 4 | public typealias Vector3 = SCNVector3 5 | 6 | public struct Vector3Nullable { 7 | public let x: Float? 8 | public let y: Float? 9 | public let z: Float? 10 | 11 | public init(_ x: Float?, _ y: Float?, _ z: Float?) { 12 | self.x = x 13 | self.y = y 14 | self.z = z 15 | } 16 | } 17 | 18 | extension Vector3 { 19 | public static var zero: Vector3 { 20 | return SCNVector3Zero 21 | } 22 | 23 | public static var one: Vector3 { 24 | return Vector3(1, 1, 1) 25 | } 26 | 27 | public static var back: Vector3 { 28 | return Vector3(0, 0, -1) 29 | } 30 | 31 | public static var down: Vector3 { 32 | return Vector3(0, -1, 0) 33 | } 34 | 35 | public static var forward: Vector3 { 36 | return Vector3(0, 0, 1) 37 | } 38 | 39 | public static var left: Vector3 { 40 | return Vector3(-1, 0, 0) 41 | } 42 | 43 | public static var right: Vector3 { 44 | return Vector3(1, 0, 0) 45 | } 46 | 47 | public static var up: Vector3 { 48 | return Vector3(0, 1, 0) 49 | } 50 | 51 | /** 52 | * Negates the vector described by Vector3 and returns 53 | * the result as a new Vector3. 54 | */ 55 | public func negated() -> Vector3 { 56 | return self * -1 57 | } 58 | 59 | /** 60 | * Negates the vector described by Vector3 61 | */ 62 | mutating public func negate() { 63 | self = negated() 64 | } 65 | 66 | /** 67 | * Returns the length (magnitude) of the vector described by the Vector3 68 | */ 69 | public func length() -> Float { 70 | return Vector3.length(self) 71 | } 72 | 73 | public static func length(_ vector: Vector3) -> Float { 74 | return sqrtf(vector.x * vector.x + vector.y * vector.y + vector.z * vector.z) 75 | } 76 | 77 | public func magnitude() -> Float { 78 | return Vector3.length(self) 79 | } 80 | 81 | /** 82 | * Normalizes the vector described by the Vector3 to length 1.0 and returns 83 | * the result as a new Vector3. 84 | */ 85 | public func normalized() -> Vector3 { 86 | return Vector3.normalized(self) 87 | } 88 | 89 | public static func normalized(_ vector: Vector3) -> Vector3 { 90 | let lenght = vector.length() 91 | guard lenght != 0 92 | else { return .zero } 93 | return vector / lenght 94 | } 95 | 96 | mutating public func normalize() { 97 | self = normalized() 98 | } 99 | 100 | /** 101 | * Returns the distance between a and b. 102 | */ 103 | public func distance(_ vector: Vector3) -> Float { 104 | return Vector3.distance(self, vector) 105 | } 106 | 107 | public static func distance(_ a: Vector3, _ b: Vector3) -> Float { 108 | return (b - a).length() 109 | } 110 | 111 | /** 112 | * Calculates the dot product between two Vector3. 113 | */ 114 | public func dot(_ vector: Vector3) -> Float { 115 | return Vector3.dot(self, vector) 116 | } 117 | 118 | public static func dot(_ a: Vector3, _ b: Vector3) -> Float { 119 | return (a.x * b.x) + (a.y * b.y) + (a.z * b.z) 120 | } 121 | 122 | /** 123 | * Projects a vector onto another vector. 124 | */ 125 | public func project(_ normal: Vector3) -> Vector3 { 126 | return Vector3.project(self, normal) 127 | } 128 | 129 | public static func project(_ vector: Vector3, _ onNormal: Vector3) -> Vector3 { 130 | return Vector3.scale(Vector3.dot(vector, onNormal) / Vector3.dot(onNormal, onNormal), onNormal) 131 | } 132 | 133 | /** 134 | * Cross Product of two vectors. 135 | */ 136 | public func cross(_ vector: Vector3) -> Vector3 { 137 | return Vector3.cross(self, vector) 138 | } 139 | 140 | public static func cross(_ a: Vector3, _ b: Vector3) -> Vector3 { 141 | return Vector3(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x) 142 | } 143 | 144 | public func degreesToRadians() -> Vector3 { 145 | return Vector3(x.degreesToRadians, y.degreesToRadians, z.degreesToRadians) 146 | } 147 | 148 | public func radiansToDegrees() -> Vector3 { 149 | return Vector3(x.radiansToDegrees, y.radiansToDegrees, z.radiansToDegrees) 150 | } 151 | 152 | public func angleClamp() -> Vector3 { 153 | return Vector3(x.clampDegree(), y.clampDegree(), z.clampDegree()) 154 | } 155 | 156 | /** 157 | * Multiplies two vectors component-wise. 158 | */ 159 | public static func scale(_ factor: Float, _ vector: Vector3) -> Vector3 { 160 | return factor * vector 161 | } 162 | 163 | /** 164 | * Calculates the SCNVector from lerping between two Vector3 vectors 165 | */ 166 | public static func lerp(from v0: Vector3, to v1: Vector3, time t: TimeInterval) -> Vector3 { 167 | return Float(1 - t) * v0 + Float(t) * v1 168 | } 169 | 170 | public func toQuaternion() -> Quaternion { 171 | var angle: Float = 0 172 | 173 | angle = x * 0.5 174 | let sr = sin(angle) 175 | let cr = cos(angle) 176 | 177 | angle = y * 0.5 178 | let sp = sin(angle) 179 | let cp = cos(angle) 180 | 181 | angle = z * 0.5 182 | let sy = sin(angle) 183 | let cy = cos(angle) 184 | 185 | let cpcy = cp * cy 186 | let spcy = sp * cy 187 | let cpsy = cp * sy 188 | let spsy = sp * sy 189 | 190 | return Quaternion((sr * cpcy - cr * spsy), (cr * spcy + sr * cpsy), (cr * cpsy - sr * spcy), (cr * cpcy + sr * spsy)).normalized() 191 | } 192 | 193 | internal func toAVAudio3DPoint() -> AVAudio3DPoint { 194 | return AVAudio3DPoint(x: x, y: y, z: z) 195 | } 196 | } 197 | 198 | extension Vector3: Equatable { 199 | public static func == (left: Vector3, right: Vector3) -> Bool { 200 | return left.x == right.x && left.y == right.y && left.z == right.z 201 | } 202 | 203 | public static func != (left: Vector3, right: Vector3) -> Bool { 204 | return !(left == right) 205 | } 206 | } 207 | 208 | /** 209 | * Adds two Vector3 vectors and returns the result as a new Vector3. 210 | */ 211 | public func + (left: Vector3, right: Vector3) -> Vector3 { 212 | return Vector3(left.x + right.x, left.y + right.y, left.z + right.z) 213 | } 214 | 215 | /** 216 | * Increments a Vector3 with the value of another. 217 | */ 218 | public func += ( left: inout Vector3, right: Vector3) { 219 | left = left + right 220 | } 221 | 222 | /** 223 | * Subtracts two Vector3 vectors and returns the result as a new Vector3. 224 | */ 225 | public func - (left: Vector3, right: Vector3) -> Vector3 { 226 | return Vector3(left.x - right.x, left.y - right.y, left.z - right.z) 227 | } 228 | 229 | /** 230 | * Decrements a Vector3 with the value of another. 231 | */ 232 | public func -= ( left: inout Vector3, right: Vector3) { 233 | left = left - right 234 | } 235 | 236 | public func + (left: Vector3, right: Float) -> Vector3 { 237 | return Vector3(left.x + right, left.y + right, left.z + right) 238 | } 239 | 240 | public func += (left: inout Vector3, right: Float) { 241 | left = left + right 242 | } 243 | 244 | public func - (left: Vector3, right: Float) -> Vector3 { 245 | return Vector3(left.x - right, left.y - right, left.z - right) 246 | } 247 | 248 | public func -= (left: inout Vector3, right: Float) { 249 | left = left - right 250 | } 251 | 252 | /** 253 | * Multiplies two Vector3 vectors and returns the result as a new Vector3. 254 | */ 255 | public func * (left: Vector3, right: Vector3) -> Vector3 { 256 | return Vector3(left.x * right.x, left.y * right.y, left.z * right.z) 257 | } 258 | 259 | /** 260 | * Multiplies a Vector3 with another. 261 | */ 262 | public func *= (left: inout Vector3, right: Vector3) { 263 | left = left * right 264 | } 265 | 266 | /** 267 | * Multiplies the x, y and z fields of a Vector3 with the same scalar value and 268 | * returns the result as a new Vector3. 269 | */ 270 | public func * (vector: Vector3, scalar: Float) -> Vector3 { 271 | return Vector3(vector.x * scalar, vector.y * scalar, vector.z * scalar) 272 | } 273 | 274 | public func * (scalar: Float, vector: Vector3) -> Vector3 { 275 | return Vector3(vector.x * scalar, vector.y * scalar, vector.z * scalar) 276 | } 277 | 278 | /** 279 | * Multiplies the x and y fields of a Vector3 with the same scalar value. 280 | */ 281 | public func *= (vector: inout Vector3, scalar: Float) { 282 | vector = vector * scalar 283 | } 284 | 285 | /** 286 | * Divides two Vector3 vectors abd returns the result as a new Vector3 287 | */ 288 | public func / (left: Vector3, right: Vector3) -> Vector3 { 289 | return Vector3(left.x / right.x, left.y / right.y, left.z / right.z) 290 | } 291 | 292 | /** 293 | * Divides a Vector3 by another. 294 | */ 295 | public func /= ( left: inout Vector3, right: Vector3) { 296 | left = left / right 297 | } 298 | 299 | /** 300 | * Divides the x, y and z fields of a Vector3 by the same scalar value and 301 | * returns the result as a new Vector3. 302 | */ 303 | public func / (vector: Vector3, scalar: Float) -> Vector3 { 304 | return Vector3(vector.x / scalar, vector.y / scalar, vector.z / scalar) 305 | } 306 | 307 | /** 308 | * Divides the x, y and z of a Vector3 by the same scalar value. 309 | */ 310 | public func /= ( vector: inout Vector3, scalar: Float) { 311 | vector = vector / scalar 312 | } 313 | -------------------------------------------------------------------------------- /Sources/UnityKit/Extension/Vector4Quaternion.swift: -------------------------------------------------------------------------------- 1 | import SceneKit 2 | import Foundation 3 | 4 | public typealias Vector4 = SCNVector4 5 | public typealias Quaternion = SCNQuaternion 6 | 7 | extension Vector4 { 8 | public static var zero: Vector4 { 9 | SCNVector4Zero 10 | } 11 | } 12 | 13 | extension Quaternion { 14 | public static func euler(_ x: Degree, _ y: Degree, _ z: Degree) -> Quaternion { 15 | Vector3(x.degreesToRadians, y.degreesToRadians, z.degreesToRadians).toQuaternion() 16 | } 17 | 18 | public func normalized() -> Quaternion { 19 | let n = x * x + y * y + z * z + w * w 20 | 21 | if n == 1 { 22 | return self 23 | } 24 | 25 | return self * (1.0 / sqrt(n)) 26 | } 27 | 28 | public func toEuler() -> Vector3 { 29 | let d = 2.0 * (y * w - x * z) 30 | 31 | switch d { 32 | case 1.0: 33 | return Vector3(0, .pi / 2.0, -2.0 * atan2(x, w)) 34 | 35 | case -1.0: 36 | return Vector3(0, .pi / -2.0, 2.0 * atan2(x, w)) 37 | 38 | default: 39 | let sqw = w * w 40 | let sqx = x * x 41 | let sqy = y * y 42 | let sqz = z * z 43 | let result = Vector3(atan2(2.0 * (y * z + x * w), (-sqx - sqy + sqz + sqw)), 44 | asin(min(max(-1, d), 1)), 45 | atan2(2.0 * (x * y + z * w), (sqx - sqy - sqz + sqw))) 46 | return result 47 | } 48 | } 49 | 50 | public static func difference(from: Vector3, to: Vector3) -> Quaternion { 51 | let v0 = from.normalized() 52 | let v1 = to.normalized() 53 | 54 | let d = v0.dot(v1) 55 | 56 | switch d { 57 | case let d where d >= 1.0: 58 | return Quaternion(0, 0, 0, 1) 59 | 60 | case let d where d <= -1.0: 61 | var axis = Vector3(1, 0, 0).cross(v0) 62 | if axis.length() == 0 { 63 | axis = Vector3(0, 1, 0).cross(v0) 64 | } 65 | return Quaternion(axis.x, axis.y, axis.z, 0).normalized() 66 | 67 | default: 68 | let s = sqrtf((1 + d) * 2) 69 | let invs = 1 / s 70 | let c = v0.cross(v1) * invs 71 | return Quaternion(c.x, c.y, c.z, s * 0.5).normalized() 72 | } 73 | } 74 | } 75 | 76 | public func * (quaternion: Quaternion, scalar: Float) -> Quaternion { 77 | Quaternion(quaternion.x * scalar, quaternion.y * scalar, quaternion.z * scalar, quaternion.w * scalar) 78 | } 79 | -------------------------------------------------------------------------------- /Sources/UnityKit/Protocol/Getteable.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | 3 | public protocol Getteable { 4 | } 5 | 6 | extension Bool: Getteable { 7 | } 8 | 9 | extension Float: Getteable { 10 | } 11 | 12 | extension CGFloat: Getteable { 13 | } 14 | 15 | extension Vector3: Getteable { 16 | } 17 | 18 | extension Vector4: Getteable { 19 | } 20 | -------------------------------------------------------------------------------- /Sources/UnityKit/Protocol/Identifiable.swift: -------------------------------------------------------------------------------- 1 | 2 | public func == (lhs: Object, rhs: Object) -> Bool { 3 | return lhs.id == rhs.id 4 | } 5 | 6 | public func == (lhs: Scene, rhs: Scene) -> Bool { 7 | return lhs.id == rhs.id 8 | } 9 | -------------------------------------------------------------------------------- /Sources/UnityKit/Static/Debug.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public final class Debug { 4 | public struct LogStyle: OptionSet { 5 | public let rawValue: Int 6 | 7 | public init(rawValue: Int) { 8 | self.rawValue = rawValue 9 | } 10 | 11 | public static let debug = LogStyle(rawValue: 1 << 0) 12 | public static let info = LogStyle(rawValue: 1 << 1) 13 | public static let warning = LogStyle(rawValue: 1 << 2) 14 | public static let error = LogStyle(rawValue: 1 << 3) 15 | public static let none = LogStyle(rawValue: 1 << 4) 16 | 17 | public static var all: LogStyle = [.debug, .info, .warning, .error] 18 | 19 | public var prefix: String { 20 | if self.contains(.debug) { 21 | return "💬" + " DEBUG: " 22 | } else if self.contains(.info) { 23 | return "ℹ️" + " INFO: " 24 | } else if self.contains(.warning) { 25 | return "⚠️" + " WARNING: " 26 | } else if self.contains(.error) { 27 | return "‼️" + " ERROR: " 28 | } 29 | return "" 30 | } 31 | } 32 | 33 | private static var dateFormat = "HH:mm:ss.SSS" 34 | fileprivate static var dateFormatter: DateFormatter { 35 | let formatter = DateFormatter() 36 | formatter.dateFormat = dateFormat 37 | formatter.locale = Locale.current 38 | formatter.timeZone = TimeZone.current 39 | return formatter 40 | } 41 | 42 | private static var enabled: LogStyle = .all 43 | 44 | public static func set(enable: LogStyle) { 45 | self.enabled = enable.contains(.none) ? .none : enable 46 | } 47 | 48 | public static func warning( 49 | _ message: String, 50 | displayTime: Bool = false, 51 | _ filepath: String = #file, 52 | _ function: String = #function, 53 | _ line: Int = #line, 54 | _ column: Int = #column 55 | ) { 56 | log(message, style: .warning, displayTime: displayTime, filepath, function, line, column) 57 | } 58 | 59 | public static func error( 60 | _ message: String, 61 | displayTime: Bool = false, 62 | _ filepath: String = #file, 63 | _ function: String = #function, 64 | _ line: Int = #line, 65 | _ column: Int = #column 66 | ) { 67 | log(message, style: .error, displayTime: displayTime, filepath, function, line, column) 68 | } 69 | 70 | public static func info( 71 | _ message: String, 72 | displayTime: Bool = false, 73 | _ filepath: String = #file, 74 | _ function: String = #function, 75 | _ line: Int = #line, 76 | _ column: Int = #column 77 | ) { 78 | log(message, style: .info, displayTime: displayTime, filepath, function, line, column) 79 | } 80 | 81 | public static func log( 82 | _ message: String, 83 | style: LogStyle = .debug, 84 | displayTime: Bool = false, 85 | _ filepath: String = #file, 86 | _ function: String = #function, 87 | _ line: Int = #line, 88 | _ column: Int = #column 89 | ) { 90 | guard !enabled.contains(.none) 91 | else { return } 92 | 93 | let time = displayTime ? Debug.dateFormatter.string(from: Date()) + " " : "" 94 | 95 | switch style { 96 | case .debug: 97 | print(time + style.prefix + message) 98 | case .info: 99 | print(time + style.prefix + message) 100 | case .warning: 101 | let filename = URL(fileURLWithPath: filepath).lastPathComponent 102 | print(time + style.prefix + "[\(filename) line:\(line) col:\(column) func:\(function)] -> " + message) 103 | case .error: 104 | let filename = URL(fileURLWithPath: filepath).lastPathComponent 105 | print(time + style.prefix + "[\(filename) line:\(line) col:\(column) func:\(function)] -> " + message) 106 | default: 107 | break 108 | } 109 | } 110 | 111 | public static func warning( 112 | items: Any..., 113 | displayTime: Bool = false, 114 | _ filepath: String = #file, 115 | _ function: String = #function, 116 | _ line: Int = #line, 117 | _ column: Int = #column 118 | ) { 119 | log(items: items, style: .warning, displayTime: displayTime, filepath, function, line, column) 120 | } 121 | 122 | public static func error( 123 | items: Any..., 124 | displayTime: Bool = false, 125 | _ filepath: String = #file, 126 | _ function: String = #function, 127 | _ line: Int = #line, 128 | _ column: Int = #column 129 | ) { 130 | log(items: items, style: .error, displayTime: displayTime, filepath, function, line, column) 131 | } 132 | 133 | public static func info( 134 | items: Any..., 135 | displayTime: Bool = false, 136 | _ filepath: String = #file, 137 | _ function: String = #function, 138 | _ line: Int = #line, 139 | _ column: Int = #column 140 | ) { 141 | log(items: items, style: .info, displayTime: displayTime, filepath, function, line, column) 142 | } 143 | 144 | public static func log( 145 | items: Any..., 146 | style: LogStyle = .debug, 147 | displayTime: Bool = false, 148 | _ filepath: String = #file, 149 | _ function: String = #function, 150 | _ line: Int = #line, 151 | _ column: Int = #column 152 | ) { 153 | guard !enabled.contains(.none) 154 | else { return } 155 | 156 | let time = displayTime ? Debug.dateFormatter.string(from: Date()) + " " : "" 157 | 158 | switch style { 159 | case .debug: 160 | print(time + style.prefix, items) 161 | case .info: 162 | print(time + style.prefix, items) 163 | case .warning: 164 | let filename = URL(fileURLWithPath: filepath).lastPathComponent 165 | print(time + style.prefix + "[\(filename) line:\(line) col:\(column) func:\(function)] -> ", items) 166 | case .error: 167 | let filename = URL(fileURLWithPath: filepath).lastPathComponent 168 | print(time + style.prefix + "[\(filename) line:\(line) col:\(column) func:\(function)] -> ", items) 169 | default: 170 | break 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /Sources/UnityKit/Static/Input.swift: -------------------------------------------------------------------------------- 1 | import SceneKit 2 | 3 | public typealias TouchPhase = UITouch.Phase 4 | public typealias TouchType = UITouch.TouchType 5 | 6 | public final class Touch { 7 | fileprivate var previousTime: TimeInterval? 8 | fileprivate var updatedTime: TimeInterval = 0 9 | 10 | internal var previousPosition: Vector2? 11 | internal var updatedPosition: Vector2 = .zero 12 | 13 | public var deltaPosition: Vector2 { 14 | guard let previousPosition = previousPosition, 15 | let _ = phase 16 | else { return .zero } 17 | 18 | return updatedPosition - previousPosition 19 | } 20 | 21 | internal(set) public var deltaTime: TimeInterval { 22 | get { 23 | guard let previousTime = previousTime 24 | else { return 0 } 25 | 26 | return updatedTime - previousTime 27 | } 28 | set { 29 | previousTime = updatedTime != 0 ? updatedTime : nil 30 | updatedTime = newValue 31 | } 32 | } 33 | 34 | public let fingerId: Int 35 | public let uitouch: UITouch 36 | public var view: UIView? { return viewAtBegin } 37 | fileprivate weak var viewAtBegin: UIView? 38 | 39 | public var phase: TouchPhase? 40 | public var altitudeAngle: Float { return uitouch.altitudeAngle.toFloat() } 41 | public var azimuthAngle: Float { return uitouch.azimuthAngle(in: uitouch.view).toFloat() } 42 | public var maximumPossiblePressure: Float { return uitouch.maximumPossibleForce.toFloat() } 43 | public var position: Vector2 { return uitouch.location(in: uitouch.view).toVector2() } 44 | public var pressure: Float { return uitouch.force.toFloat() } 45 | public var radius: Float { return uitouch.majorRadius.toFloat() } 46 | public var radiusVariance: Float { return uitouch.majorRadiusTolerance.toFloat() } 47 | public var tapCount: Int { return uitouch.tapCount } 48 | public var type: TouchType { return uitouch.type } 49 | 50 | internal init(_ uitouch: UITouch, index: Int) { 51 | self.uitouch = uitouch 52 | fingerId = index 53 | viewAtBegin = uitouch.view 54 | phase = uitouch.phase 55 | updatedTime = uitouch.timestamp 56 | updatedPosition = uitouch.location(in: uitouch.view).toVector2() 57 | } 58 | 59 | internal func updateTouch(_ touch: Touch) { 60 | if touch.view != view { 61 | phase = .cancelled 62 | } 63 | deltaTime = touch.updatedTime 64 | updatedPosition = touch.position 65 | } 66 | 67 | public func position(in view: UIView?) -> Vector2 { 68 | return uitouch.location(in: view).toVector2() 69 | } 70 | } 71 | 72 | public final class Input { 73 | private static var touches: [Touch]? 74 | private static var stackUpdates = [TouchPhase: [Touch]]() 75 | private static var clearNextFrame = false 76 | 77 | public static var touchCount: Int { 78 | guard let touches = touches 79 | else { return 0 } 80 | 81 | return touches.count 82 | } 83 | 84 | internal static func update() { 85 | guard !clearNextFrame else { 86 | clear() 87 | return 88 | } 89 | 90 | if let touches = touches { 91 | if let first = stackUpdates.first { 92 | stackUpdates.removeValue(forKey: first.key) 93 | 94 | touches.enumerated().forEach { index, touch in 95 | let updatedTouch = first.value[index] 96 | touch.phase = first.key 97 | switch first.key { 98 | case .moved: 99 | touch.previousPosition = touch.position 100 | case .ended: 101 | touch.previousPosition = touch.position 102 | clearNextFrame = true 103 | case .cancelled: 104 | clear() 105 | default: 106 | break 107 | } 108 | touch.updateTouch(updatedTouch) 109 | } 110 | } else { 111 | touches.forEach { 112 | $0.phase = .stationary 113 | } 114 | } 115 | } else if let first = stackUpdates.first, 116 | first.value.first?.phase == .began { 117 | setTouches(first.value) 118 | update() 119 | } 120 | } 121 | 122 | internal static func stackTouches(_ touches: [Touch], phase: TouchPhase) { 123 | if let currentTouches = self.touches, 124 | let currentFirst = currentTouches.first, 125 | let first = touches.first { 126 | if currentFirst.view != first.view { 127 | currentTouches.forEach { 128 | $0.phase = .ended 129 | } 130 | clearNextFrame = true 131 | return 132 | } 133 | } 134 | 135 | switch phase { 136 | case .began: 137 | if self.touches != nil || stackUpdates.count > 0 { 138 | clear() 139 | } 140 | default: 141 | break 142 | } 143 | stackUpdates[phase] = touches 144 | } 145 | 146 | public static func getTouch(_ index: Int) -> Touch? { 147 | return touches?[index] 148 | } 149 | 150 | internal static func clear() { 151 | clearNextFrame = false 152 | stackUpdates.removeAll() 153 | touches = nil 154 | } 155 | 156 | internal static func setTouches(_ touches: [Touch]) { 157 | self.touches = touches 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /Sources/UnityKit/Static/Physics.swift: -------------------------------------------------------------------------------- 1 | import SceneKit 2 | 3 | public class Physics { 4 | public static func overlapSphere( 5 | position: Vector3, 6 | radius: Float, 7 | layerMask: GameObject.Layer = .all, 8 | in scene: Scene? = Scene.shared 9 | ) -> [Collider] { 10 | guard let scene = scene 11 | else { return [] } 12 | 13 | let boundingSphere = (center: position, radius: radius) 14 | 15 | let colliders = GameObject.findGameObjects(.layer(layerMask), in: scene) 16 | .compactMap { gameObject -> Collider? in gameObject.getComponent(Collider.self) } 17 | .filter { 18 | guard let gameObject = $0.gameObject 19 | else { return false } 20 | 21 | var boundingBox = gameObject.boundingBox 22 | 23 | let min = boundingBox.min + gameObject.transform.position 24 | let max = boundingBox.max + gameObject.transform.position 25 | 26 | boundingBox = (min: min, max: max) 27 | 28 | return boxIntersectsSphere(box: boundingBox, sphere: boundingSphere) 29 | } 30 | 31 | return colliders 32 | } 33 | 34 | static func boxIntersectsSphere(box: BoundingBox, sphere: BoundingSphere) -> Bool { 35 | var dist_squared = sphere.radius * sphere.radius 36 | if sphere.center.x < box.min.x { dist_squared -= pow(sphere.center.x - box.min.x, 2) } 37 | else if sphere.center.x > box.max.x { dist_squared -= pow(sphere.center.x - box.max.x, 2) } 38 | if sphere.center.y < box.min.y { dist_squared -= pow(sphere.center.y - box.min.y, 2) } 39 | else if sphere.center.y > box.max.y { dist_squared -= pow(sphere.center.y - box.max.y, 2) } 40 | if sphere.center.z < box.min.z { dist_squared -= pow(sphere.center.z - box.min.z, 2) } 41 | else if sphere.center.z > box.max.z { dist_squared -= pow(sphere.center.z - box.max.z, 2) } 42 | return dist_squared > 0 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/UnityKit/Static/Screen.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public class Screen { 4 | internal(set) public static var width: CGFloat = 0 5 | internal(set) public static var height: CGFloat = 0 6 | } 7 | -------------------------------------------------------------------------------- /Sources/UnityKit/Static/Time.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public class Time { 4 | internal(set) public static var deltaTime: TimeInterval = 0 5 | 6 | @discardableResult internal static func evaluateTime(_ start: DispatchTime) -> TimeInterval { 7 | let nanoTime = DispatchTime.now().uptimeNanoseconds - start.uptimeNanoseconds 8 | let timeInterval = TimeInterval(nanoTime) / 1_000_000_000 9 | 10 | Debug.log("Time to evaluate: \(timeInterval) seconds") 11 | 12 | return timeInterval 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/UnityKit/UI/Components/Canvas.swift: -------------------------------------------------------------------------------- 1 | import SpriteKit 2 | 3 | extension UI { 4 | public class Canvas: UIBehaviour { 5 | internal(set) public var worldSize: Size = .zero 6 | internal(set) public var pixelPerUnit: Float = 10 7 | 8 | /** 9 | Configurable block that passes and returns itself. 10 | 11 | - parameters: 12 | - configurationBlock: block that passes itself. 13 | 14 | - returns: itself 15 | */ 16 | @discardableResult public func configure(_ configurationBlock: (Canvas) -> Void) -> Canvas { 17 | configurationBlock(self) 18 | return self 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/UnityKit/UI/Components/Image.swift: -------------------------------------------------------------------------------- 1 | import SpriteKit 2 | 3 | extension UI { 4 | public final class Image: UIBehaviour { 5 | public enum ImageType { 6 | case simple(Size) 7 | case filled(Size) 8 | } 9 | 10 | public enum FillMethod { 11 | case horizontal(FillOrigin) 12 | case vertical(FillOrigin) 13 | case radial90(FillOrigin) 14 | case radial180(FillOrigin) 15 | case radial360(FillOrigin) 16 | } 17 | 18 | public enum FillOrigin { 19 | case bottom 20 | case right 21 | case top 22 | case left 23 | } 24 | 25 | private var spriteNode: SKSpriteNode? 26 | 27 | public var type: ImageType = .filled(.zero) { 28 | didSet { 29 | updateImage() 30 | } 31 | } 32 | 33 | public var sourceImage: UIImage? { 34 | didSet { 35 | updateImage() 36 | } 37 | } 38 | 39 | public var color: Color = .white { 40 | didSet { 41 | updateImage() 42 | } 43 | } 44 | 45 | public var fillMethod: FillMethod = .horizontal(.bottom) { 46 | didSet { 47 | updateImage() 48 | } 49 | } 50 | 51 | public var fillAmount: Float = 1 { 52 | didSet { 53 | updateImage() 54 | } 55 | } 56 | 57 | public var clockwise: Bool = false { 58 | didSet { 59 | updateImage() 60 | } 61 | } 62 | 63 | /** 64 | Configurable block that passes and returns itself. 65 | 66 | - parameters: 67 | - configurationBlock: block that passes itself. 68 | - returns: itself 69 | */ 70 | @discardableResult public func configure(_ configurationBlock: (Image) -> Void) -> Image { 71 | configurationBlock(self) 72 | return self 73 | } 74 | 75 | private func updateImage() { 76 | canvasObject.resume() 77 | defer { 78 | canvasObject.pause() 79 | } 80 | 81 | spriteNode?.removeFromParent() 82 | 83 | guard let sourceImage = sourceImage 84 | else { return } 85 | 86 | var displayImage = sourceImage 87 | 88 | if color != .white { 89 | displayImage = displayImage.replaceColor(with: color) 90 | } 91 | 92 | let texture: SKTexture 93 | 94 | switch type { 95 | case let .simple(size): 96 | texture = SKTexture(image: displayImage.resize(to: size.toCGSize())) 97 | 98 | case let .filled(size): 99 | if size == .zero { 100 | displayImage = displayImage.resize(to: skScene.size) 101 | } else { 102 | displayImage = displayImage.resize(to: size.toCGSize()) 103 | } 104 | 105 | switch fillMethod { 106 | case let .radial360(fillOrigin): 107 | let to = fillAmount.clamp01() * 360 108 | displayImage = displayImage.fill( 109 | fromAngle: 0, 110 | toAngle: to, 111 | fillOrigin: fillOrigin, 112 | clockwise: clockwise 113 | ) 114 | default: 115 | break 116 | } 117 | texture = SKTexture(image: displayImage) 118 | } 119 | 120 | let sprite = SKSpriteNode(texture: texture) 121 | sprite.anchorPoint = CGPoint(x: 0, y: 0) 122 | skScene.addChild(sprite) 123 | spriteNode = sprite 124 | } 125 | 126 | public func loadImage(fileName: String, type: ImageType, color: Color = .white, bundle: Bundle = Bundle.main) { 127 | guard let url = searchPathForResource(for: fileName, extension: nil, bundle: bundle) 128 | else { return } 129 | 130 | do { 131 | let imageData = try Data(contentsOf: url) 132 | guard let sourceImage = UIImage(data: imageData) 133 | else { return } 134 | 135 | self.color = color 136 | self.type = type 137 | self.sourceImage = sourceImage 138 | } catch {} 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Sources/UnityKit/UI/Components/Slider.swift: -------------------------------------------------------------------------------- 1 | import SpriteKit 2 | 3 | extension UI { 4 | public class Slider: UIBehaviour { 5 | public var fillImage: Image? 6 | public var minValue: Float = 0 7 | public var maxValue: Float = 1 8 | public var value: Float = 0 { 9 | didSet { 10 | value.clamp(minValue...maxValue) 11 | updateValue() 12 | } 13 | } 14 | 15 | /** 16 | Configurable block that passes and returns itself. 17 | - parameters: 18 | - configurationBlock: block that passes itself. 19 | 20 | - returns: itself 21 | */ 22 | @discardableResult public func configure(_ configurationBlock: (Slider) -> Void) -> Slider { 23 | configurationBlock(self) 24 | return self 25 | } 26 | 27 | private func updateValue() { 28 | fillImage?.fillAmount = ((value - minValue) / (maxValue - minValue)) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/UnityKit/UI/Elements/CanvasObject.swift: -------------------------------------------------------------------------------- 1 | import SpriteKit 2 | import SceneKit 3 | 4 | public class CanvasObject: GameObject { 5 | internal var skScene: SKScene { 6 | guard let scene = node.geometry?.firstMaterial?.diffuse.contents as? SKScene else { 7 | fatalError("Should have a scene") 8 | } 9 | return scene 10 | } 11 | 12 | private var skView: SKView? 13 | 14 | @discardableResult public func set(worldSize: Size, pixelPerUnit: Float = 1) -> CanvasObject { 15 | getComponent(UI.Canvas.self)? 16 | .configure { 17 | $0.worldSize = worldSize 18 | $0.pixelPerUnit = pixelPerUnit 19 | } 20 | let result = CanvasObject.makeCanvas(worldSize: worldSize, pixelPerUnit: pixelPerUnit) 21 | node = result.0 22 | skView = result.1 23 | pause() 24 | return self 25 | } 26 | 27 | public required init(worldSize: Size, pixelPerUnit: Float = 50) { 28 | let result = CanvasObject.makeCanvas(worldSize: worldSize, pixelPerUnit: pixelPerUnit) 29 | super.init(result.0) 30 | skView = result.1 31 | pause() 32 | addComponent(external: false, type: UI.Canvas.self, gameObject: self) 33 | .configure { 34 | $0.worldSize = worldSize 35 | $0.pixelPerUnit = pixelPerUnit 36 | } 37 | } 38 | 39 | public func pixelSize() -> Size { 40 | return skScene.size.toSize() 41 | } 42 | 43 | public required init() { 44 | fatalError("init() has not been implemented, use init(size:)") 45 | } 46 | 47 | internal func pause() { 48 | skView?.isPaused = true 49 | } 50 | 51 | internal func resume() { 52 | skView?.isPaused = false 53 | } 54 | 55 | private static func makeCanvas(worldSize: Size, pixelPerUnit: Float) -> (SCNNode, SKView) { 56 | let geometry = SCNGeometry.createPrimitive(.plane(width: worldSize.width, height: worldSize.height)) 57 | let node = SCNNode(geometry: geometry) 58 | node.name = "Canvas" 59 | node.castsShadow = false 60 | 61 | let view = SKView(frame: CGRect(origin: .zero, size: (worldSize * pixelPerUnit).toCGSize())) 62 | let skScene = SKScene(size: (worldSize * pixelPerUnit).toCGSize()) 63 | view.presentScene(skScene) 64 | view.scene?.backgroundColor = .clear 65 | 66 | geometry.firstMaterial?.lightingModel = .constant 67 | geometry.firstMaterial?.diffuse.contents = view.scene 68 | geometry.firstMaterial?.isDoubleSided = true 69 | 70 | return (node, view) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/UnityKit/UI/Elements/View.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SceneKit 3 | import SwiftUI 4 | 5 | extension UI { 6 | 7 | public struct Options { 8 | public let allowsCameraControl: Bool? 9 | public let autoenablesDefaultLighting: Bool? 10 | public let antialiasingMode: SCNAntialiasingMode? 11 | public let preferredRenderingAPI: SCNRenderingAPI? 12 | public let showsStatistics: Bool? 13 | public let backgroundColor: Color? 14 | public let rendersContinuously: Bool? 15 | public let castShadow: Bool = true 16 | public let allocation: Scene.Allocation = .singleton 17 | 18 | public init( 19 | allowsCameraControl: Bool? = nil, 20 | autoenablesDefaultLighting: Bool? = false, 21 | antialiasingMode: SCNAntialiasingMode? = nil, 22 | preferredRenderingAPI: SCNRenderingAPI? = nil, 23 | showsStatistics: Bool? = nil, 24 | backgroundColor: Color? = nil, 25 | rendersContinuously: Bool? = true, 26 | castShadow: Bool = true, 27 | allocation: Scene.Allocation = .singleton 28 | ) { 29 | self.allowsCameraControl = allowsCameraControl 30 | self.autoenablesDefaultLighting = autoenablesDefaultLighting 31 | self.antialiasingMode = antialiasingMode 32 | self.preferredRenderingAPI = preferredRenderingAPI 33 | self.showsStatistics = showsStatistics 34 | self.backgroundColor = backgroundColor 35 | self.rendersContinuously = rendersContinuously 36 | } 37 | } 38 | 39 | public struct SwiftUIView: UIViewRepresentable { 40 | 41 | let sceneView: UI.UIKitView 42 | public var scene: Scene? { 43 | sceneView.sceneHolder 44 | } 45 | 46 | public init( 47 | sceneName: String? = nil, 48 | options: Options? = nil, 49 | extraLayers: [String]? = nil 50 | ) { 51 | self.sceneView = UI.UIKitView.makeView( 52 | on: nil, 53 | sceneName: sceneName, 54 | options: options, 55 | extraLayers: extraLayers 56 | ) 57 | } 58 | 59 | public func makeUIView(context: Context) -> UI.UIKitView { 60 | sceneView 61 | } 62 | 63 | public func updateUIView(_ uiView: UI.UIKitView, context: Context) { 64 | 65 | } 66 | } 67 | } 68 | 69 | extension UI { 70 | open class UIKitView: SCNView { 71 | private class AtomicLock { 72 | private let lock = DispatchSemaphore(value: 1) 73 | private var value: Bool = false 74 | 75 | func get() -> Bool { 76 | lock.wait() 77 | defer { lock.signal() } 78 | return value 79 | } 80 | 81 | func set(_ newValue: Bool) { 82 | lock.wait() 83 | defer { lock.signal() } 84 | value = newValue 85 | } 86 | } 87 | 88 | public override init(frame: CGRect, options: [String: Any]? = nil) { 89 | self.lock = Dictionary(uniqueKeysWithValues: Lock.all.map { ($0, AtomicLock()) }) 90 | super.init(frame: .zero, options: options) 91 | self.delegate = self 92 | } 93 | 94 | @available(*, unavailable) 95 | required public init?(coder aDecoder: NSCoder) { 96 | fatalError("init(coder:) has not been implemented") 97 | } 98 | 99 | private enum Lock: Int { 100 | case preUpdate, update, fixed, physicsBegin, physicsEnd 101 | static let all: [Lock] = [.preUpdate, .update, .fixed, .physicsBegin, .physicsEnd] 102 | } 103 | private var lock: [Lock: AtomicLock] 104 | 105 | public static func makeView( 106 | on superview: UIView? = nil, 107 | sceneName: String? = nil, 108 | options: Options? = nil, 109 | extraLayers: [String]? = nil 110 | ) -> UIKitView { 111 | let shadowCastingAllowed: Bool 112 | #if (arch(i386) || arch(x86_64)) 113 | let options = options ?? Options( 114 | antialiasingMode: SCNAntialiasingMode.none, 115 | preferredRenderingAPI: .openGLES2 116 | ) 117 | shadowCastingAllowed = false 118 | print("Shadows are disabled on the simulator for performance reason, prefer to use a device!") 119 | #else 120 | let options = options ?? Options( 121 | antialiasingMode: SCNAntialiasingMode.multisampling4X, 122 | preferredRenderingAPI: .metal 123 | ) 124 | shadowCastingAllowed = options.castShadow 125 | #endif 126 | 127 | extraLayers?.forEach { 128 | GameObject.Layer.addLayer(with: $0) 129 | } 130 | 131 | let view = UIKitView(frame: .zero, options: ["preferredRenderingAPI": options.preferredRenderingAPI ?? SCNRenderingAPI.metal]) 132 | 133 | options.allowsCameraControl.map { view.allowsCameraControl = $0 } 134 | options.autoenablesDefaultLighting.map { view.autoenablesDefaultLighting = $0 } 135 | options.antialiasingMode.map { view.antialiasingMode = $0 } 136 | options.showsStatistics.map { view.showsStatistics = $0 } 137 | options.backgroundColor.map { view.backgroundColor = $0 } 138 | options.rendersContinuously.map { view.rendersContinuously = $0 } 139 | 140 | if let sceneName = sceneName { 141 | view.sceneHolder = Scene( 142 | sceneName: sceneName, 143 | allocation: options.allocation, 144 | shadowCastingAllowed: shadowCastingAllowed 145 | ) 146 | } else { 147 | view.sceneHolder = Scene( 148 | allocation: options.allocation, 149 | shadowCastingAllowed: shadowCastingAllowed 150 | ) 151 | } 152 | 153 | view.scene?.physicsWorld.contactDelegate = view 154 | 155 | if let superview = superview { 156 | view.frame = superview.bounds 157 | view.autoresizingMask = [.flexibleWidth, .flexibleHeight] 158 | 159 | superview.addSubview(view) 160 | 161 | Screen.width = view.frame.size.width 162 | Screen.height = view.frame.size.height 163 | } 164 | 165 | return view 166 | } 167 | 168 | public var sceneHolder: Scene? { 169 | didSet { 170 | guard let scene = sceneHolder 171 | else { return } 172 | 173 | self.scene = scene.scnScene 174 | self.pointOfView = Camera.main(in: scene)?.gameObject?.node 175 | } 176 | } 177 | 178 | open override func layoutSubviews() { 179 | super.layoutSubviews() 180 | 181 | Screen.width = frame.size.width 182 | Screen.height = frame.size.height 183 | 184 | if let scene = sceneHolder, 185 | let camera = Camera.main(in: scene) { 186 | camera.calculateFieldOfViews() 187 | } 188 | } 189 | } 190 | } 191 | 192 | extension UI.UIKitView: SCNSceneRendererDelegate { 193 | public func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval) { 194 | guard lock[.preUpdate]?.get() == false else { return } 195 | lock[.preUpdate]?.set(true) 196 | DispatchQueue.main.async { [weak self] () -> Void in 197 | self?.sceneHolder?.preUpdate(updateAtTime: time) 198 | self?.lock[.preUpdate]?.set(false) 199 | } 200 | } 201 | 202 | public func renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval) { 203 | guard lock[.update]?.get() == false else { return } 204 | lock[.update]?.set(true) 205 | DispatchQueue.main.async { [weak self] () -> Void in 206 | self?.sceneHolder?.update(updateAtTime: time) 207 | Input.update() 208 | self?.lock[.update]?.set(false) 209 | } 210 | } 211 | 212 | public func renderer(_ renderer: SCNSceneRenderer, didSimulatePhysicsAtTime time: TimeInterval) { 213 | guard lock[.fixed]?.get() == false else { return } 214 | lock[.fixed]?.set(true) 215 | DispatchQueue.main.async { [weak self] () -> Void in 216 | self?.sceneHolder?.fixedUpdate(updateAtTime: time) 217 | self?.lock[.fixed]?.set(false) 218 | } 219 | } 220 | } 221 | 222 | extension UI.UIKitView: SCNPhysicsContactDelegate { 223 | public func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) { 224 | guard lock[.physicsBegin]?.get() == false else { return } 225 | lock[.physicsBegin]?.set(true) 226 | guard let sceneHolder = sceneHolder 227 | else { return } 228 | 229 | DispatchQueue.main.async { [weak self] () -> Void in 230 | GameObject.findObjectsOfType(Collider.self, in: sceneHolder).forEach { 231 | $0.physicsWorld(world, didBegin: contact) 232 | } 233 | self?.lock[.physicsBegin]?.set(false) 234 | } 235 | } 236 | 237 | public func physicsWorld(_ world: SCNPhysicsWorld, didEnd contact: SCNPhysicsContact) { 238 | guard lock[.physicsEnd]?.get() == false else { return } 239 | lock[.physicsEnd]?.set(true) 240 | guard let sceneHolder = sceneHolder 241 | else { return } 242 | 243 | DispatchQueue.main.async { [weak self] () -> Void in 244 | GameObject.findObjectsOfType(Collider.self, in: sceneHolder).forEach { 245 | $0.physicsWorld(world, didEnd: contact) 246 | } 247 | self?.lock[.physicsEnd]?.set(false) 248 | } 249 | } 250 | } 251 | 252 | extension UI.UIKitView: UIGestureRecognizerDelegate { 253 | open override func touchesBegan(_ touches: Set, with event: UIEvent?) { 254 | let touches = touches.enumerated().map { index, uitouch -> Touch in Touch(uitouch, index: index) } 255 | Input.stackTouches(touches, phase: .began) 256 | } 257 | 258 | open override func touchesMoved(_ touches: Set, with event: UIEvent?) { 259 | let touches = touches.enumerated().map { index, uitouch -> Touch in Touch(uitouch, index: index) } 260 | Input.stackTouches(touches, phase: .moved) 261 | } 262 | 263 | open override func touchesEnded(_ touches: Set, with event: UIEvent?) { 264 | let touches = touches.enumerated().map { index, uitouch -> Touch in Touch(uitouch, index: index) } 265 | Input.stackTouches(touches, phase: .ended) 266 | } 267 | 268 | open override func touchesCancelled(_ touches: Set, with event: UIEvent?) { 269 | let touches = touches.enumerated().map { index, uitouch -> Touch in Touch(uitouch, index: index) } 270 | Input.stackTouches(touches, phase: .cancelled) 271 | } 272 | 273 | public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool { 274 | return false 275 | } 276 | 277 | public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 278 | return true 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /Sources/UnityKit/UI/Engine/UI.swift: -------------------------------------------------------------------------------- 1 | public enum UI { 2 | } 3 | -------------------------------------------------------------------------------- /Sources/UnityKit/UI/Engine/UIBehaviour.swift: -------------------------------------------------------------------------------- 1 | import SpriteKit 2 | 3 | extension UI { 4 | public class UIBehaviour: Behaviour { 5 | public var canvasObject: CanvasObject! 6 | 7 | public var skScene: SKScene { 8 | canvasObject.skScene 9 | } 10 | 11 | public override func awake() { 12 | var canvasFound = false 13 | var parent = gameObject 14 | while parent != nil { 15 | if let object = parent as? CanvasObject { 16 | canvasObject = object 17 | canvasFound = true 18 | break 19 | } 20 | parent = parent?.parent 21 | } 22 | guard canvasFound 23 | else { fatalError("any subclass of UIBehaviour must be inside a Canvas") } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/UnityKit/Utilities/Ease.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum Ease { 4 | case linear 5 | case backOut 6 | case backIn 7 | case backInOut 8 | case bounceOut 9 | case bounceIn 10 | case bounceInOut 11 | case circleOut 12 | case circleIn 13 | case circleInOut 14 | case cubicOut 15 | case cubicIn 16 | case cubicInOut 17 | case elasticOut 18 | case elasticIn 19 | case elasticInOut 20 | case expoOut 21 | case expoIn 22 | case expoInOut 23 | case quadOut 24 | case quadIn 25 | case quadInOut 26 | case quartOut 27 | case quartIn 28 | case quartInOut 29 | case quintOut 30 | case quintIn 31 | case quintInOut 32 | case sineOut 33 | case sineIn 34 | case sineInOut 35 | case custom(ActionTimingFunction) 36 | 37 | public func timingFunction() -> ActionTimingFunction { 38 | switch self { 39 | case .linear: return Ease._linear 40 | case .backOut: return Ease._backOut 41 | case .backIn: return Ease._backIn 42 | case .backInOut: return Ease._backInOut 43 | case .bounceOut: return Ease._bounceOut 44 | case .bounceIn: return Ease._bounceIn 45 | case .bounceInOut: return Ease._bounceInOut 46 | case .circleOut: return Ease._circleOut 47 | case .circleIn: return Ease._circleIn 48 | case .circleInOut: return Ease._circleInOut 49 | case .cubicOut: return Ease._cubicOut 50 | case .cubicIn: return Ease._cubicIn 51 | case .cubicInOut: return Ease._cubicInOut 52 | case .elasticOut: return Ease._elasticOut 53 | case .elasticIn: return Ease._elasticIn 54 | case .elasticInOut: return Ease._elasticInOut 55 | case .expoOut: return Ease._expoOut 56 | case .expoIn: return Ease._expoIn 57 | case .expoInOut: return Ease._expoInOut 58 | case .quadOut: return Ease._quadOut 59 | case .quadIn: return Ease._quadIn 60 | case .quadInOut: return Ease._quadInOut 61 | case .quartOut: return Ease._quartOut 62 | case .quartIn: return Ease._quartIn 63 | case .quartInOut: return Ease._quartInOut 64 | case .quintOut: return Ease._quintOut 65 | case .quintIn: return Ease._quintIn 66 | case .quintInOut: return Ease._quintInOut 67 | case .sineOut: return Ease._sineOut 68 | case .sineIn: return Ease._sineIn 69 | case .sineInOut: return Ease._sineInOut 70 | case let .custom(function): return function 71 | } 72 | } 73 | 74 | static private var _linear: ActionTimingFunction = { (n: Float) -> Float in 75 | return n 76 | } 77 | 78 | static private var _quadIn: ActionTimingFunction = { (n: Float) -> Float in 79 | return n * n 80 | } 81 | 82 | static private var _quadOut: ActionTimingFunction = { (n: Float) -> Float in 83 | return n * (2 - n) 84 | } 85 | 86 | static private var _quadInOut: ActionTimingFunction = { (n: Float) -> Float in 87 | var n = n * 2 88 | if n < 1 { 89 | return 0.5 * n * n * n 90 | } 91 | n -= 1 92 | return -0.5 * (n * (n - 2) - 1) 93 | } 94 | 95 | static private var _cubicIn: ActionTimingFunction = { (n: Float) -> Float in 96 | return n * n * n 97 | } 98 | 99 | static private var _cubicOut: ActionTimingFunction = { (n: Float) -> Float in 100 | let n = n - 1 101 | return n * n * n + 1 102 | } 103 | 104 | static private var _cubicInOut: ActionTimingFunction = { (n: Float) -> Float in 105 | var n = n * 2 106 | if n < 1 { 107 | return 0.5 * n * n * n 108 | } 109 | n -= 2 110 | return 0.5 * (n * n * n + 2) 111 | } 112 | 113 | static private var _quartIn: ActionTimingFunction = { (n: Float) -> Float in 114 | return n * n * n * n 115 | } 116 | 117 | static private var _quartOut: ActionTimingFunction = { (n: Float) -> Float in 118 | return 1 - _quartIn(n - 1) 119 | } 120 | 121 | static private var _quartInOut: ActionTimingFunction = { (n: Float) -> Float in 122 | var n = n * 2 123 | if n < 1 { 124 | return 0.5 * _quartIn(n) 125 | } 126 | n -= 2 127 | return -0.5 * (_quartIn(n) - 2) 128 | } 129 | 130 | static private var _quintIn: ActionTimingFunction = { (n: Float) -> Float in 131 | return n * n * n * n * n 132 | } 133 | 134 | static private var _quintOut: ActionTimingFunction = { (n: Float) -> Float in 135 | return _quintIn(n - 1) + 1 136 | } 137 | 138 | static private var _quintInOut: ActionTimingFunction = { (n: Float) -> Float in 139 | var n = n * 2 140 | if n < 1 { 141 | return 0.5 * _quintIn(n) 142 | } 143 | n -= 2 144 | return 0.5 * (_quintIn(n) + 2) 145 | } 146 | 147 | static private var _sineIn: ActionTimingFunction = { (n: Float) -> Float in 148 | return 1 - cos(n * .pi / 2 ) 149 | } 150 | 151 | static private var _sineOut: ActionTimingFunction = { (n: Float) -> Float in 152 | return sin(n * .pi / 2) 153 | } 154 | 155 | static private var _sineInOut: ActionTimingFunction = { (n: Float) -> Float in 156 | return 0.5 * (1 - cos(.pi * n)) 157 | } 158 | 159 | static private var _expoIn: ActionTimingFunction = { (n: Float) -> Float in 160 | return 0 == n ? 0 : pow(1024, n - 1) 161 | } 162 | 163 | static private var _expoOut: ActionTimingFunction = { (n: Float) -> Float in 164 | return 1 == n ? n : 1 - pow(2, -10 * n) 165 | } 166 | 167 | static private var _expoInOut: ActionTimingFunction = { (n: Float) -> Float in 168 | if n == 0 { 169 | return 0 170 | } 171 | if n == 1 { 172 | return 1 173 | } 174 | let n = n * 2 175 | if n < 1 { 176 | return 0.5 * pow(1024, n - 1) 177 | } 178 | return 0.5 * (-pow(2, -10 * (n - 1)) + 2) 179 | } 180 | 181 | static private var _circleIn: ActionTimingFunction = { (n: Float) -> Float in 182 | return 1 - sqrt(1 - n * n) 183 | } 184 | 185 | static private var _circleOut: ActionTimingFunction = { (n: Float) -> Float in 186 | let n = n - 1 187 | return sqrt(1 - (n * n)) 188 | } 189 | 190 | static private var _circleInOut: ActionTimingFunction = { (n: Float) -> Float in 191 | var n = n * 2 192 | if n < 1 { 193 | return -0.5 * (sqrt(1 - n * n) - 1) 194 | } 195 | n -= 2 196 | return 0.5 * (sqrt(1 - n * n) + 1) 197 | } 198 | 199 | static private var _backIn: ActionTimingFunction = { (n: Float) -> Float in 200 | let s: Float = 1.70158 201 | return n * n * (((s + 1) * n) - s) 202 | } 203 | 204 | static private var _backOut: ActionTimingFunction = { (n: Float) -> Float in 205 | let n = n - 1 206 | let s: Float = 1.70158 207 | return (n * n * (((s + 1) * n) + s)) + 1 208 | } 209 | 210 | static private var _backInOut: ActionTimingFunction = { (n: Float) -> Float in 211 | var n = n * 2 212 | let s: Float = 1.70158 * 1.525 213 | if n < 1 { 214 | return 0.5 * (n * n * ((s + 1) * n - s)) 215 | } 216 | n -= 2 217 | return 0.5 * (n * n * ((s + 1) * n + s) + 2) 218 | } 219 | 220 | static private var _bounceIn: ActionTimingFunction = { (n: Float) -> Float in 221 | return 1 - _bounceOut(1 - n) 222 | } 223 | 224 | static private var _bounceOut: ActionTimingFunction = { (n: Float) -> Float in 225 | var n = n 226 | if n < 1 / 2.75 { 227 | return 7.5625 * n * n 228 | } else if n < 2 / 2.75 { 229 | n -= 1.5 / 2.75 230 | return 7.5625 * n * n + 0.75 231 | } else if n < 2.5 / 2.75 { 232 | n -= 2.25 / 2.75 233 | return 7.5625 * n * n + 0.9375 234 | } else { 235 | n -= 2.625 / 2.75 236 | return 7.5625 * n * n + 0.984375 237 | } 238 | } 239 | 240 | static private var _bounceInOut: ActionTimingFunction = { (n: Float) -> Float in 241 | if n < 0.5 { 242 | return _bounceIn(n * 2) * 0.5 243 | } 244 | return _bounceOut(n * 2 - 1) * 0.5 + 0.5 245 | } 246 | 247 | static private var _elasticIn: ActionTimingFunction = { (n: Float) -> Float in 248 | if n == 0 { 249 | return 0 250 | } 251 | if n == 1 { 252 | return 1 253 | } 254 | let n = n - 1 255 | let p: Float = 0.4 256 | let s: Float = p / 4 257 | let a: Float = 1 258 | return -(a * pow(2, 10 * n) * sin((n - s) * (2 * .pi) / p)) 259 | } 260 | 261 | static private var _elasticOut: ActionTimingFunction = { (n: Float) -> Float in 262 | if n == 0 { 263 | return 0 264 | } 265 | if n == 1 { 266 | return 1 267 | } 268 | let p: Float = 0.4 269 | let s: Float = p / 4 270 | let a: Float = 1 271 | return (a * pow(2, -10 * n) * sin((n - s) * (2 * .pi) / p) + 1) 272 | } 273 | 274 | static private var _elasticInOut: ActionTimingFunction = { (n: Float) -> Float in 275 | if n == 0 { 276 | return 0 277 | } 278 | if n == 1 { 279 | return 1 280 | } 281 | let n = (n - 1) * 2 282 | let p: Float = 0.4 283 | let s: Float = p / 4 284 | let a: Float = 1 285 | if n < 1 { 286 | return -0.5 * (a * pow(2, 10 * n) * sin((n - s) * (2 * .pi) / p)) 287 | } 288 | return a * pow(2, -10 * n) * sin((n - s) * (2 * .pi) / p) * 0.5 + 1 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /Sources/UnityKit/Utilities/File.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal func searchPathForResource( 4 | for filename: String, 5 | extension ext: String? = nil, 6 | bundle: Bundle = Bundle.main 7 | ) -> URL? { 8 | guard let enumerator = FileManager.default.enumerator(atPath: bundle.bundlePath) 9 | else { return nil } 10 | 11 | for item in enumerator { 12 | guard let filePath = item as? String, 13 | !filePath.starts(with: "Frameworks"), 14 | !filePath.starts(with: "Base.lproj"), 15 | !filePath.starts(with: "_CodeSignature"), 16 | !filePath.starts(with: "META-INF"), 17 | !filePath.starts(with: "Info.plist") 18 | else { continue } 19 | 20 | let fullPath = bundle.bundlePath + "/" + filePath 21 | let url = URL(fileURLWithPath: fullPath) 22 | 23 | if let ext = ext { 24 | if url.lastPathComponent == filename + "." + ext { 25 | return url 26 | } 27 | } else { 28 | let bundleFile = splitFilename(url.lastPathComponent) 29 | let file = splitFilename(filename) 30 | 31 | if let ext1 = file.extension, 32 | let ext2 = bundleFile.extension { 33 | if bundleFile.name == file.name, 34 | ext1 == ext2 { 35 | return url 36 | } 37 | } else if bundleFile.name == file.name { 38 | return url 39 | } 40 | } 41 | } 42 | 43 | return nil 44 | } 45 | 46 | internal func splitFilename(_ name: String) -> (name: String, extension: String?) { 47 | let filename = { () -> String in 48 | let arr = name.split { $0 == "." } 49 | return arr.count > 0 ? String(describing: arr.first!) : "" 50 | }() 51 | 52 | let ext = { () -> String? in 53 | let arr = name.split { $0 == "." } 54 | return arr.count > 1 ? String(describing: arr.last!) : nil 55 | }() 56 | 57 | return (filename, ext) 58 | } 59 | -------------------------------------------------------------------------------- /Sources/UnityKit/Utilities/Volume.swift: -------------------------------------------------------------------------------- 1 | import SceneKit 2 | 3 | public typealias BoundingBox = (min: Vector3, max: Vector3) 4 | public typealias BoundingSphere = (center: Vector3, radius: Float) 5 | 6 | public class Volume { 7 | public static func boundingSize(_ boundingBox: BoundingBox) -> Vector3 { 8 | Vector3( 9 | abs(boundingBox.max.x - boundingBox.min.x), 10 | abs(boundingBox.max.y - boundingBox.min.y), 11 | abs(boundingBox.max.z - boundingBox.min.z) 12 | ) 13 | } 14 | 15 | public static func boundingCenter(_ boundingBox: BoundingBox) -> Vector3 { 16 | let volumeSize = Volume.boundingSize(boundingBox) 17 | return Vector3( 18 | boundingBox.min.x + volumeSize.x / 2, 19 | boundingBox.min.y + volumeSize.y / 2, 20 | boundingBox.min.z + volumeSize.z / 2 21 | ) 22 | } 23 | 24 | public static func moveCenter(_ boundingBox: BoundingBox, center: Vector3Nullable) -> BoundingBox { 25 | let volumeSize = Volume.boundingSize(boundingBox) 26 | var volumeCenter = Volume.boundingCenter(boundingBox) 27 | if let x = center.x { volumeCenter.x = x } 28 | if let y = center.y { volumeCenter.y = y } 29 | if let z = center.z { volumeCenter.z = z } 30 | return ( 31 | min: Vector3( 32 | volumeCenter.x - (volumeSize.x / 2), 33 | volumeCenter.y - (volumeSize.y / 2), 34 | volumeCenter.z - (volumeSize.z / 2) 35 | ), 36 | max: Vector3( 37 | volumeCenter.x + (volumeSize.x / 2), 38 | volumeCenter.y + (volumeSize.y / 2), 39 | volumeCenter.z + (volumeSize.z / 2)) 40 | ) 41 | } 42 | } 43 | 44 | public func + (left: BoundingBox?, right: BoundingBox?) -> BoundingBox? { 45 | guard let left = left else { 46 | return right 47 | } 48 | guard let right = right else { 49 | return left 50 | } 51 | var add = left 52 | add.min.x = min(left.min.x, right.min.x) 53 | add.min.y = min(left.min.y, right.min.y) 54 | add.min.z = min(left.min.z, right.min.z) 55 | add.max.x = max(left.max.x, right.max.x) 56 | add.max.y = max(left.max.y, right.max.y) 57 | add.max.z = max(left.max.z, right.max.z) 58 | return add 59 | } 60 | 61 | public func += (left: inout BoundingBox?, right: BoundingBox?) { 62 | left = left + right 63 | } 64 | 65 | public func * (left: BoundingBox, right: Vector3) -> BoundingBox { 66 | (min: left.min * right, max: left.max * right) 67 | } 68 | 69 | public func * (left: BoundingBox, right: Float) -> BoundingBox { 70 | (min: left.min * right, max: left.max * right) 71 | } 72 | 73 | extension GameObject { 74 | public func boundingBoxFromBoundingSphere(relativeTo gameObject: GameObject? = nil) -> BoundingBox? { 75 | node.boundingBoxFromBoundingSphere(relativeTo: gameObject?.node) 76 | } 77 | 78 | public func boundingBox(relativeTo gameObject: GameObject) -> BoundingBox? { 79 | node.boundingBox(relativeTo: gameObject.node) 80 | } 81 | } 82 | 83 | extension SCNNode { 84 | func boundingBoxFromBoundingSphere(relativeTo node: SCNNode? = nil) -> BoundingBox? { 85 | guard let _ = geometry 86 | else { return nil } 87 | 88 | let node = node ?? self 89 | 90 | let boundingSphere = self.boundingSphere 91 | let relativeCenter = convertPosition(boundingSphere.center, to: node) 92 | 93 | return (min: relativeCenter - boundingSphere.radius, max: relativeCenter + boundingSphere.radius) 94 | } 95 | 96 | func boundingBox(relativeTo node: SCNNode) -> BoundingBox? { 97 | var boundingBox = childNodes 98 | .reduce(nil) { $0 + $1.boundingBox(relativeTo: node) } 99 | 100 | guard let geometry = geometry, 101 | let source = geometry.sources(for: SCNGeometrySource.Semantic.vertex).first 102 | else { return boundingBox } 103 | 104 | let vertices = SCNGeometry.vertices(source: source).map { convertPosition($0, to: node) } 105 | guard let first = vertices.first 106 | else { return boundingBox } 107 | 108 | boundingBox += vertices.reduce( 109 | into: (min: first, max: first), { boundingBox, vertex in 110 | boundingBox.min.x = min(boundingBox.min.x, vertex.x) 111 | boundingBox.min.y = min(boundingBox.min.y, vertex.y) 112 | boundingBox.min.z = min(boundingBox.min.z, vertex.z) 113 | boundingBox.max.x = max(boundingBox.max.x, vertex.x) 114 | boundingBox.max.y = max(boundingBox.max.y, vertex.y) 115 | boundingBox.max.z = max(boundingBox.max.z, vertex.z) 116 | }) 117 | 118 | return boundingBox 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Templates/UnityScript File.xctemplate/TemplateIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salt-pepper-code/UnityKit/89df21948b87aa75421faa0a265ae8d2fdd4f137/Templates/UnityScript File.xctemplate/TemplateIcon.png -------------------------------------------------------------------------------- /Templates/UnityScript File.xctemplate/TemplateIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salt-pepper-code/UnityKit/89df21948b87aa75421faa0a265ae8d2fdd4f137/Templates/UnityScript File.xctemplate/TemplateIcon@2x.png -------------------------------------------------------------------------------- /Templates/UnityScript File.xctemplate/TemplateInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Kind 6 | Xcode.IDEFoundation.TextSubstitutionFileTemplateKind 7 | Description 8 | An empty UnityKit Swift Script file. 9 | Summary 10 | An empty UnityKit Swift Script file 11 | SortOrder 12 | 1 13 | AllowedTypes 14 | 15 | public.swift-source 16 | 17 | DefaultCompletionName 18 | File 19 | MainTemplateFile 20 | ___FILEBASENAME___.swift 21 | 22 | 23 | -------------------------------------------------------------------------------- /Templates/UnityScript File.xctemplate/___FILEBASENAME___.swift: -------------------------------------------------------------------------------- 1 | import UnityKit 2 | 3 | class ___FILEBASENAMEASIDENTIFIER___: MonoBehaviour { 4 | override func start() { 5 | } 6 | 7 | override func update() { 8 | } 9 | } 10 | --------------------------------------------------------------------------------