Aeca

Capítulo 18 — Parâmetros: enviando dados para funções

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

Capítulo 18 — Parâmetros: enviando dados para funções
aeca

Nossas funções já sabem executar comportamentos. Agora elas precisam receber as informações necessárias para realizar seu trabalho.

No capítulo anterior começamos a resolver um problema que finalmente ficou evidente em nosso projeto:

  main.cpp começou a crescer

Nosso mini Game Loop já acumulava diferentes responsabilidades:

  inicialização
↓
processamento
↓
atualização
↓
validação
↓
exibição
↓
encerramento

Então criamos nossas primeiras funções:

  void ShowGameStart();
void ShowStartGameMessage();
void ShowGameOver();
void ShowGameFinished();
void ShowSeparator();

E começamos a substituir detalhes como:

  std::cout << "Player defeated." << std::endl;

por chamadas com intenção mais clara:

  ShowGameOver();

Foi nosso primeiro passo real para organizar o programa.

Mas encontramos uma limitação importante.

Considere:

  void ShowPlayerHealth()
{
}

Como essa função descobriria qual é a vida atual do jogador?

Nosso valor existe dentro de main():

  int playerHealth = 100;

Não queremos transformar tudo em variável global apenas para tornar os dados acessíveis.

Precisamos de uma maneira explícita de dizer:

esta função precisa receber esta informação para conseguir realizar seu trabalho.

Essa é uma das responsabilidades dos parâmetros.


1. Partindo do problema real

Temos:

  int main()
{
    int playerHealth = 100;

    std::cout << "Health: " << playerHealth << std::endl;

    return 0;
}

Gostaríamos de extrair o comportamento responsável por mostrar Health.

Talvez:

  void ShowPlayerHealth()
{
    std::cout << "Health: " << playerHealth << std::endl;
}

Mas existe um problema.

A variável:

  playerHealth

foi declarada dentro de:

  main()

A nova função não recebe automaticamente acesso a ela.

Precisamos entregar esse valor à função.


2. Nossa primeira função com parâmetro

Podemos escrever:

  void ShowPlayerHealth(int playerHealth)
{
    std::cout << "Health: " << playerHealth << std::endl;
}

Agora temos algo novo dentro dos parênteses:

  int playerHealth

Esse é um parâmetro.

Podemos ler:

ShowPlayerHealth precisa receber um valor do tipo int, que dentro da função será identificado pelo nome playerHealth.


3. Chamando a função

Dentro de main():

  int playerHealth = 100;

ShowPlayerHealth(playerHealth);

Programa completo:

  #include <iostream>

void ShowPlayerHealth(int playerHealth);

int main()
{
    int playerHealth = 100;

    ShowPlayerHealth(playerHealth);

    return 0;
}

void ShowPlayerHealth(int playerHealth)
{
    std::cout << "Health: " << playerHealth << std::endl;
}

Resultado:

  Health: 100

Agora nossa função recebeu a informação necessária.


4. Parâmetro e argumento não são a mesma coisa

Essa distinção é importante.

Na declaração:

  void ShowPlayerHealth(int playerHealth);

temos um parâmetro:

  int playerHealth

Na chamada:

  ShowPlayerHealth(playerHealth);

temos um argumento.

Podemos pensar:

  FUNÇÃO

void ShowPlayerHealth(int playerHealth)
                      └──────┬───────┘
                          parâmetro

E:

  CHAMADA

ShowPlayerHealth(playerHealth);
                 └────┬────┘
                    argumento

De maneira prática:

parâmetro é aquilo que a função declara que precisa receber; argumento é o valor ou expressão fornecido na chamada.


5. O argumento não precisa ter o mesmo nome

Considere:

  void ShowPlayerHealth(int health)
{
    std::cout << "Health: " << health << std::endl;
}

E:

  int playerHealth = 100;

ShowPlayerHealth(playerHealth);

Isso funciona.

Temos:

  argumento:
playerHealth

parâmetro:
health

Os nomes não precisam ser iguais.

O importante, neste exemplo, é que o valor fornecido seja compatível com o tipo esperado.

Mesmo assim, escolheremos nomes claros.


6. O parâmetro possui um tipo

Temos:

  void ShowPlayerHealth(int playerHealth)

A função espera:

  int

