一、 轮询键盘
轮询键盘会带来新的问题。首先,一次按键可能带来两次响应的问题。尽管移动设备上的FPS很低,但对于按键来说还是太快了。以下是常见的解决方法:
方法一:使用时间阈值技术。设定一个时间阈值。比如100ms,比较同一个按键两次按下的时间间隔。若小于这个时间阈值则不做处理,若大于这个时间阈值则执行相关的逻辑。
private long leftLastPressedTime=0;//记录上次左键按下的时间
private long limit=100;//定100ms为时间阈值
private void input(){
int keyState=getKeyStates();
if((keyState&LEFT_PRESSED)!=0)
long pastime=System.currentTimeMillis()-leftLastPressedTime;
if(pastTime>limit){
leftLastPressedTime+=pastime;
doSomething();
}
}
方 法二:设置标志变量。通过检查一个标志变量来确定按键的状态。当检测到按键按下后,若标记为false,则说明第一次按下,应处理逻辑。之后将标记变量改 为true,说明已经处理过了,下次就不再处理了;当按键抬起时,标记变量被重置,这样以可以进行下一次处理了。以下是运用标志变量思想的代码。
private boolean leftFlag=false;//标记变量
private void input(){//处理输入
int keyState=getKeyStates();
if((keyState&LEFT_PRESSED)!=0){
doSomething();
leftFlag=true;
}else{
leftFlag=false;
}
}
另 一个重要的问题是如何清除已经记录的按键缓冲区。通常为了界面的美观,游戏的主画面和菜单都在同一个Canvas上绘制,当从游戏主画面切换到菜单,或反 过来从菜单切换到游戏时,可能上一个画面的按键没有被处理便遗留到了新的画面。这时希望清除在上一个画面没有处理的残余按键。方法是两次调用 getKeyStates()。getKeyStates()有一个附加的作用是清空键盘缓冲区,所以两次调用getKeyStates()从理论上讲会 返回当前的按键状态。
二、 实现自己的键盘映射机制
设计一种键盘响应机制,既要基于轮询又要拥有传统回调的灵活性。此外最好能简捷的解决轮询中的常见问题。这个输入组件应该提供的功能有:
n 将键盘 keyCode映射到通用的输入action上;
n 能够模拟按键;
n 用户可自定义按键;
n 可清除按键状态;
n 一部分按键遵循只要按下就起作用的模式(支持重复按键);
n 一部分按键遵循按下后只起一次作用的模式。
这 个输入组件由两部分组成:InputAction类和InputManager类。InputAction类代表了按键的逻辑映射; InputManager类负责将keyCode映射到哪一个InputAction上。先来看看InputAction具有的两种检测模式: MODE_NORMAL和MODE_INTAL_ONLY。前者遵循只要按下就起作用的模式(支持重复按键);后者遵循按下后只起一次作用的模式。 InputAction的使用很简单,只要轮询调用InputAction实例的isPressed()方法就可以了,它会根据不同的模式,返回按键是否 按下。以下是InputAction类的代码
public
//两种检测模式
public static final int MODE_NORAML =0;
public static final int MODE_INITAL_ONLY =1;
//三种按键状态
private static final int STATE_PRESSED =1;
private static final int STATE_RELEASED =2;
private static final int STATE_HOLDON =3;
//检测模式变量
private int mode;
//按键状态变量
private int state;
public InputAction(){
this ( MODE_NORAML );
}
public InputAction( int mode){
this .mode=mode;
this .state= STATE_RELEASED ;
reset();
}
public synchronized void press(){//按下
if (state!= STATE_HOLDON )
state= STATE_PRESSED ;
}
public synchronized void release(){//释放
state= STATE_RELEASED ;
}
public synchronized void reset(){//重置
release();
}
public synchronized void tap(){//模拟一次按键
press();
release();
}
public synchronized boolean isPressed(){
int lastState=state;
if (state== STATE_PRESSED &&mode== MODE_INITAL_ONLY )
state= STATE_HOLDON ;
return lastState== STATE_PRESSED ;
}
}
InputManager 类负责管理将keyCode映射到哪一个InputAction上。它内部维护了一个Map。以keyCode作为(key),以InputAction 对象实例作为值。通过调用方法mapkeyToInputAction(int keyCode,InputAction action)来进行按键映射。这种机制允许将多个键盘映射到同一个InputStream上。例如,将方向“↑”和数字键“2”都映射到一个 InputAction上。以下是InputManager类的代码。
import java.util.Enumeration;
import java.util.Hashtable;
public class InputManager {
private Hashtable keyMap=new Hashtable();//保存键盘映射的MAP
public InputManager(){
}
public void mapkeyCodeToInputAction(int keyCode,InputAction action){
keyMap.put(new Integer(keyCode),action );//将一个整型keyCode映射到一个action
}
public void clearAll(){//清空所有的映射
keyMap.clear();
}
public void keyPressed(int keyCode){//回调方法,用于键盘按下
InputAction action=getInputAction(keyCode);
if(action!=null)
action.press();
}
public void keyReleased(int keyCode){//回调方法,用于键盘释放
InputAction action=getInputAction(keyCode);
if(action!=null)
action.release();
}
public void keyRepeated(int keyCode){//回调方法,用于键盘重复,保持空白
}
protected InputAction getInputAction(int keyCode){//取得map中与之对应的action
return (InputAction)keyMap.get(new Integer(keyCode));
}
public void resetAll(){//
for(Enumeration e=keyMap.elements();e.hasMoreElements();)
((InputAction)e.nextElement()).reset();
}
}
下 面是使用方法。首先实例化一个InputStream对象,并在职Canvas的三个回调函数中分别对InputManager的对应方法进行回调。准备 工作完成后,可以根据需求以不同的检测模式参数来构造InputAction对象。可选的模式有InputAction.MODE_NORMAL和 InputAction.MODE_INITAL_ONLY。调用inputManager.mapkeyCodeToAction()方法,将不同的 KeyCode映射到InputAction对象上。使用时只要在轮询的input()方法中轮询调用InputAction实例的isPressed方 法就可以了。
import javax.microedition.lcdui.game.GameCanvas;
public class GameScreenInputEnable extends GameCanvas implements Runnable{
//...
InputManager inputManager= new InputManager();
InputAction upAction= new InputAction(InputAction. MODE_INITAL_ONLY );
InputAction downAction= new InputAction();
//...
public GameScreenInputEnable(){
super ( false );
}
public void run(){
//The main part of the Game
}
public void init(){
//...
inputManager.mapkeyCodeToInputAction( KEY_NUM2 , upAction); //映射数字键
inputManager.mapkeyCodeToInputAction( KEY_NUM8 , downAction);
inputManager.mapkeyCodeToInputAction(-1, upAction);//映射方向导航键
inputManager.mapkeyCodeToInputAction(-2, downAction);
}
public void input(){
if (upAction.isPressed()){
// TODO
}
if (downAction.isPressed()){
// TODO
}
}
protected void keyPressed( int keyCode){//
inputManager.keyPressed(keyCode);
}
protected void keyReleased( int keyCode){
inputManager.keyReleased(keyCode);
}
protected void keyRepeated( int keyCode){
inputManager.keyRepeated(keyCode);
}
}
在这个例子中,方向导航键“↑”和数字键“2”被映射到了upAction,并只在按下后起一次作用。对应的方向导航键“↓”和数字键“8”被映射到了downAction,在按下后总是有效。
可以在运行时改变按键的检测模式和按键的映射关系,这为在运行时由用户自行配置按键提供了可能。使用这个输入组件的前提条件是:GameCanvas不能屏蔽键盘事件,即构造函数传入false,不能为true。