最近のアナログスティック事情

 2021年9月現在で購入できるゲームコントローラーのほとんどは左右のアナログスティックが付いています。 付いていない商品は、超小型のものやレトロ調、アケコンなどでその他は探すのが大変なくらいです。

 PAD設定さんを作成していた16年前とは随分状況が変わりましたね。 今は積極的にアナログスティックを活用してもあまり支障がない時代になったと思います。 しかし、そもそもゲームコントローラーを持っていない人もいるので、そこは配慮が必要ではあるので要注意。

ということで、アナログスティックの運用に関する内容です。

参考記事について

 ゲームコントローラーについていろいろ調べているとこんな記事へのリンクを見つけました。

Doing Thumbstick Dead Zones Right
https://www.gamedeveloper.com/disciplines/doing-thumbstick-dead-zones-right

 記事の内容は、「アナログスティックのデッドゾーンを正しく行う」というもので、私自身にとってこれまで適当に実装してきた部分だったのでとても面白い内容でした。 要約すると…アナログスティックは手を離すと中央付近に戻るけど、正確に中央には戻らないから触っていなくても小さい値が入力しっぱなしになって困るよね。 小さい値が反応しないデッドゾーンを設定すればいいんだけど、実装方法によってはおかしな動きになるからいいテクニックを紹介するよ。 という感じです。詳しくは元の記事を読んでください。

 最初はさくっと斜め読みして、この着眼点はなかったわー。と思い、指数を使って適当実装したんですが、 ちゃんと読み直すともっと適切な方法が書いてありました。

 ということで、参考記事の内容をHSP3で実装してみました。詳しい解説や図については元記事を御覧ください。 最近はブラウザが翻訳してくれるので問題なく読めるはずです。

アナログスティックの入力を取得する

 まずはHSP3、それからこちらのモジュールを導入してください。
PAD設定さん

 導入したら、次のようにしてゲームパッドの性能を取得します。


#include "m_joystick.hsp"

;	ゲームパッドの性能を取得
dim jinfo, 19
JStickGetDevCaps jid, jinfo
wXmax = jinfo(1)	; アナログ左スティックの左右最大値
wYmax = jinfo(3)	; アナログ左スティックの上下最大値
    

PAD設定さん を使いたくないなら、


wXmax = 0xFFFF	;(=65535)
wYmax = 0xFFFF	;(=65535)
    

 という感じで値を決め打ちしても問題ないでしょう。 私の経験上、アナログスティックの入力上限値は 65535 ばかりなので。

 次にゲームパッドからアナログスティックの情報を取得します。PAD設定さん を使うなら、


JStick key, 0, jid, 4, anakey

 また、HSP3に標準で付属の mod_joystick2 モジュールにある joyGetPosEx 命令で取得しても構いません。

取得値を正規化

 ゲームパッドのアナログスティックから入力された値は、そのままでは使いにくいので -1.0 ~ 1.0 の実数値に正規化します。


	sx = double(anakey(0) * 2 ) / wXmax - 1.0
	sy = double(anakey(1) * 2 ) / wYmax - 1.0
    

 これで、中央が 0 、スティックを目一杯倒すと大きさが 1 になります。 これだけでもかなり扱いやすくなりますね。

 アナログスティックは斜めに倒すと、上下や左右よりも大きな値を返すことがあります。 このままだと「斜め移動は前後左右より速い」という余計な問題が起きるので制限を設定します。


	m = sqrt(sx * sx + sy * sy)
	if m >= 1.0 {
		sx *= 1.0 / m
		sy *= 1.0 / m
	}
    

 アナログスティックを目一杯倒すと反応しない部分があるのがわかると思います。 この部分があるせいで斜めは大きな値になってしまうのですが、ここは構造上必要な遊びの部分です。 ハード上はどうしようもないのでアナログスティックを使う場合は、この処理は必ず付けたほうがよさそうですね。 さて準備はできたので、取得した値を使っていくことにします。

軸ごとのデッドゾーン

 ここで言うデッドゾーンとは、入力がその範囲内にいる場合は、ゼロ(未入力)を返す領域のことを指します。 デッドゾーンが設定してあれば、アナログスティックから手を離した状態はほぼ確実にゼロ(未入力)が検出されますね。 ということで、まずはシンプルなデッドゾーンの設定から。


	deadzone = 0.25	; しきい値
	if absf(sx) < deadzone : sx = 0.0
	if absf(sy) < deadzone : sy = 0.0
    

 軸ごとにデッドゾーンを設定するから、十字状に反応しない領域が出来ます。 上下と左右にノイズが入らない領域があるので、それはそれで良さがありそうです。 しかしデッドゾーンからはみ出た瞬間に急に 0 から値が大きく変わるし、そもそも中心付近以外はデッドゾーンにする必要がありません。

