Aeca

Capítulo 17 — Funções: nosso main.cpp começou a crescer

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

Capítulo 17 — Funções: nosso main.cpp começou a crescer
aeca

Nosso primeiro mini Game Loop funciona. Agora precisamos começar a organizar comportamentos sem esconder aquilo que ainda estamos aprendendo.

No capítulo anterior construímos nosso primeiro mini Game Loop no console.

Pela primeira vez nosso programa passou a possuir um ciclo contínuo:

  while (isRunning)
{
    // Process
    // Update
    // Check
    // Show State
}

Também começamos a combinar vários conhecimentos anteriores:

  variáveis
bool
constantes
enum class
operadores
expressões
if
while

Nosso main() deixou de ser apenas algumas linhas.

Ele começou a possuir:

  estado inicial
↓
configurações
↓
Game Loop
↓
processamento
↓
atualização
↓
validação
↓
saída no console
↓
encerramento

Isso é uma boa notícia.

Significa que nosso programa está crescendo.

Mas crescimento cria novos problemas.

Observe conceitualmente:

  int main()
{
    // criar estado

    // mostrar início

    while (isRunning)
    {
        // processar
        // atualizar
        // verificar
        // mostrar estado
    }

    // mostrar encerramento

    return 0;
}

Ainda conseguimos entender.

Mas imagine adicionar:

  inventário
NPCs
farming
movimento
combate
economia
mapa
áudio
renderização
input

Se continuarmos colocando tudo dentro de main(), teremos um arquivo cada vez mais difícil de:

  ler
navegar
alterar
testar
reutilizar
entender

Chegou a hora de conhecer uma das ferramentas mais fundamentais da programação:

funções.


1. Começando pelo problema, não pela sintaxe

Nosso mini Game Loop possuía trechos como:

  std::cout << "=== MINI GAME LOOP START ===" << std::endl;

e depois:

  std::cout << std::endl;
std::cout << "=== MINI GAME LOOP FINISHED ===" << std::endl;
std::cout << "Frames: " << frame << std::endl;
std::cout << "Final Health: " << playerHealth << std::endl;
std::cout << "Final Coins: " << playerCoins << std::endl;

Também começamos a identificar responsabilidades conceituais:

  Process
Update
Check
Show State

Seria ótimo poder dar nomes claros a determinados comportamentos.

Algo como:

  ShowGameStart();

ou:

  ShowGameOver();

Em vez de repetir todos os detalhes cada vez que quisermos executar aquele comportamento.

Esse é um dos problemas que funções ajudam a resolver.


2. O que é uma função?

Para nosso estágio atual, podemos utilizar esta definição:

Uma função é um bloco de código nomeado que representa um comportamento e pode ser chamado quando precisamos executar esse comportamento.

Exemplo:

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

Agora temos um comportamento chamado:

  ShowGameStart

Podemos executá-lo com:

  ShowGameStart();

Essa ação é chamada de function call, ou chamada de função.


3. Nossa primeira função

Vamos começar com algo extremamente simples:

  #include <iostream>

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

int main()
{
    ShowGameStart();

    return 0;
}

Resultado:

  === MINI GAME LOOP START ===

O comportamento não mudou.

O que mudou foi sua organização.


4. O fluxo de execução mudou

Antes:

  int main()
{
    std::cout << "=== MINI GAME LOOP START ===" << std::endl;

    return 0;
}

Agora:

  int main()
{
    ShowGameStart();

    return 0;
}

Podemos imaginar:

  main()
  ↓
ShowGameStart()
  ↓
executa o corpo
  ↓
termina a função
  ↓
volta ao ponto da chamada
  ↓
main() continua

Esse fluxo será extremamente importante daqui para a frente.


5. O programa entra na função e depois retorna

Considere:

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

e:

  int main()
{
    std::cout << "Before function." << std::endl;

    ShowGameStart();

    std::cout << "After function." << std::endl;

    return 0;
}

Resultado:

  Before function.
Starting...
After function.

A execução foi:

  main
↓
Before function
↓
chamada de ShowGameStart
↓
entra em ShowGameStart
↓
Starting...
↓
termina ShowGameStart
↓
volta ao main
↓
After function

6. Uma função possui um nome

Temos:

  void ShowGameStart()

O nome é:

  ShowGameStart

Seguiremos nossa convenção:

  funções/métodos → PascalCase

