UnityでAndroid端末の回転ベクトルを取得する

## 概要

Android端末の複数センサを用いて算出された回転ベクトルをUnityから取得する方法についてまとめます．

## やりたいこと

Android OS は複数センサの情報を複合した回転ベクトルを算出できるので，Unityからその情報を取得したい．

## 解決手法
UnityからAndroidの回転ベクトルを取得するAndroid Pluginを作成．
## 環境
- Unity 2022.3.4f1
- Windows 11
- Google Pixel 3a
- Android バージョン12

## 実装
Androidプラグインの作成にはいくつかの方法がありますが，今回はJavaファイルをUnityのプロジェクト内に配置してプラグインとして読み込ませる方法をとることにしました．

プラグインの再利用や配布を考えるなら，直接Javaファイルを配置するのではなく，[Android Libraries や Android Archives (AAR) を利用した方法](https://docs.unity3d.com/ja/2022.3/Manual/android-plugin-types.html)の方が適していると思います．

### Unityプロジェクトの準備

まずunityで新規プロジェクトを立ち上げます．
新規プロジェクトが立ち上がったら，`File > Build Settings > Android`から`switch platform`を選択して，プラットフォームをAndroidに切り替えてください．

次に実機デバッグ用に **Android Logcat**という拡張機能を導入します．
Android LogcatはPCに接続されたアンドロイド端末のログをUnityの専用タブから出力してくれます．
`Window > Packagemanager`を起動して，左上の`Packages:○○`を`Packages:Unity Registry`に切り替え，右上からAndroid Logcatと検索してインストールしてください．

![スクリーンショット 2024-03-15 145507.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3212682/ece6567f-ecf1-95cf-afd9-1b808abb64ee.png)

### Javaファイルの作成
Androidプラグインとして読み込むJavaファイルを作成します．
Unityプロジェクト内に`Assets/Plugins/Android`というフォルダを作成してください(Pluginsフォルダがない場合はそれも作成)．
`Assets/Plugins/Android`に以下のような`Rotation.java`ファイルを作成します．
```Rotation.java
package Assets.Plugins.Android;

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import com.unity3d.player.UnityPlayerActivity;

public class rotation extends UnityPlayerActivity  implements SensorEventListener {
    private SensorManager sensorManager;
    private Sensor rotationSensor = null;
    private Float sensorX = 0f;
    private Float sensorY = 0f;
    private Float sensorZ = 0f;
    private Float sensorW = 1f;
    @Override
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        sensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
        rotationSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
        sensorManager.registerListener(this,rotationSensor, SensorManager.SENSOR_DELAY_NORMAL);
    }
    @Override
    public void onSensorChanged(SensorEvent event) {
        sensorX = event.values[0];
        sensorY = event.values[1];
        sensorZ = event.values[2];
        sensorW = event.values[3];
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {

    }
    public float[] getRotation(){
        return new float[]{sensorX, sensorY, sensorZ, sensorW};
    }
}

```
#### コードの解説

```java
public class rotation extends UnityPlayerActivity  implements SensorEventListener{
//省略
}
```
Androidでセンサデータを見るには`Activity`クラスと`SensorEventListenser`インターフェースを継承する必要があります．
今回はUnityが提供する`Activity`の派生クラス`UnityPlayerActivity`を継承しました．`UnityPlayerActivity`に関する詳しい説明は[こちら](https://docs.unity3d.com/ja/2022.3/Manual/AndroidUnityPlayerActivity.html)を確認してください．


```java
@Override
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        sensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
        rotationSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
        sensorManager.registerListener(this,rotationSensor, SensorManager.SENSOR_DELAY_NORMAL);
    }
```
この部分ではセンサを取得してイベントリスナに登録しています．
取得できるセンサ情報は[Android公式ドキュメント](https://source.android.com/docs/core/interaction/sensors/sensor-types?hl=ja#limited_axes_imu_sensors)から確認できます．今回は回転ベクトル`Sensor.TYPE_ROTATION_VECTOR`を取得しました．
```java
    @Override
    public void onSensorChanged(SensorEvent event) {
        sensorX = event.values[0];
        sensorY = event.values[1];
        sensorZ = event.values[2];
        sensorW = event.values[3];
    }
```
`onSensorChanged`は新しい`SensorEvent`が発生するたびに呼び出されます．
eventからセンサの値を取得してセンサ情報を更新します．
ちなみに今回取得する回転ベクトルはオイラー角ではなくクォータニオン形式なので，変数は4つです．
```java
public float[] getRotation(){
        return new float[]{sensorX, sensorY, sensorZ, sensorW};
    }
```
Unityから利用する関数です．回転ベクトルの配列を返します．
### C#ファイルの作成
Androidプラグインを呼び出すC#のプログラムを書きます．
Unityの`Assets`フォルダ内に`Scripts`フォルダを作成し，その中に`RotationManager.cs`ファイルを作成します．
```RotationManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RotationManager : MonoBehaviour
{
    private AndroidJavaObject activity;
    // Start is called before the first frame update
    void Start()
    {
        AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
        activity  = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
    }

    // Update is called once per frame
    void Update()
    {
        var rot = activity.Call<float[]>("getRotation");
        Debug.Log(new Quaternion(rot[0],rot[1],rot[2],rot[3]));

    }
}

```
#### コードの解説
```cs
void Start()
    {
        AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
        activity  = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
    }
```
C#からのJavaやKotlinのプラグインの呼び出しには`AndroidJavaObject` と `AndroidJavaClass`を使用します．
上記のコードでは`unityPlayer.GetStatic`を用いて，`com.unity3d.player.UnityPlayer`クラスの静的なフィールド`currentActivity`を取得しています．
後述するエントリポイントの設定により，`currentActivity`には`Rotation.java`で作成した`rotation`クラスのインスタンスが登録されています．

```cs
void Update()
    {
        var rot = activity.Call<float[]>("getRotation");
        Debug.Log(new Quaternion(rot[0],rot[1],rot[2],rot[3]));
    }
```
毎フレーム回転ベクトルを出力するコードです．
`activity`には`rotation`のオブジェクトが格納されているので，`activity.Call<float[]>("getRotation")`で`getRotation`メソッドを実行し，回転ベクトルを取得します．

### カスタムマニフェストの作成
カスタムマニフェストを作成し，プラグインで作成したアクティビティをアプリケーションのエントリポイントに設定します．
`Edit > Project Settings`より`Project Settings`ウィンドウを起動して，`Player`からAndroidのマークを選び，`Publishing Settings > Build > Custom Main Manifest`にチェックを入れて有効化してください．
![スクリーンショット 2024-03-15 143656.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3212682/c6a69827-99d4-678e-7fc2-561ea1620b77.png)
`Assets/Plugins/Android`に`AndroidManifest.xml`というファイルが生成されるため，`AndroidManifest.xml`を以下のように書き換えてエントリポイントを変更します．
```diff_xml
<?xml version="1.0" encoding="utf-8"?>
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.unity3d.player"
    xmlns:tools="http://schemas.android.com/tools">
    <application>
-       <activity android:name="com.unity3d.player.UnityPlayerActivity"
+       <activity android:name="Assets.Plugins.Android.rotation"
                  android:theme="@style/UnityThemeSelector">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <meta-data android:name="unityplayer.UnityActivity" android:value="true" />
        </activity>
    </application>
</manifest>

```
### アプリケーションのビルドと動作テスト
UnityエディタのHierarchyウィンドウにEmptyオブジェクトを追加し，作成したC#スクリプトをアタッチします．
Android端末をPCに接続し，`File > Build and Run`からビルドを実行します．
ビルドが成功すると，Android端末でアプリケーションが起動してUnityエディタにAndroid Logcatのウィンドウが立ち上がります．
以下の画像ようにクォータニオンが出力されたら成功です．
![スクリーンショット 2024-03-15 185645.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3212682/388fd16c-1764-acff-ce9b-736c7945e36b.png)
## 参考
- [パソコン甲子園 センサーを使う](https://sites.google.com/view/pckmobile/kotlin%E3%81%AB%E3%82%88%E3%82%8Bandroid%E8%AC%9B%E5%BA%A7/%E3%82%BB%E3%83%B3%E3%82%B5%E3%83%BC%E3%82%92%E4%BD%BF%E3%81%86)
- [Unityマニュアル Androidプラグインタイプ](https://docs.unity3d.com/ja/2022.3/Manual/android-plugin-types.html)
- [Unityマニュアル Unity のデフォルトのアクティビティを拡張する](https://docs.unity3d.com/ja/2022.3/Manual/AndroidUnityPlayerActivity.html)
- [Unityマニュアル C# スクリプトから Java や Kotlin のプラグインコードを呼び出す](https://docs.unity3d.com/ja/2023.2/Manual/android-plugins-java-code-from-c-sharp.html)
- [Android オープンソースプロジェクト センサータイプ](https://source.android.com/docs/core/interaction/sensors/sensor-types?hl=ja#limited_axes_imu_sensors)
- [[Unity] Android NativePluginの実装の仕方
](https://qiita.com/atsutama/items/28744b1a29e79184c3fc)