├── .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 | 
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 | 
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 |
--------------------------------------------------------------------------------