Portanto:

  ShowGameStart()
ShowGameOver()
ShowSeparator()

e não:

  showGameStart()
show_game_start()

No nosso projeto, identificadores continuam em inglês.


7. O que significa void?

Na função:

  void ShowGameStart()
{
}

temos:

  void

antes do nome.

Esse é o tipo de retorno da função.

Neste momento podemos compreender void como:

esta função não produz um valor de retorno para quem a chamou.

Ela apenas executa um comportamento.

Por exemplo:

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

Chamamos:

  ShowSeparator();

A função executa.

Não estamos esperando um valor como:

  10
true
4.5

de volta.


8. Funções podem retornar valores, mas ainda não vamos aprofundar isso

Você já conhece uma função que retorna valor:

  int main()

O:

  int

indica um tipo de retorno.

E:

  return 0;

produz esse valor ao terminar main().

Então funções como:

  int CalculateSomething()

também existem.

Mas teremos um capítulo específico sobre valores de retorno.

Neste capítulo vamos começar com:

  void

para concentrar nossa atenção em:

  criação
definição
chamada
fluxo de execução
responsabilidade

9. E os parênteses ()?

Temos:

  ShowGameStart()

Por enquanto os parênteses estão vazios:

  ()

Isso significa que nossa função atual não recebe dados através de parâmetros.

Mais tarde teremos algo como:

  ShowPlayerHealth(playerHealth);

Mas isso exige compreender parâmetros, que será justamente o assunto do próximo capítulo.

Hoje trabalharemos com funções sem parâmetros.


10. O corpo da função

Temos:

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

As chaves:

  {
}

delimitam o corpo da função.

Dentro dele ficam as instruções que fazem parte daquele comportamento.

Temos:

  tipo de retorno
↓
void

nome
↓
ShowGameStart

parâmetros
↓
()

corpo
↓
{ ... }

11. Anatomia inicial de uma função

Podemos representar:

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

como:

  void      ShowGameStart      ()
 │              │             │
 │              │             └── parâmetros
 │              │
 │              └── nome
 │
 └── tipo de retorno

{
    corpo da função
}

Esse modelo crescerá nos próximos capítulos.


12. Criando outra função

Podemos extrair também um separador visual:

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

Agora:

  int main()
{
    ShowGameStart();
    ShowSeparator();

    return 0;
}

Resultado:

  === MINI GAME LOOP START ===
--------------------

Temos dois comportamentos nomeados.


13. Uma função deve representar uma responsabilidade compreensível

Compare:

  void Function1()

com:

  void ShowGameStart()

O segundo nome responde:

o que essa função faz?

Essa é uma característica importante.

O objetivo não é simplesmente mover linhas para outro lugar.

Queremos criar unidades com significado.


14. Função não é apenas "um lugar para guardar código"

É possível criar:

  void DoStuff()
{
}

Mas:

  DoStuff

não explica praticamente nada.

Da mesma forma:

  void ExecuteThings()

é vago.

Prefira nomes que revelem intenção:

  ShowGameStart()
ShowGameOver()
ShowSeparator()

Mais tarde poderemos ter:

  ProcessInput()
Update()
Render()

quando esses conceitos realmente existirem no projeto.


15. Nosso primeiro refactor

No capítulo anterior tínhamos:

  std::cout << "=== MINI GAME LOOP START ===" << std::endl;

Podemos substituir por:

  ShowGameStart();

E:

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

pode ser encapsulado inicialmente em:

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

Então:

  ShowGameOver();

Observe que estamos mudando a organização sem alterar a regra.

Isso é um pequeno refactor.


16. Refatoração não deveria mudar o comportamento pretendido

Antes:

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

Depois:

  ShowGameOver();

com:

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

A saída continua:

  Player defeated.

Mudamos a estrutura.

Não a funcionalidade esperada.

Essa distinção será muito importante durante toda a série.


17. Mas onde colocar a função?

Até agora escrevemos:

  void ShowGameStart()
{
}

int main()
{
}

Ou seja, a função apareceu antes de main().

Isso funciona porque, quando o compilador encontra:

  ShowGameStart();

dentro de main(), ele já conhece aquela função.

Mas existe outra forma muito útil.


18. Queremos manter main() perto do início

Conforme mais funções surgirem, poderíamos acabar com:

  função
função
função
função
função
função
main

e precisar rolar bastante até encontrar o fluxo principal.

