Controlar a interface do jogo com código

Introdução

Neste tutorial, você conectará um personagem a uma barra de vida e animará a perda de pontos de vida.

../../_images/lifebar_tutorial_final_result.gif

Aqui está o que você vai criar: a barra e o contador que animara quando o personagem recebe um hit. Eles desaparecem quando morre.

Você vai aprender:

  • Como conectar um caractere a uma interface gráfica com sinais
  • Como controlar uma GUI com GDscript
  • Como animar uma barra de vida com o nó: ref: Tween <class_Tween>

Se você quiser aprender como configurar a interface, confira os tutoriais passo a passo da interface do usuário:

Quando você codifica um jogo, você quer construir a jogabilidade central primeiro: a mecânica principal, a entrada do jogador, as condições de vitória e derrota. A interface do usuário vem um pouco depois. Você quer manter todos os elementos que compõem seu projeto separados, se possível. Cada personagem deve estar em sua própria cena, com seus próprios scripts, assim como os elementos da interface do usuário. Isso evita bugs, mantém seu projeto gerenciável e permite que diferentes membros da equipe trabalhem em diferentes partes do jogo.

Quando a jogabilidade central e a interface do usuário estiverem prontas, você precisará conectá-las de alguma forma. Em nosso exemplo, temos o inimigo que ataca o jogador em intervalos de tempo constantes. Queremos que a barra de vida seja atualizada quando o jogador sofrer dano.

Para fazer isso, usaremos sinais.

Nota

Sinais são a versão Godot do padrão Observador. Eles nos permitem enviar mensagens. Outros nós podem se conectar ao objeto que emite o sinal e receber as informações. É uma ferramenta poderosa que usamos muito para Interface de Usuário e sistemas de conquistas. Contudo, você não quer usá-los em todos os lugares. Conectar dois nós adiciona algum acoplamento entre eles. Quando há muitas conexões, elas se tornam difíceis de gerenciar. Para mais informações, consulte o tutorial em vídeo sobre sinais de GDquest.

Baixe e explore o projeto inicial

Baixe o projeto Godot: ui_code_life_bar.zip. Ele contém todos os recursos e scripts de que você precisa para começar. Extraia o arquivo .zip para obter duas pastas: start e` end`.

Carregue o projeto `` start`` no Godot. No painel Arquivos, clique duas vezes em LevelMockup.tscn para abrí-la. É uma maquete de um jogo de RPG onde dois personagens se enfrentam. O inimigo rosa ataca e danifica o quadrado verde em intervalos de tempo regulares, até a sua morte. Sinta-se livre para experimentar o jogo: a mecânica básica de combate já funciona. Mas como o personagem não está conectado à barra de vida, o `` GUI`` não faz nada.

Nota

Isso é típico de como você codificava um jogo: você primeiro implementa a jogabilidade central, controla a morte do jogador e somente então você adicionará a interface. Isso porque a interface do usuário ouve o que está acontecendo no jogo. Portanto, não pode funcionar se outros sistemas ainda não estiverem funcionando. Se você projetar a interface do usuário antes de prototipar e testar a jogabilidade, é provável que ela não funcione bem e você terá que recriá-la do zero.

A cena contém uma sprite de fundo, uma GUI e dois caracteres.

../../_images/lifebar_tutorial_life_bar_step_tut_LevelMockup_scene_tree.png

A árvore de cena, com a cena GUI configurada para exibir seus filhos

A cena GUI encapsula toda a interface do usuário do jogo. Ela vem com um script básico, onde obtemos o caminho para os nós que existem dentro da cena:

onready var number_label = $Bars/LifeBar/Count/Background/Number
onready var bar = $Bars/LifeBar/TextureProgress
onready var tween = $Tween
public class Gui : MarginContainer
{
    private Tween _tween;
    private Label _numberLabel;
    private TextureProgress _bar;

    public override void _Ready()
    {
        // C# doesn't have an onready feature, this works just the same.
        _bar = (TextureProgress) GetNode("Bars/LifeBar/TextureProgress");
        _tween = (Tween) GetNode("Tween");
        _numberLabel = (Label) GetNode("Bars/LifeBar/Count/Background/Number");
    }
}
  • `` number_label`` exibe uma contagem de vida como um número. É um nó `` Label``
  • `` bar`` é a própria barra de vida. É um nó `` TextureProgress``
  • `` tween`` é um nó de estilo de componente que pode animar e controlar qualquer valor ou método de qualquer outro nó

