[Swift] SwiftUI + Particle でリッチな演出を作る - 雨編

## 本記事では

- 通常の UI では実現できない **リッチな演出** を作りたい
- **Particle** を使ってみたい、学んでみたい

という人向けに、Particle を身近に感じてもらいたいと思っています。
また UIKit ではなく **SwiftUI** を利用することで、モダンで高速な開発を目指します！

### 環境
- macOS 12.2
- Xcode 13.3.1

## Apple の天気アプリ

Apple の天気アプリを使ったことがあるでしょうか？雨が降っている日には、アプリ内でも雨が降っており、こういう体験良いな〜と思った人もいるかと思います。この雨も Particle で再現可能な演出です。

そもそも **Particle** とは、**無数の同一な画像を使って、それらに様々なランダム性を付与して放出する** 技術です。かなり砕けた表現ですが、本記事ではこのくらいの理解で十分でしょう。雨に準えて表現すると、**単一の雨粒画像を使って、その画像の大きさや速度、色にランダム性を付与し、上から下に放出する** というイメージですね！

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402114/de3e6ff4-1600-98a9-976b-9fbc5dbbbee2.gif" width="250">

## Particle で雨を作ってみる

まずは Particle に慣れてもらうことが大切なので、Apple の天気アプリのような **雨** を作ってみましょう！

### 1. テンプレートの雨を表示する

Xcode を起動し、新規プロジェクトを作成し、`File > New > File...` から `SpriteKit Particle File` を選択しましょう。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402114/eb6f164b-369b-0d2e-f3cb-e8b80863b873.png" width="500">

`Particle template` の `Rain` を選択しましょう。これは Particle の基礎となるテンプレートになります。一から Particle を作るよりも、作りたい演出に似ているテンプレートを利用する方が作業時間を短縮できます。`Rain` の他にも `Fire` や `Snow` などもあるので、遊んでみると楽しいと思います！

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402114/c66a91b0-ef86-0bdd-8838-2698a36e7712.png" width="500">

名前は `RainParticle` としておきましょう。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402114/8fd5593b-f965-99d6-b7d0-104b5a96e2ed.png" width="500">

`Create` を押すと、作成された `RainParticle.sks` が開かれて、雨らしき演出が見れると思います。テンプレートの存在が Particle の敷居を下げてくれているな、と感じます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402114/f37b3a66-fcb2-9163-c348-173ad9cfe02a.gif" width="700">

では、アプリ上にテンプレートの雨の演出を表示してみましょう！新しくプロジェクトを作成した場合には、`ContentView.swift` というファイルがあると思います。そちらのコードを以下のように変更してください。

```swift
import SwiftUI
import SpriteKit

struct ContentView: View {

    @State var shouldEmitRain = false

    var body: some View {
        ZStack {
            if shouldEmitRain {
                // 画面全体に雨を降らせたいので GeometryReader で画面サイズを取得する
                GeometryReader { geometry in
                    SpriteView(
                        // SafeArea 外にも雨を降らせたいので、SafeArea も含めたサイズを指定する
                        scene: self.createRainParticleScene(size: geometry.sizeWithSafeArea),
                        // 背景を透過させる場合に、設定が必要である
                        options: [.allowsTransparency]
                    ).edgesIgnoringSafeArea(.all)
                }
            }
            Button(action: { self.shouldEmitRain = true }) {
                Text("Emit Rain!")
            }
        }
    }

    private func createRainParticleScene(size: CGSize) -> SKScene {
        let emitterNode = SKEmitterNode(fileNamed: "RainParticle")!
        let scene = SKScene(size: size)
        scene.addChild(emitterNode)
        // 背景を透過させる
        scene.backgroundColor = .clear
        // 右上から雨を降らせたい
        scene.anchorPoint = .init(x: 0.7, y: 1)

        return scene
    }
}

private extension GeometryProxy {

    /// SafeArea も含めたサイズ
    /// - Note: 画面全体のサイズを取得するときに利用すること
    var sizeWithSafeArea: CGSize {
        .init(
            width: self.size.width + self.safeAreaInsets.trailing + self.safeAreaInsets.leading,
            height: self.size.height + self.safeAreaInsets.top + self.safeAreaInsets.bottom
        )
    }
}
```

そこまで難解なコードにはなっていないと思います。このように変更したら、アプリを起動してみましょう！以下のように、`Emit Rain!` をタップしたら、雨が降るようになったと思います。テンプレートを使うだけなら、一瞬で出来ちゃいますね！

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402114/c73c9e24-63fe-092c-ed84-a476985e4ba3.gif" width="250">

### 2. テンプレートの雨を改善する

テンプレートの雨を表示することはできたと思います。しかし、この雨は Apple の天気アプリで利用されている雨よりもポップな見た目になってしまっていますよね。色も青すぎる気がします...。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402114/de3e6ff4-1600-98a9-976b-9fbc5dbbbee2.gif" width="250">

ということで、実際にテンプレートの雨に改善を加えていきましょう。先ほど作成した `RainParticle.sks` を開いてみてください。その後、`Inspectors` の中の `Attributes inspector` を表示してください。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402114/3874ee24-d1c7-4ed3-4d58-932f3593e84e.png" width="700">

