Java - ¿Cómo detecto dos teclas simultaneas con KeyListener?

 
Vista:

¿Cómo detecto dos teclas simultaneas con KeyListener?

Publicado por Luis (2 intervenciones) el 05/03/2021 03:03:48
Estoy haciendo un juego 2D de tipo mazmorras, para mover al personaje uso la interface KeyListener con sus metodos KeyPressed() y KeyReleased(), usando el clasico WASD para el movimiento (arriba, abajo, izquierda y derecha).

¿Cuál es el problema? Necesito que los metodos de KeyListener detecten MAS DE UNA TECLA a la vez para moverme diagonalmente. Por ejemplo, que detecte W y A a la vez, no W o A. No se siquiera si es posible.

Si alguien conoce alguna manera de hacerlo le agradecería mucho si la compartiera. Gracias de antemano.
Valora esta pregunta
Me gusta: Está pregunta es útil y esta claraNo me gusta: Está pregunta no esta clara o no es útil
0
Responder

¿Cómo detecto dos teclas simultaneas con KeyListener?

Publicado por Tom (1831 intervenciones) el 05/03/2021 08:38:00
No creo que sea posible, las pulsaciones del teclado se reciben y procesan en secuencia (los modificadores son los únicos que se consideran "pulsación simultánea").
Te puedes meter en el lío de esperar unos milisegundos por una segunda tecla cuando recibes la primera pero me da que tu nivel no es muy alto ...
Lo que deberías hacer es lo que se ha hecho toda la vida ... usar más teclas (en tu caso 'Q' podría ser el movimiento diagonal).
Valora esta respuesta
Me gusta: Está respuesta es útil y esta claraNo me gusta: Está respuesta no esta clara o no es útil
0
Comentar
Imágen de perfil de Kabuto
Val: 3.428
Oro
Ha mantenido su posición en Java (en relación al último mes)
Gráfica de Java

¿Cómo detecto dos teclas simultaneas con KeyListener?

Publicado por Kabuto (1381 intervenciones) el 07/03/2021 02:33:34
Hola.

No tengo ni idea de como hacer un juego en Java. Pero para explorar un poco acerca de tu duda, he improvisado un JFrame con un JLabel que muestra la imagen de mi avatar.
Y sí he conseguido moverlo en diagonal pulsando dos teclas simultáneas.
https://www.youtube.com/watch?v=EgiTWOrGu_M

Como digo, no se hacer un juego en Java, así que el procedimiento que he seguido, no se si es aplicable directamente a lo que estés haciendo, pero quizás te sirva de algo.

El problema es que si mueves algo durante el KeyPressed, KeyPressed solo responde a la última tecla que has pulsado.
Entonces, hay que idear una fórmula para que KeyPressed inicie el movimiento en una determinada dirección, pero que este movimiento siga vigente aunque otra tecla haya sido presionado, la cuál iniciaría movimiento en otra dirección distinta.
Estos movimientos estarían vigentes hasta que ocurran los equivalentes KeyReleased.

Yo lo que he hecho es crear un JLabel con un ImageIcon y que además implementa la interfaz Runnable, para que me permita crear un hilo paralelo al programa principal.
Este JLabel también tiene los clásicos atributos x e y para situarlos en un punto de la interfaz.
Pero además tiene 4 booleans, uno para cada dirección: arriba, abajo, izquierda y derecha.

Cuando ocurre un KeyPressed, el boolean correspondiente adquiere valor true. Si pulso la W, pues "arriba" pasa a valer true.
Y cuando "arriba" tiene valor true, se ejecuta el método subir(), que es el que modifica el valor del "eje y" para cambiar la localización del JLabel.

De este modo, no importa si ocurren otros KeyPressed. El boolean "arriba" sigue teniendo valor true y lo seguirá teniendo (y moviendo el JLabel), hasta que ocurra el KeyReleased de la tecla W, será entonces cuando pase a valer false y dejará de moverse hacia arriba.