Podemos preferir algo como:

  includes
↓
declarações das funções
↓
main
↓
definições das funções

Para isso precisamos conhecer uma nova ideia:

function declaration.


19. Declaração de função

Podemos escrever:

  void ShowGameStart();

Observe o ponto e vírgula:

  ;

Isso informa ao compilador:

existe uma função chamada ShowGameStart, sem parâmetros, cujo retorno é void.

Ainda não mostramos seu corpo.

Temos apenas sua declaração.


20. Definição de função

Mais abaixo:

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

Agora temos a definição.

Ela contém a implementação real.

Então:

  void ShowGameStart();

é declaração.

E:

  void ShowGameStart()
{
}

é definição.


21. Declaração e definição não são a mesma coisa

Podemos visualizar:

  DECLARAÇÃO

void ShowGameStart();

diz:

  essa função existe

Enquanto:

  DEFINIÇÃO

void ShowGameStart()
{
    ...
}

diz:

  é assim que ela funciona

Essa distinção será extremamente importante quando começarmos a utilizar arquivos:

  .h
.cpp

Mais tarde.


22. Nosso arquivo pode ficar organizado assim

  #include <iostream>

void ShowGameStart();
void ShowGameOver();

int main()
{
    ShowGameStart();

    ShowGameOver();

    return 0;
}

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

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

Agora main() aparece antes das implementações.

O compilador conhece as funções porque encontrou suas declarações antes.


23. O ponto e vírgula importa na declaração

Correto:

  void ShowGameStart();

Depois:

  void ShowGameStart()
{
}

Observe:

  declaração → ;

definição → corpo com { }

Não misture os dois formatos.


24. Erro proposital — chamar uma função desconhecida

Experimente:

  #include <iostream>

int main()
{
    ShowGameStart();

    return 0;
}

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

Dependendo da estrutura, o compilador chegará à chamada antes de possuir uma declaração para ShowGameStart.

Ele ainda não sabe o que aquele identificador representa.

Corrija adicionando antes de main():

  void ShowGameStart();

Agora o compilador conhece a assinatura antes da chamada.


25. Por que C++ precisa conhecer a função?

Quando encontra:

  ShowGameStart();

o compilador precisa saber, entre outras coisas:

  essa função existe?

qual é seu nome?

que tipo ela retorna?

quais parâmetros recebe?

Nossa declaração fornece esse contrato inicial.

Mais tarde, a definição fornece o corpo.


26. O termo "assinatura" aparecerá bastante

Você ouvirá:

  function signature

Esse assunto possui detalhes técnicos específicos em C++ que aprofundaremos gradualmente.

Por enquanto, pense de maneira prática:

  void ShowGameStart()

nos ajuda a identificar a função por sua forma declarada.

Quando parâmetros entrarem em cena, essa ideia ficará ainda mais importante.

Não precisamos aprofundar regras de overload agora.


27. Refatorando parte do mini Game Loop

Vamos recuperar uma versão reduzida:

  #include <iostream>

enum class GameState
{
    Menu,
    Playing,
    GameOver
};

int main()
{
    const int DAMAGE_PER_FRAME = 25;

    bool isRunning = true;
    int playerHealth = 100;
    GameState currentState = GameState::Menu;

    std::cout << "=== MINI GAME LOOP START ===" << std::endl;

    while (isRunning)
    {
        if (currentState == GameState::Menu)
        {
            std::cout << "Starting game." << std::endl;

            currentState = GameState::Playing;
        }

        if (currentState == GameState::Playing)
        {
            playerHealth -= DAMAGE_PER_FRAME;
        }

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

        if (currentState == GameState::GameOver)
        {
            std::cout << "Player defeated." << std::endl;

            isRunning = false;
        }
    }

    std::cout << "=== MINI GAME LOOP FINISHED ===" << std::endl;

    return 0;
}

Temos alguns comportamentos que não dependem de dados externos.

Eles são bons primeiros candidatos.


28. Extraindo mensagens independentes

Podemos criar:

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

Também:

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

E:

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

Finalmente:

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

São funções simples.

Mas já conseguimos experimentar criação, declaração e chamada.


29. O main() começa a comunicar melhor o fluxo

