J2ME键盘响应祥解及处理方法

       J2ME上的键盘响应估计是继画图后出现Bug最多的地方了,尤其像手机游戏这种键盘操作较多的J2ME程序。在工作的过程中CoCoMo曾不止一次的被问及有关键盘响应的问题,pigham前两天还在为他的游戏在7210上按键不能及时响应而发愁,就在刚才我还在努力的解决着S700上不支持 getGameAction()的问题。虽然CoCoMo在这个行业已经工作了一年多了,但是键盘响应的bug仍然时常蹦出来刺激我的神经,所以千万不要小瞧了这个不起眼的keyPressed()
       keyPressed()响应的位置:
       弄清楚keyPressed()响应的位置对最终解决按键响应不及时很有帮助。理论上keyPressed()是由KVM负责的,当Canvas的子类被Display.setCurrent()之后,只要按下任何按钮就会引发keyPressed()。但这只是某些人一厢情愿的美好愿望,仅限于理论研究的范畴,理论和实际往往相差甚远不是吗。实际上keyPressed()的响应是有位置的,CoCoMo可以用如下程序做一个实验:
       
     while(b_running)
     {
       updateState();
       repaint();
       serviceRepaints();
       long d = System.currentTimeMillis() – s_curFrameTime;
       if (d < 80) Thread.sleep(80 – d);
       s_curFrameTime = System.currentTimeMillis();
     }
     
这是一个传统意义上的游戏循环,CoCoMo分别在updateState(),paint()和keyPressed()中加入调试语句如下:
     void updateState() {
       System.out.println("UpdateState");
     }
 
     protected void paint(Graphics g) {
       System.out.println("Paint");
     }
     
     protected void keyPressed(int keyCode) {
       System.out.println("keyPressed");
     }
     
     protected void keyReleased(int keyCode) {
       System.out.println("keyReleased");

     }
然后启动MIDlet,一阵狂按之后,得出如下结果:
UpdateState
Paint

