Este tutorial ensina como criar o jogo Fifteen-Puzzle do zero usando C++ e SFML, com foco nas melhorias de UI e no sistema de dicas. Vamos explorar o código-fonte principal (main.cpp
) passo a passo, explicando cada componente e sua função.
A Estrutura do Código (main.cpp
)
O arquivo main.cpp
contém toda a lógica do jogo, desde a inicialização da janela e dos elementos gráficos até o manuseio das interações do usuário e a renderização na tela.
1. Inclusões e Namespace
Começamos incluindo as bibliotecas necessárias. SFML/Graphics.hpp
é fundamental para todas as operações gráficas e de janela. iostream
é para entrada/saída básica, e vector
, random
, algorithm
são usados para o embaralhamento das peças.
#include <SFML/Graphics.hpp> // Funcionalidades gráficas e de janela
#include <iostream> // Entrada/saída de console
#include <vector> // Para usar std::vector
#include <random> // Para geração de números aleatórios
#include <algorithm> // Para usar std::shuffle
using namespace sf; // Simplifica o uso de classes e funções da SFML
2. Estados do Jogo (GameState
)
Para gerenciar as diferentes telas e comportamentos do jogo (menu, jogabilidade), utilizamos um enum
simples chamado GameState
. Isso permite que o programa saiba em qual "modo" ele está operando.
enum GameState { MENU, GAME };
MENU
: O jogo está na tela inicial, onde o jogador pode escolher "Play" ou "Exit".
GAME
: O jogador está ativamente jogando o quebra-cabeça.
3. Embaralhamento do Tabuleiro (shuffleGrid
)
A função shuffleGrid
é responsável por randomizar a disposição das peças no tabuleiro no início de cada nova partida. Ela garante que o quebra-cabeça seja sempre diferente e solucionável.
void shuffleGrid(int grid[6][6]) {
std::vector<int> numbers;
for (int i = 1; i <= 15; ++i) {
numbers.push_back(i); // Adiciona números de 1 a 15
}
std::random_device rd; // Gera uma semente aleatória baseada no hardware
std::mt19937 g(rd()); // Motor de números aleatórios Mersenne Twister
std::shuffle(numbers.begin(), numbers.end(), g); // Embaralha a ordem dos números
int k = 0;
for (int i = 1; i <= 4; ++i) {
for (int j = 1; j <= 4; ++j) {
if (i == 4 && j == 4) {
grid[i][j] = 16; // A última posição (4,4) é o espaço vazio
} else {
grid[i][j] = numbers[k++]; // Preenche o grid com os números embaralhados
}
}
}
}
Explicação:
Um std::vector<int> numbers
é criado e preenchido com os valores de 1 a 15.
std::random_device
e std::mt19937
são usados para gerar uma sequência de números aleatórios de alta qualidade.
std::shuffle
reorganiza os elementos do vetor numbers
aleatoriamente.
O tabuleiro (grid
) é então preenchido com esses números embaralhados. A posição grid[4][4]
(que corresponde à última célula do tabuleiro 4x4) é reservada para o espaço vazio, representado pelo número 16
.
4. Verificação de Solução (isSolved
)
A função isSolved
verifica se o jogador conseguiu organizar todas as peças na ordem correta. Ela percorre o tabuleiro e compara o valor de cada peça com o valor que deveria estar naquela posição em um tabuleiro resolvido.
bool isSolved(int grid[6][6]) {
int k = 1; // Valor esperado para a peça na posição atual
for (int i = 1; i <= 4; ++i) {
for (int j = 1; j <= 4; ++j) {
if (k == 16) k = 0; // O valor 16 (espaço vazio) não é verificado
if (grid[i][j] != k && k != 0) {
return false; // Se uma peça estiver fora de lugar, o puzzle não está resolvido
}
k++; // Incrementa o valor esperado para a próxima posição
}
}
return true; // Se todas as verificações passarem, o puzzle está resolvido
}
Explicação:
A variável k
atua como um contador, representando o valor esperado para a peça na célula atual (começando de 1).
O loop itera por todas as células do tabuleiro.
Se k
for 16 (o valor do espaço vazio), ele é temporariamente ajustado para 0 para que a comparação seja ignorada para o espaço vazio.
Se o valor da peça atual (grid[i][j]
) não corresponder ao valor esperado (k
), a função retorna false
.
Se o loop for concluído sem encontrar nenhuma peça fora de lugar, o tabuleiro está resolvido e a função retorna true
.
5. Sistema de Dicas (findHintMove
)
A função findHintMove
é a inteligência por trás do sistema de dicas. Ela localiza o espaço vazio e, em seguida, procura por uma peça adjacente a ele que esteja fora de sua posição final correta. Se tal peça for encontrada, suas coordenadas são retornadas como uma dica.
Vector2i findHintMove(int grid[6][6]) {
int emptyX = -1, emptyY = -1;
// 1. Encontra a posição do espaço vazio (valor 16)
for (int i = 1; i <= 4; ++i) {
for (int j = 1; j <= 4; ++j) {
if (grid[i][j] == 16) {
emptyX = i;
emptyY = j;
break;
}
}
if (emptyX != -1) break;
}
// 2. Define as direções para verificar peças adjacentes (direita, esquerda, baixo, cima)
int dx[] = {0, 0, 1, -1};
int dy[] = {1, -1, 0, 0};
for (int i = 0; i < 4; ++i) {
int nx = emptyX + dx[i]; // Coordenada X da peça adjacente
int ny = emptyY + dy[i]; // Coordenada Y da peça adjacente
// 3. Verifica se a peça adjacente está dentro dos limites do tabuleiro
if (nx >= 1 && nx <= 4 && ny >= 1 && ny <= 4) {
int tileValue = grid[nx][ny]; // Valor da peça adjacente
// 4. Calcula o valor correto que deveria estar na posição (nx, ny)
// A fórmula (nx - 1) * 4 + ny converte coordenadas 1-baseadas (nx, ny)
// para o valor esperado em um tabuleiro resolvido (1 a 16).
if (tileValue != (nx - 1) * 4 + ny) {
return Vector2i(nx, ny); // Retorna a posição da peça que pode ser movida
}
}
}
return Vector2i(-1, -1); // Nenhuma dica útil encontrada
}
Explicação:
A função primeiro localiza as coordenadas (emptyX, emptyY)
do espaço vazio.
Em seguida, ela itera sobre as quatro direções possíveis (dx
, dy
) para encontrar peças adjacentes.
Para cada peça adjacente, ela verifica se a peça está dentro dos limites do tabuleiro.
A parte crucial é a condição tileValue != (nx - 1) * 4 + ny
. Isso compara o valor atual da peça com o valor que ela deveria ter se o tabuleiro estivesse resolvido. Se eles não corresponderem, e a peça puder ser movida para o espaço vazio, ela é considerada uma dica.
Se nenhuma dica for encontrada, Vector2i(-1, -1)
é retornado.
6. Função Principal (main
)
A função main
é o ponto de entrada do programa. Ela inicializa a janela, carrega recursos, configura os elementos da UI e contém o loop principal do jogo, que gerencia eventos, atualiza o estado do jogo e renderiza tudo na tela.
int main() {
// 1. Inicialização da Janela
RenderWindow app(VideoMode(256, 350), "15-Puzzle!"); // Cria a janela do jogo
app.setFramerateLimit(60); // Define o limite de quadros por segundo
GameState gameState = MENU; // O jogo começa no estado de menu
// 2. Carregamento da Fonte
Font font;
if (!font.loadFromFile("fonts/Carlito-Regular.ttf")) {
return -1; // Erro se a fonte não puder ser carregada
}
// 3. Configuração dos Textos da UI (Menu, Vitória, Dica)
// Cada texto é configurado com sua string, fonte, tamanho, cor, origem (para centralização) e posição.
Text playText("Play", font, 50);
// ... (configurações de playText, exitText, winText, backToMenuText, hintText) ...
// 4. Carregamento da Textura das Peças
Texture t;
t.loadFromFile("images/15.png"); // Carrega a imagem que contém todas as peças
int w = 64; // Largura/altura de cada peça (64x64 pixels)
int grid[6][6] = {0}; // Representação interna do tabuleiro (com bordas para simplificar cálculos)
Sprite sprite[20]; // Array de sprites, um para cada peça (1 a 16)
// 5. Inicialização dos Sprites das Peças
int n=0;
for (int i=0; i<4; i++)
for (int j=0; j<4; j++) {
n++;
sprite[n].setTexture(t); // Define a textura para o sprite
// Define qual parte da imagem '15.png' corresponde a esta peça
sprite[n].setTextureRect( IntRect(i*w,j*w,w,w) );
grid[i+1][j+1]=n; // Preenche o grid inicial em ordem (para referência)
}
// Variáveis para o sistema de dicas
Vector2i hintedTile(-1, -1); // Armazena a posição da peça sugerida (-1,-1 se nenhuma)
Clock hintClock; // Cronômetro para controlar a duração da dica
float hintDuration = 2.0f; // Duração da dica em segundos
// 6. Loop Principal do Jogo
while (app.isOpen()) { // O loop continua enquanto a janela estiver aberta
Event e;
while (app.pollEvent(e)) { // Processa todos os eventos pendentes
if (e.type == Event::Closed){
app.close(); // Fecha a janela se o botão 'X' for clicado
}
// 7. Manuseio de Eventos de Mouse (Movimento e Clique)
// Lógica para efeitos de hover (mudar cor do texto ao passar o mouse)
if (e.type == Event::MouseMoved) {
// ... (lógica de hover para playText, exitText, backToMenuText, hintText) ...
}
// Lógica para cliques do mouse
if (e.type == Event::MouseButtonPressed) {
if (e.key.code == Mouse::Left) { // Se o botão esquerdo do mouse for clicado
Vector2i mousePos = Mouse::getPosition(app); // Posição do clique
if (gameState == MENU) {
// Transição para o estado GAME ao clicar em "Play"
if (playText.getGlobalBounds().contains(mousePos.x, mousePos.y)) {
gameState = GAME;
shuffleGrid(grid); // Embaralha o tabuleiro para iniciar o jogo
}
// Fecha o aplicativo ao clicar em "Exit"
else if (exitText.getGlobalBounds().contains(mousePos.x, mousePos.y)) {
app.close();
}
} else if (gameState == GAME) { // Se estiver no estado de jogo
if (isSolved(grid)) { // Se o puzzle estiver resolvido
// Volta para o menu se "Back to Menu" for clicado
if (backToMenuText.getGlobalBounds().contains(mousePos.x, mousePos.y)) {
gameState = MENU;
}
}
// Ativa a dica ao clicar em "Hint"
else if (hintText.getGlobalBounds().contains(mousePos.x, mousePos.y)) {
hintedTile = findHintMove(grid); // Encontra a dica
hintClock.restart(); // Reinicia o cronômetro da dica
}
// Lógica de movimento das peças do quebra-cabeça
else {
int x = mousePos.x/w + 1; // Coluna clicada (1-baseada)
int y = mousePos.y/w + 1; // Linha clicada (1-baseada)
int dx=0; // Deslocamento em X para o espaço vazio
int dy=0; // Deslocamento em Y para o espaço vazio
// Verifica se a peça clicada é adjacente ao espaço vazio (16)
if (grid[x+1][y]==16) { dx=1; dy=0; }; // Espaço vazio à direita
if (grid[x][y+1]==16) { dx=0; dy=1; }; // Espaço vazio abaixo
if (grid[x][y-1]==16) { dx=0; dy=-1; }; // Espaço vazio acima
if (grid[x-1][y]==16) { dx=-1; dy=0; }; // Espaço vazio à esquerda
// Se a peça clicada pode se mover (dx ou dy não são 0)
if (dx != 0 || dy != 0) {
int n = grid[x][y]; // Valor da peça clicada
grid[x][y] = 16; // A posição da peça clicada se torna o espaço vazio
grid[x+dx][y+dy] = n; // A peça clicada se move para a posição do espaço vazio
// Animação do movimento da peça
sprite[16].move(-dx*w,-dy*w); // Move o sprite do espaço vazio na direção oposta
float speed=3; // Velocidade da animação
for (int i=0; i<w; i+=speed) { // Loop para animar o movimento
sprite[n].move(speed*dx,speed*dy);
app.draw(sprite[16]);
app.draw(sprite[n]);
app.display();
}
}
}
}
}
}
}
// 8. Lógica de Renderização (Desenho na Tela)
app.clear(Color::White); // Limpa a tela com a cor branca
if (gameState == MENU) {
app.draw(playText); // Desenha o texto "Play"
app.draw(exitText); // Desenha o texto "Exit"
} else if (gameState == GAME) {
// Desenha as peças do quebra-cabeça
for (int i=0; i<4; i++) {
for (int j=0; j<4; j++) {
int n = grid[i+1][j+1]; // Obtém o valor da peça na posição
sprite[n].setPosition(i*w,j*w); // Define a posição do sprite na tela
app.draw(sprite[n]); // Desenha o sprite da peça
}
}
// Se o jogo estiver resolvido, exibe a mensagem de vitória e o botão "Back to Menu"
if (isSolved(grid)) {
app.draw(winText);
app.draw(backToMenuText);
}
else {
app.draw(hintText); // Caso contrário, exibe o botão "Hint"
}
// Desenha o destaque da dica se houver uma dica ativa e dentro do tempo
if (hintedTile.x != -1 && hintClock.getElapsedTime().asSeconds() < hintDuration) {
RectangleShape hintRect(Vector2f(w, w)); // Cria um retângulo para o destaque
hintRect.setFillColor(Color::Transparent); // Fundo transparente
hintRect.setOutlineThickness(5); // Espessura da borda
hintRect.setOutlineColor(Color::Yellow); // Cor da borda
// Define a posição do destaque sobre a peça sugerida
hintRect.setPosition((hintedTile.x - 1) * w, (hintedTile.y - 1) * w);
app.draw(hintRect); // Desenha o destaque
}
}
app.display(); // Exibe o que foi desenhado na tela (troca o buffer)
}
return 0; // O programa termina com sucesso
}