`SpriteKit Particle Emitter` には多くのパラメーターが用意されていることが分かると思います。試しに `Emitter` の `Birthrate` を 150 から 1000 にしてみましょう。すると、以下のように、今までよりも大量の雨粒が表示されるようになったと思います。`Birthrate` とは、画像を新たに放出する量です。このように、用意されている様々なパラメーターを変えていくことで、演出の見た目を変えることができます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402114/e3103289-5818-b0ab-6d8d-31cb25d006b4.gif" width="700">

では、どのように理想のパラメーターを決めるのか？という話になると思います。結論から言うと、色々とパラメーターを変えながら、見た目を調整するだけです。全てが手探りになります。幸いなことに `.sks` はプレビューがついているので、パラメーターを変えれば、すぐに見た目が反映されます。ここで、特に大事なパラメーターは、`Range` と書かれているパラメーターです。このパラメーターは、特定の範囲でパラメーターにランダム性を持たせられます。例えば、`Lifetime/Range` に 10 を設定すると、雨粒画像が時間経過と共に消える時間を `Lifetime/Start` の値 `±5` の範囲でランダムに設定されるようになります。自然界の演出を Particle で作る上での肝はランダム性です。人はランダムに動く物体を自然と感じます。これだけは覚えておきましょう。

以下は、私が変更した差分になります。これが答えというわけではないので、皆さんも Apple の天気アプリで利用されている雨に近づけるためには、どのようなパラメーターをどれくらい変えれば良いのか、色々と模索してみてください！

- `Emitter/Birthrate`: 150 → 400
- `Angle/Start`: 254.393 → 270
- `Angle/Range`: 1.719 → 0
- `Speed/Start`: 340 → 1000
- `Speed/Range`: 150 → 400
- `Scale/Start`: 0.1 → 0.2
- `Scale/Range`: 0.05 → 0.4
- `Scale/Speed`: 0` → -0.08
- `Color Ramp`: R:139,G:145,B:255,A:1 → R:203,G:203,B:203,A:0.5

上記の変更以外にも、`createRainParticleScene` 関数を少し修正しています。

```diff_swift
private func createRainParticleScene(size: CGSize) -> SKScene {
    let emitterNode = SKEmitterNode(fileNamed: "RainParticle")!
+   // 実際の画面の横幅の区間で、画像の放出位置がランダムになるようにする
+   emitterNode.particlePositionRange.dx = size.width
    let scene = SKScene(size: size)
    scene.addChild(emitterNode)
    // 背景を透過させる
    scene.backgroundColor = .clear
-   // 右上から雨を降らせたい
-   scene.anchorPoint = .init(x: 0.7, y: 1)
+   // 上から雨を降らせたい
+   scene.anchorPoint = .init(x: 0.5, y: 1)

    return scene
}
``` 

さらに、背景も暗めの色にしたいので、その修正もしています。

```diff_swift
if shouldEmitRain {
    // 画面全体に雨を降らせたいので GeometryReader で画面サイズを取得する
    GeometryReader { geometry in
        SpriteView(
            // SafeArea 外にも雨を降らせたいので、SafeArea も含めたサイズを指定する
            scene: self.createRainParticleScene(size: geometry.sizeWithSafeArea),
            // 背景を透過させる場合に、設定が必要である
            options: [.allowsTransparency]
        ).edgesIgnoringSafeArea(.all)
-   }
+   }.background(.gray)    
}
``` 

最後の一工夫についても説明をしたいと思います。そのためには、テンプレートの雨で利用されている画像について話さないといけません。今まで見てきた雨粒はどこの画像を使っているか疑問に思いませんでしたか？実は、雨のテンプレートを作成した際に自動的に画像が追加されています。`Particle Sprite Atlas/{bokeh,spark}` がそれにあたります。雨のテンプレートでは、`spark` が利用されるように設定されています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402114/76d4439e-25d8-ecf8-f9c3-c480b6a02c08.png" width="700">

Apple の天気アプリの雨は、テンプレートの雨よりも速度が早いです。そのため、雨自体が細長く見えています。にも関わらず、元々利用されていた `spark` を使うと、雨の速度感が伝わりません。そのため、`spark` を 1/8 程度に細長くリサイズした画像を `rain` として用意しました。この画像を利用することで、速度感のある雨を表現できるようになりました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402114/249bfc02-5189-eb4c-1df5-6968e4fc5a96.png" width="700">

以上の修正を加えた結果、完成した雨が以下になります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402114/ceefc1c7-fca0-871b-40bb-c0608c76c229.gif" width="250">

かなり Apple の天気アプリと近づいたんじゃないかなと思います。このように

1. テンプレートの Particle を追加し、表示できるようにする
1. Particle を改善する

という手順で実装を進めることで、リッチな演出を作ることができます。

## 終わりに

今回は雨を題材に Particle を紹介しましたが、他にも多くの演出を Particle で実現することができます。テンプレートに存在している **炎** や **雪** を始めとして、**紙吹雪** などの人工物も作ることができます。以下は、雪をテンプレートとして紙吹雪の演出を作成したものになります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402114/cb930d51-588f-2204-c5f7-46b3491258e8.gif" width="250">

今回の記事で Particle に興味を持ってもらえたら幸いです。では、また次の記事で！