Java KeyListener卡顿问题

7
我正在用Java制作一个非常简单的乒乓球游戏,我正在使用KeyListener。我希望当用户按下键盘上的右或左箭头键时,乒乓球方块就会朝那个方向移动。这是一个足够简单的任务,但我发现当用户按住键时,方块只会移动一次,停顿一小段时间,然后继续移动直到用户释放该键。我注意到当你试图在计算机上按住字母键时,也会出现这种情况。如果我试着按住“a”键,计算机会做出:

a [paus] aaaaaaaaaaaaaaaa

有没有办法禁用这种卡顿呢?因为它影响了我的小游戏的流畅性。深表感激您的快速解决方案。

3个回答

9

我原本有一个关于键绑定的回答,但经过一些测试,发现它们仍然存在相同的卡顿问题。

不要依赖操作系统的重复率。它在每个平台上可能都不同,用户也可能自定义它。

相反,使用计时器来安排事件。您可以在按下按键时启动计时器,在释放按键时停止计时器。

import java.awt.*;
import java.awt.event.*;
import java.net.*;
import java.util.Map;
import java.util.HashMap;
import javax.imageio.ImageIO;
import javax.swing.*;

public class KeyboardAnimation implements ActionListener
{
    private final static String PRESSED = "pressed ";
    private final static String RELEASED = "released ";
    private final static Point RELEASED_POINT = new Point(0, 0);

    private JComponent component;
    private Timer timer;
    private Map<String, Point> pressedKeys = new HashMap<String, Point>();

    public KeyboardAnimation(JComponent component, int delay)
    {
        this.component = component;

        timer = new Timer(delay, this);
        timer.setInitialDelay( 0 );
    }

    public void addAction(String keyStroke, int deltaX, int deltaY)
    {
//      InputMap inputMap = component.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
        InputMap inputMap = component.getInputMap();
        ActionMap actionMap = component.getActionMap();

        String pressedKey = PRESSED + keyStroke;
        KeyStroke pressedKeyStroke = KeyStroke.getKeyStroke( pressedKey );
        Action pressedAction = new AnimationAction(keyStroke, new Point(deltaX, deltaY));
        inputMap.put(pressedKeyStroke, pressedKey);
        actionMap.put(pressedKey, pressedAction);

        String releasedKey = RELEASED + keyStroke;
        KeyStroke releasedKeyStroke = KeyStroke.getKeyStroke( releasedKey );
        Action releasedAction = new AnimationAction(keyStroke, RELEASED_POINT);
        inputMap.put(releasedKeyStroke, releasedKey);
        actionMap.put(releasedKey, releasedAction);
    }

    private void handleKeyEvent(String keyStroke, Point moveDelta)
    {
        //  Keep track of which keys are pressed

        if (RELEASED_POINT == moveDelta)
            pressedKeys.remove( keyStroke );
        else
            pressedKeys.put(keyStroke, moveDelta);

        //  Start the Timer when the first key is pressed

        if (pressedKeys.size() == 1)
        {
            timer.start();
        }

        //  Stop the Timer when all keys have been released

        if (pressedKeys.size() == 0)
        {
            timer.stop();
        }
    }

    //  Invoked when the Timer fires

    public void actionPerformed(ActionEvent e)
    {
        moveComponent();
    }

    //  Move the component to its new location

    private void moveComponent()
    {
        int componentWidth = component.getSize().width;
        int componentHeight = component.getSize().height;

        Dimension parentSize = component.getParent().getSize();
        int parentWidth  = parentSize.width;
        int parentHeight = parentSize.height;

        //  Calculate new move

        int deltaX = 0;
        int deltaY = 0;

        for (Point delta : pressedKeys.values())
        {
            deltaX += delta.x;
            deltaY += delta.y;
        }


        //  Determine next X position

        int nextX = Math.max(component.getLocation().x + deltaX, 0);

        if ( nextX + componentWidth > parentWidth)
        {
            nextX = parentWidth - componentWidth;
        }

        //  Determine next Y position

        int nextY = Math.max(component.getLocation().y + deltaY, 0);

        if ( nextY + componentHeight > parentHeight)
        {
            nextY = parentHeight - componentHeight;
        }

        //  Move the component

        component.setLocation(nextX, nextY);
    }

