Capítulo 07 — Constantes: valores que não deveriam mudar
Série: Do zero ao jogo comercial com C++ e SFML
Quando uma informação representa uma regra fixa, nosso código também deve deixar essa intenção explícita
No capítulo anterior começamos a perceber que nem toda informação do nosso jogo possui a mesma natureza.
Criamos:
std::string playerName = "Alex";
int playerHealth = 100;
int playerCoins = 25;
int playerLevel = 1;
float playerSpeed = 4.5f;
bool isAlive = true;
char playerGrade = 'A';
Agora nosso programa já consegue representar diferentes tipos de dados.
Mas existe outra diferença importante entre informações.
Algumas devem mudar durante a execução:
playerHealth
playerCoins
playerLevel
Outras representam regras ou limites que, depois de definidos, não deveriam ser alterados:
Maximum Player Health
Maximum Inventory Slots
Default Player Speed
Imagine uma regra do nosso jogo:
Maximum Player Health = 100
Poderíamos escrever:
int maxPlayerHealth = 100;
Mas nada impediria isto:
maxPlayerHealth = 500;
O código compilaria.
Só que talvez isso violasse completamente nossa intenção.
Precisamos conseguir dizer ao compilador:
Este valor existe, mas não deve ser modificado depois de inicializado.
É exatamente para esse tipo de situação que começaremos a utilizar const.
1. Nosso problema atual
Vamos começar com um estado simples:
int playerHealth = 100;
Durante o jogo, playerHealth pode mudar:
100
↓
80
↓
50
↓
100
Isso faz sentido.
O jogador pode:
sofrer dano;
recuperar vida;
utilizar itens;
receber efeitos.
Portanto:
int playerHealth = 100;
representa estado mutável.
Agora imagine:
Maximum Player Health = 100
Essa informação possui outro significado.
Ela representa uma regra.
2. Variável mutável versus valor constante
Podemos pensar em duas categorias iniciais:
ESTADO MUTÁVEL
playerHealth
100 → 80 → 60 → 100
e:
REGRA
MAX_PLAYER_HEALTH
100
O primeiro pode mudar.
O segundo não deveria.
Se ambos forem declarados simplesmente como int, nosso código não expressa essa diferença.
3. Nossa primeira constante
Em C++, podemos escrever:
const int MAX_PLAYER_HEALTH = 100;
Agora temos:
const int MAX_PLAYER_HEALTH = 100;
│ │ │ │
│ │ │ └── valor inicial
│ │ │
│ │ └── nome
│ │
│ └── tipo
│
└── não pode ser modificado depois
Essa declaração comunica uma intenção muito diferente de:
int playerHealth = 100;
4. Nossa convenção para constantes
No projeto utilizaremos:
UPPERCASE
para constantes.
E continuaremos utilizando nomes em inglês.
Portanto:
const int MAX_PLAYER_HEALTH = 100;
const int MAX_INVENTORY_SLOTS = 20;
const float DEFAULT_PLAYER_SPEED = 4.5f;
Observe o padrão:
MAX_PLAYER_HEALTH
MAX_INVENTORY_SLOTS
DEFAULT_PLAYER_SPEED
Palavras separadas por:
underscore.
Essa é uma convenção do nosso projeto.
C++ não exige que constantes utilizem letras maiúsculas.
Poderíamos tecnicamente escrever:
const int maxPlayerHealth = 100;
Mas nossa convenção será:
const int MAX_PLAYER_HEALTH = 100;
Consistência importa.
5. Vamos usar uma constante de verdade
Atualize nosso main.cpp:
#include <iostream>
int main()
{
const int MAX_PLAYER_HEALTH = 100;
int playerHealth = MAX_PLAYER_HEALTH;
std::cout << "Maximum Health: " << MAX_PLAYER_HEALTH << std::endl;
std::cout << "Current Health: " << playerHealth << std::endl;
return 0;
}
Faça:
Save
↓
Build
↓
Run
Resultado:
Maximum Health: 100
Current Health: 100
Agora temos dois conceitos diferentes.
6. A constante representa a regra
Temos:
const int MAX_PLAYER_HEALTH = 100;
e:
int playerHealth = MAX_PLAYER_HEALTH;
Podemos visualizar:
REGRA
MAX_PLAYER_HEALTH
┌───────┐
│ 100 │
└───────┘
│
│ valor inicial
▼
playerHealth
┌───────┐
│ 100 │
└───────┘
Depois o jogador sofre dano:
playerHealth = playerHealth - 20;
Agora:
MAX_PLAYER_HEALTH
100
playerHealth
80
A regra continua a mesma.
O estado mudou.
7. Vamos observar isso no programa
#include <iostream>
int main()
{
const int MAX_PLAYER_HEALTH = 100;
int playerHealth = MAX_PLAYER_HEALTH;
std::cout << "=== INITIAL STATE ===" << std::endl;
std::cout << "Maximum Health: " << MAX_PLAYER_HEALTH << std::endl;
std::cout << "Current Health: " << playerHealth << std::endl;
playerHealth = playerHealth - 20;
std::cout << std::endl;
std::cout << "=== AFTER DAMAGE ===" << std::endl;
std::cout << "Maximum Health: " << MAX_PLAYER_HEALTH << std::endl;
std::cout << "Current Health: " << playerHealth << std::endl;
return 0;
}
Resultado:
=== INITIAL STATE ===
Maximum Health: 100
Current Health: 100
=== AFTER DAMAGE ===
Maximum Health: 100
Current Health: 80
Agora nosso programa expressa claramente:
Maximum Health
→ regra
Current Health
→ estado
8. O que acontece se tentarmos modificar uma constante?
Vamos quebrar nosso programa de propósito.
Adicione:
MAX_PLAYER_HEALTH = 200;
Teremos:
#include <iostream>
int main()
{
const int MAX_PLAYER_HEALTH = 100;
MAX_PLAYER_HEALTH = 200;
return 0;
}
Faça Build.
O compilador deverá rejeitar essa alteração.
E isso é exatamente o que queremos.
9. O compilador agora protege nossa intenção
Quando declaramos:
const int MAX_PLAYER_HEALTH = 100;
estamos fornecendo uma informação adicional ao compilador:
este objeto não deve ser modificado através desse nome depois de inicializado.
Então:
MAX_PLAYER_HEALTH = 200;
não é simplesmente uma alteração indesejada do ponto de vista do Game Design.
Ela também se torna inválida no código.
Isso é muito poderoso.
10. Antes e depois de const
Sem const:
int MAX_PLAYER_HEALTH = 100;
MAX_PLAYER_HEALTH = 200;
Tecnicamente permitido.
Mesmo que nossa intenção fosse manter 100.
Com const:
const int MAX_PLAYER_HEALTH = 100;
MAX_PLAYER_HEALTH = 200;
o compilador consegue impedir a alteração.
Temos:
INTENÇÃO
+
LINGUAGEM
↓
proteção maior
11. const também melhora a leitura
Compare:
int MAX_INVENTORY_SLOTS = 20;
com:
const int MAX_INVENTORY_SLOTS = 20;
Mesmo sem procurar onde a variável é utilizada, o segundo código já comunica:
MAX_INVENTORY_SLOTSnão deve mudar.
Isso melhora nossa capacidade de compreender o programa.
Uma boa declaração não descreve apenas qual valor existe.
Ela também descreve como esse valor pode ser utilizado.
12. Constante precisa de valor inicial
Experimente:
const int MAX_PLAYER_HEALTH;
Para esse tipo fundamental local, isso não é válido.
Estamos declarando um objeto const, mas não fornecemos um valor com o qual ele possa ser inicializado.
A forma correta:
const int MAX_PLAYER_HEALTH = 100;
Isso faz sentido.
Se depois da inicialização não poderemos atribuir outro valor, precisamos definir adequadamente seu estado inicial.
13. Inicialização continua diferente de atribuição
No Capítulo 05 vimos essa distinção.
Aqui ela fica ainda mais importante.
Isto:
const int MAX_PLAYER_HEALTH = 100;
é inicialização.
Não estamos:
criando a constante;
deixando-a sem valor;
atribuindo
100depois.
Ela nasce inicializada.
Depois disso:
MAX_PLAYER_HEALTH = 200;
não é permitido.
14. Constantes podem ter outros tipos
Não estamos limitados a:
const int
Podemos ter:
const float DEFAULT_PLAYER_SPEED = 4.5f;
const bool DEFAULT_FULLSCREEN = false;
const char DEFAULT_PLAYER_GRADE = 'A';
Também poderíamos utilizar std::string:
#include <string>
const std::string DEFAULT_PLAYER_NAME = "Player";
O conceito é o mesmo:
aquela informação não deve ser modificada depois de inicializada através daquele objeto
const.
15. Um pequeno conjunto de regras do jogo
Podemos escrever:
#include <iostream>
int main()
{
const int MAX_PLAYER_HEALTH = 100;
const int MAX_INVENTORY_SLOTS = 20;
const float DEFAULT_PLAYER_SPEED = 4.5f;
int playerHealth = MAX_PLAYER_HEALTH;
int inventorySlotsUsed = 0;
float playerSpeed = DEFAULT_PLAYER_SPEED;
std::cout << "Maximum Health: " << MAX_PLAYER_HEALTH << std::endl;
std::cout << "Current Health: " << playerHealth << std::endl;
std::cout << "Inventory Capacity: " << MAX_INVENTORY_SLOTS << std::endl;
std::cout << "Inventory Used: " << inventorySlotsUsed << std::endl;
std::cout << "Default Speed: " << DEFAULT_PLAYER_SPEED << std::endl;
std::cout << "Current Speed: " << playerSpeed << std::endl;
return 0;
}
Agora começamos a enxergar duas categorias no nosso código.
16. Regras versus estado
CONSTANTES
MAX_PLAYER_HEALTH
MAX_INVENTORY_SLOTS
DEFAULT_PLAYER_SPEED
Representam regras ou valores configurados para nosso exemplo.
Enquanto:
VARIÁVEIS
playerHealth
inventorySlotsUsed
playerSpeed
representam estado atual.
Podemos visualizar:
REGRAS
│
├── MAX_PLAYER_HEALTH = 100
├── MAX_INVENTORY_SLOTS = 20
└── DEFAULT_PLAYER_SPEED = 4.5
ESTADO ATUAL
│
├── playerHealth = 80
├── inventorySlotsUsed = 7
└── playerSpeed = 6.0
Agora nosso modelo está ficando semanticamente mais claro.
17. Nem todo valor fixo precisa virar constante nomeada
Aqui precisamos evitar outro extremo.
Considere:
return 0;
Não precisamos imediatamente criar:
const int SUCCESS_CODE = 0;
só porque 0 aparece no código.
Da mesma forma, não devemos transformar cada número existente em uma constante apenas para dizer que "evitamos magic numbers".
Precisamos analisar contexto e intenção.
18. O que é um magic number?
Imagine:
if (playerHealth > 100)
{
}
Ainda não estudamos if, então observe apenas conceitualmente.
O que significa:
100
?
Talvez seja:
maximum player health
Se aquele valor representa uma regra importante, algo como:
MAX_PLAYER_HEALTH
expressaria melhor a intenção.
Agora imagine:
playerLevel = playerLevel + 1;
O:
1
é bastante claro como incremento de um nível naquele contexto.
Criar:
const int ONE_LEVEL = 1;
provavelmente deixaria o código pior.
Portanto:
evitar magic numbers não significa nomear cegamente todo literal numérico.
19. Uma constante deve representar um conceito
Bom exemplo:
const int MAX_PLAYER_HEALTH = 100;
Porque existe um conceito:
Maximum Player Health
Outro:
const int MAX_INVENTORY_SLOTS = 20;
Existe um conceito de limite de inventário.
Agora isto:
const int VALUE_ONE = 1;
provavelmente não comunica nenhuma regra relevante.
O nome deveria explicar por que aquele valor importa.
20. const não significa "global"
Não precisamos colocar nossas constantes fora de main.
Podemos continuar:
int main()
{
const int MAX_PLAYER_HEALTH = 100;
int playerHealth = MAX_PLAYER_HEALTH;
return 0;
}
Neste estágio isso é suficiente.
Colocar constantes globalmente apenas porque são constantes criaria outro assunto:
scope
lifetime
linkage
translation units
Ainda não precisamos disso.
Vamos manter nosso código simples.
21. Constante global não é automaticamente melhor
Você poderá encontrar códigos assim:
const int MAX_PLAYER_HEALTH = 100;
int main()
{
}
Isso pode ser adequado em certos contextos.
Mas não adotaremos a regra:
constantes devem ficar no topo do arquivo.
A pergunta correta será:
qual é o escopo adequado para esse valor?
Ainda teremos um capítulo específico sobre escopo.
Quando tivermos informação suficiente, poderemos tomar decisões melhores.
22. E #define?
Talvez você encontre:
#define MAX_PLAYER_HEALTH 100
em códigos C ou C++.
Macros existem e terão seu momento.
Mas não utilizaremos #define para representar constantes simples quando uma solução tipada como:
const int MAX_PLAYER_HEALTH = 100;
expressa melhor nosso objetivo atual.
Macros são processadas pelo pré-processador e possuem características bastante diferentes de objetos const.
Não precisamos introduzir essa complexidade agora.
23. E constexpr?
C++ moderno também possui:
constexpr
Você poderá encontrar:
constexpr int MAX_PLAYER_HEALTH = 100;
Então por que estamos começando com:
const int MAX_PLAYER_HEALTH = 100;
Porque precisamos primeiro compreender a ideia de imutabilidade através de const.
constexpr envolve também a possibilidade/requisito de avaliação em contexto de tempo de compilação e possui implicações que merecem uma explicação própria.
Não vamos misturar os conceitos prematuramente.
Primeiro:
mutable
versus
const
Depois evoluímos.
24. const começa a nos ensinar uma ideia maior
Até agora poderíamos pensar:
const = valor que não muda
Isso é útil para começar.
Mas em C++, const é muito mais importante.
Futuramente teremos:
const Player& player
métodos como:
int GetHealth() const;
pointers envolvendo const e outras combinações.
A ideia maior será:
const correctness
Ou seja, utilizar o sistema de tipos para expressar corretamente onde alterações são permitidas e onde não são.
Mas não vamos saltar para referências e métodos agora.
Hoje construímos apenas o primeiro degrau.
25. Erro proposital — alterando a constante
Vamos utilizar nossa metodologia de troubleshooting.
Código:
#include <iostream>
int main()
{
const int MAX_PLAYER_HEALTH = 100;
MAX_PLAYER_HEALTH = 200;
return 0;
}
Faça Build.
Depois responda:
O que aconteceu?
Qual linha está envolvida?
Qual objeto possui const?
Qual operação estamos tentando realizar?
Por que essa operação contradiz nossa declaração?
Depois remova:
MAX_PLAYER_HEALTH = 200;
Faça Build novamente.
Valide.
26. Erro proposital — constante sem inicialização
Teste:
const int MAX_PLAYER_HEALTH;
Faça Build.
Observe o diagnóstico.
Depois corrija:
const int MAX_PLAYER_HEALTH = 100;
Novamente:
QUEBRAR
↓
LER
↓
ENTENDER
↓
CORRIGIR
↓
VALIDAR
O erro continua sendo parte do nosso processo educacional.
27. Microdesafio — o que deve ser constante?
Considere estas informações:
Current Health
Maximum Health
Current Coins
Maximum Inventory Slots
Current Level
Default Player Speed
Quais provavelmente representam estado mutável e quais representam regras fixas no nosso exemplo?
Uma classificação possível:
MUTÁVEIS
playerHealth
playerCoins
playerLevel
playerSpeed
e:
CONSTANTES
MAX_PLAYER_HEALTH
MAX_INVENTORY_SLOTS
DEFAULT_PLAYER_SPEED
Mas atenção:
playerSpeed pode mudar durante o jogo devido a buffs ou efeitos.
Por isso temos:
const float DEFAULT_PLAYER_SPEED = 4.5f;
float playerSpeed = DEFAULT_PLAYER_SPEED;
A palavra:
DEFAULT
é importante.
Ela comunica que estamos falando do valor inicial/padrão, e não necessariamente do valor atual.
28. Microdesafio prático
Crie:
const int MAX_PLAYER_HEALTH = 100;
const int MAX_INVENTORY_SLOTS = 20;
const float DEFAULT_PLAYER_SPEED = 4.5f;
Depois:
int playerHealth = MAX_PLAYER_HEALTH;
int inventorySlotsUsed = 5;
float playerSpeed = DEFAULT_PLAYER_SPEED;
Mostre:
=== GAME RULES ===
Maximum Health: 100
Maximum Inventory Slots: 20
Default Player Speed: 4.5
=== PLAYER STATE ===
Current Health: 100
Inventory Used: 5
Current Speed: 4.5
Tente antes de ver uma solução possível.
29. Uma possível implementação
#include <iostream>
int main()
{
const int MAX_PLAYER_HEALTH = 100;
const int MAX_INVENTORY_SLOTS = 20;
const float DEFAULT_PLAYER_SPEED = 4.5f;
int playerHealth = MAX_PLAYER_HEALTH;
int inventorySlotsUsed = 5;
float playerSpeed = DEFAULT_PLAYER_SPEED;
std::cout << "=== GAME RULES ===" << std::endl;
std::cout << "Maximum Health: " << MAX_PLAYER_HEALTH << std::endl;
std::cout << "Maximum Inventory Slots: " << MAX_INVENTORY_SLOTS << std::endl;
std::cout << "Default Player Speed: " << DEFAULT_PLAYER_SPEED << std::endl;
std::cout << std::endl;
std::cout << "=== PLAYER STATE ===" << std::endl;
std::cout << "Current Health: " << playerHealth << std::endl;
std::cout << "Inventory Used: " << inventorySlotsUsed << std::endl;
std::cout << "Current Speed: " << playerSpeed << std::endl;
return 0;
}
Essa implementação continua simples.
Mas ela já expressa algo que nosso código anterior não expressava:
REGRA
≠
ESTADO
30. Nosso código final do capítulo
Podemos manter como evolução principal:
#include <iostream>
int main()
{
const int MAX_PLAYER_HEALTH = 100;
const int MAX_INVENTORY_SLOTS = 20;
const float DEFAULT_PLAYER_SPEED = 4.5f;
int playerHealth = MAX_PLAYER_HEALTH;
int inventorySlotsUsed = 5;
float playerSpeed = DEFAULT_PLAYER_SPEED;
std::cout << "=== GAME RULES ===" << std::endl;
std::cout << "Maximum Health: " << MAX_PLAYER_HEALTH << std::endl;
std::cout << "Maximum Inventory Slots: " << MAX_INVENTORY_SLOTS << std::endl;
std::cout << "Default Player Speed: " << DEFAULT_PLAYER_SPEED << std::endl;
std::cout << std::endl;
std::cout << "=== PLAYER STATE ===" << std::endl;
std::cout << "Current Health: " << playerHealth << std::endl;
std::cout << "Inventory Used: " << inventorySlotsUsed << std::endl;
std::cout << "Current Speed: " << playerSpeed << std::endl;
playerHealth = playerHealth - 20;
inventorySlotsUsed = inventorySlotsUsed + 1;
std::cout << std::endl;
std::cout << "=== UPDATED PLAYER STATE ===" << std::endl;
std::cout << "Current Health: " << playerHealth << std::endl;
std::cout << "Inventory Used: " << inventorySlotsUsed << std::endl;
std::cout << "Current Speed: " << playerSpeed << std::endl;
return 0;
}
O resultado será semelhante a:
=== GAME RULES ===
Maximum Health: 100
Maximum Inventory Slots: 20
Default Player Speed: 4.5
=== PLAYER STATE ===
Current Health: 100
Inventory Used: 5
Current Speed: 4.5
=== UPDATED PLAYER STATE ===
Current Health: 80
Inventory Used: 6
Current Speed: 4.5
As regras permaneceram.
O estado mudou.
Esse é o resultado observável deste capítulo.
31. Nossa arquitetura ainda não precisa mudar
Continuamos apenas com:
CppGameEngine/
└── main.cpp
Não precisamos criar:
Constants/
Config/
GameRules/
Settings/
ainda.
Temos três constantes.
Criar uma infraestrutura inteira para isso seria antecipar complexidade.
Quando essas regras começarem a crescer, ficar espalhadas ou depender de configuração, teremos um problema novo.
Então analisaremos a solução adequada.
32. Validando a implementação
Faça:
Ctrl + S
Depois:
Ctrl + Shift + B
Confirme:
Build succeeded
Execute:
Ctrl + F5
Verifique:
MAX_PLAYER_HEALTH permanece 100
MAX_INVENTORY_SLOTS permanece 20
DEFAULT_PLAYER_SPEED permanece 4.5
playerHealth muda
inventorySlotsUsed muda
Também faça o teste negativo:
MAX_PLAYER_HEALTH = 200;
e confirme que o compilador impede a alteração.
Depois restaure o código válido.
33. Registrando a evolução no Git
Desta vez temos uma mudança real no código.
Verifique:
git status
Depois adicione o arquivo correto:
git add CppGameEngine/main.cpp
Ajuste o caminho caso sua estrutura local seja diferente.
Podemos registrar:
git commit -m "feat: add immutable game rule constants"
O commit representa a evolução:
nosso programa agora diferencia regras constantes de estado mutável.
34. Atualizando o CHANGELOG
Em [Unreleased], podemos acrescentar:
### Added
- Added immutable game rule constants.
- Added maximum player health and inventory slot limits.
- Added default player speed configuration.
- Added separation between game rules and mutable player state.
Depois:
git add CHANGELOG.md
git commit -m "docs: update changelog with game rule constants"
Não precisamos alterar o ROADMAP técnico apenas por esse pequeno avanço de fundamentos.
35. O que aprendemos?
No capítulo anterior tínhamos apenas variáveis:
int playerHealth = 100;
Agora aprendemos a expressar outra intenção:
const int MAX_PLAYER_HEALTH = 100;
Começamos a diferenciar:
MUTÁVEL
↓
playerHealth
de:
CONSTANTE
↓
MAX_PLAYER_HEALTH
Aprendemos também que:
constimpede modificações posteriores através daquele objeto;constantes devem ser adequadamente inicializadas;
nossa convenção utilizará
UPPERCASE;identificadores continuarão em inglês;
constantes podem utilizar diferentes tipos;
nem todo literal precisa virar uma constante;
uma constante nomeada deve representar um conceito;
constnão significa global;constnão é o mesmo que#define;constexprexiste, mas possui responsabilidades adicionais;estamos dando nosso primeiro passo em direção a const correctness.
Mais importante:
nosso código agora consegue expressar não apenas o valor de uma informação, mas também a intenção de que ela não seja modificada.
Conclusão
Nosso pequeno programa já conhece diferentes tipos de informação:
int
float
bool
char
std::string
Agora também começou a diferenciar duas naturezas de estado:
VALORES QUE MUDAM
e:
VALORES QUE NÃO DEVERIAM MUDAR
Podemos representar:
GAME RULES
MAX_PLAYER_HEALTH = 100
MAX_INVENTORY_SLOTS = 20
DEFAULT_PLAYER_SPEED = 4.5
enquanto o jogador possui:
PLAYER STATE
playerHealth = 80
inventorySlotsUsed = 6
playerSpeed = 4.5
Essa separação é simples.
Mas já começa a tornar nosso código mais seguro e mais expressivo.
Programação não consiste apenas em fazer o compilador aceitar instruções.
Também queremos comunicar nossas intenções.
E const nos dá uma forma de dizer:
este valor representa algo que não deveria ser alterado.
No próximo capítulo, essas informações começarão a interagir ainda mais.
Até agora fizemos algumas operações como:
playerHealth = playerHealth - 20;
e:
inventorySlotsUsed = inventorySlotsUsed + 1;
Mas ainda não estudamos formalmente o que são:
+
-
=
*
/
%
e outros operadores.
Chegou a hora.
Próximo capítulo
Capítulo 08 — Operadores: transformando dados
Série: Do zero ao jogo comercial com C++ e SFML
No próximo artigo vamos partir de situações reais do nosso pequeno estado:
Player sofre dano
Player recupera Health
Player recebe Coins
Player gasta Coins
Experience aumenta
e descobrir como operadores permitem transformar informações.
Vamos trabalhar progressivamente com operações como:
playerHealth - damage
playerCoins + reward
playerCoins - itemPrice
e começar a diferenciar:
atribuição
aritmética
incremento
decremento
sem tentar estudar todos os operadores C++ de uma única vez.
Primeiro construiremos as operações que nosso jogo já precisa.
Depois entenderemos o que cada operador está fazendo.
Nosso programa está deixando de apenas armazenar estado.
Agora começará a transformá-lo.
Comentários
Ainda não há comentários. Sê o primeiro a comentar!