O TEMA DO FÓRUM ESTÁ EM MANUTENÇÃO. FEEDBACKS AQUI: ACESSAR

Desenvolvendo um jogo de quebra-cabeças - Parte 1

Iniciado por MayLeone, 16/01/2019 às 15:34


Olá pessoal! No tutorial de hoje nós vamos desenvolver um jogo de quebra-cabeças (aqueles clássicos de encaixar as peças para formar uma imagem) na engine Unity.
Em nosso tutorial, será possível escolher qualquer imagem de qualquer tamanho para montar o quebra-cabeças, também será possível decidir o nível de dificuldade do mesmo, onde podemos ter quantidade de peças diferentes dependendo da grade selecionada, por exemplo, uma grade 4x4 terá ao todo 16 peças para serem montadas.
Em nosso jogo, teremos uma grade que irá orientar o jogador da onde cada peça deverá se encaixar, então em nosso tutorial iremos programar também a criação dessa grade proporcional à quantidade de peças, iremos criar o sistema de clique com o mouse, verificação de vitória, recorte da imagem e muito mais.
O resultado final será exatamente esse aqui:




Recortando a imagem:
Uma das primeiras coisas que faremos em nosso tutorial será "cortar" em vários pedacinhos a imagem que será usada como quebra-cabeças.
Antes de iniciarmos de fato a programação de tal sistema no Unity, vamos entender como faríamos esse tipo de "recorte", irei usar como exemplo a imagem abaixo:



Essa imagem foi reduzida para se adequar ao tamanho do site corretamente, porém ela possui originalmente as dimensões 900x800 (largura e altura).
Supondo que desejamos cortar essa imagem em exatos 4 pedaços, isso significa que teremos uma grade 2x2 ou seja, teremos 2 pedaços a serem cortados horizontalmente e mais 2 pedaços na vertical, totalizando 4 peças.
Então tudo certo, sabemos a quantidade de peças e a grade (quantas imagens iremos ter nas linhas e colunas), mas a grande pergunta é... Qual o tamanho(dimensão) de cada pedaço, para que sejam cortados com exatidão sem sobrar partes da imagem?
O cálculo é mais simples do que pensa, para saber a largura do pedaço, basta dividir a largura total da imagem pelo valor da grade (no caso aqui, 2) e o mesmo com a altura, divida a altura da imagem pela dimensão da grade (2).
Por fim, iremos concluir que cada pedaço terá 450x400 de dimensão (900/2) e (800/2), isso significa que teremos 4 imagens de 450x400 de dimensão numa grade 2x2, ou seja, 2 imagens para as colunas e 2 para as linhas, dessa forma:



O padrão se repete caso queria ter mais peças.
Por exemplo, no caso de querer 9 peças, teremos uma grade 3x3, ou seja, teremos 3 imagens para as linhas e 3 para as colunas.
Cada pedaço terá 300x266 pixels de altura e largura (caso a imagem possua 900x800 de dimensão).

Dessa forma, podemos então descobrir o tamanho de cada retângulo (parte da imagem/peça) quando formos cortar as peças através de código.

Script para recortar as peças:
Agora que está entendido como iremos "cortar" a imagem, abra um novo projeto 2D na engine com o nome "Puzzle Game" e importe a imagem que desejá cortar, também crie um prefab de um sprite 2D com o nome de "Piece", a única coisa que ele terá por enquanto será um box collider 2D.
Esse prefab será responsável por "ser a peça", onde exibirá seu sprite como sendo uma parte da imagem.

Para cortar uma imagem devemos utilizar uma Texture2D para representá-la, porém, para poder manipular as textures importadas do projeto devemos permitir que elas sejam passíveis à leitura e escrita, portanto, clique na imagem importada e através do inspector vá à opção "Texture Type >> Advanced" e marque a caixa "Read/Write Enabled" e dê apply:



Dessa forma poderemos ter acesso à textura via código.

Agora crie um script chamado "CropTextures" para que possamos recortar as imagens.

Vamos agora criar algumas variáveis públicas para esse script: Um enumerador que irá definir o tamanho da grade do jogo, variando entre as grades 2x2 até 9x9 (você pode colocar mais, se desejar), uma variável do tipo do nosso enumerador para receber a resolução da grade, uma variável do tipo Texture2D chamada "sourceTexture" para recebermos a imagem que será cortada e uma variável do tipo "GameObject" para armazenar o prefab da peça a ser instanciada.
Também criaremos duas variáveis privadas, uma do tipo "int" chamada "amoutPieces" que representará a quantidade de peças e um Vector2 chamado "resolutionPieces" para armazenar a resolução de cada parte da imagem (aquele retângulo que vimos no exemplo anterior):