Podemos chegar a:

  int main()
{
    // estado...

    ShowGameStart();

    while (isRunning)
    {
        if (currentState == GameState::Menu)
        {
            ShowStartGameMessage();

            currentState = GameState::Playing;
        }

        // ...

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

            isRunning = false;
        }
    }

    ShowGameFinished();

    return 0;
}

Agora alguns detalhes de saída foram substituídos por nomes que explicam a intenção.


30. Por que não criamos Update() ainda?

Essa é uma pergunta muito importante.

Seria tentador fazer:

  void Update()
{
    playerHealth -= DAMAGE_PER_FRAME;
}

Mas de onde viria:

  playerHealth

?

Nosso playerHealth atualmente foi criado dentro de:

  main()

A função Update() não recebe essa informação automaticamente.

Poderíamos tentar criar variáveis globais.

Mas isso introduziria outro conjunto de problemas e esconderia o conceito que realmente precisamos aprender.


31. Não vamos usar globais para fugir do próximo conceito

Poderíamos fazer:

  int playerHealth = 100;

void Update()
{
    playerHealth -= 5;
}

Isso tecnicamente poderia funcionar.

Mas estaríamos criando compartilhamento de estado global apenas para evitar aprender como dados são enviados para funções.

Não é a evolução que queremos.

Nosso próximo problema ficou muito claro:

como uma função recebe os dados de que precisa?

Essa pergunta será respondida no próximo capítulo com parâmetros.


32. Por enquanto, extraímos apenas o que não precisa receber estado

Isso pode parecer menos impressionante do que imediatamente escrever:

  Process();
Update();
Check();
ShowState();

Mas pedagogicamente é muito mais correto.

Já conseguimos aprender:

  o que é função
declaração
definição
chamada
void
fluxo de execução
responsabilidade

Sem utilizar globais artificiais e sem antecipar parâmetros.


33. O problema nos leva ao próximo conhecimento

Nossa evolução está acontecendo assim:

  main cresceu
↓
queremos separar comportamento
↓
criamos funções
↓
algumas funções precisam de dados
↓
como enviar esses dados?
↓
parâmetros

Isso é exatamente a progressão que queremos.

A ferramenta nasce da necessidade.


34. Funções também permitem reutilização

Imagine:

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

Podemos chamar:

  ShowSeparator();

quantas vezes precisarmos.

Por exemplo:

  int main()
{
    ShowSeparator();
    ShowGameStart();
    ShowSeparator();

    return 0;
}

Não precisamos repetir:

  std::cout << "--------------------" << std::endl;

em vários lugares.


35. Mas reutilização não é o único objetivo

Mesmo uma função chamada apenas uma vez pode ser útil.

Por exemplo:

  ShowGameFinished();

Talvez apareça apenas no encerramento.

Ainda assim, ela pode representar claramente uma responsabilidade.

Portanto:

funções não existem apenas para eliminar duplicação.

Também servem para:

  nomear comportamento
separar responsabilidades
organizar fluxo
reduzir detalhes em certos níveis do código
facilitar evolução

36. Funções não deveriam esconder tudo

Também existe um extremo oposto.

Imagine:

  void Step1()
{
}

void Step2()
{
}

void Step3()
{
}

sem significado real.

Ou criar uma função para cada linha:

  void PrintOneCharacter()

apenas para dizer que usamos funções.

Isso adicionaria fragmentação sem benefício.

A pergunta continua:

existe um comportamento coerente que merece um nome?


37. Funções pequenas ajudam, mas "pequena" não significa uma quantidade mágica de linhas

Você encontrará regras como:

  funções devem ter no máximo X linhas

Não vamos adotar números arbitrários.

Uma função deve possuir uma responsabilidade clara e permanecer compreensível.

Às vezes isso resultará em poucas linhas.

Outras vezes, uma função legitimamente terá mais.

Primeiro buscamos:

  coesão
clareza
responsabilidade

e não uma contagem artificial.


38. Uma função deve fazer algo que conseguimos nomear

Por exemplo:

  void ShowGameStart()

possui um comportamento claro.

Se temos dificuldade para dar um nome sem usar:

  And
Also
Everything
Manager
Stuff

talvez a responsabilidade esteja mal definida.

Esse princípio ficará ainda mais importante quando chegarmos às classes.


39. Função e comentário não são a mesma coisa

Antes:

  // Show game start
std::cout << "=== MINI GAME LOOP START ===" << std::endl;

Depois:

  ShowGameStart();

A própria chamada pode comunicar a intenção.