円状のデッドゾーン

 アナログスティックを倒したときの中心からの距離でデッドゾーンを設定する方法です。 これなら中心付近だけがデッドゾーンになります。上下左右だけでなく斜め方向に対しても平等ですね。


	deadzone = 0.25	; しきい値
	if sqrt(sx * sx + sy * sy) < deadzone {
		sx = 0.0
		sy = 0.0
	}
    

 先ほどのように中心付近以外で値が検出されなくなるようなことは無くなりましたが、 デッドゾーンを出た瞬間に値が跳ね上がるのは困りますね。小さい値が入力できない。

スケーリングした円状のデッドゾーン

 これが参考記事の最後に書いてあった方法です。バランスの取れたいい方法だと思います。 円状のデッドゾーンを出た後は、値を直接使うのではなくデッドゾーンの外の領域でスケーリングして使います。


	deadzone = 0.25	; しきい値
	m  = sqrt(sx * sx + sy * sy)
	if m < deadzone {
		sx = 0.0
		sy = 0.0
	}
	; x,yベクトルの大きさで正規化後、しきい値からのはみ出し量で正規化
	z = 1.0 / m * ( m - deadzone ) / ( 1.0 - deadzone )
	sx *= z
	sy *= z
    

 入力がデッドゾーンを出た直後はまだ値が小さく、完全に倒すともとの入力値と同じになります。 これなら値がステップ状に変化することもないので良さそうです。 若干中央に引っ張られるような動きになってしまう問題はあるので、採用する際は要注意。

指数

 最後に記事をななめ読みだけして、私が最初に作った物を紹介。アナログスティックを傾けた量を指数に使用しています。 デッドゾーンを設定はしませんが、中央付近での感度を小さくしてあるので場合によりますが中央付近には反応しないデッドゾーンが存在します。 (小数点以下どこまで反応するかによる。)


	curve = 10.0	; 大きくすると変化が大きく、0に近いほど直線に近くなります。
	m  = sqrt(sx * sx + sy * sy)
	z = ( powf( curve + 1, m ) - 1 ) / curve
	sx *= z
	sy *= z
    

 中心付近の感度は鈍いので小さい値の入力が得意です。また大きく倒すと入力値に近い値になります。 感度は変数 curve の値を変えると変更することが出来ます。

 参考記事の方法よりも中央に強く引っ張られるような動きになるのが問題点です。これも採用する際は要注意。

動作確認サンプル

 上記の内容の動作を確認するサンプルスクリプトです。 動作には「PAD設定さん」が必要です。

 実際に操作してみて、それぞれの方法による特徴を体験してください。 どの方法を採用するべきかは、ゲーム内でどのようにアナログ値を使用するかによります。 使ってみて一番最適なものを選択するようにしてください。 これらの方法でもしっくり来ないことがあると思うので、必要に応じて新しい方法を考えてみてください。


;
;	実行サンプル9
;	アナログスティック
;
;[ Infomation ]
; アナログスティックの入力値を変換するサンプルです。
; ゲームパッドのアナログスティックは、手を離すとおおよそ中央に戻ります。
; しかし実際には、戻る際の反動やスティックの痛み具合によって、完全に中央に戻ることはありません。
; 完全な中央からわずかに外れた位置でスティックは静止します。
;
; この問題の解決策として、こちらのサイトを参考にした実装例と独自の解決策のサンプルを作成しました。
; https://www.gamedeveloper.com/disciplines/doing-thumbstick-dead-zones-right
;
; アナログスティック以外のボタンを押すと、下記の方法を使って変換した結果を表示します。
; 変換後の値はウィンドウ上に赤で表示されます。
; ・無変換
; ・デッドゾーン
; ・円状のデッドゾーン
; ・スケーリングした円状のデッドゾーン
; ・指数関数
;
;[ Update history ]
; 2021/09/11 : 1.0 : 完成
;
;################################################
#include "m_joystick.hsp"

;接続されているジョイスティクIDの番号を指定してください。
jid = 0			;ゲームパッドID

;	ゲームパッドの性能を取得
dim jinfo, 19
JStickGetDevCaps jid, jinfo
wXmax = jinfo(1)	; アナログ左スティックの左右最大値
wYmax = jinfo(3)	; アナログ左スティックの上下最大値
;wZmax = jinfo(5)	; アナログ右スティックの左右最大値
;wRmax = jinfo(10)	; アナログ右スティックの上下最大値