Se quisermos mostrar velocidade:

  void ShowPlayerSpeed(float playerSpeed)
{
    std::cout << "Speed: " << playerSpeed << std::endl;
}

E:

  float playerSpeed = 4.5f;

ShowPlayerSpeed(playerSpeed);

Resultado:

  Speed: 4.5

Os tipos continuam participando da nossa modelagem.


7. A declaração também precisa informar os parâmetros

Antes tínhamos:

  void ShowPlayerHealth();

Agora:

  void ShowPlayerHealth(int playerHealth);

A declaração precisa corresponder à função que realmente definiremos.

Mais abaixo:

  void ShowPlayerHealth(int playerHealth)
{
    std::cout << "Health: " << playerHealth << std::endl;
}

Temos o mesmo:

  tipo de retorno
nome
lista de parâmetros

na declaração e na definição.


8. A anatomia cresceu

Antes:

  void ShowGameStart()

Agora:

  void ShowPlayerHealth(int playerHealth)

Podemos decompor:

  void   ShowPlayerHealth   (int playerHealth)
 │            │                    │
 │            │                    └── parâmetro
 │            │
 │            └── nome da função
 │
 └── tipo de retorno

Dentro do parâmetro:

  int playerHealth
 │       │
 │       └── nome do parâmetro
 │
 └── tipo do parâmetro

9. O valor é entregue durante a chamada

Considere:

  int playerHealth = 100;

ShowPlayerHealth(playerHealth);

Conceitualmente:

  main()

playerHealth = 100
       │
       ▼
ShowPlayerHealth(playerHealth)
       │
       ▼
parâmetro playerHealth recebe 100
       │
       ▼
função utiliza 100

Então:

  std::cout << playerHealth;

dentro da função imprime:

  100

10. Podemos fornecer um literal diretamente

Isto também funciona:

  ShowPlayerHealth(75);

O argumento é:

  75

A função recebe esse valor em seu parâmetro:

  int playerHealth

Resultado:

  Health: 75

O argumento não precisa ser necessariamente uma variável.

Pode ser uma expressão compatível com aquilo que a função espera.


11. Também podemos enviar uma expressão

Por exemplo:

  int playerHealth = 100;
int damage = 20;

ShowPlayerHealth(playerHealth - damage);

A expressão:

  playerHealth - damage

é avaliada primeiro.

Temos:

  100 - 20 = 80

Então a função recebe:

  80

Resultado:

  Health: 80

Observe que isso não modifica automaticamente:

  playerHealth

Apenas calculamos um valor e o enviamos.


12. Uma função pode receber vários parâmetros

Mostrar apenas Health é útil.

Mas talvez queiramos mostrar um pequeno estado:

  Frame: 3
Health: 75
Coins: 4

Podemos escrever:

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

A declaração:

  void ShowPlayerState(int frame, int playerHealth, int playerCoins);

13. Chamando uma função com vários argumentos

Dentro de main():

  int frame = 3;
int playerHealth = 75;
int playerCoins = 4;

ShowPlayerState(frame, playerHealth, playerCoins);

A associação acontece pela posição:

  frame
↓
primeiro parâmetro

playerHealth
↓
segundo parâmetro

playerCoins
↓
terceiro parâmetro

Portanto a ordem importa.


14. Cuidado com argumentos na ordem errada

Considere:

  void ShowPlayerState(int frame, int playerHealth, int playerCoins);

E depois:

  ShowPlayerState(playerHealth, frame, playerCoins);

Todos os valores são:

  int

Então o compilador pode não ter nenhuma razão para rejeitar a chamada.

Mas semanticamente enviamos:

  Health onde deveria estar Frame

Frame onde deveria estar Health

O resultado ficará errado.

Isso é um bug lógico.


15. Tipos não conseguem detectar todos os erros

Se tivéssemos:

  void ShowPlayerData(int playerHealth, float playerSpeed);

e chamássemos tipos incompatíveis, o compilador poderia nos ajudar dependendo da situação.

Mas quando vários parâmetros utilizam o mesmo tipo:

  int
int
int

a ordem errada pode continuar perfeitamente válida para C++.

Isso nos ensina:

o sistema de tipos ajuda muito, mas não substitui uma API clara e uma chamada correta.

Nomes, quantidade de parâmetros e design também importam.


16. Evite funções com parâmetros sem significado claro

Compare:

  void ShowData(int value1, int value2, int value3)

