Aeca

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
aeca

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 void e int como 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 atualiza playerHealth?

  • 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;

  • return devolve um valor para o chamador;

  • return encerra 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, float e 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.

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