Comentários continuam úteis.

Mas código bem nomeado pode reduzir comentários que apenas repetem aquilo que o código já diz.


40. Não removeremos todos os comentários automaticamente

Comentários como:

  // 1. Process

podem continuar temporariamente porque ainda estamos estudando a estrutura do Game Loop.

Mais tarde, conforme funções e arquitetura evoluírem, alguns deles se tornarão desnecessários.

Não precisamos fazer uma limpeza agressiva antes da hora.


41. Código final do capítulo

Vamos manter o Game Loop e extrair apenas comportamentos que já conseguem existir sem parâmetros:

  #include <iostream>

enum class GameState
{
    Menu,
    Playing,
    GameOver
};

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

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
        std::cout << "Frame: " << frame << std::endl;
        std::cout << "Health: " << playerHealth << std::endl;
        std::cout << "Coins: " << playerCoins << std::endl;
        std::cout << std::endl;

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

            isRunning = false;
        }
    }

    ShowSeparator();
    ShowGameFinished();
    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;
}

Nosso comportamento principal continua semelhante.

Mas agora já existem comportamentos nomeados fora de main().


42. Observe o início do arquivo

Temos:

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

Essas são as declarações.

Depois encontramos:

  int main()

e podemos compreender o fluxo principal.

No final aparecem as definições.

Essa organização será importante quando começarmos a separar declarações e implementações em arquivos diferentes futuramente.


43. O main() ainda está grande

Sim.

E isso é proposital.

Ainda temos dentro dele:

  Process
Update
Check
Show State

Não resolvemos completamente o problema.

Mas agora sabemos por quê.

Esses comportamentos dependem de dados como:

  playerHealth
playerCoins
frame
currentState
isRunning

Precisamos aprender a enviar dados para funções.


44. Não estamos falhando ao deixar código dentro de main()

Muito pelo contrário.

Estamos evitando uma solução ruim.

Poderíamos mover tudo para funções usando globais.

O código pareceria visualmente mais curto:

  while (isRunning)
{
    Process();
    Update();
    Check();
    ShowState();
}

Mas os dados ficariam escondidos em estado global.

Teríamos ganho estética e perdido clareza sobre dependências.

Não queremos isso.


45. Dependências devem ficar visíveis

Se uma futura função:

  Update(...)

precisa modificar Health e Coins, seria útil conseguirmos enxergar essa necessidade.

Algo conceitualmente parecido com:

  Update precisa de:
playerHealth
playerCoins

No próximo capítulo veremos como parâmetros começam a permitir exatamente isso.


46. Erro proposital — esquecer ()

Considere:

  ShowGameStart;

quando queria:

  ShowGameStart();

Os parênteses fazem parte da chamada da função.

Neste estágio, use:

  ShowGameStart();

para executar o comportamento.

Se o compilador emitir erro ou warning em alguma variação equivocada, leia o diagnóstico e compare com uma chamada correta.


47. Erro proposital — nome incorreto

Temos:

  void ShowGameStart();

mas chamamos:

  ShowGameStarts();

Observe o:

  s

extra.

C++ diferencia identificadores.

O compilador procurará:

  ShowGameStarts

que não é a mesma função.

Isso reforça novamente:

  nomes precisam ser consistentes

48. Erro proposital — declaração e definição incompatíveis

Imagine declarar:

  void ShowGameStart();

e depois escrever:

  int ShowGameStart()
{
    return 0;
}

Agora temos uma inconsistência.

A declaração prometeu:

  retorno void

mas a definição tenta usar:

  retorno int

O contrato precisa corresponder.

Quando parâmetros aparecerem, essa consistência também incluirá os tipos recebidos.


49. Erro proposital — esquecer a definição

Suponha:

  void ShowGameStart();

int main()
{
    ShowGameStart();

    return 0;
}

Mas não adicionamos:

  void ShowGameStart()
{
}

O compilador consegue entender a chamada porque viu a declaração.

Mas, durante o processo de construção do programa, faltará a implementação real necessária.

Esse tipo de situação começa a nos aproximar de uma classe importante de erros que veremos com muito mais profundidade:

  erros de linker

Guarde essa diferença.


50. Compiler e linker começam a aparecer novamente

De maneira simplificada:

  DECLARAÇÃO

void ShowGameStart();

permite que uma parte do código saiba que aquela função existe.

