Escutando teclas simultâneas
A questão de identificar teclas apertadas simultaneamente pode surgir quando estamos desenvolvendo um sketch interativo, e em especial se estamos criando um jogo, em que mais pessoas interagem simultaneamente usando o teclado, também sempre que a interface fica mais complexa e precisa de teclas em combinação.
Entendendo o problema
Executando o código a seguir você vai notar que a variável key
aponta para um valor que descreve a última tecla que foi pressionada (ou solta) no teclado. Isso pode ser um problema se você precisar mostrar quando a e b estiverem apertadas simultaneamente.
Não deixe de executar e experimentar!
def setup():
size(256,256)
text_align(CENTER, CENTER)
text_size(20)
stroke_weight(3)
def draw():
background(200, 200, 0)
if key == 'a':
fill(200, 0, 0)
rect(64, 96, 64, 64)
fill(255)
text('a', 96, 128)
if key == 'b':
fill(0, 0, 200)
rect(128, 96, 64, 64)
fill(255)
text('b', 160, 128)
Uma modificação pode evitar que uma tecla seja indicada no momento em que é solta e também que fique aparecendo quando já não está mais sendo apertada, mas isso ainda não resolve o problema das teclas simultâneas:
if is_key_pressed and key == 'a':
fill(200, 0, 0)
rect(64, 96, 64, 64)
fill(255)
text('a', 96, 128)
if is_key_pressed and key == 'b':
fill(0, 0, 200)
rect(128, 96, 64, 64)
fill(255)
text('b', 160, 128)
Uma primeira solução
As soluções para esta questão envolvem criar uma estrutura que guarde o estado das teclas, indicando se a tecla está apertada naquele momento, e que possa ser modificada pelos eventos de apertar ou soltar uma tecla. Em um primeiro momento, para este nosso exemplo, a estrutura pode ser simplesmente um par de variáveis globais, usadas como indicadores (flags) do estado das teclas, a_apertada
e b_apertada
.
a_apertada = False
b_apertada = False
def setup():
size(256,256)
text_align(CENTER, CENTER)
text_size(20)
stroke_weight(3)
def draw():
background(0, 200, 200)
if a_apertada:
fill(200, 0, 0)
rect(64, 96, 64, 64)
fill(255)
text('a', 96, 128)
if b_apertada:
fill(0, 0, 200)
rect(128, 96, 64, 64)
fill(255)
text('b', 160, 128)
def key_pressed():
global a_apertada, b_apertada
if key == 'a':
a_apertada = True
if key == 'b':
b_apertada = True
def key_released():
global a_apertada, b_apertada
if key == 'a':
a_apertada = False
if key == 'b':
b_apertada = False
Notas
-
Se você soltar uma tecla quando o foco do seu sistema operacional não estiver na janela do sketch o evento
key_released()
não será acionado, e o sketch não fica sabendo que a tecla foi solta! -
Nas versões finais com teclado do jogo PONG neste repositóro, usamos exatamente essa estratégia, sem isso a experiência de jogo fica muito prejudicada.
Um montão de teclas
Mas como fazer se o número de teclas que queremos identificar for muito grande? Temos que fazer um monte variáveis globais e um monte de condicionais com if
dentro do key_pressed()
e do key_released
? Isso não parece muito elegante!
Vamos então explorar uma estratégia de guardar as teclas que foram apertadas em uma estrutura de dados chamada conjunto (set), removendo-as do conjunto quando forem soltas. É bom notar que conjuntos não guardam a ordem em que seus itens foram adicionados, e os itens são únicos, um conjunto nunca tem itens duplicados.
Para acrescentar um item em um conjunto usamos conjunto.add(item)
, e para remover conjunto.discard(item)
. Essa última operação, discard, não faz nada se o item não existir no conjunto.
Em Python, podemos saber se um item existe dentro de uma coleção (como listas, tuplas, deques e conjuntos) com a palavra chave in
usada como um operador, e isso é computacionalmente muito mais eficiente em um conjunto grande do que em uma lista ou tupla grande! No exemplo abaixo, se 'b' in teclas_apertadas
for verdade, o fundo fica preto.
teclas_apertadas = set() # conjunto (set) vazio
def setup():
size(512,256)
text_align(CENTER, CENTER)
text_size(20)
stroke_weight(3)
def draw():
if 'b' in teclas_apertadas:
background(0)
else:
background(100, 0, 200)
for i, k in enumerate(teclas_apertadas):
x = i * 64
fill(0, x, 255 - i * 32)
rect(x, 96, 64, 64)
fill(255)
text(str(k), x + 32, 128)
def key_pressed():
teclas_apertadas.add(key)
def key_released():
teclas_apertadas.discard(key)
Você viu um 65535
no meio das teclas?
Significa que uma tecla codificada (CODED
) foi pressionada, como SHIFT
, por exemplo. Precisamos lembrar que algumas teclas são identificadas de maneira ligeiramente diferente, as ditas teclas codificadas. Quando key == CODED
você precisa usar a variável key_code
para saber qual tecla foi apertada (ou solta), em geral comparando com uma constante numérica destas aqui:
UP DOWN LEFT RIGHT ALT CONTROL SHIFT
Note que TAB
, ENTER
e algumas outras teclas não codificadas também não foram mostradas direito no exemplo anterior. Certas teclas que não são codificadas, que podem ser identificadas usando key
, precisam ser econtradas fazendo uma comparação de key
com constantes ou strings especiais:
BACKSPACE '\b'
TAB '\t'
ENTER '\n'
RETURN '\r'
ESC '\x1b'
DELETE '\x7f'
Vamos fazer alguns ajustes no código para identificar e mostrar de maneira mais elegante essas teclas!
Para isso vamos usar outra estrutura de dados chamada dicionário (dict). Que mapeia (cria uma correspondência entre) chaves (keys) e valores (values). É muito rápido consultar um valor atrelado a uma chave em um dicionário.
Se você sabe que a chave existe no dicionário, pode consultar com a forma dicionario[chave]
(que dá erro se a chave não existir no dicionário). Quando não se tem certeza se a chave está lá, ou é parte da estratégia procurar chaves que podem não estar lá, então se usa dicionario.get(chave, valor_se_nao_tem_a_chave)
.
teclas_apertadas = set() # conjunto (set) vazio
# dicionário {tecla: 'nome para mostrar'}
nomes = {UP: '↑',
DOWN: '↓',
LEFT: '←',
RIGHT: '→',
ALT: 'Alt',
CONTROL: 'Ctrl',
SHIFT: 'Shift',
BACKSPACE: 'Bcksp',
TAB: 'Tab',
ENTER: 'Enter',
RETURN: 'Return',
ESC: 'Esc',
DELETE: 'Del',
524: 'Meta',
525: 'Menu',
65406: 'AltGr',
155: 'Insert',
36: 'Home',
35: 'End',
33: 'PgUp',
34: 'PgDwn',
144: 'NumLk',
' ': 'espaço',
}
def setup():
size(512, 256)
text_align(CENTER, CENTER)
text_size(15)
stroke_weight(3)
def draw():
# em vez de 'b' agora o espaço deixa o fundo preto
if ' ' in teclas_apertadas:
background(0)
else:
background(50, 200, 50)
for i, tecla in enumerate(sorted(teclas_apertadas)):
# tendo `tecla` no dicionário pega o 'nome para mostrar'
n = nomes.get(tecla, tecla) # se não tiver, devolve `tecla` mesmo!
x = i * 64
fill(0, x / 2, 200)
rect(x, 96, 64, 64)
fill(255)
text(n, x + 32, 128)
def key_pressed():
if key != CODED:
teclas_apertadas.add(key)
else:
teclas_apertadas.add(key_code)
# Impeça que o sketch seja encerrado com ESC!
if key == ESC:
this.key = ' '
def key_released():
if key != CODED:
teclas_apertadas.discard(key)
else:
teclas_apertadas.discard(key_code)
Notas
-
Uma vez que certas teclas modificam o efeito de outras, por exemplo,
SHIFT
faz a tecla1
aparecer como!
, então certas sequências podem trazer resultados estranhos:Apertar
SHIFT
, depois1
, soltarSHIFT
e por fim soltar1
. faz o sketch ficar sem ver a tecla!
ser ‘solta’. Uma solução possível é manter registro só dokey_code
das teclas que permanece sempre o mesmo, mas podemos converter okey_code
, que é um número, em algo mais legível, no caso das teclas não codificadas, usando chr()`:def key_pressed(): if key != CODED: teclas_apertadas.add(chr(key_code)) else: teclas_apertadas.add(key_code) def key_released(): if key != CODED: teclas_apertadas.discard(chr(key_code)) else: teclas_apertadas.discard(key_code)
Note que agora
a
eA
devem aparecer como ` Ae ,
1e
!como
1. Fique atento e teste para evitar surpresas! No meu computador o
key_codede
+e
-do teclado numérico lateral, por exemplo, aparecem como
ke
m`. - Foi usada
sorted()
para obter uma lista ordenada a partir do conjuntoteclas_apertadas
- Dentro do
key_pressed()
tem um pequeno truque que impede o sketch de ser interrompido pela teclaESC
. - No dicionário acrescentei alguns códigos de teclas que vi, estando no Linux, os códigos e nomes das teclas podem variar dependendo do seu sistema operacional.
Combinando estratégias
A estratégia dos indicadores de estado para teclas, ou de adicionar e remover indicadores de teclas apertadas em um conjunto (set) usando key_pressed()
e key_released()
é boa para saber se uma tecla está apertada em um determinado momento, muito útil principalmente em controles que podem ser acionados continuamente.
Já para alternar um ajuste, algo como ligar e desligar uma opção, por exemplo (em inglês é usado o termo toggle), pode ser melhor usar um indicador modificado por uma condicional simples em keyTyped()
,key_pressed()
ou key_released()
, para evitar que um toque da tecla acione mais de uma vez a ação.
No exemplo abaixo, vamos usar um dicionário para guardar um monte de informações a respeito de dois círculos, incluindo cores, e quais teclas podem ser usadas para alterar a posição além de uma tecla para trocar a cor de contorno pela cor de preenchimento, e vice-versa.
Use SHIFT
para ligar e desligar a animação da cor do fundo e a barra de espaço para voltar os círculos à posição inicial.
teclas_apertadas = set() # conjunto (set) vazio
pa = {'x': 128, 'y': 128,
'fill': color(0, 0, 200), 'stroke': 0,
'sobe': 'W', 'desce': 'S',
'esq': 'A', 'dir': 'D',
'inv': TAB}
pb = {'x': 384, 'y': 128,
'fill': color(200, 0, 0), 'stroke': 255,
'sobe': UP, 'desce': DOWN,
'esq': LEFT, 'dir': RIGHT,
'inv': ENTER}
players = (pa, pb)
anima_fundo = False
cor_fundo = 128
def setup():
size(512, 256)
text_align(CENTER, CENTER)
text_size(15)
stroke_weight(3)
def draw():
global cor_fundo
if anima_fundo:
cor_fundo = abs(cor_fundo + sin(frame_count / 60.)) % 256
background(cor_fundo)
for p in players:
# print(p) # debug
fill(p['fill'])
stroke(p['stroke'])
ellipse(p['x'], p['y'], 50, 50)
# Ajusta a posição dos círculos
if p['sobe'] in teclas_apertadas:
p['y'] -= 1
if p['desce'] in teclas_apertadas:
p['y'] += 1
if p['esq'] in teclas_apertadas:
p['x'] -= 1
if p['dir'] in teclas_apertadas:
p['x'] += 1
def key_pressed():
teclas_apertadas.add(key_code if key == CODED else chr(key_code))
for p in players:
if p['inv'] in teclas_apertadas:
p['fill'], p['stroke'] = p['stroke'], p['fill']
def key_released():
global anima_fundo
teclas_apertadas.discard(key_code if key == CODED else chr(key_code))
if key_code == SHIFT:
anima_fundo = not anima_fundo
if key == ' ':
pa['x'], pa['y'] = 128, 128
pb['x'], pb['y'] = 384, 128
Notas
- As funções
key_typed()
ekey_pressed()
são acionadas logo que a tecla é apertada, e são suscetíveis a repetição automática depois de um tempo da tecla mantida apertada, jákey_released()
é acionada só quando a tecla é solta.
Exercício
- Você conseguiria adicionar um terceiro círculo ao código?
- Mudar a cor ou o tamanho dos círculos conforme as teclas que estão apertadas?