;	入力値のログを表示するためのバッファ
#const LOG_MAX 60
dim xlog, LOG_MAX
dim ylog, LOG_MAX
ilog = 0
dim xlog1, LOG_MAX
dim ylog1, LOG_MAX
ilog1 = 0

;	変換モード
mode = 0


*main
	redraw 1 : await 16 : redraw 0 : color 255, 255, 255 : boxf : color : pos 0,0

	mes "アナログスティック入力"

	;---------------------
	;	ゲームパッド入力を取得
	;---------------------
	JStick key, 0, jid, 4, anakey
	;mes strf("0x%08X", key) + " (" + key + ")"
	mes "生値 :" + strf("% 5d", anakey(0)) + ", " + strf("% 5d", anakey(1))
	;mes "" + strf("% 5d", anakey(2)) + ", " + strf("% 5d", anakey(3))

	;	正規化
	; アナログ入力で取得した値を -1.0 ~ 1.0 の値にする。
	jx = double(anakey(0) * 2 ) / wXmax - 1.0
	jy = double(anakey(1) * 2 ) / wYmax - 1.0
	mes "正規化:" + strf("%6.3f, %6.3f", jx, jy)

	; 斜め入力するとxとyの加算で大きな値が入力できてしまうので、
	; 中心からの距離に制限をつけます。
	m = sqrt(jx * jx + jy * jy)
	if m >= 1.0 {
		jx *= 1.0 / m
		jy *= 1.0 / m
	}

	;---------------------
	;	変換モード切替
	;---------------------
	if key : mode++

	;---------------------
	;	背景描画
	;---------------------
	rgbcolor $C0C0C0
	line ginfo_winx/2, 0, ginfo_winx/2, ginfo_winy
	line 0, ginfo_winy/2, ginfo_winx, ginfo_winy/2


	;---------------------
	;	直接出力
	;---------------------
	;  スティックに触れていない状態は、常にニュートラル位置 (0, 0)(中央)
	; になるとは限りません。
	;  この入力をそのままキャラクターの移動に当てはめると、
	; スティックから手を離してもキャラクターがゆっくり動いて
	; しまいます。
	rgbcolor $000000
	x = jx * 200 + ginfo_winx/2
	y = jy * 200 + ginfo_winy/2
	pos x, y
	gosub *l_plotcross
	gosub *l_log1
	
	;---------------------
	;	モードに応じた出力
	;---------------------
	sx = jx
	sy = jy
	
	pos 10, 100
	switch mode
	;	無変換
	case 0
		mes "MODE : 無変換"
		gosub *l_mode_none
		swbreak
		
	;	デッドゾーン
	case 1
		mes "MODE : 軸ごとのデッドゾーン"
		gosub *l_mode_dead_zone
		swbreak
		
	;	円状のデッドゾーン
	case 2
		mes "MODE : 円状のデッドゾーン"
		gosub *l_mode_radial_dead_zone
		swbreak
		
	;	スケーリングした円状のデッドゾーン
	case 3
		mes "MODE : スケーリングした円状のデッドゾーン"
		gosub *l_mode_scaled_radial_dead_zone
		swbreak

	;	指数
	case 4
		mes "MODE : 指数"
		gosub *l_mode_pow
		swbreak

	default
		mode = 0
		swbreak
	swend

	;	描画
	gosub *l_draw_newpos

	goto *main




;-----------------------------------------------------------
;
;	ログを描画
;
;-----------------------------------------------------------
*l_log
	cx = ginfo_cx
	cy = ginfo_cy
	xlog(ilog) = cx
	ylog(ilog) = cy
	ilog++
	if ilog >= LOG_MAX : ilog = 0

	pos cx, cy
	i = ilog
	repeat LOG_MAX
		i--
		if i < 0 : i = LOG_MAX - 1
		line xlog(i), ylog(i)
	loop
	
	return

*l_log1
	cx = ginfo_cx
	cy = ginfo_cy
	xlog1(ilog1) = cx
	ylog1(ilog1) = cy
	ilog1++
	if ilog1 >= LOG_MAX : ilog1 = 0

	pos cx, cy
	i = ilog1
	repeat LOG_MAX
		i--
		if i < 0 : i = LOG_MAX - 1
		line xlog1(i), ylog1(i)
	loop
	
	return

;-----------------------------------------------------------
;
;	十字を描画
;
;-----------------------------------------------------------
*l_plotcross
	cx = ginfo_cx
	cy = ginfo_cy
	r  = 5
	line cx-r, cy  , cx+r, cy
	line cx  , cy-r, cx  , cy+r
	return
	