Nota

The project uses a simple organization that works for game jams and tiny games.

Na raiz do projeto, na pasta res: //, você encontrará o LevelMockup. Essa é a cena principal do jogo e a que vamos trabalhar. Todos os componentes que compõem o jogo estão na pasta scenes /. A pasta assets / contém os sprites do jogo e a fonte do contador de HP. Na pasta scripts /, você encontrará o inimigo, o player e os scripts do controlador GUI.

Clique no ícone de edição de cena à direita do nó na árvore de cena para abrir a cena no editor. Você verá o LifeBar e o EnergyBar como subcenas.

../../_images/lifebar_tutorial_Player_with_editable_children_on.png

A árvore de cena, com a cena Player configurada para exibir seus filhos

Configurar a barra de vida com o máximo de segurança do jogador

Temos que dizer à GUI de alguma forma qual é a saúde atual do player, atualizar a textura da barra de vida e exibir a integridade restante no contador HP no canto superior esquerdo da tela. Para fazer isso, enviamos a saúde do jogador para a GUI toda vez que receberem dano. A GUI irá então atualizar os nós `` Lifebar`` e `` Number`` com este valor.

Nós poderíamos parar aqui para exibir o número, mas precisamos inicializar o `` max_value`` para que ele seja atualizado nas proporções corretas. O primeiro passo é dizer ao `` GUI`` qual é o `` max_health`` do personagem verde.

Dica

A barra, TextureProgress, tem um` max_value` de 100 por padrão. Se você não precisa exibir a saúde do personagem com um número, não é necessário alterar a propriedade max_value. Você envia uma porcentagem do Player para o` GUI` em vez disso:health / max_health * 100.

../../_images/lifebar_tutorial_TextureProgress_default_max_value.png

Clique no ícone de roteiro à direita da `` GUI`` no painel Cena para abrir seu roteiro. Na função _ready, vamos armazenar max_health do Player em uma nova variável e usá-la para definir o max_value do bar:

func _ready():
    var player_max_health = $"../Characters/Player".max_health
    bar.max_value = player_max_health
public override void _Ready()
{
    // Add this below _bar, _tween, and _numberLabel.
    var player = (Player) GetNode("../Characters/Player");
    _bar.MaxValue = player.MaxHealth;
}

Vamos acabar com isso. `` $ "../ Characters / Player" `` é uma abreviação que sobe um nó na árvore de cena, e recupera o nó `` Characters / Player`` de lá. Isso nos dá acesso ao nó. A segunda parte da instrução, `` .max_health``, acessa o `` max_health`` no nó do Player.

A segunda linha atribui este valor a `` bar.max_value``. Você pode combinar as duas linhas em uma, mas precisaremos usar `` player_max_health`` novamente mais tarde no tutorial.

`` Player.gd`` define o `` health`` para `` max_health`` no início do jogo, então podemos trabalhar com isso. Por que ainda usamos `` max_health``? Existem dois motivos:

Nós não temos a garantia de que `` health`` será sempre igual a `` max_health``: uma versão futura do jogo pode carregar um nível em que o jogador já perdeu alguma vida.

Nota

Quando você abre uma cena no jogo, Godot cria os nós um por um, seguindo a ordem no seu painel Cena, de cima para baixo. GUI e Player não fazem parte do mesmo ramo de nó. Para ter certeza de que ambos existem quando nós os acessamos, temos que usar a função _ready. Godot chama _ready logo após carregar todos os nós, antes do jogo começar. É a função perfeita para configurar tudo e preparar a sessão do jogo. Saiba mais sobre _ready: Roteirizando (continuação)

Atualize a vida com um sinal quando o jogador leva um golpe

Nossa GUI está pronta para receber as atualizações do valor `` health`` do `` Player``. Para conseguir isso, vamos usar ** sinais **.

Nota

Existem muitos sinais internos úteis como enter_tree e` exit_tree`, que todos os nós emitem quando são respectivamente criados e destruídos. Você também pode criar seus próprios usando a palavra-chave signal. No nó Player, você encontrará dois sinais que criamos para você:` died` e health_changed.

