Tips.1-2 ボタン入力の取得法

使うのは簡単だけど・・・。

入力を取得すること自体はとても簡単です。ところが実際のゲームに使うとなると、気をつけなくてはいけないことや、テクニックがいくつかあります。たとえば以下のソースコードはやってはいけない例の1つです。これにはチャタリングの問題が絡んでいます。

//---------------------------------------------------------------------------
int main()
{
	for(;;)
	{
		u16 key = *(volatile u16*)0x4000130;

		TRACEOUT("KEY: %x\n", key);
	}
}

チャタリングとは

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

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

人間の指は、ボタンの状態を瞬間的に「ON/OFF」に出来るほど強靭なものではないため、ボタンを押したごく一瞬にも「中間の状態」が発生してしまいます。ウェイト処理を入れずに連続してキーセンスを読んだ場合(大抵はμsec〜nsec単位)、この波形を読む事により誤動作を起こすわけです。この現象がチャタリングです。

ちょっとした改良

そこでVBLANK期間中(1/60秒間隔)に1回だけ呼び出すことで、その分キーセンスを読みに行く間隔も遅くなりチャタリングも解決しています。つまり「押した、押さない」という入力は最大30回までの認識になるということです。

//---------------------------------------------------------------------------
int main()
{
	IntrInit();
	IntrStart();

	for(;;)
	{
		SystemCall(5);

		u16 key = *(volatile u16*)0x4000130;
		TRACEOUT("KEY: %x\n", key);
	}
}

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

さらなる改良

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

  • main.c
    #include "lib/gba.h"
    #include "intr_arm.h"
    #include "bg.h"
    #include "key.h"
    
    
    //---------------------------------------------------------------------------
    // key.c
    extern ST_KEY Key;
    
    
    //---------------------------------------------------------------------------
    int main()
    {
    	BgInit();
    	KeyInit();
    	IntrInit();
    
    	IntrStart();
    
    	char cnt[40];
    	char trg[40];
    	char off[40];
    	char rep[40];
    
    	for(;;)
    	{
    		SystemCall(5);
    		KeyExec();
    
    		_Sprintf(cnt, "cnt: %4x", Key.cnt);
    		_Sprintf(trg, "trg: %4x", Key.trg);
    		_Sprintf(off, "off: %4x", Key.off);
    		_Sprintf(rep, "rep: %4x", Key.rep);
    
    		BgAsciiDrawStr(0, 0, cnt);
    		BgAsciiDrawStr(0, 1, trg);
    		BgAsciiDrawStr(0, 2, off);
    		BgAsciiDrawStr(0, 3, rep);
    	}
    }
  • key.h
    #ifndef __KEY_H__
    #define __KEY_H__
    #ifdef __cplusplus
    extern "C" {
    #endif
    
    #include "lib/gba.h"
    
    //---------------------------------------------------------------------------
    #define KEY_MAX_BTN_CNT		10
    #define KEY_REPEAT_CNT		10
    
    
    enum {
    	SYS_KEY_A = 0x00,
    	SYS_KEY_B,
    	SYS_KEY_SL,
    	SYS_KEY_ST,
    	SYS_KEY_RI,
    	SYS_KEY_LE,
    	SYS_KEY_UP,
    	SYS_KEY_DO,
    	SYS_KEY_R,
    	SYS_KEY_L,
    };
    
    //---------------------------------------------------------------------------
    typedef struct {
    	u16 tmp[KEY_MAX_BTN_CNT];	// ボタンの判定用
    	u16 cnt;					// 現在のキー
    	u16 trg;					// 押されたキー
    	u16 off;					// 離されたキー
    	u16 rep;					// リピートキー
    	s16 repCnt;					// リピートカウント
    } ST_KEY;
    
    
    //---------------------------------------------------------------------------
    EWRAM_CODE void KeyInit();
    EWRAM_CODE bool KeyChgBtn(u16 b1, u16 b2);
    IWRAM_CODE void KeyExec();
    
    
    #ifdef __cplusplus
    }
    #endif
    #endif /* _KEY_H_ */
  • key.c
    #include "key.h"
    
    
    //---------------------------------------------------------------------------
    ST_KEY Key;
    
    
    //---------------------------------------------------------------------------
    EWRAM_CODE void KeyInit()
    {
    	_Memset(&Key, 0x00, sizeof(ST_KEY));
    
    	Key.tmp[SYS_KEY_A]  = KEY_A;
    	Key.tmp[SYS_KEY_B]  = KEY_B;
    	Key.tmp[SYS_KEY_SL] = KEY_SELECT;
    	Key.tmp[SYS_KEY_ST] = KEY_START;
    	Key.tmp[SYS_KEY_RI] = KEY_RIGHT;
    	Key.tmp[SYS_KEY_LE] = KEY_LEFT;
    	Key.tmp[SYS_KEY_UP] = KEY_UP;
    	Key.tmp[SYS_KEY_DO] = KEY_DOWN;
    	Key.tmp[SYS_KEY_R]  = KEY_R;
    	Key.tmp[SYS_KEY_L]  = KEY_L;
    }
    //---------------------------------------------------------------------------
    EWRAM_CODE bool KeyChgBtn(u16 b1, u16 b2)
    {
    	if(b1 > KEY_MAX_BTN_CNT)
    	{
    		return FALSE;
    	}
    
    	Key.tmp[b1] = b2;
    
    	return TRUE;
    }
    //---------------------------------------------------------------------------
    // vblank中に1回だけ呼び出します(チャタリング防止)
    IWRAM_CODE void KeyExec()
    {
    	u16 inkey = REG_KEYINPUT;
    	u16 cnt   = 0;
    
    	if(inkey & Key.tmp[SYS_KEY_A])  cnt += KEY_A;
    	if(inkey & Key.tmp[SYS_KEY_B])  cnt += KEY_B;
    	if(inkey & Key.tmp[SYS_KEY_SL]) cnt += KEY_SELECT;
    	if(inkey & Key.tmp[SYS_KEY_ST]) cnt += KEY_START;
    	if(inkey & Key.tmp[SYS_KEY_RI]) cnt += KEY_RIGHT;
    	if(inkey & Key.tmp[SYS_KEY_LE]) cnt += KEY_LEFT;
    	if(inkey & Key.tmp[SYS_KEY_UP]) cnt += KEY_UP;
    	if(inkey & Key.tmp[SYS_KEY_DO]) cnt += KEY_DOWN;
    	if(inkey & Key.tmp[SYS_KEY_R])  cnt += KEY_R;
    	if(inkey & Key.tmp[SYS_KEY_L])  cnt += KEY_L;
    
    	cnt     = ~cnt;
    	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;
    	}
    }
  • 実行画面
    clip_1.png

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

cnt

今、押されているボタンを格納する。

trg

押された時だけ、ボタンを格納する。押しつづけているボタンは格納しない。

off

離された時だけ、ボタンを格納する。

rep

十字キーのみ対応。repの用途は、たとえばゲームの設定画面があって、メニューの項目がたくさんあったとします。十字キーを押したとき、選択されている項目を1つ移動した後、ウェイトがしばらく入ります(無入力状態)。ウェイト後はcntと短いウェイトが交互に入って移動をするといったことになります。

出典元

  • M-KAIさんの「Witchシューティングゲーム制作記帳」

履歴

  • 2008/07/06

添付ファイル: fileclip_1.png 423件 [詳細]

Last-modified: 2008-08-07 (木) 21:55:58 (5119d)