Seu primeiro jogo

Visão geral

Este tutorial lhe guiará sobre como fazer seu primeiro projeto no Godot. Você aprenderá como o editor Godot funciona, como estruturar um projeto e como construir um jogo 2D.

Nota

Este projeto é uma introdução ao motor Godot. Ele assume que você já tenha uma experiência com programação. Se você é completamente cru em programação, deveria começar por aqui: Scripting.

O jogo se chama “Dodge the Creeps!” (em tradução livre, “Desvie das Criaturas!”). Seu personagem deve se mover e evitar os inimigos por quanto tempo puder. Aqui está uma prévia do resultado final:

../../_images/dodge_preview.gif

Por que 2D? Jogos 3D são muito mais complexos do que os 2D. Você deveria se concentrar em 2D até que tenha uma boa compreensão do processo de desenvolvimento de jogos.

Configuração do projeto

Execute o Godot e crie um novo projeto. Então, baixe dodge_assets.zip – as imagens e os sons que você usará para fazer o jogo. Descompacte esses arquivos na pasta do seu projeto.

Nota

Para este tutorial, vamos assumir que você está familiarizado com o editor. Se ainda não leu Cenas e nós, faça isso agora para uma explicação de configuração de um projeto e de uso do editor.

Este jogo usará o modo retrato, então precisamos ajustar as dimensões da janela de jogo. Clique em Projeto -> Configurações do Projeto -> Display (Exibição) -> Window (Janela) e configure “Width” (“Largura”) para 480 e “Height” (Altura) para 720.

Organizando o projeto

Neste projeto, vamos criar três cenas independentes: Jogador, Inimigo e HUD, que combinaremos na cena Principal do jogo. Em um projeto maior, pode ser útil criar pastas para armazenar as várias cenas e seus roteiros, mas para este jogo relativamente pequeno, você pode salvar tudo na pasta raiz, referenciada como res://. Você pode ver as pastas do seu projeto no painel Arquivos no canto inferior esquerdo:

../../_images/filesystem_dock.png

Cena do jogador

A primeira cena que faremos define o objeto Jogador. Um dos benefícios de criar uma cena Jogador em separado é que podemos testá-la separadamente, mesmo antes de criarmos outras partes do jogo.

Estrutura de nós

Para começar, clique no botão “Adicionar Nó Filho” e adicione à cena um nó Area2D.

../../_images/add_node.png

Com Area2D, nós podemos detectar objetos que se sobreponham ou vão de encontro ao jogador. Mude seu nome para Jogador clicando no nome do nó. Este é o nó raiz da cena. Nós podemos inserir nós adicionais ao jogador para adicionar funcionalidades.

Antes de adicionar filhos ao nó Jogador, queremos ter certeza que não os moveremos nem os redimensionaremos acidentalmente ao clicar neles. Selecione o nó e clique no ícone à direita do cadeado; seu texto de dica diz “Garante que os filhos do objeto não sejam selecionáveis.”

../../_images/lock_children.png

Salve a cena: clique em Cena -> Salvar ou pressione Ctrl+S no Windows/Linux ou Command+S no Mac.

Nota

Para esse projeto, vamos seguir as convenções de nomeação Godot.

  • GDScript: Classes (nós) usam o estilo PascalCase (IniciaisMaiúsculas), variáveis e funções usam snake_case (minúsculas_separadas_por_sublinha), e constantes usam ALL_CAPS (TODAS_MAIÚSCULAS) (Veja mais em GDScript style guide).
  • ** C # **: Classes, variáveis de exportação e métodos usam PascalCase, campos privados usam _camelCase, variáveis locais e parâmetros usam camelCase (Veja: ref: doc_c_sharp_styleguide). Tenha o cuidado de digitar os nomes dos métodos com precisão ao conectar os sinais.

Animação por Sprites

Click on the Player node and add an AnimatedSprite node as a child. The AnimatedSprite will handle the appearance and animations for our player. Notice that there is a warning symbol next to the node. An AnimatedSprite requires a SpriteFrames resource, which is a list of the animations it can display. To create one, find the Frames property in the Inspector and click “[empty]” -> “New SpriteFrames”. This should automatically open the SpriteFrames panel.