com:

  void ShowPlayerState(int frame, int playerHealth, int playerCoins)

A segunda assinatura comunica muito mais.

Nossos parâmetros devem explicar aquilo que representam.


17. O parâmetro existe dentro da função

Considere:

  void ShowPlayerHealth(int playerHealth)
{
    std::cout << playerHealth << std::endl;
}

O parâmetro:

  playerHealth

pode ser utilizado dentro do corpo dessa função.

Ele é uma variável local associada à chamada da função.

Ainda teremos um capítulo específico sobre escopo.

Então não aprofundaremos todas as regras agora.

Por enquanto, guarde:

o parâmetro existe para que aquela função trabalhe com o dado recebido.


18. Passagem por valor

Nos exemplos atuais estamos utilizando algo chamado:

passagem por valor.

Por exemplo:

  void ShowPlayerHealth(int playerHealth)

Quando chamamos:

  ShowPlayerHealth(playerHealth);

o parâmetro da função recebe seu próprio valor inicial a partir do argumento fornecido.

Para tipos simples como int, podemos pensar didaticamente como uma cópia daquele valor.

Isso possui uma consequência muito importante.


19. Vamos descobrir a consequência na prática

Considere:

  #include <iostream>

void ApplyDamage(int playerHealth, int damage);

int main()
{
    int playerHealth = 100;

    ApplyDamage(playerHealth, 25);

    std::cout << "Health in main: " << playerHealth << std::endl;

    return 0;
}

void ApplyDamage(int playerHealth, int damage)
{
    playerHealth -= damage;

    std::cout << "Health inside function: " << playerHealth << std::endl;
}

Qual será a saída?

Talvez você espere:

  Health inside function: 75
Health in main: 75

Mas não será isso.


20. Execute

Resultado:

  Health inside function: 75
Health in main: 100

Dentro da função:

  playerHealth -= damage;

alterou o parâmetro local.

Mas o:

  playerHealth

que existe em main() permaneceu:

  100

Encontramos uma limitação importante da passagem por valor para esse caso.


21. Visualizando o que aconteceu

Em main():

  playerHealth
┌─────┐
│ 100 │
└─────┘

Chamamos:

  ApplyDamage(playerHealth, 25);

A função recebe valores:

  parâmetro playerHealth
┌─────┐
│ 100 │
└─────┘

parâmetro damage
┌────┐
│ 25 │
└────┘

Dentro da função:

  playerHealth
100 → 75

Mas o valor de main() continua:

  100

Modificar o parâmetro por valor não modifica automaticamente a variável original utilizada como argumento.


22. Isso não é um bug da linguagem

C++ está fazendo exatamente aquilo que pedimos.

Declaramos:

  void ApplyDamage(int playerHealth, int damage)

A função recebe os valores necessários para trabalhar.

Não dissemos que ela deveria acessar e modificar diretamente a variável original de main().

Essa diferença é importante.


23. Então passagem por valor é ruim?

Não.

Na verdade, para muitas funções, é exatamente o comportamento desejado.

Nossa função:

  void ShowPlayerHealth(int playerHealth)

precisa apenas observar o valor.

Ela não deveria modificar a Health original.

Passagem por valor funciona perfeitamente para esse exemplo didático.


24. Funções de leitura ficam claras

Considere:

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

Essa função recebe os valores de que precisa.

Exibe.

Termina.

Não modifica o estado do main().

Isso é simples e previsível.


25. Mas ApplyDamage precisa de outra estratégia

Nossa intenção conceitual é:

  Health atual
↓
aplicar dano
↓
obter nova Health

Poderíamos tentar resolver utilizando referências.

C++ oferece esse recurso.

Mas não vamos introduzi-lo apenas para escapar da dificuldade atual.

Existe uma solução que se encaixa perfeitamente no próximo conceito da nossa trilha:

a função pode calcular um novo valor e devolvê-lo para quem a chamou.

Por exemplo, conceitualmente:

  ApplyDamage
↓
recebe Health e Damage
↓
calcula nova Health
↓
retorna nova Health

Esse será o tema do Capítulo 19.


26. Não vamos antecipar referências

Você poderá encontrar código como:

  void ApplyDamage(int& playerHealth, int damage)

O:

  &

muda significativamente a forma como o parâmetro se relaciona com o argumento.

