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 e de como deve-se usar o Godot.

Configuração do projeto

Execute o Godot e crie um novo projeto. Então, baixe dodge_assets.zip. Isso contém 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 sobre a configuração de um projeto e 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 -> Exibição -> Janela e configure "Largura" para 480 e "Altura" para 720.

Também nesta seção, nas opções "Alongamento(Stretch)", defina `` Modo(Mode)`` para "2d" e `` Aspecto(Aspect)`` para "manter(keep)". Isso garante que o jogo seja escalado consistentemente em telas de tamanhos diferentes.

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 scripts, 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

The first scene will define the Player object. One of the benefits of creating a separate Player scene is that we can test it separately, even before we've created other parts of the game.

Estrutura de nós

Para iniciarmos, precisamos escolher o nó raiz para o objeto jogador. Como regra geral, o nó raiz da cena deve refletir à funcionalidade desejado do objeto - o que o objeto é. Clique no botão "Outros Nós" e adicione o nó Area2D à cena.

../../_images/add_node.png

Godot vai mostrar um ícone de aviso próximo do nó na árvore de cenas. Você pode ignorar isso por enquanto. Nós vamos falar disso mais tarde.

Com Area2D, nós podemos detectar objetos que se sobreponham ou vão de encontro ao jogador. Mude seu nome para Jogador com um clique duplo no nome do nó. Já que nós configuramos o nó raiz, nós agora podemos inserir nós adicionais para adicionar mais 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 Cmd + S no macOS.

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 Guia de Estilo GDScript).
  • ** 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

Clique no nó Jogador e adicione um nó AnimatedSprite (sprite animado) como filho. O AnimatedSprite irá lidar com a aparência e as animações do nosso jogador. Note que existe um símbolo de alerta ao lado do nó. Um AnimatedSprite exige um recurso do tipo SpriteFrames (quadros de sprite), que é uma lista das animações que ele pode mostrar. Para criar um, encontre a propriedade Frames no Inspetor e clique em "[empty]" -> "Novo SpriteFrames". Clique novamente para abrir o painel de "SpriteFrames":

../../_images/spriteframes_panel.png

À esquerda está uma lista de animações. Clique em "default" e a renomeie para "caminhar". Então, clique no botão "Nova Animação" para criar uma segunda animação chamada "cima". Encontre as imagens do jogador na aba "Sistema de Arquivos" - eles estão na pasta art que você descompactou mais cedo. Arraste as duas imagens de cada animação, chamadas playerGrey_up[1/2] e playerGrey_walk[1/2] para dentro do "Animation Frames" ao lado do painel correspondende de cada animação:

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

Certifique-se de salvar a cena novamente após as alterações.

Movendo o Jogador

Agora precisamos adicionar algumas funcionalidades que não conseguimos em um nó embutido, então adicionaremos um script. 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 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

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, 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 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().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.

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

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

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

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.

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)
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 confira se consegue mover o jogador pela tela em todas as direções.

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

Se você vir no painel "Depurador" um erro referente a "null instance" (instância nula), provavelmente significa que você digitou o nome do nó errado. Letras 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 "andar", 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
if (velocity.x != 0)
{
    animatedSprite.Animation = "walk";
    animatedSprite.FlipV = false;
    // See the note below about boolean assignment
    animatedSprite.FlipH = velocity.x < 0;
}
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. 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
if (velocity.x < 0)
{
    animatedSprite.FlipH = true;
}
else
{
    animatedSprite.FlipH = 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()
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
// 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 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")
    $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 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
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 ao sair da tela.

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

Nota

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

Configuração de nós

Clique Cena -> Nova Cena e adcione os seguintes nós:

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.

Ajuste o "Speed (FPS)" para 3 em todas as animações.

../../_images/mob_animations.gif

Defina a propriedade Playing no Inspector para "On".

Vamos selecionar uma das animações aleatoriamente para que os inimigos tenham alguma variedade.

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 do 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.

Salvar Cena.

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

}

