Flutterを使ってキャラ操作アプリを作った話

# 背景
スマホを使ってロボットを簡単に操作できたらいいなぁ　と思いついたのですが，
AndroidとかiPhoneとかいろいろ対応するの大変そうだなぁ　と探していたところ，こちらのツールを発見

https://flutter.dev/

要はAndroidでもiOSでもmacOSでもWindowsでも動くようなアプリを一度に作れてしまうというツヨツヨなツールです
今回はこれを使ってVirtual Joystickでキャラクターを操作するアプリを作ってみました！

なおこちらの記事は，Flutterに関する基礎的な知識を有した方向けです！
環境構築やFlutterの使い方に関しては，[公式の記事](https://docs.flutter.dev/get-started/install)や[チュートリアル](https://codelabs.developers.google.com/codelabs/flutter-codelab-first?hl=ja#0)をご確認ください！

# 環境
- OS: Windows 11
- [Flutter](https://flutter.dev/): 
    ```
    $ flutter --version
    Flutter 3.10.5 • channel stable • https://github.com/flutter/flutter.git
    Framework • revision 796c8ef792 (11 days ago) • 2023-06-13 15:51:02 -0700
    Engine • revision 45f6e00911
    Tools • Dart 3.0.5 • DevTools 2.23.1
    ```
- [Andoroid Studio](https://developer.android.com/studio): Flamingo | 2022.2.1
- Java: [java17 for Windows](https://www.oracle.com/jp/java/technologies/downloads/#jdk17-windows)
- [Visual Studio Code](https://code.visualstudio.com/)　いわずと知れたエディタ Flutterの拡張機能がかなり充実いていてとても使いやすいです！
- Application: for Windows　今回はWindows用のアプリを作る手順で説明していきます！


# プロジェクト作成
まずはVSCodeを開き新しいFlutterプロジェクトを作成します.
`Ctrl`+`Shift`+`P`でコマンドパレットを開き，`Flutter`と検索すると`New Project`の欄が出てきます．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633039/0f0402a3-586e-c8c4-55b3-cc9a2c4415ae.png)

これが出ない方はVSCodeのFlutter環境構築ができていないので，以下の記事を参考に環境を作ってみてください！
https://docs.flutter.dev/get-started/editor

次にApplicationを選択し，
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633039/599bc1d6-1e07-5839-cb0e-6719397e7971.png)
アプリケーションを作成するフォルダパスを指定します．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633039/5d73274e-b1d9-0078-09a9-1eb5645313a8.png)
最後にアプリケーションの名前を設定すると，
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633039/69234f99-b43b-399a-69d4-4f2111afe947.png)

このような形でプロジェクトが自動生成されます　とても便利で最初はかなり驚きました!
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633039/cf2ef17f-bb0e-820f-7e02-491af8cd818d.png)

# アプリ設定
[こちら](https://codelabs.developers.google.com/codelabs/flutter-codelab-first?hl=ja#2)のサイトで使った設定ファイルをほとんどそのまま用います．
プロジェクトフォルダ直下の，`pubspec.yaml`と`analysis_options.yaml`を以下のように編集します．
```pubspec.yaml
name: vitrual_controller
description: A new Flutter project.

publish_to: 'none' # Remove this line if you wish to publish to pub.dev

version: 0.0.1+1

environment:
  sdk: '>=2.19.4 <4.0.0'

dependencies:
  flutter:
    sdk: flutter

  english_words: ^4.0.0
  provider: ^6.0.0

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^2.0.0

flutter:
  uses-material-design: true
```
```analysis_options.yaml
include: package:flutter_lints/flutter.yaml

linter:
  rules:
    prefer_const_constructors: false
    prefer_final_fields: false
    use_key_in_widget_constructors: false
    prefer_const_literals_to_create_immutables: false
    prefer_const_constructors_in_immutables: false
    avoid_print: false
```

# Virtual Joystickパッケージのインストール
今回は以下のツールを用います．

https://pub.dev/packages/flutter_joystick

まずはインストール
VSCodeで`Ctrl`+`@`でターミナルを開き，以下を実行します
```
$ flutter pub add flutter_joystick
```
これでVirtualJoystickを用いてアプリケーションを作れるようになります．

# アプリ実装
## まずは実行
では実際にアプリを作ります！`main.dart`に以下のコードを張り付けてみてください！

```main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_joystick/flutter_joystick.dart';
import 'dart:math';

void main() {
  runApp(MyApp());
}

class MyAppState extends ChangeNotifier {
}

const ballSize = 20.0;
const transition_step = 10.0;
const rotation_step = 0.1;

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

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => MyAppState(),
      child: MaterialApp(
        title: 'Virtual Controller App',
        theme: ThemeData(
          useMaterial3: true,
          colorScheme: ColorScheme.fromSeed(seedColor: Color.fromARGB(255, 13, 0, 255)),
        ),
        darkTheme: ThemeData.dark(),
        themeMode: ThemeMode.system, 
        home: MyHomePage(),
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  double _x = 500;
  double _y = 250;
  double _yaw = 0;

  JoystickMode _joystickMode = JoystickMode.all;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          Align(
            alignment: const Alignment(-0.8, 0.8),
            child: Joystick(
              mode: _joystickMode,
              listener: (details) {
                setState(() {
                  _x = _x - transition_step * details.y * sin(_yaw) + transition_step * details.x * cos(_yaw);
                  _y = _y + transition_step * details.y * cos(_yaw) + transition_step * details.x * sin(_yaw);
                });
              },
            ),
          ),
          Align(
            alignment: const Alignment(0.8, 0.8),
            child: Joystick(
              mode: _joystickMode,
              listener: (details) {
                setState(() {
                  _yaw = _yaw +  rotation_step * details.x;
                });
              },
            ),
          ),
          Transform.translate(
            offset: Offset(_x, _y),
            child: Transform.rotate(
              angle: _yaw,
              child: Icon(
                Icons.flutter_dash,
                color: Theme.of(context).colorScheme.primary,
                size: 200,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

```

いったん動かしてみます！
ビルドがWindowsになっているのを確認して，（自分が試したいデバイスで問題ないです！）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633039/12182945-c664-eadc-c796-d05941943c64.png)
右上の再生ボタンを押します．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633039/f8ab7ca2-8fbe-229b-39a1-90593fac8ddd.png)

