英文:
I want my object to flow! The Keyboard Delay - Java
问题
这是源代码:
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JFrame;
public class Frame extends JFrame implements KeyListener {
private static final long serialVersionUID = 1L;
private static int width = 800;
private static int height = width / 16 * 9;
static //// For Action
Panel panel = new Panel();
Square sq = new Square();
private int x = 0;
private int y = 0;
// Constructor
Frame() {
this.setSize(width, height);
this.setLocationByPlatform(true);
this.setLayout(null);
this.setResizable(false);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setFocusable(true);
this.setName("Window");
this.addKeyListener(this);
// Action is starting
sq.setLocation(x, y);
panel.add(sq);
this.add(panel);
}
// KeyListener==
@Override
public void keyTyped(KeyEvent e) {
// TODO Auto-generated method stub
}
// My problem is here, or I'm wrong!?
@Override
public void keyPressed(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_W:
sq.setLocation(x, y - 5);
y -= 5;
this.repaint();
System.out.println(e.getKeyChar());
break;
case KeyEvent.VK_S:
sq.setLocation(x, y + 5);
y += 5;
this.repaint();
System.out.println(e.getKeyChar());
break;
case KeyEvent.VK_A:
sq.setLocation(x - 5, y);
x -= 5;
this.repaint();
System.out.println(e.getKeyChar());
break;
case KeyEvent.VK_D:
sq.setLocation(x + 5, y);
x += 5;
this.repaint();
System.out.println(e.getKeyChar());
break;
}
}
@Override
public void keyReleased(KeyEvent e) {
}
// Main
public static void main(String arguments[]) {
Frame frame = new Frame();
frame.add(panel);
frame.setVisible(true);
}
}
这是我的 Square 类,我不确定它是否应该扩展 JPanel:
import java.awt.Color;
import javax.swing.JPanel;
public class Square extends JPanel {
private static final long serialVersionUID = 1L;
private int width = 70;
private int height = 70;
Square() {
setSize(width, height);
setBackground(Color.white);
}
}
最后是我的 Panel 类:
public class Panel extends JPanel {
private static final long serialVersionUID = 1L;
private static int width = 800;
private static int height = width / 16 * 9;
Panel() {
this.setSize(width, height);
this.setBackground(Color.DARK_GRAY);
this.setLayout(null);
}
}
我需要你们的帮助。谢谢你们所有人的回复。
英文:
I don't know which component should I use to create a moving square on my JPanel. So I decided to use another JPanel to make an object. My first goal was moving the object but you know the games, when you press a button on your keyboard it is not performing like it's typing, it is performing like it is in the game. I mean the character movement is pretty delayed when you're typing. How can I fix the delay without changing my computer's keyboard delay settings? Also IDK let me know if you know a better way to create a moving square.
This is the source code:
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JFrame;
public class Frame extends JFrame implements KeyListener {
private static final long serialVersionUID = 1L;
private static int width = 800;
private static int height = width / 16 * 9;
static //// For Action
Panel panel = new Panel();
Square sq = new Square();
private int x = 0;
private int y = 0;
// Constructor
Frame() {
this.setSize(width, height);
this.setLocationByPlatform(true);
this.setLayout(null);
this.setResizable(false);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setFocusable(true);
this.setName("Window");
this.addKeyListener(this);
// Action is starting
sq.setLocation(x, y);
panel.add(sq);
this.add(panel);
}
// KeyListener==
@Override
public void keyTyped(KeyEvent e) {
// TODO Auto-generated method stub
}
//My problem is here, or I'm wrong!?
@Override
public void keyPressed(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_W:
sq.setLocation(x, y - 5);
y-=5;
this.repaint();
System.out.println(e.getKeyChar());
break;
case KeyEvent.VK_S:
sq.setLocation(x, y + 5);
y+=5;
this.repaint();
System.out.println(e.getKeyChar());
break;
case KeyEvent.VK_A:
sq.setLocation(x - 5, y);
x-=5;
this.repaint();
System.out.println(e.getKeyChar());
break;
case KeyEvent.VK_D:
sq.setLocation(x + 5, y);
x+=5;
this.repaint();
System.out.println(e.getKeyChar());
break;
}
}
@Override
public void keyReleased(KeyEvent e) {
}
// Main
public static void main(String arguments[]) {
Frame frame = new Frame();
frame.add(panel);
frame.setVisible(true);
}
}
And this is my Square class, and I'm not sure about the fact that it is extending JPanel:
import java.awt.Color;
import javax.swing.JPanel;
public class Square extends JPanel {
private static final long serialVersionUID = 1L;
private int width = 70;
private int height = 70;
Square(){
setSize(width, height);
setBackground(Color.white);
}
}
And finally my Panel Class:
public class Panel extends JPanel{
private static final long serialVersionUID = 1L;
private static int width = 800;
private static int height = width / 16 * 9;
Panel() {
this.setSize(width, height);
this.setBackground(Color.DARK_GRAY);
this.setLayout(null);
}
}
I need your help guys. Thanks for all of the replies.
答案1
得分: 3
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.geom.Point2D;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Objects;
import java.util.function.BiFunction;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
public class CharacterOfJComponent {
public static class ManualLayout implements LayoutManager, Serializable {
public static void setOperatedSize(final Dimension input,
final BiFunction<Integer, Integer, Integer> operator,
final Dimension inputOutput) {
inputOutput.setSize(operator.apply(input.width, inputOutput.width), operator.apply(input.height, inputOutput.height));
}
protected Dimension getGoodEnoughSize(final Component comp,
final Dimension defaultSize) {
final Dimension dim = new Dimension(defaultSize);
if (comp != null) {
if (comp.isMaximumSizeSet())
setOperatedSize(comp.getMaximumSize(), Math::min, dim);
if (comp.isMinimumSizeSet())
setOperatedSize(comp.getMinimumSize(), Math::max, dim);
}
return dim;
}
protected Dimension getLayoutComponentSize(final Component comp) {
return getGoodEnoughSize(comp, (comp.getWidth() <= 0 && comp.getHeight() <= 0) ? comp.getPreferredSize() : comp.getSize());
}
@Override
public void addLayoutComponent(final String name,
final Component comp) {
}
@Override
public void removeLayoutComponent(final Component comp) {
}
@Override
public Dimension preferredLayoutSize(final Container parent) {
return minimumLayoutSize(parent);
}
@Override
public Dimension minimumLayoutSize(final Container parent) {
final Component[] comps = parent.getComponents();
if (comps == null || comps.length <= 0)
return new Dimension();
final Rectangle totalBounds = new Rectangle(comps[0].getLocation(), getLayoutComponentSize(comps[0]));
for (int i = 1; i < comps.length; ++i)
totalBounds.add(new Rectangle(comps[i].getLocation(), getLayoutComponentSize(comps[i])));
final Insets parins = parent.getInsets();
final int addw, addh;
if (parins == null)
addw = addh = 0;
else {
addw = (parins.left + parins.right);
addh = (parins.top + parins.bottom);
}
return new Dimension(Math.max(0, totalBounds.x + totalBounds.width) + addw, Math.max(0, totalBounds.y + totalBounds.height) + addh);
}
@Override
public void layoutContainer(final Container parent) {
for (final Component comp : parent.getComponents())
comp.setSize(getLayoutComponentSize(comp));
}
}
public static class GameObject extends JLabel {
private final Point2D.Double highPrecisionLocation;
public GameObject() {
highPrecisionLocation = copyToDouble(super.getLocation());
}
public void setHighPrecisionLocation(final Point2D highPrecisionLocation) {
this.highPrecisionLocation.setLocation(highPrecisionLocation);
final Insets parentInsets = getParent().getInsets();
setLocation((int) Math.round(highPrecisionLocation.getX()) + parentInsets.left + parentInsets.right,
(int) Math.round(highPrecisionLocation.getY()) + parentInsets.top + parentInsets.bottom);
}
public Point2D getHighPrecisionLocation() {
return copyToDouble(highPrecisionLocation);
}
@Override
public World getParent() {
return (World) super.getParent();
}
}
public static class World extends JPanel {
public World() {
super(new ManualLayout());
}
public ArrayList<GameObject> getGameObjects() {
final ArrayList<GameObject> gos = new ArrayList<>();
for (final Component child : getComponents())
if (child instanceof GameObject)
gos.add((GameObject) child);
return gos;
}
}
private static Point2D addPoints(final Point2D p1, final Point2D p2, final double limit) {
final double limitAbs = Math.abs(limit);
return new Point2D.Double(Math.max(Math.min(p1.getX() + p2.getX(), limitAbs), -limitAbs), Math.max(Math.min(p1.getY() + p2.getY(), limitAbs), -limitAbs));
}
private static Point2D.Double copyToDouble(final Point2D pt) {
return new Point2D.Double(pt.getX(), pt.getY());
}
public static class Animation extends Timer {
private final Point2D.Double lvel;
public Animation(final int delay, final GameObject go) {
super(delay, null);
Objects.requireNonNull(go);
lvel = new Point2D.Double();
super.setRepeats(true);
super.setCoalesce(true);
super.addActionListener(e -> {
final Point2D pos = go.getHighPrecisionLocation();
go.setHighPrecisionLocation(new Point2D.Double(pos.getX() + lvel.getX(), pos.getY() + lvel.getY()));
go.getParent().revalidate();
go.getParent().repaint();
});
}
public void setLinearVelocity(final Point2D lvel) {
this.lvel.setLocation(lvel);
}
public Point2D getLinearVelocity() {
return copyToDouble(lvel);
}
}
private static AbstractAction restartAnimationAction(final Animation animation, final Point2D acceleration, final double max) {
final Point2D acc = copyToDouble(acceleration);
return new AbstractAction() {
@Override
public void actionPerformed(final ActionEvent e) {
animation.setLinearVelocity(addPoints(animation.getLinearVelocity(), acc, max));
if (!animation.isRunning())
animation.restart();
}
};
}
private static AbstractAction stopAnimationAction(final Animation animation, final Point2D acceleration, final double max) {
final Point2D acc = copyToDouble(acceleration);
return new AbstractAction() {
@Override
public void actionPerformed(final ActionEvent e) {
if (animation.isRunning()) {
final Point2D newlvel = addPoints(animation.getLinearVelocity(), acc, max);
animation.setLinearVelocity(newlvel);
if (newlvel.getX() == 0 && newlvel.getY() == 0)
animation.stop();
}
}
};
}
private static void installAction(final InputMap inmap, final ActionMap actmap, final Animation animation, final String onPressName, final Point2D off, final double max) {
final KeyStroke onPressStroke = KeyStroke.getKeyStroke(onPressName);
inmap.put(onPressStroke, onPressName + " press");
actmap.put(onPressName + " press", restartAnimationAction(animation, off, max));
<details>
<summary>英文:</summary>
One solution one can find, is to use a `javax.swing.Timer` that will constantly move the character and have the speed of the character as a property of it. So whenever the `Timer` fires its action listeners, one of them will add the current speed to the location of the game's object (ie the character). When you press a key, the action listener will just adjust the speed of the animation `Timer` (according to what key was pressed) and then [re]start the `Timer`. When you release all motion-related keys, the corresponding action listener will stop the `Timer`.
General suggestions:
--------------------
1. Make sure you don't block the [EDT](https://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html).
2. As already suggested in the comments of the question, use [Key Bindings](https://docs.oracle.com/javase/tutorial/uiswing/misc/keybinding.html) for more flexibility in the long run (instead of `KeyListener`s).
Two variations follow, for the character representation:
Variation 1: Custom painting
----------------------------
You can have every object of the game's world to be painted inside the world's `JComponent` (for example a `JPanel`) by overriding the `paintComponent` method.
Follows sample code...
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Objects;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.Icon;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
public class CustomPaintedCharacters {
//This is the customly painted object representing an entity in the World:
public static class GameObject {
private final World world;
private final Icon how;
private final Point2D.Double where; //Important: use a Point2D.Double... Not a plain Point, because of precision.
public GameObject(final World world, final Point2D where, final Icon how) {
this.world = Objects.requireNonNull(world);
this.how = Objects.requireNonNull(how);
this.where = copyToDouble(where);
}
public void setPosition(final Point2D where) {
this.where.setLocation(where);
}
public Point2D getPosition() {
return copyToDouble(where);
}
public World getWorld() {
return world;
}
public Icon getIcon() {
return how;
}
public Rectangle2D getBounds() {
return new Rectangle2D.Double(where.getX(), where.getY(), how.getIconWidth(), how.getIconHeight());
}
public final void paint(final Graphics2D g) {
g.translate(where.getX(), where.getY()); /*Use as much of the precision as possible...
ie instead of calling 'how.paintIcon' with rounded coordinates converted to ints, we can
translate to where we want to by using doubles...*/
how.paintIcon(world, g, 0, 0);
}
}
public static interface RectangleIcon extends Icon {
@Override
default void paintIcon(final Component comp, final Graphics g, final int x, final int y) {
g.fillRect(x, y, getIconWidth(), getIconHeight());
}
}
public static class DefaultRectangleIcon implements RectangleIcon {
private final Color c;
private final int w, h;
public DefaultRectangleIcon(final Color c, final int width, final int height) {
this.c = Objects.requireNonNull(c);
w = width;
h = height;
}
@Override
public void paintIcon(final Component comp, final Graphics g, final int x, final int y) {
g.setColor(c);
RectangleIcon.super.paintIcon(comp, g, x, y);
}
@Override
public int getIconWidth() {
return w;
}
@Override
public int getIconHeight() {
return h;
}
}
public static class World extends JPanel {
private final ArrayList<GameObject> gameObjects;
public World() {
this.gameObjects = new ArrayList<>();
}
public GameObject createGameObject(final Point2D where, final Icon how) {
final GameObject go = new GameObject(this, where, how);
gameObjects.add(go);
return go;
}
@Override
protected void paintComponent(final Graphics g) {
super.paintComponent(g); //Don't forget this!
gameObjects.forEach(go -> { //Paint every object in the world...
final Graphics2D graphics = (Graphics2D) g.create();
go.paint(graphics);
graphics.dispose();
});
}
@Override
public Dimension getPreferredSize() {
if (isPreferredSizeSet())
return super.getPreferredSize();
final Rectangle2D bounds = new Rectangle2D.Double(); //Important: use a Rectangle2D.Double... Not a plain Rectangle, because of precision.
gameObjects.forEach(go -> bounds.add(go.getBounds()));
return new Dimension((int) Math.ceil(bounds.getX() + bounds.getWidth()), (int) Math.ceil(bounds.getY() + bounds.getHeight()));
}
}
public static class Animation extends Timer {
private final Point2D.Double lvel; //Important: use a Point2D.Double... Not a plain Point, because of precision.
public Animation(final int delay, final GameObject go) {
super(delay, null);
Objects.requireNonNull(go);
lvel = new Point2D.Double();
super.setRepeats(true);
super.setCoalesce(true);
super.addActionListener(e -> {
final Point2D pos = go.getPosition();
go.setPosition(new Point2D.Double(pos.getX() + lvel.getX(), pos.getY() + lvel.getY()));
go.getWorld().repaint();
});
}
public void setLinearVelocity(final Point2D lvel) {
this.lvel.setLocation(lvel);
}
public Point2D getLinearVelocity() {
return copyToDouble(lvel);
}
}
/*Adds two points using a limit. As you have probably understood yourself, and as I have tested
it, the keys pressed with key bindings are keeping to invoke events repeatedly which would mean
for example that if we add to the velocity of a GameObject again and again (after holding the
same direction-button for a while) then the velocity would add up to a number greater than
intended so we must ensure this will not happen by forcing each coordinate to not exceed the
range of [-limit, limit].*/
private static Point2D addPoints(final Point2D p1, final Point2D p2, final double limit) {
final double limitAbs = Math.abs(limit); //We take the absolute value, in case of missusing the method.
return new Point2D.Double(Math.max(Math.min(p1.getX() + p2.getX(), limitAbs), -limitAbs), Math.max(Math.min(p1.getY() + p2.getY(), limitAbs), -limitAbs));
}
/*This method solely exists to ensure that any given Point2D we give (even plain Points) are
converted to Point2D.Double instances because we are working with doubles, not ints. For
example imagine if we were to update a plain Point with a Point2D.Double; we would lose precision
(in the location or velocity of the object) which is highly not wanted, because it could mean
that after a while, the precision would add up to distances that the character would be supposed
to have covered, but it wouldn't. I should note though that doubles are also not lossless, but
they are supposed to be better in this particular scenario. The lossless solution would probably
be to use BigIntegers of pixels but this would be a bit more complex for such a short
demonstration of another subject (so we settle for it).*/
private static Point2D.Double copyToDouble(final Point2D pt) {
return new Point2D.Double(pt.getX(), pt.getY());
}
private static AbstractAction restartAnimationAction(final Animation animation, final Point2D acceleration, final double max) {
final Point2D acc = copyToDouble(acceleration); //Deffensive copy.
//When we press a key (eg UP) we want to activate the timer once (and after changing velocity):
return new AbstractAction() {
@Override
public void actionPerformed(final ActionEvent e) {
animation.setLinearVelocity(addPoints(animation.getLinearVelocity(), acc, max));
if (!animation.isRunning())
animation.restart();
}
};
}
private static AbstractAction stopAnimationAction(final Animation animation, final Point2D acceleration, final double max) {
final Point2D acc = copyToDouble(acceleration); //Deffensive copy.
/*When we release a key (eg UP) we want to undo the movement of the character in the corresponding
direction (up) and possibly even stop the animation (if both velocity coordinates are zero):*/
return new AbstractAction() {
@Override
public void actionPerformed(final ActionEvent e) {
if (animation.isRunning()) {
//Decrement the velocity:
final Point2D newlvel = addPoints(animation.getLinearVelocity(), acc, max);
animation.setLinearVelocity(newlvel);
//If both velocities are zero, we stop the timer, to speed up EDT:
if (newlvel.getX() == 0 && newlvel.getY() == 0)
animation.stop();
}
}
};
}
private static void installAction(final InputMap inmap, final ActionMap actmap, final Animation animation, final String onPressName, final Point2D off, final double max) {
//One key binding for key press:
final KeyStroke onPressStroke = KeyStroke.getKeyStroke(onPressName); //By default binds to the key-press event.
inmap.put(onPressStroke, onPressName + " press");
actmap.put(onPressName + " press", restartAnimationAction(animation, off, max));
//One key binding for key release:
final KeyStroke onReleaseStroke = KeyStroke.getKeyStroke(onPressStroke.getKeyCode(), onPressStroke.getModifiers(), true); //Convert the key-stroke of key-press event to key-release event.
inmap.put(onReleaseStroke, onPressName + " release");
actmap.put(onPressName + " release", stopAnimationAction(animation, new Point2D.Double(-off.getX(), -off.getY()), max));
}
public static Animation installAnimation(final GameObject go, final int delayMS, final double stepOffset) {
final World world = go.getWorld();
final InputMap in = world.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
final ActionMap act = world.getActionMap();
final Animation anim = new Animation(delayMS, go);
/*The Strings used in each invocation of 'installAction' are very important as they will
define the KeyStroke obtained by 'KeyStroke.getKeyStroke' method calls inside 'installAction'.
So you shouldn't need to change those for this particular demonstration.*/
installAction(in, act, anim, "LEFT", new Point2D.Double(-stepOffset, 0), stepOffset);
installAction(in, act, anim, "RIGHT", new Point2D.Double(stepOffset, 0), stepOffset);
installAction(in, act, anim, "UP", new Point2D.Double(0, -stepOffset), stepOffset);
installAction(in, act, anim, "DOWN", new Point2D.Double(0, stepOffset), stepOffset);
return anim;
}
public static void main(final String[] args) {
SwingUtilities.invokeLater(() -> { //Make sure to 'invokeLater' EDT related code.
final World world = new World();
final GameObject worldCharacter = world.createGameObject(new Point(200, 200), new DefaultRectangleIcon(Color.CYAN.darker(), 100, 50));
final JFrame frame = new JFrame("Move the character");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(world);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
installAnimation(worldCharacter, 10, 2);
});
}
}
Variation 2: Extending a `JComponent`
-------------------------------------
You can also extend a `JComponent` for any game's object representation (eg the character).
In this solution, you can use other `JComponent`s added to the character, changing in this way how the character looks without having to paint everything in `paintComponent`.
For example, if you have an external image for the character, you can:
1. Load the image with `ImageIO.read` (which will give you a `BufferedImage`, ie a non-animated image fully loaded into memory).
2. Construct an `ImageIcon` by supplying the `BufferedImage` (which will give you an implementation of the `Icon` interface, suitable for the next step).
3. Use a `JLabel` as the character and supply it with the `ImageIcon` created in step 2.
But it is not just this, because, by having a `JComponent` as the character you may add to it other `JComponent`s as it already is a `Container`, thus having many more options.
It would be best here to use a `LayoutManager` for positioning the character (which is a `JComponent`) inside the parent (which is another `JComponent`). You can use the `setLocation` method of the `JComponent` game objects to position them inside the world. As far as I know, it is highly discouraged to use `null` as a layout for the world, so I created a small `LayoutManager` from scratch as a suggestion on how you can go about it.
Follows sample code of a `JLabel` as the character without images, just to demonstrate the concept...
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.geom.Point2D;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Objects;
import java.util.function.BiFunction;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
public class CharacterOfJComponent {
public static class ManualLayout implements LayoutManager, Serializable {
public static void setOperatedSize(final Dimension input,
final BiFunction<Integer, Integer, Integer> operator,
final Dimension inputOutput) {
inputOutput.setSize(operator.apply(input.width, inputOutput.width), operator.apply(input.height, inputOutput.height));
}
protected Dimension getGoodEnoughSize(final Component comp,
final Dimension defaultSize) {
final Dimension dim = new Dimension(defaultSize);
if (comp != null) { // && comp.isVisible()) {
/*Start with default size, and then listen to max and min
(if both max and min are set, we prefer the min one):*/
if (comp.isMaximumSizeSet())
setOperatedSize(comp.getMaximumSize(), Math::min, dim);
if (comp.isMinimumSizeSet())
setOperatedSize(comp.getMinimumSize(), Math::max, dim);
}
return dim;
}
protected Dimension getLayoutComponentSize(final Component comp) {
return getGoodEnoughSize(comp, (comp.getWidth() <= 0 && comp.getHeight() <= 0)? comp.getPreferredSize(): comp.getSize());
}
@Override
public void addLayoutComponent(final String name,
final Component comp) {
}
@Override
public void removeLayoutComponent(final Component comp) {
}
@Override
public Dimension preferredLayoutSize(final Container parent) {
return minimumLayoutSize(parent); //Preferred and minimum coincide for simplicity.
}
@Override
public Dimension minimumLayoutSize(final Container parent) {
final Component[] comps = parent.getComponents();
if (comps == null || comps.length <= 0)
return new Dimension();
final Rectangle totalBounds = new Rectangle(comps[0].getLocation(), getLayoutComponentSize(comps[0]));
for (int i = 1; i < comps.length; ++i)
totalBounds.add(new Rectangle(comps[i].getLocation(), getLayoutComponentSize(comps[i])));
final Insets parins = parent.getInsets();
final int addw, addh;
if (parins == null)
addw = addh = 0;
else {
addw = (parins.left + parins.right);
addh = (parins.top + parins.bottom);
}
return new Dimension(Math.max(0, totalBounds.x + totalBounds.width) + addw, Math.max(0, totalBounds.y + totalBounds.height) + addh);
}
@Override
public void layoutContainer(final Container parent) {
for (final Component comp: parent.getComponents())
comp.setSize(getLayoutComponentSize(comp)); //Just set the size. The locations are taken care by the class's client supposedly.
}
}
public static class GameObject extends JLabel {
private final Point2D.Double highPrecisionLocation;
public GameObject() {
highPrecisionLocation = copyToDouble(super.getLocation());
}
public void setHighPrecisionLocation(final Point2D highPrecisionLocation) {
this.highPrecisionLocation.setLocation(highPrecisionLocation);
final Insets parentInsets = getParent().getInsets();
setLocation((int) Math.round(highPrecisionLocation.getX()) + parentInsets.left + parentInsets.right,
(int) Math.round(highPrecisionLocation.getY()) + parentInsets.top + parentInsets.bottom);
}
public Point2D getHighPrecisionLocation() {
return copyToDouble(highPrecisionLocation);
}
@Override
public World getParent() {
return (World) super.getParent();
}
}
public static class World extends JPanel {
public World() {
super(new ManualLayout());
}
public ArrayList<GameObject> getGameObjects() {
final ArrayList<GameObject> gos = new ArrayList<>();
for (final Component child: getComponents())
if (child instanceof GameObject)
gos.add((GameObject) child);
return gos;
}
}
/*Adds two points using a limit. As you have probably understood yourself, and as I have tested
it, the keys pressed with key bindings are keeping to invoke events repeatedly which would mean
for example that if we add to the velocity of a GameObject again and again (after holding the
same direction-button for a while) then the velocity would add up to a number greater than
intended so we must ensure this will not happen by forcing each coordinate to not exceed the
range of [-limit, limit].*/
private static Point2D addPoints(final Point2D p1, final Point2D p2, final double limit) {
final double limitAbs = Math.abs(limit); //We take the absolute value, in case of missusing the method.
return new Point2D.Double(Math.max(Math.min(p1.getX() + p2.getX(), limitAbs), -limitAbs), Math.max(Math.min(p1.getY() + p2.getY(), limitAbs), -limitAbs));
}
/*This method solely exists to ensure that any given Point2D we give (even plain Points) are
converted to Point2D.Double instances because we are working with doubles, not ints. For
example imagine if we were to update a plain Point with a Point2D.Double; we would lose precision
(in the location or velocity of the object) which is highly not wanted, because it could mean
that after a while, the precision would add up to distances that the character would be supposed
to have covered, but it wouldn't. I should note though that doubles are also not lossless, but
they are supposed to be better in this particular scenario. The lossless solution would probably
be to use BigIntegers of pixels but this would be a bit more complex for such a short
demonstration of another subject (so we settle for it).*/
private static Point2D.Double copyToDouble(final Point2D pt) {
return new Point2D.Double(pt.getX(), pt.getY());
}
public static class Animation extends Timer {
private final Point2D.Double lvel; //Important: use a Point2D.Double... Not a plain Point, because of precision.
public Animation(final int delay, final GameObject go) {
super(delay, null);
Objects.requireNonNull(go);
lvel = new Point2D.Double();
super.setRepeats(true);
super.setCoalesce(true);
super.addActionListener(e -> {
final Point2D pos = go.getHighPrecisionLocation();
go.setHighPrecisionLocation(new Point2D.Double(pos.getX() + lvel.getX(), pos.getY() + lvel.getY()));
go.getParent().revalidate(); //Layout has changed.
go.getParent().repaint();
});
}
public void setLinearVelocity(final Point2D lvel) {
this.lvel.setLocation(lvel);
}
public Point2D getLinearVelocity() {
return copyToDouble(lvel);
}
}
private static AbstractAction restartAnimationAction(final Animation animation, final Point2D acceleration, final double max) {
final Point2D acc = copyToDouble(acceleration); //Deffensive copy.
//When we press a key (eg UP) we want to activate the timer once (and after changing velocity):
return new AbstractAction() {
@Override
public void actionPerformed(final ActionEvent e) {
animation.setLinearVelocity(addPoints(animation.getLinearVelocity(), acc, max));
if (!animation.isRunning())
animation.restart();
}
};
}
private static AbstractAction stopAnimationAction(final Animation animation, final Point2D acceleration, final double max) {
final Point2D acc = copyToDouble(acceleration); //Deffensive copy.
/*When we release a key (eg UP) we want to undo the movement of the character in the corresponding
direction (up) and possibly even stop the animation (if both velocity coordinates are zero):*/
return new AbstractAction() {
@Override
public void actionPerformed(final ActionEvent e) {
if (animation.isRunning()) {
//Decrement the velocity:
final Point2D newlvel = addPoints(animation.getLinearVelocity(), acc, max);
animation.setLinearVelocity(newlvel);
//If both velocities are zero, we stop the timer, to speed up EDT:
if (newlvel.getX() == 0 && newlvel.getY() == 0)
animation.stop();
}
}
};
}
private static void installAction(final InputMap inmap, final ActionMap actmap, final Animation animation, final String onPressName, final Point2D off, final double max) {
//One key binding for key press:
final KeyStroke onPressStroke = KeyStroke.getKeyStroke(onPressName); //By default binds to the key-press event.
inmap.put(onPressStroke, onPressName + " press");
actmap.put(onPressName + " press", restartAnimationAction(animation, off, max));
//One key binding for key release:
final KeyStroke onReleaseStroke = KeyStroke.getKeyStroke(onPressStroke.getKeyCode(), onPressStroke.getModifiers(), true); //Convert the key-stroke of key-press event to key-release event.
inmap.put(onReleaseStroke, onPressName + " release");
actmap.put(onPressName + " release", stopAnimationAction(animation, new Point2D.Double(-off.getX(), -off.getY()), max));
}
public static Animation installAnimation(final GameObject go, final int delayMS, final double stepOffset) {
final World world = go.getParent();
final InputMap in = world.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
final ActionMap act = world.getActionMap();
final Animation anim = new Animation(delayMS, go);
/*The Strings used in each invocation of 'installAction' are very important as they will
define the KeyStroke obtained by 'KeyStroke.getKeyStroke' method calls inside 'installAction'.
So you shouldn't need to change those for this particular demonstration.*/
installAction(in, act, anim, "LEFT", new Point2D.Double(-stepOffset, 0), stepOffset);
installAction(in, act, anim, "RIGHT", new Point2D.Double(stepOffset, 0), stepOffset);
installAction(in, act, anim, "UP", new Point2D.Double(0, -stepOffset), stepOffset);
installAction(in, act, anim, "DOWN", new Point2D.Double(0, stepOffset), stepOffset);
return anim;
}
public static void main(final String[] args) {
SwingUtilities.invokeLater(() -> { //Make sure to 'invokeLater' EDT related code.
final World world = new World();
final GameObject worldCharacter = new GameObject();
worldCharacter.setText("Character");
worldCharacter.setBorder(BorderFactory.createLineBorder(Color.CYAN.darker(), 2));
world.add(worldCharacter);
worldCharacter.setHighPrecisionLocation(new Point(200, 200));
final JFrame frame = new JFrame("Move the character");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(world);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
installAnimation(worldCharacter, 10, 2);
});
}
}
As you can see, there is a custom `LayoutManager` named `ManualLayout`. It respects the minimum, preferred and maximum sizes of the layed out `Component`s. Then the client of this class is responsible for accomodating with the parent's insets and child's location using `setLocation` method accordingly.
In the long run, since you may want to put multiple objects in the world, which may overlap, then you can use a `JLayeredPane` as the `World`.
Additional notes
----------------
1. You can mix those variations.
2. Don't forget to add `super.paintComponent` call in overriden `paintComponent` in *custom painting* variation.
3. In case you want to be able to pause and resume the simulation, you may create an action listener which sets the other keyboard actions enabled or disabled.
4. Only tested in JDK/JRE 8, but I think it should work in others also. At least the concept should work.
</details>
# 答案2
**得分**: 0
为了使您的程序更快,您可以尝试创建一个包含 `while` 循环的 `Thread`,该循环将使用一个 `boolean` 变量不断检查某个键是否被按下。
通过这种方式,您还可以通过使用 `Thread.sleep(time);` 来在特定时间内停止 `Thread` 以设置程序的灵敏度。
演示代码:
```java
package test;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
public class Frame extends JFrame implements KeyListener {
private static final long serialVersionUID = 1L;
private static int width = 800;
private static int height = width / 16 * 9;
static Panel panel = new Panel();
Square sq = new Square();
private int x = 0;
private int y = 0;
int key;
// 灵敏度不应大于1000。
int sensitivity = 100;
boolean keyPressed = false;
public Frame() {
this.setSize(width, height);
this.setLocationByPlatform(true);
this.setLayout(null);
this.setResizable(false);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setFocusable(true);
this.setName("Window");
this.addKeyListener(this);
// 动作开始
sq.setLocation(x, y);
panel.add(sq);
add(panel);
setVisible(true);
new Thread(new MoveSquare()).start();
}
// KeyListener
@Override
public void keyTyped(KeyEvent e) {
// TODO Auto-generated method stub
}
// 我的问题可能在这里,或者我理解错了!?
@Override
public void keyPressed(KeyEvent e) {
key = e.getKeyCode();
keyPressed = true;
}
@Override
public void keyReleased(KeyEvent e) {
keyPressed = false;
}
// 主函数
public static void main(String arguments[]) {
new Frame();
}
protected class MoveSquare implements Runnable {
@Override
public void run() {
while (true) {
try {
if (keyPressed) {
try {
int s = 1;
switch (key) {
case KeyEvent.VK_W:
sq.setLocation(x, y - s);
y -= s;
Frame.this.repaint();
break;
case KeyEvent.VK_S:
sq.setLocation(x, y + s);
y += s;
Frame.this.repaint();
break;
case KeyEvent.VK_A:
sq.setLocation(x - s, y);
x -= s;
Frame.this.repaint();
break;
case KeyEvent.VK_D:
sq.setLocation(x + s, y);
x += s;
Frame.this.repaint();
break;
}
} catch (Exception ex) {
}
}
Thread.sleep(1000 / sensitivity);
} catch (Exception ex) {
}
}
}
}
}
英文:
To make your program fast, you can try creating a Thread
consisting a while
loop which keep checking if a key is pressed down using a boolean
variable.
In this way, you can also set the sensitivity of your program by stopping the Thread
for a particular time using Thread.sleep(time);
Demo code:
package test;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
public class Frame extends JFrame implements KeyListener {
private static final long serialVersionUID = 1L;
private static int width = 800;
private static int height = width / 16 * 9;
static Panel panel = new Panel();
Square sq = new Square();
private int x = 0;
private int y = 0;
int key;
//Sensitivity should not be greater than 1000.
int sensitivity = 100;
boolean keyPressed = false;
public Frame() {
this.setSize(width, height);
this.setLocationByPlatform(true);
this.setLayout(null);
this.setResizable(false);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setFocusable(true);
this.setName("Window");
this.addKeyListener(this);
// Action is starting
sq.setLocation(x, y);
panel.add(sq);
add(panel);
setVisible(true);
new Thread(new MoveSquare()).start();
}
// KeyListener==
@Override
public void keyTyped(KeyEvent e) {
// TODO Auto-generated method stub
}
//My problem is here, or I'm wrong!?
@Override
public void keyPressed(KeyEvent e) {
key = e.getKeyCode();
keyPressed = true;
}
@Override
public void keyReleased(KeyEvent e) {
keyPressed = false;
}
// Main
public static void main(String arguments[]) {
new Frame();
}
protected class MoveSquare implements Runnable {
@Override
public void run() {
while (true) {
try {
if (keyPressed) {
try {
int s = 1;
switch (key) {
case KeyEvent.VK_W:
sq.setLocation(x, y - s);
y -= s;
Frame.this.repaint();
break;
case KeyEvent.VK_S:
sq.setLocation(x, y + s);
y += s;
Frame.this.repaint();
break;
case KeyEvent.VK_A:
sq.setLocation(x - s, y);
x -= s;
Frame.this.repaint();
break;
case KeyEvent.VK_D:
sq.setLocation(x + s, y);
x += s;
Frame.this.repaint();
break;
}
} catch (Exception ex) {
}
}
Thread.sleep(1000 / sensitivity);
} catch (Exception ex) {
}
}
}
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论