Programando o jogador

Nesta lição, adicionaremos movimento e animação do jogador e o configuraremos para detectar colisões.

Para isso, precisamos adicionar algumas funcionalidades que não conseguimos em um nó embutido, então adicionaremos um script. Clique no nó Player e depois clique no botão "Adicionar Script":

../../_images/add_script_button.png

Na janela de configurações do script, você pode deixar as configurações padrão inalteradas. Basta clicar em "Criar":

Nota

Se você estiver criando um script em C# ou em outras linguagens, selecione a linguagem no menu suspenso Idioma antes de clicar em Criar.

../../_images/attach_node_window.png

Nota

Se é a primeira vez que você se depara com GDScript, por favor, leia :ref:`doc_scripting`antes de continuar.

Comece declarando as variáveis membro que este objeto irá precisar:

extends Area2D

export var speed = 400 # How fast the player will move (pixels/sec).
var screen_size # Size of the game window.

Usar a palavra-chave export na primeira variável, speed, nos permite definir seu valor pelo Inspetor. Isto pode ser útil para os valores que você deseja ser capaz de ajustar do mesmo jeito que se faz com as propriedades internas de um nó. Clique no nó Jogador e você verá que a propriedade agora aparece na seção "Variáveis de Script" do Inspetor. Lembre-se, se você modificar o valor aqui, ele irá sobrepor o valor que está escrito no roteiro.

Aviso

Se você estiver usando o C#, precisará (re)construir os assemblies do projeto sempre que desejar ver as novas variáveis exportadas ou os novos sinais. Essa construção pode ser acionada manualmente clicando na palavra "Mono" na parte inferior da janela do editor para revelar o Painel Mono e, em seguida, clicando no botão "Construir Projeto".

../../_images/export_variable.png

A função _ready() é chamada quando um nó entra na árvore de cena, que é uma boa hora para descobrir o tamanho da janela do jogo:

func _ready():
    screen_size = get_viewport_rect().size

Agora podemos usar a função _process() para definir o que o jogador fará. _process() é chamada a cada quadro, então usaremos isso para atualizar os elementos de nosso jogo que esperamos que mudem frequentemente. Para o jogador, precisamos fazer o seguinte:

  • Verificar as entradas.

  • Movimentar na direção desejada.

  • Reproduzir a animação apropriada.

Primeiro, precisamos verificar as entradas – o jogador está pressionando uma tecla? Para este jogo, temos 4 entradas de direção para verificar. Ações de entrada são definidas nas Configurações do Projeto na aba "Mapa de entrada". Nela você pode definir eventos personalizados e atribuir diferentes teclas, eventos de mouse ou outras entradas para eles. Para este jogo, vamos mapear as teclas de seta do teclado para as quatro direções.

Clique em Projeto -> Configurações do Projeto para abrir a janela das configurações do projeto e clique na aba Mapa de Entrada, no topo. Digite "mover_direita" na barra do topo e clique no botão "Adicionar" para adicionar a ação mover_direita.

../../_images/input-mapping-add-action.png

Precisamos atribuir uma tecla para esta ação. Clique no ícone "+" à direita, e depois clique na opção "Tecla" no menu suspenso. Um diálogo lhe pede para digitar a tecla desejada. Pressione a seta para direita no seu teclado e clique em "Ok".

../../_images/input-mapping-add-key.png

Repita estes passos para adicionar três outros mapeamentos:

  1. mover_esquerda mapeado para a tecla seta para esquerda.

  2. mover_cima mapeado para tecla seta para cima.

  3. E mover_baixo mapeado para tecla seta para baixo.

Sua aba de mapa de entrada deveria se parecer com isto:

../../_images/input-mapping-completed.png

Clique no botão "Fechar" para fechar as configurações de projeto.

Nota

Nós mapeamos apenas uma tecla para cada ação de entrada, mas você pode mapear múltiplas teclas, botões de joystick ou botões de mouse para a mesma ação de entrada.

Você pode detectar se uma tecla é pressionada usando Input.is_action_pressed(), que retorna true (verdadeiro) se é pressionada ou false (falso) em caso contrário.

func _process(delta):
    var velocity = Vector2.ZERO # The player's movement vector.
    if Input.is_action_pressed("move_right"):
        velocity.x += 1
    if Input.is_action_pressed("move_left"):
        velocity.x -= 1
    if Input.is_action_pressed("move_down"):
        velocity.y += 1
    if Input.is_action_pressed("move_up"):
        velocity.y -= 1

    if velocity.length() > 0:
        velocity = velocity.normalized() * speed
        $AnimatedSprite.play()
    else:
        $AnimatedSprite.stop()

Iniciamos definindo a velocidade como sendo (0, 0) - por padrão o jogador não deve estar se movendo. Então nós verificamos cada entrada e adicionamos/subtraímos da velocidade para obter a direção resultante. Por exemplo, se você segurar direita e baixo ao mesmo tempo, o vetor resultante velocidade será (1, 1). Neste caso, já que estamos adicionando um movimento vertical e um horizontal, o jogador iria se mover mais rápido do que se apenas se movesse horizontalmente.

Podemos evitar isso se normalizarmos o vetor da velocidade, o que significa que podemos definir seu comprimento (módulo) para 1 e multiplicar pela intensidade da velocidade desejada. Isso resolve o problema de movimentação mais rápida nas diagonais.

Dica

Se você nunca usou matemática vetorial antes, ou precisa refrescar a memória, você pode ver uma explicação do uso de vetores no Godot em Matemática vetorial. É bom conhecê-la, mas não será necessário para o resto deste tutorial.

Nós também verificamos se o jogador está se movendo, para que possamos invocar play() (reproduzir) ou stop() (parar) no AnimatedSprite.