Referências são extremamente importantes em C++.

Mas merecem ser estudadas com cuidado.

Não precisamos delas para entender os fundamentos de parâmetros neste capítulo.

Primeiro vamos dominar passagem por valor e retorno.


27. Isso também explica por que não usamos globais

Seria fácil fazer:

  int playerHealth = 100;

void ApplyDamage()
{
    playerHealth -= 25;
}

Agora funciona aparentemente.

Mas a função depende de um valor que não aparece em sua assinatura.

Quando vemos:

  ApplyDamage();

não conseguimos perceber quais dados serão alterados.

O estado está escondido.

Para nosso aprendizado, preferimos dependências explícitas.


28. Compare as dependências

Com global:

  ApplyDamage();

Pergunta:

  Do que essa função depende?

Precisamos abrir a implementação para descobrir.

Agora imagine uma futura função como:

  ApplyDamage(playerHealth, damage);

Mesmo sem ver a implementação, sabemos:

  ela precisa de Health
ela precisa de Damage

A chamada começa a revelar dependências.

Essa ideia será muito importante em nossa arquitetura futura.


29. Refatorando Show State

No capítulo anterior ainda tínhamos dentro do Game Loop:

  std::cout << "Frame: " << frame << std::endl;
std::cout << "Health: " << playerHealth << std::endl;
std::cout << "Coins: " << playerCoins << std::endl;
std::cout << std::endl;

Agora podemos criar:

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

E dentro do loop:

  ShowPlayerState(frame, playerHealth, playerCoins);

Essa é uma refatoração muito mais interessante.

A função recebe explicitamente aquilo de que precisa.


30. Nosso Game Loop começa a ficar mais legível

Antes:

  while (isRunning)
{
    frame++;

    // ...

    std::cout << "Frame: " << frame << std::endl;
    std::cout << "Health: " << playerHealth << std::endl;
    std::cout << "Coins: " << playerCoins << std::endl;

    // ...
}

Agora:

  while (isRunning)
{
    frame++;

    // ...

    ShowPlayerState(frame, playerHealth, playerCoins);

    // ...
}

No nível do Game Loop enxergamos:

  mostrar estado do jogador

Na função enxergamos:

  como esse estado é mostrado

Estamos começando a separar intenção de implementação.


31. Uma função com dois parâmetros

Também podemos separar:

  void ShowGameSummary(int frame, int playerCoins)
{
    std::cout << "Frames: " << frame << std::endl;
    std::cout << "Final Coins: " << playerCoins << std::endl;
}

Então:

  ShowGameSummary(frame, playerCoins);

A função não precisa conhecer:

  playerHealth
currentState
isRunning

porque não utiliza esses valores.

Esse é outro princípio importante:

forneça à função os dados de que ela realmente precisa.


32. Não envie tudo "por garantia"

Evite algo assim:

  void ShowGameSummary(
    int frame,
    int playerHealth,
    int playerCoins,
    int damage,
    bool isRunning,
    GameState currentState
)

se a implementação usa apenas:

  frame
playerCoins

Parâmetros desnecessários aumentam acoplamento e confundem responsabilidades.

No nosso estilo de código, mantendo a assinatura compacta quando couber:

  void ShowGameSummary(int frame, int playerCoins)

é muito mais claro.


33. Quantos parâmetros são demais?

Não adotaremos uma regra artificial como:

  máximo de 3 parâmetros

ou:

  máximo de 5 parâmetros

Uma lista muito grande pode ser um sinal de que:

  a função faz coisas demais
os dados pertencem a um conceito que ainda não modelamos
a responsabilidade está mal definida

Mas ainda não temos classes suficientes para discutir soluções maiores.

Por enquanto, observaremos o problema quando ele surgir.


34. Nossa primeira evolução do mini Game Loop

Podemos melhorar o código do capítulo anterior assim:

  #include <iostream>

enum class GameState
{
    Menu,
    Playing,
    GameOver
};

void ShowGameStart();
void ShowStartGameMessage();
void ShowGameOver();
void ShowGameFinished();
void ShowSeparator();
void ShowPlayerState(int frame, int playerHealth, int playerCoins);
void ShowGameSummary(int frame, int playerHealth, int playerCoins);

