[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 用法
留言
張貼留言