KeyListener / KeyBinding does not start sequentially
I'm trying to make a simple game, and I need my spaceship to rotate around its center (using the A and D keys) and move relative to the direction it faces (W and S Keys).
I got the math for movement and rotation mostly figured out, but I have a problem with key listeners.
When I compile, the key listener works fine for the first pair of keystrokes. But if I hold down A (turn left when pressed) and then switch to D and then switch to A again, it stops working. The result is a different keystroke combination.
Basically, if I press the keys too hard, it stops working. If I go slowly it starts working again after I click on the button I want to work multiple times. BUT if I press a bunch of keys at the same time (press A, then D, then W in quick succession) then it stops working completely and all keys stop responding.
This is not a processing speed issue, I guess, since the program doesn't crash or produce any memory errors.
I read about similar problems and saw people suggesting not to use KeyListeners and instead use KeyBindings, but I tried both and got the same thing.
Here is the updated code for my program (tried to do MCVE): The part I commented in the middle was the key binding code.
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.geom.AffineTransform;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.math.*;
import java.net.URL;
public class Game extends JPanel implements ActionListener{
private Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
JFrame frame = new JFrame();
private Ship ship = new Ship();
private Timer gameTimer, turnRight, turnLeft, moveForward, moveBackward;
private static final int IFW = JComponent.WHEN_IN_FOCUSED_WINDOW;
public Game(){
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setTitle("Study Buddy Menu");
frame.setSize(800,700);
frame.setLocation(dim.width/2 - 400, dim.height/2 - 350);
turnLeft = new Timer(20, new ActionListener(){
public void actionPerformed(ActionEvent e) {
ship.setAngle(ship.getAngle() + ship.getTurnSpeed());
}
});
turnRight = new Timer(20, new ActionListener(){
public void actionPerformed(ActionEvent e) {
ship.setAngle(ship.getAngle() - ship.getTurnSpeed());
}
});
moveForward = new Timer(20, new ActionListener(){
public void actionPerformed(ActionEvent e) {
ship.setX(ship.getX() + (float)Math.cos(Math.toRadians(ship.getAngle())));
ship.setY(ship.getY() - (float)Math.sin(Math.toRadians(ship.getAngle())));
System.out.println("UP");
}
});
moveBackward = new Timer(20, new ActionListener(){
public void actionPerformed(ActionEvent e) {
ship.setX(ship.getX() - (float)Math.cos(Math.toRadians(ship.getAngle())));
ship.setY(ship.getY() + (float)Math.sin(Math.toRadians(ship.getAngle())));
System.out.println("DOWN");
}
});
this.setFocusable(true);
this.requestFocus();
/*getInputMap(IFW).put(KeyStroke.getKeyStroke('d'), "right");
getActionMap().put("right", new MoveAction("d"));
getInputMap(IFW).put(KeyStroke.getKeyStroke('a'), "left");
getActionMap().put("left", new MoveAction("a"));
getInputMap(IFW).put(KeyStroke.getKeyStroke('w'), "up");
getActionMap().put("up", new MoveAction("w"));
getInputMap(IFW).put(KeyStroke.getKeyStroke('s'), "down");
getActionMap().put("down", new MoveAction("s"));*/
this.addKeyListener(new KeyAdapter(){
public void keyPressed(KeyEvent e){
//TURN
if(e.getKeyCode() == KeyEvent.VK_D){
turnLeft.start();
turnRight.stop();
}
if(e.getKeyCode() == KeyEvent.VK_A){
turnRight.start();
turnLeft.stop();
}
//MOVE
if(e.getKeyCode() == KeyEvent.VK_W){
moveForward.start();
moveBackward.stop();
}
if(e.getKeyCode() == KeyEvent.VK_S){
moveBackward.start();
moveForward.stop();
}
}
public void keyReleased(KeyEvent e){
turnRight.stop();
turnLeft.stop();
moveForward.stop();
moveBackward.stop();
}
});
frame.add(this);
repaint();
gameTimer = new Timer(20, this);
gameTimer.start();
frame.setVisible(true);
}
public void paintComponent(Graphics g){
super.paintComponent(g);
ship.draw(g);
}
public void actionPerformed(ActionEvent e) {
repaint();
}
private class MoveAction extends AbstractAction {
String d = null;
MoveAction(String direction) {
d = direction;
}
public void actionPerformed(ActionEvent e) {
switch (d){
case "d": turnLeft.start(); turnRight.stop();break;
case "a": turnRight.start(); turnLeft.stop();break;
case "w": moveForward.start(); moveBackward.stop();break;
case "s": moveBackward.start(); moveForward.stop();break;
}
}
}
class main {
public void main(String[] args) {
Game game = new Game();
}
}
class Ship {
private float x, y, radius, speed, angle, turnSpeed;
private Image icon;
public Ship(){
x = 400;
y = 350;
speed = 1;
radius = 20;
angle = 90;
turnSpeed = 5;
try {
icon = ImageIO.read(new URL("https://i.stack.imgur.com/L5DGx.png"));
} catch (IOException e) {
e.printStackTrace();
}
}
//GETTERS
public float getX(){
return x;
}
public float getY(){
return y;
}
public float getTurnSpeed(){
return turnSpeed;
}
public float getAngle(){
return angle;
}
public float getRadius(){
return radius;
}
public float getSpeed(){
return speed;
}
public Image getIcon(){
return icon;
}
//SETTERS
public void setX(float X){
x = X;
}
public void setTurnSpeed(float S){
turnSpeed = S;
}
public void setY(float Y){
y = Y;
}
public void setAngle(float A){
angle = A;
}
public void setSpeed(float S){
speed = S;
}
public void setRadius(float R){
radius = R;
}
public void setIcon(Image image){
icon = image;
}
//DRAW
public void draw(Graphics g){
Graphics2D g2d = (Graphics2D) g;
AffineTransform at = new AffineTransform();
at.setToRotation(Math.toRadians(angle-90), x, y);
//at.translate(x, y);
g2d.setTransform(at);
g2d.drawImage(icon,(int)(x - radius), (int)(y - radius),(int)(radius * 2),(int)(radius * 2), null);
g2d.dispose();
}
}
}
source to share
I'm just going to go to bed, so I can't look too deep into your code, but a good way of doing motion is something like this:
- Binding to KeyListener / key / any sets some variables such as
isTurningLeft
,isTurningRight
,isMovingForwards
,isMovingBackwards
. - Then you only have one timer that checks the state of the control at each iteration and calculates motion at the same time based on it all.
Keeping track of every button at the same time is important so that you have all the information. For example, you can do something like this:
double turnRate = 0;
if (isTurningLeft) {
turnRate -= maxTurnRate;
}
if (isTurningRight) {
turnRate += maxTurnRate;
}
rotationAngle += timeElapsed * turnRate;
This way, if both buttons are held down at the same time, the rotation rate is 0. This will avoid a lot of strange behaviors such as sticking and stuttering. However, to do this, you need to know the state of both buttons in the same place.
change
I modified your program to serve as an example:
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.net.URL;
public class KeyListenerGame extends JPanel implements ActionListener{
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
new KeyListenerGame();
}
});
}
// private static final int IFW = JComponent.WHEN_IN_FOCUSED_WINDOW;
// private Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
private JFrame frame;
private Ship ship;
private Timer gameTimer;//, turnRight, turnLeft, moveForward, moveBackward;
private KeyBindingController controller;
// This is just an example. I recommend using
// the key bindings instead, even though they
// are a little more complicated to set up.
class KeyListenerController extends KeyAdapter {
boolean isTurningLeft;
boolean isTurningRight;
boolean isMovingForward;
boolean isMovingBackward;
@Override
public void keyPressed(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_A:
isTurningLeft = true;
break;
case KeyEvent.VK_D:
isTurningRight = true;
break;
case KeyEvent.VK_W:
isMovingForward = true;
break;
case KeyEvent.VK_S:
isMovingBackward = true;
break;
}
}
@Override
public void keyReleased(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_A:
isTurningLeft = false;
break;
case KeyEvent.VK_D:
isTurningRight = false;
break;
case KeyEvent.VK_W:
isMovingForward = false;
break;
case KeyEvent.VK_S:
isMovingBackward = false;
break;
}
}
}
// This could be simplified a lot with Java 8 features.
class KeyBindingController {
boolean isTurningLeft;
boolean isTurningRight;
boolean isMovingForward;
boolean isMovingBackward;
JComponent component;
KeyBindingController(JComponent component) {
this.component = component;
// Bind key pressed Actions.
bind(KeyEvent.VK_A, true, new AbstractAction("turnLeft.pressed") {
@Override
public void actionPerformed(ActionEvent e) {
isTurningLeft = true;
}
});
bind(KeyEvent.VK_D, true, new AbstractAction("turnRight.pressed") {
@Override
public void actionPerformed(ActionEvent e) {
isTurningRight = true;
}
});
bind(KeyEvent.VK_W, true, new AbstractAction("moveForward.pressed") {
@Override
public void actionPerformed(ActionEvent e) {
isMovingForward = true;
}
});
bind(KeyEvent.VK_S, true, new AbstractAction("moveBackward.pressed") {
@Override
public void actionPerformed(ActionEvent e) {
isMovingBackward = true;
}
});
// Bind key released Actions.
bind(KeyEvent.VK_A, false, new AbstractAction("turnLeft.released") {
@Override
public void actionPerformed(ActionEvent e) {
isTurningLeft = false;
}
});
bind(KeyEvent.VK_D, false, new AbstractAction("turnRight.released") {
@Override
public void actionPerformed(ActionEvent e) {
isTurningRight = false;
}
});
bind(KeyEvent.VK_W, false, new AbstractAction("moveForward.released") {
@Override
public void actionPerformed(ActionEvent e) {
isMovingForward = false;
}
});
bind(KeyEvent.VK_S, false, new AbstractAction("moveBackward.released") {
@Override
public void actionPerformed(ActionEvent e) {
isMovingBackward = false;
}
});
}
void bind(int keyCode, boolean onKeyPress, Action action) {
KeyStroke keyStroke = KeyStroke.getKeyStroke(keyCode, 0, !onKeyPress);
String actionName = (String) action.getValue(Action.NAME);
component.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(keyStroke, actionName);
component.getActionMap()
.put(actionName, action);
}
}
public KeyListenerGame(){
frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setTitle("Study Buddy Menu");
frame.add(this);
frame.pack();
frame.setLocationRelativeTo(null);
gameTimer = new Timer(20, this);
controller = new KeyBindingController(this);
ship = new Ship(getSize());
gameTimer.start();
frame.setVisible(true);
}
@Override
public void actionPerformed(ActionEvent e) {
double secsElapsed = gameTimer.getDelay() / 1000.0;
double maxSpeed = ship.getSpeed();
double maxTurnSpeed = ship.getTurnSpeed();
double theta = Math.toRadians( ship.getAngle() );
double x = ship.getX();
double y = ship.getY();
double turnSpeed = 0;
if (controller.isTurningLeft) {
turnSpeed -= maxTurnSpeed;
}
if (controller.isTurningRight) {
turnSpeed += maxTurnSpeed;
}
theta += secsElapsed * Math.toRadians(turnSpeed);
double speed = 0;
if (controller.isMovingForward) {
speed += maxSpeed;
}
if (controller.isMovingBackward) {
speed -= maxSpeed;
}
double velX = speed * Math.cos(theta);
double velY = speed * Math.sin(theta);
x += secsElapsed * velX;
y += secsElapsed * velY;
ship.setX( (float) x );
ship.setY( (float) y);
ship.setAngle( (float) Math.toDegrees(theta) );
repaint();
}
@Override
public Dimension getPreferredSize() {
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
// Computes a preferred size based on the
// dimensions of the screen size.
int prefWidth = screenSize.width / 2;
// Compute 4:3 aspect ratio.
int prefHeight = prefWidth * 3 / 4;
return new Dimension(prefWidth, prefHeight);
}
@Override
protected void paintComponent(Graphics g){
super.paintComponent(g);
ship.draw(g);
}
class Ship {
private float x, y, radius, speed, angle, turnSpeed;
private Image icon;
public Ship(Dimension gameSize) {
// x = 400;
// y = 350;
// speed = 1;
// radius = 20;
// angle = 90;
// turnSpeed = 5;
x = gameSize.width / 2;
y = gameSize.height / 2;
radius = 20;
angle = -90;
// 1/4 of the game height per second
speed = gameSize.height / 4;
// 180 degrees per second
turnSpeed = 180;
try {
icon = ImageIO.read(new URL("http://i.stack.imgur.com/L5DGx.png"));
} catch (IOException e) {
e.printStackTrace();
}
}
//GETTERS
public float getX(){
return x;
}
public float getY(){
return y;
}
public float getTurnSpeed(){
return turnSpeed;
}
public float getAngle(){
return angle;
}
public float getRadius(){
return radius;
}
public float getSpeed(){
return speed;
}
public Image getIcon(){
return icon;
}
//SETTERS
public void setX(float X){
x = X;
}
public void setTurnSpeed(float S){
turnSpeed = S;
}
public void setY(float Y){
y = Y;
}
public void setAngle(float A){
angle = A;
}
public void setSpeed(float S){
speed = S;
}
public void setRadius(float R){
radius = R;
}
public void setIcon(Image image){
icon = image;
}
//DRAW
public void draw(Graphics g){
Graphics2D g2d = (Graphics2D) g.create();
//
// Draw the ship movement vector.
double theta = Math.toRadians(angle);
double velX = speed * Math.cos(theta);
double velY = speed * Math.sin(theta);
g2d.setColor(java.awt.Color.blue);
g2d.draw(new java.awt.geom.Line2D.Double(x, y, x + velX, y + velY));
//
g2d.rotate(theta, x, y);
int imgX = (int) ( x - radius );
int imgY = (int) ( y - radius );
int imgW = (int) ( 2 * radius );
int imgH = (int) ( 2 * radius );
g2d.drawImage(icon, imgX, imgY, imgW, imgH, null);
//
g2d.dispose();
}
}
}
In addition to the controls, I've changed some other things:
- Always start a Swing program with a call
SwingUtilities.invokeLater(...)
. This is because Swing is running on a different thread than the one that startsmain
. https://docs.oracle.com/javase/tutorial/uiswing/concurrency/initial.html - Rather than setting the size directly
JFrame
, I triedgetPreferredSize
and calculated an initial size based on the screen sizes and then calledpack()
onJFrame
. This is much better than trying to statically set pixel sizes. Also, the size of aJFrame
includes its inserts (like the title bar), so if you call eg.jFrame.setSize(800,600)
the content area that displays the game actually ends up slightly less than800x600
. - I added a line that displays the ship's motion vector to visualize what is happening. This is good for debugging.
- In
ship.draw
I am making a copyg
withg.create()
, because thisg
is a graphics object that Swing personally uses. It is not recommended to do things like delete or set the transform toGraphics
. - I changed the rotation code a bit. Since the Y-axis is inverted in AWT coordinates, the positive angles actually rotate clockwise, and the negative angles rotate counterclockwise.
There are ways to make the key binding code nicer, but I just made it in the most obvious way so you can clearly see what I was actually doing. You have to set up bindings for both pushed and fired events. However, you will notice that there is a template for it, so you can program a small class to do general logic logic instead of copying and pasting like I did.
Java 8 lambdas will be very nice too, but my Java 8 computer is broken. With Java 8, you can get something like this:
bind(KeyEvent.VK_A, "moveLeft", b -> isMovingLeft = b);
and
void bind(int keyCode, String name, Consumer<Boolean> c) {
Action pressAction = new AbstractAction(name + ".pressed") {
@Override
public void actionPerformed(ActionEvent e) {
c.accept(true);
}
};
Action releaseAction = new AbstractAction(name + ".released") {
@Override
public void actionPerformed(ActionEvent e) {
c.accept(false);
}
};
// Then bind it to the InputMap/ActionMap like I did
// in the MCVE.
}
source to share