Prefácio
Neste tutorial, implementaremos nosso próprio clone do Tetris. Sim, o jogo russo que conquistou o mundo desde o seu primeiro lançamento jogável em 6 de junho de 1984. Nosso clone será igualmente simples, com apenas cerca de 130 linhas de código e dois recursos, mantendo a jogabilidade simples, mas altamente viciante. . Embora o jogo pareça bastante simples de implementar, ele ainda apresenta alguns desafios e será um ótimo exercício para iniciantes e usuários experientes que trabalham com Unity.
Como sempre, tudo será explicado da forma mais fácil possível para que todos possam entender.
Requisitos
Conhecimento
Este tutorial não requer nenhuma habilidade especial, exceto algum conhecimento sobre os fundamentos do Unity, mesmo que você ainda não conheça esses conceitos, porém recomendamos que você aprimore seu conhecimento dos assuntos mencionados antes de tentar este tutorial, veja no canal DFILITTO.
Versão da unidade
Nosso tutorial de Tetris usará o Unity 2018.3 . Versões mais recentes também devem funcionar bem; versões mais antigas podem ou não funcionar. É importante usarmos pelo menos o Unity 2018.3, pois é uma versão recente do Unity, amplamente utilizada por milhares de desenvolvedores e tem tudo o que precisamos para este tutorial.
Configuração do projeto
Tudo bem, vamos fazer nosso jogo Tetris! Criaremos um novo projeto Unity usando o Unity Hub. Primeiramente, escolhemos um diretório para salvá-lo, selecionamos o modelo de jogo 2D Unity e clicamos em Criar . Observe que essas capturas de tela foram tiradas em um PC Windows com Unity Hub 2.x instalado. Adapte-se adequadamente se o seu Unity Hub parecer diferente das capturas de tela mostradas abaixo.
Se selecionarmos a Câmera Principal na Hierarquia então podemos definir a Cor de Fundo para preto, ajustar o Tamanho e a Posição como mostrado na imagem a seguir:
Nota: é importante que nossa câmera esteja em X=4,5 porque este será o centro da nossa cena mais tarde.
Sobre blocos e grupos
Vamos fazer algumas definições para que todos saibamos do que estamos falando. Teremos blocos e grupos . Um grupo conterá alguns blocos também conhecidos como “tetrominós” no jogo original, mas para facilitar a lembrança, vamos simplesmente chamá-los de blocos:
Existem vários tipos de blocos no Tetris, que são os blocos I , J , L , O , S , T e Z :
Criando a arte do jogo
Como pode ser visto nas imagens acima, manteremos o estilo artístico simples. Cada grupo pode ser criado com apenas um tipo de bloco com um retângulo verde arredondado:
Nota: clique com o botão direito na imagem, selecione Salvar como… e salve na pasta Assets do projeto.
Vamos selecionar a imagem do bloco na Área do Projeto e depois ajustar as configurações de importação no Inspetor :
Nota: a propriedade Pixels to Units especifica o tamanho do jogo.
Usaremos mais um ativo para as bordas para dar alguma ajuda visual ao jogador:
Obs: clique com o botão direito na imagem, selecione Salvar como… e salve na pasta Assets do projeto.
Usaremos as seguintes configurações de importação para a borda:
Adicionando Bordas
Tudo bem, agora que configuramos a arte do jogo, podemos arrastar a borda da Área do Projeto para a Hierarquia duas vezes:
Em nossa implementação, a cena do Tetris terá exatamente 10 blocos de largura e cerca de 20 blocos de altura. Portanto, as coordenadas dos blocos estão sempre entre (0, 0) e (9, 19) .
Nota: se você contar, serão 10 unidades na horizontal e 20 unidades na vertical, começando em 0.
Agora as bordas devem estar à esquerda e à direita do nosso jogo, então uma delas estará em algum lugar em X=0 e a outra em X=9 . Também é uma boa ideia adicionar algum espaçamento, então vamos selecionar uma borda após a outra na Hierarquia e então ajustar as posições e escalas como mostrado abaixo:
Esta é a aparência de nossas bordas se pressionarmos Play :
Criando os Grupos
Ok, agora é hora de criar os grupos I , J , L , O , S , T e Z. Para ser mais exato, queremos um Prefab para cada um.
Começaremos criando um GameObject vazio selecionando GameObject -> Criar Vazio no menu superior. Isso adiciona um GameObject vazio à nossa hierarquia:
Podemos arrastar a imagem do bloco para o GameObject vazio 4 vezes, então os 4 blocos são seus filhos:
Agora o truque é posicionar os blocos para que se tornem o Grupo O :
Aqui estão as coordenadas que usamos para os quatro blocos. Comece no primeiro GameObject e vá trabalhando um por um:
- X = 0 Y = 0
- X = 0 Y = 1
- X = 1 Y = 0
- X = 1 Y = 1
Nota: Como o Tetris é 2D, não nos preocupamos com o eixo Z. É por isso que não há eixo Z nas posições acima.
É importante usarmos coordenadas arredondadas como 1 em vez de 1,1 porque o tamanho do nosso bloco é sempre exato 1×1 e um bloco sempre se moverá 1 unidade. Portanto, se usarmos tamanhos de bloco como 1,1 e movê-los por 1 , terminaremos em uma coordenada como 2,1 – que pode estar dentro de outro bloco.
Ou em outras palavras: desde que utilizemos coordenadas arredondadas, estaremos bem.
Tudo bem, vamos renomear o GameObject (não mais vazio) para GroupO no Inspector . Isso pode ser feito selecionando o GameObject e pressionando F2 no Windows para Renomear ou clicando com o botão direito e escolhendo Renomear no menu pop-up que aparece. É assim que parece na Hierarquia agora:
Agora podemos arrastá-lo para ProjectArea para criar um Prefab :
Não precisamos mais dele na Hierarquia , então podemos Excluí- lo selecionando-o na Hierarquia e pressionando a tecla Delete em nosso teclado, ou clicando com o botão direito e escolhendo excluir no menu pop-up que aparece.
Repetiremos o mesmo fluxo de trabalho para o restante dos grupos:
O Gerador de Tetromino
Vamos criar outro GameObject vazio, nomeá-lo Spawner e posicioná-lo no topo da Cena:
O Spawner fornecerá uma função spawnNext que gera um grupo aleatório quando necessário. Vamos clicar com o botão direito na Project Area , selecionar Create -> C# Script e nomeá-lo Spawner . Primeiramente adicionaremos um array público GameObject[] que nos permitirá arrastar todos os grupos para o Inspector posteriormente:
using UnityEngine;
using System.Collections;
public class Spawner : MonoBehaviour {
// Groups
public GameObject[] groups;
}
Nota: array significa que é um monte de GameObjects, e não apenas um.
Agora podemos criar a função spawnNext que seleciona um elemento aleatório do array groups e o lança no mundo usando Instantiate :
public void spawnNext() {
// Random Index
int i = Random.Range(0, groups.Length);
// Spawn Group at current Position
Instantiate(groups[i],
transform.position,
Quaternion.identity);
}
Nota: transform.position é a posição do Spawner, Quaternion.identity é a rotação padrão.
O Spawner também deve gerar um grupo aleatório assim que o jogo começar. É para isso que serve a função Iniciar :
void Start() {
// Spawn initial Group
spawnNext();
}
Nota: a função Start será chamada automaticamente pelo Unity quando a cena do jogo for carregada e o script Spawner for iniciado.
Até agora tudo bem. Vamos selecionar o Spawner na Hierarquia e depois clicar em Add Component -> Scripts -> Spawner no Inspector . Depois arrastamos um grupo após o outro da nossa área de projeto para o slot Grupos :
Se pressionarmos Play , poderemos ver como o Spawner gera o primeiro grupo:
A aula do campo de jogo
Motivação
Para implementar o restante dos recursos de jogo vistos no jogo Tetris original, precisaremos de algumas funções auxiliares para:
- Verifique se todos os blocos estão entre as bordas
- Verifique se todos os blocos estão acima de y=0
- Verifique se um grupo pode ser movido para uma determinada posição
- Verifique se uma linha está cheia de blocos
- Excluir uma linha
- Diminuir a coordenada y de uma linha
A maneira óbvia de verificar coisas com outros blocos no Unity seria usando FindGameObjectsWithTag . Além dos problemas de desempenho, o principal problema com esta função é que não conseguimos descobrir se existe um bloqueio em uma determinada posição. Em vez disso, teríamos sempre que percorrer todos os blocos e verificar as suas posições.
A estrutura de dados
A solução para resolver este problema é implementar uma grade , ou em outras palavras: um array (ou matriz) bidimensional. Você deve ter ouvido o termo nas aulas de matemática durante a escola. A estrutura de dados é mais ou menos assim:
___|_0_|_1_|_2_|...
0 | o | x | x |...
1 | o | x | o |...
2 | x | x | o |...
...|...|...|...|...
O x significa que há um bloco, o o significa que não há bloco. Portanto na coordenada (0,0) não há bloco, em (0,1) há bloco e assim por diante.
E aqui está como podemos acessar facilmente um bloco em uma determinada posição:
// Is there a block at (3,4)?
if (grid[3,4] != null) {
// Do Stuff...
}
Agora que sabemos a resposta para o nosso problema, há um problema. Infelizmente, se chamarmos nosso novo script de Grid , isso provavelmente causará um conflito contra uma classe interna do Unity com o mesmo nome. Então, vamos criar um novo script C# e nomeá-lo Playfield . Ele armazenará a própria grade e algumas funções úteis para trabalhar com ela. Aqui está como podemos definir um array bidimensional em C#:
Criando o script do Playfield
using UnityEngine;
using System.Collections;
public class Playfield : MonoBehaviour {
// The Grid itself
public static int w = 10;
public static int h = 20;
public static Transform[,] grid = new Transform[w, h];
}
Fácil, certo? A grade também pode ser do tipo GameObject , mas ao torná-la do tipo Transform não teremos que escrever algo.transform.position o tempo todo. E como todo GameObject possui um Transform, ele funcionará perfeitamente.
A função auxiliar roundVec2
Nossa primeira função auxiliar arredondará um vetor. Por exemplo, um vetor como (1.0001, 2) torna-se (1, 2) . Precisaremos desta função porque as rotações podem fazer com que as coordenadas não sejam mais redondas. De qualquer forma, aqui está a função:
public static Vector2 roundVec2(Vector2 v) {
return new Vector2(Mathf.Round(v.x),
Mathf.Round(v.y));
}
Nota: uma função estática pública permite que ela também seja acessada por outros scripts. Muito útil para funções auxiliares/utilitárias.
A função auxiliar insideBorder
A próxima função será igualmente fácil. Isso nos ajudará a descobrir se uma determinada coordenada está entre as fronteiras ou fora das fronteiras:
public static bool insideBorder(Vector2 pos) {
return ((int)pos.x >= 0 &&
(int)pos.x < w &&
(int)pos.y >= 0);
}
O que acontece é que primeiro ele testa a posição x que deve estar entre 0 e a largura da grade w , e depois descobre se a posição y ainda é positiva.
Nota: não verifica se pos.y < h porque os grupos não se movem para cima, exceto em algumas rotações.
A função auxiliar deleteRow
A próxima função exclui todos os blocos em uma determinada linha. Será útil quando o jogador conseguir preencher todas as entradas consecutivas (nesse caso, será excluído):
public static void deleteRow(int y) {
for (int x = 0; x < w; ++x) {
Destroy(grid[x, y].gameObject);
grid[x, y] = null;
}
}
A função usa o parâmetro y , que é a linha que deve ser excluída do campo de jogo. Em seguida, ele percorre cada bloco daquela linha, Destroy s do jogo e limpa a referência a ele definindo a entrada da grade como null .
A função auxiliar diminuiçãoRow
Sempre que uma linha for excluída, as linhas acima deverão cair uma unidade para baixo. A seguinte função cuidará disso:
public static void decreaseRow(int y) {
for (int x = 0; x < w; ++x) {
if (grid[x, y] != null) {
// Move one towards bottom
grid[x, y-1] = grid[x, y];
grid[x, y] = null;
// Update Block position
grid[x, y-1].position += new Vector3(0, -1, 0);
}
}
}
Semelhante à nossa função anterior, esta toma o valor y da linha como parâmetro, passando por cada bloco daquela linha dentro do loop for e então movendo-o uma unidade para baixo. Porém, o que devemos ter em mente aqui é que também precisamos atualizar a posição mundial do bloco. Caso contrário, o bloco seria atribuído à entrada correta da grade, mas ainda pareceria que está na posição antiga no mundo do jogo, causando uma falha visual indesejada.
A posição mundial do bloco é modificada adicionando o Vetor (0, -1, 0) a ele. Ou, em outras palavras, estamos diminuindo a coordenada y do bloco em um.
A função diminuirRowsAbove
Nossa próxima função usará a função diminuiRow anterior e a usará em todas as linhas acima de um determinado índice porque sempre que uma linha for excluída, queremos diminuir todas as linhas acima dela, não apenas uma:
public static void decreaseRowsAbove(int y) {
for (int i = y; i < h; ++i)
decreaseRow(i);
}
Como antes, a função leva o parâmetro y que é a linha. Em seguida, ele percorre todas as linhas acima usando i , começando em y e fazendo um loop while i < h (em outras palavras, fazemos um loop while i for menor que h ).
A função isRowFull
Mencionamos antes que uma linha deve ser excluída quando estiver cheia de blocos. Então, vamos direto ao assunto e criar uma função que descobre se uma linha está cheia de blocos:
public static bool isRowFull(int y) {
for (int x = 0; x < w; ++x)
if (grid[x, y] == null)
return false;
return true;
}
Esta função é bastante fácil. Ele usa o parâmetro de linha y , percorre cada entrada da grade e retorna falso assim que não há bloco em uma entrada da grade. Se o loop for foi concluído e ainda não retornamos false, então a linha deve estar cheia de blocos; nesse caso, retornamos true .
A função deleteFullRows
Agora é hora de juntar tudo e escrever uma função que exclua todas as linhas completas e sempre diminua a coordenada y da linha acima em um. Nada disso é mais difícil, agora que temos todas as nossas funções auxiliares:
public static void deleteFullRows() {
for (int y = 0; y < h; ++y) {
if (isRowFull(y)) {
deleteRow(y);
decreaseRowsAbove(y+1);
--y;
}
}
}
Nota: –y diminui y em um sempre que uma linha é excluída. É para garantir que a próxima etapa do loop for continue no índice correto (que deve ser diminuído em um, porque acabamos de deletar uma linha).
E isso é tudo que precisamos para nossa aula de grade. O que acabamos de fazer aqui é conhecido como Programação Bottom-Up . Começamos com a função mais fácil e depois criamos cada vez mais funções que fazem uso das criadas anteriormente.
O benefício dessa técnica de desenvolvimento é que ela torna nossas vidas um pouco mais fáceis, porque não precisamos retroceder tanto em nossas mentes.
O roteiro do grupo
Criando o roteiro
É hora de finalmente adicionar um pouco de jogabilidade ao nosso clone do Unity Tetris. Vamos criar um novo script C# e nomeá-lo Group :
using UnityEngine;
using System.Collections;
public class Group : MonoBehaviour {
// Use this for initialization
void Start () {
}
// Update is called once per frame
void Update () {
}
}
Criando as funções auxiliares
A princípio adicionaremos mais duas funções auxiliares. Lembra como colocamos vários blocos em um GameObject e o chamamos de grupo? Precisaremos de uma função que nos ajude a verificar a posição de cada bloco filho:
bool isValidGridPos() {
foreach (Transform child in transform) {
Vector2 v = Playfield.roundVec2(child.position);
// Not inside Border?
if (!Playfield.insideBorder(v))
return false;
// Block in grid cell (and not part of same group)?
if (Playfield.grid[(int)v.x, (int)v.y] != null &&
Playfield.grid[(int)v.x, (int)v.y].parent != transform)
return false;
}
return true;
}
A função é realmente fácil de entender. Primeiro, ele percorre cada filho usando foreach e depois armazena a posição arredondada do filho em uma variável. Depois ele descobre se aquela posição está dentro da borda, e então descobre se já existe um bloco na mesma entrada da grade ou não.
Antes de prosseguirmos muito, há um caso extremo que precisamos cuidar aqui. Temos que permitir interseções entre blocos dentro do mesmo grupo, para garantir que algumas rotações e translações não serão detectadas como inválidas . Por exemplo, se um grupo I mover uma unidade para baixo, então a maioria dos blocos dentro desse grupo se cruzariam como mostrado abaixo, onde a é a primeira posição e b é a segunda posição:
a
a b <- intersection
a b <- intersection
a b <- intersection
b
Podemos evitar esse tipo de interseção comparando a transformação pai de um bloco com a transformação atual, como fizemos acima.
Tudo bem, vamos criar a última função auxiliar. Se um grupo mudou sua posição, ele deverá remover todas as posições de bloco antigas da grade e adicionar todas as novas posições de bloco à grade:
void updateGrid() {
// Remove old children from grid
for (int y = 0; y < Playfield.h; ++y)
for (int x = 0; x < Playfield.w; ++x)
if (Playfield.grid[x, y] != null)
if (Playfield.grid[x, y].parent == transform)
Playfield.grid[x, y] = null;
// Add new children to grid
foreach (Transform child in transform) {
Vector2 v = Playfield.roundVec2(child.position);
Playfield.grid[(int)v.x, (int)v.y] = child;
}
}
Como feito várias vezes anteriormente, percorremos a grade e verificamos se o bloco (se houver) faz parte do grupo usando a propriedade pai . Se o pai do bloco for igual à transformação do grupo atual, então ele é filho desse grupo. Depois, percorremos todos os filhos novamente para adicioná-los à grade.
Mover e cair
Ok, agora podemos adicionar um pouco de jogabilidade. Em breve tudo fará sentido…
Modificaremos nossa função Update para que ela verifique o pressionamento de teclas agora, começando com a tecla de seta para a esquerda:
void Update() {
// Move Left
if (Input.GetKeyDown(KeyCode.LeftArrow)) {
// Modify position
transform.position += new Vector3(-1, 0, 0);
// See if it's valid
if (isValidGridPos())
// It's valid. Update grid.
updateGrid();
else
// Its not valid. revert.
transform.position += new Vector3(1, 0, 0);
}
}
É aqui que todas as nossas funções auxiliares são úteis. Tudo o que precisamos fazer para mover o grupo para a esquerda é:
- aguarde o pressionamento da tecla
- mova-o para a esquerda
- descubra se a posição ainda é válida
- se sim, atualize a grade com a nova posição
- se não, volte para a direita novamente
Aqui está como podemos mover para a direita:
// Move Right
else if (Input.GetKeyDown(KeyCode.RightArrow)) {
// Modify position
transform.position += new Vector3(1, 0, 0);
// See if valid
if (isValidGridPos())
// It's valid. Update grid.
updateGrid();
else
// It's not valid. revert.
transform.position += new Vector3(-1, 0, 0);
}
Aqui está como podemos girar:
// Rotate
else if (Input.GetKeyDown(KeyCode.UpArrow)) {
transform.Rotate(0, 0, -90);
// See if valid
if (isValidGridPos())
// It's valid. Update grid.
updateGrid();
else
// It's not valid. revert.
transform.Rotate(0, 0, 90);
}
Observe como é sempre exatamente o mesmo fluxo de trabalho?
E aqui está como podemos descer:
// Fall
else if (Input.GetKeyDown(KeyCode.DownArrow)) {
// Modify position
transform.position += new Vector3(0, -1, 0);
// See if valid
if (isValidGridPos()) {
// It's valid. Update grid.
updateGrid();
} else {
// It's not valid. revert.
transform.position += new Vector3(0, 1, 0);
// Clear filled horizontal lines
Playfield.deleteFullRows();
// Spawn next Group
FindObjectOfType<Spawner>().spawnNext();
// Disable script
enabled = false;
}
}
Desta vez, mais algumas coisas estão acontecendo. Quando movemos um bloco para baixo e a nova posição não é mais válida, temos que desabilitar o movimento, deletar todas as linhas completas e gerar o próximo grupo de blocos. Como temos uma função auxiliar para tudo, é tudo curto e fácil.
O grupo deve cair automaticamente uma vez por segundo, podemos fazer isso criando primeiro uma variável que registra o tempo da última queda:
// Time since last gravity tick
float lastFall = 0;
E então modificando nossa função de movimento para baixo para que ela também seja acionada uma vez por segundo e não apenas quando o jogador pressiona o botão de seta para baixo:
// Move Downwards and Fall
else if (Input.GetKeyDown(KeyCode.DownArrow) ||
Time.time - lastFall >= 1) {
// Modify position
transform.position += new Vector3(0, -1, 0);
// See if valid
if (isValidGridPos()) {
// It's valid. Update grid.
updateGrid();
} else {
// It's not valid. revert.
transform.position += new Vector3(0, 1, 0);
// Clear filled horizontal lines
Playfield.deleteFullRows();
// Spawn next Group
FindObjectOfType<Spawner>().spawnNext();
// Disable script
enabled = false;
}
lastFall = Time.time;
}
E aqui está a função de atualização final do nosso script de grupo:
void Update() {
// Move Left
if (Input.GetKeyDown(KeyCode.LeftArrow)) {
// Modify position
transform.position += new Vector3(-1, 0, 0);
// See if valid
if (isValidGridPos())
// It's valid. Update grid.
updateGrid();
else
// It's not valid. revert.
transform.position += new Vector3(1, 0, 0);
}
// Move Right
else if (Input.GetKeyDown(KeyCode.RightArrow)) {
// Modify position
transform.position += new Vector3(1, 0, 0);
// See if valid
if (isValidGridPos())
// It's valid. Update grid.
updateGrid();
else
// It's not valid. revert.
transform.position += new Vector3(-1, 0, 0);
}
// Rotate
else if (Input.GetKeyDown(KeyCode.UpArrow)) {
transform.Rotate(0, 0, -90);
// See if valid
if (isValidGridPos())
// It's valid. Update grid.
updateGrid();
else
// It's not valid. revert.
transform.Rotate(0, 0, 90);
}
// Move Downwards and Fall
else if (Input.GetKeyDown(KeyCode.DownArrow) ||
Time.time - lastFall >= 1) {
// Modify position
transform.position += new Vector3(0, -1, 0);
// See if valid
if (isValidGridPos()) {
// It's valid. Update grid.
updateGrid();
} else {
// It's not valid. revert.
transform.position += new Vector3(0, 1, 0);
// Clear filled horizontal lines
Playfield.deleteFullRows();
// Spawn next Group
FindObjectOfType<Spawner>().spawnNext();
// Disable script
enabled = false;
}
lastFall = Time.time;
}
}
É realmente muito fácil!
Há mais uma coisa que devemos observar. Se um novo grupo for gerado e colidir imediatamente com outro, o jogo termina:
void Start() {
// Default position not valid? Then it's game over
if (!isValidGridPos()) {
Debug.Log("GAME OVER");
Destroy(gameObject);
}
}
Quase pronto. Para cada pré-fabricado que fizemos na Área do Projeto , clique no pré-fabricado e depois clique no botão “Abrir Pré-fabricado” no Inspetor . Siga as capturas de tela abaixo para orientação:
Selecione o script Group – o primeiro desta lista ou você pode encontrá-lo em Scripts -> Group . Você verá isso aparecer no inspetor de pré-fabricados:
Por fim, clique na seta para trás para sair do modo Editor pré-fabricado. As alterações pré-fabricadas serão salvas automaticamente.
Nota: Você precisa repetir isso para todos os outros blocos pré-fabricados que criamos. Caso contrário, você receberá um erro durante a reprodução.
Se apertarmos Play agora podemos desfrutar de uma bela partida de Tetris:
Resumo
Parabéns! Você criou um clone do Tetris totalmente funcional em cerca de 130 linhas de código. Como sempre, um tutorial bem longo para poucas linhas de código.
Pode parecer surpreendente como gastamos tanto tempo trabalhando em funções auxiliares e como o jogo foi concluído rapidamente sem ter que nos preocupar com nada no final. Esta é a magia da programação bottom up.
Para onde vamos daqui?
Agora cabe a você, leitor, deixar o jogo ainda mais divertido. Aqui estão algumas idéias que você pode adotar:
– Gerar os tetrominós com cores diferentes
– Acelerar o jogo conforme você limpa as linhas
– Adicionar os bons e velhos efeitos sonoros do Tetris
– Mecânica de Hold e Next Piece
– Implementar um sistema melhor de game over com reinicialização
– O as possibilidades são infinitas. Faça sua própria variante do Tetris!
E ai, esta gostando da área de games? Então que tal se tornar um desenvolvedor de jogos completo com o curso Desenvolvedor de Jogos 2D e 3D. Neste curso você irá aprender na prática tudo sobre Game Design, Criação de artes para jogos, Lógica de Programação, Programação Orienta a Objetos e é claro, criar seus próprios jogos utilizando a Engine Unity.
FONTE: Esse texto foi retirado do site https://noobtuts.com/unity,
Super Dicas
Venha conhecer os nossos cursos da Hotmart Clube e Udemy.
Se inscreva em nosso canal, compartilhe as matérias que gostar com os seus colegas, e participe da nossa comunidade no Telegram.
Aproveite também e venha fazer parte do nosso clube de vantagens e ter acesso exclusivo a vídeos, tutoriais, cursos e muito mais. Clique no link para se tornar um membro do dfilitto – clube de vantagens e ter acesso a todos os benefícios do nosso clube.