Esto es posible por usar la interfaz Runnable. Así el JLabel inicia un hilo paralelo y continuamente comprueba los valores de esos 4 booleans. En cuanto estos adquieren valor true, se ejecutan los métodos para mover en la dirección correspondiente.
Y esto permite mover en diagonal.

En fin, aunque ha quedado resultón, el programa en sí es un poco chapucero y ya imagino que debe ser la peor forma de intentar hacer un videojuego je je.
De hecho, no se por qué, para que el JLabel responda, tengo que hacer un System.out al iniciarse el método run() de la interfaz Runnable:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Override
public void run() {
    while(true) {
        System.out.println();
        if (moverArriba) {
            subir();
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        if (moverAbajo) {
            bajar();
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        if (moverIzquierda) {
            izquierda();
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        if (moverDerecha) {
            derecha();
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Esto implica que continuamente estoy haciendo saltos de línea en la consola ja ja, pero es que si no lo pongo, si no imprimo algo cualquiera en consola, el JLabel no responde a los KeyListener

En fin, a ver si esto de usar booleans que mantengan sus valores hasta el correspondiente KeyReleased, para que no afecte que ocurran otros KeyPressed, puede servirte de inspiración.

Dejo aquí el código completo. Un saludo.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
package moverConTeclas;
 
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
 
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
 
@SuppressWarnings("serial")
public class Mover extends JFrame implements KeyListener{
 
	private Kabuto kabuto = new Kabuto(); //JLabel que implementa Runnable
	private PanelBotones pnBotones = new PanelBotones();
 
	public Mover() {
 
		JPanel superficie = new JPanel();
		superficie.setLayout(null);
		superficie.setBackground(Color.WHITE);
		superficie.setBorder(BorderFactory.createLoweredSoftBevelBorder());
		superficie.add(kabuto);
 
		setLayout(new BorderLayout());
		add(superficie, BorderLayout.CENTER);
		add(pnBotones, BorderLayout.SOUTH);
 
		addKeyListener(this);
		setTitle("Mover con teclas");
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		setSize(800, 600);
		setLocationRelativeTo(null);
		setResizable(false);
		setVisible(true);
		//Creamos hilo paralelo con el JLabel
		Thread hilo = new Thread(kabuto);
		hilo.start();
	}
 
	/*
	 * Clase JLabel que implementa Runnable para poder iniciar
	 * un hilo paralelo al programa principal.
	 * Este JLabel se desplazará en las direcciones que indiquen
	 * los distintos atributos booleans que posee.
	 * Los valores de estos booleans, cambiarán cuando ocurran
	 * eventos KeyPressed y KeyReleased
	 */
	private class Kabuto extends JLabel implements Runnable{
		private int x = 0;
		private int y = 0;
		private ImageIcon imagen;
		public boolean moverArriba = false;
		public boolean moverAbajo = false;
		public boolean moverIzquierda = false;
		public boolean moverDerecha = false;
 
		public Kabuto() {
			imagen = new ImageIcon(Mover.class.getClassLoader().getResource("moverConTeclas/kabuto.jpeg"));
			setIcon(imagen);
			setBounds(x, y, imagen.getIconWidth(), imagen.getIconHeight());
		}
 
		private void subir() {
			if (y > 0) {
				y--;
				setLocation(x, y);
			}
		}
 
		private void bajar() {
			if (y < 500) {
				y++;
				setLocation(x, y);
			}
		}
 
		private void derecha() {
			if (x < 500) {
				x++;
				setLocation(x, y);
			}
		}
 
		private void izquierda() {
			if (x > 0) {
				x--;
				setLocation(x, y);
			}
		}
 
		@Override
		public void run() {
			while(true) {
				System.out.println();
				if (moverArriba) {
					subir();
					try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				if (moverAbajo) {
					bajar();
					try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				if (moverIzquierda) {
					izquierda();
					try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				if (moverDerecha) {
					derecha();
					try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		}
	}
 
	private class PanelBotones extends JPanel {
		private JButton btW;
		private JButton btS;
		private JButton btA;
		private JButton btD;
 
		public PanelBotones() {
			btW = new JButton("W");
			btW.setEnabled(false);
			btW.setBackground(Color.WHITE);
			btS = new JButton("S");
			btS.setEnabled(false);
			btS.setBackground(Color.WHITE);
			btA = new JButton("A");
			btA.setEnabled(false);
			btA.setBackground(Color.WHITE);
			btD = new JButton("D");
			btD.setEnabled(false);
			btD.setBackground(Color.WHITE);
			JPanel arriba = new JPanel();
			arriba.add(btW);
			JPanel medio = new JPanel();
			medio.add(btA);
			medio.add(btD);
			JPanel abajo = new JPanel();
			abajo.add(btS);
			setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
			add(arriba);
			add(medio);
			add(abajo);
		}
 
		public void pulsarW(boolean pulsar) {
			if (pulsar) {
				btW.setBackground(Color.YELLOW);
			}
			else {
				btW.setBackground(Color.WHITE);
			}
		}
 
		public void pulsarS(boolean pulsar) {
			if (pulsar) {
				btS.setBackground(Color.YELLOW);
			}
			else {
				btS.setBackground(Color.WHITE);
			}
		}
 
		public void pulsarA(boolean pulsar) {
			if (pulsar) {
				btA.setBackground(Color.YELLOW);
			}
			else {
				btA.setBackground(Color.WHITE);
			}
		}
 
		public void pulsarD(boolean pulsar) {
			if (pulsar) {
				btD.setBackground(Color.YELLOW);
			}
			else {
				btD.setBackground(Color.WHITE);
			}
		}
	}
 
	public static void main(String[] args) {
		SwingUtilities.invokeLater(new Runnable() {
			@Override
			public void run() {
				new Mover();
			}
		});
	}
 
	@Override
	public void keyTyped(KeyEvent e) {}
 
	@Override
	public void keyPressed(KeyEvent e) {
		switch(e.getKeyCode()) {
		case KeyEvent.VK_W:
			kabuto.moverArriba = true;
			pnBotones.pulsarW(true);
			break;
		case KeyEvent.VK_S:
			kabuto.moverAbajo = true;
			pnBotones.pulsarS(true);
			break;
		case KeyEvent.VK_A:
			kabuto.moverIzquierda = true;
			pnBotones.pulsarA(true);
			break;
		case KeyEvent.VK_D:
			kabuto.moverDerecha = true;
			pnBotones.pulsarD(true);
		}
	}
 
	@Override
	public void keyReleased(KeyEvent e) {
		switch(e.getKeyCode()) {
		case KeyEvent.VK_W:
			kabuto.moverArriba = false;
			pnBotones.pulsarW(false);
			break;
		case KeyEvent.VK_S:
			kabuto.moverAbajo = false;
			pnBotones.pulsarS(false);
			break;
		case KeyEvent.VK_A:
			kabuto.moverIzquierda = false;
			pnBotones.pulsarA(false);
			break;
		case KeyEvent.VK_D:
			kabuto.moverDerecha = false;
			pnBotones.pulsarD(false);
		}
	}
 
}
Valora esta respuesta
Me gusta: Está respuesta es útil y esta claraNo me gusta: Está respuesta no esta clara o no es útil
0
Comentar

¿Cómo detecto dos teclas simultaneas con KeyListener?

Publicado por Luis (Autor) (2 intervenciones) el 07/03/2021 04:35:43
¡Muchisimas gracias!
Valora esta respuesta
Me gusta: Está respuesta es útil y esta claraNo me gusta: Está respuesta no esta clara o no es útil
0
Comentar

¿Cómo detecto dos teclas simultaneas con KeyListener?

Publicado por Tom (1831 intervenciones) el 08/03/2021 12:16:57
Es buena idea, @Kabuto, salvo que no parece estrictamente un movimiento en diagonal, sino más bien dos movimientos (1 horizontal, 1 vertical o viceversa).
Se puede detectar así si dos teclas están pulsadas en el mismo momento, de acuerdo, pero no si las dos teclas se pulsan al mismo tiempo (simultáneamente) o casi al mismo tiempo.
Para visualizarlo, imagina que un jugador solamente puede mover una casilla en su turno y/o que cada movimiento de una casilla se valida para ver si hay colisión (por ejemplo).
Valora esta respuesta
Me gusta: Está respuesta es útil y esta claraNo me gusta: Está respuesta no esta clara o no es útil
0
Comentar
Imágen de perfil de Kabuto
Val: 3.428
Oro
Ha mantenido su posición en Java (en relación al último mes)
Gráfica de Java

¿Cómo detecto dos teclas simultaneas con KeyListener?

Publicado por Kabuto (1381 intervenciones) el 09/03/2021 00:32:32
Cierto, el movimiento de mi JLabel, da el pego, pero no hace las diagonales rectas, porque en cada acción solo altero uno de los ejes

Sin embargo, quizás ampliando el código pudiera mejorarse. Yo solo pregunto por los booleans de forma individual, pero si antes preguntase por "parejas" de booleans, tipo "si arriba es true y derecha es true", pues entonces en la misma acción pueda alterar los dos ejes de forma simultánea.

Incluso había pensado (mi trabajo es muy monótono a veces y me evado pensando en estas cosas xDD ) en que el código quizás podría simplificarse usando un array con dos enteros, uno para cada eje. Y que cada entero tenga tres posibles valores:
0 no hay movimiento en el eje correspondiente (keyReleased)
1 el eje correspondiente ha de incrementarse (keyPressed de la tecla asociada sentido del eje)
-1 el eje ha de decrementarse (keyPressed de la tecla asociada al sentido opuesto del eje)

Así en cada acción, a los ejes solo habría que sumar los valores de ese array
[0,0] -- no hay desplazamiento
[0,1] -- derecha (incrementa eje Y)
[0, -1] -- izquierda (decrementa eje y)
[1, -1] -- diagonal izquierda-abajo (incremente eje x y decrementa eje y)
etc...


Así no habría que preguntar cada vez por los 4 booleans y todos sus posibles emparejamientos.


Sobre detectar dos pulsaciones REALMENTE simultáneas, ahí ya supongo que nos topamos con limitaciones de la propia interfaz KeyListener, que no creo que se hiciera pensando en videojuegos.
Tal vez se requiera de otro tipo de librerías, que trabajen a más "bajo nivel".
Pero vamos, hablo sin conocimiento de la materia.

Un saludo.
Valora esta respuesta
Me gusta: Está respuesta es útil y esta claraNo me gusta: Está respuesta no esta clara o no es útil
0
Comentar

¿Cómo detecto dos teclas simultaneas con KeyListener?

Publicado por Tom (1831 intervenciones) el 09/03/2021 08:57:38
En mi opinión está muy bien darle vueltas a las cosas hasta llegar al quid de la cuestión. En este caso, la clave es que los keypress te llegan en secuencia, uno detrás de otro (el teclado los envía así, el driver del kernel los procesa así y el sistema gráfico los envía así a tu aplicación).
De ahí lo fundamental, poner un timer o algo parecido y no tratar inmediatamente cada keypress sino tratar los keypress que se hayan producido en un intervalo corto de tiempo. Por ejemplo puedes hacer que cada 15 ó 20 milisegundos se compruebe qué teclas están pulsadas ... y se haga el movimiento apropiado.
Seguro que si te pones lo sacas en un rato.
Valora esta respuesta
Me gusta: Está respuesta es útil y esta claraNo me gusta: Está respuesta no esta clara o no es útil
0
Comentar