Por que nós não pegamos diretamente o nó Player na função _process e olhamos o valor de saúde? Acessar nós dessa maneira cria um acoplamento firme entre eles. Se você faz isso com moderação, pode funcionar. Conforme seu jogo cresce, você pode ter muito mais conexões. Se você pegar nós dessa maneira, as coisas ficam complexas rapidamente. Não só isso: você precisa ouvir a mudança de estado constantemente na função _process. Essa verificação acontece 60 vezes por segundo e você provavelmente quebrará o jogo por causa da ordem na qual o código é executado.

Em um determinado quadro, você pode olhar para a propriedade de outro nó antes de ele ser atualizado: você obtém um valor do último quadro. Isso leva a erros obscuros que são difíceis de corrigir. Por outro lado, um sinal é emitido logo após a ocorrência de uma mudança. Isso garante que você está recebendo uma informação recente. E você irá atualizar o estado do seu nó conectado logo após a mudança acontecer.

Nota

The Observer pattern, that signals derive from, still adds a bit of coupling between node branches. But it's generally lighter and more secure than accessing nodes directly to communicate between two separate classes. It can be okay for a parent node to get values from its children. But you'll want to favor signals if you're working with two separate branches. Read Game Programming Patterns for more information on the Observer pattern. The full book is available online for free.

Com isso em mente, vamos conectar a GUI ao Player. Clique no nó Player no painel Cena para selecioná-lo. Vá até o Inspetor e clique na guia Nó. Este é o lugar para conectar nós para escutar àquele que você selecionou.

A primeira seção lista os sinais personalizados definidos em `` Player.gd``:

  • died ("morreu") é emitido quando o personagem morre. Vamos usá-lo em um momento para esconder a interface do usuário.
  • `` health_changed`` é emitido quando o personagem é atingido.
../../_images/lifebar_tutorial_health_changed_signal.png

Estamos nos conectando ao sinal health_changed

Selecione health_changed e clique no botão Conectar no canto inferior direito para abrir a janela Conectar Sinal. No lado esquerdo, você pode escolher o nó que escutará este sinal. Selecione o nó GUI. O lado direito da tela permite empacotar valores opcionais com o sinal. Nós já cuidamos disso em Player.gd. Em geral, eu recomendo não adicionar muitos argumentos usando esta janela, já que eles são menos convenientes do que fazer a partir do código.

../../_images/lifebar_tutorial_connect_signal_window_health_changed.png

A janela Conectar Sinal com o nó GUI selecionado

Dica

Opcionalmente você pode conectar nós do código. Mas fazê-lo do editor tem duas vantagens:

  1. Godot pode escrever novas funções de retorno de chamada para você no script conectado
  2. Um ícone de emissor aparece próximo ao nó que emite o sinal no painel Cena

Na parte inferior da janela, você encontrará o caminho para o nó selecionado. Estamos interessados na segunda linha chamada "Método no Nó". Este é o método no nó `` GUI`` que é chamado quando o sinal é emitido. Este método recebe os valores enviados com o sinal e permite processá-los. Se você olhar para a direita, há um botão de opção "Make Function" que está ativado por padrão. Clique no botão de conexão na parte inferior da janela. Godot cria o método dentro do nó `` GUI``. O editor de script é aberto com o cursor dentro de uma nova função `` _on_player_health_changed``.

Nota

Quando você conecta nós no editor, Godot gera um nome de método com o seguinte padrão: `` _on_EmitterName_signal_name``. Se você já escreveu o método, a opção "Make Function" irá mantê-lo. Você pode substituir o nome com o que quiser.

../../_images/lifebar_tutorial_godot_generates_signal_callback.png

Godot escreve o método de callback para você e leva você até ele

Dentro dos parênteses após o nome da função, adicione um argumento player_health. Quando o jogador emite o sinal health_changed, ele enviará sua health atual juntamente. Seu código deve parecer assim:

func _on_Player_health_changed(player_health):
    pass
public void OnPlayerHealthChanged(int playerHealth)
{
}

Nota

A engine não converte PascalCase para snake_case, por exemplos em C# estaremos usando PascalCase para nomes de métodos e camelCase para parâmetros de métodos que seguem as `convenções oficiais de nomenclatura em C#. <https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/capitalization-conventions> `_