するとこんな感じのアプリが立ち上がります！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633039/ad9086d6-94bf-586c-00f5-9b00b724ddbe.png)
左でキャラクターの平行移動，右で回転が制御できます．実際に動かしてみてください！
動画だとこんな感じ

https://drive.google.com/file/d/1MhC0gdYj06HH8c3PdYM9mmrCHD8F5TBQ/view?usp=sharing

## コード解説
ココからはコードの解説をしていきます！　基本的なところは飛ばして，JoyStickの配置やキャラクター動作の実装のあたりを中心に解説していきます!

まずは44~48行目でキャラクターの位置・姿勢，JoyStickのモードを初期化します．
```main.dart
// lines 44-48
  double _x = 0;
  double _y = 500;
  double _yaw = 0;

  JoystickMode _joystickMode = JoystickMode.all;
```

画面配置とキャラ移動は５１行目以降で実装されています
```main.dart
// lines 51-
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          Align(
            alignment: const Alignment(-0.8, 0.8),
            child: Joystick(
              mode: _joystickMode,
              listener: (details) {
                setState(() {
                  _x = _x - transition_step * details.y * sin(_yaw) + transition_step * details.x * cos(_yaw);
                  _y = _y + transition_step * details.y * cos(_yaw) + transition_step * details.x * sin(_yaw);
                });
              },
            ),
          ),
          Align(
            alignment: const Alignment(0.8, 0.8),
            child: Joystick(
              mode: _joystickMode,
              listener: (details) {
                setState(() {
                  _yaw = _yaw +  rotation_step * details.x;
                });
              },
            ),
          ),
          Transform.translate(
            offset: Offset(_x, _y),
            child: Transform.rotate(
              angle: _yaw,
              child: Icon(
                Icons.flutter_dash,
                color: Theme.of(context).colorScheme.primary,
                size: 200,
              ),
            ),
          ),
        ],
      ),
    );
  }
```
それぞれのボタンの役割とAPIを以下に示します
- `Scaffold`: アプリの表示レイアウト全体を示す．https://api.flutter.dev/flutter/material/Scaffold-class.html
- `Stack`: `Widget`を重ねて表示できる`Widget`．https://flutter.ctrnost.com/basic/layout/stack/
- `Align`: `Widget`の表示位置を相対的に調整できる`Widget`．https://api.flutter.dev/flutter/widgets/Align-class.html
- `Joystick`: Virtual Joystickの`Widget`．https://pub.dev/packages/flutter_joystick
- `Transform`: `Widget`などの表示位置や表示角度を設定できる`Widget`．https://api.flutter.dev/flutter/widgets/Transform-class.html

`Joystick`ウィジェット内の`listener`パラメータ内で，`setState()`関数を用いてJoyStickの状態から位置と角度を更新します．この辺りはロボティクスにも通じそう．
位置の更新は以下
```main.dart
// lines 60-63
                setState(() {
                  _x = _x - transition_step * details.y * sin(_yaw) + transition_step * details.x * cos(_yaw);
                  _y = _y + transition_step * details.y * cos(_yaw) + transition_step * details.x * sin(_yaw);
                });
```
yaw角の更新は以下
```main.dart
// lines 72-74
                setState(() {
                  _yaw = _yaw +  rotation_step * details.x;
                });
```

更新された位置・姿勢が，`Transform`ウィジェットの`translate`と`rotation`メソッドを用いて，リアルタイムでキャラクターに設定されます．
```main.dart
//lines 78-88
         Transform.translate(
            offset: Offset(_x, _y),
            child: Transform.rotate(
              angle: _yaw,
              child: Icon(
                Icons.flutter_dash,
                color: Theme.of(context).colorScheme.primary,
                size: 200,
              ),
            ),
          ),
```

# 今後の展望
これをロボットを動かすためのアプリケーションにしたい...。



# 余談
FlutterではGoogleさんが作った`Dart`という言語を使ってアプリケーションを作成します．
おそらく見慣れない方が多いはずです（私も完全に初心者です）．ですが，ある程度`Java`に理解があればその考え方を踏襲しているらしいのでなんとなくわかるみたいです．

ちなみに私の場合はJavaもわからないので初めは全くピンと来ていませんでした．しかし，DocumentやTutorial，VSCodeの拡張機能がかなりしっかりしていて，こんな私でも3日くらいで簡単にアプリを作れるようになるくらいにはなれました．おすすめです．

今回はこれで以上です！　最後まで読んでいただきありがとうございました！