JPanel 에서 KeyEvent가 지연되는 현상을 피하기 위해 Timer를 사용하는 예
자바 Swing 프로그래밍에서 키보드 이벤트를 처리할 때 볼 수 있는 이벤트 핸들러 지연현상(Stutter Problem)을 해결하는 한가지 예를 알아보려고 한다. JPanel등의 콤포넌트에 KeyListener를 구현하고 keyPressed() 등의 이벤트핸들러 메소드를 오버라이드하여 키 이벤트를 처리하고자 하는 경우에 키를 지속적으로 누른 상태로 있을 경우에는 처음에 한번은 이벤트가 즉시 발생하지만 그 후에도 지속적으로 키를 누르고 있으면 이벤트가 잠시 동안 발생하지 않는 기간이 있는데, 일반적인 애플리케이션에서는 별로 문제가 되지 않지만 키를 누르면 즉시 반응해야 하는 게임과 같은 애플리케이션에서는 심각한 문제이다.
만약 키를 누를 때마다 짧은 순간에도 수십번의 이벤트가 발생하여 이용자가 의도하지 않은 상태의 변화가 생긴다면 더 큰 문제가 될 수 있기 때문에 이벤트 초기에 한번만 효과를 내고 이어지는 이벤트에서는 잠시 이벤트에 반응하지 않도록 시스템이 설계되어 있다.
그러나 게임 등에서는 키가 눌리는 동안에는 지속적으로 화면을 갱신해야 하기 때문에 이 점은 문제점으로 인식되는데, 다행히 이 문제를 해결하는 방법은 알려져 있으며 테스트해본 결과 만족할 만한 효과를 내기에 여기에 소개하고자 한다
여기서 해결하려는 문제의 핵심은 키가 눌리고 첫 이벤트가 발생한 후에 대략 1초정도 이벤트가 발생하지 않는 기간에도 키가 눌려져 있다면 지속적으로 화면을 갱신하여 게임 등의 애플리케이션에서 이용자의 요구에 즉시 반응하며 지연됨이 없는 효과를 내려는 것이다.
아래의 코드는 키가 눌릴 때 발생하는 첫 이벤트에서 keyCode를 컬렉션에 보관하고 제 2의 쓰레드 코드에서 컬렉션에 보관된 키코드를 확인하여 적절한 화면갱신 코드를 반복적으로 실행하다가 키가 릴리즈될 때 컬렉션에서 해당 키코드를 제거하고 제 2의 쓰레드를 종료하는 내용이다. 첫 이벤트에서 발생한 키코드를 다른 쓰레드 내에서 받아서 이벤트가 발생하지 않는 기간일지라도 키가 눌려져 있다면 화면 갱신을 위한 코드를 반복적으로 실행하므로 마치 키가 눌려져 있을 때는 이벤트가 발생하지 않는 기간이 전혀 없는 것과 같은 효과를 낼 수 있다. 여기서 제 2의 쓰레드는 Timer 에 의해서 주기적이고 강제적으로 발생하는 이벤트 핸들러를 의미한다
Timer를 이용하여 주기적으로 실행되는 메소드 안에서 이벤트 핸들러가 전달한 키코드(keyCode)를 확인하고 반복적으로 화면을 갱신하는 내용이다
package keyevent; import java.awt.*; import java.awt.event.*; import java.awt.image.BufferedImage; import java.util.*; import javax.swing.*; import javax.swing.Timer; /**키를 지속적으로 누르는 경우 첫 키 이벤트가 발생한 후에 잠시 지연되는 문제를 해결하기 위해 * Timer를 사용한 예이다. * 프로그램을 실행한 후에 A, D키를 누르면 화면에 출력된 검은 사각형이 좌우로 회전하는 내용이다 */ public class GamePanel extends JPanel { BufferedImage bImg; double deg; // Rotation Degree public GamePanel() { super(); setBounds(0,0,400,300); setLayout(null); setFocusable(true); requestFocus(); addKeyListener(new KeyHandler()); bImg = new BufferedImage(100,20,BufferedImage.TYPE_BYTE_GRAY); Graphics g = bImg.getGraphics(); g.setColor(Color.BLACK); g.fillRect(0, 0, 100, 20); } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2d = (Graphics2D) g; g2d.rotate(Math.toRadians(deg), 150, 30); g2d.drawImage(bImg, 100, 20, null); } public static void main(String args[]) { //개발자가 상속하여 새로 정의한 콤포넌트로부터 이벤트 핸들러가 제대로 작동하려면 //모든 코드는 EDT(Event Dispatched Thread) 안에서 실행되어야 한다 EventQueue.invokeLater(new Runnable() { public void run() { JFrame frame = new JFrame(); frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); frame.setContentPane(new GamePanel()); frame.setBounds(100,100,400,300); frame.setVisible(true); } }); } public class KeyHandler implements KeyListener{ /** * keyPressed()에서 발생한 키코드를 아래의 HashSet에 저장하면 타이머 이벤트 핸들러에서 * 키코드를 확인하여 화면을 갱신하는 코드를 실행한다 * 여기서 컬렉션 중에서 Set을 선택한 이유는 키코드가 중복되어 저장하는 것을 막고 키를 뗄 때 * HashSet에서 해당 키코드를 한개만 제거해주면 즉시 이벤트 효과가 제거되므로 이벤트에 즉시 반응하는 효과를 낼 수 있다 */ HashSet<Integer> pressedKeys = new HashSet<Integer>(); Timer timer; public KeyHandler() { timer = new Timer(50, new ActionListener(){ // 50ms마다 액션 이벤트 발생 @Override public void actionPerformed(ActionEvent arg0) // 50ms마다 발생한 액션 이벤트 처리 { if(!pressedKeys.isEmpty()){ Iterator<Integer> i = pressedKeys.iterator(); int n = 0; while(i.hasNext()){ n = i.next(); if (n==KeyEvent.VK_A) deg--; else if (n==KeyEvent.VK_D) deg++; repaint(); } }else { timer.stop(); } } }); } @Override public void keyPressed(KeyEvent keyEvent){ //발생한 키코드를 HsshSet에 저장한다 int keyCode = keyEvent.getKeyCode(); pressedKeys.add(keyCode); if(!timer.isRunning()) timer.start(); } @Override public void keyReleased(KeyEvent keyEvent){ //HashSet에서 키코드를 제거한다 int keyCode = keyEvent.getKeyCode(); pressedKeys.remove(keyCode); } @Override public void keyTyped(KeyEvent keyEvent){} } }