I am trying to make a little game using java swing and have gotten pretty far but now my KeyListener is not using the method keyPressed.
Here is some of my code
public class Screen extends JPanel implements Runnable{
private static final int WIDTH = 300, HEIGHT = 300, RIGHT = 0, LEFT = 1, UP = 2, DOWN = 3, STILL = 4;
private Thread thread;
private boolean running = false;
private int direction = DOWN;
public Screen() {
setFocusable(true);
addKeyListener(new Key());
setPreferredSize(new Dimension(WIDTH, HEIGHT));
start();
}
public void tick(){
System.out.println(direction)
}
public void start() {
running = true;
thread = new Thread(this, "Game Loop");
thread.start();
}
public void run() {
while (running) {
tick();
repaint();
}
}
private class Key implements KeyListener{
#Override
public void keyTyped(KeyEvent e) {
// TODO Auto-generated method stub
}
#Override
public void keyPressed(KeyEvent e) {
int key = e.getKeyCode();
if (key == KeyEvent.VK_D) {
direction = RIGHT;
}
if (key == KeyEvent.VK_A) {
direction = LEFT;
}
if (key == KeyEvent.VK_W) {
direction = UP;
}
if (key == KeyEvent.VK_S) {
direction = DOWN;
}
}
#Override
public void keyReleased(KeyEvent e) {
// TODO Auto-generated method stub
}
}
}
I am looking at the console and expecting the output to change from 3 (which is DOWN) whatever button I press but it never does. After some time I realized that something was wrong with the keyPressed method, I just don't know what.
You don't need a separate "Game Loop" thread. Swing is event driven. Your key listener code will be called when the user presses a key on the computer keyboard.
As MadProgrammer mentioned in his comment to your question
modifying he state of the UI from outside the context of the EDT may cause painting issues
In other words, Swing is single threaded and that thread is referred to as the Event Dispatch Thread (EDT) and all your code that changes the GUI should be executed on the EDT only.
The below code is a stand-alone, Swing application that displays a focussable JPanel. When you press one of the relevant keyboard "direction" keys, i.e. A or D or S or W, the console displays the direction. Pressing any other key causes the direction STILL to be written to the console. Note that I used a enum for the various directions, rather than integer constants.
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.WindowConstants;
public class Screen01 extends JPanel implements Runnable {
public enum Directions {RIGHT, LEFT, UP, DOWN, STILL};
private static final int WIDTH = 300, HEIGHT = 300;
private Directions direction;
public Screen01() {
setPreferredSize(new Dimension(WIDTH, HEIGHT));
setFocusable(true);
addKeyListener(new Key());
}
public void run() {
createAndDisplayGui();
}
private void createAndDisplayGui() {
JFrame frame = new JFrame("Screen");
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.add(this, BorderLayout.CENTER);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
private class Key implements KeyListener {
#Override
public void keyTyped(KeyEvent e) {
// Do nothing.
}
#Override
public void keyPressed(KeyEvent e) {
int key = e.getKeyCode();
switch (key) {
case KeyEvent.VK_D:
direction = Directions.RIGHT;
break;
case KeyEvent.VK_A:
direction = Directions.LEFT;
break;
case KeyEvent.VK_W:
direction = Directions.UP;
break;
case KeyEvent.VK_S:
direction = Directions.DOWN;
break;
default:
direction = Directions.STILL;
}
System.out.println("Direction = " + direction);
}
#Override
public void keyReleased(KeyEvent e) {
// Do nothing.
}
}
public static void main(String[] args) {
EventQueue.invokeLater(new Screen01());
}
}
Make use of the key bindings API, it will solve this, and all other, KeyListener related issues without a lot of hacking and head scratching over why it "sometimes" works
Swing is single threaded. This means you should be careful about updating the state of the UI (directly or indirectly) from outside of the context of the Event Dispatching Thread, this also includes any state the UI might be dependent on.
Have a look at Concurrency in Swing for more details.
Without more details, one of the simplest solutions would be to use a Swing Timer to act as the primary "tick" action. Each time it "ticks" you'd inspect the state of the input and update the state of the model and trigger a repaint to update the UI
For example...
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.util.HashSet;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.Timer;
public class Main {
public static void main(String[] args) {
new Main();
}
public Main() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new JFrame();
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public static class TestPane extends JPanel {
enum Input {
UP, DOWN, LEFT, RIGHT
}
private Set<Input> input = new HashSet<>();
public TestPane() {
InputMap im = getInputMap(WHEN_IN_FOCUSED_WINDOW);
ActionMap am = getActionMap();
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0, false), "Left.pressed");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0, true), "Left.released");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0, false), "Right.pressed");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0, true), "Right.released");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0, false), "Up.pressed");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0, true), "Up.released");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0, false), "Down.pressed");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0, true), "Down.released");
am.put("Left.pressed", new InputAction(Input.LEFT, input, false));
am.put("Left.released", new InputAction(Input.LEFT, input, true));
am.put("Right.pressed", new InputAction(Input.RIGHT, input, false));
am.put("Right.released", new InputAction(Input.RIGHT, input, true));
am.put("Down.pressed", new InputAction(Input.DOWN, input, false));
am.put("Down.released", new InputAction(Input.DOWN, input, true));
am.put("Up.pressed", new InputAction(Input.UP, input, false));
am.put("Up.released", new InputAction(Input.UP, input, true));
input.add(Input.UP);
input.add(Input.DOWN);
input.add(Input.LEFT);
input.add(Input.RIGHT);
Timer timer = new Timer(5, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
// Check what's currently been pressed
// and update the state accordingly...
if (input.contains(Input.UP)) {
//..
}
if (input.contains(Input.DOWN)) {
//..
}
if (input.contains(Input.LEFT)) {
//..
}
if (input.contains(Input.RIGHT)) {
//..
}
repaint();
}
});
timer.start();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
int midX = getWidth() / 2;
int midY = getHeight() / 2;
FontMetrics fm = g2d.getFontMetrics();
int spaceWidth = fm.stringWidth("M");
int spaceHeight = fm.getHeight();
if (input.contains(Input.UP)) {
String text = "UP";
g2d.drawString(text,
midX - (fm.stringWidth(text)) / 2,
(midY - (spaceHeight * 2) + fm.getAscent()));
}
if (input.contains(Input.DOWN)) {
String text = "DOWN";
g2d.drawString(text,
midX - (fm.stringWidth(text)) / 2,
(midY + spaceHeight + fm.getAscent()));
}
if (input.contains(Input.LEFT)) {
String text = "LEFT";
g2d.drawString(text,
(midX - spaceWidth - fm.stringWidth(text)),
(midY + (fm.getAscent() / 2)));
}
if (input.contains(Input.RIGHT)) {
String text = "RIGHT";
g2d.drawString(text,
(midX + spaceWidth),
(midY + (fm.getAscent() / 2)));
}
g2d.dispose();
}
public class InputAction extends AbstractAction {
private Input input;
private Set<Input> inputSet;
private boolean onRelease;
public InputAction(Input input, Set<Input> inputSet, boolean onRelease) {
this.input = input;
this.inputSet = inputSet;
this.onRelease = onRelease;
}
#Override
public void actionPerformed(ActionEvent e) {
if (onRelease) {
inputSet.remove(input);
} else {
inputSet.add(input);
}
}
}
}
}
This is a basic example and prints the state of the key inputs directly within the paintComponent method. A more realistic implementation would use the Timer's ActionListener to inspect the input state and make changes to the desired model accordingly, which is hinted at in the example.
If you require more direct control, then you need to avoid the Swing painting system and take control yourself. For this, you'd need to use a BufferStrategy. This is a more complicated solution, but it is also more powerful, as you gain complete control over when the UI is updated
Related
So I have a painted rectangle that I want to move with the arrow keys that includes diagonal movement, or rather allowance of multiple keys being pressed at the same time (In other words, movement that is similar to player movement in a 2D game). I have attempted to do this with a KeyListener, which was not working. So I decided to move to KeyBindings and I found an example from this website: https://coderanch.com/t/606742/java/key-bindings (Rob Camick's post)
I directly copied the code and it works as intended, except it is moving an icon and not a painted rectangle like I want to do. I have attempted to modify the code so that it would move a painted rectangle, but was only failing. Here is my latest attempt, which is also a minimal reproducible:
import java.awt.event.*;
import java.awt.*;
import javax.swing.*;
public class Test extends JPanel implements ActionListener
{
public JPanel component = this;
public int x = 100;
public int y = 100;
private Timer timer;
private int keysPressed;
private InputMap inputMap;
public void addAction(String name, int keyDeltaX, int keyDeltaY, int keyCode)
{
new NavigationAction(name, keyDeltaX, keyDeltaY, keyCode);
}
// Move the component to its new location
private void handleKeyEvent(boolean pressed, int deltaX, int deltaY)
{
keysPressed += pressed ? 1 : -1;
x += deltaX;
y += deltaY;
// Start the Timer when the first key is pressed
if (keysPressed == 1)
timer.start();
// Stop the Timer when all keys have been released
if (keysPressed == 0)
timer.stop();
}
// Invoked when the Timer fires
public void actionPerformed(ActionEvent e)
{
repaint();
}
class NavigationAction extends AbstractAction implements ActionListener
{
private int keyDeltaX;
private int keyDeltaY;
private KeyStroke pressedKeyStroke;
private boolean listeningForKeyPressed;
public NavigationAction(String name, int keyDeltaX, int keyDeltaY, int keyCode)
{
super(name);
this.keyDeltaX = keyDeltaX;
this.keyDeltaY = keyDeltaY;
pressedKeyStroke = KeyStroke.getKeyStroke(keyCode, 0, false);
KeyStroke releasedKeyStroke = KeyStroke.getKeyStroke(keyCode, 0, true);
inputMap.put(pressedKeyStroke, getValue(Action.NAME));
inputMap.put(releasedKeyStroke, getValue(Action.NAME));
component.getActionMap().put(getValue(Action.NAME), this);
listeningForKeyPressed = true;
}
public void actionPerformed(ActionEvent e)
{
// While the key is held down multiple keyPress events are generated,
// we only care about the first event generated
if (listeningForKeyPressed)
{
handleKeyEvent(true, keyDeltaX, keyDeltaY);
inputMap.remove(pressedKeyStroke);
listeningForKeyPressed = false;
}
else // listening for key released
{
handleKeyEvent(false, -keyDeltaX, -keyDeltaY);
inputMap.put(pressedKeyStroke, getValue(Action.NAME));
listeningForKeyPressed = true;
}
}
}
public void paintComponent(Graphics tool) {
super.paintComponent(tool);
System.out.println(x + ", " + y);
tool.drawRect(x, y, 50, 50);
}
public Test() {}
public static void main(String[] args)
{
new Test().create();
}
public void create() {
JFrame frame = new JFrame();
frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
frame.setSize(600, 600);
frame.setLocationRelativeTo( null );
frame.getContentPane().add(component);
inputMap = component.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
timer = new Timer(24, this);
timer.setInitialDelay( 0 );
addAction("Left", -3, 0, KeyEvent.VK_LEFT);
addAction("Right", 3, 0, KeyEvent.VK_RIGHT);
addAction("Up", 0, -3, KeyEvent.VK_UP);
addAction("Down", 0, 3, KeyEvent.VK_DOWN);
frame.setVisible(true);
}
}
Okay, so one of the issues I have with the code you've presented is the fact that you're leak responsibility. It's not the responsibility of the NavigationAction to register key bindings or modify the state the of UI. It should only be responsible for generating a notification that the state has changed back to a responsible handler.
This could be achieved through the use of some kind of model, which maintains the some kind of state, which is updated by the NavigationAction and read by the UI or, as I've chosen to do, via a delegation callback.
Another issue is, what happens when the user releases the key? Another issue is, how do you deal with the delay in repeated key press (ie when you hold the key down, there is a small delay between the first key event and the repeating key events)?
We can deal with these things through a simple change in mind set. Instead of using the NavigationAction to determine "what" should happen, it should be used to tell our UI "when" something has happened. In this a directional key has been pressed or released.
Instead of reacting to key stroke directly, we either "add" or "remove" the direction from a state model and allow the Swing Timer to update the state of the UI accordingly, this will generally be faster and smoother.
It also has the side effect of decoupling logic, as you no longer need to care about "how" the state was changed, only that it was and you can then decide how you want to react to it.
This approach is generally commonly used (in some form or another) in gaming engines.
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.util.HashSet;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.Timer;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new JFrame();
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public interface KeyActionHandler {
public void keyActionPerformed(Direction direction, boolean pressed);
}
public enum Direction {
LEFT, RIGHT, DOWN, UP;
}
class NavigationAction extends AbstractAction implements ActionListener {
private KeyActionHandler keyActionHandler;
private Direction direction;
private Boolean pressed;
public NavigationAction(Direction direction, boolean pressed, KeyActionHandler keyActionHandler) {
this.direction = direction;
this.pressed = pressed;
this.keyActionHandler = keyActionHandler;
}
public void actionPerformed(ActionEvent e) {
keyActionHandler.keyActionPerformed(direction, pressed);
}
}
public class TestPane extends JPanel implements ActionListener, KeyActionHandler {
private int xDelta = 0;
private int yDelta = 0;
public int x = 100;
public int y = 100;
private Timer timer;
public TestPane() {
timer = new Timer(24, this);
InputMap inputMap = getInputMap(WHEN_IN_FOCUSED_WINDOW);
ActionMap actionMap = getActionMap();
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), "Pressed-Left");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), "Pressed-Right");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, false), "Pressed-Up");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, false), "Pressed-Down");
actionMap.put("Pressed-Left", new NavigationAction(Direction.LEFT, true, this));
actionMap.put("Pressed-Right", new NavigationAction(Direction.RIGHT, true, this));
actionMap.put("Pressed-Up", new NavigationAction(Direction.UP, true, this));
actionMap.put("Pressed-Down", new NavigationAction(Direction.DOWN, true, this));
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, true), "Released-Left");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, true), "Released-Right");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, true), "Released-Up");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, true), "Released-Down");
actionMap.put("Released-Left", new NavigationAction(Direction.LEFT, false, this));
actionMap.put("Released-Right", new NavigationAction(Direction.RIGHT, false, this));
actionMap.put("Released-Up", new NavigationAction(Direction.UP, false, this));
actionMap.put("Released-Down", new NavigationAction(Direction.DOWN, false, this));
}
#Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
#Override
public void addNotify() {
super.addNotify();
timer.start();
}
#Override
public void removeNotify() {
super.removeNotify();
timer.stop();
}
private Set<Direction> directions = new HashSet<>();
// Invoked when the Timer fires
#Override
public void actionPerformed(ActionEvent e) {
if (directions.contains(Direction.LEFT)) {
x -= 3;
} else if (directions.contains(Direction.RIGHT)) {
x += 3;
}
if (directions.contains(Direction.UP)) {
y -= 3;
} else if (directions.contains(Direction.DOWN)) {
y += 3;
}
repaint();
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawRect(x, y, 50, 50);
}
#Override
public void keyActionPerformed(Direction direction, boolean pressed) {
if (pressed) {
directions.add(direction);
} else {
directions.remove(direction);
}
}
}
}
This is just one more approach to solve the same problem ;)
However, I am encountering a problem: it seems that removeNotify() is never called. I wanted to verify this and copied this post and pasted it into a new java file. I then added print statements in the add and remove notify methods, and only the print statement in the addNotify() method was printed. Also, if you add a print statement in the actionPerformed method, it prints repeatedly never ending
removeNotify is only called when the component, or a parent container, is removed.
import java.awt.Container;
import java.awt.Dimension;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new JFrame();
frame.add(new TestPane());
frame.pack();
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
public TestPane() {
setLayout(new GridBagLayout());
JButton btn = new JButton("Remove");
btn.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent evt) {
Container parent = getParent();
parent.remove(TestPane.this);
parent.revalidate();
parent.repaint();
}
});
add(btn);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
#Override
public void addNotify() {
super.addNotify();
System.out.println("Add");
}
#Override
public void removeNotify() {
super.removeNotify();
System.out.println("Remove");
}
}
}
I verified that removeNotify will be called through the hierarchy when a parent container is removed.
I'm new to swing and I am trying to make the square move but only when the key is released (ex W) but when i hold down the key the square just moves
KeyListener Class
I want to make sure that a key was pressed and if it is still pressed it should return false but true if it was pressed then released
package javaGD.GameAssistant.Input;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
public class KeyManager implements KeyListener {
private boolean[] keys;
public KeyManager() {
keys = new boolean[256];
}
#Override
public void keyPressed(KeyEvent e) {
keys[e.getKeyCode()] = true;
}
#Override
public void keyReleased(KeyEvent e) {
keys[e.getKeyCode()] = false;
}
#Override
public void keyTyped(KeyEvent e) {
}
public boolean KeyPressed(int keycode) {
return keys[keycode];
}
public boolean KeyReleased(int keycode) {
return keys[keycode];
}
}
Class where the square should move.
KeyListener is inherited from GameAssistant(JFrame is created with the KeyListener)
package TestCode;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import javaGD.GameAssistant.GameAssistant;
public class Game extends GameAssistant {
public int x, xSpeed, y, ySpeed;
public Game(String title, int width, int height, boolean makeResizable) {
super(title, width, height, makeResizable);
}
#Override
public void setup() {
x = 0;
y = 0;
xSpeed = 5;
ySpeed = 5;
}
#Override
public void update() {
if (this.Keyboard().KeyReleased(KeyEvent.VK_D)) {
x += xSpeed;
}
if (this.Keyboard().KeyReleased(KeyEvent.VK_A)) {
x -= xSpeed;
}
if (this.Keyboard().KeyReleased(KeyEvent.VK_W)) {
y -= ySpeed;
}
if (this.Keyboard().KeyReleased(KeyEvent.VK_S)) {
y += ySpeed;
}
}
#Override
public void draw(Graphics g) {
g.setColor(Color.BLACK);
g.fillRect(x, y, 20, 20);
}
}
KeyReleased should be returning !keys[keycode] Otherwise it will return false when released and true when pressed
public boolean KeyReleased(int keycode) {
return !keys[keycode];
}
I would also recommend using the Key bindings API over KeyListener as it's more reliable and re-usable.
If you are only planning on a limited number of input operations, I would use Set and a enum, this way you can decouple the "how" from the "what".
You update method doesn't care "how" the inputs are managed, only the "what is the state"
Conceptually, maybe something like...
public enum GameInput {
UP, DOWN, LEFT, RIGHT;
}
public class KeyManager implements KeyListener {
private Set<GameInput> inputs = new HashSet<>();
public KeyManager() {
}
#Override
public void keyPressed(KeyEvent e) {
// Check the key code, verify if it's one of the configured
// actions keys
// The key code could come from a configuration file which might
// be customisable by the user...
if (e.getKeyCode() == KeyEvent.VK_W) {
inputs.add(GameInput.UP);
} else if (e.getKeyCode() == KeyEvent.VK_S) {
// etc...
} // etc...
}
#Override
public void keyReleased(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_W) {
inputs.remove(GameInput.UP);
} else if (e.getKeyCode() == KeyEvent.VK_S) {
// etc...
} // etc...
}
#Override
public void keyTyped(KeyEvent e) {
}
public boolean isKeyPressed(GameInput input) {
return inputs.contains(input);
}
public boolean isKeyReleased(GameInput input) {
return !isKeyPressed(input);
}
}
And your update method might look like...
#Override
public void update() {
if (this.Keyboard().isKeyReleased(GameInput.RIGHT)) {
x += xSpeed;
}
if (this.Keyboard().isKeyReleased(GameInput.LEFT)) {
x -= xSpeed;
}
if (this.Keyboard().isKeyReleased(GameInput.UP)) {
y -= ySpeed;
}
if (this.Keyboard().isKeyReleased(GameInput.DOWN)) {
y += ySpeed;
}
}
Now your update method doesn't care "how" the inputs are generated, only what to do when they are (or aren't) set.
Personally, I'd use a InputManager class and further decouple it, so inputs could be generate via other means, like buttons, mouse inputs, game pads, etc...
Runnable example...
Conceptually, this should work. I say "conceptually", because while I was testing I came across a number of "weird" issues that I can't contribute to either Java (1.8) or MacOS - or because they are a combination of both...more details below...
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.util.HashSet;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.Timer;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new JFrame();
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
private Box box = new Box();
public TestPane() {
InputMap im = getInputMap(WHEN_IN_FOCUSED_WINDOW);
ActionMap am = getActionMap();
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0, false), "Up.pressed");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0, true), "Up.released");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0, false), "Down.pressed");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0, true), "Down.released");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0, false), "Left.pressed");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0, true), "Left.released");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0, false), "Right.pressed");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0, true), "Right.released");
am.put("Up.pressed", new KeyAction(InputAction.UP, true));
am.put("Up.released", new KeyAction(InputAction.UP, false));
am.put("Down.pressed", new KeyAction(InputAction.DOWN, true));
am.put("Down.released", new KeyAction(InputAction.DOWN, false));
am.put("Left.pressed", new KeyAction(InputAction.LEFT, true));
am.put("Left.released", new KeyAction(InputAction.LEFT, false));
am.put("Right.pressed", new KeyAction(InputAction.RIGHT, true));
am.put("Right.released", new KeyAction(InputAction.RIGHT, false));
Timer timer = new Timer(5, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
box.update(getBounds());
repaint();
}
});
timer.start();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
box.draw(g);
}
}
public class Box {
public int x, xSpeed, y, ySpeed, width, height;
public Box() {
x = 0;
y = 0;
xSpeed = 1;
ySpeed = 1;
width = 10;
height = 10;
}
public void update(Rectangle bounds) {
if (!InputManager.INSTANCE.isSet(InputAction.LEFT)) {
x -= xSpeed;
}
if (!InputManager.INSTANCE.isSet(InputAction.RIGHT)) {
x += xSpeed;
}
if (InputManager.INSTANCE.isSet(InputAction.UP) && InputManager.INSTANCE.isSet(InputAction.DOWN)) {
//
} else if (!InputManager.INSTANCE.isSet(InputAction.UP)) {
y -= ySpeed;
} else if (!InputManager.INSTANCE.isSet(InputAction.DOWN)) {
y += ySpeed;
}
if (x < bounds.x) {
x = 0;
} else if (x + width > (bounds.x + bounds.width)) {
x = bounds.x + (bounds.width - width);
}
if (y < bounds.y) {
y = 0;
} else if (y + height > (bounds.y + bounds.height)) {
y = bounds.y + (bounds.height - height);
}
}
public void draw(Graphics g) {
g.setColor(Color.BLACK);
g.fillRect(x, y, width, height);
}
}
public enum InputAction {
UP, DOWN, LEFT, RIGHT;
}
public enum InputManager {
INSTANCE;
private Set<InputAction> actions = new HashSet<>();
public void set(InputAction action) {
actions.add(action);
}
public void remove(InputAction action) {
actions.remove(action);
}
public boolean isSet(InputAction action) {
return actions.contains(action);
}
}
public class KeyAction extends AbstractAction {
private InputAction action;
private boolean apply;
public KeyAction(InputAction action, boolean apply) {
this.action = action;
this.apply = apply;
}
#Override
public void actionPerformed(ActionEvent e) {
System.out.println("!");
if (apply) {
System.out.println("Apply " + action);
InputManager.INSTANCE.set(action);
} else {
System.out.println("Remove " + action);
InputManager.INSTANCE.remove(action);
}
}
}
}
TL;DR
While testing the above example, I came across a bizarre number of issues, I've not seen before (not that I've been doing this kind of thing recently).
When using KeyListener, if I pressed (and held) two buttons, I could see the "pressed" action, but there was no repeating events, which is something I would normally expect (for any keys). When I released on key, I saw the "release" action, but when I pressed (and held it), no new "press" action was generated.
I tried the key bindings API (as demonstrated above) and still had no success (similar results).
I then attached a AWTEventListener directly to the event queue and monitored ALL the key strokes.
I noted that, some times (even is just tapping a key repeatedly) that "pressed" might not be generated.
I also noted that holding one or more keys down, releasing and pressing a key again, more often then not, did not generate a new press event (only release events)
I'm using macOS 10.13.6 and Java 1.8.0_144-b01 - it could be bug in either or both, but I don't have the means to test it otherwise
Update...
So, after updating from Java 1.8 to Java 1.10, the above mentioned issue seems to be resoled - however, this highlights another, hardware issue, where only a certain number of keys can be actively pressed at a time - See How do I remove the limit on PC keyboard button presses? for more details
I'm working on a game using Swing and I'm using KeyBindings for input from the keyboard.
I'm having issues where the KeyBindings stop responding. It happens every time I run the application, but as far as I can tell, it's not when a certain chain of events occurs. The KeyBindings just stop receiving input from the keyboard. I also use mouse input, which continues to work, so I know it's not related to input in general.
Some things I've tried:
made sure my object was not garbage collected
looked for a certain reason the issue occurs (ex: after a certain amount of time, a certain key combination pressed), to which I could find none
tried to use a KeyListener instead
None of these worked.
I then copied the project (without changing any code) to a windows machine, and after testing, I could not get the problem to occur once. I didn't paste code here because I'm 99% positive this is not related to my code, but related to the operating system it's running on.
Is this a problem with the macOS, or the JDK for Mac, or something else I don't know about?
I'm using JDK version 8 update 112, the computer is running macOS Sierra version 10.12.2.
It seems someone else had the same problem I now do, but they never got an answer.
EDIT
Here's a code example where the problem occurs:
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
//an example of the problem where the keyboard stops receiving input randomly
public class ProblemExample extends JPanel {
private static final long serialVersionUID = 1L;
private int xPos = 200, yPos = 200;
private boolean wKey, aKey, sKey, dKey;
private BufferedImage image; //sprite
public ProblemExample() {
JFrame frame = new JFrame();
frame.add(this);
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setResizable(false);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
//makes the sprite a red square
image = new BufferedImage(50, 50, BufferedImage.TYPE_INT_ARGB);
int[] redPixels = new int[50 * 50];
for (int i = 0; i < redPixels.length; i++) {
redPixels[i] = 0xffff0000;
}
image.setRGB(0, 0, 50, 50, redPixels, 0, 50);
initializeKeys();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(800, 600);
}
//sets up Key Bindings
private void initializeKeys() {
final String W = "W",
A = "A",
S = "S",
D = "D",
PRESSED = "PRESSED",
RELEASED = "RELEASED";
InputMap inputMap = this.getInputMap(JPanel.WHEN_IN_FOCUSED_WINDOW);
ActionMap actionMap = this.getActionMap();
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0, false), W + PRESSED);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0, false), A + PRESSED);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0, false), S + PRESSED);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0, false), D + PRESSED);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0, true), W + RELEASED);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0, true), A + RELEASED);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0, true), S + RELEASED);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0, true), D + RELEASED);
Action wActionPressed = new AbstractAction() {
#Override
public void actionPerformed(ActionEvent e) {
wKey = true;
}
};
Action aActionPressed = new AbstractAction() {
#Override
public void actionPerformed(ActionEvent e) {
aKey = true;
}
};
Action sActionPressed = new AbstractAction() {
#Override
public void actionPerformed(ActionEvent e) {
sKey = true;
}
};
Action dActionPressed = new AbstractAction() {
#Override
public void actionPerformed(ActionEvent e) {
dKey = true;
}
};
Action wActionReleased = new AbstractAction() {
#Override
public void actionPerformed(ActionEvent e) {
wKey = false;
}
};
Action aActionReleased = new AbstractAction() {
#Override
public void actionPerformed(ActionEvent e) {
aKey = false;
}
};
Action sActionReleased = new AbstractAction() {
#Override
public void actionPerformed(ActionEvent e) {
sKey = false;
}
};
Action dActionReleased = new AbstractAction() {
#Override
public void actionPerformed(ActionEvent e) {
dKey = false;
}
};
actionMap.put(W + PRESSED, wActionPressed);
actionMap.put(A + PRESSED, aActionPressed);
actionMap.put(S + PRESSED, sActionPressed);
actionMap.put(D + PRESSED, dActionPressed);
actionMap.put(W + RELEASED, wActionReleased);
actionMap.put(A + RELEASED, aActionReleased);
actionMap.put(S + RELEASED, sActionReleased);
actionMap.put(D + RELEASED, dActionReleased);
}
public void loop() {
if (wKey) yPos -= 5;
if (aKey) xPos -= 5;
if (sKey) yPos += 5;
if (dKey) xPos += 5;
repaint();
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawImage(image, xPos, yPos, null);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
ProblemExample example = new ProblemExample();
Timer timer = new Timer(60, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
example.loop();
}
});
timer.start();
}
});
}
}
You might think this is a bug in MAC, while it is not Because MAC has the feature of secondary keys. As you are the user of MAC, I hope you are aware of this feature. This feature might be the the cause of your problem.
The Secondary key feature of MAC : is used where holding down a letter key will display variations of that letter, like holding down “u” to get “ü.” This comes in handy when spelling non-English words.
There’s a simple way to take control and change the behavior of long key presses to accommodate your needs.
Open the Terminal app and write:
defaults write NSGlobalDomain ApplePressAndHoldEnabled -bool false
Then restart any open app in which you want this setting to activate.
REVERTING BACK: Just,simply add true in place of false to the previous command like this:
defaults write NSGlobalDomain ApplePressAndHoldEnabled -bool true
UPDATE:
You can speed up the rate of key repeats or decrease the delay before a held key starts repeating, by going to the System Preferences, and make changes under the keyboard header.
Besides what Tahir Hussain Mir suggest about changing the Mac key settings, I find the problem lies with the looping, it'd be more efficient to implement it in a event-driven way, I took the liberty to change your code a bit. Together with Hussain Mir suggest 's solution, it should solve your problem.
You could also handle key-repeating yourself, by e.g., starting a timer on key press and stoping it on key release, but then the key pressing behavior will differ between Windows and Mac, which isn't really the way you want to go.
package swing;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
//an example of the problem where the keyboard stops receiving input randomly
public class SOMacKeyBindings extends JPanel
{
private BufferedImage image; //sprite
private Point point = new Point(200, 200);
private int steps = 5;
private class KeyAction extends AbstractAction
{
private Runnable runnable;
public KeyAction(Runnable runnable)
{
this.runnable = runnable;
}
#Override
public void actionPerformed(ActionEvent e)
{
runnable.run();
}
}
public SOMacKeyBindings()
{
JFrame frame = new JFrame();
frame.add(this);
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setResizable(false);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
//makes the sprite a red square
image = new BufferedImage(50, 50, BufferedImage.TYPE_INT_ARGB);
int[] redPixels = new int[50 * 50];
for (int i = 0; i < redPixels.length; i++)
{
redPixels[i] = 0xffff0000;
}
image.setRGB(0, 0, 50, 50, redPixels, 0, 50);
initializeKeys();
}
#Override
public Dimension getPreferredSize()
{
return new Dimension(800, 600);
}
//sets up Key Bindings
private void initializeKeys()
{
final String W = "W",
A = "A",
S = "S",
D = "D",
PRESSED = "PRESSED";
InputMap inputMap = this.getInputMap(JPanel.WHEN_IN_FOCUSED_WINDOW);
ActionMap actionMap = this.getActionMap();
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0, false), W + PRESSED);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0, false), A + PRESSED);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0, false), S + PRESSED);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0, false), D + PRESSED);
actionMap.put(W + PRESSED, new KeyAction(() -> { point.y -= steps; repaint(); }));
actionMap.put(A + PRESSED, new KeyAction(() -> { point.x -= steps; repaint(); }));
actionMap.put(S + PRESSED, new KeyAction(() -> { point.y += steps; repaint(); }));
actionMap.put(D + PRESSED, new KeyAction(() -> { point.x += steps; repaint(); }));
}
#Override
public void paintComponent(Graphics g)
{
super.paintComponent(g);
g.drawImage(image, point.x, point.y, null);
}
public static void main(String[] args)
{
SwingUtilities.invokeLater(() -> new SOMacKeyBindings());
}
}
When I run the code, the added Example1 class to the JOptionPane (in Frame) should get keyInput and then change the y value of the player instance (in example1), but it doesn't work. Also, how would I be able to rotate the ship on its axis and then move in the direction its facing? Currently it moves in the direction its rotated towards, but it rotates on what seems the coordinates 0,0.
Frame
import javax.swing.*;
import java.awt.*;
/**
* Created by griffin on 12/7/2015.
*/
public class Frame extends JFrame {
public Frame() {
initUI();
}
private void initUI() {
JTabbedPane jtp = new JTabbedPane();
Example1 e1 = new Example1();
Example2 e2 = new Example2();
getContentPane().add(jtp);
jtp.add(e1);
jtp.add(e2);
jtp.addTab("Example 1", e1);
jtp.addTab("Example 2", e2);
setResizable(false);
pack();
setTitle("NLTP");
setLocationRelativeTo(null);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new Frame();
frame.setVisible(true);
}
});
}
}
Example1
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.geom.AffineTransform;
/**
* Created by griffin on 12/7/2015.
*/
public class Example1 extends JPanel implements Runnable {
private final int B_WIDTH = 640;
private final int B_HEIGHT = 480;
private int DELAY = 25;
private Thread thread;
Ship player = new Ship(320, 240);
public Example1() {
initScreen();
}
private void initScreen() {
JButton explanation = new JButton("Explanation");
explanation.setBounds(0, 0, 150, 30);
this.setLayout(null);
add(explanation);
addKeyListener(new keyInput());
setFocusable(true);
setBackground(Color.BLACK);
setPreferredSize(new Dimension(B_WIDTH, B_HEIGHT));
setDoubleBuffered(true);
}
#Override
public void addNotify() {
super.addNotify();
thread = new Thread(this);
thread.start();
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
draw(g);
}
public void draw(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
AffineTransform old = g2d.getTransform();
g2d.rotate(Math.toRadians(player.angle));
g2d.drawImage(player.ship, player.x, player.y, null);
g2d.setTransform(old);
Toolkit.getDefaultToolkit().sync();
}
public void cycle() {
player.y += player.vY;
}
#Override
public void run() {
long beforeTime, timeDiff, sleep;
beforeTime = System.currentTimeMillis();
while (true) {
cycle();
repaint();
timeDiff = System.currentTimeMillis() - beforeTime;
sleep = DELAY - timeDiff;
if (sleep < 0) {
sleep = 2;
}
try {
Thread.sleep(sleep);
} catch (InterruptedException e) {
System.out.println("Interrupted: " + e.getMessage());
}
beforeTime = System.currentTimeMillis();
}
}
public class keyInput extends KeyAdapter {
#Override
public void keyReleased(KeyEvent e) {
int key = e.getKeyCode();
if(key == KeyEvent.VK_W) {
}
}
#Override
public void keyTyped(KeyEvent e) {
}
#Override
public void keyPressed(KeyEvent e) {
int key = e.getKeyCode();
if (key == KeyEvent.VK_W) {
player.vY -= 1;
}
if(key == KeyEvent.VK_A) {
player.angle++;
}
if(key == KeyEvent.VK_D) {
player.angle--;
}
}
}
}
KeyListener is well known for having focus related issues. Not only does the component need to be focusable, but it has to have keyboard focus before it will register key events. This is an issue as it's all to easy for your component to lose focus for any number of reasons.
The solution is to use the key bindings API which was designed to help solve this issue. See How to Use Key Bindings for more details.
Swing uses a passive rendering approach. That is, updates occur when ever the API decides something needs to be updated. See Painting in AWT and Swing for more details
What you really need is some kind of active rendering approach, where updates are made on a regular bases. This can be accomplished in a verity of ways, the simplest would be to use a Swing Timer, as it's safe to update the UI (or values that the UI relies on) without the risk of introducing race conditions. See How to use Swing Timers for more details
So, this example basically abstracts the input into four basic movements, rotate left/right and up/down. The code doesn't care how those inputs are generated, only that they can be.
It then uses the key bindings API to register actions for the inputs. This is all done with a single Action class, which just adds the Input to a Set, which the Timer uses to determine what actions should be applied before requesting the UI be repainted.
The rotation is accomplished through the use of a AffineTransform. First we translate the origin to the sprites current x/y position, then rotate it around the center of the sprite. This reduces a lot of the complexity/issues related to rotating things (me simple)
AffineTransform at = AffineTransform.getTranslateInstance(xPos, yPos);
at.rotate(Math.toRadians(angle), sprite.getWidth() / 2, sprite.getHeight() / 2);
g2d.setTransform(at);
g2d.drawImage(sprite, 0, 0, this);
Just beware, a Graphics context is a shared resource, before making these type of changes, you need to make a copy of it (and dispose of it when you are finished)
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import javax.imageio.ImageIO;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class FlyingPoniesWithGuns {
public static void main(String[] args) {
new FlyingPoniesWithGuns();
}
public FlyingPoniesWithGuns() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
} catch (IOException ex) {
ex.printStackTrace();
}
}
});
}
public enum Input {
ROTATE_LEFT,
ROTATE_RIGHT,
UP,
DOWN
}
public class TestPane extends JPanel {
private BufferedImage sprite;
private double angle;
private int xPos, yPos;
private double xDelta, yDelta;
private Set<Input> inputs;
public TestPane() throws IOException {
inputs = new HashSet<>(25);
sprite = ImageIO.read(getClass().getResource("/Pony.png"));
xPos = (400 - sprite.getWidth()) / 2;
yPos = (400 - sprite.getHeight()) / 2;
addKeyBinding("rotate-left", KeyEvent.VK_A, Input.ROTATE_LEFT);
addKeyBinding("rotate-right", KeyEvent.VK_D, Input.ROTATE_RIGHT);
Timer timer = new Timer(40, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if (inputs.contains(Input.ROTATE_LEFT)) {
angle -= 5;
} else if (inputs.contains(Input.ROTATE_RIGHT)) {
angle += 5;
}
repaint();
}
});
timer.start();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
AffineTransform at = AffineTransform.getTranslateInstance(xPos, yPos);
at.rotate(Math.toRadians(angle), sprite.getWidth() / 2, sprite.getHeight() / 2);
g2d.setTransform(at);
g2d.drawImage(sprite, 0, 0, this);
g2d.dispose();
}
protected void addKeyBinding(String name, int keyCode, Input input) {
InputMap inputMap = getInputMap(WHEN_IN_FOCUSED_WINDOW);
ActionMap actionMap = getActionMap();
inputMap.put(KeyStroke.getKeyStroke(keyCode, 0, false), name + ".pressed");
actionMap.put(name + ".pressed", new InputAction(input, true));
inputMap.put(KeyStroke.getKeyStroke(keyCode, 0, true), name + ".released");
actionMap.put(name + ".released", new InputAction(input, false));
}
protected class InputAction extends AbstractAction {
private Input input;
private boolean pressed;
public InputAction(Input input, boolean pressed) {
this.input = input;
this.pressed = pressed;
}
#Override
public void actionPerformed(ActionEvent e) {
if (pressed) {
inputs.add(input);
} else {
inputs.remove(input);
}
}
}
}
}
Okay, what about movement? You could have a look at How do I make an entity move in a direction? and How can I move a sprite in the direction it is facing? for more ideas on how you might be able to achieve that
So I have seen a few threads about this already and I need some help about how to fix it specifically. When you hold down a key, Java will read the first key press, then there will be a small delay, and then it will continuously read the key press until you release the key.
public void keyPressed(KeyEvent key) {
int code = key.getKeyCode();
if (code == KeyEvent.VK_DOWN) {
//Do stuff
}
if (code == KeyEvent.VK_LEFT) {
//Do stuff
}
if (code == KeyEvent.VK_RIGHT) {
//Do stuff
}
if (code == KeyEvent.VK_UP) {
//Do stuff
}
}
That is my current code. I heard that to fix this you can create a timer which rapidly checks for key presses, but I'm not really sure how to do that. Would appreciate some help here or if there is a better solution.
The basic answer to your question is, you can't, the delay is OS specific.
The longer answer is, you should be ignoring the individual events themselves and monitor a change in state (between press and release) through the use of appropriate flags.
This means, that when a key is pressed, you set some flag which you program can use to change the state of the program and when released, you reset it.
This disassociates the event from state change and provides you with much more flexibility, as your program doesn't care what caused the state change, only that the state has changed and it should react to it.
This will require you to have some kind of "loop" whose responsibility it is, is to monitor this change and react to it accordingly. In gaming, this is commonly known as a "game-loop", but can also been known as "main-loop".
It's this "loops" responsibility to update the state of the program and get it painted.
Below is a very simple example which uses the key bindings API and a javax.swing.Timer to demonstrate the basic concepts
import com.sun.glass.events.KeyEvent;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class MoveMe {
public static void main(String[] args) {
new MoveMe();
}
public MoveMe() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class MovementState {
public int xDirection;
public int yDirection;
}
public class TestPane extends JPanel {
private MovementState movementState;
private Rectangle box;
public TestPane() {
movementState = new MovementState();
InputMap im = getInputMap(WHEN_IN_FOCUSED_WINDOW);
ActionMap am = getActionMap();
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, false), "down-pressed");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, true), "down-released");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, false), "up-pressed");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, true), "up-released");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), "left-pressed");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, true), "left-released");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), "right-pressed");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, true), "right-released");
am.put("down-pressed", new YDirectionAction(movementState, 2));
am.put("down-released", new YDirectionAction(movementState, 0));
am.put("up-pressed", new YDirectionAction(movementState, -2));
am.put("up-released", new YDirectionAction(movementState, 0));
am.put("left-pressed", new XDirectionAction(movementState, -2));
am.put("left-released", new XDirectionAction(movementState, 0));
am.put("right-pressed", new XDirectionAction(movementState, 2));
am.put("right-released", new XDirectionAction(movementState, 0));
box = new Rectangle(90, 90, 20, 20);
Timer timer = new Timer(40, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
box.x += movementState.xDirection;
box.y += movementState.yDirection;
if (box.x < 0) {
box.x = 0;
} else if (box.x + box.width > getWidth()) {
box.x = getWidth() - box.width;
}
if (box.y < 0) {
box.y = 0;
} else if (box.y + box.height > getHeight()) {
box.y = getHeight() - box.height;
}
repaint();
}
});
timer.start();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(Color.RED);
g2d.fill(box);
g2d.dispose();
}
}
public abstract class AbstractDirectionAction extends AbstractAction {
private final MovementState movementState;
private final int value;
public AbstractDirectionAction(MovementState movementState, int value) {
this.movementState = movementState;
this.value = value;
}
public MovementState getMovementState() {
return movementState;
}
public int getValue() {
return value;
}
}
public class YDirectionAction extends AbstractDirectionAction {
public YDirectionAction(MovementState movementState, int value) {
super(movementState, value);
}
#Override
public void actionPerformed(ActionEvent e) {
getMovementState().yDirection = getValue();
}
}
public class XDirectionAction extends AbstractDirectionAction {
public XDirectionAction(MovementState movementState, int value) {
super(movementState, value);
}
#Override
public void actionPerformed(ActionEvent e) {
getMovementState().xDirection = getValue();
}
}
}