    private class AnimationAction extends AbstractAction implements ActionListener
    {
        private Point moveDelta;

        public AnimationAction(String keyStroke, Point moveDelta)
        {
            super(PRESSED + keyStroke);
            putValue(ACTION_COMMAND_KEY, keyStroke);

            this.moveDelta = moveDelta;
        }

        public void actionPerformed(ActionEvent e)
        {
            handleKeyEvent((String)getValue(ACTION_COMMAND_KEY), moveDelta);
        }
    }

    public static void main(String[] args)
    {
        JPanel contentPane = new JPanel();
        contentPane.setLayout( null );

        Icon dukeIcon = null;

        try
        {
            dukeIcon = new ImageIcon( "dukewavered.gif" );
//          dukeIcon = new ImageIcon( ImageIO.read( new URL("http://duke.kenai.com/iconSized/duke4.gif") ) );
        }
        catch(Exception e)
        {
            System.out.println(e);
        }

        JLabel duke = new JLabel( dukeIcon );
        duke.setSize( duke.getPreferredSize() );
        duke.setLocation(100, 100);
        contentPane.add( duke );

        KeyboardAnimation navigation = new KeyboardAnimation(duke, 24);
        navigation.addAction("LEFT", -3,  0);
        navigation.addAction("RIGHT", 3,  0);
        navigation.addAction("UP",    0, -3);
        navigation.addAction("DOWN",  0,  3);

        navigation.addAction("A", -5,  0);
        navigation.addAction("S",  5,  0);
        navigation.addAction("Z",  0, -5);
        navigation.addAction("X",  0,  5);
        navigation.addAction("V",  5,  5);

        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
//      frame.getContentPane().add(new JTextField(), BorderLayout.SOUTH);
        frame.getContentPane().add(contentPane);
        frame.setSize(600, 600);
        frame.setLocationRelativeTo( null );
        frame.setVisible(true);
    }

}

此代码在Windows上进行了测试,其中事件的顺序为keyPressed,keyPressed,keyPressed... keyReleased。

但是,在Mac(或Unix)上,我认为事件的顺序是keyPressed,keyReleased,keyPressed,keyReleased... 因此,我不确定这段代码是否比您当前的代码更好地工作。


只是提供信息,那句话是我说的,但我不是原帖作者。 - syb0rg

5
  1. 你应该使用键绑定,因为它们可以解决大多数焦点相关问题,并且通常更加灵活...
  2. 你需要定义一个标志来指示按下了哪个键。在按下时,不应执行任何其他任务...

例如...

更新:增加简单示例

在大多数游戏中,你应该对“状态更改”做出反应,而不是实际的键事件。这意味着实际更改状态的事件可以是可变的(考虑自定义键)。

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class SinglePressKeyBinding {

    public static void main(String[] args) {
        new SinglePressKeyBinding();
    }

    public SinglePressKeyBinding() {
        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 TestPane extends JPanel {

        private JLabel message;

        private boolean spacedOut = false;

        public TestPane() {
            message = new JLabel("Waiting");
            setLayout(new GridBagLayout());
            add(message);

            InputMap im = getInputMap(WHEN_IN_FOCUSED_WINDOW);
            ActionMap am = getActionMap();

            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0, false), "space-pressed");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0, true), "space-released");

            am.put("space-pressed", new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    if (spacedOut) {
                        message.setText("I'm ignoring you");
                    } else {
                        spacedOut = true;
                        message.setText("Spaced out");
                    }
                }
            });
            am.put("space-released", new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    spacedOut = false;
                    message.setText("Back to earth");
                }
            });

        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(200, 200);
        }

    }
}

我原本有一个关于键绑定的答案,但是经过一些测试,我发现它们仍然存在相同的卡顿问题。不过其他方面还是加1分。 - syb0rg
@syb0rg 这就是为什么你需要那个小标志的原因。最后一个示例演示了一个巧妙的想法,允许按键事件应用加速度增量,按得越久,增量就越大,直到释放按键并反转增量,随着时间的推移减缓对象的速度 ;) - MadProgrammer
1
@syb0rg 添加了一个简单的示例来演示原理;) - MadProgrammer

1
一个好的想法是为你想要追踪的键设置布尔值,然后在按下键时激活其中一个布尔值,在释放键时取消激活。这将消除按键的延迟,并允许多个键同时按下!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接