Mas o programa final também precisa encontrar a definição correspondente:

  void ShowGameStart()
{
}

Quando separarmos arquivos .h e .cpp, essa distinção ficará ainda mais importante.

Os fundamentos que aprendemos sobre build e linker voltarão com bastante força.


51. Não vamos separar .h e .cpp ainda

Nosso projeto continua:

  CppGameEngine/
└── main.cpp

Poderíamos criar:

  Game.h
Game.cpp

agora?

Tecnicamente, poderíamos.

Mas ainda não temos uma classe Game.

Nem uma responsabilidade que justifique esses arquivos.

Poderíamos também criar:

  Functions.h
Functions.cpp

Mas isso seria uma pasta/arquivo genérico sem um domínio claro.

Então não faremos.


52. Arquivos nascerão junto com responsabilidades reais

Quando tivermos algo como:

  Application
Player
Game
Window
Input

poderemos discutir:

  declaração
implementação
header
source
include
dependência

com problemas reais.

Agora tudo continua cabendo em main.cpp.


53. Testando a refatoração

O objetivo principal é preservar o comportamento.

Com:

  const int DAMAGE_PER_FRAME = 25;

temos:

  Health inicial = 100

Atualizações:

  Frame 1 → 75
Frame 2 → 50
Frame 3 → 25
Frame 4 → 0

Então esperamos:

  4 frames
4 coins
Health final = 0

A extração das funções de mensagem não deveria modificar isso.


54. Resultado esperado

Algo semelhante a:

  ------------------------------
MINI GAME LOOP START
------------------------------
Starting game.
Frame: 1
Health: 75
Coins: 1

Frame: 2
Health: 50
Coins: 2

Frame: 3
Health: 25
Coins: 3

Frame: 4
Health: 0
Coins: 4

Player defeated.
------------------------------
MINI GAME LOOP FINISHED
------------------------------

Compare o resultado antes e depois da refatoração.


55. Refactor precisa ser validado

Um erro comum é pensar:

eu só reorganizei código, então não preciso testar.

Mas mover comportamento pode introduzir problemas como:

  função não chamada
chamada no lugar errado
mensagem duplicada
ordem alterada
definição incorreta
estado modificado sem intenção

Então:

  refactor
↓
build
↓
run
↓
compare comportamento

56. Microdesafio — ShowSeparator

Sem olhar o código final, crie:

  void ShowSeparator();

Defina:

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

Depois utilize:

  ShowSeparator();

em dois lugares diferentes.

O objetivo é perceber a reutilização de comportamento.


57. Microdesafio — mensagem de início

Extraia:

  std::cout << "Starting game." << std::endl;

para:

  void ShowStartGameMessage()

Depois substitua a linha original por uma chamada.

Confirme que a saída permanece igual.


58. Microdesafio — função ainda desconhecida

Coloque a definição:

  void ShowTestMessage()
{
    std::cout << "Test" << std::endl;
}

depois de main().

Dentro de main():

  ShowTestMessage();

Primeiro não adicione declaração.

Faça Build.

Depois coloque antes de main():

  void ShowTestMessage();

Compile novamente.

Observe a diferença.


59. Microdesafio — fluxo de execução

Considere:

  void FunctionA()
{
    std::cout << "A" << std::endl;
}

void FunctionB()
{
    std::cout << "B" << std::endl;
}

e:

  int main()
{
    std::cout << "Start" << std::endl;

    FunctionA();
    FunctionB();

    std::cout << "End" << std::endl;

    return 0;
}

Antes de executar, escreva a saída esperada.

Depois valide.

Esperado:

  Start
A
B
End

60. Checklist de compreensão

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

  • Qual problema nos levou a funções?

  • O que é uma função?

  • O que significa chamar uma função?

  • O que significa void?

  • O que representam os parênteses vazios?

  • Qual é o corpo da função?

  • Qual diferença existe entre declaração e definição?

  • Por que uma função definida depois de main() pode precisar ser declarada antes?

  • O que acontece conceitualmente quando uma função é chamada?

  • Por que funções não servem apenas para diminuir duplicação?

  • Por que um bom nome importa?

  • Por que não extraímos Update() ainda?

  • Por que evitamos utilizar variáveis globais apenas para possibilitar essa refatoração?

  • O que precisamos aprender para enviar playerHealth ou playerCoins a uma função?

  • Por que ainda mantemos tudo em main.cpp?

