In my video game I'm programming, I'm experiencing a bit of a delay in response to key presses. I'm hoping someone can help me out. Once in a while whenever I move the main character around, there seems to be a half-second delay between when I press the arrow key and when the character actually moves. I'm not sure why.
First, let me post my key binding method. This goes into the GamePanel class I created, which is the main JPanel that displays the game graphics. I give the ActionMap my InputProcessor class, which mainly deals with static variables, but also has a few non-static variables so that each KeyStroke can be associated with its own unique InputProcessor.
public void bindKeys() { // variable declarations: InputMap inputMap = getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); ActionMap actionMap = getActionMap(); int PRESSED = InputProcessor.PRESSED; int RELEASED = InputProcessor.RELEASED; int UP = KeyEvent.VK_UP; int RIGHT = KeyEvent.VK_RIGHT; int DOWN = KeyEvent.VK_DOWN; int LEFT = KeyEvent.VK_LEFT; int N = KeyEvent.VK_N; // new maze int R = KeyEvent.VK_R; // restart level int Q = KeyEvent.VK_Q; // quit int P = KeyEvent.VK_P; // pause // up arrow key inputMap.put(KeyStroke.getKeyStroke(UP, 0, false), "up_pressed"); actionMap.put("up_pressed", new InputProcessor(PRESSED, UP)); inputMap.put(KeyStroke.getKeyStroke(UP, 0, true), "up_released"); actionMap.put("up_released", new InputProcessor(RELEASED, UP)); // right arrow key inputMap.put(KeyStroke.getKeyStroke(RIGHT, 0, false), "right_pressed"); actionMap.put("right_pressed", new InputProcessor(PRESSED, RIGHT)); inputMap.put(KeyStroke.getKeyStroke(RIGHT, 0, true), "right_released"); actionMap.put("right_released", new InputProcessor(RELEASED, RIGHT)); // down arrow key inputMap.put(KeyStroke.getKeyStroke(DOWN, 0, false), "down_pressed"); actionMap.put("down_pressed", new InputProcessor(PRESSED, DOWN)); inputMap.put(KeyStroke.getKeyStroke(DOWN, 0, true), "down_released"); actionMap.put("down_released", new InputProcessor(RELEASED, DOWN)); // left arrow key inputMap.put(KeyStroke.getKeyStroke(LEFT, 0, false), "left_pressed"); actionMap.put("left_pressed", new InputProcessor(PRESSED, LEFT)); inputMap.put(KeyStroke.getKeyStroke(LEFT, 0, true), "left_released"); actionMap.put("left_released", new InputProcessor(RELEASED, LEFT)); // 'n' (for new maze) inputMap.put(KeyStroke.getKeyStroke(N, 0, false), "n_pressed"); actionMap.put("n_pressed", new InputProcessor(PRESSED, N)); // 'r' (for restart maze) inputMap.put(KeyStroke.getKeyStroke(R, 0, false), "r_pressed"); actionMap.put("r_pressed", new InputProcessor(PRESSED, R)); // 'q' (for quit) inputMap.put(KeyStroke.getKeyStroke(Q, 0, false), "q_pressed"); actionMap.put("q_pressed", new InputProcessor(PRESSED, Q)); // 'p' (for pause) inputMap.put(KeyStroke.getKeyStroke(P, 0, false), "p_pressed"); actionMap.put("p_pressed", new InputProcessor(PRESSED, P)); }
Here's the InputProcessor class (sorry for the length):
package game; import javax.swing.AbstractAction; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.util.Date; public class InputProcessor extends AbstractAction { /************************** EMBEDDED CLASS **************************/ /* The TimedKeyEvent class is to be placed in the timedKeyEventArray (below) * and represents a key having been pressed. */ private class TimedKeyEvent { public long time_stamp = -1; // when key was pressed public boolean held_down = false; // is it being held down? public int key_code = -1; // which key was it? (-1 = no key pressed) public TimedKeyEvent(int kc, boolean hd) { key_code = kc; held_down = hd; time_stamp = new Date().getTime(); } } /************************** END EMBEDDED CLASS **************************/ // key codes public static final int UP = KeyEvent.VK_UP; public static final int RIGHT = KeyEvent.VK_RIGHT; public static final int DOWN = KeyEvent.VK_DOWN; public static final int LEFT = KeyEvent.VK_LEFT; public static final int N = KeyEvent.VK_N; // new maze public static final int R = KeyEvent.VK_R; // restart level public static final int Q = KeyEvent.VK_Q; // quit public static final int P = KeyEvent.VK_P; // pause game // array to hold TimedKeyEvents. To be placed at index = TimedKeyEvent.key_code. // To be indexed using index_array (in case above key codes aren't consecutive on some systems // helm points to oldest TimedKeyEvent (among the arrow keys) public static final int ARRAY_SIZE = 256; private static TimedKeyEvent timedKeyEvents[] = new TimedKeyEvent[ARRAY_SIZE]; private static int helm = -1; private int index_array[] = {UP, RIGHT, DOWN, LEFT}; // true when GameControl needs to be notified when an arrow key has been released // (for enabling GoodGuy's movement) private static boolean notify_on_key_release = false; private static GameControl gameControl; // what type of key event was it? public final static int PRESSED = 1; public final static int RELEASED = 2; public final static int TYPED = 3; private final int key_event_type; // which key was it? private final int key_code; /************************** CONSTRUCTOR **************************/ // to be used for key binding only (doesn't initialize TimedKeyEvents array) public InputProcessor(int type, int code) { if (type != PRESSED && type != RELEASED && type != TYPED) { System.out.println("type = " + type + " in InputProcessor constructor, which is invalid."); } key_event_type = type; key_code = code; } /************************** END CONSTRUCTOR **************************/ /************************** ACTION PERFORMED **************************/ public void actionPerformed(ActionEvent actionEvent) { // arrow keys if (key_code == UP || key_code == RIGHT || key_code == DOWN || key_code == LEFT) { if (key_event_type == PRESSED) { timedKeyEvents[key_code] = new TimedKeyEvent(key_code, true); if (helm == -1) updateHelm(); } else if (key_event_type == RELEASED) { timedKeyEvents[key_code] = null; if (helm == key_code) updateHelm(); } } // 'n' (for new maze) if (key_code == N) { gameControl.getGameFrame().getEastPanel().actionPerformed(new ActionEvent( gameControl.getGameFrame().getEastPanel().getNewMazeButton(), 0, "")); } // 'r' (for restart level) if (key_code == R) { gameControl.getGameFrame().getEastPanel().actionPerformed(new ActionEvent( gameControl.getGameFrame().getEastPanel().getRestartLevelButton(), 0, "")); } // 'q' (for quit) if (key_code == Q) { gameControl.getGameFrame().getEastPanel().actionPerformed(new ActionEvent( gameControl.getGameFrame().getEastPanel().getQuitButton(), 0, "")); } // 'p' (for pause) if (key_code == P) { if (gameControl.getGameState() == gameControl.PAUSED) gameControl.unpause(); else gameControl.pause(); } // notify GameControl that key has been released (in case GoodGuy's movements needs enabling) if (key_event_type == RELEASED && notify_on_key_release) { if (gameControl.notify(key_code)) notify_on_key_release = false; // only set to false if GoodGuy DID need enabling (depends on which key it was) } } /************************** END ACTION PERFORMED **************************/ /************************** HELM CONTROL **************************/ // updates helm to point at arraw key with oldest time stamp private void updateHelm() { long oldest_time_stamp; // helm = -1 means not pointing at any key // figure out if an arrow key is held down and point to it (should be AT MOST one) if (helm == -1) { if (timedKeyEvents[RIGHT] != null) helm = RIGHT; if (timedKeyEvents[DOWN] != null) helm = DOWN; if (timedKeyEvents[LEFT] != null) helm = LEFT; if (timedKeyEvents[UP] != null) helm = UP; // if hilm != -1, see if key it points at is null // if so, find remaining arrow key with oldest time stamp // initialize helm to -1 so that if no arrow keys found, // it will remain at -1 indicating no arrow keys are down. } else if (timedKeyEvents[helm] == null) { oldest_time_stamp = new Date().getTime(); helm = -1; for (int i = 0; i < 4; i++) if (timedKeyEvents[index_array[i]] != null && timedKeyEvents[index_array[i]].time_stamp < oldest_time_stamp) { oldest_time_stamp = timedKeyEvents[index_array[i]].time_stamp; helm = index_array[i]; } } // if helm isn't pointing to null, no need to change it } /************************** END HELM CONTROL **************************/ /************************** ACCESSORS **************************/ /*** DOES/IS ***/ public static boolean doesRightHaveHelm() { if (helm == RIGHT) return timedKeyEvents[helm].held_down; else return false; } ///////////// public static boolean doesDownHaveHelm() { if (helm == DOWN) return timedKeyEvents[helm].held_down; else return false; } ///////////// public static boolean doesLeftHaveHelm() { if (helm == LEFT) return timedKeyEvents[helm].held_down; else return false; } ///////////// public static boolean doesUpHaveHelm() { if (helm == UP) return timedKeyEvents[helm].held_down; else return false; } ///////////// // this might pose problems for key typed events (ex. when traversing ballchain) public static boolean isArrowKeyPressed() { if (helm == RIGHT || helm == DOWN || helm == LEFT || helm == UP) return true; return false; } /*** RESET ***/ // to be used ONLY for reseting key typed events for arrow keys public static void resetArrowKeyTyped() { timedKeyEvents[helm] = null; helm = -1; } /*** SET ***/ public static void setGameControl(GameControl gc) { if (gc == null) { System.out.println("gc is null in InputProcessor.setGameControl()."); } gameControl = gc; } ///////////// public static void setNotifyOnKeyRelease() {notify_on_key_release = true;} /*** INIT ***/ public static void initVariables() { // initialize all TimedKeyEvents to null for (int i = 0; i < ARRAY_SIZE; i++) timedKeyEvents[i] = null; } /************************** END ACCESSORS **************************/ }
I'll briefly explain the InputProcessor class.
It deals with a static array of TimedKeyEvents. A TimedKeyEvent is a class I created and embedded in the InputProcessor. It basically represents a key being pressed and records the time at which it was pressed. When a key is pressed, a TimedKeyEvent is created and stored in the array at the index equal to its key code. When the key is released, that index is set to null.
Another static variable of importance is helm, an int that points to whichever TimedKeyEvent (out of the arrow keys only) is the oldest. This way, while holding down some arrow key, the player can simultaneously press down another arrow key without interfering with the main character's movement (it will basically ignore the newer key press). If the newer key press is released before the older key press is released, it will be totally ignored. However, if the older key press is released before the newer one is, then the helm will search for and point to the newer one, thereby moving the main character in that direction (essentially, the helm looks for the TimedKeyEvent (out of the arrow keys) with the oldest time stamp).
Finally, there are the does*HaveHelm() methods (doesRightHaveHelm(), doesLeftHaveHelm(),...). These are the methods called from the main character's move() method. If they return true, that means that arrow key * has the helm, and the main character should move in that direction.
This move() method is called once ever millisecond. That is to say, the main game loop, which includes a call to move(), is called in response to a Timer set to fire an ActionEvent every millisecond. This is where things get confusing. The delay I'm trying to debug definitely lasts longer than a millisecond (more like 500 milliseconds, or half a second). I don't see anywhere where a delay should occur. As soon as an arrow key is pressed, a TimedKeyEvent should immediately be created and stored in the array, and the helm should immediately point to it. Then within a millisecond, the main character's move() method should be called, and within it, a call to does*HaveHelm() should be made. It should return true in the case when * is the arrow key that has just been pressed, and at this point helm should already point to it. This should all occur in less than a millisecond.
Can anyone see what the problem is?
I can offer a few leads: 1) the delay usually happens when the release of one key occurs almost simultaneously with the pressing of another (the one that experiences a delay). This tells me the problem *might* have something to do with the helm searching for the TimedKeyEvent with the oldest time stamp in response to the one it last pointed at being released, but I still don't see where the delay could be coming from. 2) I've only ever experienced this problem on my laptop, which is a lot slower than my desktop on which I haven't experienced this problem at all.
Any help would be greatly appreciated.