In the code below, two circles are drawn, and added to one grouping node.
The following behavior variants observed:
1) Able to drag by any point of circle, including exterior and interior; if drag by intercircle point, drag does not occur
2) Able to drag only by exterior
3) Unable to drag
4) Able to drag by any point in extended bounds
Any behavior is inited by subinitialize() params.
I wonder, is it possible to finetune active "pickeble" points of a node? For example, can I leave subnodes not pickable, but make the entire group is draggable by circles exteriors only, as it was in case (2)?
The need is required because Piccolo does not allow to determine, in which group object the click was made. Particularly, I can't determine the group node, to which listener were attached, so if I have single listener and attach it to multiple nodes, I won't be able to distinguish, which one was called.
public class Try_Picking_01 {
private static final Logger log = LoggerFactory.getLogger(Try_Picking_01.class);
public static void main(String[] args) {
new PFrame() {
final PNode group = new PNode();
#Override
public void initialize() {
group.addInputEventListener(new PBasicInputEventHandler() {
#Override
public void mouseDragged(PInputEvent event) {
PDimension dim = event.getDeltaRelativeTo(group.getParent());
group.offset(dim.getWidth(), dim.getHeight());
}
});
getCanvas().getLayer().addChild(group);
getCanvas().setPanEventHandler(null);
// able to drag by any point of circle, including exterior and interior
// if drag by intercircle point, drag does not occur
// subinitialize(false, true, false);
// able to drag only by exterior
//subinitialize(true, true, false);
// unable to drag
// subinitialize(true, false, false);
// able to drag by any point in extended bounds
subinitialize(true, false, true);
}
private void subinitialize(boolean emptyFill, boolean pickable, boolean expandBounds) {
PPath path1 = PPath.createEllipse(100, 100, 100, 100);
path1.setStrokePaint(Color.RED);
path1.setPaint( emptyFill ? null : Color.YELLOW ); // line #1
log.info("path1.bounds = {}", path1.getBounds());
path1.setPickable(pickable); // line #2
PPath path2 = PPath.createEllipse(200, 200, 100, 100);
path2.setPaint( emptyFill ? null : Color.YELLOW ); // line #3
log.info("path2.bounds = {}", path2.getBounds());
path2.setPickable(pickable); // line #4
group.addChild(path1);
group.addChild(path2);
log.info("group.bounds = {}", group.getBounds());
if( expandBounds ) {
group.setBounds(group.getFullBounds()); // line #5
log.info("group.bounds = {}", group.getBounds());
}
}
};
}
}
Suzan,
Looking at how Piccolo manages input events the most sensible thing would be to create a specific event handler for each of your node groups. The piccolo only provides you with a generic input handler at the PNode level. This renders all PNode events effectively the same. If you want to define different behavior per node (or group) you'll need to derive a class (for instance from PNode or PPath) and add the logic to detect which node group was clicked and drag according to the settings passed in subInitialize.
The great thing about Java is that you can easily extend libraries like Piccolo to suit your own purpose.
Related
I'm new to arcore and am using sceneform for a project. Specifically, I'm using the solar system example and trying to implement the info cards it has in Planet.java to my project. I have the info cards set up(at least i think i do) however, they do not appear on tap. Am I missing something or is there something I am doing wrong? Thank you in advance for your help.
private void makeinfoCards(ArFragment arFragment, Anchor anchor, Context context) {
if (infoCard == null) {
AnchorNode anchorNode = new AnchorNode(anchor);
infoCard = new Node();
infoCard.setParent(anchorNode);
infoCard.setEnabled(false);
infoCard.setLocalPosition(new Vector3(0.0f, 2.90f * INFO_CARD_Y_POS_COEFF, 0.0f));
//below would hide/bring up the info card on tap
infoCard.setOnTapListener( //anchorNode.setOnTapListener doesn't make info card appear either
(hitTestResult, motionEvent) -> {
infoCard.setEnabled(!infoCard.isEnabled());
}
);
ViewRenderable.builder()
.setView(arFragment.getContext(), R.layout.card_view) //putting context instead makes no difference
.build()
.thenAccept(
(renderable) -> {
infoCard.setRenderable(renderable);
TextView textView = (TextView) renderable.getView();
textView.setText("random");
})
.exceptionally(
(throwable) -> {
throw new AssertionError("Could not load plane card view.", throwable);
});
}
}
You have set the onTapListener on the infoCard Node which has been disabled by default. You should use the setOnTapListener on the node which contains the 3D model you will be clicking, try passing the specific node that has the 3D model set as the renderable as an argument to the function instead of the Anchor. If you have the Node say modelNode which has the 3D model.
In your ARActivity call
makeInfoCard(arFragment,modelNode,context);
And in the function use
private void makeInfoCard(ArFragment arFragment, Node modelNode, Context context){
//
modelNode.setOnTapListener((hitTestResult, motionEvent) -> {
infoCard.setEnabled(!infoCard.isEnabled());
});
//
}
Implement Node.OnTapListener interface and than override onTap method.
When model is going to be projected keep record of model nodes in a list. Here I set card for each node present on a sceneform.
#Override
public void onTap(HitTestResult hitTestResult, MotionEvent motionEvent) {
modelDataObj = map.get(hitTestResult.getNode()); // get object againt each node
tvModelName.setText(modelDataObj.getItem_name());
Picasso.get().load(modelDataObj.get_thumbnail()).placeholder(R.drawable.imageplaceholder).into(imgLoadAddInfoImage);
tvTitle.setText(modelDataObj.getItem_name());
tvLoadAddInfoDescText.setText(modelDataObj.getDescription())
}
I'm using KeyListeners in my code (game or otherwise) as the way for my on-screen objects to react to user key input. Here is my code:
public class MyGame extends JFrame {
static int up = KeyEvent.VK_UP;
static int right = KeyEvent.VK_RIGHT;
static int down = KeyEvent.VK_DOWN;
static int left = KeyEvent.VK_LEFT;
static int fire = KeyEvent.VK_Q;
public MyGame() {
// Do all the layout management and what not...
JLabel obj1 = new JLabel();
JLabel obj2 = new JLabel();
obj1.addKeyListener(new MyKeyListener());
obj2.addKeyListener(new MyKeyListener());
add(obj1);
add(obj2);
// Do other GUI things...
}
static void move(int direction, Object source) {
// do something
}
static void fire(Object source) {
// do something
}
static void rebindKey(int newKey, String oldKey) {
// Depends on your GUI implementation.
// Detecting the new key by a KeyListener is the way to go this time.
if (oldKey.equals("up"))
up = newKey;
if (oldKey.equals("down"))
down = newKey;
// ...
}
public static void main(String[] args) {
new MyGame();
}
private static class MyKeyListener extends KeyAdapter {
#Override
public void keyPressed(KeyEvent e) {
Object source = e.getSource();
int action = e.getExtendedKeyCode();
/* Will not work if you want to allow rebinding keys since case variables must be constants.
switch (action) {
case up:
move(1, source);
case right:
move(2, source);
case down:
move(3, source);
case left:
move(4, source);
case fire:
fire(source);
...
}
*/
if (action == up)
move(1, source);
else if (action == right)
move(2, source);
else if (action == down)
move(3, source);
else if (action == left)
move(4, source);
else if (action == fire)
fire(source);
}
}
}
I have problems with the responsiveness:
I need to click on the object for it to work.
The response I get for pressing one of the keys is not how I wanted it to work - too responsive or too unresponsive.
Why does this happen and how do I fix this?
This answer explains and demonstrates how to use key bindings instead of key listeners for educational purpose. It is not
How to write a game in Java.
How good code writing should look like (e.g. visibility).
The most efficient (performance- or code-wise) way to implement key bindings.
It is
What I would post as an answer to anyone who is having trouble with key listeners.
Answer; Read the Swing tutorial on key bindings.
I don't want to read manuals, tell me why I would want to use key bindings instead of the beautiful code I have already!
Well, the Swing tutorial explains that
Key bindings don't require you to click the component (to give it focus):
Removes unexpected behavior from the user's point of view.
If you have 2 objects, they can't move simultaneously as only 1 of the objects can have the focus at a given time (even if you bind them to different keys).
Key bindings are easier to maintain and manipulate:
Disabling, rebinding, re-assigning user actions is much easier.
The code is easier to read.
OK, you convinced me to try it out. How does it work?
The tutorial has a good section about it. Key bindings involve 2 objects InputMap and ActionMap. InputMap maps a user input to an action name, ActionMap maps an action name to an Action. When the user presses a key, the input map is searched for the key and finds an action name, then the action map is searched for the action name and executes the action.
Looks cumbersome. Why not bind the user input to directly to the action and get rid of the action name? Then you need only one map and not two.
Good question! You will see that this is one of the things that make key bindings more manageable (disable, rebind etc.).
I want you to give me a full working code of this.
No (the Swing tutorial has working examples).
You suck! I hate you!
Here is how to make a single key binding:
myComponent.getInputMap().put("userInput", "myAction");
myComponent.getActionMap().put("myAction", action);
Note that there are 3 InputMaps reacting to different focus states:
myComponent.getInputMap(JComponent.WHEN_FOCUSED);
myComponent.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
myComponent.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
WHEN_FOCUSED, which is also the one used when no argument is supplied, is used when the component has focus. This is similar to the key listener case.
WHEN_ANCESTOR_OF_FOCUSED_COMPONENT is used when a focused component is inside a component which is registered to receive the action. If you have many crew members inside a spaceship and you want the spaceship to continue receiving input while any of the crew members has focus, use this.
WHEN_IN_FOCUSED_WINDOW is used when a component which is registered to receive the action is inside a focused component. If you have many tanks in a focused window and you want all of them to receive input at the same time, use this.
The code presented in the question will look something like this assuming both objects are to be controlled at the same time:
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.
}
}
}
You can see that separating the input map from the action map allow reusable code and better control of bindings. In addition, you can also control an Action directly if you need the functionality. For example:
FireAction p1Fire = new FireAction(1);
p1Fire.setEnabled(false); // Disable the action (for both players in this case).
See the Action tutorial for more information.
I see that you used 1 action, move, for 4 keys (directions) and 1 action, fire, for 1 key. Why not give each key its own action, or give all keys the same action and sort out what to do inside the action (like in the move case)?
Good point. Technically you can do both, but you have to think what makes sense and what allows for easy management and reusable code. Here I assumed moving is similar for all directions and firing is different, so I chose this approach.
I see a lot of KeyStrokes used, what are those? Are they like a KeyEvent?
Yes, they have a similar function, but are more appropriate for use here. See their API for info and on how to create them.
Questions? Improvements? Suggestions? Leave a comment.
Have a better answer? Post it.
Note: this is not an answer, just a comment with too much code :-)
Getting keyStrokes via getKeyStroke(String) is the correct way - but needs careful reading of the api doc:
modifiers := shift | control | ctrl | meta | alt | altGraph
typedID := typed <typedKey>
typedKey := string of length 1 giving Unicode character.
pressedReleasedID := (pressed | released) key
key := KeyEvent key code name, i.e. the name following "VK_".
The last line should better be exact name, that is case matters: for the down key the exact key code name is VK_DOWN, so the parameter must be "DOWN" (not "Down" or any other variation of upper/lower case letters)
Not entirely intuitive (read: had to dig a bit myself) is getting a KeyStroke to a modifier key. Even with proper spelling, the following will not work:
KeyStroke control = getKeyStroke("CONTROL");
Deeper down in the awt event queue, a keyEvent for a single modifier key is created with itself as modifier. To bind to the control key, you need the stroke:
KeyStroke control = getKeyStroke("ctrl CONTROL");
Here is an easyway that would not require you to read hundreds of lines of code just learn a few lines long trick.
declare a new JLabel and add it to your JFrame (I didn't test it in other components)
private static JLabel listener= new JLabel();
The focus needs to stay on this for the keys to work though.
In constructor :
add(listener);
Use this method:
OLD METHOD:
private void setKeyBinding(String keyString, AbstractAction action) {
listener.getInputMap().put(KeyStroke.getKeyStroke(keyString), keyString);
listener.getActionMap().put(keyString, action);
}
KeyString must be written properly. It is not typesafe and you must consult the official list to learn what is the keyString(it is not an official term) for each button.
NEW METHOD
private void setKeyBinding(int keyCode, AbstractAction action) {
int modifier = 0;
switch (keyCode) {
case KeyEvent.VK_CONTROL:
modifier = InputEvent.CTRL_DOWN_MASK;
break;
case KeyEvent.VK_SHIFT:
modifier = InputEvent.SHIFT_DOWN_MASK;
break;
case KeyEvent.VK_ALT:
modifier = InputEvent.ALT_DOWN_MASK;
break;
}
listener.getInputMap().put(KeyStroke.getKeyStroke(keyCode, modifier), keyCode);
listener.getActionMap().put(keyCode, action);
}
In this new method you can simply set it using KeyEvent.VK_WHATEVER
EXAMPLE CALL:
setKeyBinding(KeyEvent.VK_CONTROL, new AbstractAction() {
#Override
public void actionPerformed(ActionEvent e) {
System.out.println("ctrl pressed");
}
});
Send an anonymous class (or use subclass) of AbstractAction. Override its public void actionPerformed(ActionEvent e) and make it do whatever you want the key to do.
PROBLEM:
I couldn't get it running for VK_ALT_GRAPH.
case KeyEvent.VK_ALT_GRAPH:
modifier = InputEvent.ALT_GRAPH_DOWN_MASK;
break;
does not make it work for me for some reason.
Here is an example of how to get key bindings working.
(Inside JFrame subclass using extends, which is called by the constructor)
// Create key bindings for controls
private void createKeyBindings(JPanel p) {
InputMap im = p.getInputMap(JPanel.WHEN_IN_FOCUSED_WINDOW);
ActionMap am = p.getActionMap();
im.put(KeyStroke.getKeyStroke("W"), MoveAction.Action.MOVE_UP);
im.put(KeyStroke.getKeyStroke("S"), MoveAction.Action.MOVE_DOWN);
im.put(KeyStroke.getKeyStroke("A"), MoveAction.Action.MOVE_LEFT);
im.put(KeyStroke.getKeyStroke("D"), MoveAction.Action.MOVE_RIGHT);
am.put(MoveAction.Action.MOVE_UP, new MoveAction(this, MoveAction.Action.MOVE_UP));
am.put(MoveAction.Action.MOVE_DOWN, new MoveAction(this, MoveAction.Action.MOVE_DOWN));
am.put(MoveAction.Action.MOVE_LEFT, new MoveAction(this, MoveAction.Action.MOVE_LEFT));
am.put(MoveAction.Action.MOVE_RIGHT, new MoveAction(this, MoveAction.Action.MOVE_RIGHT));
}
Separate class to handle those key bindings created above (where Window is the class that extends from JFrame)
// Handles the key bindings
class MoveAction extends AbstractAction {
enum Action {
MOVE_UP, MOVE_DOWN, MOVE_LEFT, MOVE_RIGHT;
}
private static final long serialVersionUID = /* Some ID */;
Window window;
Action action;
public MoveAction(Window window, Action action) {
this.window = window;
this.action = action;
}
#Override
public void actionPerformed(ActionEvent e) {
switch (action) {
case MOVE_UP:
/* ... */
break;
case MOVE_DOWN:
/* ... */
break;
case MOVE_LEFT:
/* ... */
break;
case MOVE_RIGHT:
/* ... */
break;
}
}
}
Triggered by a question about Slider and a slight bug, I tried to implement a SliderSkin that makes use of axis services, in particular its conversion methods between pixel and value. Works fine except that NumberAxis keeps its conversion offset and scale factor as internal fields that are updated only during a layout pass.
Which poses a problem, if I want to use the conversion during a layout pulse for updating another collaborator: in the case of a Slider that would be the thumb.
Below is a small examples to demonstrate the problem: simply a NumberAxis and a CheckBox at some value. On startup, the box is placed at the value at the middle. For maximal effect, maximize the window and note that the box position is not changed - now sitting near the beginning of the axis. Actually, it's the same when resizing the window but not so visible - see the printout of the difference.
Options to make it work
delay the thumb positioning until the next layout pulse (feels clumsy)
force the axis to update immediately
Looking for a way for the latter (couldn't find anything except reflectively invoke the axis' layoutChildren).
The example:
public class AxisInvalidate extends Application {
public static class AxisInRegion extends Region {
NumberAxis axis;
Control thumb;
IntegerProperty value = new SimpleIntegerProperty(50);
private double thumbWidth;
private double thumbHeight;
public AxisInRegion() {
axis = new NumberAxis(0, 100, 25);
thumb = new CheckBox();
getChildren().addAll(axis, thumb);
}
#Override
protected void layoutChildren() {
thumbWidth = snapSize(thumb.prefWidth(-1));
thumbHeight = snapSize(thumb.prefHeight(-1));
thumb.resize(thumbWidth, thumbHeight);
double axisHeight = axis.prefHeight(-1);
axis.resizeRelocate(0, getHeight() /4, getWidth(), axisHeight);
// this marks the layout as dirty but doesn't immediately update internals
// doesn't make a difference, shouldn't be needed anyway
//axis.requestAxisLayout();
double pixelOnAxis = axis.getDisplayPosition(value.getValue());
Platform.runLater(() -> {
LOG.info("diff " + (pixelOnAxis - axis.getDisplayPosition(value.getValue())));
});
// moving this line into the runlater "solves" the problem
thumb.relocate(pixelOnAxis, getHeight() /4);
}
}
private Parent getContent() {
AxisInRegion region = new AxisInRegion();
BorderPane content = new BorderPane(region);
return content;
}
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setScene(new Scene(getContent(), 500, 200));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
#SuppressWarnings("unused")
private static final Logger LOG = Logger.getLogger(AxisInvalidate.class
.getName());
}
Actually, I think it's a bug: the value/pixel conversion is a public service of the Axis - that should work always.
Not sure if it helps for your initial issue but simply do the calculation manually. Another plus - request layout becomes needless and can be removed.
double range = axis.getUpperBound()-axis.getLowerBound();
double pixelOnAxis = axis.getWidth()*(value.get()-axis.getLowerBound())/range;
Just got a working hack from the bug report: we need to call axis.layout() after changing size/location and before querying the conversion methods, something like:
axis.resizeRelocate(0, getHeight() /4, getWidth(), axisHeight);
// doesn't make a difference, shouldn't be needed anyway
//axis.requestAxisLayout();
// working hack from bug report:
axis.layout();
double pixelOnAxis = axis.getDisplayPosition(value.getValue());
thumb.relocate(pixelOnAxis, getHeight() /4);
How can I add mouseListener to a MapMarker (MepMarkerDot or MapMarkerCircle) that makes it like button?
I tried this soloution but it makes whole map clickable (mouse Event works on all the map).
You're on the right path to start with TrashGod's MouseListener solution, but you need to add a little more code, the key part being, that you need to get the Point location of where the user pressed, something the MouseEvent#getPoint() method will tell you, and then based on that information, and the bounds of the "active" area of the component decide whether to respond. Something like:
#Override
public void mousePressed(MouseEvent e) {
Point p = e.getPoint(); // this is where the user pressed
if (isPointValid(p)) {
// do something
}
System.out.println(map.getPosition(e.getPoint()));
}
private boolean isPointValid(Point p) {
// here you have code to decide if the point was pressed in the area of interest.
}
Note that if your code uses Shape derived objects, such as Ellipse2D or Rectangle2D, you can use their contains(Point p) method to easily tell you if the point press was within the Shape or not. Or if there are several locations that you want to check, you may have a collection of Shapes, iterate through them within your mousePressed or (if you have it) isPointValid method, and check containment within the for loop.
I found this nice example:
https://www.programcreek.com/java-api-examples/index.php?source_dir=netention-old1-master/swing/automenta/netention/swing/map/Map2DPanel.java
It has an interface MarkerClickable and its own LabeledMarker which implements MapMarker and MarkerClickable:
public boolean onClick(final Coordinate p, final MouseEvent e) {
for (final MapMarker x : getMap().getMapMarkerList()) {
if (x instanceof MarkerClickable) {
final MarkerClickable mc = (MarkerClickable)x;
final Rectangle a = mc.getClickableArea();
if (a == null)
continue;
if (a.contains(e.getPoint())) {
mc.onClicked(e.getPoint(), e.getButton());
return false;
}
}
}
return true;
}
I'm using KeyListeners in my code (game or otherwise) as the way for my on-screen objects to react to user key input. Here is my code:
public class MyGame extends JFrame {
static int up = KeyEvent.VK_UP;
static int right = KeyEvent.VK_RIGHT;
static int down = KeyEvent.VK_DOWN;
static int left = KeyEvent.VK_LEFT;
static int fire = KeyEvent.VK_Q;
public MyGame() {
// Do all the layout management and what not...
JLabel obj1 = new JLabel();
JLabel obj2 = new JLabel();
obj1.addKeyListener(new MyKeyListener());
obj2.addKeyListener(new MyKeyListener());
add(obj1);
add(obj2);
// Do other GUI things...
}
static void move(int direction, Object source) {
// do something
}
static void fire(Object source) {
// do something
}
static void rebindKey(int newKey, String oldKey) {
// Depends on your GUI implementation.
// Detecting the new key by a KeyListener is the way to go this time.
if (oldKey.equals("up"))
up = newKey;
if (oldKey.equals("down"))
down = newKey;
// ...
}
public static void main(String[] args) {
new MyGame();
}
private static class MyKeyListener extends KeyAdapter {
#Override
public void keyPressed(KeyEvent e) {
Object source = e.getSource();
int action = e.getExtendedKeyCode();
/* Will not work if you want to allow rebinding keys since case variables must be constants.
switch (action) {
case up:
move(1, source);
case right:
move(2, source);
case down:
move(3, source);
case left:
move(4, source);
case fire:
fire(source);
...
}
*/
if (action == up)
move(1, source);
else if (action == right)
move(2, source);
else if (action == down)
move(3, source);
else if (action == left)
move(4, source);
else if (action == fire)
fire(source);
}
}
}
I have problems with the responsiveness:
I need to click on the object for it to work.
The response I get for pressing one of the keys is not how I wanted it to work - too responsive or too unresponsive.
Why does this happen and how do I fix this?
This answer explains and demonstrates how to use key bindings instead of key listeners for educational purpose. It is not
How to write a game in Java.
How good code writing should look like (e.g. visibility).
The most efficient (performance- or code-wise) way to implement key bindings.
It is
What I would post as an answer to anyone who is having trouble with key listeners.
Answer; Read the Swing tutorial on key bindings.
I don't want to read manuals, tell me why I would want to use key bindings instead of the beautiful code I have already!
Well, the Swing tutorial explains that
Key bindings don't require you to click the component (to give it focus):
Removes unexpected behavior from the user's point of view.
If you have 2 objects, they can't move simultaneously as only 1 of the objects can have the focus at a given time (even if you bind them to different keys).
Key bindings are easier to maintain and manipulate:
Disabling, rebinding, re-assigning user actions is much easier.
The code is easier to read.
OK, you convinced me to try it out. How does it work?
The tutorial has a good section about it. Key bindings involve 2 objects InputMap and ActionMap. InputMap maps a user input to an action name, ActionMap maps an action name to an Action. When the user presses a key, the input map is searched for the key and finds an action name, then the action map is searched for the action name and executes the action.
Looks cumbersome. Why not bind the user input to directly to the action and get rid of the action name? Then you need only one map and not two.
Good question! You will see that this is one of the things that make key bindings more manageable (disable, rebind etc.).
I want you to give me a full working code of this.
No (the Swing tutorial has working examples).
You suck! I hate you!
Here is how to make a single key binding:
myComponent.getInputMap().put("userInput", "myAction");
myComponent.getActionMap().put("myAction", action);
Note that there are 3 InputMaps reacting to different focus states:
myComponent.getInputMap(JComponent.WHEN_FOCUSED);
myComponent.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
myComponent.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
WHEN_FOCUSED, which is also the one used when no argument is supplied, is used when the component has focus. This is similar to the key listener case.
WHEN_ANCESTOR_OF_FOCUSED_COMPONENT is used when a focused component is inside a component which is registered to receive the action. If you have many crew members inside a spaceship and you want the spaceship to continue receiving input while any of the crew members has focus, use this.
WHEN_IN_FOCUSED_WINDOW is used when a component which is registered to receive the action is inside a focused component. If you have many tanks in a focused window and you want all of them to receive input at the same time, use this.
The code presented in the question will look something like this assuming both objects are to be controlled at the same time:
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.
}
}
}
You can see that separating the input map from the action map allow reusable code and better control of bindings. In addition, you can also control an Action directly if you need the functionality. For example:
FireAction p1Fire = new FireAction(1);
p1Fire.setEnabled(false); // Disable the action (for both players in this case).
See the Action tutorial for more information.
I see that you used 1 action, move, for 4 keys (directions) and 1 action, fire, for 1 key. Why not give each key its own action, or give all keys the same action and sort out what to do inside the action (like in the move case)?
Good point. Technically you can do both, but you have to think what makes sense and what allows for easy management and reusable code. Here I assumed moving is similar for all directions and firing is different, so I chose this approach.
I see a lot of KeyStrokes used, what are those? Are they like a KeyEvent?
Yes, they have a similar function, but are more appropriate for use here. See their API for info and on how to create them.
Questions? Improvements? Suggestions? Leave a comment.
Have a better answer? Post it.
Note: this is not an answer, just a comment with too much code :-)
Getting keyStrokes via getKeyStroke(String) is the correct way - but needs careful reading of the api doc:
modifiers := shift | control | ctrl | meta | alt | altGraph
typedID := typed <typedKey>
typedKey := string of length 1 giving Unicode character.
pressedReleasedID := (pressed | released) key
key := KeyEvent key code name, i.e. the name following "VK_".
The last line should better be exact name, that is case matters: for the down key the exact key code name is VK_DOWN, so the parameter must be "DOWN" (not "Down" or any other variation of upper/lower case letters)
Not entirely intuitive (read: had to dig a bit myself) is getting a KeyStroke to a modifier key. Even with proper spelling, the following will not work:
KeyStroke control = getKeyStroke("CONTROL");
Deeper down in the awt event queue, a keyEvent for a single modifier key is created with itself as modifier. To bind to the control key, you need the stroke:
KeyStroke control = getKeyStroke("ctrl CONTROL");
Here is an easyway that would not require you to read hundreds of lines of code just learn a few lines long trick.
declare a new JLabel and add it to your JFrame (I didn't test it in other components)
private static JLabel listener= new JLabel();
The focus needs to stay on this for the keys to work though.
In constructor :
add(listener);
Use this method:
OLD METHOD:
private void setKeyBinding(String keyString, AbstractAction action) {
listener.getInputMap().put(KeyStroke.getKeyStroke(keyString), keyString);
listener.getActionMap().put(keyString, action);
}
KeyString must be written properly. It is not typesafe and you must consult the official list to learn what is the keyString(it is not an official term) for each button.
NEW METHOD
private void setKeyBinding(int keyCode, AbstractAction action) {
int modifier = 0;
switch (keyCode) {
case KeyEvent.VK_CONTROL:
modifier = InputEvent.CTRL_DOWN_MASK;
break;
case KeyEvent.VK_SHIFT:
modifier = InputEvent.SHIFT_DOWN_MASK;
break;
case KeyEvent.VK_ALT:
modifier = InputEvent.ALT_DOWN_MASK;
break;
}
listener.getInputMap().put(KeyStroke.getKeyStroke(keyCode, modifier), keyCode);
listener.getActionMap().put(keyCode, action);
}
In this new method you can simply set it using KeyEvent.VK_WHATEVER
EXAMPLE CALL:
setKeyBinding(KeyEvent.VK_CONTROL, new AbstractAction() {
#Override
public void actionPerformed(ActionEvent e) {
System.out.println("ctrl pressed");
}
});
Send an anonymous class (or use subclass) of AbstractAction. Override its public void actionPerformed(ActionEvent e) and make it do whatever you want the key to do.
PROBLEM:
I couldn't get it running for VK_ALT_GRAPH.
case KeyEvent.VK_ALT_GRAPH:
modifier = InputEvent.ALT_GRAPH_DOWN_MASK;
break;
does not make it work for me for some reason.
Here is an example of how to get key bindings working.
(Inside JFrame subclass using extends, which is called by the constructor)
// Create key bindings for controls
private void createKeyBindings(JPanel p) {
InputMap im = p.getInputMap(JPanel.WHEN_IN_FOCUSED_WINDOW);
ActionMap am = p.getActionMap();
im.put(KeyStroke.getKeyStroke("W"), MoveAction.Action.MOVE_UP);
im.put(KeyStroke.getKeyStroke("S"), MoveAction.Action.MOVE_DOWN);
im.put(KeyStroke.getKeyStroke("A"), MoveAction.Action.MOVE_LEFT);
im.put(KeyStroke.getKeyStroke("D"), MoveAction.Action.MOVE_RIGHT);
am.put(MoveAction.Action.MOVE_UP, new MoveAction(this, MoveAction.Action.MOVE_UP));
am.put(MoveAction.Action.MOVE_DOWN, new MoveAction(this, MoveAction.Action.MOVE_DOWN));
am.put(MoveAction.Action.MOVE_LEFT, new MoveAction(this, MoveAction.Action.MOVE_LEFT));
am.put(MoveAction.Action.MOVE_RIGHT, new MoveAction(this, MoveAction.Action.MOVE_RIGHT));
}
Separate class to handle those key bindings created above (where Window is the class that extends from JFrame)
// Handles the key bindings
class MoveAction extends AbstractAction {
enum Action {
MOVE_UP, MOVE_DOWN, MOVE_LEFT, MOVE_RIGHT;
}
private static final long serialVersionUID = /* Some ID */;
Window window;
Action action;
public MoveAction(Window window, Action action) {
this.window = window;
this.action = action;
}
#Override
public void actionPerformed(ActionEvent e) {
switch (action) {
case MOVE_UP:
/* ... */
break;
case MOVE_DOWN:
/* ... */
break;
case MOVE_LEFT:
/* ... */
break;
case MOVE_RIGHT:
/* ... */
break;
}
}
}