Quando gerarmos um inimigo, 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 inimigo irá se mover (seria bem entediante se todos eles se movessem na mesma velocidade).

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

func _ready():
    var mob_types = $AnimatedSprite.frames.get_animation_names()
    $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()
{
    var animSprite = GetNode<AnimatedSprite>("AnimatedSprite");
    var mobTypes = animSprite.Frames.GetAnimationNames();
    animSprite.Animation = mobTypes[_random.Next(0, mobTypes.Length)];
}

Primeiro nós pegamos a lista dos nomes das animações da propriedade frames do AnimatedSprite. Que retorna um vetor contendo os nomes das três animações: ["walk", "swim", "fly"].

Nós precisamos, então, de um número randômico entre 0 e 2 para selecionar um dos nomes da lista(os índices de um vetor começam do 0 ). randi() % n``seleciona randomicamente um inteiro entre ``0 e n-1.

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 os inimigo se autodestruirem ao sair da tela. Conecte o sinal screen_exited() (saiu da tela) do nó VisibilityNotifier2D e adicione este código:

func _on_VisibilityNotifier2D_screen_exited():
    queue_free()
public void OnVisibilityNotifier2DScreenExited()
{
    QueueFree();
}

Isto completa a cena dos Inimigos.

Cena principal

Now it's time to bring it all together. Create a new scene and add a Node named Main. Ensure you create a Node, not a Node2D. Click the "Instance" button and select your saved Player.tscn.

../../_images/instance_scene.png

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

Select the middle one ("Add Point") and draw the path by clicking to add the points at the corners shown. To have the points snap to the grid, make sure "Use Grid Snap" and "Use Snap" are both selected. These options can be found to the left of the "Lock" button, appearing as a magnet next to some dots and intersecting lines, respectively.

../../_images/grid_snap_button.png

Importante

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

../../_images/draw_path2d.gif

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.

Sua cena deve se parecer com isso:

../../_images/main_scene_nodes.png

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;
    }
}

Clique no nó Principal``e você verá a propriedade ``Mob no inspetor, abaixo das Variáveis do Script.

Você pode atribuir o valor dessa propriedade de duas formas:

  • Arraste Mob.tscn do painel do "Sistema de Arquivos" e solte-o na propriedade Mob.
  • Clique na seta para baixo ao lado de "[vazio]" e escolha "Carregar". Selecione Mob.tscn.

Em seguida, selecione o nó do Player no painel Cena, e acesse o nó na barra lateral. Certifique-se de selecionar a aba Sinais no Painel de nós.

Você deve ver uma lista de sinais para o nó Player. Em seguida, encontre e dê dois cliques no sinal hit da lista (ou clique com o botão direito e selecione "Conectar..."). Isso abrirá a caixa de diálogo de conexão de sinal. Queremos criar uma função chamada game_over, que lidará com tudo o que precisa acontecer quando um jogo acaba. Digite "game_over" na caixa "Método no Nó" na parte inferior da janela de conexão de sinal e clique em "Conectar". Adicione o código a seguir à nova função, 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() de cada um dos nós de Timer (StartTimer, ScoreTimer , e``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.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.Offset = _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.LinearVelocity = new Vector2(RandRange(150f, 250f), 0).Rotated(direction);
}

Importante

Por que PI? 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.

Testando a cena

Vamos testar a cena para garantir que tudo esta funcionando. Adicione isso ao _ready():

func _ready():
    randomize()
    new_game()
    public override void _Ready()
    {
        NewGame();
    }
}

Vamos também atribuir Main como nossa "Cena Principal" - aquela que é executada automaticamente quando o jogo é iniciado. Pressione o botão "Reproduzir" e selecione `` Main.tscn`` quando solicitado.

Você deve ser capaz de mover o jogador, ver os inimigos nascendo, e ver o jogador desaparecer quando atingido por um inimigo.

