
Quando o protótipo precisa virar jogo de verdade
Até aqui, você já percorreu uma base essencial para criar jogos com MonoGame.
Você entendeu o que é o framework, preparou o ambiente, estudou o game loop, desenhou sprites, carregou texturas, capturou entrada do jogador e começou a trabalhar com movimentação, colisão e física simples.
Agora chega um dos capítulos mais importantes para sair do “protótipo bagunçado” e entrar em uma estrutura mais profissional:
cenas, menus e arquitetura do jogo.
No começo, é normal colocar tudo dentro do Game1.cs. Você coloca ali o jogador, o inimigo, o fundo, o teclado, a colisão, o score, o menu, o pause, o game over e tudo mais.
Funciona por um tempo.
Mas, conforme o jogo cresce, o arquivo vira um monstro. Fica difícil entender, testar, corrigir e evoluir.
É exatamente aqui que entra a arquitetura.
No MonoGame, a classe Game é o ponto de entrada da maioria dos jogos e é responsável por rodar o game loop, chamando Update(GameTime) e Draw(GameTime). Ou seja, o MonoGame entrega a base do ciclo, mas a organização interna do seu jogo fica por sua conta.
Isso significa que você precisa criar sua própria forma de organizar:
🎮 tela inicial;
📜 menu principal;
⏸️ pause;
🧱 fase do jogo;
💀 game over;
🏆 vitória;
⚙️ opções;
💾 carregamento;
🔄 transições;
🧠 estados do jogo;
🧩 classes e responsabilidades.
Este capítulo ensina exatamente isso.
🧠 O problema do Game1.cs gigante
Todo projeto MonoGame começa com uma classe principal parecida com esta:
public class Game1 : Game
{
protected override void Initialize()
{
base.Initialize();
}
protected override void LoadContent()
{
}
protected override void Update(GameTime gameTime)
{
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
base.Draw(gameTime);
}
}
No início, colocar tudo ali parece natural.
Você adiciona o jogador:
private Player _player;
Depois o inimigo:
private Enemy _enemy;
Depois o menu:
private int _selectedMenuIndex;
Depois a pontuação:
private int _score;
Depois a lista de moedas:
private List<Coin> _coins;
Depois o pause:
private bool _isPaused;
Depois o game over:
private bool _isGameOver;
Depois a tela de vitória:
private bool _hasWon;
E quando você percebe, o Game1.cs virou um arquivo enorme com centenas ou milhares de linhas.
O problema não é apenas o tamanho. O problema é que tudo fica misturado:
- lógica de menu;
- lógica de gameplay;
- lógica de pause;
- lógica de game over;
- desenho do HUD;
- input;
- colisão;
- carregamento de assets;
- regras do jogo;
- transições;
- inicialização;
- reinício de fase.
Isso torna o projeto difícil de evoluir.
A arquitetura serve para impedir esse caos.
🧩 O que é arquitetura de jogo?
Arquitetura de jogo é a forma como você organiza o código, os sistemas, as responsabilidades e o fluxo do projeto.
Em um jogo simples, você pode até sobreviver com um arquivo principal grande.
Mas em um jogo real, você precisa separar responsabilidades.
A pergunta central da arquitetura é:
Quem faz o quê dentro do jogo?
Por exemplo:
| Responsabilidade | Classe ou sistema ideal |
|---|---|
| Ler teclado, mouse e controle | InputManager |
| Controlar cenas | SceneManager |
| Tela inicial | MenuScene |
| Gameplay | GameScene |
| Pause | PauseScene |
| Game over | GameOverScene |
| Jogador | Player |
| Inimigos | Enemy |
| Botões | Button |
| Pontuação e vida | Hud |
| Assets globais | ContentManager ou serviço próprio |
| Configurações | GameSettings |
Arquitetura não é complicar. É organizar para crescer.
🎬 O que são cenas em um jogo?
Uma cena é uma parte ou tela do jogo com comportamento próprio.
Exemplos:
🎮 MenuScene — tela inicial com botões;
🧱 GameScene — fase principal do jogo;
⏸️ PauseScene — tela de pausa;
💀 GameOverScene — tela de fim de jogo;
🏆 VictoryScene — tela de vitória;
⚙️ OptionsScene — configurações;
📜 CreditsScene — créditos.
Cada cena possui sua própria lógica de atualização e desenho.
Por exemplo, no menu:
- setas mudam a opção selecionada;
- Enter confirma;
- mouse clica em botões;
- o fundo pode ter animação leve.
Na fase do jogo:
- jogador anda;
- inimigos se movem;
- colisões acontecem;
- moedas são coletadas;
- vida e pontuação mudam.
No game over:
- o jogador não se move mais;
- aparece mensagem de derrota;
- Enter reinicia;
- Escape volta ao menu.
Cada uma dessas telas é uma cena diferente.
🧠 Por que usar cenas?
Porque cada tela do jogo tem regras diferentes.
Sem cenas, você começa a criar muitos if dentro do Update:
if (_currentState == GameState.Menu)
{
// lógica do menu
}
else if (_currentState == GameState.Playing)
{
// lógica do jogo
}
else if (_currentState == GameState.Paused)
{
// lógica do pause
}
else if (_currentState == GameState.GameOver)
{
// lógica do game over
}
Isso funciona no começo.
Mas logo fica difícil de manter.
Com cenas, você pode organizar assim:
_currentScene.Update(gameTime);
_currentScene.Draw(spriteBatch);
O Game1.cs não precisa saber todos os detalhes da tela atual. Ele apenas delega.
Essa é uma mudança importante: o Game1.cs deixa de ser o lugar onde tudo acontece e passa a ser o coordenador geral do jogo.
🧭 Primeira abordagem: GameState simples
Antes de criar um sistema completo de cenas, vale entender a abordagem mais simples: usar um enum de estados.
public enum GameState
{
MainMenu,
Playing,
Paused,
GameOver,
Victory
}
No Game1.cs:
private GameState _currentState = GameState.MainMenu;
No Update:
protected override void Update(GameTime gameTime)
{
switch (_currentState)
{
case GameState.MainMenu:
UpdateMainMenu(gameTime);
break;
case GameState.Playing:
UpdateGameplay(gameTime);
break;
case GameState.Paused:
UpdatePause(gameTime);
break;
case GameState.GameOver:
UpdateGameOver(gameTime);
break;
case GameState.Victory:
UpdateVictory(gameTime);
break;
}
base.Update(gameTime);
}
No Draw:
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
_spriteBatch.Begin();
switch (_currentState)
{
case GameState.MainMenu:
DrawMainMenu();
break;
case GameState.Playing:
DrawGameplay();
break;
case GameState.Paused:
DrawGameplay();
DrawPauseOverlay();
break;
case GameState.GameOver:
DrawGameOver();
break;
case GameState.Victory:
DrawVictory();
break;
}
_spriteBatch.End();
base.Draw(gameTime);
}
Essa abordagem é boa para projetos pequenos.
Mas conforme o jogo cresce, ela ainda deixa muita coisa dentro do Game1.cs.
Por isso, o próximo passo é criar cenas como classes separadas.
🧱 Criando uma interface IScene
A forma mais limpa de organizar cenas é criar uma interface.
public interface IScene
{
void LoadContent();
void Update(GameTime gameTime);
void Draw(SpriteBatch spriteBatch);
}
Essa interface define que toda cena precisa saber:
- carregar conteúdo;
- atualizar lógica;
- desenhar na tela.
Depois, cada cena implementa essa interface.
Exemplo:
public class MenuScene : IScene
{
public void LoadContent()
{
// carregar fontes, texturas, botões
}
public void Update(GameTime gameTime)
{
// lógica do menu
}
public void Draw(SpriteBatch spriteBatch)
{
// desenhar menu
}
}
E a fase principal:
public class GameScene : IScene
{
public void LoadContent()
{
// carregar jogador, inimigos, mapa
}
public void Update(GameTime gameTime)
{
// atualizar gameplay
}
public void Draw(SpriteBatch spriteBatch)
{
// desenhar fase
}
}
Agora cada tela tem sua própria classe.
🎛️ Criando um SceneManager
O SceneManager é o responsável por controlar qual cena está ativa.
public class SceneManager
{
private IScene _currentScene;
public void ChangeScene(IScene newScene)
{
_currentScene = newScene;
_currentScene.LoadContent();
}
public void Update(GameTime gameTime)
{
_currentScene?.Update(gameTime);
}
public void Draw(SpriteBatch spriteBatch)
{
_currentScene?.Draw(spriteBatch);
}
}
No Game1.cs, você teria:
private SceneManager _sceneManager;
No Initialize:
protected override void Initialize()
{
_sceneManager = new SceneManager();
base.Initialize();
}
No LoadContent:
protected override void LoadContent()
{
_spriteBatch = new SpriteBatch(GraphicsDevice);
var menuScene = new MenuScene();
_sceneManager.ChangeScene(menuScene);
}
No Update:
protected override void Update(GameTime gameTime)
{
_sceneManager.Update(gameTime);
base.Update(gameTime);
}
No Draw:
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
_spriteBatch.Begin();
_sceneManager.Draw(_spriteBatch);
_spriteBatch.End();
base.Draw(gameTime);
}
O Game1.cs ficou muito mais limpo.
Essa é a ideia principal da arquitetura: cada parte faz sua função.
⚠️ Um problema: como a cena troca para outra cena?
A MenuScene precisa conseguir iniciar o jogo.
Mas ela não deve criar bagunça acessando tudo diretamente.
Uma solução simples é passar uma função para a cena.
Exemplo:
public class MenuScene : IScene
{
private Action _startGame;
public MenuScene(Action startGame)
{
_startGame = startGame;
}
public void LoadContent()
{
}
public void Update(GameTime gameTime)
{
KeyboardState keyboard = Keyboard.GetState();
if (keyboard.IsKeyDown(Keys.Enter))
{
_startGame.Invoke();
}
}
public void Draw(SpriteBatch spriteBatch)
{
// desenhar menu
}
}
No Game1.cs:
protected override void LoadContent()
{
_spriteBatch = new SpriteBatch(GraphicsDevice);
_sceneManager.ChangeScene(new MenuScene(() =>
{
_sceneManager.ChangeScene(new GameScene());
}));
}
Agora o menu consegue iniciar o jogo sem conhecer detalhes internos do Game1.
🧠 Cenas precisam de acesso a conteúdo
Na prática, uma cena precisa carregar imagens, fontes e sons.
O MonoGame usa ContentManager para carregar assets processados pelo Content Pipeline. A documentação oficial define ContentManager como o componente em tempo de execução que carrega objetos gerenciados a partir de arquivos .xnb produzidos pelo MonoGame Content Builder e também gerencia o ciclo de vida desses conteúdos carregados.
Então, em vez de criar cenas vazias, podemos passar o ContentManager para elas.
public interface IScene
{
void LoadContent(ContentManager content);
void Update(GameTime gameTime);
void Draw(SpriteBatch spriteBatch);
}
Agora o SceneManager pode guardar o ContentManager:
public class SceneManager
{
private IScene _currentScene;
private ContentManager _content;
public SceneManager(ContentManager content)
{
_content = content;
}
public void ChangeScene(IScene newScene)
{
_currentScene = newScene;
_currentScene.LoadContent(_content);
}
public void Update(GameTime gameTime)
{
_currentScene?.Update(gameTime);
}
public void Draw(SpriteBatch spriteBatch)
{
_currentScene?.Draw(spriteBatch);
}
}
No Game1.cs:
_sceneManager = new SceneManager(Content);
Isso deixa o carregamento mais organizado.
🎨 Criando uma MenuScene simples
Vamos criar uma cena de menu inicial.
public class MenuScene : IScene
{
private SpriteFont _font;
private int _selectedIndex = 0;
private string[] _options =
{
"Jogar",
"Opções",
"Sair"
};
private Action _startGame;
private Action _exitGame;
public MenuScene(Action startGame, Action exitGame)
{
_startGame = startGame;
_exitGame = exitGame;
}
public void LoadContent(ContentManager content)
{
_font = content.Load<SpriteFont>("fonts/default");
}
public void Update(GameTime gameTime)
{
KeyboardState keyboard = Keyboard.GetState();
if (keyboard.IsKeyDown(Keys.Down))
{
_selectedIndex++;
if (_selectedIndex >= _options.Length)
_selectedIndex = 0;
}
if (keyboard.IsKeyDown(Keys.Up))
{
_selectedIndex--;
if (_selectedIndex < 0)
_selectedIndex = _options.Length - 1;
}
if (keyboard.IsKeyDown(Keys.Enter))
{
if (_selectedIndex == 0)
_startGame.Invoke();
if (_selectedIndex == 2)
_exitGame.Invoke();
}
}
public void Draw(SpriteBatch spriteBatch)
{
spriteBatch.DrawString(_font, "MonoGame Curso Completo", new Vector2(420, 180), Color.White);
for (int i = 0; i < _options.Length; i++)
{
Color color = i == _selectedIndex ? Color.Yellow : Color.White;
spriteBatch.DrawString(
_font,
_options[i],
new Vector2(560, 280 + i * 50),
color
);
}
}
}
Esse menu já tem:
✅ título;
✅ opções;
✅ seleção;
✅ entrada por teclado;
✅ ação de iniciar;
✅ ação de sair.
Mas existe um detalhe importante: usando IsKeyDown, a seleção pode mudar rápido demais se o jogador segurar a tecla.
O ideal é usar o InputManager criado no capítulo anterior.
⌨️ Menu com InputManager
A interface da cena pode receber também um InputManager.
public interface IScene
{
void LoadContent(ContentManager content);
void Update(GameTime gameTime, InputManager input);
void Draw(SpriteBatch spriteBatch);
}
Agora o MenuScene fica mais correto:
public void Update(GameTime gameTime, InputManager input)
{
if (input.IsKeyPressed(Keys.Down))
{
_selectedIndex++;
if (_selectedIndex >= _options.Length)
_selectedIndex = 0;
}
if (input.IsKeyPressed(Keys.Up))
{
_selectedIndex--;
if (_selectedIndex < 0)
_selectedIndex = _options.Length - 1;
}
if (input.IsConfirmPressed())
{
if (_selectedIndex == 0)
_startGame.Invoke();
if (_selectedIndex == 2)
_exitGame.Invoke();
}
}
Agora o menu responde apenas a toques únicos.
Isso cria uma experiência muito mais profissional.
🧱 Criando uma GameScene
A GameScene é onde fica a fase principal.
public class GameScene : IScene
{
private Player _player;
private List<Enemy> _enemies;
private List<Coin> _coins;
private Texture2D _playerTexture;
private Texture2D _enemyTexture;
private Texture2D _coinTexture;
private int _score;
private int _lives = 3;
private Action _goToGameOver;
private Action _goToVictory;
private Action _pauseGame;
public GameScene(
Action goToGameOver,
Action goToVictory,
Action pauseGame
)
{
_goToGameOver = goToGameOver;
_goToVictory = goToVictory;
_pauseGame = pauseGame;
}
public void LoadContent(ContentManager content)
{
_playerTexture = content.Load<Texture2D>("sprites/player");
_enemyTexture = content.Load<Texture2D>("sprites/enemy");
_coinTexture = content.Load<Texture2D>("items/coin");
_player = new Player(_playerTexture, new Vector2(100, 300));
_enemies = new List<Enemy>();
_coins = new List<Coin>();
_score = 0;
_lives = 3;
}
public void Update(GameTime gameTime, InputManager input)
{
if (input.IsPausePressed())
{
_pauseGame.Invoke();
return;
}
_player.Update(gameTime, input);
foreach (Enemy enemy in _enemies)
enemy.Update(gameTime);
foreach (Coin coin in _coins)
{
if (!coin.Collected && _player.Bounds.Intersects(coin.Bounds))
{
coin.Collected = true;
_score += 10;
}
}
if (_lives <= 0)
_goToGameOver.Invoke();
if (AllCoinsCollected())
_goToVictory.Invoke();
}
public void Draw(SpriteBatch spriteBatch)
{
_player.Draw(spriteBatch);
foreach (Enemy enemy in _enemies)
enemy.Draw(spriteBatch);
foreach (Coin coin in _coins)
coin.Draw(spriteBatch);
// desenhar HUD aqui
}
private bool AllCoinsCollected()
{
return _coins.All(c => c.Collected);
}
}
Agora a lógica do jogo está dentro da cena de gameplay, não no Game1.cs.
⏸️ Como criar pause corretamente
Existem duas formas comuns de criar pausa.
1. Pause como estado dentro da GameScene
A GameScene tem um booleano:
private bool _isPaused;
No Update:
if (input.IsPausePressed())
{
_isPaused = !_isPaused;
}
if (_isPaused)
{
return;
}
// atualizar gameplay
No Draw:
DrawGameplay(spriteBatch);
if (_isPaused)
{
DrawPauseOverlay(spriteBatch);
}
Essa abordagem é simples.
2. Pause como cena separada
A PauseScene fica por cima ou substitui a cena atual.
Exemplo:
_sceneManager.ChangeScene(new PauseScene(() =>
{
_sceneManager.ChangeScene(gameScene);
}));
Essa abordagem é mais arquitetural, mas exige cuidado para preservar o estado da fase.
Para iniciantes, o mais simples é manter o pause dentro da GameScene.
Para projetos maiores, pode fazer sentido criar uma pilha de cenas.
🧱 Scene Stack: pilha de cenas
Uma arquitetura mais avançada é usar uma pilha de cenas.
Funciona assim:
GameScene
PauseScene por cima
A GameScene continua existindo, mas para de atualizar. A PauseScene aparece sobre ela.
A estrutura seria:
public class SceneManager
{
private Stack<IScene> _scenes = new Stack<IScene>();
public void PushScene(IScene scene)
{
_scenes.Push(scene);
}
public void PopScene()
{
if (_scenes.Count > 0)
_scenes.Pop();
}
public void Update(GameTime gameTime, InputManager input)
{
if (_scenes.Count > 0)
_scenes.Peek().Update(gameTime, input);
}
public void Draw(SpriteBatch spriteBatch)
{
foreach (var scene in _scenes.Reverse())
{
scene.Draw(spriteBatch);
}
}
}
Esse modelo permite:
✅ pause sobre gameplay;
✅ inventário sobre gameplay;
✅ diálogo sobre fase;
✅ modal de confirmação;
✅ menu de opções sobre o menu principal.
Mas ele também é mais complexo. Use apenas quando fizer sentido.
🧠 GameComponent e DrawableGameComponent: alternativa do MonoGame
O MonoGame também possui classes próprias para componentes.
A documentação oficial descreve GameComponent como um objeto que pode ser anexado a um Game e ter seu método Update(GameTime) chamado quando o Update do jogo é chamado.
Já DrawableGameComponent é descrito como um objeto desenhável que, quando adicionado à coleção Game.Components, tem seu método Draw(GameTime) chamado quando Game.Draw(GameTime) é chamado.
A documentação sobre game loop também explica que componentes podem oferecer uma forma modular de adicionar funcionalidade ao jogo, registrando-os em Game.Components.Add, para que seus métodos de inicialização, atualização e desenho sejam chamados pelo ciclo principal do jogo.
Isso significa que existe uma alternativa oficial baseada em componentes.
Porém, para quem está construindo um curso progressivo, é mais didático começar com IScene, SceneManager, InputManager e classes próprias. Depois, você pode estudar GameComponent e DrawableGameComponent como uma abordagem adicional.
📜 Menus: o que um bom menu precisa ter?
Um menu parece simples, mas ele precisa ser claro, responsivo e fácil de navegar.
Um bom menu normalmente possui:
✅ título do jogo;
✅ opção selecionada visível;
✅ navegação por teclado;
✅ navegação por controle;
✅ clique por mouse, se fizer sentido;
✅ feedback visual;
✅ som de seleção;
✅ opção de iniciar;
✅ opção de sair;
✅ opção de configurações;
✅ retorno fácil.
Um menu ruim dá sensação amadora, mesmo que a gameplay seja boa.
🎨 Criando uma classe Button
Para menus com mouse, crie um botão reutilizável.
public class Button
{
public Rectangle Bounds { get; private set; }
public string Text { get; private set; }
private SpriteFont _font;
private bool _isHovered;
public Button(Rectangle bounds, string text, SpriteFont font)
{
Bounds = bounds;
Text = text;
_font = font;
}
public bool Update(InputManager input)
{
_isHovered = Bounds.Contains(input.MousePosition);
return _isHovered && input.IsLeftMouseClicked();
}
public void Draw(SpriteBatch spriteBatch)
{
Color color = _isHovered ? Color.Yellow : Color.White;
spriteBatch.DrawString(
_font,
Text,
new Vector2(Bounds.X, Bounds.Y),
color
);
}
}
Na MenuScene:
private Button _playButton;
private Button _exitButton;
No LoadContent:
_playButton = new Button(
new Rectangle(500, 300, 220, 50),
"Jogar",
_font
);
_exitButton = new Button(
new Rectangle(500, 370, 220, 50),
"Sair",
_font
);
No Update:
if (_playButton.Update(input))
_startGame.Invoke();
if (_exitButton.Update(input))
_exitGame.Invoke();
No Draw:
_playButton.Draw(spriteBatch);
_exitButton.Draw(spriteBatch);
Agora você tem botões reutilizáveis.
🧩 Organização de pastas recomendada
Uma estrutura saudável para o projeto seria:
MeuJogo/
├── Core/
│ ├── InputManager.cs
│ ├── SceneManager.cs
│ ├── IScene.cs
│ └── GameSettings.cs
│
├── Scenes/
│ ├── MenuScene.cs
│ ├── GameScene.cs
│ ├── PauseScene.cs
│ ├── GameOverScene.cs
│ └── VictoryScene.cs
│
├── Entities/
│ ├── Player.cs
│ ├── Enemy.cs
│ ├── Coin.cs
│ └── Projectile.cs
│
├── UI/
│ ├── Button.cs
│ ├── Hud.cs
│ └── TextLabel.cs
│
├── World/
│ ├── Level.cs
│ ├── Tile.cs
│ └── CollisionMap.cs
│
├── Content/
│ ├── sprites/
│ ├── backgrounds/
│ ├── audio/
│ ├── fonts/
│ └── ui/
│
├── Game1.cs
└── Program.cs
Essa estrutura facilita muito a evolução.
Você sabe onde cada coisa mora.
🧱 O papel do Game1.cs em uma arquitetura melhor
Depois que você organiza cenas, o Game1.cs deve ficar mais limpo.
Ele deve cuidar principalmente de:
✅ criar GraphicsDeviceManager;
✅ criar SpriteBatch;
✅ inicializar sistemas centrais;
✅ atualizar input;
✅ atualizar cena atual;
✅ desenhar cena atual;
✅ trocar cenas quando necessário.
Exemplo:
public class Game1 : Game
{
private GraphicsDeviceManager _graphics;
private SpriteBatch _spriteBatch;
private InputManager _input;
private SceneManager _sceneManager;
public Game1()
{
_graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
IsMouseVisible = true;
}
protected override void Initialize()
{
_input = new InputManager();
_sceneManager = new SceneManager(Content);
base.Initialize();
}
protected override void LoadContent()
{
_spriteBatch = new SpriteBatch(GraphicsDevice);
_sceneManager.ChangeScene(new MenuScene(
startGame: () => _sceneManager.ChangeScene(new GameScene()),
exitGame: () => Exit()
));
}
protected override void Update(GameTime gameTime)
{
_input.Update();
_sceneManager.Update(gameTime, _input);
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
_spriteBatch.Begin();
_sceneManager.Draw(_spriteBatch);
_spriteBatch.End();
base.Draw(gameTime);
}
}
Perceba a diferença.
O Game1.cs não conhece mais todos os detalhes da gameplay. Ele apenas coordena.
🧠 Separação de responsabilidades
Essa é uma das ideias mais importantes da arquitetura.
Cada classe deve ter uma responsabilidade clara.
Exemplo ruim
Game1.cs
- lê input
- move player
- desenha menu
- calcula colisão
- toca música
- cria inimigos
- desenha HUD
- controla game over
- salva jogo
- carrega fase
Exemplo melhor
InputManager → lê entrada
SceneManager → troca cenas
MenuScene → controla menu
GameScene → controla gameplay
Player → controla jogador
Enemy → controla inimigo
Hud → desenha vida e score
Level → controla fase
Button → controla botão
Essa divisão torna o projeto mais fácil de manter.
📊 Tabela: arquitetura simples vs arquitetura organizada
| Aspecto | Tudo no Game1 | Com cenas e arquitetura |
|---|---|---|
| Começo rápido | ✅ Sim | ⚠️ Um pouco mais lento |
| Organização | ❌ Baixa | ✅ Alta |
| Manutenção | ❌ Difícil | ✅ Melhor |
| Crescimento | ❌ Bagunça rápido | ✅ Escalável |
| Menus | ⚠️ Misturados | ✅ Separados |
| Game Over | ⚠️ Vira if/else | ✅ Cena própria |
| Testes | ❌ Difícil | ✅ Mais fácil |
| Reuso | ❌ Baixo | ✅ Alto |
| Clareza | ❌ Menor | ✅ Maior |
💀 Criando uma GameOverScene
public class GameOverScene : IScene
{
private SpriteFont _font;
private Action _restartGame;
private Action _goToMenu;
public GameOverScene(Action restartGame, Action goToMenu)
{
_restartGame = restartGame;
_goToMenu = goToMenu;
}
public void LoadContent(ContentManager content)
{
_font = content.Load<SpriteFont>("fonts/default");
}
public void Update(GameTime gameTime, InputManager input)
{
if (input.IsConfirmPressed())
_restartGame.Invoke();
if (input.IsCancelPressed())
_goToMenu.Invoke();
}
public void Draw(SpriteBatch spriteBatch)
{
spriteBatch.DrawString(
_font,
"Game Over",
new Vector2(520, 260),
Color.Red
);
spriteBatch.DrawString(
_font,
"Enter para tentar novamente",
new Vector2(430, 330),
Color.White
);
spriteBatch.DrawString(
_font,
"Esc para voltar ao menu",
new Vector2(450, 380),
Color.White
);
}
}
Agora o game over não está misturado com a gameplay.
Ele tem sua própria cena.
🏆 Criando uma VictoryScene
public class VictoryScene : IScene
{
private SpriteFont _font;
private Action _goToMenu;
public VictoryScene(Action goToMenu)
{
_goToMenu = goToMenu;
}
public void LoadContent(ContentManager content)
{
_font = content.Load<SpriteFont>("fonts/default");
}
public void Update(GameTime gameTime, InputManager input)
{
if (input.IsConfirmPressed())
_goToMenu.Invoke();
}
public void Draw(SpriteBatch spriteBatch)
{
spriteBatch.DrawString(
_font,
"Vitória!",
new Vector2(540, 260),
Color.Gold
);
spriteBatch.DrawString(
_font,
"Você completou a fase.",
new Vector2(460, 330),
Color.White
);
spriteBatch.DrawString(
_font,
"Pressione Enter para voltar ao menu.",
new Vector2(380, 390),
Color.White
);
}
}
Essa cena pode ser usada quando o jogador coleta todos os itens, derrota o chefe ou termina a fase.
⚙️ OptionsScene: preparando configurações
Uma tela de opções pode controlar:
- volume da música;
- volume dos efeitos;
- tela cheia;
- resolução;
- dificuldade;
- controles;
- idioma.
Exemplo inicial:
public class OptionsScene : IScene
{
private SpriteFont _font;
private Action _goBack;
public OptionsScene(Action goBack)
{
_goBack = goBack;
}
public void LoadContent(ContentManager content)
{
_font = content.Load<SpriteFont>("fonts/default");
}
public void Update(GameTime gameTime, InputManager input)
{
if (input.IsCancelPressed())
_goBack.Invoke();
}
public void Draw(SpriteBatch spriteBatch)
{
spriteBatch.DrawString(
_font,
"Opções",
new Vector2(540, 200),
Color.White
);
spriteBatch.DrawString(
_font,
"Volume: 100%",
new Vector2(500, 300),
Color.White
);
spriteBatch.DrawString(
_font,
"Esc para voltar",
new Vector2(500, 400),
Color.Yellow
);
}
}
Mesmo que no início seja simples, criar a cena já prepara o projeto para crescer.
🎼 Arquitetura também inclui áudio
Cenas diferentes podem ter músicas diferentes:
- menu principal;
- fase;
- chefe;
- vitória;
- game over.
Você pode criar um AudioManager:
public class AudioManager
{
public void PlayMusic(string name)
{
// tocar música
}
public void PlaySound(string name)
{
// tocar efeito
}
public void StopMusic()
{
// parar música
}
}
Assim, a MenuScene pode tocar música de menu e a GameScene música de fase.
O importante é não espalhar chamadas de áudio por todo lado sem controle.
🧱 HUD separado da GameScene
O HUD mostra informações como:
❤️ vida;
⭐ pontuação;
⏱️ tempo;
🪙 moedas;
🔋 energia;
🎯 objetivo.
Crie uma classe:
public class Hud
{
private SpriteFont _font;
public Hud(SpriteFont font)
{
_font = font;
}
public void Draw(SpriteBatch spriteBatch, int score, int lives)
{
spriteBatch.DrawString(
_font,
$"Score: {score}",
new Vector2(20, 20),
Color.White
);
spriteBatch.DrawString(
_font,
$"Vidas: {lives}",
new Vector2(20, 55),
Color.White
);
}
}
Na GameScene:
_hud.Draw(spriteBatch, _score, _lives);
Isso evita que a GameScene fique responsável por tudo.
🌍 Level: separando a fase da lógica principal
A fase também pode virar classe.
public class Level
{
public List<Rectangle> Platforms { get; private set; }
public List<Coin> Coins { get; private set; }
public List<Enemy> Enemies { get; private set; }
public Level()
{
Platforms = new List<Rectangle>();
Coins = new List<Coin>();
Enemies = new List<Enemy>();
}
public void Load()
{
Platforms.Add(new Rectangle(0, 500, 1280, 80));
Platforms.Add(new Rectangle(300, 420, 200, 30));
Platforms.Add(new Rectangle(650, 350, 200, 30));
}
public void Update(GameTime gameTime)
{
foreach (Enemy enemy in Enemies)
enemy.Update(gameTime);
}
public void Draw(SpriteBatch spriteBatch)
{
foreach (Coin coin in Coins)
coin.Draw(spriteBatch);
foreach (Enemy enemy in Enemies)
enemy.Draw(spriteBatch);
}
}
Agora a fase pode cuidar dos elementos do mundo.
A GameScene coordena, mas não precisa saber cada detalhe.
🔄 Transições entre cenas
Uma troca brusca de cena funciona, mas pode parecer simples demais.
Você pode criar transições:
- fade in;
- fade out;
- tela preta;
- slide;
- zoom;
- cortina;
- loading.
Exemplo conceitual:
public enum TransitionState
{
None,
FadingOut,
FadingIn
}
Campos:
private float _alpha = 0f;
private TransitionState _transitionState = TransitionState.None;
private IScene _nextScene;
Durante o fade out:
_alpha += deltaTime;
if (_alpha >= 1f)
{
ChangeSceneImmediately(_nextScene);
_transitionState = TransitionState.FadingIn;
}
Durante o fade in:
_alpha -= deltaTime;
if (_alpha <= 0f)
{
_alpha = 0f;
_transitionState = TransitionState.None;
}
No Draw, você desenha um retângulo preto por cima com transparência.
Esse tipo de polimento deixa o jogo mais profissional.
💾 Arquitetura pensando em salvar e carregar
Mesmo que seu jogo ainda não tenha sistema de save, a arquitetura deve permitir isso no futuro.
Você pode criar:
public class SaveData
{
public int CurrentLevel { get; set; }
public int Score { get; set; }
public int Lives { get; set; }
}
E um serviço:
public class SaveManager
{
public void Save(SaveData data)
{
// salvar em arquivo
}
public SaveData Load()
{
// carregar de arquivo
return new SaveData();
}
}
Se o jogo está bem organizado, adicionar save depois fica muito mais fácil.
Se tudo está no Game1.cs, fica muito mais difícil.
🧠 Arquitetura boa não nasce perfeita
Um erro comum é querer criar uma arquitetura perfeita antes de criar o jogo.
Isso também pode virar problema.
Você passa semanas criando sistemas e não cria gameplay.
O caminho ideal é equilíbrio:
- crie o protótipo simples;
- perceba o que está ficando repetido;
- extraia classes;
- crie cenas;
- organize sistemas;
- melhore aos poucos.
Arquitetura deve servir ao jogo.
Não o contrário.
⚠️ Erros comuns ao criar cenas e menus
1. Criar arquitetura complexa demais cedo
Se o jogo ainda é pequeno, não precisa de 50 classes.
Comece simples.
2. Deixar tudo no Game1 até tarde demais
O oposto também é ruim. Se o jogo já tem menu, gameplay, pause e game over, está na hora de separar.
3. Misturar lógica de menu com lógica de gameplay
Menu e gameplay têm responsabilidades diferentes.
4. Recarregar assets toda vez sem necessidade
Trocas de cena precisam ser pensadas. Assets podem ser carregados uma vez ou gerenciados com cuidado.
5. Não preservar estado ao pausar
Se você troca para PauseScene e destrói a GameScene, perde o progresso da fase.
6. Usar IsKeyDown em menus
Isso pode fazer o cursor pular várias opções rapidamente. Use tecla recém-pressionada.
7. Criar dependências circulares
Evite que todas as classes conheçam todas as outras.
8. Não separar HUD
HUD é interface. Gameplay é regra. São coisas diferentes.
9. Não pensar em transições
Trocas bruscas funcionam, mas transições melhoram a experiência.
10. Fazer o SceneManager virar “deus”
O SceneManager deve trocar e atualizar cenas, não controlar toda a lógica do jogo.
📊 Tabela prática: qual classe deve fazer o quê?
| Classe | Responsabilidade |
|---|---|
Game1 | Inicializar sistemas e chamar cena atual |
SceneManager | Controlar cena ativa |
IScene | Contrato básico de cena |
MenuScene | Menu principal |
GameScene | Gameplay |
PauseScene | Pausa |
GameOverScene | Fim de jogo |
VictoryScene | Vitória |
InputManager | Entrada do jogador |
Player | Lógica do jogador |
Enemy | Lógica dos inimigos |
Hud | Interface da fase |
Button | Botões clicáveis |
Level | Plataformas, inimigos e itens da fase |
AudioManager | Música e efeitos |
SaveManager | Salvar e carregar progresso |
✅ Checklist do Capítulo 7
Ao final deste capítulo, você deve entender:
✅ por que o Game1.cs não deve concentrar tudo;
✅ o que são cenas;
✅ o que é arquitetura de jogo;
✅ como usar GameState;
✅ quando evoluir para SceneManager;
✅ como criar uma interface IScene;
✅ como criar MenuScene;
✅ como criar GameScene;
✅ como criar GameOverScene;
✅ como criar VictoryScene;
✅ como criar menus navegáveis;
✅ como separar HUD;
✅ como organizar pastas;
✅ como pensar em pause;
✅ o que é scene stack;
✅ quando usar GameComponent;
✅ como evitar dependências confusas;
✅ como preparar o projeto para crescer.
🏁 Conclusão: arquitetura transforma protótipo em projeto
Cenas, menus e arquitetura são o ponto em que seu jogo começa a ganhar estrutura profissional.
Antes, você tinha sistemas funcionando separadamente: input, sprites, colisão, movimentação e física simples. Agora, esses sistemas precisam viver dentro de uma organização maior.
O menu não deve se misturar com a fase.
O pause não deve bagunçar a gameplay.
O game over não deve estar perdido no meio do código do jogador.
O HUD não deve ser responsabilidade do inimigo.
O Game1.cs não deve carregar o jogo inteiro nas costas.
A arquitetura resolve isso.
Ela distribui responsabilidades, melhora a manutenção e permite que o jogo cresça sem virar caos.
No MonoGame, essa organização não vem pronta como em engines visuais. Mas isso é parte do aprendizado. Você entende como um jogo é estruturado de verdade e cria sua própria base.
A partir daqui, seu projeto deixa de ser apenas um conjunto de códigos funcionando e começa a se tornar um jogo com telas, fluxo, navegação, estados, menus e estrutura.
Um protótipo prova uma ideia. Uma boa arquitetura permite transformar essa ideia em jogo completo.
❓ FAQ — Cenas, Menus e Arquitetura no MonoGame
1. O que são cenas no MonoGame?
Cenas são telas ou estados principais do jogo, como menu, fase, pause, game over e vitória. MonoGame não impõe uma estrutura única de cenas, então é comum criar sua própria interface IScene e um SceneManager.
2. Preciso criar um SceneManager?
Não no início, mas ele se torna muito útil quando o jogo passa a ter múltiplas telas, como menu, gameplay, pause e game over.
3. Posso usar apenas um enum GameState?
Sim. Para jogos pequenos, GameState com switch pode ser suficiente. Para jogos maiores, cenas separadas são mais organizadas.
4. O que deve ficar no Game1.cs?
O ideal é que o Game1.cs inicialize sistemas centrais, atualize input, chame a cena atual e desenhe a cena atual. Ele não deve conter toda a lógica do jogo.
5. O que é GameComponent?
GameComponent é uma classe do MonoGame que pode ser anexada ao jogo e ter seu Update(GameTime) chamado pelo ciclo do jogo.
6. O que é DrawableGameComponent?
DrawableGameComponent é um componente desenhável que pode ter seu Draw(GameTime) chamado quando o Draw do jogo é executado.
7. Menu deve ser uma cena?
Em projetos organizados, sim. Um menu tem lógica própria, input próprio e desenho próprio.
8. Pause deve ser uma cena separada?
Depende. Para jogos simples, pode ser um estado dentro da GameScene. Para jogos maiores, pode ser uma cena sobreposta usando uma pilha de cenas.
9. Como evitar código bagunçado?
Separe responsabilidades: InputManager para input, SceneManager para cenas, Player para jogador, Hud para interface, Level para fase e assim por diante.
10. Qual frase resume este capítulo?
Arquitetura é o que permite seu jogo crescer sem desmoronar.
Capítulo 1 — O que é MonoGame e por que usar para criar jogos
Capítulo 2 — Preparando o Ambiente de Desenvolvimento
Capítulo 3 — Fundamentos do Game Loop: Update e Draw
Capítulo 4 — Sprites, Texturas e Content Pipeline
Capítulo 5 — Entrada do Jogador: Teclado, Mouse e Controle
Capítulo 6 — Colisão, Física Simples e Movimentação
Capítulo 7 — Cenas, Menus e Arquitetura do Jogo
Capítulo 8 — Áudio, Partículas, Animações e Polimento
Capítulo 9 — Shaders, Câmera, Mapas e Performance
Capítulo 10 — Publicação, Monetização e Projeto Final no MonoGame