问题
I am trying to make a simple game, and i need my spaceship to rotate on its centre (using A and D keys) , and move relative to the direction it is facing (W and S Keys).
I got the math for the movement and rotation mostly figured out, but im having an issue with the key listeners.
When i compile, the key listener works fine for th e first couple of key presses. But if i hold down A (rotate left while being pressed down) and then switch to D and then switch to A again, it stops working. Any other combination of key presses also results in this.
Basically, if i press the keys too much eventually it will stop working. If i go slow, it will start to work again after i press the key i want to work a few times. BUT if i press a bunch of keys at once (hit A, then D, then W in quick succession), then it completely stops working, and all keys become unresponsive.
This is not a processing speed issue i think, as the program does not crash, or produce any memory errors.
I have read up on similar issues, and saw that people suggested to not use KeyListeners, and use KeyBindings instead, but i tried both and got the exact same issues.
Here is the UPDATED code for my program (tried to make an MCVE): The portion i commented out in the middle was the code for the keybinding.
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();
}
}
}
回答1:
I'm just about to go to bed so I can't look too deeply in to your code, but a well-behaved method for doing movement is something like this:
- Your KeyListener/key binding/whatever sets some variables like
isTurningLeft
,isTurningRight
,isMovingForwards
,isMovingBackwards
. - Then you have just one timer that checks the control state on every iteration and computes the movement based on all of it at the same time.
Keeping track of every button simultaneously is important so that you have all the information. So for example, you could 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 at the same time, the turn rate is just 0. That will avoid a lot of weird sorts of behaviors, like sticking and stuttering. However, to do that you need to know the state of both buttons in one place.
edit
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's 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 changed some other things:
- Always begin a Swing program with a call to
SwingUtilities.invokeLater(...)
. This is because Swing runs on a different thread than the one whichmain
gets run on. https://docs.oracle.com/javase/tutorial/uiswing/concurrency/initial.html - Instead of setting the size of a
JFrame
directly, I overrodegetPreferredSize
and computed an initial size based on the screen dimensions, then calledpack()
on theJFrame
. This is much more well-behaved than trying to set pixel sizes statically. Also, the size of aJFrame
includes its insets (for example the title bar), so if you call e.g.jFrame.setSize(800,600)
the content pane which displays the game actually ends up being a little smaller than800x600
. - I added a line which displays the ship's movement vector, just to visualize what's going on. This sort of thing is nice for debugging.
- In
ship.draw
I create a copy ofg
withg.create()
, becauseg
is the graphics object which Swing personally uses. Doing things like disposing or setting a transform on theGraphics
passed in isn't a good idea. - I fixed the rotation code a little. Because the y-axis in AWT coordinates is upside-down, positive angles actually rotate clockwise and negative angles rotate counter-clockwise.
There are ways to make the key binding code nicer, but I just did it the most obvious way so you can clearly see what I was actually doing. You have to set up bindings both for pressed and released events. You'll notice, though, that there is a pattern to it, so you could program a small class to perform the generic boolean logic instead of copy-and-pasting like I did.
Java 8 lambdas would also be very nice, but my computer with Java 8 is broken. With Java 8 you could end up with something like this, though:
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.
}
来源:https://stackoverflow.com/questions/43691987/keylistener-keybinding-does-not-trigger-consistently