int main()
{
    const int DAMAGE_PER_FRAME = 25;

    bool isRunning = true;
    int frame = 0;
    int playerHealth = 100;
    int playerCoins = 0;

    GameState currentState = GameState::Menu;

    ShowSeparator();
    ShowGameStart();
    ShowSeparator();

    while (isRunning)
    {
        frame++;

        // Process
        if (currentState == GameState::Menu)
        {
            ShowStartGameMessage();

            currentState = GameState::Playing;
        }

        // Update
        if (currentState == GameState::Playing)
        {
            playerHealth -= DAMAGE_PER_FRAME;
            playerCoins++;
        }

        // Check
        if (playerHealth <= 0)
        {
            playerHealth = 0;
            currentState = GameState::GameOver;
        }

        // Show State
        ShowPlayerState(frame, playerHealth, playerCoins);

        if (currentState == GameState::GameOver)
        {
            ShowGameOver();

            isRunning = false;
        }
    }

    ShowSeparator();
    ShowGameFinished();
    ShowGameSummary(frame, playerHealth, playerCoins);
    ShowSeparator();

    return 0;
}

void ShowGameStart()
{
    std::cout << "MINI GAME LOOP START" << std::endl;
}

void ShowStartGameMessage()
{
    std::cout << "Starting game." << std::endl;
}

void ShowGameOver()
{
    std::cout << "Player defeated." << std::endl;
}

void ShowGameFinished()
{
    std::cout << "MINI GAME LOOP FINISHED" << std::endl;
}

void ShowSeparator()
{
    std::cout << "------------------------------" << std::endl;
}

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

Agora já existem funções que recebem dados reais do Game Loop sem depender de variáveis globais.


35. Validando se o comportamento permaneceu igual

Utilizamos:

  const int DAMAGE_PER_FRAME = 25;

Começamos:

  Health = 100
Coins = 0

Esperamos:

  Frame 1
Health 75
Coins 1

Frame 2
Health 50
Coins 2

Frame 3
Health 25
Coins 3

Frame 4
Health 0
Coins 4

E no final:

  Frames: 4
Final Health: 0
Final Coins: 4

A criação de parâmetros não deveria alterar essa regra.


36. Erro proposital — argumento faltando

Temos:

  void ShowPlayerState(int frame, int playerHealth, int playerCoins);

Agora tente:

  ShowPlayerState(frame, playerHealth);

Fornecemos apenas dois argumentos.

A função espera três.

Faça Build.

Leia o diagnóstico.

Depois corrija:

  ShowPlayerState(frame, playerHealth, playerCoins);

37. Erro proposital — argumentos demais

Tente:

  ShowPlayerState(frame, playerHealth, playerCoins, 999);

Nossa função foi declarada para receber três parâmetros.

Estamos fornecendo quatro argumentos.

Novamente, o contrato da chamada não corresponde à função disponível.

Corrija removendo o argumento extra.


38. Erro proposital — ordem trocada

Use:

  ShowPlayerState(playerCoins, playerHealth, frame);

O programa provavelmente compilará porque todos são:

  int

Mas a saída poderá ficar conceitualmente errada.

Por exemplo:

  Frame: 4

poderia estar mostrando Coins.

Este é um excelente exemplo de:

  compila
+
executa
+
está errado

Teste e depois corrija a ordem.


39. Erro proposital — modificar parâmetro por valor

Crie:

  void ResetCoins(int playerCoins)
{
    playerCoins = 0;
}

Depois:

  int playerCoins = 50;

ResetCoins(playerCoins);

std::cout << playerCoins << std::endl;

Antes de executar, responda:

o valor original será alterado?

Execute.

Resultado:

  50

A alteração aconteceu apenas no parâmetro local.


40. Esse experimento prepara o próximo capítulo

Temos agora uma necessidade muito concreta.

Queremos uma função capaz de calcular algo como:

  Health atual = 100
Damage = 25
↓
nova Health = 75

Mas isto:

  void ApplyDamage(int playerHealth, int damage)
{
    playerHealth -= damage;
}

não altera a variável original quando usamos passagem por valor.

Não queremos recorrer a globais.

Então podemos mudar a pergunta:

em vez de modificar silenciosamente o valor externo, e se a função produzir o novo valor e devolvê-lo?

Essa ideia nos leva diretamente a valores de retorno.


41. Microdesafio — mostrar Coins

Crie:

  void ShowPlayerCoins(int playerCoins);

