[Java] javax 中 KeyListener 的遲鈍問題
看 GrandmaCan -我阿嬤都會(小白) 的3小時用 Pygame 做出遊戲影片,想說自己來用 Java 做做看,在移動太空船的地方,想法大概是用 JLabel 來做太空船,在 JFrame 用 .addKeyListener(new KeyListener(){ }) 來取得鍵盤輸入,當輸入 'a' 時( code 65)將太空船的位置 x 減少就會左移,輸入 'd' 時(code 68)將 x 增加就會右移,程式碼大概如下:
基本的 KeyListener 聽取鍵盤輸入
mainStart.java
public class mainStart {
public static void main(String[] args) {
new TestBasicKeyListener();
}
}
TestBasicKeyListener.java
import java.awt.Color;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
public class TestBasicKeyListener extends JFrame{
JPanel myPanel;
JLabel myLabel;
public TestBasicKeyListener() {
myPanel = new JPanel();
myLabel = new JLabel();
myPanel.setSize(500,600);
myLabel.setSize(50,40); //設定一個長50寬40的JLabel
myLabel.setBackground(Color.orange);
myLabel.setOpaque(true); //背景橘色,不透明
myPanel.setLayout(null);
myPanel.add(myLabel);
myPanel.setVisible(true);
this.setSize(500,600);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.addKeyListener(new KeyListener() {
@Override
public void keyTyped(KeyEvent e) {
// 其實操作若是abcd等字母鍵可用 keyTyped() 即可
} // 當操作有方向鍵等要用 keyPressed()
@Override
public void keyPressed(KeyEvent e) {
int posX = (int)label.getLocation().getX();
int posY = (int)label.getLocation().getY();
if(e.getKeyCode()==65) { //按下'a'時將JLabel坐標x減少
label.setLocation(posX - 8, posY);
}
if(e.getKeyCode()==68) { //按下'd'時將JLabel坐標x增加
label.setLocation(posX + 8, posY);
}
}
@Override
public void keyReleased(KeyEvent e) {
}
});
this.setVisible(true);
this.add(myPanel);
myLabel.setLocation(300,300);
}
}
這是上述的結果,如影片的左邊
有很大的問題是 KeyListener 在取得按鍵後有一個遲鈍感,這個在 StackOverflow 有很多人問過,這篇 Java KeyListener stutters 的描述非常到位:
他呈現的樣子並不是 aaaaaaaaaa 而是 a [頓一下] aaaaaaaaa,問題是怎麼把那個頓點取消掉?
這篇 StackOverflow 的答案大概是這樣:他並沒有用 KeyListener ,而是做了一個類別 KeyboardAnimation,參數有 JComponent(像是 JLabel)和延遲值,物件有個 HashMap pressedKeys 將字串與 Point 對應(Point 是做為向量位移用),實做了 javax.swing.Action 和 KeyStroke (聽說這叫 key binding,我還要再研究一下),但因為遊戲後期需要定義太空船按下空白鍵發射字彈,所以我暫時不考慮他的作法,僅使用整個問題最關鍵的「取消按鍵的那個頓點」
關鍵是聽取到按下 a 鍵的時候,也就是 pressed 時,打開 timer 讓計時器高速執行一個函式,這個函式另外寫而不是放在 KeyListener() 之中;而當放開 a 鍵也就是 released 時,則停止 timer 不再執行。如此一來就沒有那個 a [頓一下] aaaaa 的問題了。程式碼像是這樣:
改良後,不會頓一下
mainStart.java
public class mainStart {
public static void main(String[] args) {
new NoStutterFrame();
}
}
NoStutterFrame.java
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JFrame;
import javax.swing.JLabel;
public class NoStutterFrame extends JFrame{
NoShutterPanel myPanel;
public MainFrame() {
myPanel = new NoStutterPanel();
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setSize(500,600);
this.add(myPanel);
this.addKeyListener(new KeyListener() {
@Override
public void keyTyped(KeyEvent e) { // 同上,暫不考慮用 keyTyped()
}
@Override
public void keyPressed(KeyEvent e) {
// 在 NoStutterPanel 中有一個 ArrayList<Integer>存放目前按下的所有鍵的 code
int pressedCode = e.getKeyCode();
if(!myPanel.pressedKeyList.contains(pressedCode)) { //不存在就加入 code
myPanel.pressedKeyList.add(pressedCode);
}
if(myPanel.pressedKeyList.size() == 1) { // 若按下鍵則計時器啓動
myPanel.timerKeyboard.start(); // 開始處理有按的鍵
}
}
@Override
public void keyReleased(KeyEvent e) {
// 當放掉按鍵,就將 pressedKeyList 之中的 code 值去掉,這樣就不會處理到該鍵
int pressedCode = e.getKeyCode();
if(myPanel.pressedKeyList.contains(pressedCode)) { // 若存在就刪去
myPanel.pressedKeyList.remove(
myPanel.pressedKeyList.indexOf(pressedCode)
);
}
if(myPanel.pressedKeyList.size() == 0) { // 若清空到完全沒有按鍵
myPanel.timerKeyboard.stop(); // 則計時器停止,不再處理
}
}
});
this.setVisible(true);
}
}
NoStutterPanel.java
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.Timer;
public class NoStutterPanel extends JPanel implements ActionListener{
Timer timerKeyboard;
JLabel myLabel;
public ArrayList<Integer> pressedKeyList;
public NoStutterPanel() {
myLabel = new JLabel();
myLabel.setSize(50,40); // 設定一個長50寬40的JLabel
myLabel.setBackground(Color.orange); //背景橘色,不透明
myLabel.setOpaque(true);
myLabel.setLocation(300, 300);
timerKeyboard = new Timer(16, this);
timerKeyboard.setInitialDelay(0); // 初次執行計時器距離多久
pressedKeyList = new ArrayList<Integer>();
this.setSize(500,600);
this.setLayout(null);
this.add(myLabel);
this.setVisible(true);
}
private void handleKey() { // 處理按下的鍵
int posX = (int)myLabel.getLocation().getX();
int posY = (int)myLabel.getLocation().getY();
if(pressedKeyList.contains(65)) { // 有按 a 的話
myLabel.setLocation(posX -8, posY); // 向左移
}
if(pressedKeyList.contains(68)) { // 有按 d 的話
myLabel.setLocation(posX +8, posY); // 向右移
}
}
@Override
public void actionPerformed(ActionEvent e) {
handleKey(); // 計時器會呼叫 handleKey()
}
}
解釋一下為什麼會有 ArrayList<Integer>,因為在 KeyListener 之中即使一直按著 a 鍵,可能會判斷一次也可能判斷好幾次,這在用 System.out.println( e.getKeyCode() ); 就會發現,所以要用一個集合蒐集現在共按了哪些鍵,StackOverflow 上的解答者也是用類似的 HashMap 。但是我的做法在將來做出空白鍵射子彈的部分可能要重做,因為不用 HashMap 而一個個用 if ,使得無法做到「同時按下」的功能,我上面的寫法如果加入「按下 w 鍵則 posY 減少也就是上移」,會發現同時按下 a 和 w 並不會往左上方移動,除非在 if 中單獨判斷是否 pressedKeyList 中同時有 a 和 w 的 code,所以要來瞭解實做 Action 和 KeyStroke 的做法了,上面只是一個簡單的記錄,取消 KeyListener 中延遲 lag 的情形。
簡單的 InputMap、ActionMap 和 KeyStroke 用法
留言
張貼留言