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

Criando um jogo da memória em C#

Iniciado por MayLeone, 13/01/2018 às 17:10

Antes de mais nada, este tutorial já tem uns meses que foi escrito, eu pensei tê-lo postado aqui, mas acabei por descobrir que não, então estarei fazendo agora.



Introdução:
Nesta postagem trago um texto que tem como principal foco ser curto e prático separado em sete tópicos, de como criar um jogo da memória em C# utilizando a aplicação de formulário.
Além de ser um jogo popular e todos saberem as regras, ainda é algo interessante em se desenvolver em linguagem de programação pois aborda assuntos já estudados em aulas teóricas como: Instância de classes, métodos, vetores, tipagem de dados, listas, estrutura condicional, conversões explícitas, aleatoriedade, propriedades de objetos, eventos, laço de repetição e muita lógica.
Veja como é um prato cheio colocar na prática tudo o que foi ensinado em teoria em uma única aplicação.



1) Design de formulário:
1- Crie um novo projeto chamado "JogoDaMemoria" e abra seu formulário;
2- Altere a propriedade "text" do formulário para "Jogo da memória";
3- Ajuste seu tamanho na opção "size" com as dimensões que desejar;
4- Deixe a propriedade "StartPosition" em "Centerscreen" e "Maximize box" em 'false;'
5- Clique com o lado direito do mouse sobre o nome do projeto através do "Solution Explore" e vá em >> Properties >> Resourcers >> Add Resource >> Add Existing file >> E selecione as imagens que você vai utilizar no jogo. *Guarde bem o nome da imagem que você vai utilizar como verso. No meu caso, o nome é "verse";
6- Feche essa janela, clique em "Yes" e altere o ícone e a imagem de fundo do formulário, caso desejar, com as imagens que importou;
7- Arraste através do "toolbox" as PictureBoxes que você vai utilizar. Traga ao formulário a quantidade de pictureboxes equivalente à quantidade de cartas do seu jogo (incluindo os pares);
8- Coloque as imagens dentro das pictureboxes e ajuste a opção "Size mode" para "AutoSize";
9- Altere a tag dessas pictureboxes em ordem crescente, iniciando do zero (0).
*Os pares de imagens devem ter o mesmo índice de tag do seu respectivo par;
10- Arraste do toolbox uma "label" para registrar os movimentos dos jogadores: Essa label deve ter o texto "Movimentos:", tamanho, fonte e cor de sua preferência, cor de fundo transparente e deve ter o nome de "lblMove". Ao fazer tudo isso, seu formulário deve estar mais ou menos assim:



2) Programando - Início do jogo:
Dê F7 no teclado e abra o código .cs do seu programa.
Dentro da classe do formulário crie quatro variáveis do tipo inteiro: Uma vai registrar os movimentos do jogo, a outra vai gerenciar a quantidade de cliques feitos nas cartas, a terceira vai armazenar quantos pares foram encontrados para checar se o jogo acabou e a última vai guardar a tag das pictureboxes para que possamos checar os pares das cartas e para gerenciar o resgate de imagens dessas pictureboxes.
Crie também duas listas do tipo inteiro: A primeira vai guardar as coordenadas das cartas no formulário através do eixo X, e a outra as coordenadas no eixo Y, para que mais tarde possamos sortear valores aleatórios nessas posições para "embaralhar" as cartas.
Também crie um vetor do tipo "Image" para que possamos guardar as imagens das pictureboxes antes delas serem viradas para baixo, assim poderemos resgatar essa imagem quando clicarmos na picturebox de referência. O tamanho deste vetor vai depender da quantidade de cartas que você quer colocar no jogo (desconsiderando os pares).
Agora defina um método privado sem retorno com o nome de "InitializeVerse" para que possamos virar as cartas para baixo assim que o jogo iniciar e para guardar suas posições dentro das listas.
Dentro do método, inicialize uma varredura nos controles do jogo, especificando o tipo "PictureBox" (pois queremos apenas acessar os campos dessa classe no formulário) através do laço "foreach".
Faça uma conversão de "String Format" para inteiro na tag da picturebox que está sendo referenciada na variável do laço, atribuindo essa conversão para a variável de indexação de tags.
Coloque dentro do vetor de imagens na posição de indexação da tag, a propriedade da imagem da picturebox referenciada.
Agora faremos com que o verso da imagem fique à mostra: Atribua à propriedade de imagem da picturebox o caminho da imagem importada: "Properties.Resourcers.verse"
Deixe verdadeira a propriedade "Enable" da picturebox, e faça duas condições para checar se as coordenadas X e Y daquela pciturebox referenciada ainda não existem nas listas de coordenadas. Caso não existir, adicione essas posições em suas respectivas listas.
Fora do laço, chame um método que iremos criar com o nome de "RandomPositions" e chame este método que criamos agora dentro da inicialização do formulário:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace JogoDaMemoria
{
public partial class Form1 : Form
{
int movements, clicks, foundCards, tagIndex;
List xLocations = new List();
List yLocations = new List();
Image[] images = new Image[9];
public Form1()
{
InitializeComponent();
InitializeVerse();
}
private void InitializeVerse() {
foreach (var pb in Controls.OfType<PictureBox>()) {
tagIndex = int.Parse(String.Format("{0}", pb.Tag));
images[tagIndex] = pb.Image;
pb.Image = Properties.Resources.verse;
pb.Enabled = true;
if (!xLocations.Contains(pb.Location.X)) {xLocations.Add(pb.Location.X);}
if (!yLocations.Contains(pb.Location.Y)) {yLocations.Add(pb.Location.Y);}
}
RandomPositions();
}
}
}





