Coding the player

In this lesson, we'll add player movement, animation, and set it up to detect collisions.

To do so, we need to add some functionality that we can't get from a built-in node, so we'll add a script. Click the Player node and click the "Attach Script" button:

../../_images/add_script_button.png

Na janela de configurações de roteiro, você pode deixar as configurações padrões do jeito que estão. Apenas clique em "Criar":

Nota

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

../../_images/attach_node_window.png

Nota

If this is your first time encountering GDScript, please read Scripting languages before continuing.

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á definido no script.

Aviso

Se você estiver usando o C#, precisará (re) construir os assemblies do projeto sempre que desejar ver novas variáveis exportadas ou 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 "Build Project".

../../_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:

  • Verificação de entradas.

  • Movimentação em uma certa direção.

  • Reprodução da animação apropriada.

First, we need to check for input - is the player pressing a key? For this game, we have 4 direction inputs to check. Input actions are defined in the Project Settings under "Input Map". Here, you can define custom events and assign different keys, mouse events, or other inputs to them. For this game, we will map the arrow keys to the four directions.

Click on Project -> Project Settings to open the project settings window and click on the Input Map tab at the top. Type "move_right" in the top bar and click the "Add" button to add the move_right action.

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

We need to assign a key to this action. Click the "+" icon on the right, then click the "Key" option in the drop-down menu. A dialog asks you to type in the desired key. Press the right arrow on your keyboard and click "Ok".

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

Repeat these steps to add three more mappings:

  1. move_left mapped to the left arrow key.

  2. move_up mapped to the up arrow key.

  3. And move_down mapped to the down arrow key.

Your input map tab should look like this:

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

Click the "Close" button to close the project settings.

Nota

We only mapped one key to each input action, but you can map multiple keys, joystick buttons, or mouse buttons to the same input action.

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 a velocidade, o que significa que podemos definir seu comprimento (módulo) para 1 e multiplicar pela 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 Vector math. É bom conhecê-la, mas não será necessário para o resto deste tutorial.

We also check whether the player is moving so we can call play() or stop() on the AnimatedSprite.

Dica

$ is shorthand for get_node(). So in the code above, $AnimatedSprite.play() is the same as get_node("AnimatedSprite").play().

In GDScript, $ returns the node at the relative path from the current node, or returns null if the node is not found. Since AnimatedSprite is a child of the current node, we can use $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 (fixar) um valor significa restringi-lo a um determinado intervalo. Adicione o seguinte código embaixo da função ``_process``(Tenha certeza de que não está identado ao 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 ao comprimento 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 o frame rate sofrer alterações.

Click "Play Scene" (F6, Cmd + R on macOS) and confirm you can move the player around the screen in all directions.

Aviso

Se encontrar um erro no painel "Depurador" que diz

A tentativa de chamar a função 'play' na base 'instance null' em uma instância nula

this likely means you spelled the name of the AnimatedSprite node wrong. Node names are case-sensitive and $NodeName must match the name you see in the scene tree.

Selecionado as Animações

Now that the player can move, we need to change which animation the AnimatedSprite is playing based on its direction. We have the "walk" animation, which shows the player walking to the right. This animation should be flipped horizontally using the flip_h property for left movement. We also have the "up" animation, which should be flipped vertically with flip_v for downward movement. Let's place this code at the end of the _process() function:

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çoes booleanas no código acima são um encurtamento comum para programadores. Já que estamos fazendo um teste de comparação(booleano) e atribuindo um valor booleano, é possível fazer os dois ao mesmo tempo. Compare esse código a atribuição booleana encurtada, 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 "Walk" você também deve usar a letra maiúscula "W" no código.

Quando tiver certeza 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) quando 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 ); ele será emitido quando um corpo entrar em contato com o jogador. Clique em "Conectar.." e a janela "Conectando Sinal" aparece. Nós não precisamos alterar nenhuma destas configurações – o Godot criará automaticamente uma função chamada _on_Jogador_body_entered no script do jogador. Esta função será chamada sempre que o sinal for emitido - ela trata do sinal.

../../_images/player_signal_connection.png

Note que o icone verde indica que um sinal está conectado a esta função. Adicione esse 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 da engine. Usando set_deferred () nos permite que o Godot aguarde para desabilitar a forma até que seja seguro fazê-lo.

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

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

With the player working, we'll work on the enemy in the next lesson.