;-----------------------------------------------------------
;
;	変換後の表示
;
;-----------------------------------------------------------
*l_draw_newpos
	;	描画用に数値変換
	x = sx * 200 + ginfo_winx/2
	y = sy * 200 + ginfo_winy/2
	
	;	描画
	rgbcolor $FF0000
	pos x, y
	gosub *l_plotcross
	gosub *l_log
	return


;-----------------------------------------------------------
;
;	無変換
;
;-----------------------------------------------------------

;---------------------
;	無変換
;---------------------
*l_mode_none
	; 何もしない
	return


;-----------------------------------------------------------
;
;	デッドゾーンを用いた変換
;
;-----------------------------------------------------------
; ここからはデッドゾーン(=スティクが反応しない領域)を用いた変換の例を示します。
;
; 共通メリット
; ・スティックから手を離せば、0.0 を返す。
;
; 共通デメリット
; ・傷んだスティックの場合、手を離していても中心に戻りきらずにしきい値を超えてしまう可能性がある。
;

;---------------------
;	軸ごとのデッドゾーン
;---------------------
; 仕組み
;  x,y方向それぞれに対して、反応しないしきい値を設定し
; ています。しきい値を設けることで、入力に対して反応しな
; いデッドゾーンを作成しています。
; アナログ入力の精度が必要なシーンでの使用には向いていません。
;
; メリット
; ・上下や左右方向にのみ動かす場合は、ノイズが入らずまっすぐ動かせる。
;
; デメリット
; ・デッドゾーンの境界で、入力がステップ状に変化してしまう。
;   微調整が必要なシーンでは不便。
; ・デッドゾーン以下の小さい値を入力できない。
; ・デッゾーン際で斜め入力をした際に、突然大きな値になったり 0 に
;   なったりといった現象が発生する。
;
*l_mode_dead_zone
	deadzone = 0.25	; しきい値
	if absf(jx) < deadzone : sx = 0.0
	if absf(jy) < deadzone : sy = 0.0
	return



;---------------------
;	円状のデッドゾーン
;---------------------
; 仕組み
; デッドゾーンの判定を各く軸ごとに行うのではなく、x,y ベクトルの大きさで判定します。
;
; メリット
; ・斜めに入力しても値が急激に変化しない。
;
; デメリット
; ・デッドゾーンの境界で、入力がステップ状に変化してしまう。
;   微調整が必要なシーンでは不便。
; ・デッドゾーン以下の小さい値を入力できない。
;
*l_mode_radial_dead_zone
	deadzone = 0.25	; しきい値
	if sqrt(sx * sx + sy * sy) < deadzone {
		sx = 0.0
		sy = 0.0
	}
	return



;---------------------
;	スケーリングした円状のデッドゾーン
;---------------------
; 仕組み
; しきい値のラインが 0.0 となるように調整します。
; 小さい値を入力できないデメリットを克服した手法です。
; 
; メリット
; ・斜めに入力しても値が急激に変化しない。
; ・デッドゾーン境界で、値が急激に変化しない。
;
; デメリット
; ・中央に引っ張られるような動きになる。
;
*l_mode_scaled_radial_dead_zone
	deadzone = 0.25	; しきい値
	m  = sqrt(sx * sx + sy * sy)
	if m < deadzone {
		sx = 0.0
		sy = 0.0
	}
	; x,yベクトルの大きさで正規化後、しきい値からのはみ出し量で正規化
	z = 1.0 / m * ( m - deadzone ) / ( 1.0 - deadzone )
	sx *= z
	sy *= z
	return


;-----------------------------------------------------------
;
;	その他の変換
;
;-----------------------------------------------------------
; ここからはデッドゾーンを用いない変換の例を示します。
;

;---------------------
;	指数関数
;---------------------
; 仕組み
; 入力値を指数に使用します。
; 中央付近では感度が鈍く、大きく傾けるほど感度は大きくなります。
; 
; メリット
; ・スティックから手を離せばおおよそ 0.0 に近い値を返す。
; ・値が急激に変化しない。
; ・中心付近での細かい操作がしやすい一方で、大きく傾ければ大きな値も入力できる。
; ・感度調整が可能。(感度変化の急峻さを調整できる。)
;
; デメリット
; ・スティックから手を離しても完全に 0.0 にはならない。
; ・中央に強く引っ張られるような動きになる。
;
*l_mode_pow
	curve = 10.0	; 大きくすると変化が大きく、0に近いほど直線に近くなります。
	m  = sqrt(sx * sx + sy * sy)
	z = ( powf( curve + 1, m ) - 1 ) / curve
	sx *= z
	sy *= z
	return