Defina:

  void ShowPlayerCoins(int playerCoins)
{
    std::cout << "Coins: " << playerCoins << std::endl;
}

Chame:

  int playerCoins = 25;

ShowPlayerCoins(playerCoins);

Esperado:

  Coins: 25

42. Microdesafio — dois parâmetros

Crie:

  void ShowHealthStatus(int playerHealth, int maxPlayerHealth);

Apenas mostre:

  Health: 75 / 100

Uma possível implementação:

  void ShowHealthStatus(int playerHealth, int maxPlayerHealth)
{
    std::cout << "Health: " << playerHealth << " / " << maxPlayerHealth << std::endl;
}

Chamada:

  ShowHealthStatus(75, 100);

43. Microdesafio — expressão como argumento

Comece:

  int playerHealth = 100;
int damage = 25;

Sem alterar playerHealth, mostre como ficaria a Health depois do dano usando:

  ShowPlayerHealth(playerHealth - damage);

Esperado:

  Health: 75

Depois mostre novamente:

  ShowPlayerHealth(playerHealth);

Esperado:

  Health: 100

Isso comprova que apenas calculamos um valor temporário para o argumento.


44. Microdesafio — passagem por valor

Implemente:

  void ApplyDamage(int playerHealth, int damage)
{
    playerHealth -= damage;

    std::cout << "Inside: " << playerHealth << std::endl;
}

Com:

  int playerHealth = 100;

ApplyDamage(playerHealth, 25);

std::cout << "Outside: " << playerHealth << std::endl;

Preveja a saída antes de executar.

Esperado:

  Inside: 75
Outside: 100

Explique o motivo com suas próprias palavras.


45. Checklist de compreensão

Antes de avançar, tente responder sem consultar o artigo:

  • O que é um parâmetro?

  • O que é um argumento?

  • Onde o parâmetro aparece?

  • Onde o argumento aparece?

  • O argumento precisa possuir o mesmo nome do parâmetro?

  • Por que o tipo do parâmetro importa?

  • Como declaramos uma função que recebe um int?

  • Como uma função recebe vários parâmetros?

  • Por que a ordem dos argumentos importa?

  • Por que trocar dois argumentos do mesmo tipo pode gerar um bug lógico?

  • O que significa passagem por valor neste contexto?

  • Alterar um parâmetro int recebido por valor modifica automaticamente a variável original?

  • Por que funções de exibição funcionam muito bem com parâmetros por valor?

  • Por que ApplyDamage(int playerHealth, int damage) não consegue alterar o playerHealth original dessa maneira?

  • Por que não resolvemos isso usando uma variável global?

  • Por que ainda não introduzimos referências?

  • Qual novo problema nos leva naturalmente aos valores de retorno?

Se consegue explicar essas respostas, estamos prontos para continuar.


46. Estrutura do projeto

Ainda permanecemos com:

  CppGameEngine/
└── main.cpp

Não precisamos de:

  Functions/
Helpers/
Utils/
GameServices/

apenas porque nossas funções aumentaram.

Nosso problema atual continua sendo de fundamentos da linguagem.

A separação física em novos arquivos virá quando responsabilidades reais começarem a justificar isso.


47. O que melhorou no main()?

Antes tínhamos detalhes como:

  std::cout << "Frame: " << frame << std::endl;
std::cout << "Health: " << playerHealth << std::endl;
std::cout << "Coins: " << playerCoins << std::endl;

Agora:

  ShowPlayerState(frame, playerHealth, playerCoins);

A chamada comunica:

  o que queremos fazer

Enquanto a definição mostra:

  como fazemos

E os parâmetros deixam explícito:

  quais dados esse comportamento precisa

Esse é um avanço importante.


48. Dependências começam a aparecer nas assinaturas

Observe:

  void ShowPlayerState(int frame, int playerHealth, int playerCoins)

Só pela assinatura já conseguimos perceber:

  ShowPlayerState precisa de:

frame
playerHealth
playerCoins

Isso é muito melhor que depender silenciosamente de variáveis globais.

Mais tarde, essa ideia terá impacto enorme em:

  testabilidade
baixo acoplamento
design de APIs
classes
injeção de dependências
arquitetura

Mas ainda não precisamos chegar lá.


49. Git

Depois de validar:

  git status

Adicione:

  git add CppGameEngine/main.cpp

