Capítulo 09 — Expressões: combinando valores e operações
Série: Do zero ao jogo comercial com C++ e SFML
Quando valores, variáveis e operadores começam a trabalhar juntos para produzir novos resultados
No capítulo anterior nosso programa deixou de apenas armazenar informações.
Começamos a transformá-las.
Utilizamos operadores como:
playerHealth -= 20;
playerCoins += 15;
playerExperience += 60;
playerLevel++;
Também realizamos cálculos:
int criticalDamage = baseDamage * criticalMultiplier;
e:
int completedLevels = totalExperience / EXPERIENCE_PER_LEVEL;
int remainingExperience = totalExperience % EXPERIENCE_PER_LEVEL;
Até aqui conseguimos compreender cada operação isoladamente.
Mas considere:
int finalDamage = baseDamage + bonusDamage * criticalMultiplier;
Agora temos várias coisas acontecendo na mesma instrução.
Temos:
variáveis
+
operadores
+
valores
+
uma atribuição
E surge uma pergunta importante:
Como C++ combina tudo isso para produzir um resultado?
Para responder, precisamos compreender um dos conceitos mais fundamentais da programação:
expressões.
1. Começando pelo problema
Imagine:
int baseDamage = 20;
int bonusDamage = 10;
int criticalMultiplier = 2;
Queremos calcular um dano final.
Uma possibilidade seria:
int finalDamage = baseDamage + bonusDamage * criticalMultiplier;
Temos:
baseDamage
bonusDamage
criticalMultiplier
+
*
=
Mas qual operação acontece primeiro?
Talvez você leia da esquerda para a direita:
20 + 10
↓
30
30 * 2
↓
60
e espere:
finalDamage = 60
Mas C++ não interpreta essa expressão simplesmente da esquerda para a direita.
O resultado será:
40
Por quê?
Precisamos desmontar a expressão.
2. O que é uma expressão?
Para nosso estágio atual, podemos utilizar esta definição:
Uma expressão é uma combinação de valores, variáveis, operadores e outras construções da linguagem que produz um valor ou resultado.
Por exemplo:
baseDamage + bonusDamage
é uma expressão.
Se:
baseDamage = 20
bonusDamage = 10
então:
baseDamage + bonusDamage
produz:
30
Podemos visualizar:
baseDamage + bonusDamage
↓ ↓
20 + 10
↓
30
A expressão foi avaliada e produziu um valor.
3. Uma variável também pode participar de uma expressão
Considere:
int playerHealth = 100;
int damage = 20;
A expressão:
playerHealth - damage
utiliza os valores associados a essas variáveis.
Conceitualmente:
playerHealth - damage
↓ ↓
100 - 20
↓
80
O resultado da expressão é:
80
Mas isso ainda não significa que playerHealth mudou.
4. Expressão e atribuição continuam sendo coisas diferentes
No capítulo anterior vimos:
playerHealth - damage;
Essa expressão produz um resultado.
Mas não armazenamos esse resultado.
Para alterar playerHealth:
playerHealth = playerHealth - damage;
Podemos pensar:
playerHealth - damage
↓
100 - 20
↓
80
↓
playerHealth = 80
A expressão do lado direito produz um valor.
Depois esse valor é atribuído à variável do lado esquerdo.
5. O lado direito precisa produzir um valor compatível
Considere:
int finalDamage = baseDamage + bonusDamage;
Temos:
baseDamage + bonusDamage
↓
expressão
↓
produz int
↓
finalDamage recebe o resultado
Os tipos continuam importantes.
Uma expressão não existe separada do sistema de tipos.
6. Literais também são expressões simples
Quando escrevemos:
100
temos um literal inteiro.
Da mesma maneira:
4.5f
é um literal de ponto flutuante.
E:
true
é um literal booleano.
Em muitos contextos, até uma construção simples como um literal pode ser tratada como uma expressão que produz aquele valor.
Então podemos começar com:
100
e evoluir para:
playerHealth
depois:
playerHealth - 20
e finalmente:
(playerHealth - damage) + healing
As expressões podem crescer.
7. Operand, operator e expression
Precisamos organizar três conceitos.
Considere:
baseDamage + bonusDamage
Temos:
baseDamage + bonusDamage
│ │ │
│ │ └── operando
│ │
│ └── operador
│
└── operando
O conjunto:
baseDamage + bonusDamage
forma uma expressão.
Portanto:
OPERANDOS
+
OPERADOR
↓
EXPRESSÃO
↓
RESULTADO
8. Uma expressão pode conter outra expressão
Agora observe:
(baseDamage + bonusDamage) * criticalMultiplier
Dentro dela existe:
baseDamage + bonusDamage
que também é uma expressão.
Podemos chamá-la de subexpressão dentro da expressão maior.
Visualmente:
(baseDamage + bonusDamage) * criticalMultiplier
└─────────────┬──────────┘
subexpressão
│
▼
resultado
│
└────── * criticalMultiplier
│
▼
resultado final
Essa composição é extremamente importante.
Programas reais utilizam expressões dentro de expressões constantemente.
9. Nosso primeiro cálculo composto
Vamos utilizar:
int baseDamage = 20;
int bonusDamage = 10;
int criticalMultiplier = 2;
Queremos:
(baseDamage + bonusDamage) * criticalMultiplier
Substituindo valores:
(20 + 10) * 2
Primeiro:
20 + 10
↓
30
Depois:
30 * 2
↓
60
Resultado:
finalDamage = 60
Código:
int finalDamage = (baseDamage + bonusDamage) * criticalMultiplier;
10. Agora remova os parênteses
Compare:
int finalDamage = (baseDamage + bonusDamage) * criticalMultiplier;
com:
int finalDamage = baseDamage + bonusDamage * criticalMultiplier;
Temos:
(20 + 10) * 2
e:
20 + 10 * 2
Essas expressões não possuem necessariamente o mesmo resultado.
Na segunda:
10 * 2
↓
20
depois:
20 + 20
↓
40
Resultado:
40
Enquanto a primeira produz:
60
Isso acontece por causa da precedência dos operadores.
11. O que é precedência?
Quando uma expressão contém operadores diferentes, C++ possui regras que determinam como eles se agrupam.
Em:
10 + 5 * 2
a multiplicação possui precedência maior que a adição.
Portanto podemos visualizar:
10 + (5 * 2)
Primeiro:
5 * 2 = 10
Depois:
10 + 10 = 20
Resultado:
20
12. A precedência não depende de como nós lemos
É comum um iniciante olhar:
10 + 5 * 2
e pensar:
10 + 5 = 15
15 * 2 = 30
porque leu da esquerda para a direita.
Mas a linguagem possui regras próprias.
Não é nossa leitura visual que determina o agrupamento.
13. Parênteses alteram o agrupamento
Agora:
(10 + 5) * 2
Os parênteses agrupam:
10 + 5
Primeiro:
15
Depois:
15 * 2
Resultado:
30
Compare:
10 + 5 * 2
→ 20
com:
(10 + 5) * 2
→ 30
Uma pequena alteração mudou completamente o resultado.
14. Parênteses também comunicam intenção
Mesmo quando os parênteses não são estritamente necessários, eles podem melhorar a leitura.
Considere:
int finalDamage = baseDamage + bonusDamage * criticalMultiplier;
Agora:
int finalDamage = baseDamage + (bonusDamage * criticalMultiplier);
Os dois podem produzir o mesmo resultado.
Mas o segundo deixa explícito:
apenas o bônus deve ser multiplicado.
Já:
int finalDamage = (baseDamage + bonusDamage) * criticalMultiplier;
expressa outra regra:
some o dano base e o bônus e depois multiplique tudo.
Esses são dois modelos diferentes.
15. A expressão representa uma regra
Esse ponto é importante para Game Engineering.
Compare:
baseDamage + (bonusDamage * criticalMultiplier)
com:
(baseDamage + bonusDamage) * criticalMultiplier
Não estamos discutindo apenas sintaxe.
Estamos discutindo duas regras diferentes de cálculo.
Primeira:
Base Damage
+
Bonus Damage × Critical Multiplier
Segunda:
(Base Damage + Bonus Damage)
×
Critical Multiplier
A estrutura da expressão representa uma decisão do nosso sistema.
16. Não use parênteses aleatoriamente
Se parênteses ajudam, poderíamos pensar:
então vou colocar parênteses em tudo.
Por exemplo:
int finalDamage = (((baseDamage) + (bonusDamage)) * (criticalMultiplier));
Isso não melhora necessariamente a leitura.
Nosso objetivo não é maximizar a quantidade de parênteses.
É tornar o agrupamento e a intenção claros.
Prefira:
int finalDamage = (baseDamage + bonusDamage) * criticalMultiplier;
17. Uma pequena ordem útil para nosso estágio
Para as operações aritméticas que conhecemos até agora, podemos pensar inicialmente:
1. ( )
2. * / %
3. + -
4. atribuição
Isso é suficiente para nossos exemplos atuais.
Mas atenção:
C++ possui muitos outros operadores.
Existe uma tabela completa de precedência muito maior.
Não vamos decorá-la agora.
18. Por que não decorar a tabela inteira?
Porque ainda nem estudamos muitos dos operadores presentes nela.
Decorar antecipadamente uma grande tabela contendo operadores desconhecidos produziria memorização sem contexto.
Nossa abordagem continua sendo:
problema
↓
operador necessário
↓
expressão
↓
regra de precedência relevante
↓
teste
↓
compreensão
Quando novos operadores aparecerem, expandiremos nosso conhecimento.
19. Associatividade é outro conceito
Precedência responde principalmente:
operadores diferentes se agrupam em qual ordem?
Associatividade ajuda a determinar o agrupamento quando operadores de mesma precedência aparecem em sequência.
Considere:
int result = 20 - 5 - 3;
Para subtração, o agrupamento ocorre da esquerda para a direita:
(20 - 5) - 3
Primeiro:
20 - 5 = 15
Depois:
15 - 3 = 12
Resultado:
12
Não seria:
20 - (5 - 3)
porque isso produziria:
18
20. Precedência e associatividade não são a mesma coisa
Precisamos manter a distinção:
Precedência
Ajuda a determinar o agrupamento entre operadores com níveis diferentes.
Exemplo:
10 + 5 * 2
Associatividade
Ajuda a determinar o agrupamento entre operadores com a mesma precedência.
Exemplo:
20 - 5 - 3
Não precisamos memorizar todas as regras agora.
Precisamos saber que os dois conceitos existem.
21. Um cuidado importante: agrupamento não é simplesmente ordem temporal
Aqui existe uma sutileza importante de C++.
Quando falamos de:
precedência
e:
associatividade
estamos falando sobre como uma expressão é agrupada sintaticamente.
Isso não significa automaticamente que estamos descrevendo toda a ordem temporal em que cada parte será executada.
C++ possui regras específicas sobre ordem de avaliação.
Esse é um assunto mais avançado.
Por enquanto, guarde:
precedência e associatividade determinam agrupamento; não devemos tratá-las como uma descrição universal da ordem de execução.
Essa distinção evitará confusões futuramente.
22. Não vamos explorar efeitos colaterais complexos ainda
Talvez você encontre códigos como:
int result = playerLevel++ + ++playerLevel;
Não use algo assim em nossos exercícios.
Misturar modificações de estado dentro de expressões complexas pode tornar o código difícil de compreender e pode envolver regras de avaliação que ainda não estudamos.
Prefira clareza:
playerLevel++;
int result = playerLevel + bonusLevel;
Nosso objetivo é aprender a construir código compreensível.
23. Expressões possuem tipo
No capítulo anterior vimos que:
5 / 2
produz um resultado diferente de:
5.0f / 2.0f
Agora podemos dizer de maneira mais precisa:
uma expressão também possui um tipo.
Considere:
5 + 2
Os operandos são inteiros.
O resultado dessa expressão é inteiro.
Agora:
5.0f + 2.0f
trabalha com float.
Os tipos envolvidos influenciam o tipo e o comportamento do resultado.
24. O tipo da variável de destino não decide sozinho a operação
Relembre:
float result = 5 / 2;
A expressão do lado direito é:
5 / 2
Ela é avaliada primeiro.
Como temos operandos inteiros:
5 / 2
→ 2
Depois:
2
é convertido para ser armazenado em:
float result
e teremos algo equivalente a:
2.0
Não:
2.5
25. Corrigindo a expressão
Podemos escrever:
float result = 5.0f / 2.0f;
Agora:
5.0f / 2.0f
→ 2.5f
O resultado já nasce como ponto flutuante.
Essa distinção é extremamente importante.
26. Misturando tipos
Também poderíamos ter:
float result = 5.0f / 2;
Agora temos:
float
/
int
C++ possui regras de conversão para determinar um tipo comum adequado à operação.
Essas regras podem se tornar bastante detalhadas.
Não precisamos estudar todas agora.
Por enquanto, prefira exemplos claros:
float result = 5.0f / 2.0f;
Quando conversões se tornarem um problema concreto, estudaremos conversões de tipos de maneira apropriada.
27. Uma expressão pode ser armazenada
Considere:
int finalDamage = (baseDamage + bonusDamage) * criticalMultiplier;
A expressão:
(baseDamage + bonusDamage) * criticalMultiplier
produz um valor.
Esse valor é utilizado para inicializar:
finalDamage
Isso nos permite separar:
dados de entrada
↓
expressão
↓
resultado
Por exemplo:
baseDamage = 20
bonusDamage = 10
criticalMultiplier = 2
↓
(20 + 10) * 2
↓
60
↓
finalDamage = 60
28. Uma expressão também pode ser exibida diretamente
Podemos escrever:
std::cout << (baseDamage + bonusDamage) * criticalMultiplier << std::endl;
A expressão é avaliada e seu resultado enviado ao stream.
Mas existe uma pergunta de legibilidade.
Compare:
std::cout << (baseDamage + bonusDamage) * criticalMultiplier << std::endl;
com:
int finalDamage = (baseDamage + bonusDamage) * criticalMultiplier;
std::cout << finalDamage << std::endl;
Quando o resultado possui significado importante, dar um nome a ele pode tornar o código mais claro.
29. Variáveis intermediárias podem ajudar
Considere:
int finalDamage = (baseDamage + bonusDamage) * criticalMultiplier;
Essa expressão ainda é simples.
Mas imagine que ela cresça.
Podemos separar:
int totalBaseDamage = baseDamage + bonusDamage;
int finalDamage = totalBaseDamage * criticalMultiplier;
Agora temos:
baseDamage + bonusDamage
↓
totalBaseDamage
totalBaseDamage * criticalMultiplier
↓
finalDamage
Isso pode facilitar:
leitura;
debugging;
inspeção;
testes;
entendimento da regra.
30. Mas não precisamos criar variável para cada símbolo
O extremo oposto também é ruim.
Isto:
int valueOne = baseDamage;
int valueTwo = bonusDamage;
int valueThree = valueOne + valueTwo;
int valueFour = criticalMultiplier;
int finalDamage = valueThree * valueFour;
adiciona nomes sem aumentar a compreensão.
A pergunta é:
essa variável intermediária representa um conceito útil?
Por exemplo:
int totalBaseDamage = baseDamage + bonusDamage;
sim.
totalBaseDamage possui significado.
31. Nomes ajudam a explicar expressões
Compare:
int result = (20 + 10) * 2;
com:
int baseDamage = 20;
int bonusDamage = 10;
int criticalMultiplier = 2;
int finalDamage = (baseDamage + bonusDamage) * criticalMultiplier;
O segundo código explica muito mais.
Temos:
20
mas também sabemos:
por que 20 existe
Temos:
2
mas sabemos:
por que estamos multiplicando por 2
Expressões ficam mais legíveis quando seus operandos possuem significado.
32. Expressões começam a representar fórmulas do jogo
Imagine uma fórmula didática:
int finalDamage = (baseDamage + bonusDamage) * criticalMultiplier;
Ela pode ser representada conceitualmente como:
Final Damage
=
(Base Damage + Bonus Damage)
×
Critical Multiplier
Essa relação entre:
regra
↓
fórmula
↓
expressão C++
será recorrente durante toda a série.
Mais tarde teremos expressões relacionadas a:
movimento;
velocidade;
aceleração;
distância;
interpolação;
economia;
experiência;
agricultura;
física;
probabilidade;
IA.
33. Matemática e código começam a se aproximar
Considere:
Final Damage = (Base Damage + Bonus Damage) × Multiplier
Em C++:
int finalDamage = (baseDamage + bonusDamage) * multiplier;
Temos uma tradução de uma regra para uma expressão computacional.
Mas precisamos sempre lembrar:
código não é simplesmente matemática digitada.
Tipos, conversões, precisão, overflow e regras da linguagem também influenciam o resultado.
Esses problemas aparecerão gradualmente.
34. Uma expressão pode usar constantes
Do capítulo anterior:
const int EXPERIENCE_PER_LEVEL = 100;
Agora:
int completedLevels = playerExperience / EXPERIENCE_PER_LEVEL;
Temos:
playerExperience
/
EXPERIENCE_PER_LEVEL
A constante participa normalmente da expressão.
Isso mostra como os capítulos começam a se conectar.
35. Uma expressão pode usar resultados anteriores
int totalBaseDamage = baseDamage + bonusDamage;
int finalDamage = totalBaseDamage * criticalMultiplier;
O resultado da primeira expressão:
baseDamage + bonusDamage
é armazenado em:
totalBaseDamage
Depois participa de outra:
totalBaseDamage * criticalMultiplier
Nosso programa começa a formar pequenos fluxos de transformação de dados.
36. Construindo um pequeno cálculo de dano
Vamos implementar:
#include <iostream>
int main()
{
int baseDamage = 20;
int bonusDamage = 10;
int criticalMultiplier = 2;
int totalBaseDamage = baseDamage + bonusDamage;
int finalDamage = totalBaseDamage * criticalMultiplier;
std::cout << "Base Damage: " << baseDamage << std::endl;
std::cout << "Bonus Damage: " << bonusDamage << std::endl;
std::cout << "Critical Multiplier: " << criticalMultiplier << std::endl;
std::cout << "Total Base Damage: " << totalBaseDamage << std::endl;
std::cout << "Final Damage: " << finalDamage << std::endl;
return 0;
}
Resultado:
Base Damage: 20
Bonus Damage: 10
Critical Multiplier: 2
Total Base Damage: 30
Final Damage: 60
37. Agora escreva a expressão diretamente
Podemos substituir:
int totalBaseDamage = baseDamage + bonusDamage;
int finalDamage = totalBaseDamage * criticalMultiplier;
por:
int finalDamage = (baseDamage + bonusDamage) * criticalMultiplier;
Ambas as versões podem estar corretas.
A escolha depende da clareza necessária.
Para nosso aprendizado, observar as duas versões é útil.
38. Erro proposital — removendo os parênteses
Comece:
int baseDamage = 20;
int bonusDamage = 10;
int criticalMultiplier = 2;
int finalDamage = (baseDamage + bonusDamage) * criticalMultiplier;
Resultado:
60
Agora altere:
int finalDamage = baseDamage + bonusDamage * criticalMultiplier;
Resultado:
40
Não houve erro de compilação.
Esse é um tipo diferente de problema.
O código é válido.
Mas a regra mudou.
39. Nem todo bug produz erro de compilação
Esse experimento é extremamente importante.
Temos:
(baseDamage + bonusDamage) * criticalMultiplier
e:
baseDamage + bonusDamage * criticalMultiplier
As duas expressões compilam.
As duas executam.
As duas produzem um número.
Mas produzem números diferentes.
Se nossa regra era:
somar os danos
e depois multiplicar
a segunda expressão possui um bug lógico.
O compilador não consegue saber nossa intenção.
40. Começamos a encontrar bugs de lógica
Nos primeiros capítulos quebramos código de maneiras que produziam erros de compilação.
Agora encontramos outra categoria:
código compila
+
programa executa
+
resultado está errado
Isso é um bug lógico.
Nosso processo de debugging começa a evoluir:
RESULTADO INESPERADO
↓
QUAL ERA O RESULTADO ESPERADO?
↓
QUAIS SÃO OS VALORES DE ENTRADA?
↓
QUAL EXPRESSÃO FOI AVALIADA?
↓
COMO ELA FOI AGRUPADA?
↓
QUAL REGRA QUERÍAMOS REPRESENTAR?
↓
CORRIGIR
↓
EXECUTAR NOVAMENTE
41. Testar resultados começa a ficar mais importante
Considere:
int finalDamage = (baseDamage + bonusDamage) * criticalMultiplier;
Com:
baseDamage = 20
bonusDamage = 10
criticalMultiplier = 2
sabemos manualmente:
(20 + 10) * 2
= 30 * 2
= 60
Então esperamos:
Final Damage: 60
Se o programa imprimir:
40
temos evidência de que alguma coisa está errada.
Isso é uma forma muito inicial de pensar em teste:
entrada conhecida
↓
resultado esperado
↓
resultado obtido
↓
comparação
Nosso framework de testes ainda está muito distante.
Mas a mentalidade começa agora.
42. Expressões gigantes dificultam debugging
Imagine algo como:
int result = ((baseDamage + bonusDamage) * criticalMultiplier + playerLevel * 5 - armor) / 2;
Talvez seja válido.
Mas agora precisamos perguntar:
qual regra isso representa?
por que existe
5?por que dividimos por
2?o multiplicador afeta qual parte?
armoré aplicado antes ou depois?o resultado intermediário pode ser negativo?
estamos utilizando divisão inteira?
Uma única linha começou a esconder várias decisões.
43. Quebrando uma expressão complexa
Poderíamos evoluir para algo como:
int levelBonus = playerLevel * 5;
int rawDamage = (baseDamage + bonusDamage) * criticalMultiplier;
int damageAfterArmor = rawDamage - armor;
int finalDamage = (damageAfterArmor + levelBonus) / 2;
Agora cada etapa possui um significado.
Podemos inspecionar:
levelBonus
rawDamage
damageAfterArmor
finalDamage
Não significa que toda expressão complexa deve obrigatoriamente ser quebrada.
Significa que legibilidade e capacidade de investigação importam.
44. Ainda não vamos criar funções
Você pode perceber que o cálculo de dano começou a crescer.
Talvez já esteja pensando:
CalculateDamage(...)
E essa ideia será excelente.
Mas funções serão introduzidas no Capítulo 17.
Até lá precisamos sentir o problema real:
main.cpp cresce
↓
código se repete
↓
responsabilidades começam a se misturar
↓
precisamos organizar comportamento
Só então a função aparecerá como solução natural.
Não vamos antecipá-la.
45. Ainda não vamos criar uma classe DamageCalculator
Muito menos:
DamageCalculator
DamageService
CombatSystem
CombatManager
DamageStrategy
Nosso problema atual cabe em algumas expressões.
Criar abstrações agora esconderia justamente o conhecimento que estamos tentando desenvolver.
46. Nossa estrutura continua mínima
CppGameEngine/
└── main.cpp
Esse é um ponto importante da série.
Estamos avançando bastante em conhecimento sem inflar artificialmente a arquitetura.
Arquitetura deve surgir porque o sistema exige organização.
Não porque queremos parecer sofisticados.
47. Microdesafio — precedência
Sem executar primeiro, tente descobrir:
int result = 10 + 4 * 3;
Qual é o resultado?
Depois faça a avaliação manual:
4 * 3 = 12
10 + 12 = 22
Resultado esperado:
22
Agora execute e confirme.
48. Microdesafio — parênteses
Altere para:
int result = (10 + 4) * 3;
Agora:
10 + 4 = 14
14 * 3 = 42
Resultado:
42
Compare:
10 + 4 * 3
→ 22
(10 + 4) * 3
→ 42
49. Microdesafio — subexpressões
Considere:
int baseDamage = 15;
int weaponBonus = 5;
int multiplier = 3;
int finalDamage = (baseDamage + weaponBonus) * multiplier;
Identifique:
Operandos
baseDamage
weaponBonus
multiplier
Operadores
+
*
Subexpressão
baseDamage + weaponBonus
Expressão completa
(baseDamage + weaponBonus) * multiplier
Resultado
(15 + 5) * 3
20 * 3
60
50. Microdesafio — divisão inteira escondida
Considere:
int currentHealth = 75;
int maxHealth = 100;
float healthPercentage = currentHealth / maxHealth * 100.0f;
Antes de executar, qual resultado você espera?
Talvez:
75
Mas observe:
currentHealth / maxHealth
é:
75 / 100
com operandos inteiros.
Então:
75 / 100
→ 0
Depois:
0 * 100.0f
→ 0.0
Temos um bug lógico.
51. Corrigindo a porcentagem
Uma forma simples, com o conhecimento atual:
float currentHealth = 75.0f;
float maxHealth = 100.0f;
float healthPercentage = currentHealth / maxHealth * 100.0f;
Agora:
75.0 / 100.0
→ 0.75
0.75 * 100.0
→ 75.0
Resultado:
75
Mais tarde estudaremos conversões explícitas e teremos outras formas de resolver o problema.
52. Esse bug mostra por que tipos continuam importantes
A expressão parecia matematicamente correta:
75 / 100 × 100
Mas em C++:
75 / 100
com inteiros produz:
0
Portanto:
matemática conceitual
+
tipos C++
+
operadores C++
=
resultado real do programa
Não podemos ignorar nenhuma dessas partes.
53. Um cenário um pouco mais completo
Vamos combinar alguns conceitos sem criar sistemas novos.
Queremos representar:
Base Damage: 20
Bonus Damage: 10
Critical Multiplier: 2
Armor: 15
Regra didática:
Raw Damage = (Base Damage + Bonus Damage) × Critical Multiplier
Final Damage = Raw Damage - Armor
Em C++:
int rawDamage = (baseDamage + bonusDamage) * criticalMultiplier;
int finalDamage = rawDamage - armor;
54. Implementação
#include <iostream>
int main()
{
int baseDamage = 20;
int bonusDamage = 10;
int criticalMultiplier = 2;
int armor = 15;
int rawDamage = (baseDamage + bonusDamage) * criticalMultiplier;
int finalDamage = rawDamage - armor;
std::cout << "=== DAMAGE CALCULATION ===" << std::endl;
std::cout << "Base Damage: " << baseDamage << std::endl;
std::cout << "Bonus Damage: " << bonusDamage << std::endl;
std::cout << "Critical Multiplier: " << criticalMultiplier << std::endl;
std::cout << "Armor: " << armor << std::endl;
std::cout << "Raw Damage: " << rawDamage << std::endl;
std::cout << "Final Damage: " << finalDamage << std::endl;
return 0;
}
Resultado:
=== DAMAGE CALCULATION ===
Base Damage: 20
Bonus Damage: 10
Critical Multiplier: 2
Armor: 15
Raw Damage: 60
Final Damage: 45
55. Avaliando manualmente
Sempre que estivermos aprendendo expressões, vale fazer o cálculo manual.
Temos:
baseDamage = 20
bonusDamage = 10
criticalMultiplier = 2
armor = 15
Primeira expressão:
(baseDamage + bonusDamage) * criticalMultiplier
Substituindo:
(20 + 10) * 2
Então:
30 * 2
Resultado:
60
Segunda expressão:
rawDamage - armor
Substituindo:
60 - 15
Resultado:
45
Nosso programa deve imprimir:
Final Damage: 45
56. E se armor for maior que o dano?
Imagine:
int armor = 100;
Então:
Raw Damage: 60
60 - 100 = -40
Teríamos:
Final Damage: -40
Matematicamente a expressão funciona.
C++ também consegue calculá-la.
Mas será que dano negativo faz sentido para nossa regra?
Talvez não.
Mais uma vez:
expressão válida ≠ regra de gameplay válida
Ainda não sabemos tomar decisões com if.
Esse problema ficará registrado.
57. Os problemas começam a pedir decisões
Já acumulamos situações como:
Health pode ficar negativo.
Coins podem ficar negativas.
Armor pode produzir dano negativo.
Divisor pode ser zero.
Experience pode ultrapassar um limite.
Até agora apenas observamos essas limitações.
Logo aprenderemos a fazer o programa escolher caminhos diferentes.
Mas antes disso ainda precisamos consolidar como expressões produzem resultados.
58. Código final do capítulo
Vamos manter uma implementação que reúne o que realmente precisamos neste momento:
#include <iostream>
int main()
{
const int MAX_PLAYER_HEALTH = 100;
int playerHealth = 75;
int baseDamage = 20;
int bonusDamage = 10;
int criticalMultiplier = 2;
int armor = 15;
int rawDamage = (baseDamage + bonusDamage) * criticalMultiplier;
int finalDamage = rawDamage - armor;
float currentHealth = 75.0f;
float maximumHealth = 100.0f;
float healthPercentage = currentHealth / maximumHealth * 100.0f;
std::cout << "=== PLAYER STATE ===" << std::endl;
std::cout << "Health: " << playerHealth << " / " << MAX_PLAYER_HEALTH << std::endl;
std::cout << std::endl;
std::cout << "=== DAMAGE CALCULATION ===" << std::endl;
std::cout << "Base Damage: " << baseDamage << std::endl;
std::cout << "Bonus Damage: " << bonusDamage << std::endl;
std::cout << "Critical Multiplier: " << criticalMultiplier << std::endl;
std::cout << "Armor: " << armor << std::endl;
std::cout << "Raw Damage: " << rawDamage << std::endl;
std::cout << "Final Damage: " << finalDamage << std::endl;
std::cout << std::endl;
std::cout << "=== HEALTH CALCULATION ===" << std::endl;
std::cout << "Health Percentage: " << healthPercentage << "%" << std::endl;
return 0;
}
Resultado esperado:
=== PLAYER STATE ===
Health: 75 / 100
=== DAMAGE CALCULATION ===
Base Damage: 20
Bonus Damage: 10
Critical Multiplier: 2
Armor: 15
Raw Damage: 60
Final Damage: 45
=== HEALTH CALCULATION ===
Health Percentage: 75%
59. Por que mantivemos playerHealth e currentHealth?
Neste exemplo didático temos:
int playerHealth = 75;
e:
float currentHealth = 75.0f;
Em um sistema real isso provavelmente seria redundante e uma fonte potencial de inconsistência.
Estamos fazendo isso apenas porque ainda não estudamos conversões explicitamente e queremos demonstrar divisão de ponto flutuante sem introduzir um assunto novo no meio do capítulo.
Essa não é uma arquitetura que queremos preservar.
É uma simplificação didática temporária.
Mais tarde poderemos refatorá-la.
60. Não esconda simplificações didáticas
Esse princípio será importante durante toda a série.
Quando fizermos algo apenas para facilitar um conceito, devemos dizer claramente:
isto existe para o exercício atual e não representa necessariamente a solução definitiva.
Assim evitamos transformar exemplos didáticos em padrões arquiteturais acidentais.
61. Validando a implementação
Faça:
Ctrl + S
Depois:
Ctrl + Shift + B
Confirme:
Build succeeded
Execute:
Ctrl + F5
Compare os resultados.
Principalmente:
Raw Damage: 60
Final Damage: 45
Health Percentage: 75%
62. Testes manuais do capítulo
Faça três pequenas alterações.
Teste 1
int bonusDamage = 0;
Resultado esperado:
Raw Damage: 40
porque:
(20 + 0) * 2 = 40
Teste 2
int criticalMultiplier = 3;
Com bônus novamente em 10:
(20 + 10) * 3 = 90
Teste 3
Remova os parênteses:
int rawDamage = baseDamage + bonusDamage * criticalMultiplier;
Com:
20
10
2
resultado:
40
Explique para você mesmo por que mudou.
Se conseguir explicar sem apenas dizer "porque C++ faz assim", o objetivo principal do capítulo foi atingido.
63. Checklist de compreensão
Antes de avançar, tente responder sem consultar o artigo:
O que é uma expressão?
O que é um operando?
O que é um operador?
O que é uma subexpressão?
O que significa avaliar uma expressão?
Qual é a diferença entre calcular um valor e atribuí-lo?
O que é precedência?
O que é associatividade?
Por que parênteses podem mudar o resultado?
Por que o tipo dos operandos importa?
Por que
float result = 5 / 2;não produz2.5?Por que uma expressão que compila ainda pode conter um bug lógico?
Se alguma resposta ainda estiver nebulosa, volte aos exemplos e execute-os novamente.
64. Git
Depois de validar:
git status
Adicione:
git add CppGameEngine/main.cpp
Ajuste o caminho caso sua estrutura local seja diferente.
Commit sugerido:
git commit -m "feat: add compound gameplay expressions"
Esse commit registra a evolução:
nosso programa agora combina valores e operadores em expressões mais significativas.
65. Atualizando o CHANGELOG
Em [Unreleased]:
### Added
- Added compound gameplay expressions.
- Added damage calculation using arithmetic precedence and parentheses.
- Added intermediate values for clearer expression evaluation.
- Added floating-point health percentage calculation.
- Added manual validation examples for expression results.
Depois:
git add CHANGELOG.md
git commit -m "docs: update changelog with gameplay expressions"
Não precisamos atualizar o ROADMAP técnico neste capítulo.
66. O que aprendemos?
Começamos com operações simples:
playerHealth - damage
e avançamos para:
(baseDamage + bonusDamage) * criticalMultiplier
Aprendemos que uma expressão combina elementos da linguagem para produzir um valor.
Conhecemos melhor:
operandos
operadores
expressões
subexpressões
Também introduzimos:
precedência
associatividade
parênteses
tipo da expressão
E fizemos uma distinção importante:
precedência e associatividade dizem respeito ao agrupamento da expressão; não devemos confundi-las automaticamente com todas as regras de ordem de avaliação da linguagem.
Também descobrimos bugs que:
compilam
executam
produzem resultado
mas ainda assim estão errados.
Isso representa uma evolução importante em nossa visão sobre debugging.
67. O estado atual do nosso conhecimento
Começamos a série com:
std::cout << "Game Engineering com C++ começou!" << std::endl;
Depois aprendemos:
variáveis
↓
tipos
↓
constantes
↓
operadores
↓
expressões
Agora já conseguimos representar cálculos como:
(Base Damage + Bonus Damage)
×
Critical Multiplier
-
Armor
e traduzi-los para C++.
Mas nossos programas ainda executam praticamente tudo de maneira linear.
Eles recebem instruções e continuam.
Não conseguem realmente perguntar:
Health chegou a zero?
Player possui Coins suficientes?
Experience atingiu o necessário?
Inventory está cheio?
Armor é maior que Damage?
Divisor é zero?
Nosso programa ainda não sabe decidir.
E essa limitação está ficando impossível de ignorar.
Conclusão
Expressões são uma das bases sobre as quais construiremos praticamente toda a lógica futura do jogo.
Movimento poderá envolver algo conceitualmente parecido com:
position + velocity × deltaTime
Dano poderá envolver:
(baseDamage + bonusDamage) × multiplier
Porcentagens:
currentValue / maximumValue × 100
Economia:
basePrice × quantity
Experiência:
totalExperience / experiencePerLevel
Física, animação, câmera, IA, agricultura, economia e muitos outros sistemas utilizarão expressões continuamente.
Mas cálculos sozinhos não constroem comportamento inteligente.
Precisamos conseguir avaliar uma situação e escolher o que fazer.
Já encontramos problemas como:
playerHealth < 0
playerCoins insuficientes
damage negativo
divisão por zero
Ainda não conseguimos reagir a eles.
Isso muda no próximo capítulo.
Próximo capítulo
Capítulo 10 — Decisões: ensinando nosso programa a escolher
Série: Do zero ao jogo comercial com C++ e SFML
Até agora nosso programa segue aproximadamente:
instrução
↓
instrução
↓
instrução
↓
instrução
No próximo capítulo começaremos a construir algo diferente:
condição
│
├── verdadeira → executar uma ação
│
└── falsa → não executar aquela ação
Partiremos de problemas que já existem em nosso próprio código:
Health não deveria ficar abaixo de zero.
Player não deveria comprar sem Coins suficientes.
Não deveríamos dividir por zero.
Talvez o jogador esteja morto quando Health chegar a zero.
E finalmente conheceremos uma das estruturas mais importantes da programação:
if
Mas não começaremos decorando sintaxe.
Começaremos pelo problema:
nosso programa precisa observar uma situação e decidir o que fazer.
Depois construiremos a solução passo a passo.
Nosso código está prestes a deixar de ser apenas uma sequência fixa de cálculos.
No próximo capítulo, ele começará a tomar decisões.
Comentários
Ainda não há comentários. Sê o primeiro a comentar!