Godot4.0で重力センサーでボールを転がしてみた（Androidスマホ実行限定）

　ゲームエンジンGodot4.0で3Dスマホゲームを作りたいと思いますが、その前にお勉強しています。
　2023/4/4リリースのGodot_v4.0.2-stable_win64を使用しています。

# 目的
　下記の記事でAndroidスマホのジャイロセンサーを使用して床を傾けてボールを転がす処理を実装しましたが今回は重力センサーを使って同じことをしたいと思います。
　下記の記事で作成したプロジェクトをベースに、GDScriptのみを修正します。

https://qiita.com/FootInGlow/items/186f7cabd5137135db86

ベースプロジェクトとするgithub

https://github.com/footinglow/Godot4/tree/main/03_study/GyroscopeMoveFloor
　
# ジャイロセンサーと重力センサーの違いについて
　ジャイロセンサーは、x,y,z 3軸の回転速度をrad/sで出力するので、出力をそのまま(delta時間をかけて）rotate_x/y/zメソッドに渡すとノードが回転します。現在のノードの姿勢は回転速度を積算したものになります。
　それに対して、重力センサーは重力加速度を軸毎に取得します。重力加速度x,y,zをベクトルに見立てて軸の回転角度を計算したものが、現在の姿勢になります。

# 座標系のおさらい
　今回スマホを下図のように水平に置いて使います。
　重力センサーの出力はジャイロセンサー同様スマホデバイスの座標系です。スマホデバイスの座標系は下図のX/Y/Zdeviceで表しています。
　Godotの座標系はカメラの設置位置により決定します。X/Y/Zgodotで表しています。
　スマホ前面をＹ軸プラスになるようにカメラを設置していて、画面下方向がＺ軸プラス、左右はデバイスもGodot座標系も同じ右方向がプラスになります。
![スクリーンショット (743).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/e0b1a24c-c4d8-d78c-bd45-b3421f2ea136.png)


# デバイスの重力加速度からGodot座標系のX軸の回転量を求める
　デバイスを奥・手前に傾けた場合はデバイスもGodot座標系もX軸が回転します。
　下図はスマホを平面に置いて、側面から見た図です。
![スクリーンショット (748).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/92361501-c226-18f2-e0e6-2a7cfc2fc9f2.png)
　デバイスのX軸の傾きは重力センサのY,Zの重力加速度の大きさからatan2で求めます。
　atan2で求めた角度はスマホを水平に置いた場合（左側の図）、-90°になります。
　スマホを右図のように手前に起こすと、atan2は-100°、-110°とマイナスになります。その時の床の角度は水平からプラス方向に回転した角度にしたいので、床のX軸の回転角度＝-atan2-90°になります。

# デバイスの重力加速度からGodot座標系のZ軸の回転量を求める　
　デバイスを左右に傾けた場合はデバイスはｙ軸、Godot座標系はＺ軸が回転します。
　下図はスマホを平面に置いて、底面から見た図です。
![スクリーンショット (749).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/d0d65145-8952-902f-dfb0-caaaf8eabe84.png)
　考え方はX軸の回転量を求めたときと同じです。
　Z軸の回転量＝-atan2-90°です。

# スクリプト差し替えて、重力センサーで床を傾ける
　「res://world.gd」を開いて、下記のスクリプトに差し替えます。

```GDScript
extends Node3D

func _physics_process(delta):
	# 重力加速度を取得。スマホデバイス座標系
	var g : Vector3 = Input.get_gravity()

	# 現在のノードxの角度をオイラー角[rad]で取得する。インスペクタのNode3D/Transform/Rotationの値(をラジアンで表したもの）
	var v3_x_rot : Vector3 =  $x.transform.basis.get_euler()
	# 重力センサー値（加速度）をベクトルに見立てて、床の(Godot座標系の)目標角度を計算する
	var x_rad = -atan2(g.z, g.y) - PI/2
	# 目標角度と現在の角度との差分を回転するように指示する
	$x.rotate_x( x_rad - v3_x_rot.x )

	# 考え方はx軸の回転と同じ
	var v3_z_rot : Vector3 = $x/z.transform.basis.get_euler()
	var z_rad = -atan2(g.z, g.x) - PI/2	# g.z, g.xはデバイス座標系、z_radはGodot座標系
	$x/z.rotate_z( z_rad - v3_z_rot.z)
```
* **var v3_x_rot : Vector3 =  $x.transform.basis.get_euler()**
　クラスBasisのget_euler()でノードのオイラー角を取得することができます。単位はラジアンです。

# アプリ起動時のスマホの手前奥方向の傾きを初期位置にする
　スマホを水平にして操作するのはやりにくいので、アプリ起動時のスマホの奥手前方向の傾きを初期値にするようにします。

```GDScript
extends Node3D

# 重力センサの有効な初期値を保存する
var m_v3_g_init = Vector3.ZERO
var m_v3_x_init_rad = 0.0

func _physics_process(delta):
	# 重力加速度を取得。スマホデバイス座標系
	var g : Vector3 = Input.get_gravity()
	if m_v3_g_init == Vector3.ZERO and g.length() > 9.0 :
		m_v3_x_init_rad = clampf(-atan2(g.z, g.y) - PI/2, 0, PI/4)		# 水平から、手間に45°起こすくらいまででクランプする
		m_v3_g_init = g

	# 現在のノードxの角度をオイラー角[rad]で取得する。インスペクタのNode3D/Transform/Rotationの値(をラジアンで表したもの）
	var v3_x_rot : Vector3 =  $x.transform.basis.get_euler()
	# 重力センサー値（加速度）をベクトルに見立てて、床の(Godot座標系の)目標角度を計算する
	var x_rad = -atan2(g.z, g.y) - PI/2 - m_v3_x_init_rad
	# 目標角度と現在の角度との差分を回転するように指示する
	$x.rotate_x( x_rad - v3_x_rot.x )

	# 考え方はx軸の回転と同じ
	var v3_z_rot : Vector3 = $x/z.transform.basis.get_euler()
	var z_rad = -atan2(g.z, g.x) - PI/2	# g.z, g.xはデバイス座標系、z_radはGodot座標系
	$x/z.rotate_z( z_rad - v3_z_rot.z)
```

* **if m_v3_g_init == Vector3.ZERO and g.length() > 9.0 :**
　_ready()メソッド内では重力センサの有効値が取得できなかったので、_physics_processメソッドの中でg.length() > 9.0により有効値が取得できた時を初期姿勢とするようにしました。

* **m_v3_x_init_rad = clampf(-atan2(g.z, g.y) - PI/2, 0, PI/4)**
　スマホを垂直にしてdeviceのZ軸方向の重力加速度が０になると、左右方向の傾きが計算できなくなるため、水平にスマホを置いた時の角度0度から手前に45°(=π/4 ラジアン）まででクランプしています。

# 考察
　ジャイロセンサーの蓄積は時間経過とともにずれがでてきて、そのうちスマホを水平にしても床は水平には戻らなくなります。センサー値の積算はどこかでリセットする必要があります。
　重力センサーはスマホの傾きに対して床を傾けることができるので、時間経過とともにずれることはありません。
　ただしスマホの持ち方は指定する必要があります。寝っ転がりながらやりたい場合はもっとソースコードに工夫が必要になります。

　床を傾けて操作するゲームをつくるなら、重力センサーの方がシンプルに使えてよさそうです。

# github

https://github.com/footinglow/Godot4/tree/main/03_study/GravitySensorMoveFloor

　以上