3) Randomizado posições:
Crie o método com o nome que chamamos acima, sem retorno e privado.
Novamente adicione um laço foreach a realizar uma varredura pelos controles do tipo "PictureBox" e dentro do laço instancie a classe "Random".
Crie duas variáveis que irão armazenar o sorteio de um elemento das listas de posições, obviamente que este sorteio deve ocorrer utilizando o método "Next" da instância de Random, e dentro do índice das listas.
Crie uma nova lista dentro da classe do formulário, essa lista será do tipo string.
Faça uma condição que verifique se os números sorteados em X e em Y (concatenados e convertidos para string) existem nessa lista que criamos.
Caso essas coordenadas já tenham sido sorteadas, utilize o comando "goto" e o leve para uma etiqueta com o nome de "Repeat". Coloque a etiqueta nos comandos de sorteio, para que assim sejam sorteadas novas coordenadas para a picturebox referenciada.
Criando um 'else' nessa condição, significando que essas coordenadas não existem na lista, então coloque a picturebox nessa posição através da propriedade "Location" e se utilizando da estrutura "Point", passando como argumentos as duas coordenadas que receberam os valores sorteados. Após fazer isso, adicione à lista esses valores para que eles não se repitam.
private void RandomPositions() {
foreach (var pb in Controls.OfType<PictureBox>()) {
Random rnd = new Random();
Repeat:
var X = xLocations[rnd.Next(0, xLocations.Count)];
var Y = yLocations[rnd.Next(0, yLocations.Count)];
if (locationsRegister.Contains(X.ToString() + Y.ToString())) { goto Repeat; }
else
{
pb.Location = new Point(X, Y);
locationsRegister.Add(X.ToString() + Y.ToString());
}
}
}





4) Clicando e revelando imagens:
Vamos criar agora outro método privado, sem retorno com o nome de "ImagesReference_Click" contendo como parâmetros o object sender e EventsArgs e, para que possamos criar o evento de clique nas pictureboxes.
Para funcionar corretamente, colocamos a referência da picturebox clicada dentro de uma variável do tipo "PictureBox" (não esquecendo de realizar o casting para este objeto) e logo em seguida fazemos um incremento na variável "clicks".
Vamos resgatar a imagem dentro da picturebox através do vetor de imagens: Atribua à propriedade "Image" do objeto referenciado a imagem no vetor "images" de acordo com a tag daquela picturebox. Antes disso porém, converta novamente em String.Formart para inteiros a tag da picturebox e armazene na variável "TagIndex".
Não se esqueça de dar um "refresh" nesta picturebox, para que atualize corretamente o conteúdo de sua imagem e de deixar como "false" sua propriedade "Enable".
Volte ao design do formulário, selecione todas suas pictureboxes, vá para a aba "Events" e na opção "Click" referencie o método "ImagesReference_Click" que acabamos de criar.
Teste o debug do formulário e vá clicando sobre as pictureboxes: Perceba que todas elas resgatam as imagens que tinham armazenadas anteriormente e que também estão distribuídas de forma aleatória, cada vez que você roda novamente a depuração.



private void ImagesRerefence_Click(object sender, EventArgs e) {
PictureBox pic = (PictureBox)sender;
clicks++;
tagIndex = int.Parse(String.Format("{0}", pic.Tag));
pic.Image = images[tagIndex];
pic.Refresh();
pic.Enabled = false;
}





