ボタン入力を取得すること自体はとても簡単です。ところが実際のゲームに使うとなると、気をつけなくてはいけないことや、テクニックがいくつかあります。たとえば以下のソースコードはやってはいけない例の1つです。これにはチャタリングの問題が絡んでいます。
int main() { for(;;) { u16 key = *(volatile u16*)0x4000130; TRACE("KEY: %x\n", key); } }
信号線はデジタルでも、ボタンを押す強さそのものはぴよこアナログです。したがって、ボタンを押している/押していないの間の「中間の状態」が必ず存在することになります。その「中間の状態」がどういう波形になるかというと、細かい話を抜きにしておおざっぱに書けばこうです。
OFF ───┐┌┐┌┐┌┐┌┐┌┐ ON └┘└┘└┘└┘└┘└───
人間の指は、ボタンの状態を瞬間的に「ON/OFF」に出来るほど強靭なものではないため、ボタンを押したごく一瞬にも「中間の状態」が発生してしまいます。ウェイト処理を入れずに連続してキーセンスを読んだ場合(大抵はμsec~nsec単位)、この波形を読む事により誤動作を起こすわけです。この現象がチャタリングです。そこでVBLANK期間中(1/60秒間隔)に1回だけ呼び出すことで、その分キーセンスを読みに行く間隔を遅くします。つまり「押した、押さない」という入力は最大30回までの認識になるということです。
int main() { IrqInit(); for(;;) { VBlankIntrWait(); u16 key = *(volatile u16*)0x4000130; TRACE("KEY: %x\n", key); } }
また「メインループの初めにボタン状態を取得しておき、以後のキー入力チェック処理(自機移動など)では、その取得した値を参照する」ようにします。理由は、「必要になったそのつど入出力を読みに行っていたのでは、連続で読むような構造の場合に、短い間隔で読んでいる事と同じ」になってしまうためです。キーセンスは「必要最低限の間隔で一気に」読む事が重要です。
さらにゲームに特化した場合、モジュール化を進めるのが得策です。以下に自分の使っているライブラリを表します。
#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); } }
#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
#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; }
それぞれのボタンを押したときの特徴です。
Key.cnt | 現在、押されているボタンを格納する。 |
Key.trg | 押された時だけのボタンを格納する。押しつづけているボタンは格納しない。 |
Key.off | 離された時だけのボタンを格納する。もう一度「離されたボタン」を押さない限り保持する。 |
Key.rep | 十字キーのみ対応。押されているボタンをウェイトを挟んで格納する。 |