keyReleased
UpdateState
Paint
keyPressed
UpdateState
Paint
keyReleased
keyPressed
你会惊奇的发现keyPressed()和keyReleased()总是在paint()之后,UpdateState()之前。实际上 keyPressed()是在线程sleep的时候引发的,也就是说当Canvas这个线程在空闲状态时,KVM才有机会向激活的Display传递消息说有人按了某键,或者KVM总能向Display传递消息,但Display需要在空闲状态时才能调用keyPressed(),至于是哪种方式那是底层实现的事情,我们并不得而知,但有一点是可以肯定的:keyPressed()是在Canvas线程sleep的时候被引发的。这也是按键不响应或延时的根本原因:Canvas线程过于繁忙,没有sleep或很少sleep。解决办法:
一:优化程序,减轻Canvas线程负担,使sleep时间增加。
二:在优化后情况仍然存在,可以在需要按键响应的地方强行使线程sleep(20),从而引发keyPressed()。在我的一个项目中需要频繁创建图片,致使线程过度繁忙,即便在S700这样的机型上也出现了按键响应不及时的问题,我就是这么处理的,对帧速率影响并不大。
       J2ME按键处理方法:
       一:按键逻辑直接写在keyPressed()里
       优点:测试时经常使用,对于短小程序编写速度快。
       缺点:需要在keyPressed()里再写个switch(gameState)状态机,这样的缺点估计地球人都知道了。而且将逻辑运算写进keyPressed()里影响keyPressed()的响应。
       二:将keyPressed()里的键值提取,在需要的地方做判断。

       基本上现在手机游戏编写都使用这种方法了,实现方式也千变万化,最简单的就是定义一个curKey变量,在keyPressed()里将其赋值,在 keyReleased()里将其清空,在updateState()里判断该变量的值。这里需要一个keyCode表来对应curKey是什么键,类似这样:
   public static final int     PADDLE_UP       = 1;
   public static final int     PADDLE_DOWN     = 6;
   public static final int     PADDLE_LEFT     = 2;
   public static final int     PADDLE_RIGHT    = 5;
   public static final int     PADDLE_FIRE     = 20;
   public static final int     PADDLE_SOFT1    = 21;
   public static final int     PADDLE_SOFT2    = 22;
   public static final int     PADDLE_SOFT3    = 23;
   
   演化而来的在波斯王子里由于需要判断连续按键而引入了按键状态的概念:
 public static final byte Up_Instant     = -1; //瞬间抬起,中间过度状态
 public static final byte Up_Continuous  =  0; //持续抬起,无按键
 public static final byte Dn_Instant     =  1; //瞬间按下,走
 public static final byte Dn_Continuous  =  2; //持续按下,跑

 public static final byte Dn_Continuous2 =  3;  //连击两下,滚
 
 这种处理方式需要每帧里都更新按键状态,键被按下在第一帧为Dn_Instant瞬间按下,在第二帧变为Dn_Continuous持续按下。这种按键处理方式在CoCoMo的引擎里使用了很长一段时间,估计SF的兄弟们现在还在使用着这种方式。他最大的缺点是在Dn_Instant转 Dn_Continuous时容易出错。
   
   到了彩虹六号和Medieval Combat时因为在短时间内需要判断连续不同按键来发大招,所以按键处理引入了虚拟按键的概念:
   public static final int gk_UP         = 1;
   public static final int gk_DOWN       = 1<<1;

   public static final int gk_LEFT       = 1<<2;
   public static final int gk_RIGHT      = 1<<3;
   public static final int gk_NUM0       = 1<<4;
   public static final int gk_NUM1       = 1<<5;
   public static final int gk_NUM2       = 1<<6;
   public static final int gk_NUM3       = 1<<7;

   public static final int gk_NUM4       = 1<<8;
   public static final int gk_NUM5       = 1<<9;
   public static final int gk_NUM6       = 1<<10;
   public static final int gk_NUM7       = 1<<11;
   public static final int gk_NUM8       = 1<<12;
   public static final int gk_NUM9       = 1<<13;
   public static final int gk_STAR       = 1<<14;
   public static final int gk_POUND      = 1<<15;
   public static final int gk_LSOFT      = 1<<16;
   public static final int gk_RSOFT      = 1<<17;
   public static final int gk_MSOFT      = 1<<18;
   
   在一定时间内用mask对curKey做掩码就可以判断是否按下了一组特定键,时间过了就清空curKey。
   
   CoCoMo因为厌烦每个机型上都需要一个不同的keyCode表,所以CoCoMo用getGameAction()和Canvas自带的 keyCode表,只需要一个keyConvert()将按键转换到CoCoMo自定义的keyCode表即可,这个自定义的keyCode表在每个机型上都是一样的:
   public static final byte KEY_NONE = -1;
   public static final byte KEY_0 = 0;
   public static final byte KEY_UL = 1;
   public static final byte KEY_U = 2;

   public static final byte KEY_UR = 3;
   public static final byte KEY_L = 4;
   public static final byte KEY_ATTACK = 5;
   public static final byte KEY_R = 6;
   public static final byte KEY_DL = 7;
   public static final byte KEY_D = 8;

   public static final byte KEY_DR = 9;

   public static final byte KEY_STAR = 10;
   public static final byte KEY_POUND = 11;
   public static final byte KEY_SOFT1 = 12;
   public static final byte KEY_SOFT2 = 13;
 
   这样可以免去移植之苦,对于某些机型例如S700不支持getGameAction()的问题,CoCoMo用此种方法来解决:
   try { //解决getGameAction不被支持的情况
     keyCode = s_game.getGameAction(code);
   }
   catch(Exception e) {
     keyCode = CRes.KEY_NONE;
   }
   
   不支持的时候会抛出一个异常,让keyCode不做转换即可。