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

Why 2D? 3D games are much more complex than 2D ones. You should stick to 2D until you have a good understanding of the game development process and how to use Godot.

Configuração do projeto

Launch Godot and create a new project. Then, download dodge_assets.zip. This contains the images and sounds you’ll be using to make the game. Unzip these files in your project folder.

Nota

For this tutorial, we will assume you are familiar with the Godot editor. If you haven’t read Cenas e nós, do so now for an explanation of setting up a project and using the editor.

This game is designed for portrait mode, so we need to adjust the size of the game window. Click on Project -> Project Settings -> Display -> Window and set “Width” to 480 and “Height” to 720.

Also in this section, under the “Stretch” options, set Mode to “2d” and Aspect to “keep”. This ensures that the game scales consistently on different sized screens.

Organizando o projeto

In this project, we will make 3 independent scenes: Player, Mob, and HUD, which we will combine into the game’s Main scene. In a larger project, it might be useful to create folders to hold the various scenes and their scripts, but for this relatively small game, you can save your scenes and scripts in the project’s root folder, identified by res://. You can see your project folders in the FileSystem Dock in the lower left corner:

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

With Area2D we can detect objects that overlap or run into the player. Change the node’s name to Player by double-clicking on it. Now that we’ve set the scene’s root node, we can add additional nodes to give it more functionality.

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 :kbd: Ctrl+S no Windows/Linux ou :kbd: 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

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”. Click again to open the “SpriteFrames” panel:

../../_images/spriteframes_panel.png

On the left is a list of animations. Click the “default” one and rename it to “walk”. Then click the “New Animation” button to create a second animation named “up”. Find the player images in the “FileSystem” tab - they’re in the art folder you unzipped earlier. Drag the two images for each animation, named playerGrey_up[1/2] and playerGrey_walk[1/2], into the “Animation Frames” side of the panel for the corresponding animation:

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

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

You can detect whether a key is pressed using Input.is_action_pressed(), which returns true if it’s pressed or false if it isn’t.

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

We start by setting the velocity to (0, 0) - by default, the player should not be moving. Then we check each input and add/subtract from the velocity to obtain a total direction. For example, if you hold right and down at the same time, the resulting velocity vector will be (1, 1). In this case, since we’re adding a horizontal and a vertical movement, the player would move faster diagonally than if it just moved horizontally.

We can prevent that if we normalize the velocity, which means we set its length to 1, then multiply by the desired speed. This means no more fast diagonal movement.

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.

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

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 (make sure it’s not indented under the 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

Attempt to call function 'play' in base 'null instance' on a null instance

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

The boolean assignments in the code above are a common shorthand for programmers. Since we’re doing a comparison test (boolean) and also assigning a boolean value, we can do both at the same time. Consider this code versus the one-line boolean assignment above:

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

When you’re sure the movement is working correctly, add this line to _ready(), so the player will be hidden when the game starts:

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

Notice our custom “hit” signal is there as well! Since our enemies are going to be RigidBody2D nodes, we want the body_entered(body: Node) signal. This signal will be emitted when a body contacts the player. Click “Connect..” and the “Connect a Signal” window appears. We don’t need to change any of these settings so click “Connect” again. Godot will automatically create a function in your player’s script.

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

Disabling the area’s collision shape can cause an error if it happens in the middle of the engine’s collision processing. Using set_deferred() tells Godot to wait to disable the shape until it’s safe to do so.

The last piece is to add a function we can call to reset the player when starting a new game.

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

Now it’s time to make the enemies our player will have to dodge. Their behavior will not be very complex: mobs will spawn randomly at the edges of the screen, choose a random direction, and move in a straight line.

We’ll create a Mob scene, which we can then instance to create any number of independent mobs in the game.

Nota

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

Configuração de nós

Click Scene -> New Scene and add the following nodes:

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

Set up the AnimatedSprite like you did for the player. This time, we have 3 animations: fly, swim, and walk. There are two images for each animation in the art folder.

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

../../_images/mob_animations.gif

Set the Playing property in the Inspector to “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).

As in the Player scene, add a CapsuleShape2D for the collision. To align the shape with the image, you’ll need to set the Rotation Degrees property to 90 (under “Transform” in the Inspector).

Save the scene.

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.

}

When we spawn a mob, we’ll pick a random value between min_speed and max_speed for how fast each mob will move (it would be boring if they were all moving at the same speed).

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()
{
    GetNode<AnimatedSprite>("AnimatedSprite").Animation = _mobTypes[_random.Next(0, _mobTypes.Length)];
}

First, we get the list of animation names from the AnimatedSprite’s frames property. This returns an Array containing all three animation names: ["walk", "swim", "fly"].

