#author("2023-05-15T12:26:09+09:00;2023-04-17T21:00:09+09:00","","")
#author("2023-05-24T00:43:29+09:00","","")
* ボタン入力の応用 [#mff35492]
ボタン入力を取得すること自体はとても簡単です。ところが実際のゲームに使うとなると、気をつけなくてはいけないことや、テクニックがいくつかあります。たとえば以下のソースコードはやってはいけない例の1つです。これには''チャタリングの問題''が絡んでいます。

 int main()
 {
 	for(;;)
 	{
 		u16 key = *(volatile u16*)0x4000130;
  		TRACE("KEY: %x\n", key);
 	}
 }

** チャタリングとは [#ce336b76]
信号線はデジタルでも、ボタンを押す強さそのものは%%ぴよこ%%アナログです。したがって、ボタンを押している/押していないの間の「中間の状態」が必ず存在することになります。その「中間の状態」がどういう波形になるかというと、細かい話を抜きにしておおざっぱに書けばこうです。


  OFF ───┐┌┐┌┐┌┐┌┐┌┐
  ON     └┘└┘└┘└┘└┘└───


人間の指は、ボタンの状態を瞬間的に「ON/OFF」に出来るほど強靭なものではないため、ボタンを押したごく一瞬にも「中間の状態」が発生してしまいます。ウェイト処理を入れずに連続してキーセンスを読んだ場合(大抵はμsec~nsec単位)、この波形を読む事により誤動作を起こすわけです。この現象がチャタリングです。そこでVBLANK期間中(1/60秒間隔)に1回だけ呼び出すことで、その分キーセンスを読みに行く間隔を遅くします。つまり「押した、押さない」という入力は最大30回までの認識になるということです。
人間の指は、ボタンの状態を瞬間的に「ON/OFF」に出来るほど強靭なものではないため、ボタンを押したごく一瞬にも「中間の状態」が発生してしまいます。ウェイト処理を入れずに連続してキーセンスを読んだ場合(大抵はμsec~nsec単位)、この波形を読む事により誤動作を起こすわけです。この現象がチャタリングと呼ばれています。そこでVBLANK期間中(1/60秒間隔)に1回だけ呼び出すことで、その分キーセンスを読みに行く間隔を遅くします。つまり「押した、押さない」という入力は最大30回までの認識になるということです。

 int main()
 {
 	IrqInit();
 
 	for(;;)
 	{
 		VBlankIntrWait();
 
 		u16 key = *(volatile u16*)0x4000130;
 		TRACE("KEY: %x\n", key);
 	}
 }


また「メインループの初めにボタン状態を取得しておき、以後のキー入力チェック処理(自機移動など)では、その取得した値を参照する」ようにします。理由は、「必要になったそのつど入出力を読みに行っていたのでは、連続で読むような構造の場合に、短い間隔で読んでいる事と同じ」になってしまうためです。キーセンスは「必要最低限の間隔で一気に」読む事が重要です。


** さらなる改良 [#yf35a53f]
さらにゲームに特化した場合、モジュール化を進めるのが得策です。以下に自分の使っているライブラリを表します。

- main.c
 #include "lib/gba.h"
 #include "bg.h"
 #include "irq.arm.h"
 #include "key.h"
 
 //---------------------------------------------------------------------------
 int main(void)
 {
 	REG_WSCNT = 0x4317;
 
 	BgInit();
 	IrqInit();
 	KeyInit();
 
 	char str[32];
 
 	for(;;)
 	{
 	    VBlankIntrWait();
 
 		KeyExec();
 
 		_Sprintf(str, "cnt:[%04x]", KeyGetCnt());
 		BgAsciiDrawStr(0, 0, str);
 
 		_Sprintf(str, "trg:[%04x]", KeyGetTrg());
 		BgAsciiDrawStr(0, 1, str);
 
 		_Sprintf(str, "off:[%04x]", KeyGetOff());
 		BgAsciiDrawStr(0, 2, str);
 
 		_Sprintf(str, "rep:[%04x]", KeyGetRep());
 		BgAsciiDrawStr(0, 3, str);
 	}
 }

- key.h
 #ifndef KEY_H
 #define KEY_H
 #ifdef __cplusplus
 extern "C" {
 #endif
 
 #include "lib/gba.h"
 
 //---------------------------------------------------------------------------
 #define KEY_REPEAT_CNT		10
 
 
 //---------------------------------------------------------------------------
 typedef struct {
 	u16 cnt;					// 現在のキー
 	u16 trg;					// 押されたキー
 	u16 off;					// 離されたキー
 	u16 rep;					// リピートキー
 	s16 repCnt;					// リピートカウント
 } ST_KEY;
 
 
 //---------------------------------------------------------------------------
 EWRAM_CODE void KeyInit(void);
 IWRAM_CODE void KeyExec(void);
 
 IWRAM_CODE u32  KeyGetCnt(void);
 IWRAM_CODE u32  KeyGetTrg(void);
 IWRAM_CODE u32  KeyGetOff(void);
 IWRAM_CODE u32  KeyGetRep(void);
 
 
 #ifdef __cplusplus
 }
 #endif
 #endif

- key.c
 #include "key.h"
 
 //---------------------------------------------------------------------------
 ST_KEY Key;
 
 
 //---------------------------------------------------------------------------
 EWRAM_CODE void KeyInit(void)
 {
 	_Memset(&Key, 0x00, sizeof(ST_KEY));
 }
 //---------------------------------------------------------------------------
 // vblank中に1回だけ呼び出します(チャタリング防止)
 IWRAM_CODE void KeyExec(void)
 {
 	u16 cnt = REG_KEYINPUT;
 
 	cnt     = ~cnt & 0x3ff;
 	Key.trg = (Key.trg ^ cnt) & ~Key.cnt;
 	Key.off = Key.off ^ (~cnt & Key.cnt);
 	Key.cnt = cnt;
 
 
 	// キーリピート
 	if(Key.trg & DPAD || Key.repCnt == 0)
 	{
 		Key.rep = Key.cnt;
 		Key.repCnt = KEY_REPEAT_CNT;
 	}
 	else
 	{
 		Key.rep = 0;
 	}
 
 	if(Key.cnt & DPAD)
 	{
 		if(Key.repCnt != 0) Key.repCnt--;
 	}
 	else
 	{
 		Key.repCnt = 0;
 	}
 }
 //---------------------------------------------------------------------------
 // 現在押されているボタン
 IWRAM_CODE u32 KeyGetCnt(void)
 {
 	return Key.cnt;
 }
 //---------------------------------------------------------------------------
 // 押された時のボタン
 IWRAM_CODE u32 KeyGetTrg(void)
 {
 	return Key.trg;
 }
 //---------------------------------------------------------------------------
 // 離された時のボタン
 IWRAM_CODE u32 KeyGetOff(void)
 {
 	return Key.off;
 }
 //---------------------------------------------------------------------------
 // キーリピートのボタン。十字キーのみ対応
 IWRAM_CODE u32 KeyGetRep(void)
 {
 	return Key.rep;
 }

** 実行画面 [#hacc9509]
#ref(1.png,nolink)

それぞれのボタンを押したときの特徴です。

|Key.cnt|現在、押されているボタンを格納する。|
|Key.trg|押された時だけのボタンを格納する。押しつづけているボタンは格納しない。|
|Key.off|離された時だけのボタンを格納する。もう一度「離されたボタン」を押さない限り保持する。|
|Key.rep|十字キーのみ対応。押されているボタンをウェイトを挟んで格納する。|

** 出典元 [#p825bd04]
- M-KAIさんの「Witchシューティングゲーム制作記帳」
- 正直日記さん

** 履歴 [#p7782138]
- 2023/04/17
- 2008/07/06

トップ   差分 履歴 リロード   一覧 検索 最終更新   ヘルプ   最終更新のRSS