../../_images/lifebar_tutorial_player_gd_emits_health_changed_code.png

No Player.gd, quando o Player emite o sinal health _changed, ele também envia seu valor de integridade

Dentro de `` _on_Player_health_changed`` vamos chamar uma segunda função chamada `` update_health`` e passar a variável `` player_health``.

Nota

Poderíamos atualizar diretamente o valor de saúde em LifeBar e` Number`. Existem duas razões para usar este método:

  1. O nome deixa claro para os nossos futuros eus e companheiros de equipe que, quando o jogador levou dano, nós atualizamos a contagem de saúde na GUI
  2. Nós vamos reutilizar este método um pouco mais tarde

Crie um novo método `` update_health`` abaixo de `` _on_Player_health_changed``. Leva um novo valor como seu único argumento:

func update_health(new_value):
    pass
public void UpdateHealth(int health)
{
}

Este método precisa:

  • setar o `` text`` do nó `` Number`` para `` new_value`` convertido para uma string
  • setar `` value`` do `` TextureProgress`` para `` new_value``
func update_health(new_value):
    number_label.text = str(new_value)
    bar.value = new_value
public void UpdateHealth(int health)
{
    _numberLabel.Text = health.ToString();
    _bar.Value = health;
}

Dica

`` str`` é uma função interna que converte qualquer valor para texto. A propriedade `` text`` do `` Number`` requer uma string, então não podemos atribuí-la ao `` new_value`` diretamente

Also call update_health at the end of the _ready function to initialize the Number node's text with the right value at the start of the game. Press F5 to test the game: the life bar updates with every attack!

../../_images/lifebar_tutorial_LifeBar_health_update_no_anim.gif

O nó Number e o TextureProgress são atualizados quando o Player recebe um hit

Animar a perda de vida com o nó Tween

Nossa interface é funcional, mas poderia ter alguma animação. Essa é uma boa oportunidade para introduzir o nó Tween, uma ferramenta essencial para animar propriedades. Tween anima tudo o que você quiser de um valor inicial a um final por um determinada período de tempo. Por exemplo, ele pode animar a saúde no TextureProgress do seu nível atual para o novo valor de health do `` Player`` quando o personagem recebe dano.

A cena `` GUI`` já contém um nó filho `` Tween`` armazenado na variável `` tween``. Vamos agora usá-lo. Nós temos que fazer algumas mudanças em `` update_health``.

Nós usaremos o método `` interpolate_property`` do nó `` Tween``. São necessários sete argumentos:

  1. Uma referência ao nó que possui a propriedade para animar
  2. O identificador da propriedade como uma string
  3. O valor inicial
  4. O valor final
  5. A duração da animação em segundos
  6. O tipo da transição
  7. O alívio para usar em combinação com a equação.

Os dois últimos argumentos combinados correspondem a uma equação de suavização. Isso controla como o valor evolui do ponto inicial ao final.

Clique no ícone do script ao lado do nó `` GUI`` para abri-lo novamente. O nó `` Number`` precisa de texto para se atualizar, e a `` Bar`` precisa de um float ou um inteiro. Podemos usar `` interpolate_property`` para animar um número, mas não para animar o texto diretamente. Vamos usá-lo para animar uma nova variável `` GUI`` chamada `` animated_health``.

No topo do script, defina uma nova variável, nomeie-a como `` animated_health`` e defina seu valor como 0. Navegue de volta para o método `` update_health`` e limpe seu conteúdo. Vamos animar o valor `` animated_health``. Chame o método `` interpolate_property`` do nó `` Tween``:

func update_health(new_value):
    tween.interpolate_property(self, "animated_health", animated_health, new_value, 0.6)
// Add this to the top of your class.
private float _animatedHealth = 0;

public void UpdateHealth(int health)
{
    _tween.InterpolateProperty(this, "_animatedHealth", _animatedHealth, health, 0.6f, Tween.TransitionType.Linear,
        Tween.EaseType.In);
}

Vamos dividir a ligação:

tween.interpolate_property(self, "animated_health", ...

Nosso alvo é `` animated_health`` em `` self``, isto é, o nó `` GUI``. A interpolação de `` Tween`` _property toma o nome da propriedade como uma string. É por isso que nós escrevemos como `` "animated_health" ``.

... _health", animated_health, new_value, 0.6 ...

O ponto de partida é o valor atual em que a barra está. Nós ainda temos que codificar esta parte, mas ela será `` animated_health``. O ponto final da animação é `` health`` do `` Player`` depois do `` health_changed``: é `` new_value``. E `` 0.6`` é a duração da animação em segundos.

A animação não será reproduzida até que ativemos o nó `` Tween`` com `` tween.start () ``. Nós só temos que fazer isso uma vez se o nó não estiver ativo. Adicione este código após a última linha:

if not tween.is_active():
    tween.start()
if (!_tween.IsActive())
{
    _tween.Start();
}

Nota

Embora possamos animar a propriedade health no` Player`, não devemos. Personagens devem perder a vida instantaneamente quando são atingidos. Isso torna muito mais fácil gerenciar seu estado, como saber quando um morreu. Você sempre deseja armazenar animações em um contêiner de dados ou nó separado. O nó tween é perfeito para animações controladas por código. Para animações feitas à mão, confira AnimationPlayer.

Atribuir a _health animada para o LifeBar

Agora a variável `` animated_health`` anima, mas nós não atualizamos mais os nós `` Bar`` e `` Number``. Vamos consertar isso.

Até agora, o método update _health se parece com isto:

func update_health(new_value):
    tween.interpolate_property(self, "animated_health", animated_health, new_value, 0.6)
    if not tween.is_active():
        tween.start()
public void UpdateHealth(int health)
{
    _tween.InterpolateProperty(this, "_animatedHealth", _animatedHealth, health, 0.6f, Tween.TransitionType.Linear,
        Tween.EaseType.In);

    if(!_tween.IsActive())
    {
        _tween.Start();
    }
}

Neste caso específico, porque `` number_label`` recebe texto, nós precisamos usar o método `` _process`` para animá-lo. Vamos agora atualizar os nós `` Number`` e `` TextureProgress`` como antes, dentro de `` _process``:

func _process(delta):
    number_label.text = str(animated_health)
    bar.value = animated_health
public override void _Process(float delta)
{
    _numberLabel.Text = _animatedHealth.ToString();
    _bar.Value = _animatedHealth;
}

Nota

number_label e` bar` são variáveis que armazenam referências aos nós Number e` TextureProgress`.

Jogue o jogo para ver a barra animar suavemente. Mas o texto exibe o número decimal e parece uma bagunça. E considerando o estilo do jogo, seria bom para a barra de vida para animar de uma forma mais bonita.

../../_images/lifebar_tutorial_number_animation_messed_up.gif

A animação é suave, mas o número está quebrado

Podemos consertar ambos os problemas, completando `` animated_health``. Use uma variável local chamada `` round_value`` para armazenar o `` animated_health`` arredondado. Em seguida, atribua-o a `` number_label.text`` e `` bar.value``:

func _process(delta):
    var round_value = round(animated_health)
    number_label.text = str(round_value)
    bar.value = round_value
public override void _Process(float delta)
{
    var roundValue = Mathf.Round(_animatedHealth);
    _numberLabel.Text = roundValue.ToString();
    _bar.Value = roundValue;
}

Rode o jogo novamente para ver uma bela animação em blocos.

../../_images/lifebar_tutorial_number_animation_working.gif

Ao arredondar animated_health, já resolvemos dois problemas

Dica

Toda vez que o jogador recebe um hit, o `` GUI`` chama `` _on_Player_health_changed``, que por sua vez chama `` update_health``. Isso atualiza a animação e o `` number_label`` e `` bar`` seguem em `` _process``. A barra de vida animada que mostra a saúde diminuindo gradualmente é um truque. Faz a GUI parecer viva. Se o `` Player`` receber 3 de dano, isso acontece em um instante.

Clareia a barra quando o jogador morre

Quando o personagem verde morre, ele executa uma animação de morte e desaparece. Neste ponto, não devemos mais mostrar a interface. Vamos desaparecer também quando o personagem morrer. Vamos reutilizar o mesmo nó `` Tween`` enquanto ele gerencia múltiplas animações em paralelo para nós.

Primeiro, o `` GUI`` precisa se conectar ao sinal `` died`` do `` Player`` para saber quando ele morreu. Pressione: kbd: F1 para voltar ao espaço de trabalho 2D. Selecione o nó `` Player`` na doca Scene e clique na aba Node ao lado do Inspector.

Encontre o sinal `` morreu``, selecione-o e clique no botão Conectar.

../../_images/lifebar_tutorial_player_died_signal_enemy_connected.png

O sinal já deve ter o inimigo conectado a ele

Na janela Connecting Signal, conecte-se ao nó `` GUI`` novamente. O caminho para o nó deve ser `` ../../ GUI`` e o método no nó deve mostrar `` _on_Player_died``. Deixe a opção Make Function ativada e clique em Connect na parte inferior da janela. Isso levará você ao arquivo `` GUI.gd`` no espaço de trabalho do Script.

../../_images/lifebar_tutorial_player_died_connecting_signal_window.png

Você deve obter esses valores na janela Conectando um sinal

Nota

Você deve ver um padrão agora: toda vez que a GUI precisar de uma nova informação, emitimos um novo sinal. Use-os com sabedoria: quanto mais conexões você adicionar, mais difícil será rastrear.

Para animar um fade em um elemento da interface do usuário, temos que usar sua propriedade `` modulate``. `` modulate`` é uma `` Cor`` que multiplica as cores de nossas texturas.

Nota

modulate vem da classe` CanvasItem`, todos os nós 2D e UI são herdados. Ele permite que você alterne a visibilidade do nó, atribua um sombreador a ele e modifique-o usando uma cor com modulate.

modulate recebe um valor Color com 4 canais: vermelho, verde, azul e alfa. Se escurecermos qualquer um dos três primeiros canais, escurecerá a interface. Se abaixarmos o canal alfa, nossa interface fica translúcida.

Vamos interpor entre dois valores de cores: de um branco com um alfa de `` 1``, isto é, com opacidade total, a um branco puro com um valor alfa de `` 0``, completamente transparente. Vamos adicionar duas variáveis no topo do método `` _on_Player_died`` e nomea-las `` start_color`` e `` end_color``. Use o construtor `` Color () `` para construir dois valores `` Color``.

func _on_Player_died():
    var start_color = Color(1.0, 1.0, 1.0, 1.0)
    var end_color = Color(1.0, 1.0, 1.0, 0.0)
public void OnPlayerDied()
{
    var startColor = new Color(1.0f, 1.0f, 1.0f);
    var endColor = new Color(1.0f, 1.0f, 1.0f, 0.0f);
}

`` Color (1.0, 1.0, 1.0) `` corresponde ao branco. O quarto argumento, respectivamente `` 1.0`` e `` 0.0`` em `` start_color`` e `` end_color``, é o canal alfa.

Nós então temos que chamar o método `` interpolate_property`` do nó `` Tween`` novamente:

tween.interpolate_property(self, "modulate", start_color, end_color, 1.0)
_tween.InterpolateProperty(this, "modulate", startColor, endColor, 1.0f, Tween.TransitionType.Linear,
  Tween.EaseType.In);

This time, we change the modulate property and have it animate from start_color to the end_color. The duration is of one second, with a linear transition. Here's the complete _on_Player_died method:

func _on_Player_died():
    var start_color = Color(1.0, 1.0, 1.0, 1.0)
    var end_color = Color(1.0, 1.0, 1.0, 0.0)
    tween.interpolate_property(self, "modulate", start_color, end_color, 1.0)
public void OnPlayerDied()
{
    var startColor = new Color(1.0f, 1.0f, 1.0f);
    var endColor = new Color(1.0f, 1.0f, 1.0f, 0.0f);

    _tween.InterpolateProperty(this, "modulate", startColor, endColor, 1.0f, Tween.TransitionType.Linear,
        Tween.EaseType.In);
}

E é isso. Agora você pode jogar o jogo para ver o resultado final!

../../_images/lifebar_tutorial_final_result.gif

O resultado, final Parabéns por chegar lá!

Nota

Usando exatamente as mesmas técnicas, você pode mudar a cor da barra quando o jogador é envenenado, transformar a barra em vermelho quando a sua saúde cai, sacudir a interface do usuário quando eles tomam um dano crítico ... o princípio é o mesmo: emitir um sinal para encaminhar as informações do Player para o` GUI` e deixar o GUI processá-lo.