5) Verificando pares:
Agora precisamos checar se o jogador acertou ou não as cartas reveladas.
Vamos gerenciar primeiramente quais cartas de fato que o jogador revelou e para isso vamos utilizar as tags dessas pictureboxes, ou seja, se as tags das mesmas forem iguais, o jogador acertou o par, caso contrário, as cartas serão viradas pro verso novamente.
Criaremos na classe do formulário um vetor com tamanho 2(dois) do tipo inteiro para guardar em uma de suas posições a tag das duas cartas reveladas no momento.
Crie uma condição que verifique se a variável "clicks" é igual a 1 (um), caso sim, armazene na posição zero deste vetor a tag da picturebox clicada, caso não, armazene na posição 1(um) deste vetor esta tag.
Também incremente a variável "movimentos", atualize a sua label de registro e através de uma variável local do tipo "bool" checaremos se os pares são iguais, ou seja, se o conteúdo do vetor "tags'' na posição 0 é igual ao conteúdo deste vetor na posição 1.
Após isso, chame um método "TurnCards" e passe como argumento a variável boolena.

private void ImagesRerefence_Click(object sender, EventArgs e) {
PictureBox pic = (PictureBox)sender;
clicks++;
tagIndex = int.Parse(String.Format("{0}", pic.Tag));
pic.Image = images[tagIndex];
pic.Refresh();
pic.Enabled = true;
if (clicks == 1) { tags[0] = tagIndex; } else {
tags[1] = tagIndex;
movements++;
lblMove.Text = "Movimentos " + movements.ToString();
bool check = tags[0] == tags[1]; 
TurnCards(check);
}
}




6) Desvirando cartas:
Nesse momento precisamos verificar através do retorno booleano se o jogador acertou as cartas viradas ou não.
Para isso, crie um novo método privado, sem retorno , com o nome de "TurnCards" e que aceita como parâmetro um booleano.
Para saber de fato quais eram as pictureboxes reveladas, precisamos novamente fazer uma varredura com o laço foreach nos controles das pictureboxes.
Faremos também uma verificação se a tag daquelas pictureboxes referenciadas for igual ao conteúdo do vetor de tags, tanto na posição 0(zero) quanto na posição 1(um). Caso sim, então saberemos quais cartas estão reveladas.
Tendo essa confirmação, podemos enfim checar se a variável booleana retornou true (acertou os pares) ou false (não acertou).
Se acertou os pares, a carta continua com a imagem revelada, sem alterar para seu verso e a variável "foundCards" recebe um incremento.
Caso não tenha acertado os pares, a variável mostra seu verso novamente e volta como "true" sua propriedade "Enable", e após tudo isso fora do laço, a variável "clicks" volta a ser zero, chamando também um método que verifica se o jogo acabou.
Note que ao depurar (comentando a chamada do método), quando os pares são errados as cartas se revelam muito rapidamente e não há tempo suficiente para visualizar as mesmas.
Acabe com este problema referenciando a biblioteca "System.Threading" no topo do formulário. Volte ao método "TurnCards" e antes do laço chame o procedimento estático "Sleep" da classe "Thread" e passe como argumento o tempo de espera para as cartas virarem. Eu deixei 500, mas isso vai de sua opinião, quanto maior o número, mais tempo de espera.

private void TurnCards(bool check) {
Thread.Sleep(500);
foreach (var pb in Controls.OfType<PictureBox>()) {
if (int.Parse(String.Format("{0}", pb.Tag)) == tags[0] ||
int.Parse(String.Format("{0}", pb.Tag)) == tags[1]){
if (check) { foundCards++; } else {
pb.Image = Properties.Resources.verse;
pb.Enabled = true;
}
}
}
clicks = 0;
EndGame();
}





7) Jogo terminado:
Vamos criar o último método do programa que irá checar se o jogo já se encerrou através da variável "foundCards".
Crie este método como privado e sem retorno com o nome que chamamos: "EndGame"
Faça uma verificação que valide se "foundCards" é igual ao tamanho do vetor de imagens (que guarda as cartas) vezes dois (para considerar os pares encontrados).
Dentro dessa validação exiba uma mensagem ao usuário através do "MessageBox" o congratulando por ter terminado o jogo e o pergunte se deseja jogar novamente, utilizando a classe MessageBoxButtons com a propriedade "YesNo".
Caso o jogador escolha sim, zere as variáveis "clicks", "movements" e "foundCards", limpe a lista de registros de coordenadas e chame novamente o método "InitializeVerse", para que o jogo recomece.
Caso o jogador não queira mais jogar, chame o método "Exit" através da classe "Application" para fechar o formulário automaticamente.

private void EndGame(){
if (foundCards == (images.Length*2)) {
MessageBox.Show("Parabéns, você terminou o jogo!", "Parabéns");
DialogResult msg = MessageBox.Show("Deseja jogar novamente?!", "Parabéns", MessageBoxButtons.YesNo);
if (msg == DialogResult.Yes)
{
clicks = 0; movements = 0; foundCards = 0; locationsRegister.Clear(); lblMove.Text = "Movimentos:"; InitializeVerse();
}
else
{
Application.Exit();
}
}
}


E o jogo está pronto para ser jogado!