../../_images/spriteframes_panel.png

À esquerda, está uma lista de animações. Clique na “default” e a renomeie para “direita”. Então, clique no botão “Adicionar” para criar uma segunda animação chamada “cima”. Arraste as duas imagens de cada animação, chamadas playerGrey_up[1/2] e playerGrey_walk[1/2] para o lado “Quadros da Animação” do painel:

../../_images/spriteframes_panel2.png

As imagens do jogador são um tanto grandes demais para a janela de jogo, então precisamos reduzir sua escala. Clique no nó AnimatedSprite e configure a propriedade Scale para (0.5, 0.5). Você pode encontrá-la no Inspetor na seção Node2D.

../../_images/player_scale.png

Finalmente, adicione um CollisionShape2D (forma de colisão 2D) como filho de Jogador. Isso determina a “caixa de acerto” do jogador, ou seja, os limites da sua área de colisão. Para este personagem, um nó CapsuleShape2D (forma cápsula 2D) é o que melhor se encaixa. Então, ao lado de “Shape” (forma) no Inspetor, clique em “<null>” -> “Novo CapsuleShape2D”. Utilizando os dois manipuladores, redimensione a forma para cobrir o sprite:

../../_images/player_coll_shape.png

Quando tiver finalizado, sua cena Jogador deveria se parecer assim:

../../_images/player_scene_nodes.png

Movendo o Jogador

Agora precisamos adicionar alguma funcionalidade que não conseguimos em um nó embutido, então adicionaremos um roteiro. Clique no nó Jogador e depois clique no botão “Adicionar Script”:

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

Se esta é sua primeira vez se deparando com a GDScript, por favor leia 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.
public class Player : Area2D
{
    [Export]
    public int Speed = 400; // How fast the player will move (pixels/sec).

    private Vector2 _screenSize; // Size of the game window.
}

Usar a palavra-chave export na primeira variável, SPEED (velocidade), 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 Scrip” 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 ou sinais de exportação. Essa construção pode ser acionada manualmente clicando na palavra “Mono” na parte inferior da janela do editor para revelar o Mono Panel 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
public override void _Ready()
{
    _screenSize = GetViewport().GetSize();
}

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.

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”. Você pode definir eventos personalizados e atribuir diferentes teclas, eventos de mouse ou outras entradas para eles. Para esta demonstração, vamos usar os eventos padrões que são atribuídos para as teclas de seta no teclado.

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()  # The player's movement vector.
    if Input.is_action_pressed("ui_right"):
        velocity.x += 1
    if Input.is_action_pressed("ui_left"):
        velocity.x -= 1
    if Input.is_action_pressed("ui_down"):
        velocity.y += 1
    if Input.is_action_pressed("ui_up"):
        velocity.y -= 1
    if velocity.length() > 0:
        velocity = velocity.normalized() * speed
        $AnimatedSprite.play()
    else:
        $AnimatedSprite.stop()
