这个答案解释并演示了如何使用键绑定而不是键侦听器来达到教育目的。它不是
- 如何用 Java 编写游戏。
- 良好的代码编写应该是什么样的(例如可见性)。
- 实现键绑定的最有效(性能或代码方面)的方法。
It is
-
我要发布的内容作为对那些与关键听众有困难的人的答案.
回答;阅读Swing 键绑定教程.
我不想阅读手册,请告诉我为什么我想使用按键绑定而不是我已经拥有的漂亮代码!
嗯,摇摆教程解释说
- Key bindings don't require you to click the component (to give it focus):
- 从用户的角度消除意外行为。
- 如果您有 2 个对象,它们无法同时移动,因为在给定时间只有 1 个对象可以获得焦点(即使您将它们绑定到不同的键)。
- Key bindings are easier to maintain and manipulate:
- 禁用、重新绑定、重新分配用户操作要容易得多。
- 代码更容易阅读。
好吧,你说服我尝试一下。它是如何工作的?
The tutorial有一个关于它的很好的部分。键绑定涉及 2 个对象InputMap
and ActionMap
. InputMap
将用户输入映射到操作名称,ActionMap
将操作名称映射到Action
。当用户按下某个键时,会在输入映射中搜索该键并找到动作名称,然后在动作映射中搜索该动作名称并执行该动作。
看起来很麻烦。为什么不将用户输入直接绑定到操作并去掉操作名称?那么您只需要一张地图,而不是两张。
好问题!您将看到这是使键绑定更易于管理的事情之一(禁用、重新绑定等)。
我希望你给我一个完整的工作代码。
No (the 摇摆教程 has 工作示例).
You suck! I hate you!
以下是如何进行单键绑定:
myComponent.getInputMap().put("userInput", "myAction");
myComponent.getActionMap().put("myAction", action);
请注意,有 3 个InputMap
对不同焦点状态的反应:
myComponent.getInputMap(JComponent.WHEN_FOCUSED);
myComponent.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
myComponent.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
-
WHEN_FOCUSED
,这也是在未提供参数时使用的,当组件具有焦点时使用。这与关键侦听器的情况类似。
-
WHEN_ANCESTOR_OF_FOCUSED_COMPONENT
当焦点组件位于注册为接收操作的组件内部时使用。如果一艘宇宙飞船内有许多船员,并且您希望宇宙飞船在任何船员获得焦点时继续接收输入,请使用此选项。
-
WHEN_IN_FOCUSED_WINDOW
当注册接收操作的组件位于焦点组件内时使用。如果聚焦窗口中有许多坦克,并且希望所有坦克同时接收输入,请使用此选项。
假设要同时控制两个对象,问题中提供的代码将类似于以下内容:
public class MyGame extends JFrame {
private static final int IFW = JComponent.WHEN_IN_FOCUSED_WINDOW;
private static final String MOVE_UP = "move up";
private static final String MOVE_DOWN = "move down";
private static final String FIRE = "move fire";
static JLabel obj1 = new JLabel();
static JLabel obj2 = new JLabel();
public MyGame() {
// Do all the layout management and what not...
obj1.getInputMap(IFW).put(KeyStroke.getKeyStroke("UP"), MOVE_UP);
obj1.getInputMap(IFW).put(KeyStroke.getKeyStroke("DOWN"), MOVE_DOWN);
// ...
obj1.getInputMap(IFW).put(KeyStroke.getKeyStroke("control CONTROL"), FIRE);
obj2.getInputMap(IFW).put(KeyStroke.getKeyStroke("W"), MOVE_UP);
obj2.getInputMap(IFW).put(KeyStroke.getKeyStroke("S"), MOVE_DOWN);
// ...
obj2.getInputMap(IFW).put(KeyStroke.getKeyStroke("T"), FIRE);
obj1.getActionMap().put(MOVE_UP, new MoveAction(1, 1));
obj1.getActionMap().put(MOVE_DOWN, new MoveAction(2, 1));
// ...
obj1.getActionMap().put(FIRE, new FireAction(1));
obj2.getActionMap().put(MOVE_UP, new MoveAction(1, 2));
obj2.getActionMap().put(MOVE_DOWN, new MoveAction(2, 2));
// ...
obj2.getActionMap().put(FIRE, new FireAction(2));
// In practice you would probably create your own objects instead of the JLabels.
// Then you can create a convenience method obj.inputMapPut(String ks, String a)
// equivalent to obj.getInputMap(IFW).put(KeyStroke.getKeyStroke(ks), a);
// and something similar for the action map.
add(obj1);
add(obj2);
// Do other GUI things...
}
static void rebindKey(KeyEvent ke, String oldKey) {
// Depends on your GUI implementation.
// Detecting the new key by a KeyListener is the way to go this time.
obj1.getInputMap(IFW).remove(KeyStroke.getKeyStroke(oldKey));
// Removing can also be done by assigning the action name "none".
obj1.getInputMap(IFW).put(KeyStroke.getKeyStrokeForEvent(ke),
obj1.getInputMap(IFW).get(KeyStroke.getKeyStroke(oldKey)));
// You can drop the remove action if you want a secondary key for the action.
}
public static void main(String[] args) {
new MyGame();
}
private class MoveAction extends AbstractAction {
int direction;
int player;
MoveAction(int direction, int player) {
this.direction = direction;
this.player = player;
}
@Override
public void actionPerformed(ActionEvent e) {
// Same as the move method in the question code.
// Player can be detected by e.getSource() instead and call its own move method.
}
}
private class FireAction extends AbstractAction {
int player;
FireAction(int player) {
this.player = player;
}
@Override
public void actionPerformed(ActionEvent e) {
// Same as the fire method in the question code.
// Player can be detected by e.getSource() instead, and call its own fire method.
// If so then remove the constructor.
}
}
}
您可以看到,将输入映射与操作映射分开可以实现可重用的代码和更好的绑定控制。此外,如果您需要该功能,还可以直接控制Action。例如:
FireAction p1Fire = new FireAction(1);
p1Fire.setEnabled(false); // Disable the action (for both players in this case).
See the 动作教程了解更多信息。
我发现您对 4 个键(方向)使用了 1 个动作(移动),对 1 个键使用了 1 个动作(开火)。为什么不给每个键自己的操作,或者给所有键相同的操作并弄清楚在操作中要做什么(就像在移动情况下一样)?
好点子。从技术上讲,您可以两者兼得,但您必须考虑什么是有意义的以及什么可以轻松管理和可重用代码。在这里,我假设所有方向的移动都是相似的,而射击是不同的,所以我选择了这种方法。
我看到很多KeyStroke
用过的,那些是什么?他们是否像一个KeyEvent
?
是的,它们具有类似的功能,但更适合用在这里。看看他们的API获取信息以及如何创建它们。
问题?改进?建议?发表评论。
有更好的答案吗?发表它。