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

Desenvolvendo um jogo de quiz em C#

Iniciado por MayLeone, 24/01/2018 às 22:49

Olá, no tutorial de hoje nós vamos aprender a desenvolver uma aplicação de QUIZ onde a pergunta é um texto e as alternativas são imagens, fazendo com que o jogador possa clicar numa das imagens para acertar (ou não) a resposta.
Este quiz pode ser desenvolvido com qualquer tema que você desejar, tanto de cunho educativo quanto de entretenimento ou conhecimentos gerais, o importante é que a lógica mostrada nesta postagem poderá ser utilizada para qualquer situação em que se deseje criar um quiz.
Como eu pessoalmente gosto do tema de bandeiras de países do mundo, o quiz será ilustrado com este tema, mas tenha em mente que você pode fazer com o que quiser.

Neste tutorial veremos: Métodos , modificadores de parâmetros, laço de repetição, coleções, uso de bibliotecas, classe Random e eventos.
Também veremos como novidade: Cópia de listas para outras listas, manipulação de recursos em tempo de execução, e o uso de delegados.



Design do formulário:
Como de costume no início de tudo tutorial prático que fazemos através do WindowsForms, desenvolveremos o design de formulário.
Deixe o formulário com dimensões e cor de fundo de sua preferência, o texto de acordo com o nome do seu quiz, a propriedade "StartPosition" em ''CenterScreen'' e oculte o "MaximizeBox".
Os componentes do seu formulário serão simples: Apenas 4 pictureboxes com a propriedade "Cursor" em "Hand" (ou qualquer outro de sua preferência) e "SizeMode" em "StretchImage" para que todas as imagens fiquem do mesmo tamanho. Também terá duas labels, onde o texto, fonte e tamanho das mesmas ficarão a seu critério, a primeira com o nome de "lblPais" e a outra "lblContador".
O resultado será mais ou menos este:



As variáveis:
Agora com o design pronto, dê F7 no teclado e vá para a janela de codificação do formulário.
Dentro da classe do mesmo crie uma constante do tipo short que irá armazenar a quantidade total de imagens dentro do seu quiz (no meu caso são 192 bandeiras).
Também crie 3 variáveis do tipo inteiro, uma armazenará os acertos, a outra os erros,  e a outra as rodadas que se passaram no jogo.
Teremos uma variável do tipo string que guardará as respostas, uma do tipo Bitmap que guardará a imagem da bandeira correta, a instância da classe Random, uma lista de Pictureboxes para guardamos nossas pictureboxes, 2 listas de string para guardar as perguntas (uma delas pra cópia) e mais três listas de bitmap, uma para representar a imagem da bandeira correta, e as outras duas para as alternativas erradas (sendo uma delas a cópia).
Essas cópias servirão para quando o jogo for reiniciado, as listas principais voltem a ter todos os conteúdos novamente, visto que durante o jogo as perguntas e bandeiras corretas que serão sorteadas serão removidas da lista para evitar repetições, então quando o jogo se iniciar de novo, essas listas alteradas voltarão a conter todo seu conteúdo, sem as remoções:



Importando e carregando recursos gráficos:
Vamos agora importar as nossas bandeiras para o projeto:
1º Vá em "Solution Explorer">> "Properties" e com o lado direito do mouse clique em "Open";
2º Na janela que abrir vá para a guia "Resources" e em "Add Resource" clica na seta;
3º Vá à opção "Add existing file" e selecione todos os arquivos de imagens que você deseja importar para o projeto;
4º É importante que o nome da imagem seja a resposta do quiz, por exemplo, a imagem da bandeira do Afeganistão possui o nome de "Afeganistão":

Spoiler
[close]

Isso porque quando formos exibir a pergunta da bandeira na tela, podemos exibir diretamente o nome do arquivo (que também servirá como verificação de resposta) assim não precisamos criar uma coleção que armazene essas respostas separadamente, onde coloríamos cada uma manualmente, uma por uma.
5º Após importar as imagens, feche essa janela e clique em "Yes";
6º Volte ao código.

Nesse momento podemos adicionar todos nossos arquivos de imagens em nossa lista de Bitmaps.
Crie um método privado e sem retorno com o nome de "CarregarRecursos" para realizarmos esta tarefa.
Para adicionar um recurso gráfico do diretório "Resources" em qualquer coleção, basta indicar seu caminho, seguido do nome do arquivo:



Perceba que este caminho é até válido e você de fato vai adicionar as imagens em sua lista, mas como no meu caso tenho 192 imagens para adicionar, se eu for fazer dessa forma irei ter um trabalho muito grande fazendo uma a uma, manualmente.
O que podemos fazer para acabar com este problema é importar todos os recursos de uma vez para a lista, através de um laço de repetição, dessa forma, os recursos serão adicionados à lista em tempo de execução (runtime).

Para isso, precisamos obter acesso ao diretório "Resources", portanto precisamos utilizar três bibliotecas: System.Resources, System.Globalization e System.Collections:



A primeira biblioteca nos permitirá ter acesso às classes de "Resources". A classe de globalização nos permitirá obter a cultura do arquivo (necessário em um dos métodos da classe "ResourceSet") e a biblioteca "Collections" nos permitirá utilizar a entrada de dicionário, para obtermos o nome do arquivo (Key) e o seu valor (Value).

Dentro do método que criamos, podemos fazer acesso ao diretório de recursos utilizando o "ResourceManager" que possui o método "GetResourceSet". Este método nos permite o acesso aos recursos contidos no diretório. Ele nos pedirá três argumentos: O primeiro é a cultura do arquivo, que podemos obter através da classe "CultureInfo" e sua propriedade "CurrentUICulture" e dois valores booleanos que devem ser "true".
O primeiro é para carregar o arquivo se ele não tiver sido carregado e o outro é para obtermos um "fallback" caso o arquivo não consiga ser encontrado no diretório.
Esse método nos retorna um tipo "ResourceSet", por isso devemos definir uma variável de retorno com este tipo:



Agora que temos acesso ao diretório, podemos iniciar um laço foreach pelo mesmo, fazendo com que a variável de passagem seja do tipo "DictionaryEntry", assim podemos obter as propriedades "Key" e "Value" do arquivo.
Faça com que uma variável chamada "nomeArquivo" receba a propriedade Key do mesmo, e uma variável chamada "imagemArquivo" receba seu valor.
Precisamos agora validar se o arquivo é de fato um Bitmap (imagem), porque se você tiver outros arquivos em "Resources" você irá causar erros ao adicioná-los à lista, então precisamos garantir que ele seja um Bitmap.
Dentro da validação, adicione então a variável "imagemArquivo" (convertida para Bitmap) para a lista "Bandeiras" e "Alternativas" e a variável "nomeArquivo" na lista "perguntas".
Após o término deste laço, faça com que a lista "alternativasCopia" receba a lista "Alternativas" e "perguntasCopia" receba "perguntas":



Como a label irá exibir o nome do arquivo como pergunta, alguns nomes que contém espaços vão exibir o nome da seguinte forma, por exemplo: "África_do_Sul", ou seja, no lugar onde deveríamos ter espaços teremos underlines (_).
Isso ocorre pois o Visual Studio irá substituir os caracteres de espaços nos nomes dos arquivos por underlines.
Para acabar com este pequeno problema, crie um método chamado "RetirarEspaco" que receba como parâmetro um ref de string.
Dentro desse método valide se a string passada como parâmetro possui um caractere de "underline", e caso sim, utilize o método "Replace" para trocar o caractere underline, por espaço.
Como este método retorna uma string, defina a string passada como parâmetro como o próprio recebedor do retorno:



Agora no método "IniciarRecursos" antes de adicionar "nomeArquivo" à lista, chame o método "RetirarEspaco" e passe como argumento justamente o nome do arquivo:



Lembrando que utilizamos o modificador "ref" para passar a string como referência, assim seu valor é alterado através do método "Replace" fora da função.

Depois de tudo isso, você pode chamar o seu método "IniciarRecursos" dentro do construtor do formulário, após a chamada de ''InitializeComponents".
Caso você perceba que a aplicação demore alguns segundos para rodar (pela quantidade de arquivos sendo carregados) e utilize o C# 5.0, você pode fazer com que esse método seja assíncrono.

Sorteando a bandeira:
Agora que carregamos todos os recursos gráficos no projeto e os adicionamos em suas devidas listas, podemos iniciar as verificações das bandeiras e sortear uma resposta.
Como iremos trabalhar com as 4 pictureboxes, podemos desenvolver um método que inicialize as mesmas dentro do vetor:



Nós podemos conferir se a picturebox clicada é a resposta certa através de sua tag, por isso é importante que quando elas sejam reiniciadas todas elas voltem a conter tag vazia.

Após o laço, podemos fazer com que a lista de "Alternativas" receba sua cópia (sem que as imagens tenham sido removidas), e chamar o método "SortearBandeira":



Criaremos agora o método responsável por sortear uma bandeira (a bandeira correta) de acordo com a lista.
Crie este método sem retorno e sem parâmetros, com o nome de "SortearBandeira''.
Por ser realizado um novo sorteado, incremente a variável "rodadas" e atualize sua label.
Através da instância de "Random", sorteie aleatoriamente um valor através do método "Next" que seja do tamanho da lista de "Bandeiras" e armazene este valor sorteado numa variável chamada "rand".
Faça com que a variável "resposta" receba o elemento da lista "perguntas" na posição de "rand", e que a variável "bandeiraCerta" receba o elemento da lista "Bandeiras" na posição também de ''rand".
"lblPais.Text" deve receber o conteúdo de "resposta" e após tudo isso, remova das listas "Bandeiras" e "perguntas" o conteúdo sorteado, para evitar repetições.
Agora precisamos exibir em alguma das 4 pictureboxes a bandeira correta, pois devemos ter certeza que a alternativa certa irá aparecer na tela do jogo.
Para isso, sorteie uma das 4 pictureboxes também utilizando a variável "rand" e o método "Next" da classe Random, e faça com que a propriedade "Image" da picturebox sorteada de acordo com o vetor "pictures" na posição de "rand" receba o conteúdo de "bandeiraCerta" e que sua tag receba o conteúdo de "resposta".
Após tudo isso, remova também da lista "Alternativas" o conteúdo "bandeiraCerta", para não correr o risco de possuir duas alternativas com a mesma resposta (repetições):



Isso tudo foi feito para sortearmos a bandeira correta, mas precisamos agora sortear nas 3 pictureboxes restantes as bandeiras erradas.
Crie um método sem retorno e sem parâmetros com o nome de "SortearAlternativas".
Dentro do método, realize um laço foreach que percorra todas as pictureboxes do controle do formulário e verifique se a tag dessa picturebox é diferente de "resposta". Caso for, faça com que a propriedade "Image" dessa picturebox seja uma bandeira aleatória da lista "Alternativas" e depois remova-a da lista:



Agora não se esqueça de chamar este método no final do método "SortearBandeiras" e de chamar o método "IniciarPbs" após chamar o método "IniciarRecursos" no construtor da classe.

Teste o debug e perceba que a cada vez que é iniciado o jogo, as bandeiras são sorteadas aleatoriamente, e a resposta (a imagem da bandeira correta) é exibida numa picturebox aleatória:



Clicando na bandeira:
Precisamos também de um método que permite que cliquemos nas bandeiras para checarmos se acertamos ou não a resposta.
Para isso, crie o método com a assinatura de um evento click, ou seja, sem retorno e com dois parâmetros, um do tipo "object" e outro do tipo "EventArgs".
Também armazene dentro de uma variável chamada "pic" a referência da picturebox clicada, ou seja, fazendo com que o parâmetro do tipo "object" seja armazenado nesta variável e seja devidamente convertido para "Picturebox".
Para verificar se a pessoa acertou a bandeira, basta checar sua tag. Caso a tag da picturebox for igual ao conteúdo de "resposta" a pessoa acertou, ou seja, a variável "acertos" recebe um incremento e uma mensagem será exibida dizendo que a pessoa acertou.
Caso tenha errado, podemos incrementar a variável "erros" e mostrar ao usuário qual é a resposta certa, ou seja, fazendo com que a picturebox que contenha a imagem da bandeira certa fique visível e as outras fiquem desabilitadas:



Não se esqueça de selecionar as 4 pictureboxes do formulário, ir à opção "Events" e em "Click" escolher a chamada do método "ClicarImagem".

Teste o debug e tente acertar. Veja que ao clicar na bandeira correta, a mensagem dizendo que você acertou é mostrada na tela, do contrário, todas as pictureboxes que contém as imagens das bandeiras erradas ficam invisíveis e só fica à mostra a imagem da picturebox que contém a bandeira correta:



Verificando o final do jogo:
Também precisamos de um método que verifique se o jogo terminou. Caso ele tenha terminado, é exibida uma mensagem dizendo a quantidade de erros e acertos do jogador, e perguntando se ele quer jogar novamente, fazendo com que as listas modificadas voltem a conter seus conteúdos novamente, reiniciando o jogo.
Caso o jogo ainda não ter terminado, é preciso sortear uma nova bandeira e uma nova pergunta:

Spoiler
[close]

Não se esqueça de chamar este método ao final da função "ClicarImagem".
E a lógica do aplicativo está pronta! Mas e se quisermos deixar o código menos repetitivo?

Utilizando Delegates:
Perceba que em vários momentos do nosso código nós utilizamos o laço foreach com a mesma sintaxe, ou seja:
foreach(var pb in Controls.OfType<Picturebox>()){}

Dentro deste laço nós fazemos validações e verificações através de "pb" para manipular alguma picturebox do controle, correto?
E se nós criarmos apenas um único laço foreach com esta sintaxe dentro de um método que aceite como parâmetro um outro método? Assim, dentro deste laço (escrito uma única vez) nós podemos chamar algum método específico que desejarmos (que foi passado como parâmetro), ou seja, para manipular as pictureboxes do controle, sortear as bandeiras das alternativas erradas e ainda validar quais as bandeiras ficarão visíveis no formulário!
Ou seja, precisamos de três métodos com assinaturas similares (para poder manipulá-los através de delegates).
A assinatura seria a seguinte: Modificador de acesso privado, sem retorno, e que aceita como parâmetro uma picturebox (que seria justamente a variável de passagem do laço):



Perceba que o método "SortearAlternativas" que estávamos utilizando anteriormente, já possui as instruções que desejamos, isto significa que não precisamos recriá-lo a partir do método "SortearAlternativaErrada", basta adicionar a este método um parâmetro do tipo picturebox, e eliminar o laço:



Para o método "ManipularPbs" basta realizar a rotina que ela estava realizando no método "IniciarPbs", porém sem a presença do laço foreach:



E o método "MostrarRespostaCorreta" ganha o conteúdo de quando estamos validando as respostas erradas através da picturebox, para deixá-la com a propriedadade "Visible" em "false":



Veja que são procedimentos simples, mas estavam se utilizando de um código repetitivo (o laço foreach com mesma sintaxe) para realizar suas tarefas.

Agora que separamos os métodos que serão invocados através de um delegate no projeto, vamos desenvolver o método que conterá de fato o laço foreach que será definido uma única vez.
Este laço apenas chamará o método passado como parâmetro, apenas isso.
O método para ser passado como parâmetro precisa conter sua referência armazenada dentro de um delegate.
Você pode optar por definir um delegate através da palavra-chave "delegate", ou pode optar por definir este parâmetro como um "Action" genérico, que nada mais é que um delegate personalizado, que nos poupa o trabalho de defini-lo dentro do escopo do projeto.
Como todas as assinaturas de nossos métodos não contém retorno (void) podemos utilizar a classe "Action" para tal tarefa, porém, se nossos métodos tivessem retorno, deveríamos então utilizar a classe "Func".
Todos nossos métodos tem como assinatura um parâmetro do tipo Picturebox, correto? Então nosso método do laço ficaria assim:



Neste momento nós temos um método que realiza um laço foreach (com a sintaxe que tínhamos anteriormente) mas que aceita como parâmetro um método! Esse método não tem retorno (por isso utilizamos a classe "Action") e seu parâmetro é uma Picturebox.
Dentro do laço a única coisa que fazemos é chamar o método que é passado como parâmetro, e o seu argumento é do tipo Picturebox, que é justamente "pb", ou seja, a variável de passagem do laço.

Agora para fazer o método funcionar, chame-o pelo nome mesmo (LacoForeach) e passe como argumento o método que deseja chamar para aquela situação:

Spoiler




[close]

Note que dessa forma, muitos dos seus métodos ficaram menores e mais legíveis, o laço foreach foi definido apenas uma vez, e o projeto continua com seu funcionamento da mesma forma que a anterior, só que agora de uma forma mais organizada.

Finalização:
E aqui chegamos a mais um final de tutorial! Espero que tenham gostado de aprender como importar recursos para o projeto e adicioná-los a coleções de uma forma dinâmica, e de uma boa utilização dos delegates.
Até a próxima.