【SpriteKit入門】SpriteKitとは【SwiftUI × SpriteKit】<2/2作成中>

# SpriteKit

## はじめに
[SpriteKit](https://developer.apple.com/spritekit/)
[Apple公式ドキュメント](https://developer.apple.com/documentation/spritekit/)

## SpriteKit概要
### SpriteKitにおける階層関係

- UIResponder
    - UIView(macOS以外), NSView(macOSのみ)
        - SpriteView(SKView)
            - SKScene
                - SKSpriteNode

### 各階層の概要

#### SKScene
##### クラスメソッド
###### sceneDidLoad()
> Tells you when the scene is presented.

> This method is intended to be overridden in a subclass. It is the preferred location to perform custom setup after the scene has been initialized or decoded.

シーンの表示を通知。
サブクラスでオーバーライドすることで、シーンの初期化・デコード時の挙動を自由に操作することができる。

###### didMove(to view: SKView)
> Tells you when the scene is presented by a view.

> This method is intended to be overridden in a subclass. You can use this method to implement any custom behavior for your scene when it is about to be presented by a view. For example, you might use this method to create the scene’s contents.


プロパティ:
- view: シーンを表示するビュー

ビューによるシーンの表示を通知。
サブクラスでオーバーライドすることで、シーンの挙動を自由に操作することができる。関数内で`Node`を作成することができる。

###### touchesBegan(_:with:)
> Tells this object that one or more new touches occurred in a view or window.

> UIKit calls this method when a new touch is detected in a view or window. Many UIKit classes override this method and use it to handle the corresponding touch events. The default implementation of this method forwards the message up the responder chain. When creating your own subclasses, call super to forward any events that you do not handle yourself.

ビューやウィンドウでのタッチをオブジェクトに通知。
UIKitでは、ユーザによるタップが検出された場合にこのメソッドを呼び出します。
オーバーライドすることで、タップイベントを操作することができます。
デフォルト動作は、`Responder Chain`(UIResponderオブジェクト間のリンク)に通知。

パラメータ:
- touches:
> A set of UITouch instances that represent the touches for the starting phase of the event, which is represented by event. For touches in a view, this set contains only one touch by default. To receive multiple touches, you must set the view's isMultipleTouchEnabled property to true.

`UITouch`インスタンス。
ビューにおけるタップの場合、デフォルトでは「単一タップ」のみが対象となる。
なお、「複数タップ」を受け取る場合はビューの`isMultipleTouchEnabled`プロパティを`true`にする必要がある。

- event:
> The event to which the touches belong.

`touches`が属するイベント。

***

## SpriteKitとSwiftUI

[参考](https://www.hackingwithswift.com/quick-start/swiftui/how-to-integrate-spritekit-using-spriteview)

SpriteKitは`UIKit`フレームワークを使用しています。
そのため、SwiftUIではなく、UIKitに沿った構造にしなければなりません。

***

## 物理エンジンの実装
[Apple公式ドキュメント](https://developer.apple.com/documentation/spritekit/getting_started_with_physics)

物理法則をアプリに導入するには、以下の条件を満たす必要があります。
> - Attach physics bodies to nodes in the node tree and configure their physical properties. See SKPhysicsBody.
> - Define global characteristics of the scene’s physics simulation, such as gravity. See SKPhysicsWorld.
> - Where necessary to support your gameplay, set the velocity of physics bodies in the scene or apply forces or impulses to them. See SKFieldNode and SKPhysicsBody.
> - Decide whether physics bodies in the scene should be connected with each other. See SKPhysicsJoint and the pinned property on SKPhysicsBody.
> - Define how the physics bodies in the scene interact when they come in contact with each other. See SKPhysicsContactDelegate.
> - Optimize your physics simulation to limit the number of calculations it must perform.

- `Node`に対して`SKPhysicsBody`を実装し、物理プロパティを設定
- シーンに対して物理シミュレーションのグローバル特性を定義
- `SKSpriteNode`に準拠する`SKFieldNode`、`NSObject`に準拠する`SKPhysicsBody`に対して速度を設定
- `SKPhysicsJoint`や`SKPhysicsBody`の`pinned`プロパティを設定し、`SKPhysicsBody`同士が相互に働き合うようにする
- `SKPhysicsContactDelegate`を設定し、衝突時の相互作用を定義
- 物理シミュレーションを最適化し、処理する計算量を調整

### 各ノードの説明
#### SKPhysicsBody

```swift:定義
class SKphysicsBody: NSObject
```

> An SKPhysicsBody object defines the shape and simulation parameters for a physics body in the system. When the scene simulates physics, it performs the calculations for all physics bodies connected to the scene tree. So, you create an SKPhysicsBody object, configure its properties, and then assign it to a node’s physicsBody property.
> When a scene processes a new frame, it performs physics calculations on physics bodies attached to nodes in the scene. After the scene completes these calculations, it updates the positions and orientations of the node objects.

`SKPhysicsBody`オブジェクトを`SKSpriteNode`の`physicsBody`プロパティに割り当てることで、ノードに対して物理シミュレーションを追加することができます。
`SKPhysicsBody`オブジェクトは、物理ボディに対して物体形状やシミュレーションにおけるパラメータを定義します。
実装する場合は、`SKPhysicsBody`オブジェクトを生成し、そのプロパティを設定してから`SKSpriteNode`の`physicsBody`プロパティに割り当てましょう。
なお、シーンがフレームを処理する際に、物理ボディに対して計算処理を行い、計算を終えると、ノードの位置や角度を更新します。

##### 物体形状

物体形状には、以下の3種類があります。
> - A dynamic volume simulates a physical object with volume and mass that can be affected by forces and collisions in the system. Use dynamic volumes to represent items in the scene that need to move around and collide with each other.
> - A static volume is similar to a dynamic volume, but its velocity is ignored and it is unaffected by forces or collisions. However, because it still has volume, other objects can bounce off it or interact with it. Use static volumes to represent items that take up space in the scene, but that should not be moved by the simulation. For example, you might use static volumes to represent the walls of a maze. While it is useful to think of static and dynamic volumes as distinct entities, in practice these are two different modes you can apply to any volume-based physics body. This can be useful because you can selectively enable or disable effects for a body.
> - An edge is a static volume-less body. Edges are never moved by the simulation and their mass doesn’t matter. Edges are used to represent negative space within a scene (such as a hollow spot inside another entity) or an uncrossable, invisibly thin boundary. For example, edges are frequently used to represent the boundaries of your scene. The main difference between an edge and a volume is that an edge permits movement inside its own boundaries, while a volume is considered a solid object. If edges are moved through other means, they only interact with volumes, not with other edges.

- `動的ボリュームベース`は、物体の体積と質量がシミュレーションに影響します。移動や衝突を伴う物体に対して適用します。
- `静的ボリュームベース`は、物体が速度をもたず、力や衝突による影響を受けません。ただし、体積をもっているため、他の物体が当たって跳ね返ったり相互作用を及ぼす可能性があります。
- `エッジベース`は静的ボリュームがありません。シミュレーションによって物体が移動することはなく、質量も関係しません。ボリュームベースとは異なり、固形オブジェクトではありません。また、エッジベースの物体はエッジの領域内でのみ移動を許可しています。

###### 物体形状の設定

![volume-based_body.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/892b2960-16fb-cf1f-1944-e5a569233856.png)

物体形状を定義するコードは、以下の通りです。

```swift:形状定義
let spaceShipTexture = SKTexture(imageNamed: "spaceShip.png")
   
// Spaceship 1: circular physics body
let circularSpaceShip = SKSpriteNode(texture: spaceShipTexture)
circularSpaceShip.physicsBody = SKPhysicsBody(circleOfRadius: max(circularSpaceShip.size.width / 2,
                                                                  circularSpaceShip.size.height / 2))
   
// Spaceship 2: rectangular physics body
let rectangularSpaceShip = SKSpriteNode(texture: spaceShipTexture)
rectangularSpaceShip.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: circularSpaceShip.size.width,
                                                                     height: circularSpaceShip.size.height))
   
// Spaceship 3: polygonal physics body
let polygonalSpaceShip = SKSpriteNode(texture: spaceShipTexture)
let path = CGMutablePath()
path.addLines(between: [CGPoint(x: -5, y: 37), CGPoint(x: 5, y: 37), CGPoint(x: 10, y: 20),
                        CGPoint(x: 56, y: -5), CGPoint(x: 37, y: -35), CGPoint(x: 15, y: -30),
                        CGPoint(x: 12, y: -37), CGPoint(x: -12, y: -37), CGPoint(x: -15, y: -30),
                        CGPoint(x: -37, y: -35), CGPoint(x: -56, y: -5), CGPoint(x: -10, y: 20),
                        CGPoint(x: -5, y: 37)])
path.closeSubpath()
polygonalSpaceShip.physicsBody = SKPhysicsBody(polygonFrom: path)
  
// Spaceship 4: physics body using texture’s alpha channel
let texturedSpaceShip = SKSpriteNode(texture: spaceShipTexture)
texturedSpaceShip.physicsBody = SKPhysicsBody(texture: spaceShipTexture,
                                              size: CGSize(width: circularSpaceShip.size.width,
                                                           height: circularSpaceShip.size.height))
```

物体の形状はパフォーマンスに影響を及ぼします。円形に近ければ近いほど物体は高速に移動し続けることができます。一方、長方形や多角形は、衝突精度を向上させることができます。

##### 物理ボディの作成と割り当て

物理ボディを作成し、`SKPhysicsBody`に割り当て、シーンに追加するコードは以下の通りです。

```swift:物理ボディの作成
// Create the rocket ship node and physics body
let spriteNode = SKSpriteNode(imageNamed: "rocketShip")
spriteNode.position = CGPoint(x: 320, y: 320)
spriteNode.physicsBody = SKPhysicsBody(texture: spriteNode.texture!,
                                       size: spriteNode.texture!.size())
spriteNode.physicsBody?.usesPreciseCollisionDetection = true
     
// Create the ground node and physics body
var splinePoints = [CGPoint(x: 0, y: 500),
                    CGPoint(x: 100, y: 50),
                    CGPoint(x: 400, y: 110),
                    CGPoint(x: 640, y: 20)]
let ground = SKShapeNode(splinePoints: &splinePoints,
                         count: splinePoints.count)
ground.lineWidth = 5
ground.physicsBody = SKPhysicsBody(edgeChainFrom: ground.path!)
ground.physicsBody?.restitution = 0.75
ground.physicsBody?.isDynamic = false
     
// Add the two nodes to the scene
scene.addChild(spriteNode)
scene.addChild(ground)
```

***

## SKFieldNode

SKFieldNodeの全クラスメソッドについては以下を参照してください。
[Apple公式ドキュメント](https://developer.apple.com/documentation/spritekit/skfieldnode)

```swift:宣言
class SKFieldNode: SKNode
```

`SKFieldNode`は、付近のノードに対して物理法則を適用するノードです。

ただし、物理法則をノードに対して適用する場合、以下の条件を満たしている必要があります。
- `SKFieldNode`が`SKScene`のノードツリーに存在している
- `SKFieldNode`の`isEnabled`プロパティが`true`である
- `SKPhysicsBody?`が`SKScene`のノードツリー上のノードに付与されている
- `SKPhysicsBody?`が`SKFieldNode`の領域内に存在している
- `SKPhysicsBody?`が`Exclusive`プロパティが`true`に設定されている他の`SKFieldNode`の領域内に存在していない
- `SKFieldNode`の`categoryBitMask`値と`fieldBitMask`値の論理AND演算が0でない

### Linear Gravity Field

[参考](https://hajihaji-lemon.com/swift/spritekit_linear_gravity_field/)

`Linear Gravity Field`は、`SKFieldNode`に線形重力場(=直線方向の加速度)を与えます。

#### プロパティ
Name: ノード名
Parent: 親ノード
Position: 2次元座標位置
Z Position: z座標位置
Strength: 加速度
Falloff: 加速度の減衰率
計算式は以下の通り。(d = ノード間距離, minR = Min Radius, f = falloff)

```math
(d - minR)^{-f}
```

Anim. Speed: 音・乱気流の変化速度
Min Radius: ノード間の最小距離(設定値より短い距離にあるノードは全て同距離とみなされる)
Category Mask: カテゴリマスク(ノード間のCategory Mask値のAND演算が0になるまで加速度が適用される)
Enabled: チェックが入っていれば物理法則が適用