【Flutter × Unity】UnityのSceneをFlutterで制御する

FlutterにUnityを組み込むことができる「flutter_unity_widget」
今回はこのプラグインを用いて、UnityのScene切替をFlutterで制御する方法を紹介します。

# はじめに
### 前提条件
- **flutter_unity_widget** プラグインを使用したことがある

https://github.com/juicycleff/flutter-unity-view-widget?tab=readme-ov-file

:::note warn
注意
本稿では導入に際する詳細な手順（UnityプロジェクトのセットアップやRunnerの設定など）については省略をしています。
:::

# 開発環境
```
macOS: 14.1.2
iOS: 17.3
Dart: 3.3.0
Flutter: 3.19.0
Unity: 2022.3.15f1
Vuforia: 10.21.3
VS Code: 1.84.2
XCode: 15.1 (15C65)
```

# Unity側の設定
`Assets` > `Scene`に「Scene1」「Scene2」を追加します。
それぞれに任意の3D Objectを追加して区別できるようにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3716019/228d4f68-f9fd-11c2-0ed0-298c96c49581.png)

3D Objectの作成後、`Main Camera`を選択し、`Add Component` > `New script`より「setScene1」というスクリプトを作成してください。

`Edit script`よりスクリプトを以下のように編集します。

```setScene1.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class SetScene1 : MonoBehaviour
{
    void setScene(string sceneName)
    {
        SceneManager.LoadScene(sceneName); //シーンの切替
    }
}
```

次に「⇧⌘B」を押下して`Build Settings`を起動します。
`Add Open Scenes`を選択し、作成したSceneを`Scenes In Build`に追加してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3716019/1db1b8f4-0b11-a7c5-3492-07ee1b9dfa43.png)

Scene2についても同様です。
2つのSceneの設定が完了すれば`Flutter` > `Export iOS (Debug)`よりエクスポートしてください。

# Flutter側の設定
lib直下に`scene1.dart`と`scene2.dart`というファイルを作成します。

```
-lib
    -main.dart
    -scene1.dart
    -scene2.dart
```

それぞれのファイルを以下のように書き換えます。

### main.dartのソースコード

```main.dart
import 'package:flutter/material.dart';
import 'scene1.dart';
import 'scene2.dart';

void main() => runApp(const MyApp());

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: Home(),
    );
  }
}

class Home extends StatelessWidget {
  const Home({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Unity Flutter Demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(builder: (context) => const Scene1()),
                );
              },
              child: const Text('Move Scene1'),
            ),
            const SizedBox(height: 32),
            ElevatedButton(
              onPressed: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(builder: (context) => const Scene2()),
                );
              },
              child: const Text('Move Scene2'),
            )
          ],
        ),
      ),
    );
  }
}
```

### scene1.dartのソースコード
```scene1.dart
import 'package:flutter/material.dart';
import 'package:flutter_unity_widget/flutter_unity_widget.dart';

class Scene1 extends StatefulWidget {
  const Scene1({super.key});

  @override
  State<Scene1> createState() => _Scene1State();
}

class _Scene1State extends State<Scene1> {
  UnityWidgetController? _unityWidgetController;

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Scene1'),
      ),
      body: Card(
        margin: const EdgeInsets.all(8),
        clipBehavior: Clip.antiAlias,
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(20.0),
        ),
        child: UnityWidget(
          onUnityCreated: onUnityCreated,
          fullscreen: false,
        ),
      ),
    );
  }

  // Callback that connects the created controller to the unity controller
  void onUnityCreated(controller) {
    controller.resume();
    _unityWidgetController = controller;
    _unityWidgetController?.postMessage("Main Camera", "setScene", "Scene1");
  }
}
```

### scene2.dartのソースコード
```scene2.dart
import 'package:flutter/material.dart';
import 'package:flutter_unity_widget/flutter_unity_widget.dart';

class Scene2 extends StatefulWidget {
  const Scene2({super.key});

  @override
  State<Scene2> createState() => _Scene2State();
}

class _Scene2State extends State<Scene2> {
  UnityWidgetController? _unityWidgetController;

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Scene2'),
      ),
      body: Card(
        margin: const EdgeInsets.all(8),
        clipBehavior: Clip.antiAlias,
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(20.0),
        ),
        child: UnityWidget(
          onUnityCreated: onUnityCreated,
          fullscreen: false,
        ),
      ),
    );
  }

  // Callback that connects the created controller to the unity controller
  void onUnityCreated(controller) {
    controller.resume();
    _unityWidgetController = controller;
    _unityWidgetController?.postMessage("Main Camera", "setScene", "Scene2");
  }
}
```

ビルドに成功すると、以下のようにボタンに応じたSceneが表示されます。

<iframe width="493" height="877" src="https://www.youtube.com/embed/ll4N05mT1NE" title="Unity Flutter Demo" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

# 解説
UnityのSceneをFlutterで制御するために、UnityとFlutter間で通信を行う必要があります。

### scene1.dartの解説
`UnityWidgetController`で以下のように定義した値を、UnityWidget内の`onUnityCreated`に渡すことでUnityに値を送ることができます。
```dart
postMessage("Main Camera", "setScene", "Scene1");
```

> 第1引数：メッセージを送りたいコンポーネントがアタッチされているGameObject名
> 第2引数：呼び出したいメソッド名
> 第3引数：送りたい値の情報

### setScene1.csの解説
`postMessage`で定義した`setScene`メソッドを記述しています。
引数を`string sceneName`とすることで、`Scene1`という値をFlutterから受け取ることができます。
```setScene1.cs
void setScene(string sceneName)
{
    SceneManager.LoadScene(sceneName);
}
```

# おわりに
今回はUnityのSceneをFlutterで制御する方法を紹介しました。
UnityをFlutterに導入する手順を割愛してしまいましたが、今後その解説もできたらいいなと思います。