Capítulo 18 — Parâmetros: enviando dados para funções
Série: Do zero ao jogo comercial com C++ e SFML
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:
ShowPlayerHealthprecisa receber um valor do tipoint, que dentro da função será identificado pelo nomeplayerHealth.
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
intrecebido 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 oplayerHealthoriginal 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.
Comentários
Ainda não há comentários. Sê o primeiro a comentar!