Capítulo 19 — Valores de retorno: quando uma função precisa devolver um resultado
Série: Do zero ao jogo comercial com C++ e SFML
Capítulo 19 — Valores de retorno: quando uma função precisa devolver um resultado
Série: Do zero ao jogo comercial com C++ e SFML
Algumas funções apenas executam ações. Outras precisam calcular algo e devolver esse resultado para quem as chamou.
No capítulo anterior nossas funções finalmente aprenderam a receber informações.
Começamos com funções sem parâmetros:
void ShowGameStart()
{
std::cout << "MINI GAME LOOP START" << std::endl;
}
Depois evoluímos para:
void ShowPlayerHealth(int playerHealth)
{
std::cout << "Health: " << playerHealth << std::endl;
}
e:
void ShowPlayerState(int frame, int playerHealth, int playerCoins)
{
std::cout << "Frame: " << frame << std::endl;
std::cout << "Health: " << playerHealth << std::endl;
std::cout << "Coins: " << playerCoins << std::endl;
}
Agora nossas funções conseguem receber dados explicitamente.
Mas encontramos um problema importante.
Considere:
void ApplyDamage(int playerHealth, int damage)
{
playerHealth -= damage;
}
E:
int playerHealth = 100;
ApplyDamage(playerHealth, 25);
std::cout << playerHealth << std::endl;
O resultado continua sendo:
100
Dentro da função, o parâmetro recebeu seu próprio valor.
Alterá-lo não modificou automaticamente a variável original de main().
Então surge uma nova pergunta:
E se, em vez de tentar alterar a variável externa, a função calcular o novo valor e devolvê-lo?
É exatamente isso que vamos aprender agora.
1. Partindo do problema
Queremos calcular:
Health atual: 100
Damage: 25
Resultado:
Nova Health: 75
Poderíamos fazer diretamente:
playerHealth = playerHealth - damage;
Mas agora queremos que uma função represente esse cálculo.
Conceitualmente:
Health atual
+
Damage
↓
função calcula
↓
nova Health
↓
valor volta para main()
Precisamos de uma função que produza um resultado.
2. Nossa primeira função que retorna um valor
Podemos escrever:
int CalculateHealthAfterDamage(int playerHealth, int damage)
{
return playerHealth - damage;
}
Aqui temos duas novidades importantes trabalhando juntas:
int
antes do nome da função.
E:
return
dentro dela.
3. O int agora é o tipo de retorno
Compare:
void ShowPlayerHealth(int playerHealth)
com:
int CalculateHealthAfterDamage(int playerHealth, int damage)
Na primeira:
void
indica que não esperamos um valor de retorno.
Na segunda:
int
indica:
esta função produz um valor do tipo
int.
Então:
int CalculateHealthAfterDamage(...)
promete devolver um inteiro.
4. O papel de return
Dentro da função:
return playerHealth - damage;
A expressão:
playerHealth - damage
é avaliada.
Se:
playerHealth = 100
damage = 25
temos:
100 - 25
=
75
Então:
return 75;
conceitualmente envia esse valor de volta para o ponto em que a função foi chamada.
5. Utilizando o valor retornado
Podemos escrever:
int playerHealth = 100;
int damage = 25;
playerHealth = CalculateHealthAfterDamage(playerHealth, damage);
Observe o fluxo:
playerHealth = 100
damage = 25
↓
CalculateHealthAfterDamage(100, 25)
↓
100 - 25
↓
75
↓
return 75
↓
playerHealth = 75
Agora a variável original em main() é atualizada porque atribuímos a ela o valor retornado pela função.
6. Programa completo
#include <iostream>
int CalculateHealthAfterDamage(int playerHealth, int damage);
int main()
{
int playerHealth = 100;
int damage = 25;
playerHealth = CalculateHealthAfterDamage(playerHealth, damage);
std::cout << "Health: " << playerHealth << std::endl;
return 0;
}
int CalculateHealthAfterDamage(int playerHealth, int damage)
{
return playerHealth - damage;
}
Resultado:
Health: 75
Agora nossa função:
CalculateHealthAfterDamage
realmente produz uma informação útil para quem a chamou.
7. A função não modificou diretamente a variável externa
Esse detalhe é importante.
Dentro dela:
int CalculateHealthAfterDamage(int playerHealth, int damage)
{
return playerHealth - damage;
}
não temos acesso direto à variável original de main().
Recebemos valores.
Calculamos.
Retornamos um novo valor.
Depois main() decide o que fazer com esse resultado:
playerHealth = CalculateHealthAfterDamage(playerHealth, damage);
Isso deixa o fluxo de dados bastante explícito.
8. A função pode calcular sem alterar estado externo
Podemos pensar:
ENTRADAS
playerHealth
damage
↓ FUNÇÃO ↓
CÁLCULO
playerHealth - damage
↓ RETORNO ↓
RESULTADO
newHealth
Essa estrutura será muito útil.
Uma função pode receber dados e produzir uma resposta sem depender de estado global.
9. Podemos armazenar o retorno em outra variável
Não somos obrigados a sobrescrever playerHealth imediatamente.
Podemos fazer:
int newHealth = CalculateHealthAfterDamage(playerHealth, damage);
Agora temos:
playerHealth = 100
newHealth = 75
Isso pode ser útil quando queremos observar o resultado antes de decidir o que fazer.
10. Exemplo completo preservando o valor original
#include <iostream>
int CalculateHealthAfterDamage(int playerHealth, int damage);
int main()
{
int playerHealth = 100;
int damage = 25;
int newHealth = CalculateHealthAfterDamage(playerHealth, damage);
std::cout << "Current Health: " << playerHealth << std::endl;
std::cout << "Health After Damage: " << newHealth << std::endl;
return 0;
}
int CalculateHealthAfterDamage(int playerHealth, int damage)
{
return playerHealth - damage;
}
Resultado:
Current Health: 100
Health After Damage: 75
A função calculou.
Quem chamou decidiu onde armazenar.
11. Podemos usar o retorno diretamente em uma expressão
Também podemos fazer:
std::cout << CalculateHealthAfterDamage(100, 25) << std::endl;
A função retorna:
75
e esse valor participa diretamente da expressão usada por std::cout.
Isso mostra algo importante:
uma chamada de função que retorna valor pode participar de outras expressões.
12. Uma chamada pode produzir um valor
Antes tínhamos expressões como:
playerHealth - damage
Agora também podemos ter:
CalculateHealthAfterDamage(playerHealth, damage)
que produz um valor.
Então isto:
int newHealth = CalculateHealthAfterDamage(playerHealth, damage);
possui uma expressão do lado direito cujo resultado vem de uma função.
13. O tipo do retorno precisa fazer sentido
Se declaramos:
int CalculateHealthAfterDamage(int playerHealth, int damage)
a função promete produzir um:
int
Portanto:
return playerHealth - damage;
faz sentido porque a expressão produz um valor inteiro neste contexto.
Da mesma maneira podemos ter:
float CalculateSpeed(float baseSpeed, float multiplier)
{
return baseSpeed * multiplier;
}
Ou:
bool IsPlayerDefeated(int playerHealth)
{
return playerHealth <= 0;
}
O tipo de retorno deve representar o tipo de resultado produzido.
14. Uma função pode retornar bool
Já conhecemos expressões booleanas:
playerHealth <= 0
Então podemos criar:
bool IsPlayerDefeated(int playerHealth)
{
return playerHealth <= 0;
}
Se:
playerHealth = 0
a expressão retorna:
true
Se:
playerHealth = 50
retorna:
false
15. Utilizando uma função booleana no if
Podemos escrever:
if (IsPlayerDefeated(playerHealth))
{
std::cout << "Player defeated." << std::endl;
}
Observe como a condição fica legível.
Em vez de:
if (playerHealth <= 0)
temos:
if (IsPlayerDefeated(playerHealth))
A função dá um nome para a pergunta.
16. Mas não extraia toda comparação para uma função
Isso não significa que devemos substituir automaticamente:
if (playerHealth <= 0)
por uma função em todos os lugares.
Se a comparação é simples, aparece uma vez e já é clara, talvez a função não adicione valor.
Neste caso estamos utilizando:
IsPlayerDefeated
porque é um excelente exemplo didático de retorno bool.
A decisão real de extração deve continuar considerando clareza e responsabilidade.
17. Uma função pode retornar float
Imagine um pequeno cálculo:
float CalculateHealthPercentage(float currentHealth, float maximumHealth)
{
return currentHealth / maximumHealth * 100.0f;
}
Chamamos:
float healthPercentage = CalculateHealthPercentage(75.0f, 100.0f);
Resultado:
75
Temos:
entrada float
↓
expressão
↓
resultado float
O conceito é o mesmo.
18. return também encerra a função
Existe outra característica fundamental.
Quando uma função executa:
return value;
ela termina naquele ponto e devolve o controle para quem a chamou.
Considere:
int GetDefaultCoins()
{
return 10;
std::cout << "This will not execute." << std::endl;
}
Depois de:
return 10;
a função já terminou.
A instrução seguinte não faz parte de um caminho executável após aquele retorno.
19. Não coloque código depois de um retorno incondicional esperando que ele execute
Evite:
int CalculateReward()
{
int reward = 10;
return reward;
reward += 5;
}
O:
reward += 5;
não será utilizado para alterar o valor que já foi retornado.
Organize a função para que o fluxo fique claro.
20. Podemos calcular primeiro e retornar depois
Em vez de:
return playerHealth - damage;
podemos escrever:
int CalculateHealthAfterDamage(int playerHealth, int damage)
{
int newHealth = playerHealth - damage;
return newHealth;
}
As duas versões podem funcionar.
A segunda torna explícito o valor intermediário:
newHealth
Isso pode ajudar no aprendizado e em cálculos maiores.
21. Qual versão preferir?
Para um cálculo simples:
return playerHealth - damage;
é perfeitamente legível.
Se a lógica começar a crescer:
int newHealth = playerHealth - damage;
if (newHealth < 0)
{
newHealth = 0;
}
return newHealth;
a variável intermediária se torna bastante útil.
Ela representa um conceito.
22. Vamos proteger Health contra valores negativos
No capítulo do if, aprendemos a impedir Health negativa.
Podemos trazer essa regra para a função:
int CalculateHealthAfterDamage(int playerHealth, int damage)
{
int newHealth = playerHealth - damage;
if (newHealth < 0)
{
newHealth = 0;
}
return newHealth;
}
Agora:
CalculateHealthAfterDamage(30, 50)
retorna:
0
e não:
-20
23. Nossa função começou a representar uma regra
Antes:
return playerHealth - damage;
era apenas um cálculo aritmético.
Agora:
if (newHealth < 0)
{
newHealth = 0;
}
introduz uma regra:
Health mínima é zero.
Temos:
dados
↓
cálculo
↓
validação
↓
resultado
Essa função já começa a representar uma pequena regra de domínio.
24. Ainda não é nosso sistema de combate
Não temos:
Armor
Critical
Resistance
DamageType
Buffs
Debuffs
CombatSystem
Nem precisamos.
Nossa função existe para ensinar retorno.
Não vamos transformá-la prematuramente em uma arquitetura de combate.
25. Uma função que calcula recompensa
Também podemos escrever:
int CalculateCoinsReward(int baseCoins, int bonusCoins)
{
return baseCoins + bonusCoins;
}
Uso:
int reward = CalculateCoinsReward(3, 2);
Resultado:
5
Ou:
playerCoins += CalculateCoinsReward(3, 2);
Agora a função participa de uma expressão maior.
26. Funções com retorno podem ser combinadas com operadores
Considere:
int playerCoins = 10;
playerCoins += CalculateCoinsReward(3, 2);
Primeiro:
CalculateCoinsReward(3, 2)
↓
5
Depois:
playerCoins += 5
Resultado:
15
O valor retornado se comporta como qualquer outro valor compatível naquele contexto.
27. A função deve responder com o tipo certo
Imagine:
bool IsPlayerDefeated(int playerHealth)
{
return playerHealth <= 0;
}
A função responde a uma pergunta:
Player está derrotado?
Então:
bool
é um tipo de retorno natural.
Agora:
int CalculateCoinsReward(int baseCoins, int bonusCoins)
responde:
Quantas Coins?
Então:
int
faz sentido.
O tipo de retorno também comunica intenção.
28. void versus função com retorno
Compare:
void ShowPlayerHealth(int playerHealth)
{
std::cout << playerHealth << std::endl;
}
e:
int CalculateHealthAfterDamage(int playerHealth, int damage)
{
return playerHealth - damage;
}
A primeira:
executa comportamento
não devolve valor
A segunda:
calcula
produz um valor
devolve esse valor
Isso não significa que toda função deve se encaixar rigidamente em apenas uma categoria conceitual para sempre.
Mas essa distinção inicial ajuda bastante.
29. Uma função de cálculo não precisa imprimir
Compare:
int CalculateHealthAfterDamage(int playerHealth, int damage)
{
int newHealth = playerHealth - damage;
std::cout << "Health: " << newHealth << std::endl;
return newHealth;
}
com:
int CalculateHealthAfterDamage(int playerHealth, int damage)
{
return playerHealth - damage;
}
Na segunda versão, a função apenas calcula.
Quem chamou decide se deve:
mostrar
armazenar
comparar
descartar
usar em outra expressão
Essa separação pode tornar a função mais reutilizável.
30. Separando cálculo de apresentação
Podemos fazer:
playerHealth = CalculateHealthAfterDamage(playerHealth, damage);
ShowPlayerHealth(playerHealth);
Temos:
CalculateHealthAfterDamage
→ cálculo
ShowPlayerHealth
→ apresentação
Duas responsabilidades diferentes.
Isso começa a tornar o desenho do programa mais interessante.
31. O retorno pode ser ignorado?
Tecnicamente podemos chamar uma função que retorna valor sem armazená-lo:
CalculateHealthAfterDamage(playerHealth, damage);
O cálculo acontece.
Mas o resultado é descartado.
Se nossa intenção era atualizar Health, isso provavelmente é um bug lógico.
Precisamos usar o valor:
playerHealth = CalculateHealthAfterDamage(playerHealth, damage);
32. Erro proposital — esquecer de utilizar o retorno
Comece:
int playerHealth = 100;
CalculateHealthAfterDamage(playerHealth, 25);
std::cout << playerHealth << std::endl;
Resultado:
100
Agora:
playerHealth = CalculateHealthAfterDamage(playerHealth, 25);
Resultado:
75
A função devolveu o resultado nos dois casos.
Mas apenas no segundo nós o utilizamos para atualizar o estado.
33. Isso é diferente de passagem por referência
Nosso fluxo atual é:
valor entra
↓
função calcula
↓
valor retorna
↓
quem chamou decide armazenar
Não estamos dizendo:
função modifica diretamente a variável original
Essa segunda possibilidade existe em C++ através de referências e outros mecanismos.
Mas ainda não precisamos dela.
Nossa solução atual deixa a transformação explícita:
playerHealth = CalculateHealthAfterDamage(playerHealth, damage);
34. Retorno e atribuição trabalham juntos
Observe:
playerHealth = CalculateHealthAfterDamage(playerHealth, damage);
Temos:
lado esquerdo
↓
onde o resultado será armazenado
e:
lado direito
↓
expressão que produz o novo valor
A chamada da função faz parte dessa expressão.
Esse padrão será extremamente comum.
35. Podemos utilizar retorno dentro de uma condição
Temos:
bool IsPlayerDefeated(int playerHealth)
{
return playerHealth <= 0;
}
Então:
if (IsPlayerDefeated(playerHealth))
{
currentState = GameState::GameOver;
}
Fluxo:
IsPlayerDefeated(playerHealth)
↓
true ou false
↓
if utiliza o resultado
Nenhuma variável intermediária é obrigatória.
36. Ou podemos armazenar primeiro
Também poderíamos escrever:
bool isDefeated = IsPlayerDefeated(playerHealth);
if (isDefeated)
{
currentState = GameState::GameOver;
}
As duas abordagens podem ser válidas.
A escolha depende da clareza.
Para uma condição pequena e bem nomeada:
if (IsPlayerDefeated(playerHealth))
é bastante expressiva.
37. Nomes de funções que retornam bool
Quando uma função responde a uma pergunta, nomes como:
IsPlayerDefeated()
HasEnoughCoins()
CanBuyItem()
IsRunning()
podem comunicar que esperamos uma resposta booleana.
No nosso projeto, continuamos utilizando:
PascalCase
para funções.
38. Cuidado para não esconder regras demais cedo demais
Não devemos imediatamente criar:
bool IsEverythingValid(...)
contendo dezenas de condições diferentes.
Uma função bem nomeada deve representar uma responsabilidade compreensível.
Por exemplo:
IsPlayerDefeated(playerHealth)
é específica.
39. Refatorando nosso mini Game Loop
Agora conseguimos começar a retirar mais responsabilidades de main() sem utilizar globais.
Podemos ter:
int CalculateHealthAfterDamage(int playerHealth, int damage);
int CalculateCoinsReward(int baseCoins, int bonusCoins);
bool IsPlayerDefeated(int playerHealth);
void ShowPlayerState(int frame, int playerHealth, int playerCoins);
Isso começa a permitir um Game Loop mais declarativo.
40. Uma versão evoluída
#include <iostream>
enum class GameState
{
Menu,
Playing,
GameOver
};
int CalculateHealthAfterDamage(int playerHealth, int damage);
int CalculateCoinsReward(int baseCoins, int bonusCoins);
bool IsPlayerDefeated(int playerHealth);
void ShowPlayerState(int frame, int playerHealth, int playerCoins);
void ShowGameOver();
int main()
{
const int DAMAGE_PER_FRAME = 20;
const int BASE_COINS_REWARD = 1;
const int BONUS_COINS_REWARD = 2;
bool isRunning = true;
int frame = 0;
int playerHealth = 100;
int playerCoins = 0;
GameState currentState = GameState::Menu;
while (isRunning)
{
frame++;
if (currentState == GameState::Menu)
{
currentState = GameState::Playing;
}
if (currentState == GameState::Playing)
{
playerHealth = CalculateHealthAfterDamage(playerHealth, DAMAGE_PER_FRAME);
int coinsReward = CalculateCoinsReward(BASE_COINS_REWARD, BONUS_COINS_REWARD);
playerCoins += coinsReward;
}
if (IsPlayerDefeated(playerHealth))
{
currentState = GameState::GameOver;
}
ShowPlayerState(frame, playerHealth, playerCoins);
if (currentState == GameState::GameOver)
{
ShowGameOver();
isRunning = false;
}
}
return 0;
}
int CalculateHealthAfterDamage(int playerHealth, int damage)
{
int newHealth = playerHealth - damage;
if (newHealth < 0)
{
newHealth = 0;
}
return newHealth;
}
int CalculateCoinsReward(int baseCoins, int bonusCoins)
{
return baseCoins + bonusCoins;
}
bool IsPlayerDefeated(int playerHealth)
{
return playerHealth <= 0;
}
void ShowPlayerState(int frame, int playerHealth, int playerCoins)
{
std::cout << "Frame: " << frame << std::endl;
std::cout << "Health: " << playerHealth << std::endl;
std::cout << "Coins: " << playerCoins << std::endl;
std::cout << std::endl;
}
void ShowGameOver()
{
std::cout << "Player defeated." << std::endl;
}
Agora nosso programa possui funções que:
recebem dados
↓
calculam
↓
devolvem resultados
sem depender de variáveis globais.
41. Prevendo o resultado
Temos:
Health inicial = 100
Damage por frame = 20
Então:
Frame 1 → Health 80
Frame 2 → Health 60
Frame 3 → Health 40
Frame 4 → Health 20
Frame 5 → Health 0
A recompensa por frame:
BASE = 1
BONUS = 2
Então:
1 + 2 = 3 Coins por frame
Depois de cinco frames:
5 × 3 = 15
Resultado final:
Health = 0
Coins = 15
Frames = 5
42. O código ficou mais expressivo
Dentro do update:
playerHealth = CalculateHealthAfterDamage(playerHealth, DAMAGE_PER_FRAME);
Essa linha comunica:
calcule a nova Health depois do dano e armazene o resultado.
Depois:
int coinsReward = CalculateCoinsReward(BASE_COINS_REWARD, BONUS_COINS_REWARD);
playerCoins += coinsReward;
comunica:
calcule a recompensa e depois aplique-a ao estado.
E:
if (IsPlayerDefeated(playerHealth))
comunica a pergunta diretamente.
43. Ainda não temos funções perfeitas
Nosso código está melhorando.
Mas isso não significa que chegamos a uma arquitetura final.
Por exemplo:
CalculateHealthAfterDamage
talvez futuramente pertença a:
Player
Stats
Combat
Damage
dependendo de como nosso domínio evoluir.
Não precisamos decidir isso agora.
Primeiro estamos aprendendo funções.
44. Retornar um valor facilita testes mentais
Considere:
int CalculateCoinsReward(int baseCoins, int bonusCoins)
{
return baseCoins + bonusCoins;
}
Podemos pensar em casos:
Entrada: 1, 2
Esperado: 3
Entrada: 10, 5
Esperado: 15
Entrada: 0, 0
Esperado: 0
A função possui:
entradas conhecidas
↓
resultado esperado
Isso é naturalmente testável.
45. Funções sem efeitos externos são particularmente fáceis de raciocinar
Nossa função:
int CalculateCoinsReward(int baseCoins, int bonusCoins)
{
return baseCoins + bonusCoins;
}
não:
imprime
altera global
abre arquivo
muda janela
Ela apenas:
recebe
calcula
retorna
Isso torna o comportamento bastante previsível.
Mais tarde discutiremos conceitos mais profundos relacionados a funções puras e efeitos colaterais.
Ainda não precisamos formalizá-los agora.
46. Erro proposital — esquecer return
Considere:
int CalculateCoinsReward(int baseCoins, int bonusCoins)
{
int reward = baseCoins + bonusCoins;
}
A função foi declarada como:
int
Portanto esperamos que um caminho normal da função produza um valor compatível.
Mas não existe:
return reward;
Faça Build.
Observe os warnings ou erros produzidos pela sua configuração.
Depois corrija:
return reward;
47. Não ignore warnings sobre retorno
Dependendo do compilador e das configurações, uma função que deveria retornar algo mas não o faz corretamente pode gerar diagnóstico.
Nossa regra continua:
warning deve ser investigado.
Especialmente quando envolve fluxo de retorno.
48. Erro proposital — tipo de retorno incompatível
Imagine:
int CalculateReward()
{
return 4.5f;
}
Existe uma conversão possível nesse caso, mas o valor será convertido para o tipo retornado.
Tal comportamento pode perder informação.
Ainda teremos capítulos específicos sobre conversões.
Neste estágio, prefira que o tipo de retorno corresponda naturalmente ao conceito.
Se queremos preservar ponto flutuante:
float CalculateMultiplier()
{
return 4.5f;
}
49. Não escolha int por hábito
Agora que funções começam a retornar valores, precisamos pensar:
qual é a natureza do resultado?
Exemplos:
int CalculateCoinsReward(...)
Coins inteiras.
float CalculateSpeed(...)
Velocidade.
bool IsPlayerDefeated(...)
Resposta verdadeira ou falsa.
O tipo continua sendo parte da modelagem.
50. Erro proposital — retorno descartado
Faça:
int playerHealth = 100;
CalculateHealthAfterDamage(playerHealth, 25);
std::cout << playerHealth << std::endl;
Resultado:
100
Depois:
playerHealth = CalculateHealthAfterDamage(playerHealth, 25);
Resultado:
75
Explique a diferença sem dizer apenas:
porque faltava o
=.
A explicação melhor é:
a primeira chamada produziu um valor, mas nós o descartamos; na segunda, utilizamos o valor retornado para atualizar
playerHealth.
51. Microdesafio — calcular recompensa
Crie:
int CalculateCoinsReward(int baseCoins, int bonusCoins);
Defina:
int CalculateCoinsReward(int baseCoins, int bonusCoins)
{
return baseCoins + bonusCoins;
}
Teste:
int reward = CalculateCoinsReward(10, 5);
std::cout << reward << std::endl;
Esperado:
15
52. Microdesafio — verificar derrota
Crie:
bool IsPlayerDefeated(int playerHealth);
Implemente:
bool IsPlayerDefeated(int playerHealth)
{
return playerHealth <= 0;
}
Teste:
Health = 10 → false
Health = 1 → false
Health = 0 → true
Health = -1 → true
Essa é uma ótima pequena tabela de cenários.
53. Microdesafio — dano com limite zero
Implemente:
int CalculateHealthAfterDamage(int playerHealth, int damage)
com a regra:
Health nunca deve ficar abaixo de zero.
Teste:
HealthDamageEsperado10020802020020300110
Verifique se a função atende todos os cenários.
54. Microdesafio — utilizar retorno diretamente
Com:
bool IsPlayerDefeated(int playerHealth)
escreva:
if (IsPlayerDefeated(playerHealth))
{
std::cout << "Player defeated." << std::endl;
}
Depois altere Health e confirme os dois caminhos.
55. Código final do capítulo
Vamos manter uma versão que reúne parâmetros e valores de retorno sem antecipar referências:
#include <iostream>
enum class GameState
{
Menu,
Playing,
GameOver
};
int CalculateHealthAfterDamage(int playerHealth, int damage);
int CalculateCoinsReward(int baseCoins, int bonusCoins);
bool IsPlayerDefeated(int playerHealth);
void ShowPlayerState(int frame, int playerHealth, int playerCoins);
void ShowGameSummary(int frame, int playerHealth, int playerCoins);
void ShowGameOver();
int main()
{
const int DAMAGE_PER_FRAME = 20;
const int BASE_COINS_REWARD = 1;
const int BONUS_COINS_REWARD = 2;
bool isRunning = true;
int frame = 0;
int playerHealth = 100;
int playerCoins = 0;
GameState currentState = GameState::Menu;
std::cout << "=== MINI GAME LOOP START ===" << std::endl;
while (isRunning)
{
frame++;
if (currentState == GameState::Menu)
{
currentState = GameState::Playing;
}
if (currentState == GameState::Playing)
{
playerHealth = CalculateHealthAfterDamage(playerHealth, DAMAGE_PER_FRAME);
playerCoins += CalculateCoinsReward(BASE_COINS_REWARD, BONUS_COINS_REWARD);
}
if (IsPlayerDefeated(playerHealth))
{
currentState = GameState::GameOver;
}
ShowPlayerState(frame, playerHealth, playerCoins);
if (currentState == GameState::GameOver)
{
ShowGameOver();
isRunning = false;
}
}
std::cout << "=== MINI GAME LOOP FINISHED ===" << std::endl;
ShowGameSummary(frame, playerHealth, playerCoins);
return 0;
}
int CalculateHealthAfterDamage(int playerHealth, int damage)
{
int newHealth = playerHealth - damage;
if (newHealth < 0)
{
newHealth = 0;
}
return newHealth;
}
int CalculateCoinsReward(int baseCoins, int bonusCoins)
{
return baseCoins + bonusCoins;
}
bool IsPlayerDefeated(int playerHealth)
{
return playerHealth <= 0;
}
void ShowPlayerState(int frame, int playerHealth, int playerCoins)
{
std::cout << "Frame: " << frame << std::endl;
std::cout << "Health: " << playerHealth << std::endl;
std::cout << "Coins: " << playerCoins << std::endl;
std::cout << std::endl;
}
void ShowGameSummary(int frame, int playerHealth, int playerCoins)
{
std::cout << "Frames: " << frame << std::endl;
std::cout << "Final Health: " << playerHealth << std::endl;
std::cout << "Final Coins: " << playerCoins << std::endl;
}
void ShowGameOver()
{
std::cout << "Player defeated." << std::endl;
}
Resultado esperado:
=== MINI GAME LOOP START ===
Frame: 1
Health: 80
Coins: 3
Frame: 2
Health: 60
Coins: 6
Frame: 3
Health: 40
Coins: 9
Frame: 4
Health: 20
Coins: 12
Frame: 5
Health: 0
Coins: 15
Player defeated.
=== MINI GAME LOOP FINISHED ===
Frames: 5
Final Health: 0
Final Coins: 15
56. O main() está começando a ler como uma sequência de intenções
Observe:
playerHealth = CalculateHealthAfterDamage(playerHealth, DAMAGE_PER_FRAME);
playerCoins += CalculateCoinsReward(BASE_COINS_REWARD, BONUS_COINS_REWARD);
Depois:
if (IsPlayerDefeated(playerHealth))
{
currentState = GameState::GameOver;
}
E:
ShowPlayerState(frame, playerHealth, playerCoins);
Começamos a enxergar:
calcular
↓
decidir
↓
mostrar
sem precisar ler todos os detalhes na mesma região do código.
57. Mas nosso main.cpp ainda possui uma limitação
Agora temos funções no mesmo arquivo.
Isso é suficiente para nosso estágio.
Mas também estamos acumulando:
variáveis dentro de main
parâmetros
variáveis dentro das funções
Considere:
int playerHealth = 100;
dentro de main().
E:
int CalculateHealthAfterDamage(int playerHealth, int damage)
Aqui também existe um identificador:
playerHealth
Mas são a mesma variável?
Não.
Por que uma está disponível em determinado lugar e a outra em outro?
Quando exatamente cada uma começa a existir?
Quando deixa de existir?
Até onde podemos acessá-la?
Essas perguntas nos levam ao próximo fundamento.
58. Validando a implementação
Faça:
Ctrl + S
Depois:
Ctrl + Shift + B
Confirme:
Build succeeded
Execute:
Ctrl + F5
Valide especialmente:
Frames = 5
Health final = 0
Coins finais = 15
Depois altere os valores.
59. Cenários de validação
Teste:
Health inicialDamageReward/frameFrames esperados10020351002534100503210010031302032
Observe especialmente casos em que o dano não divide Health exatamente.
60. Checklist de compreensão
Antes de avançar, tente responder sem consultar o artigo:
O que é um valor de retorno?
O que significa o tipo antes do nome da função?
Qual a diferença entre
voideintcomo tipo de retorno?Para que serve
return?O que acontece com o fluxo quando
returné executado?Uma chamada de função pode participar de uma expressão?
Podemos armazenar o retorno em uma variável?
Podemos utilizar o retorno diretamente em um
if?O que acontece se ignorarmos um valor retornado?
Por que
playerHealth = CalculateHealthAfterDamage(...)atualiza o estado?Por que
CalculateHealthAfterDamage(...)sozinho não atualizaplayerHealth?Por que nossa função de cálculo não precisa imprimir nada?
Por que
bool IsPlayerDefeated(...)é uma assinatura coerente?Por que ainda não precisamos de referências?
Qual diferença existe entre receber dados, produzir um resultado e modificar estado externo?
Por que nossas variáveis com o mesmo nome dentro de funções diferentes não são necessariamente a mesma variável?
Se consegue explicar essas respostas, podemos continuar.
61. Estrutura do projeto
Continuamos:
CppGameEngine/
└── main.cpp
Não criaremos novos arquivos apenas porque possuímos mais funções.
Ainda estamos consolidando fundamentos.
Mas já temos uma estrutura conceitual:
includes
↓
tipos
↓
declarações de funções
↓
main
↓
definições
Esse conhecimento será importante quando chegarmos à separação entre headers e source files.
62. Git
Depois de validar:
git status
Adicione:
git add CppGameEngine/main.cpp
Ajuste o caminho caso necessário.
Commit sugerido:
git commit -m "refactor: return calculated game state values"
A principal evolução é a substituição de cálculos locais por funções que recebem dados e retornam resultados explícitos.
63. Atualizando o CHANGELOG
Em [Unreleased]:
### Added
- Added functions that return calculated gameplay values.
- Added health calculation with a minimum value of zero.
- Added coin reward calculation using function return values.
- Added boolean player defeat evaluation.
### Refactored
- Updated the mini Game Loop to consume returned calculation results.
- Reduced direct calculation details inside the main execution flow.
- Kept state transformations explicit without introducing global variables.
Depois:
git add CHANGELOG.md
git commit -m "docs: update changelog with function return values"
O ROADMAP técnico ainda não precisa de alteração neste ponto.
64. O que aprendemos?
No capítulo anterior nossas funções aprenderam a receber:
ShowPlayerState(frame, playerHealth, playerCoins);
Hoje aprenderam a responder.
Criamos:
int CalculateHealthAfterDamage(int playerHealth, int damage)
{
return playerHealth - damage;
}
E utilizamos:
playerHealth = CalculateHealthAfterDamage(playerHealth, damage);
Também construímos:
bool IsPlayerDefeated(int playerHealth)
{
return playerHealth <= 0;
}
e usamos:
if (IsPlayerDefeated(playerHealth))
Aprendemos que:
funções podem produzir valores;
o tipo de retorno comunica qual tipo de resultado esperamos;
returndevolve um valor para o chamador;returnencerra o fluxo daquela função naquele ponto;chamadas podem participar de expressões;
valores retornados podem ser armazenados, comparados ou utilizados diretamente;
ignorar um retorno significa descartar aquele resultado;
funções de cálculo podem separar transformação de apresentação;
passagem por valor continua sem modificar automaticamente o estado original;
retornar um novo valor torna a atualização explícita;
bool,int,floate outros tipos podem ser utilizados como retornos quando fizerem sentido;funções bem focadas começam a tornar nosso mini Game Loop mais legível e testável.
Conclusão
Nossa evolução com funções aconteceu em três passos muito claros.
Primeiro:
void ShowGameStart()
A função apenas executava um comportamento.
Depois:
void ShowPlayerHealth(int playerHealth)
Ela passou a receber informações.
Agora:
int CalculateHealthAfterDamage(int playerHealth, int damage)
ela recebe informações e produz um resultado.
Podemos visualizar:
FUNÇÃO SEM DADOS
ShowGameStart()
↓
executa
Depois:
FUNÇÃO COM ENTRADA
ShowPlayerHealth(playerHealth)
↓
recebe
↓
executa
Agora:
FUNÇÃO COM ENTRADA E SAÍDA
CalculateHealthAfterDamage(playerHealth, damage)
↓
recebe
↓
calcula
↓
return
↓
devolve resultado
Essa transformação muda bastante o que conseguimos construir.
Nosso código agora pode representar cálculos de forma explícita:
playerHealth = CalculateHealthAfterDamage(playerHealth, DAMAGE_PER_FRAME);
Perguntas:
if (IsPlayerDefeated(playerHealth))
E resultados reutilizáveis:
playerCoins += CalculateCoinsReward(BASE_COINS_REWARD, BONUS_COINS_REWARD);
Mas os parâmetros e variáveis locais também criaram um novo problema conceitual.
Temos:
int playerHealth = 100;
dentro de main().
E também:
int CalculateHealthAfterDamage(int playerHealth, int damage)
com outro playerHealth.
Por que esses nomes podem coexistir?
Por que a função não consegue acessar automaticamente todas as variáveis de main()?
Por que uma variável criada dentro de um bloco pode desaparecer quando saímos dele?
Onde exatamente um identificador pode ser utilizado?
Chegamos a uma ideia central da linguagem:
escopo.
Próximo capítulo
Capítulo 20 — Escopo: onde uma variável realmente existe
Série: Do zero ao jogo comercial com C++ e SFML
No próximo artigo partiremos exatamente de situações que já existem em nosso código:
int main()
{
int playerHealth = 100;
}
e:
int CalculateHealthAfterDamage(int playerHealth, int damage)
{
int newHealth = playerHealth - damage;
return newHealth;
}
Temos três nomes:
playerHealth
damage
newHealth
Mas eles não podem ser utilizados em qualquer lugar do programa.
Vamos investigar:
bloco
função
parâmetro
variável local
visibilidade
sombreamento de nomes
chaves
if
while
for
e entender por que uma variável declarada aqui:
if (condition)
{
int value = 10;
}
não pode simplesmente ser utilizada depois:
std::cout << value;
Também veremos por que colocar tudo como variável global parece resolver o problema de acesso, mas cria outros problemas de design e acoplamento.
Ainda não entraremos profundamente em duração de vida.
Primeiro responderemos:
onde um nome pode ser visto e utilizado?
Depois, no Capítulo 21, avançaremos para uma pergunta ainda mais profunda:
por quanto tempo aquele objeto realmente existe?
Nosso código já sabe dividir comportamento em funções.
Agora precisamos compreender os limites dentro dos quais seus dados são visíveis.
Comentários
Ainda não há comentários. Sê o primeiro a comentar!