Ajuste o caminho se necessário.

Commit sugerido:

  git commit -m "refactor: pass game state data to functions"

A principal evolução deste capítulo foi organizar funções para receber explicitamente os dados de que precisam.


50. Atualizando o CHANGELOG

Em [Unreleased]:

  ### Refactored

- Updated console output functions to receive game state through parameters.
- Extracted player state and game summary output from the main Game Loop.
- Reduced direct console output responsibilities inside `main()`.
- Kept game state dependencies explicit without introducing global variables.

### Added

- Added examples of function parameters and call arguments.
- Added multiple-parameter function examples.
- Added pass-by-value demonstrations.

Depois:

  git add CHANGELOG.md
git commit -m "docs: update changelog with function parameters"

O ROADMAP técnico continua sem necessidade de alteração neste capítulo.


51. O que aprendemos?

No capítulo anterior tínhamos:

  void ShowGameStart()
{
}

uma função que não precisava receber dados.

Hoje evoluímos para:

  void ShowPlayerHealth(int playerHealth)
{
}

e:

  void ShowPlayerState(int frame, int playerHealth, int playerCoins)
{
}

Aprendemos que:

  • parâmetros representam dados que uma função declara precisar;

  • argumentos são os valores ou expressões fornecidos durante a chamada;

  • parâmetros possuem tipos;

  • uma função pode receber vários parâmetros;

  • a ordem dos argumentos precisa corresponder à intenção da assinatura;

  • argumentos e parâmetros não precisam possuir o mesmo nome;

  • expressões também podem ser fornecidas como argumentos;

  • passagem por valor fornece à função um valor próprio para aquele parâmetro;

  • modificar esse parâmetro não altera automaticamente uma variável original do chamador;

  • dependências explícitas são preferíveis a esconder estado em variáveis globais;

  • funções devem receber apenas aquilo de que precisam;

  • parâmetros demais podem indicar que uma responsabilidade está ficando grande;

  • ainda não precisamos de referências para compreender o problema atual.


Conclusão

No Capítulo 17 nossas funções finalmente passaram a existir.

Mas eram bastante isoladas:

  ShowGameStart();
ShowGameOver();
ShowSeparator();

Elas não precisavam saber muito sobre o estado do programa.

Agora conseguimos escrever:

  ShowPlayerState(frame, playerHealth, playerCoins);

A função recebe exatamente as informações necessárias para executar seu comportamento.

Podemos visualizar:

  MAIN
│
├── frame
├── playerHealth
└── playerCoins
        │
        ▼
ShowPlayerState(...)
        │
        ▼
parâmetros recebem os valores
        │
        ▼
função executa
        │
        ▼
retorna ao main

Isso começou a criar uma separação saudável entre:

  quem possui os dados

e:

  quem precisa utilizá-los temporariamente

Mas nosso experimento com:

  void ApplyDamage(int playerHealth, int damage)

revelou algo muito importante.

Dentro da função podemos fazer:

  playerHealth -= damage;

e obter:

  75

Mas depois da função:

  playerHealth

no main() ainda pode continuar:

  100

Então nossa função conseguiu receber dados.

Mas ainda precisamos aprender outra coisa:

como uma função pode produzir um resultado e entregá-lo de volta para quem a chamou?

É exatamente o próximo passo.


Próximo capítulo

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

Partiremos exatamente deste problema:

  void ApplyDamage(int playerHealth, int damage)
{
    playerHealth -= damage;
}

A função calcula alguma coisa.

Mas a variável original continua intacta quando os parâmetros são passados por valor.

Queremos evoluir para uma ideia como:

  int CalculateHealthAfterDamage(int playerHealth, int damage)
{
    return playerHealth - damage;
}

E então:

  playerHealth = CalculateHealthAfterDamage(playerHealth, damage);

Pela primeira vez estudaremos formalmente aquilo que já aparecia desde nosso primeiro programa:

  return

Vamos entender:

  tipo de retorno
return
valor devolvido
armazenamento do resultado
uso direto em expressões
funções que calculam
funções void
fluxo de retorno

Também começaremos a separar duas ideias importantes:

  uma função executa um comportamento

e:

  uma função produz um valor

Nosso main.cpp já consegue enviar informações para funções.

No próximo capítulo, essas funções finalmente aprenderão a responder com resultados.

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