Vamos agora criar um método chamado "StartComponents" para inicializarmos alguns valores.
Por enquanto os únicos valores a serem iniciados serão a quantidade de peças que deve representar o índice do enum "Options", e a resolução das peças que deve ser calculada de acordo com o cálculo que eu mostrei a vocês, ou seja, resolutionPieces.x deve ser a largura da texture dividida pela grade na horizontal, e resolutionPieces.y deve ser a altura da texture dividida pela grade na vertical:



Chame esse método dentro do Start do script.

Agora vamos criar o método que de fato cortará a textura em vários pedaços, para criá-lo deixe-o retornar uma Texture2D, chame-o de "CropTexture" e defina dois parâmetros do tipo inteiro, um chamado "row" para representar a coluna que ele deve cortar a imagem e outro chamado "line" para representar a linha.
Agora crie duas variáveis locais (chamadas resolutionX e resolutionY) para que elas recebam os valores da resolução das peças arredondadas (utilize o Mathf.RoundToInt para isso):



Então agora utilizaremos uma função da classe "Texture2D" que se chama "GetPixels".
Essa função irá retornar os pixels de uma textura/imagem de acordo com a posição e tamanho que serão passados como argumento em formato de um retângulo, deverá ser passado a posição na imagem onde os pixels serão obtidos e a resolução dessa área. A função retorna os pixels da área desejada em forma de um vetor do tipo "Color".

Vamos então criar uma variável local chamada "pixels" do tipo array Color e fazer com que receba a função "GetPixels" sendo chamada através da textura que será anexada ao inspector que ficará armazenada na variável "sourceTexture":



Agora devemos passar o retângulo (área que deve-se obter os pixels da imagem) como argumento da função.
Esse argumento é um retângulo como já dito, que vai pedir a posição X da onde ele vai obter os pixels e a posição Y, então multiplique o valor da coluna (row) pela resolução em X da peça (row*resolutionX) e multiplique a linha pela resolução em Y (line*resolutionY).
Para saber o tamanho da área a ser obtida, basta passar o valor da resolução em X e em Y, respectivamente:



Como iremos criar um laço de repetição que irá percorrer a imagem completa de acordo com as dimensões da mesma, a função "CropTexture" irá ser chamada toda vez que uma parte da imagem for percorrida, representada pelos parâmetros "row" e "line" para sabermos em qual coluna e linha o laço está, dessa forma, a nossa função irá retornar um pedaço da textura num local específico da imagem toda vez que for chamada.

Agora vamos criar uma nova Texture2D para ser retornada da função com a parte da imagem que foi recortada através da função "GetPixels"
Para isso, basta criar uma variável local chamada "tex" e dentro do seu construtor passar a resolução dessa nova textura a ser criada, no caso, resolutionX e resolutionY.
Após fazer isso, preencha essa nova textura com os pixels obtidos anteriormente e que estão armazenados no array "pixels", através da função "SetPixels". Aplique essa atualização através da função "Apply" e retorne da função a variável "tex":



Criando as posições para as peças:
Nessa altura do tutorial nós temos como retornar cada parte da imagem de acordo com sua resolução, mas ainda não temos onde colocar essas imagens das peças.
Nós iremos representar a imagem de cada peça a partir daquele prefab já citado anteriormente que é apenas um sprite2D com um collider.
Antes de instanciarmos o prefab com a imagem cortada em seu sprite, vamos definir as possíveis posições onde esses prefabs serão sorteados.
Para que o jogo fique mais interesse, vamos sortear os pedacinhos em locais aleatórios da cena e toda vez que o jogo ser iniciado novamente, os pedacinhos irão trocar de posições.

Vamos criar mais algumas variáveis do tipo "Vector2": uma chamada "position" que irá guardar a posição da peça que será instanciada, outra chamada "distancePieces" que irá guardar a resolução do tamanho das peças para que possamos instanciá-las uma do lado da outra, e duas listas do tipo "Vector2" uma chamada "positions" para guardar todas as posições possíveis que as peças ficarão, e outra chamada "sortedPieces" que irá armazenadas as peças já sorteadas, para que não se repita um local e alguma peça fique sobre a outra:



Criaremos agora um método sem retorno chamado "CreatePositions" que vai ser responsável por de fato, criar as possíveis posições para que as peças possam ser instanciadas.
Vamos calcular primeiramente a distância de cada peça, que deve ser em "x" a resolução da peça em X divida por 100 (se a resolução da peça for 300, por exemplo, a distância entre essas peças na Unity será de 3 pixels, já que estamos utilizando 100 pixels per unit para o tamanho da imagem) e para Y será o mesmo princípio.
Depois disso, crie um laço de repetição que tem como contador um inteiro chamado "x" e que vai ser incrementado até que o mesmo seja menor que a quantidade de peças, e dentro desse laço defina outro laço com o contador chamado "y" que irá ser incrementado até que seu valor seja menor que a quantidade de peças.
Agora adicione dentro da lista "positions" um novo Vector2 em x tendo a distância das peças multiplicado pelo contador "x" e em "y" a distância das peças multiplicado pelo contador "y":



Então se por exemplo temos uma peça com resolução de 400 pixels(de largura e altura) e uma grade 2x2, as posições serão:

0 e 0,
0 e 4,
4 e 0,
4 e 4.

Ou seja, teremos duas peças nas posições 0;0 e 0;4 (primeira linha) e duas peças nas posições 4;0 e 4;4 (segunda linha), ou seja, uma grade 2x2 com duas linhas e duas colunas.
A distância entre cada peça é de 4 pixels tanto em x quanto em y, pois dividimos a resolução da peça por 100, fazendo com que quando as peças forem instanciadas em alguma dessas quatro posições, elas fiquem exatamente uma do lado da outra, formando então uma grade 2x2 com 4 peças.

Chame esse método "CreatePositions" dentro do Start do script.

Randomizando posições:
Agora que temos dentro da lista "positions" todas as possíveis posições para as peças, vamos randomizá-las, sorteando aleatoriamente sempre uma nova posição para a peça que está sendo instanciada.
Para tal, crie um método chamado "RandomPosition" que retorna um Vector2.
Dentro desse método crie uma variável local chamada "sorted" para definirmos se alguma posição válida foi sorteada, então inicie essa variável como sendo "false".
Crie uma outra variável local chamada "pos" que irá conter as posições sorteadas e a inicialize como um Vector zero.
Agora crie um laço "while" que irá ficar sendo executado enquanto "sorted" for false.
Dentro do laço faça com que a variável "pos" receba um valor aleatório dentro da lista "positions" através do método Random.Range.
Após isso, nós iremos armazenar dentro da variável "sorted" se essa posição sorteada aleatoriamente já foi sorteada antes, para isso, basta verificar se dentro da lista "sortedPositions" o valor de "pos" não está contido nela.
Caso "sorted" seja true (ou seja, o valor ainda não foi sorteado) armazene dentro da variável "sortedPositions" o valor de "pos", já que é uma posição que ainda não foi sorteada. Após o laço, apenas retorne "pos":



Pronto! Agora temos um método que irá randomizar as posições das peças de acordo com as possíveis posições na cena.

Instanciando as peças:
Vamos então finalmente instanciar as peças na cena que irão representar as partes da textura recortada.
Crie um método sem retorno com o nome "CreatePiece" e o chame dentro do Start do script.
Agora vamos instanciar os prefabs de acordo com a quantidade de peças.
Para isso, vamos criar dois laços de repetições aninhados (igual fizemos com o CreatePositions), assim, nós iremos percorrer as linhas e colunas da textura como um todo.
Vamos representar o primeiro laço pela obtenção das linhas da imagem(esse contador será chamado de 'i') e o segundo laço, as colunas da imagem (contador com o nome de 'j'), para representarmos uma matriz ixj.
Se começarmos o laço 'i' do zero, ele vai começar da última linha da imagem, ou seja, a primeira parte da imagem vai ser obtida a partir da última linha da mesma, então se quisermos que a primeira parte da imagem seja obtida a partir da sua primeira linha, devemos iniciar esse laço de trás pra frente, assim a imagem será recortada de cima para baixo da esquerda para a direita.
Então crie uma variável local chamada "start" e nela armazene "amoutPieces" -1.
Agora crie o laço "i" iniciando de start, e tendo seu delimitador até que "i" seja maior ou igual a 0, recebendo um decremento de um em um.
Já dentro do laço 'i' com o laço 'j', sua definição é mais simples: basta o iniciar a partir do 0, e se delimitar até que seu valor seja menor que "amoutPieces", recebendo o incremento de um em um:



Vamos finalmente criar o prefab contendo a parte da textura cortada, crie uma variável local chamada "texture" e a faça receber a chamada da nossa função "CropTexture", passando como argumento j e i, respectivamente.
Faça com que a variável "position" receba a chamada da nossa função "RandomPosition" para que seja sorteada uma posição para esse prefab, e por fim, faça com que uma variável local chamada "quad" receba a instância do prefab "piecePrefab", na posição "position".



Agora faremos com que esse prefab tenha como imagem (sprite) o pedaço da peça cortada que está armazenada na variável "texture":
Através da variável "quad" pegue o componente "SpriteRenderer" do objeto e acesse sua propriedade "sprite". Através do comando "Sprite.Create" crie um novo sprite para esse prefab, passando como argumento a textura que irá ser criada (no caso, a variável "texture" que contém a parte cortada da imagem), e um novo retângulo que irá definir a dimensão desse sprite, ou seja, 0 e 0 para a imagem ser obtida a partir do canto inferior esquerdo, com as dimensões de "texture" para a largura e altura, e após definir o retângulo, defina o pivot dessa imagem, recomendo que seja central, sendo um novo Vector2 com os valores 0.5 e 0.5 (centro da imagem):



Vamos então ajustar o colisor do sprite para que fique do tamanho da textura cortada, para isso, através da variável "quad" obtenha o componente "BoxCollider2D"e faça acesso à propriedade "size", dentro dela armazene um novo Vector2 com "X" sendo o valor de "distancePieces.x" e "Y" sendo "distancePieces.y":



Salve o script.
Agora crie um novo objeto vazio em cena com o nome de "GameManager" e nele adicione o script "CropTexture".
Através do inspector selecione um tamanho qualquer para a grade, anexe a imagem que deseja ser recortada para o quebra-cabeças e o prefab da peça:



Teste o jogo e em tempo de execução ajuste a posição e campo de visão da câmera.
Perceba que as peças são instanciadas de acordo com a grade selecionada, em diversos pedaços, um do lado do outro:


Exemplo de grade 4x4 com 16 peças.


Exemplo de grade 2x2 com 4 peças.

Criando a grade:
Com a instanciação das peças, vamos então criar a instância da grade, para que cada parte da imagem seja encaixada corretamente.
Para tal, crie através de um editor de imagens de sua preferência um quadrado com a resolução 150x150(por exemplo), transparente e com uma borda interna cinza com 2 pixels de espessura. Salve a imagem em formato .png com o nome de "quad", a imagem será mais ou menos assim:



Importe a imagem para o projeto.
Agora crie um novo Sprite na cena com o nome de "Grid", contendo apenas um BoxCollider2D com a opção "Is Trigger" marcada, tendo como "sprite" a imagem que acabara de importar. Faça com que esse objeto se torne um prefab.

Vamos então voltar para o script "CropTexture" e criar a variável "gridPrefab" como pública:



Após ter feito isso, crie um método sem retorno chamado "CreateGrid" que vai receber três parâmetros: dois inteiros chamados "j" e "i" e um GameObject chamado "quad".

Crie uma variável local chamada "grid" e a faça receber o Instantiate contendo o prefab "gridPrefab", um novo Vector2 recebendo em "x" a variável 'j' multiplicando a distância das peças em X e subtraindo esse valor por 10 (fazendo com que a grade seja instanciada com 10 pixels de distância das peças), e em "y" a variável 'i' multiplicando a distância das peças em Y.
Vamos também criar uma variável local chamada "newScale" para determinar a nova escala desse sprite de acordo com a grade das peças, fazendo com que essa variável receba um novo Vector2 em "x" sendo a resolução da peça em x dividida pela largura do sprite "quad" (que no caso é 150) e em "y" a resolução da peça em y também dividida por 150.
Agora é só aplicar "newScale" em x e y para "localScale" de "grid":



Agora chame o método "CreateGrid" dentro do método "CreatePiece" após criar a peça passando como argumento as variáveis "j" e "i" e também "quad":




Salve o script e através do inspector anexe o prefab "grid" na variável "gridPrefab" no GameManager. Teste o jogo:



Exemplo de uma grade criada a partir de 9 peças (grade 3x3).




Se gostaram do tutorial deixem o feedback, e postarei a próxima parte.
Obrigada se leu até aqui, até mais!

Excelente tutorial, May o/
Está postando tudo no canal/github ou possui um site?

Citação de: Sotelie online 16/01/2019 às 16:42
Excelente tutorial, May o/
Está postando tudo no canal/github ou possui um site?
Valeu!  :XD:
Então, ando postando esses projetinhos no git, mas também tenho o blog: https://compilemosfacil.blogspot.com/