Se consegue explicar isso com suas próprias palavras, o fundamento está estabelecido.


61. Estado atual do projeto

Continuamos:

  CppGameEngine/
└── main.cpp

Mas nosso arquivo agora contém uma nova organização conceitual:

  includes
↓
tipos
↓
declarações de funções
↓
main
↓
definições de funções

Esse é um pequeno passo.

Mas será muito importante quando chegarmos aos 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: extract console output functions"

Por que:

  refactor

?

Porque nossa principal mudança reorganizou comportamento existente sem alterar a funcionalidade central do mini Game Loop.


63. Atualizando o CHANGELOG

Em [Unreleased]:

  ### Refactored

- Extracted console output behavior into named functions.
- Added function declarations and definitions to organize `main.cpp`.
- Reduced repeated console output details inside the main execution flow.
- Preserved the existing mini Game Loop behavior after refactoring.

### Added

- Added introductory examples for `void` functions and function calls.

Depois:

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

Ainda não precisamos alterar o ROADMAP técnico.


64. O que aprendemos?

Nosso main() começou a crescer.

Em vez de responder criando imediatamente:

  classes
managers
services
modules

introduzimos uma ferramenta muito mais fundamental:

  função

Criamos:

  void ShowGameStart()
{
}

e chamamos:

  ShowGameStart();

Aprendemos que:

  • uma função representa um comportamento nomeado;

  • uma chamada transfere temporariamente o fluxo para aquela função;

  • ao terminar, a execução retorna ao ponto da chamada;

  • void indica que nossa função atual não retorna um valor;

  • () representa a lista de parâmetros, vazia neste capítulo;

  • função possui nome e corpo;

  • declaração e definição são conceitos diferentes;

  • declarações permitem que o compilador conheça funções antes de suas definições;

  • refatorar não deveria modificar o comportamento pretendido;

  • funções ajudam a nomear responsabilidades;

  • nem toda linha precisa virar uma função;

  • uma função deve existir porque representa um comportamento coerente;

  • não devemos utilizar globais apenas para facilitar uma refatoração;

  • nosso Game Loop ainda não pode ser completamente extraído de maneira limpa sem aprender a enviar dados às funções.


Conclusão

Nosso primeiro mini Game Loop nos colocou diante de um problema inevitável:

  main.cpp está crescendo

A primeira reação poderia ser criar uma arquitetura enorme.

Não fizemos isso.

Primeiro aprendemos a menor ferramenta capaz de começar a organizar comportamento:

  void ShowGameStart()
{
}

Agora podemos escrever:

  ShowGameStart();

em vez de repetir detalhes de implementação naquele ponto do fluxo.

Nosso código começou a adquirir níveis de leitura.

Em:

  ShowGameStart();

vemos a intenção.

Na definição:

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

vemos a implementação.

Essa separação simples será extremamente poderosa conforme o projeto crescer.

Mas encontramos uma limitação importante.

Considere nosso update:

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

Queremos muito chegar a algo como:

  Update();

Mas Update() precisaria saber:

  qual é a Health atual?

quantas Coins o jogador possui?

qual é o estado do jogo?

qual é o dano aplicado?

Não queremos tornar tudo global.

Então precisamos aprender a fornecer dados para uma função.

Nosso próximo problema já está definido.


Próximo capítulo

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

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

Partiremos de uma função simples:

  void ShowPlayerHealth()
{
}

e da pergunta:

como essa função saberia qual Health deve mostrar?

Queremos chegar a uma ideia como:

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

e então:

  ShowPlayerHealth(playerHealth);

Vamos entender cuidadosamente:

  parâmetro
argumento
tipo
nome
passagem de dados
escopo local
múltiplos parâmetros
ordem dos argumentos

Também começaremos a perceber algo muito importante.

Nem toda função precisa modificar os dados que recebe.

Algumas apenas precisam:

  observar
calcular
mostrar
validar

Outras precisam produzir alterações.

Essa distinção abrirá gradualmente o caminho para assuntos como:

  passagem por valor
referências
const
retorno

mas não vamos antecipar tudo.

No próximo capítulo o objetivo será direto:

ensinar nossas funções a receber as informações de que precisam sem depender de estado global.

Nosso main.cpp começou a ser dividido em comportamentos.

Agora precisamos permitir que esses comportamentos conversem com os dados do programa.

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