Capítulo 03 — Quebrando nosso programa de propósito
Série: Do zero ao jogo comercial com C++ e SFML
Erros de compilação não são inimigos: vamos aprender a ler, investigar, corrigir e validar nossos primeiros problemas em C++
No capítulo anterior fizemos algo fundamental.
Pegamos nosso pequeno programa:
#include <iostream>
int main()
{
std::cout << "Game Engineering com C++ começou!" << std::endl;
return 0;
}
e deixamos de enxergá-lo apenas como um conjunto de símbolos que precisávamos copiar.
Começamos a entender o papel de:
#include
<iostream>
int
main
()
{}
std
::
cout
<<
std::endl
;
return 0
Também realizamos alguns pequenos experimentos.
Removemos um ;.
Alteramos <iostream>.
Retiramos uma chave.
Observamos o Visual Studio reclamar.
Corrigimos.
E continuamos.
Hoje vamos transformar essa experiência em um processo consciente.
Porque existe uma habilidade que acompanhará praticamente toda a nossa jornada em Game Engineering:
saber investigar quando alguma coisa deixa de funcionar.
Quando estivermos trabalhando com um único main.cpp, os problemas serão pequenos.
Quando tivermos uma Game Engine com rendering, input, áudio, física, resources, scenes, gameplay, ferramentas e testes, os problemas poderão ser consideravelmente mais difíceis.
A metodologia, entretanto, começa agora.
1. Hoje queremos que o Build falhe
Sim.
Nos capítulos anteriores, nosso objetivo era encontrar:
Build succeeded.
Hoje vamos procurar propositalmente:
Build FAILED.
Parece estranho.
Mas existe uma razão.
Quando estamos começando a programar, um erro frequentemente provoca esta reação:
“Eu fiz alguma coisa errada.”
Depois:
“Como faço essa mensagem desaparecer?”
Essa segunda pergunta é perigosa.
Porque podemos começar a modificar coisas aleatoriamente até o código voltar a funcionar.
Uma pergunta muito melhor é:
O que essa mensagem está tentando me dizer?
E depois:
Qual é a causa do problema?
Esse será nosso foco.
2. Nosso laboratório continua propositalmente pequeno
Não criaremos nenhuma nova classe.
Nenhuma pasta.
Nenhum sistema.
Continuamos apenas com:
CppGameEngine/
└── main.cpp
E com nosso programa conhecido:
#include <iostream>
int main()
{
std::cout << "Game Engineering com C++ começou!" << std::endl;
return 0;
}
Antes de quebrá-lo, faça:
Build
↓
Build Solution
ou:
Ctrl + Shift + B
Confirme que o estado inicial é válido.
Queremos partir de:
Build succeeded.
Isso é importante.
3. Nunca investigue sem conhecer o estado inicial
Imagine que você altere cinco coisas e depois descubra que o projeto não compila.
Qual delas causou o problema?
Talvez:
Alteração A
Alteração B
Alteração C
Alteração D
Alteração E
Uma delas.
Duas delas.
Todas.
Ou talvez o problema já existisse antes.
Por isso nossa experiência de hoje seguirá:
Estado conhecido e funcionando
↓
Uma alteração
↓
Build
↓
Observação
↓
Investigação
↓
Correção
↓
Build
↓
Validação
Essa disciplina parece exagerada para cinco linhas de código.
Não será exagerada quando estivermos investigando um bug dentro de uma Game Engine.
4. Experimento 1 — o famoso ponto e vírgula
Nosso código válido possui:
std::cout << "Game Engineering com C++ começou!" << std::endl;
Vamos remover somente:
;
Teremos:
#include <iostream>
int main()
{
std::cout << "Game Engineering com C++ começou!" << std::endl
return 0;
}
Não altere mais nada.
Agora faça Build.
5. Não corrija imediatamente
O Visual Studio provavelmente destacará o problema.
A Error List poderá apresentar uma ou mais mensagens.
A janela Output também exibirá informações relacionadas ao processo de Build.
Sua primeira reação talvez seja:
“Já sei. Está faltando
;."
E está.
Mas não coloque de volta ainda.
Queremos aprender a trabalhar como investigadores.
Observe:
qual arquivo aparece;
qual linha é indicada;
qual código de diagnóstico aparece;
qual descrição foi fornecida;
quantos erros foram relatados;
se existem warnings;
o que aparece na janela Output.
Dependendo da versão do Visual Studio, MSVC e das configurações utilizadas, os diagnósticos exatos podem variar.
Por isso não queremos decorar uma mensagem específica.
Queremos aprender a interpretar diagnósticos.
6. O que é um diagnóstico do compilador?
Quando o compilador encontra algo incompatível com as regras que consegue processar, ele produz informações para nos ajudar a localizar o problema.
Conceitualmente:
Código-fonte
↓
Compilador
↓
Problema encontrado
↓
Diagnóstico
O diagnóstico poderá conter informações como:
arquivo
linha
coluna
código
descrição
Algo conceitualmente parecido com:
main.cpp
linha 5
error ...
expected ';'
A mensagem real pode variar.
O importante é começar a enxergá-la como informação técnica, não como uma tela vermelha que precisa desaparecer.
7. Error List versus Output
No Visual Studio existem duas áreas que começaremos a consultar com frequência.
Error List
A Error List organiza diagnósticos encontrados pelo ambiente e pelo processo de desenvolvimento.
Ela facilita visualizar:
erros;
warnings;
mensagens;
arquivos;
linhas.
É extremamente conveniente.
Mas existe outra janela importante.
Output
Durante o Build, a janela Output mostra informações produzidas pelo processo de construção.
Procure:
Show output from:
Build
À medida que nossa jornada avançar, essa janela ficará cada vez mais importante.
Quando chegarmos a problemas de compilação, linking e configuração, não dependeremos apenas dos sublinhados vermelhos do editor.
8. O editor pode saber antes do Build?
Você provavelmente já percebeu que o Visual Studio consegue marcar alguns problemas enquanto digitamos.
Isso acontece porque a IDE possui mecanismos de análise que trabalham enquanto editamos o código.
Mas precisamos diferenciar conceitualmente:
Editor / análise da IDE
de:
Build real do projeto
Às vezes o editor poderá mostrar algo antes de compilarmos.
Em outros casos, o diagnóstico decisivo aparecerá durante o Build.
Essa distinção será particularmente importante quando começarmos a trabalhar com:
headers;
bibliotecas;
configurações;
linker;
diferentes targets;
Debug;
Release.
Por enquanto, guarde:
um sublinhado vermelho no editor e um erro produzido durante o Build estão relacionados, mas não são necessariamente a mesma fonte de diagnóstico.
9. Voltando ao nosso erro
Temos:
std::cout << "Game Engineering com C++ começou!" << std::endl
return 0;
O compilador precisa determinar onde termina uma instrução e começa outra.
Nós removemos o terminador necessário.
Então o problema não é:
“returnestá errado."
O problema nasceu na instrução anterior.
Isso nos leva a uma lição importante.
10. A linha indicada nem sempre é a causa real
Imagine:
std::cout << "Game Engineering com C++ começou!" << std::endl
return 0;
O compilador continua analisando o código depois de std::endl.
Ele encontra:
return
e percebe que a construção anterior não foi encerrada como esperado.
Dependendo do diagnóstico, podemos receber uma indicação próxima à linha seguinte.
Isso significa que devemos distinguir:
Lugar onde o compilador percebeu o problema
de:
Lugar onde nós realmente causamos o problema
Eles podem coincidir.
Mas nem sempre.
Essa ideia será extremamente importante futuramente.
11. Causa versus sintoma
Vamos introduzir dois conceitos simples:
CAUSA
↓
algo incorreto aconteceu
SINTOMA
↓
algo observável apareceu por causa disso
Em nosso exemplo:
CAUSA
↓
ponto e vírgula removido
SINTOMA
↓
compilação falhou
E ainda podemos ter:
CAUSA
↓
uma única construção inválida
SINTOMAS
↓
vários diagnósticos posteriores
Quando trabalharmos com bugs reais, procurar a causa raiz será muito mais valioso do que atacar sintomas isoladamente.
12. Corrigindo o primeiro problema
Agora sim.
Restaure:
std::cout << "Game Engineering com C++ começou!" << std::endl;
Faça Build novamente.
Queremos retornar a:
Build succeeded.
Mas não terminamos.
A correção não está completa apenas porque editamos o código.
Precisamos validar.
13. Correção e validação não são a mesma coisa
Nosso processo foi:
Problema
↓
Investigação
↓
Alteração
Ainda falta:
Validação
Faça:
Ctrl + Shift + B
Depois:
Ctrl + F5
Confirme que:
o projeto compila;
o executável inicia;
a mensagem esperada aparece.
Agora podemos dizer que nossa correção foi validada.
Esse hábito será essencial quando estivermos corrigindo bugs maiores.
14. Nossa primeira metodologia de investigação
Acabamos de executar:
1. Reproduzir
↓
2. Ler
↓
3. Localizar
↓
4. Criar hipótese
↓
5. Corrigir
↓
6. Compilar novamente
↓
7. Executar
↓
8. Validar
Essa ainda é uma metodologia simples.
Vamos refiná-la ao longo da série.
Mas ela já é muito superior a:
deu erro
↓
mudar coisas aleatoriamente
↓
funcionou
15. Experimento 2 — quebrando o nome do header
Voltamos ao estado válido:
#include <iostream>
int main()
{
std::cout << "Game Engineering com C++ começou!" << std::endl;
return 0;
}
Agora altere somente:
#include <iostream>
para:
#include <iostreax>
Faça Build.
Não existe um header padrão chamado iostreax disponível dessa forma em nosso ambiente.
Observe o diagnóstico.
16. O problema agora é diferente
No primeiro experimento, o compilador conseguiu chegar à instrução dentro de main, mas encontrou uma construção sintaticamente problemática.
Agora temos outra situação.
Estamos solicitando:
#include <iostreax>
e o arquivo solicitado não pode ser encontrado nos caminhos utilizados para esse tipo de inclusão.
Conceitualmente:
#include
↓
procure iostreax
↓
não encontrado
↓
processamento não pode continuar normalmente
Isso nos mostra algo importante:
mensagens diferentes representam problemas diferentes.
Não queremos desenvolver o hábito de pesquisar apenas:
“Visual Studio C++ error fix”
Queremos identificar qual categoria de problema temos diante de nós.
17. Não chame tudo de “erro do C++”
Durante a série encontraremos problemas em diferentes partes do sistema.
Por exemplo:
Source Code
↓
Preprocessor
↓
Compiler
↓
Object Files
↓
Linker
↓
Executable
↓
Runtime
Além disso teremos:
Game Logic
Rendering
Resources
File System
Operating System
GPU
Audio
Network
Dizer:
“C++ deu erro.”
quase não fornece informação.
Progressivamente queremos dizer coisas como:
“O compilador rejeitou esta construção.”
ou:
“O header não foi encontrado.”
ou futuramente:
“A compilação terminou, mas o linker não encontrou a definição.”
ou:
“O executável foi gerado, mas falhou durante a execução.”
Vocabulário técnico melhora nossa capacidade de investigar.
18. Corrigindo o header
Restaure:
#include <iostream>
Faça Build.
Depois execute.
Voltamos ao estado conhecido:
Build succeeded
↓
programa executa
↓
saída correta
Agora podemos avançar para outro tipo de problema.
19. Experimento 3 — removendo o #include
Em vez de escrever um nome incorreto, remova completamente:
#include <iostream>
Teremos:
int main()
{
std::cout << "Game Engineering com C++ começou!" << std::endl;
return 0;
}
Faça Build.
Observe.
20. O código parece quase igual
Visualmente removemos apenas uma linha.
Mas as instruções:
std::cout
e:
std::endl
dependem de declarações disponibilizadas através do header apropriado em nosso programa.
Sem:
#include <iostream>
o compilador não possui o contexto necessário para utilizarmos esses nomes daquela maneira.
O erro nos ajuda a reforçar uma relação aprendida no capítulo anterior:
<iostream>
↓
declarações de I/O
↓
std::cout
std::endl
Não estamos apenas decorando que #include <iostream> deve ficar no topo.
Estamos observando por que nosso código atual precisa dele.
21. Dependência já existe em nosso pequeno programa
Ainda não estudamos arquitetura de dependências.
Mas nosso primeiro programa já possui uma dependência.
Ele utiliza funcionalidades da biblioteca padrão.
Podemos representar:
main.cpp
↓
iostream
Isso ainda é extremamente simples.
No futuro poderemos ter algo como:
Game
↓
Engine
↓
Rendering
↓
SFML
ou outras relações que descobriremos durante o projeto.
A ideia de dependência começa em coisas pequenas.
22. Experimento 4 — quebrando std
Restaure o código válido.
Agora altere:
std::cout
para:
stx::cout
Teremos:
#include <iostream>
int main()
{
stx::cout << "Game Engineering com C++ começou!" << std::endl;
return 0;
}
Faça Build.
Observe.
23. O compilador não corrige nossa intenção
Nós sabemos que queríamos:
std
e digitamos:
stx
Para um humano, parece óbvio.
Mas o compilador trabalha com aquilo que escrevemos.
Ele não deveria simplesmente substituir qualquer nome parecido pelo que imagina ser nossa intenção.
Imagine se tivéssemos legitimamente:
namespace stx
{
}
em algum contexto futuro.
Corrigir silenciosamente nomes seria extremamente perigoso.
Programação exige precisão.
24. Experimento 5 — quebrando cout
Agora restaure:
std::cout
e altere para:
std::coutx
Faça Build.
Observe novamente.
Temos o namespace correto:
std
mas estamos procurando um nome que não corresponde ao objeto que queríamos utilizar.
Podemos começar a perceber que diagnósticos carregam pistas sobre qual parte da expressão não pôde ser resolvida.
25. Experimento 6 — removendo uma aspas
Volte ao estado válido:
std::cout << "Game Engineering com C++ começou!" << std::endl;
Agora remova apenas a segunda aspas:
std::cout << "Game Engineering com C++ começou! << std::endl;
Faça Build.
Observe cuidadosamente.
Esse tipo de erro pode provocar diagnósticos aparentemente mais confusos do que o simples ponto e vírgula.
Por quê?
Porque o compilador precisa interpretar onde nosso literal textual termina.
Nós abrimos:
"
mas não fechamos corretamente.
Isso pode afetar a interpretação do restante da linha ou de partes posteriores do código.
26. Um erro pequeno pode afetar muito código
Esse princípio aparecerá várias vezes.
Um problema aqui:
linha 5
pode gerar sintomas aqui:
linha 5
linha 6
linha 7
linha 8
Por isso:
quantidade de mensagens não significa necessariamente quantidade de causas.
Se aparecerem dez erros depois de uma alteração, não comece corrigindo dez coisas.
Pergunte:
O que mudou?
Essa pergunta é extremamente poderosa.
27. “O que mudou?”
Imagine que o programa compilava cinco minutos atrás.
Agora não compila.
Uma das primeiras perguntas deveria ser:
O que mudou desde o último estado conhecido como funcionando?
Talvez:
adicionei uma linha
ou:
renomeei uma função
ou futuramente:
adicionei uma biblioteca
ou:
mudei de Debug para Release
ou:
troquei x64 por x86
ou:
atualizei uma dependência
Comparar estados é uma técnica fundamental de troubleshooting.
É também uma das razões pelas quais Git será tão valioso.
28. Git não serve apenas para guardar código
Imagine futuramente:
Commit A
↓
projeto funcionando
Commit B
↓
problema apareceu
A pergunta:
“O que mudou?”
pode ser respondida comparando os dois estados.
Git nos permitirá investigar:
arquivos modificados;
linhas alteradas;
commits;
histórico;
responsáveis;
regressões.
Ainda teremos uma fase específica para aprofundar Git.
Mas já conseguimos enxergar por que controle de versão é também uma ferramenta de engenharia e debugging.
29. Experimento 7 — removendo uma chave
Restaure o código.
Agora remova:
}
Teremos:
#include <iostream>
int main()
{
std::cout << "Game Engineering com C++ começou!" << std::endl;
return 0;
Faça Build.
Observe.
No capítulo anterior aprendemos que:
{
}
delimita o corpo da nossa função.
Abrimos:
{
e não fechamos.
Nossa estrutura ficou incompleta.
30. Delimitadores precisam estar balanceados
Esse conceito aparecerá repetidamente.
Por exemplo:
()
{}
[]
Em muitos contextos, estruturas abertas precisam ser fechadas adequadamente.
Quando nosso código crescer:
if (...)
{
while (...)
{
if (...)
{
}
}
}
perder uma chave poderá se tornar muito mais difícil de enxergar.
Indentação e formatação consistente ajudarão bastante.
31. O Visual Studio pode nos ajudar
Quando trabalhamos com chaves, o editor possui recursos visuais para mostrar correspondência entre delimitadores.
Também possui formatação e indentação automáticas.
Ferramentas são úteis.
Mas queremos compreender o conceito antes de depender completamente delas.
A ferramenta deve aumentar nossa capacidade.
Não substituir nosso entendimento.
32. Experimento 8 — escrevendo Return
Restaure a chave.
Agora altere:
return 0;
para:
Return 0;
Faça Build.
Observe.
C++ diferencia letras maiúsculas de minúsculas.
Ou seja:
return
e:
Return
não são a mesma coisa.
Isso é chamado de case sensitivity.
33. C++ é case-sensitive
Considere:
player
Player
PLAYER
Para C++, esses podem representar identificadores diferentes.
Essa característica terá impacto direto em nossas convenções de nomenclatura.
Ao longo da série utilizaremos código em inglês e seguiremos um padrão consistente.
Por exemplo, quando criarmos funções:
StartGame();
LoadResources();
CalculateDamage();
utilizaremos PascalCase.
Quando tivermos variáveis:
playerHealth
playerEnergy
currentScene
utilizaremos camelCase.
E quando criarmos constantes:
MAX_PLAYER_HEALTH
DEFAULT_WINDOW_WIDTH
DEFAULT_WINDOW_HEIGHT
utilizaremos UPPERCASE.
Ainda aprenderemos cada uma dessas construções quando chegar o momento apropriado.
Por enquanto, o importante é perceber que:
PlayerHealth
playerHealth
PLAYER_HEALTH
não são apenas diferenças visuais.
Em C++, a capitalização faz parte do identificador.
34. Nossa convenção começa antes das classes existirem
Não precisamos criar uma classe apenas para demonstrar PascalCase.
Mas já podemos registrar nossa direção:
Classes → PascalCase
Interfaces → PascalCase
Enums → PascalCase
Funções/Métodos → PascalCase
Variáveis → camelCaseConstantes → UPPERCASE
E todos os identificadores técnicos serão escritos em:
English
Enquanto nossas explicações continuarão em:
Português do Brasil
A consistência será importante quando o projeto crescer.
35. Experimento 9 — mudando main
Agora faça algo diferente.
Altere:
int main()
para:
int Main()
Observe que acabamos de aplicar PascalCase visualmente.
Mas existe um problema conceitual.
main não é uma função qualquer criada por nós.
Ela possui significado especial como ponto de entrada da aplicação hospedada.
Portanto, nossa convenção de funções não substitui regras da linguagem.
Isso é muito importante.
36. Convenção não vence sintaxe ou contrato
Podemos definir:
“Nossas funções utilizam PascalCase.”
Então criaremos futuramente:
LoadGame();
SaveGame();
UpdatePlayer();
RenderWorld();
Mas isso não significa renomear elementos especiais ou APIs externas para encaixá-los em nossa convenção.
Continuaremos utilizando:
main()
porque esse nome possui significado definido pela linguagem.
Da mesma forma, futuramente utilizaremos APIs da biblioteca padrão ou da SFML respeitando seus nomes originais.
Nossa convenção governa o código que nós nomeamos.
Não reescreve C++.
37. Erro de sintaxe
Já encontramos vários exemplos.
Podemos começar a utilizar um termo:
erro de sintaxe.
Sintaxe envolve as regras estruturais utilizadas para escrever construções válidas da linguagem.
Por exemplo:
return 0;
é válido.
Algo como:
return 0
em determinado contexto pode produzir problema por faltar o terminador esperado.
Ou:
std::cout << "Hello;
possui um literal malformado.
Uma analogia imperfeita, mas útil, seria pensar na gramática de uma língua.
As palavras podem existir, mas a maneira como são organizadas também precisa obedecer regras.
38. Nem todo problema será de sintaxe
Agora chegamos a uma distinção fundamental.
Considere:
#include <iostream>
int main()
{
std::cout << "Player coins: 500" << std::endl;
return 0;
}
O código pode compilar perfeitamente.
Mas imagine que, segundo nossa regra de jogo, o jogador deveria possuir:
50 moedas
e não:
500 moedas
O compilador não necessariamente possui como saber disso.
Para ele:
"Player coins: 500"
é um literal válido.
Temos:
Sintaxe válida
↓
Build succeeded
↓
Programa executa
↓
Resultado errado para nossa regra
Isso é outra categoria de problema.
39. Bug não significa necessariamente Build failed
Essa ideia é essencial.
Podemos ter:
Build succeeded
e ainda possuir um software incorreto.
Imagine futuramente:
Jogador possui 100 moedas
Compra item de 30
Saldo apresentado: 130
O programa:
compilou;
linkou;
abriu;
executou;
mas a regra está errada.
Temos um bug lógico.
O compilador verifica várias regras da linguagem.
Ele não conhece automaticamente todas as regras do nosso jogo.
40. O compilador não conhece nosso Game Design
Imagine:
Semente custa: 10
Jogador possui: 50
Compra: 1 semente
Saldo esperado: 40
Se nossa implementação produzir:
Saldo: 60
o compilador não vai necessariamente dizer:
“Alexander, sua economia está errada.”
Para o compilador, as operações utilizadas podem ser perfeitamente válidas.
Quem conhece a regra é nosso software — porque nós a implementamos — e quem precisa validá-la somos nós através de:
raciocínio;
testes;
debugging;
QA;
playtesting;
telemetria;
dependendo do problema.
É por isso que Game Engineering envolve muito mais do que simplesmente fazer o código compilar.
41. Uma primeira classificação de problemas
Ainda não conhecemos tecnicamente todas essas categorias.
Mas podemos construir um mapa inicial:
Problemas de desenvolvimento
│
├── Compilação
│
├── Linking
│
├── Runtime
│
└── Lógica
Mais adiante esse mapa crescerá.
Hoje trabalhamos principalmente com problemas percebidos durante compilação.
Ainda não estamos prontos para estudar profundamente linker e runtime.
Eles terão seus próprios capítulos.
42. Compiler Error versus Warning
Você também encontrará:
Error
e:
Warning
Eles não são equivalentes.
De maneira inicial:
Error
Problema que impede aquela compilação de ser concluída com sucesso.
Warning
O compilador conseguiu continuar, mas detectou algo potencialmente problemático ou digno de atenção.
Não interprete warning como:
“Pode ignorar.”
Essa é uma péssima prática.
Um warning merece investigação.
43. “Mas o programa funciona mesmo com warning”
Essa frase aparecerá muitas vezes durante a vida de um desenvolvedor.
E precisa ser tratada com cuidado.
Algo funcionar em:
meu computador
+
meu input
+
minha configuração
+
este momento
não prova que está correto.
Warnings podem revelar:
conversões perigosas;
perda de informação;
código suspeito;
uso incorreto;
problemas de portabilidade;
construções potencialmente inseguras.
Mais adiante estabeleceremos políticas de warnings para nosso projeto.
Por enquanto:
warning é informação técnica que deve ser compreendida, não ruído que deve ser escondido.
44. Não corrija código apenas para silenciar o compilador
Imagine que recebemos um warning.
Uma abordagem ruim seria procurar qualquer alteração que faça a mensagem desaparecer.
Queremos perguntar:
Por que apareceu?
↓
Qual risco está indicando?
↓
Meu código está realmente correto?
↓
Qual correção expressa melhor a intenção?
Isso evita soluções do tipo:
“coloquei um cast e o warning sumiu.”
Talvez o cast seja correto.
Talvez apenas tenhamos escondido um problema.
Engenharia exige compreender a diferença.
45. Erros em cascata
Vamos voltar a um fenômeno que já observamos.
Um único erro pode gerar muitos diagnósticos.
Considere conceitualmente:
Erro real na linha 10
↓
parser perde contexto
↓
linha 11 parece errada
linha 12 parece errada
linha 13 parece errada
linha 14 parece errada
Agora temos:
5 mensagens
mas talvez apenas:
1 causa
Isso é conhecido informalmente como uma cascata de erros.
46. Por onde começar quando existem muitos erros?
Uma estratégia inicial:
observe os primeiros diagnósticos;
procure a primeira região onde o código deixa de fazer sentido;
verifique alterações recentes;
corrija uma causa provável;
faça Build novamente;
veja quantos diagnósticos desapareceram.
Não tente necessariamente corrigir do último erro para o primeiro.
O último pode ser apenas consequência do primeiro.
47. Experimento 10 — criando uma cascata
Faça uma cópia do estado válido antes.
Depois transforme:
std::cout << "Game Engineering com C++ começou!" << std::endl;
em uma construção propositalmente malformada, por exemplo removendo aspas e terminadores de maneira controlada.
Faça Build.
Observe quantos diagnósticos aparecem.
Depois restaure apenas a primeira causa que você introduziu.
Faça Build novamente.
Veja quantas mensagens desaparecem.
Não queremos memorizar os códigos.
Queremos observar o comportamento do compilador diante de código malformado.
48. Um erro por vez durante o aprendizado
Durante nossos experimentos, prefira:
1 alteração
↓
1 observação
em vez de:
15 alterações
↓
caos
Essa abordagem também ajuda quando estamos aprendendo uma API.
Futuramente, ao integrar SFML, não faremos simultaneamente:
configurar include
+
configurar libraries
+
criar Window
+
criar Game Loop
+
carregar Texture
+
criar Sprite
+
adicionar Audio
e só então tentar compilar.
Vamos validar etapas.
Isso reduz drasticamente o espaço de investigação.
49. Reproduzir o problema
Uma habilidade central em debugging é conseguir responder:
Eu consigo reproduzir o problema?
Nosso erro do ponto e vírgula é 100% reproduzível.
Sempre que temos:
std::cout << "Hello" << std::endl
return 0;
o código está incorreto daquela maneira.
Mas bugs futuros poderão ser mais difíceis:
acontece somente em Release
ou:
acontece depois de trocar de mapa
ou:
acontece quando o inventário está cheio
ou:
acontece depois de jogar durante 40 minutos
ou:
acontece somente em determinada máquina
Reprodução é uma parte essencial da investigação.
Estamos treinando isso em escala microscópica.
50. Reduzindo o problema
Outra técnica extremamente poderosa é reduzir.
Imagine futuramente um sistema com milhares de linhas.
Se conseguirmos transformar um bug complexo em:
int main()
{
// menor código capaz de reproduzir o problema
}
a investigação fica muito mais fácil.
Esse conceito é frequentemente chamado de criar um minimal reproducible example ou exemplo mínimo reproduzível.
Nosso projeto atual já é praticamente isso.
Cinco linhas são um excelente laboratório.
51. Leia a mensagem antes de pesquisar
Quando aparecer um erro, resista à sequência:
copiar código do erro
↓
Google
↓
Stack Overflow
↓
copiar primeira resposta
Pesquisa é uma ferramenta excelente.
Documentação é essencial.
Comunidades são extremamente úteis.
IA também poderá ajudar.
Mas primeiro:
leia a mensagem.
Pergunte:
o que ela diz?
qual arquivo?
qual linha?
qual símbolo?
qual etapa?
o que eu alterei?
Depois pesquise com contexto.
52. Pesquisar melhor é consequência de compreender melhor
Compare:
"C++ error help"
com:
"MSVC expected semicolon before return"
ou futuramente:
"MSVC unresolved external symbol x64 Debug"
Quanto melhor conseguimos classificar o problema, melhor conseguimos pesquisar.
Conhecimento técnico melhora até nossa capacidade de fazer perguntas.
53. E quando usarmos IA?
A mesma regra vale.
Uma pergunta ruim:
“Meu C++ não funciona. Arrume.”
Uma pergunta muito melhor:
“Estou compilando um projeto C++20 com MSVC no Visual Studio 2022 em Debug x64. Após esta alteração recebo este diagnóstico. Este é o código mínimo reproduzível. Explique a causa antes de sugerir a correção.”
Percebe a diferença?
IA não substitui capacidade de diagnóstico.
Um desenvolvedor que sabe fornecer contexto consegue extrair muito mais valor das ferramentas.
54. Não esconda o erro ao pedir ajuda
Quando alguém solicitar ajuda, envie a mensagem real.
Evite:
“Apareceu um negócio vermelho.”
Ou:
“Diz alguma coisa sobre símbolo.”
Ou:
“Está dando erro de DLL.”
A mensagem exata contém informação.
Mais adiante aprenderemos a coletar:
mensagem
stack trace
logs
configuration
environment
steps to reproduce
expected result
actual result
Isso também faz parte da comunicação técnica.
55. Nosso primeiro processo formal de troubleshooting
Podemos agora definir uma versão inicial do processo que utilizaremos durante a série:
1. IDENTIFICAR
O que está errado?
2. REPRODUZIR
Consigo fazer acontecer novamente?3. OBSERVAR
Qual é a mensagem ou comportamento?4. LOCALIZAR
Onde o problema aparece?5. COMPARAR
O que mudou desde o estado funcionando?6. CRIAR HIPÓTESE
Qual pode ser a causa?7. TESTAR A HIPÓTESE
Faça uma alteração controlada.8. CORRIGIR
Resolva a causa.9. VALIDAR
Build + execução + comportamento esperado.10. PREVENIR
Como evitar ou detectar isso futuramente?
Esse processo crescerá.
Mas já podemos começar a utilizá-lo.
56. “Prevenir” parece exagerado para um ;
Para nosso ponto e vírgula, talvez sim.
Mas imagine futuramente:
Bug
↓
save corrompido
↓
jogador perdeu 60 horas de progresso
Corrigir o bug é necessário.
Mas também perguntaremos:
Como impedir que isso aconteça novamente?
Talvez a resposta envolva:
teste automatizado;
validação;
backup;
versionamento do save;
checksum;
transação;
logging;
monitoramento.
Um bug pode produzir uma melhoria sistêmica.
Essa mentalidade começa agora.
57. Debugging não é adivinhação
Existe uma diferença enorme entre:
"acho que é isso"
e:
"minha hipótese é esta e vou executar um teste que pode confirmá-la ou rejeitá-la"
Debugging profissional é muito mais próximo da segunda abordagem.
Temos:
Observação
↓
Hipótese
↓
Experimento
↓
Evidência
↓
Conclusão
É quase um pequeno método científico aplicado ao software.
58. Exemplo: formulando uma hipótese
Imagine:
#include <iostream>
int main()
{
std::cout << "Hello" << std::endl
return 0;
}
Observação:
Build falhou.
Vemos um diagnóstico próximo à transição entre as duas instruções.
Hipótese:
A instrução anterior não foi terminada corretamente.
Experimento:
std::cout << "Hello" << std::endl;
Novo Build:
Build succeeded.
Evidência favorável à hipótese.
Isso é muito melhor que adicionar símbolos aleatoriamente.
59. Mas cuidado com falsas conclusões
Imagine que fizemos três alterações simultaneamente:
A
B
C
e o problema desapareceu.
Qual delas resolveu?
Não sabemos.
Talvez:
A resolveu
B não fez nada
C criou outro problema ainda invisível
É por isso que alterações pequenas e controladas são tão valiosas durante investigação.
60. O futuro debugger do Visual Studio
Até agora estamos usando “debugging” em sentido amplo: investigar problemas.
O Visual Studio também possui um debugger, uma ferramenta específica que futuramente nos permitirá:
definir breakpoints;
pausar execução;
avançar linha por linha;
inspecionar variáveis;
observar call stack;
analisar memória;
acompanhar fluxo;
avaliar expressões.
Ainda não temos variáveis interessantes nem fluxo suficiente para explorar toda essa ferramenta.
Quando surgir a necessidade, aprenderemos.
Novamente:
problema primeiro, ferramenta depois.
61. Compiler Error não precisa de breakpoint
Nosso programa nem chega a ser gerado corretamente quando existe determinado erro de compilação.
Portanto, colocar um breakpoint não resolverá um problema que impede a construção do executável.
Precisamos entender em qual etapa estamos.
Essa distinção ficará muito clara quando aprofundarmos:
Source
↓
Preprocessor
↓
Compiler
↓
Linker
↓
Executable
↓
Runtime
Cada etapa possui tipos diferentes de problemas e ferramentas.
62. Game Engineering e troubleshooting
Pode parecer que estamos passando muito tempo falando de um ponto e vírgula.
Mas imagine alguns dos problemas que eventualmente poderemos encontrar:
Texture não carrega
Sprite não aparece
Input não responde
Player atravessa parede
NPC fica preso
Save não abre
Audio toca duas vezes
Frame Time dispara
Memory cresce continuamente
Release funciona diferente de Debug
DLL não é encontrada
Linker não encontra símbolo
A reação não pode ser:
“vou mexer até funcionar.”
Precisamos construir disciplina de investigação antes de os problemas ficarem grandes.
63. Um bug visual também precisa de hipótese
Imagine futuramente:
“O personagem desapareceu.”
Possíveis hipóteses:
Texture não carregou?
Sprite está fora da câmera?
Posição está errada?
Scale virou zero?
Objeto não foi renderizado?
Scene errada está ativa?
Resource foi destruído?
Alpha está zero?
O sintoma é um:
personagem não aparece
As causas possíveis são muitas.
Debugging é o processo de reduzir esse espaço até encontrar evidência suficiente.
64. Logs entrarão justamente por causa disso
Hoje nosso projeto é tão pequeno que conseguimos observar praticamente tudo.
No futuro, talvez precisemos saber:
Qual Scene carregou?
Qual Resource falhou?
Qual arquivo foi procurado?
Qual estado o Player possui?
Quanto tempo uma operação levou?
Qual sistema iniciou primeiro?
É aí que um sistema de Logging começará a fazer sentido.
Mas não criaremos Logger agora.
Hoje:
main.cpp
continua suficiente.
Quando std::cout deixar de ser suficiente para diagnóstico, teremos um problema real que poderá justificar um sistema melhor.
65. Testes também nascerão dessa necessidade
Imagine um bug futuro:
Player tem 100 moedas
Compra item de 30
Saldo vira 130
Corrigimos:
Saldo agora é 70
Mas como garantimos que alguém não quebre essa regra novamente daqui a seis meses?
Uma resposta possível:
teste automatizado
Por isso nosso Game Automation Framework também não nascerá porque “projetos profissionais têm testes”.
Ele nascerá porque teremos comportamento que queremos validar repetidamente.
66. Erros também ensinam arquitetura
Imagine que um sistema esteja tão acoplado que qualquer alteração quebra cinco módulos.
Isso é informação.
Talvez nossa arquitetura possua um problema.
Imagine que seja impossível testar economia sem abrir a janela gráfica.
Isso é informação.
Talvez responsabilidades estejam acopladas demais.
Imagine que carregar uma textura exija conhecer detalhes de cinco classes.
Isso é informação.
Problemas não servem apenas para encontrar linhas incorretas.
Eles também revelam qualidade estrutural.
67. Nosso código final do capítulo
Depois de todos os experimentos, restaure o estado simples e válido:
#include <iostream>
int main()
{
std::cout << "Game Engineering com C++ começou!" << std::endl;
return 0;
}
Faça:
Build
Confirme:
succeeded
Depois execute.
Confirme:
Game Engineering com C++ começou!
Terminamos exatamente com um programa simples.
Mas nossa capacidade de lidar com ele mudou.
68. Microdesafio — encontre os erros
Não execute imediatamente.
Observe:
#include <iostrem>
int Main()
{
std::cout << "Game Engineering com C++ começou! << std::endl
return 0;
}
Quantos problemas você consegue identificar utilizando apenas aquilo que estudamos?
Analise antes de corrigir.
69. Investigando o microdesafio
Podemos identificar pelo menos estes problemas relevantes:
1. Header incorreto
Temos:
#include <iostrem>
quando pretendíamos utilizar:
#include <iostream>
2. Ponto de entrada incorreto
Temos:
int Main()
em vez de:
int main()
Nossa convenção PascalCase não altera o nome especial main.
3. Literal não fechado
Temos:
"Game Engineering com C++ começou!
sem a aspas de fechamento adequada.
4. Instrução sem ;
A expressão de saída também não termina corretamente com:
;
Agora vem a parte importante.
Não corrija tudo mentalmente e esqueça.
Experimente.
70. Corrija um problema por vez
Comece:
#include <iostream>
int Main()
{
std::cout << "Game Engineering com C++ começou! << std::endl
return 0;
}
Faça Build.
Observe o que mudou.
Depois:
#include <iostream>
int main()
{
std::cout << "Game Engineering com C++ começou! << std::endl
return 0;
}
Faça Build novamente.
Continue até chegar a:
#include <iostream>
int main()
{
std::cout << "Game Engineering com C++ começou!" << std::endl;
return 0;
}
Faça Build.
Execute.
Esse processo é mais importante que a resposta pronta.
71. Desafio extra — produza um erro e documente
Escolha um dos experimentos deste capítulo.
Por exemplo:
remover ;
ou:
alterar iostream
ou:
remover }
Agora escreva, para você mesmo:
Problema:
O que alterei?
Sintoma:
O que aconteceu?Mensagem:
O que o compilador informou?Hipótese:
Por que acredito que aconteceu?Correção:
O que alterei?Validação:
Como confirmei que voltou a funcionar?
Você acabou de produzir uma versão extremamente simples de um relatório técnico de investigação.
72. Esse modelo vai crescer conosco
Futuramente poderemos ter algo como:
Título:
Player atravessa collision bounds ao mover na diagonal.
Environment:
Windows 11
Debug x64
Build 0.8.14Steps to reproduce:
1. ...
2. ...
3. ...Expected:
Player permanece fora do obstáculo.Actual:
Player atravessa o canto.Evidence:
logs...
video...
coordinates...Root cause:
...Fix:
...Regression tests:
...
Hoje documentamos um ;.
A habilidade é a mesma.
A escala é que muda.
73. Precisamos alterar o projeto permanentemente?
Não.
Os erros deste capítulo foram experimentos didáticos.
Nosso estado final continua:
#include <iostream>
int main()
{
std::cout << "Game Engineering com C++ começou!" << std::endl;
return 0;
}
Portanto, não precisamos criar um commit dizendo:
fix: add missing semicolon
se nós removemos o ponto e vírgula propositalmente apenas durante o exercício e nunca registramos aquele estado.
Isso seria criar uma história falsa do software.
74. Git registra a evolução real, não nosso teatro didático
Essa distinção será importante durante toda a série.
No Medium podemos mostrar:
quebrar
↓
investigar
↓
corrigir
para ensinar.
No repositório, o histórico deve representar mudanças relevantes do projeto.
Portanto:
Experimento temporário
→ não precisa de commit
Mudança real no software
→ commitCorreção de bug real
→ commitDocumentação relevante
→ commit
O GitHub não precisa reproduzir cada tecla utilizada no artigo.
75. CHANGELOG também não precisa ser alterado
Pelo mesmo motivo, não precisamos adicionar:
Fixed missing semicolon.
ao CHANGELOG.md.
Não existia um bug real persistente no projeto.
Criamos o erro propositalmente e restauramos o estado.
Nosso CHANGELOG deve continuar descrevendo a evolução real do software.
76. O que evoluiu então?
Nós.
No começo do capítulo:
Build failed
↓
"deu erro"
Agora queremos começar a pensar:
Build failed
↓
Qual etapa?
↓
Qual diagnóstico?
↓
Qual arquivo?
↓
Qual região?
↓
O que mudou?
↓
Qual hipótese?
↓
Como testar?
↓
Qual é a causa?
↓
Como corrigir?
↓
Como validar?
Esse é um avanço técnico real, mesmo sem adicionarmos uma única classe.
77. Nossa estrutura continua mínima
Ao final:
07_Game_Engineering_na_Pratica_CPP_SFML/
│
├── CppGameEngine/
│ └── main.cpp
│
├── README.md
├── CHANGELOG.md
├── CONTRIBUTING.md
└── ROADMAP.md
Ainda não temos:
Core/
Engine/
Game/
Input/
Scene/
Resources/
Physics/
Tests/
E continuamos não precisando deles.
Estamos construindo fundamentos antes de construir abstrações.
78. O que aprendemos neste capítulo?
Começamos a diferenciar:
erro
de:
diagnóstico
Aprendemos a observar:
Error List;
Output;
arquivo;
linha;
mensagens;
alterações recentes.
Provocamos problemas envolvendo:
;;<iostream>;ausência do header;
std;cout;aspas;
{};capitalização;
main.
Começamos a compreender:
sintaxe;
case sensitivity;
causa;
sintoma;
erros em cascata;
reprodução;
hipótese;
validação;
warnings;
bugs lógicos;
troubleshooting.
E estabelecemos nossa primeira metodologia:
IDENTIFICAR
↓
REPRODUZIR
↓
OBSERVAR
↓
LOCALIZAR
↓
COMPARAR
↓
CRIAR HIPÓTESE
↓
TESTAR
↓
CORRIGIR
↓
VALIDAR
↓
PREVENIR
Vamos utilizar essa sequência inúmeras vezes.
79. Ainda existe uma pergunta importante
Até agora fizemos:
main.cpp
↓
Build
↓
CppGameEngine.exe
Falamos várias vezes em:
compilador
Mas ainda estamos tratando Build quase como uma caixa-preta.
Pressionamos:
Ctrl + Shift + B
e alguma coisa acontece.
Arquivos aparecem.
Um .exe é produzido.
Depois pressionamos:
Ctrl + F5
e o programa executa.
Mas:
Build e Run são a mesma coisa?
Não.
E:
O código-fonte e o executável são a mesma coisa?
Também não.
Então está na hora de abrir um pouco mais essa caixa-preta.
Conclusão
Hoje fizemos algo que parece estranho à primeira vista:
quebramos um programa que já funcionava.
E fizemos isso de propósito.
Removemos símbolos.
Alteramos nomes.
Quebramos headers.
Desbalanceamos delimitadores.
Produzimos diagnósticos.
Mas nosso objetivo nunca foi simplesmente gerar erros.
Foi mudar nossa relação com eles.
Um erro não deve produzir automaticamente:
Pânico
↓
Copiar mensagem
↓
Pesquisar solução
↓
Colar código
↓
Torcer
Queremos construir:
Erro
↓
Observação
↓
Classificação
↓
Hipótese
↓
Experimento
↓
Evidência
↓
Correção
↓
Validação
↓
Aprendizado
Esse processo acompanhará toda a nossa evolução.
Quando encontrarmos nosso primeiro erro de linker, utilizaremos essa mentalidade.
Quando uma DLL não carregar, também.
Quando um sprite desaparecer.
Quando uma colisão falhar.
Quando um save corromper.
Quando um teste quebrar.
Quando um frame demorar demais.
Quando um bug aparecer somente em Release.
Os problemas ficarão maiores.
Nossa capacidade de investigá-los também.
E agora que já conseguimos escrever, executar, compreender e quebrar nosso primeiro código, podemos finalmente investigar melhor aquilo que acontece entre o arquivo que escrevemos e o programa que executamos.
Próximo capítulo
Capítulo 04 — Build, Run e Executable não são a mesma coisa
No próximo artigo vamos investigar três conceitos que até agora utilizamos quase como se fossem uma única operação:
BUILD
RUN
EXECUTABLE
Vamos entender por que:
Ctrl + S
não é:
Ctrl + Shift + B
e por que nenhum deles é exatamente:
Ctrl + F5
Vamos localizar o executável gerado pelo nosso projeto e começar a construir um modelo mais preciso:
main.cpp
↓
processo de Build
↓
artefatos gerados
↓
CppGameEngine.exe
↓
execução
↓
processo em memória
Também começaremos a preparar o terreno para uma pergunta muito maior:
O que realmente acontece dentro de um Build C++?
Porque antes de encontrarmos compilador, object files e linker em profundidade, precisamos parar de tratar salvar, construir e executar como se fossem a mesma coisa.
Comentários
Ainda não há comentários. Sê o primeiro a comentar!