We then need to pick a random number between 0 and 2 to select one of these names from the list (array indices start at 0). randi() % n selects a random integer between 0 and n-1.

Nota

You must use randomize() if you want your sequence of “random” numbers to be different every time you run the scene. We’re going to use randomize() in our Main scene, so we won’t need it here.

The last piece is to make the mobs delete themselves when they leave the screen. Connect the screen_exited() signal of the VisibilityNotifier2D node and add this code:

func _on_VisibilityNotifier2D_screen_exited():
    queue_free()
public void OnVisibilityNotifier2DScreenExited()
{
    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

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 à esquerda do botão “Travar”, parecido como um imã e ao lado de uma série de 3 pontos verticais.

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

Your scene should look like this:

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

Click the Main node and you will see the Mob property in the Inspector under “Script Variables”.

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

  • Drag Mob.tscn from the “FileSystem” panel and drop it in the Mob property .
  • Click the down arrow next to “[empty]” and choose “Load”. Select Mob.tscn.

Next, click on the Player and connect the hit signal. We want to make a new function named game_over, which will handle what needs to happen when a game ends. Type “game_over” in the “Receiver Method” box at the bottom of the “Connect a Signal” window and click “Connect”. Add the following code to the new function, as well as a new_game function that will set everything up for a new game:

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

Now connect the timeout() signal of each of the Timer nodes (StartTimer, ScoreTimer , and MobTimer) to the main script. StartTimer will start the other two timers. ScoreTimer will increment the score by 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.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

Why PI? In functions requiring angles, GDScript uses radians, not degrees. If you’re more comfortable working with degrees, you’ll need to use the deg2rad() and rad2deg() functions to convert between the two.

Testing the scene

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

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

Let’s also assign Main as our “Main Scene” - the one that runs automatically when the game launches. Press the “Play” button and select Main.tscn when prompted.

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.

The HUD needs to display the following information:

  • 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:

Click on the ScoreLabel and type a number into the Text field in the Inspector. The default font for Control nodes is small and doesn’t scale well. There is a font file included in the game assets called “Xolonium-Regular.ttf”. To use this font, do the following:

  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

Once you’ve done this on the ScoreLabel, you can click the down arrow next to the DynamicFont property and choose “Copy”, then “Paste” it in the same place on the other two Control nodes.

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”

Message

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

StartButton

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

On the MessageTimer, set the Wait Time to 2 and set the One Shot property to “On”.

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

This function is called when we want to display a message temporarily, such as “Get Ready”.

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();

    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

When you need to pause for a brief time, an alternative to using a Timer node is to use the SceneTree’s create_timer() function. This can be very useful to add delays such as in the above code, where we want to wait some time before showing the “Start” button.

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.

Connect the timeout() signal of MessageTimer and the pressed() signal of StartButton and add the following code to the new functions:

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

Now that we’re done creating the HUD scene, go back to Main. Instance the HUD scene in Main like you did the Player scene. The scene tree should look like this, so make sure you didn’t miss anything:

../../_images/completed_main_scene.png

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

In the Node tab, connect the HUD’s start_game signal to the new_game() function of the Main node by typing “new_game” in the “Receiver Method” in the “Connect a Signal” window. Verify that the green connection icon now appears next to func new_game() in the 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

If you play until “Game Over” and then start a new game right away, the creeps from the previous game may still be on the screen. It would be better if they all disappeared at the start of a new game. We just need a way to tell all the mobs to remove themselves. We can do this with the “group” feature.

In the Mob scene, select the root node and click the “Node” tab next to the Inspector (the same place where you find the node’s signals). Next to “Signals”, click “Groups” and you can type a new group name and click “Add”.

../../_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");

The call_group() function calls the named function on every node in a group - in this case we are telling every mob to delete itself.

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

The default gray background is not very appealing, so let’s change its color. One way to do this is to use a ColorRect node. Make it the first node under Main so that it will be drawn behind the other nodes. ColorRect only has one property: Color. Choose a color you like and select “Layout” -> “Full Rect” so that it covers the screen.

You could also add a background image, if you have one, by using a TextureRect node instead.

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

Keyboard shortcut

Since the game is played with keyboard controls, it would be convenient if we could also start the game by pressing a key on the keyboard. We can do this with the “Shortcut” property of the Button node.

In the HUD scene, select the StartButton and find its Shortcut property in the Inspector. Select “New Shortcut” and click on the “Shortcut” item. A second Shortcut property will appear. Select “New InputEventAction” and click the new “InputEventAction”. Finally, in the Action property, type the name ui_select. This is the default input event associated with the spacebar.

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