Quando tiver certeza que tudo esta funcionando, remova a chamada de new_game() em _ready().

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

Depois de fazer isso no `` ScoreLabel``, você pode clicar na seta para baixo ao lado da propriedade DynamicFont e escolher "Copiar", depois "Colar" no mesmo local que os outros dois nós de Controle.

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.

Organize os nós conforme indicado abaixo. Clique no botão "Disposição (Layout)" para definir a disposição de um nó de controle:

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

  • Layout : "Top Wide"
  • Texto: 0
  • Alinhamento : "Centralizado"

MessageLabel

  • Layout : "Center" (Centro)
  • Text: Desvie dos Bichos!
  • Alinhamento : "Centralizado"
  • Autowrap : "Ativo"

StartButton

  • Text: Iniciar
  • Layout: "Center Bottom"
  • Margem (Margin) :
    • Top: -200
    • Inferior: -100

No MessageTimer, defina o Wait Time (Tempo de Espera) para 2 e configure a propriedade One Shot (Apenas uma Vez) como "Ativo".

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):
    $Message.text = text
    $Message.show()
    $MessageTimer.start()
public void ShowMessage(string text)
{
    var message = GetNode<Label>("Message");
    message.Text = text;
    message.Show();

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

Esta função é chamada quando queremos mostrar uma mensagem temporariamente, como a "Prepare-se".

func show_game_over():
    show_message("Game Over")
    # Wait until the MessageTimer has counted down.
    yield($MessageTimer, "timeout")

    $Message.text = "Dodge the\nCreeps!"
    $Message.show()
    # Make a one-shot timer and wait for it to finish.
    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 message = GetNode<Label>("Message");
    message.Text = "Dodge the\nCreeps!";
    message.Show();

    await ToSignal(GetTree().CreateTimer(1), "timeout");
    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 ao do 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()``do ``MessageTimer e o sinal pressed() do StartButton e adicione o seguinte código às novas funções:

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

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

public void OnMessageTimerTimeout()
{
    GetNode<Label>("Message").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 se 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 guia Nó, conecte o sinal `` start_game`` do HUD à função `` new_game () `` do nó Principal, digitando "new_game" no "Receiver Method" na janela "Connect a Signal". Verifique se o ícone de conexão verde agora aparece ao lado de `` func new_game () `` no script.

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 iniciar um novo jogo, as criaturas do jogo anterior ainda poderão estar na tela. Seria melhor se todas elas desaparecessem no iníco de cada partida. Nós só precisamos de um jeito de falar para todos os inimigos se auto-destruirem. Nós podemos fazer isso com a funcionalidade "group"(grupo).

Na cena do Inimigo, selecione o nó raiz e clique na aba "Nó" próxima ao Inspetor(No mesmo lugar onde vocÊ encontra os sinais do nó). Próximo a "Sinais", clique "Grupos", você pode digitar o nome do novo grupo e clicar em "Adicionar".

../../_images/group_tab.png

Agora todos os inimigos estarão no grupo "inimigos". Podemos então adicionar a seguinte linha à função game_over() em Main:

get_tree().call_group("mobs", "queue_free")
GetTree().CallGroup("mobs", "queue_free");

A função ``call_group()``chama a função passada como parâmetro em cada nó do grupo - neste caso nós estamos falando para cada inimigo se auto-destruir.

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 padrão cor cinza 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 selecione, em "Layout", "Full Rect" para cobrir toda a tela.

Você também pode adicionar uma imagem de plano de fundo, se tiver uma, ao usar um nó TextureRect.

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 de teclado

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

Na cena HUD, selecione o StartButton e encontre a propriedade Atalho no Inspetor. Selecione "Novo atalho" e clique no item "Atalho". Uma segunda propriedade Atalho aparecerá. Selecione "New InputEventAction" e clique no novo "InputEventAction". 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 :kbd: Barra de Espaço para iniciar o jogo.

Arquivos do projeto

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