Dica

$ é um atalho para get_node(). Então, no código acima, $AnimatedSprite.play() é o mesmo que get_node("AnimatedSprite").play().

Em GDScript, $ retorna o nó no caminho relativo a partir deste nó, ou retorna null (nulo) se o nó não for encontrado. Já que AnimatedSprite é um filho do nó atual, podemos usar $AnimatedSprite.

Agora que temos uma direção de movimento, podemos atualizar a posição do jogador. Podemos também usar clamp() para impedir que ele saia da tela. Clamp (limitar) um valor significa restringi-lo a um determinado intervalo. Adicione o seguinte código ao final da função ``_process``(tenha certeza de que não está recuado para o else):

position += velocity * delta
position.x = clamp(position.x, 0, screen_size.x)
position.y = clamp(position.y, 0, screen_size.y)

Dica

O parâmetro delta na função _process() se refere à duração do frame - a quantidade de tempo que o frame anterior levou para ser completado. Usando este valor você se certifica que seu movimento será constante, mesmo quando a taxa de quadros sofrer alterações.

Clique em "Rodar Cena" (F6, Cmd + R no macOS) e confira se consegue mover o jogador pela tela em todas as direções.

Aviso

Se encontrar um erro no painel "Depurador" que diz

Tentativa de chamar a função 'play' na base 'instance null' em uma instância nula ( Attempt to call function 'play' in base 'null instance' on a null instance)

provavelmente significa que você digitou o nome do nó AnimatedSprite errado. Letras maiúsculas e minúsculas fazem diferença nos nomes dos nós, e $NomeDoNo tem que coincidir com o nome que você vê na árvore da cena.

Selecionado as Animações

Agora que o jogador consegue se mover, precisamos alterar qual animação do AnimatedSprite é reproduzida conforme sua direção. Temos uma animação "caminhada", que mostra o jogador andando para a direita. Essa animação deve ser espelhada horizontalmente usando a propriedade flip_h para o jogador se movimentar à esquerda. Temos também a animação "cima", que deveria ser espelhada verticalmente com flip_v para o movimento para baixo. Vamos colocar este código no fim da nossa função _process():

if velocity.x != 0:
    $AnimatedSprite.animation = "walk"
    $AnimatedSprite.flip_v = false
    # See the note below about boolean assignment.
    $AnimatedSprite.flip_h = velocity.x < 0
elif velocity.y != 0:
    $AnimatedSprite.animation = "up"
    $AnimatedSprite.flip_v = velocity.y > 0

Nota

As atribuições boolianas no código acima são um encurtamento comum para programadores. Já que estamos fazendo um teste de comparação (booliana) e também atribuindo um valor booliano, é possível fazer os dois ao mesmo tempo. Compare este código a atribuição booliana encurtada para uma linha acima:

if velocity.x < 0:
    $AnimatedSprite.flip_h = true
else:
    $AnimatedSprite.flip_h = false

Execute a cena novamente e verifique se as animações estão corretas em cada uma das direções.

Dica

Um erro comum aqui é digitar os nomes das animações de forma errada. O nome das animações no painel SpriteFrames deve ser igual ao que foi digitado no código. Se você nomeou a animação "Caminhada" você também deve usar a letra maiúscula "C" no código.

Quando tiver certeza de que a movimentação está funcionando corretamente, adicione esta linha à _ready() para que o jogador fique oculto no início do jogo:

hide()

Preparando para colisões

Nós queremos que o Jogador detecte quando é atingido por um inimigo, mas nós não fizemos nenhum inimigo ainda! Não tem problema, porque nós iremos utilizar a funcionalidade de sinal do Godot para fazer com que funcione.

Adicione o trecho a seguir no início do script, depois de extends Area2D:

signal hit

Isto define um sinal personalizado chamado "hit" (atingir) que faremos com que nosso jogador emita (envie) ao colidir com um inimigo. Iremos utilizar Area2D para detectar a colisão. Selecione o nó Jogador e clique na aba "Nó" (ao lado da aba "Inspetor") para ver a lista de sinais que o jogador pode emitir:

../../_images/player_signals.png

Observe que o nosso sinal personalizado "hit" também está lá! Já que nossos inimigos serão nós do tipo RigidBody2D, queremos o sinal body_entered( Object body ). Esse sinal será emitido quando um corpo entrar em contato com o jogador. Clique em "Conectar.." e a janela "Conectar um Sinal a um Método" aparece. Nós não precisamos alterar nenhuma destas configurações, então clique em "Conectar" novamente. O Godot criará automaticamente uma função no roteiro do seu jogador.

../../_images/player_signal_connection.png

Note o ícone verde indicando que um sinal está conectado a esta função. Adicione este código à função:

func _on_Player_body_entered(body):
    hide() # Player disappears after being hit.
    emit_signal("hit")
    # Must be deferred as we can't change physics properties on a physics callback.
    $CollisionShape2D.set_deferred("disabled", true)

Cada vez que um inimigo atinge o jogador, o sinal será emitido. Precisamos desativar a colisão do jogador para que não acione o sinal hit mais de uma vez.

Nota

Desativar o formato de colisão da área pode causar um erro se ocorrer no meio do processamento de colisão do motor de jogo. Usar set_deferred() fala para o Godot aguardar para desabilitar a forma até que seja seguro fazê-lo.

A última peça é adicionar uma função que possamos chamar para reiniciar/reconfigurar o jogador quando começarmos um novo jogo.

func start(pos):
    position = pos
    show()
    $CollisionShape2D.disabled = false

Com o jogador funcionando, vamos trabalhar com o inimigo na próxima lição.