Aeca

Capítulo 09 — Expressões: combinando valores e operações

Série: Do zero ao jogo comercial com C++ e SFML

Capítulo 09 — Expressões: combinando valores e operações
aeca

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 + 1030

30 * 260

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 - 2080
        ↓
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 * 260

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 * 220

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 * 220

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) * 260

          ↓

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 * 322

(10 + 4) * 342

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.0f0.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.00.75

0.75 * 100.075.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 produz 2.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.

Subscreve "Aeca" para receber atualizações diretamente na tua caixa de entrada
aeca

Subscreve aeca para reagir

Subscrever

Comentários

Ainda não há comentários. Sê o primeiro a comentar!

Subscreve Aeca para receber atualizações diretamente na tua caixa de entrada