public override void _Process(float delta)
{
    var velocity = new Vector2(); // The player's movement vector.

    if (Input.IsActionPressed("ui_right"))
    {
        velocity.x += 1;
    }

    if (Input.IsActionPressed("ui_left"))
    {
        velocity.x -= 1;
    }

    if (Input.IsActionPressed("ui_down"))
    {
        velocity.y += 1;
    }

    if (Input.IsActionPressed("ui_up"))
    {
        velocity.y -= 1;
    }

    var animatedSprite = GetNode<AnimatedSprite>("AnimatedSprite");

    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 a 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 uma 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.

Nós também verificamos se o jogador está se movendo, para que possamos iniciar ou parar a animação do AnimatedSprite.

Dica

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.

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

Now that we have a movement direction, we can update the player’s position. We can also use clamp() to prevent it from leaving the screen. Clamping a value means restricting it to a given range. Add the following to the bottom of the _process function:

position += velocity * delta
position.x = clamp(position.x, 0, screen_size.x)
position.y = clamp(position.y, 0, screen_size.y)
Position += velocity * delta;
Position = new Vector2(
    x: Mathf.Clamp(Position.x, 0, _screenSize.x),
    y: Mathf.Clamp(Position.y, 0, _screenSize.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.

Clique em “Rodar Cena” (F6) e confirme que você pode mover o jogador pela tela em todas as direções.A saída do console que abre ao rodar a cena pode ser fechada ao clicar em Saída (que deve estar destacada em azul) no canto inferior esquerdo do Painel Inferior.

Aviso

Se você vir no painel “Depurador” um erro referente a “null instance” (instância nula), provavelmente significa que você digitou o nome errado do nó. Maiúsculas e minúsculas fazem diferença nos nomes dos nós, e $NomeDoNo ou get_node("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 a direção. Temos uma animação “direita”, que deveria ser espelhada horizontalmente usando a propriedade flip_h para o jogador se movimentar à esquerda, e uma 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() (substitua “right” por “direita” e “up” por “cima”):

if velocity.x != 0:
    $AnimatedSprite.animation = "right"
    $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
if (velocity.x != 0)
{
    animatedSprite.Animation = "right";
    // See the note below about boolean assignment
    animatedSprite.FlipH = velocity.x < 0;
    animatedSprite.FlipV = false;
}
else if(velocity.y != 0)
{
    animatedSprite.Animation = "up";
    animatedSprite.FlipV = velocity.y > 0;
}

Nota

As atribuiçoes booleanas no código acima são um encurtamento comum para programadores.Considere esse código contra a atribuição booleana encurtada acima:

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

Rode a cena novamente e confira se as animações estão corretas para cada direção. 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()
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 roteiro, depois de extends Area2d:

signal hit
// Don't forget to rebuild the project so the editor knows about the new signal.

[Signal]
public delegate void 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 depois em “Conectar” novamente na janela “Conectando Sinal”. 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.

Dica

Ao conectar um sinal, em vez de fazer com que o Godot crie uma função para você, você também pode fornecer o nome de uma função existente à qual deseja vincular o sinal.

Adicione este código à função:

func _on_Player_body_entered(body):
    hide()  # Player disappears after being hit.
    emit_signal("hit")
    $CollisionShape2D.set_deferred("disabled", true)
public void OnPlayerBodyEntered(PhysicsBody2D body)
{
    Hide(); // Player disappears after being hit.
    EmitSignal("Hit");
    GetNode<CollisionShape2D>("CollisionShape2D").SetDeferred("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 a 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 quando começarmos um novo jogo.

func start(pos):
    position = pos
    show()
    $CollisionShape2D.disabled = false
public void Start(Vector2 pos)
{
    Position = pos;
    Show();
    GetNode<CollisionShape2D>("CollisionShape2D").Disabled = false;
}

Cena do inimigo

Agora é hora de criarmos os inimigos que nosso jogador terá que se desviar. Seu comportamento não será tão complexo: inimigos irão nascer aleatoriamente nos cantos da tela e irão se mover em uma direção também aleatória, só que em linha reta, para, então desaparecerem quando saírem da tela.

Nós iremos construir isso em uma cena Turba, que poderemos então instanciar para criar uma quantidade de inimigos independentes no jogo.

Configuração de nós

Clique Cena -> Nova cena e criaremos a Turba.

A cena Turba utilizará os seguintes nós:

  • RigidBody2D (chamado Turba)
    • AnimatedSprite
    • CollisionShape2D
    • VisibilityNotifier2D (chamado Visibilidade)

Não se esqueça de configurar os filhos para que não possam ser selecionados, assim como você fez na cena Jogador.

Nas propriedades de RigidBody2D, configure Gravity Scale (escala da gravidade) para 0, para que os inimigos não caiam para a parte de baixo da tela. Além disso, dentro da seção PhysicsBody2D, clique na propriedade Mask (máscara) e desmarque a primeira caixa. Isso vai garantir que o inimigos não colidam uns com os outros.

../../_images/set_collision_mask.png

Configure o AnimatedSprite assim como você fez para o jogador. Desta vez, temos 3 animações: voar (fly), nadar (swim) e andar (walk). Defina a propriedade Playing (reproduzindo_ no Inspetor para “Ativo” e ajuste a configuração “Velocidade (FPS)” como mostrado abaixo. Vamos selecionar uma dessas animações aleatoriamente para que a turba tenha alguma variedade.

../../_images/mob_animations.gif

voar deve ser ajustado para 3 FPS (quadros por segundo), e nadar e andar ajustados para 4 FPS.

Como as imagens do jogador, essas imagens da turba precisam ser reduzidas. Defina a propriedade Scale (escala) do AnimatedSprite para (0.75, 0.75).

Como na cena Jogador, adicione um CapsuleShape2D para a colisão. Para alinhar a forma com a imagem, você precisará definir a propriedade Rotation Degrees (graus de rotação) como 90 na seção Node2D.

Script do inimigo

Adicione um roteiro ao nó Turba e adicione as seguintes variáveis membros:

extends RigidBody2D

export var min_speed = 150  # Minimum speed range.
export var max_speed = 250  # Maximum speed range.
var mob_types = ["walk", "swim", "fly"]
public class Mob : RigidBody2D
{
    // Don't forget to rebuild the project so the editor knows about the new export variables.

    [Export]
    public int MinSpeed = 150; // Minimum speed range.

    [Export]
    public int MaxSpeed = 250; // Maximum speed range.

    private String[] _mobTypes = {"walk", "swim", "fly"};
}

Quando gerarmos um mob, vamos escolher um valor aleatório entre min_speed (velocidade mínima) e max_speed (velocidade máxima) para definir a velocidade com que cada mob irá se mover (seria bem entediante se todos eles se movessem na mesma velocidade). Também temos um array contendo o nome das três animações, que usaremos para selecionar uma delas aleatóriamente. Certifique-se que eles estejam soletrados da mesma forma no script e no recurso SpriteFrames.

Agora, vamos ver o resto do roteiro. Em ``_ready () ``, escolhemos aleatoriamente um dos três tipos de animação:

func _ready():
    $AnimatedSprite.animation = mob_types[randi() % mob_types.size()]
// C# doesn't implement GDScript's random methods, so we use 'System.Random' as an alternative.
static private Random _random = new Random();

public override void _Ready()
{
    GetNode<AnimatedSprite>("AnimatedSprite").Animation = _mobTypes[_random.Next(0, _mobTypes.Length)];
}

Nota

Você deve usar randomize() se quiser que sua sequência de números “aleatórios” seja diferente toda vez que você executar a cena. Nós vamos usar randomize() em nossa cena Principal, então não precisaremos dessa função aqui. randi () %  n é a maneira padrão de obter um inteiro aleatório entre 0 e n-1.

A última parte é fazer a turba se excluir ao sair da tela. Conecte o sinal screen_exited() (saiu da tela) do nó Visibilidade e adicione este código:

func _on_Visibility_screen_exited():
    queue_free()
public void OnVisibilityScreenExited()
{
    QueueFree();
}

Isto completa a cena Turba.

Cena principal

Agora é hora de juntarmos tudo. Crie uma nova cena e adicione um chamado Principal. Clique no botão “Instanciar” e selecione seu Jogador.tscn já salvo.

../../_images/instance_scene.png

Nota

Veja Instâncias para aprender mais sobre instanciação.

Agora adicione os seguintes nós como filhos de Principal, e os nomeie como mostrado (os valores estão em segundos):

  • Timer (nomeado MobTimer) - para controlar a frequência com que a turba é gerada
  • Timer (nomeado ScoreTimer) - para incrementar a pontuação a cada segundo
  • Timer (nomeado StartTimer) - para dar um atraso antes de começar
  • Position2D (nomeado StartPosition) - para indicar a posição inicial do jogador

Defina a propriedade Wait Time (tempo de espera) para cada um dos nós Timer da seguinte forma:

  • MobTimer: 0.5
  • ScoreTimer: 1
  • StartTimer: 2

Além disso, configure a propriedade One Shot (“uma só vez”) de StartTimer para “Ativo” e Position do nó StartPosition para (240, 450).

Gerando monstros

O nó Principal ficará gerando novos inimigos, e nós queremos que eles apareçam em lugares aleatórios nos cantos da tela. Adicione um nó Path2D chamado CaminhoTurba como filho de Principal. Quando você selecionar Path2D, aparecerão alguns botões novos na parte superior do editor:

../../_images/path2d_buttons.png

Selecione o do meio (“Adicionar Ponto”) e desenhe o caminho clicando para adicionar os pontos nos cantos mostrados. Para que os pontos fiquem alinhados à grade, verifique se a opção “Encaixar na grade” está marcada. Essa opção pode ser encontrada pelo botão “Opções da Encaixe” à esquerda do botão “Trava”, indicada por uma série de 3 pontos verticais.

../../_images/draw_path2d.gif

Importante

Desenhe o caminho em sentido horário, ou sua turba vai surgir apontando para fora em vez de para dentro!

Depois de colocar o ponto 4 na imagem, clique no botão “Fechar Curva”, e sua curva estará completa.

Agora que o caminho está definido, adicione um nó PathFollow2D como filho de CaminhoTurba e dê o nome de LocalGeraçãoTurba. Esse nó vai rotacionar automaticamente e seguir o caminho conforme ele se move, para que possamos usá-lo para selecionar uma posição e uma direção aleatória ao longo do caminho.

Script principal

Adicione um roteiro a Principal. No começo do script, nós usamos export (PackedScene) para permitir-nos escolher a cena Inimigo que queremos instanciar.

extends Node

export (PackedScene) var Mob
var score

func _ready():
    randomize()
public class Main : Node
{
    // Don't forget to rebuild the project so the editor knows about the new export variable.

    [Export]
    public PackedScene Mob;

    private int _score;

    // We use 'System.Random' as an alternative to GDScript's random methods.
    private Random _random = new Random();

    public override void _Ready()
    {
    }

    // We'll use this later because C# doesn't support GDScript's randi().
    private float RandRange(float min, float max)
    {
        return (float)_random.NextDouble() * (max - min) + min;
    }
}

Arraste Turba.tscn do painel “Arquivos” e solte-o na propriedade Mob (Turba) na seção Script Variables (“Variáveis do Roteiro”) do nó Principal.

Em seguida, clique no Jogador e conecte o sinal hit. Queremos criar uma função chamada game_over, que lidará com tudo o que precisa acontecer quando o jogo acabar. Digite “game_over” na caixa “Método no Nó” na parte inferior da janela “Conectando Sinal”. Adicione o código a seguir, assim como uma função “new_game” (novo jogo) para definir tudo para um novo jogo:

func game_over():
    $ScoreTimer.stop()
    $MobTimer.stop()

func new_game():
    score = 0
    $Player.start($StartPosition.position)
    $StartTimer.start()
public void GameOver()
{
    GetNode<Timer>("MobTimer").Stop();
    GetNode<Timer>("ScoreTimer").Stop();
}

public void NewGame()
{
    _score = 0;

    var player = GetNode<Player>("Player");
    var startPosition = GetNode<Position2D>("StartPosition");
    player.Start(startPosition.Position);

    GetNode<Timer>("StartTimer").Start();
}

Agora, conecte o sinal timeout() (tempo esgotado) de cada um dos nós de Timer (StartTimer, ScoreTimer ,and MobTimer) para o script principal. StartTimer irá iniciar os outros dois temporizadores. ScoreTimer irá incrementar a pontuação em 1.

func _on_StartTimer_timeout():
    $MobTimer.start()
    $ScoreTimer.start()

func _on_ScoreTimer_timeout():
    score += 1
public void OnStartTimerTimeout()
{
    GetNode<Timer>("MobTimer").Start();
    GetNode<Timer>("ScoreTimer").Start();
}

public void OnScoreTimerTimeout()
{
    _score++;
}

Em _on_MobTimer_timeout(), vamos criar uma instância de inimigo, pegar um local de início aleatório ao longo do Path2D, e pôr o mob em movimento. O nó PathFollow2D irá rotacionar automaticamente à medida em que ele segue o caminho, então usaremos isso para escolher a direção do inimigo, bem como sua posição.

Note que uma nova instância deve ser adicionada à cena usando add_child() (adicionar filho).

func _on_MobTimer_timeout():
    # Choose a random location on Path2D.
    $MobPath/MobSpawnLocation.set_offset(randi())
    # Create a Mob instance and add it to the scene.
    var mob = Mob.instance()
    add_child(mob)
    # Set the mob's direction perpendicular to the path direction.
    var direction = $MobPath/MobSpawnLocation.rotation + PI / 2
    # Set the mob's position to a random location.
    mob.position = $MobPath/MobSpawnLocation.position
    # Add some randomness to the direction.
    direction += rand_range(-PI / 4, PI / 4)
    mob.rotation = direction
    # Set the velocity (speed & direction).
    mob.linear_velocity = Vector2(rand_range(mob.min_speed, mob.max_speed), 0)
    mob.linear_velocity = mob.linear_velocity.rotated(direction)
public void OnMobTimerTimeout()
{
    // Choose a random location on Path2D.
    var mobSpawnLocation = GetNode<PathFollow2D>("MobPath/MobSpawnLocation");
    mobSpawnLocation.SetOffset(_random.Next());

    // Create a Mob instance and add it to the scene.
    var mobInstance = (RigidBody2D)Mob.Instance();
    AddChild(mobInstance);

    // Set the mob's direction perpendicular to the path direction.
    float direction = mobSpawnLocation.Rotation + Mathf.Pi / 2;

    // Set the mob's position to a random location.
    mobInstance.Position = mobSpawnLocation.Position;

    // Add some randomness to the direction.
    direction += RandRange(-Mathf.Pi / 4, Mathf.Pi / 4);
    mobInstance.Rotation = direction;

    // Choose the velocity.
    mobInstance.SetLinearVelocity(new Vector2(RandRange(150f, 250f), 0).Rotated(direction));
}

Importante

Em funções que demandem ângulos, GDScript usa radianos, e não graus. Se você se sente mais confortável trabalhando com graus, precisará usar as funções deg2rad() (graus para radianos) e rad2deg() (radianos para graus) para fazer as conversões.

HUD

A peça final que nosso jogo precisa é uma interface com usuário: uma interface que mostra coisas como pontuação, uma mensagem de “fim de jogo” e um botão de reinício. Crie uma cena e adicione um nó CanvasLayer chamado HUD. “HUD” significa “heads-up display”, um mostrador informativo que aparece como uma camada de sobreposição à visão do jogo.

O nó CanvasLayer nos permite desenhar nossos elementos de interface em uma camada acima do resto do jogo, de forma que as informações que ela mostrar não fiquem cobertas por quaisquer elementos do jogo, como o jogador ou os inimigos.

O HUD exibirá as seguintes informações:

  • Pontuação, alterado por ScoreTimer.
  • Uma mensagem, como “Game Over” ou “Prepare-se!”
  • Um botão “Iniciar” para começar o jogo.

O nó básico para elementos de interface é Control. Para criar nossa interface, usaremos dois tipos de nós Control: Label e Button.

Crie os seguintes itens como filhos do nó HUD:

  • Label nomeado ScoreLabel.
  • Label nomeado MessageLabel.
  • Button nomeado StartButton.
  • Timer nomeado MessageTimer.

Clique no ScoreLabel e digite um número no campo Text no Inspetor. A fonte padrão para os nós Control é pequena e não escala bem. Há um arquivo de fonte incluído nos assets do jogo chamado “Xolonium-Regular.ttf”. Para usar esta fonte, faça o seguinte para cada um dos três nós Control:

  1. Na propriedade “Custom Fonts” (Fontes Personalizadas), escolha “Novo DynamicFont”
../../_images/custom_font1.png
  1. Clique na “DynamicFont” que você adicionou, e em “Font/Font Data” (dados da fonte), escolha “Carregar” e selecione o arquivo “Xolonium-Regular.ttf”. Você tem também que definir o tamanho Size da fonte. Uma configuração de 64 funciona bem.
../../_images/custom_font2.png

Nota

Âncoras e Margens: Nós Control têm uma posição e um tamanho, mas eles também têm âncoras e margens. Âncoras definem a origem – o ponto de referência para as bordas do nó. Margens atualizam automaticamente quando você move ou redimensiona um nó de controle. Elas representam a distância das bordas do nó Control até sua âncora. Veja Design de interfaces com os nós de Controle para mais detalhes.

Arrange the nodes as shown below. Click the “Layout” button to set a Control node’s layout:

../../_images/ui_anchor.png

Você pode arrastar os nós para colocá-los manualmente ou, para um posicionamento mais preciso, usar as seguintes configurações:

ScoreLabel

  • Texto: 0
  • Layout : “Top Wide”
  • Alinhamento : “Centralizado”

MessageLabel

  • Text: Desvie dos Bichos!
  • Layout : “Center” (Centro)
  • Alinhamento : “Centralizado”
  • Autowrap : “On”

StartButton

  • Text: Iniciar
  • Layout: “Center Bottom”
  • Margem (Margin) :
    • Top: -200
    • Bottom: -100

Agora, adicione este script ao HUD:

extends CanvasLayer

signal start_game
public class HUD : CanvasLayer
{
    // Don't forget to rebuild the project so the editor knows about the new signal.

    [Signal]
    public delegate void StartGame();
}

O sinal start_game diz ao nó Main que o botão foi pressionado.

func show_message(text):
    $MessageLabel.text = text
    $MessageLabel.show()
    $MessageTimer.start()
public void ShowMessage(string text)
{
    var messageLabel = GetNode<Label>("MessageLabel");
    messageLabel.Text = text;
    messageLabel.Show();

    GetNode<Timer>("MessageTimer").Start();
}

Esta função é chamada quando queremos mostrar uma mensagem temporariamente, como a “Prepare-se”. No MessageTimer, defina o Wait Time (Tempo de Espera) para 2 e configure a propriedade One Shot (Apenas uma Vez) como “Ativo”.

func show_game_over():
    show_message("Game Over")

    yield($MessageTimer, "timeout")

    $MessageLabel.text = "Dodge the\nCreeps!"
    $MessageLabel.show()

    yield(get_tree().create_timer(1), "timeout")

    $StartButton.show()
async public void ShowGameOver()
{
    ShowMessage("Game Over");

    var messageTimer = GetNode<Timer>("MessageTimer");
    await ToSignal(messageTimer, "timeout");

    var messageLabel = GetNode<Label>("MessageLabel");
    messageLabel.Text = "Dodge the\nCreeps!";
    messageLabel.Show();

    GetNode<Button>("StartButton").Show();
}

Esta função é chamada quando o jogador perde. Ela mostrará “Game Over” por 2 segundos e depois retornará à tela de título após uma breve pausa e mostrará o botão “Iniciar”.

Nota

Quando você precisa pausar por um breve tempo, uma alternativa para usar um nó Timer é usar a função create_timer() do SceneTree. Isso pode ser muito útil para atrasar, como no código acima, onde queremos esperar um pouco de tempo antes de mostrar o botão “Iniciar”.

func update_score(score):
    $ScoreLabel.text = str(score)
public void UpdateScore(int score)
{
    GetNode<Label>("ScoreLabel").Text = score.ToString();
}

Esta função é chamada por ``Main``sempre que a pontuação for alterada.

Conecte o sinal timeout() (“tempo esgotado”) de MessageTimer e o sinal pressed() (“pressionado”) de StartButton.

func _on_StartButton_pressed():
    $StartButton.hide()
    emit_signal("start_game")

func _on_MessageTimer_timeout():
    $MessageLabel.hide()
public void OnStartButtonPressed()
{
    GetNode<Button>("StartButton").Hide();
    EmitSignal("StartGame");
}

public void OnMessageTimerTimeout()
{
    GetNode<Label>("MessageLabel").Hide();
}

Conectando HUD a Principal

Agora que terminamos de criar a cena HUD, salve-a e volte para a Principal. Crie uma instância da cena HUD como fez com a cena Jogador, e coloque-a no final da árvore. A árvore completa deveria se parecer assim, então confira que não falta alguma coisa:

../../_images/completed_main_scene.png

Agora precisamos conectar a funcionalidade de HUD ao roteiro de Principal. Isso exige algumas adições à cena Principal:

Na aba Nó, conecte o sinal start_game de HUD à função new_game().

Em new_game(), atualize o mostrador de pontuação e mostre a mensagem “Prepare-se”:

$HUD.update_score(score)
$HUD.show_message("Get Ready")
var hud = GetNode<HUD>("HUD");
hud.UpdateScore(_score);
hud.ShowMessage("Get Ready!");

Em game_over(), precisamos chamar a correspondente função de HUD:

$HUD.show_game_over()
GetNode<HUD>("HUD").ShowGameOver();

Finalmente, adicione isto a _on_ScoreTimer_timeout() para manter o mostrador em sincronia com as mudanças de pontuação:

$HUD.update_score(score)
GetNode<HUD>("HUD").UpdateScore(_score);

Agora está tudo pronto para jogar! Clique no botão “Rodar o Projeto”. Será solicitada a seleção de uma cena principal, então escolha Principal.tscn.

Removendo antigas criaturas

Se você jogar até o “Game Over” e der start em um novo jogo as criaturas do jogo anterior ainda estarão na tela. Seria melhor se todas elas desaparecessem quando iniciar o novo jogo.

Nós vamos usar o sinal start_game que já está sendo emitido pelo nó HUD para remover as criaturas restantes. Nós não podemos usar o editor para conectar o sinal nos mobs da maneira que precisamos, porque não há nós Mob na arvore de cena Main até que o jogo seja executado.Em vez disso, vamos usar o código.

Começando por adicionar a nova função para Mob.gd. queue_free() que excluirá o nó atual no final do quadro atual.

func _on_start_game():
    queue_free()
public void OnStartGame()
{
    QueueFree();
}

Então, em Main.gd adicione uma nova linha dentro da função _on_MobTimer_timeout(), no final.

$HUD.connect("start_game", mob, "_on_start_game")
GetNode("HUD").Connect("StartGame", mobInstance, "OnStartGame");

Essa linha diz ao novo nó Mob ( referenciado pela variável mob) para responder a qualquer sinal start_game emitido pelo nó HUD rodando sua função _on_start_game().

Terminando

Agora completamos toda a funcionalidade do nosso jogo. Abaixo estão alguns passos restantes para adicionar um pouco mais de “sabor” para melhorar a experiência do jogo. Sinta-se livre para expandir a jogabilidade com suas próprias ideias.

Plano de fundo

O plano de fundo cinza padrão não é muito apelativo, então vamos mudar sua cor. Uma maneira de fazer isso é usar um nó ColorRect (“retângulo colorido”). Faça ele ser o primeiro nó de Principal para que ele seja desenhado por trás dos outros nós. ColorRect tem apenas uma propriedade: Color (“cor”). Escolha uma cor que goste e, com o mouse, altere as dimensões do ColorRect para que cubra a tela.

Você também pode adicionar uma imagem de plano de fundo, se tiver uma, usando um nó Sprite.

Efeitos sonoros

Som e música podem ser a forma mais efetiva de adicionar um atrativo à experiência de jogo. Na pasta de ativos do seu jogo, você tem dois arquivos de áudio: “House In a Forest Loop.ogg” para música de fundo e “gameover.wav” para quando o jogador perde.

Adicione dois nós AudioStreamPlayer como filhos de Principal. Nomeie um deles como Musica e o outro como SomDeMorte. Em cada um, clique na propriedade Stream (“fluxo”), selecione “Carregar”, e escolha o arquivo sonoro correspondente.

Para reproduzir a música, adicione $Musica.play()``na função ``new_game() e $Musica.stop() na função game_over().

Por fim, adicione $SomDeMorte.play() na função game_over().

Atalho do teclado

Como o jogo é jogado com controles de teclado, seria conveniente se pudéssemos iniciar o jogo pressionando uma tecla no teclado. Uma maneira de fazer isso é usando a propriedade “Atalho” do nó Button.

Na cena HUD, selecione o StartButton e encontre sua propriedade Atalho no Inspetor. Selecione “Novo atalho” e clique no item “Atalho”. Uma segunda propriedade Atalho aparecerá. Selecione “New InputEventAction” e clique no novo “InputEvent”. Finalmente, na propriedade Action, digite o nome “ui_select”. Este é o evento de entrada padrão associado à barra de espaço.

../../_images/start_button_shortcut.png

Agora, quando o botão iniciar aparecer, você pode clicar nele ou pressionar a barra de espaço para iniciar o jogo.

Arquivos do projeto

Você pode encontrar uma versão finalizada deste projeto nestes locais: