- C
- raylib
- 3D
- Jogos
AstroBeat
Jogo rítmico 3D de corrida infinita, desenvolvido em C com a biblioteca raylib.
AstroBeat é um jogo rítmico 3D no estilo corrida infinita, escrito em C com a raylib. O jogador pilota uma nave por uma pista neon dividida em três faixas, desviando de obstáculos e acertando notas no ritmo da música. Cada música tem um beatmap próprio, então a jogabilidade é sincronizada com o áudio em vez de ser gerada proceduralmente.
Estrutura e máquina de estados
O jogo é dividido em módulos por responsabilidade (main.c, menu.c, notas.c, score.c, player.c, environment.c, buffs.c, audio.c, highscores.c), e o main roda uma máquina de estados declarada em globals.h:
typedef enum {
MENU, SONG_SELECT, SETTINGS, HIGHSCORES,
PLAYING, PAUSED, RESULTS, QUIT
} GameState;
O laço principal é um while (true) com um switch grande que atualiza e depois desenha o estado atual. Duas câmeras 3D coexistem: uma para o cenário de fundo (menu) e outra para a jogabilidade.
A sincronia com a música
O coração do jogo é o relógio da música. Enquanto joga, o tempo do jogo é a posição tocada pela raylib descontada do instante em que a música começou:
if (gameState == PLAYING || gameState == PAUSED) {
tempo_jogo = obterTempoMusica(&audio) - tempo_inicio_musica;
if (tempo_jogo < 0.0f) tempo_jogo = 0.0f;
}
As notas e obstáculos não usam física por frame: a posição 3D é uma função do tempo da música. Uma nota nasce em z = -80 e viaja a VEL_NOTAS = 10.0 unidades por segundo; o instante do acerto é quando tempo_jogo cruza o tempo da nota (a linha de acerto fica em z ≈ -2, o HIT_OFFSET):
float z = -80.0f + (tempo_atual - (array_notas[i].tempo - TEMPO_ATE_HIT)) * VEL_NOTAS;
Como a posição deriva do tempo, o desenho fica desacoplado da taxa de frames (o alvo é 144 FPS), mas todo o julgamento passa a depender do relógio do áudio.
Mapas e notas
Cada música em songs/ tem um beatmap em mapasMusicas/ e uma lista de coletáveis em mapasColetaveis/. A leitura usa um formato simples de texto, tempo faixa tipo duracao por linha:
while (i < 1000 && fscanf(f, "%f %d %d %f",
&array_notas[i].tempo,
&array_notas[i].lane,
(int*)&array_notas[i].tipo,
&array_notas[i].duracao) == 4) {
array_notas[i].ativa = 0;
array_notas[i].resultado = JULG_PENDENTE;
i++;
}
total_de_notas = i;
Cada nota tem um tipo (grave, agudo, longa, esquerda, direita) e uma faixa (0 a 2). Para acertar uma nota normal é preciso estar na mesma faixa dela (laneAnterior == lane) e pressionar a tecla correspondente ao tipo no instante certo; a nota longa é segurada com espaço.
Julgamento e pontuação
O julgamento usa janelas de tempo em torno do instante ideal da nota, definidas como constantes:
float JANELA_PERFECT = 0.050f;
float JANELA_GREAT = 0.075f;
float JANELA_GOOD = 0.100f;
float JANELA_OK = 0.125f;
float JANELA_MISS = 0.150f;
O hit acontece quando a diferença |tempo_atual - nota.tempo| cai na janela certa, e cada julgamento tem um multiplicador próprio somado ao multiplicador de streak:
float multiplicador_julgamento(Julgamento nota) {
case JULG_OK: return 1.0f;
case JULG_GOOD: return 1.25f;
case JULG_GREAT: return 1.5f;
case JULG_PERFECT: return 2.0f;
}
float multiplicador_streak(int streak) {
if (streak >= 30) return 8.0f;
if (streak >= 20) return 4.0f;
if (streak >= 10) return 2.0f;
return 1.0f;
}
Errar zera o streak e volta o multiplicador para 1. As notas longas são julgadas em duas partes: a cabeça segue a janela normal e, enquanto a tecla é segurada, a cada 0.1s soma 10 pontos por multiplicador; soltar antes do fim conta como erro. Ao final, os acertos são organizados numa matriz por tipo (grave, agudo, longa, movimento e geral) com totais, acertos, erros e precisão em %, e o rank (S, A, B, C, D ou F) sai da precisão geral.
Os buffs (coletáveis nas pistas) mexem nessa lógica: o multiplicador 2x dobra os pontos por um tempo e a “janela +20ms” alarga as janelas de julgamento em 20 ms.
Áudio, configuração e recordes
O audio.c carrega a música com a raylib, controla play/pause/seek e persiste o volume em configuracoes.cfg (texto, com padrão 0.8). O highscores.c guarda, por música, a maior pontuação em highscores.cfg no formato Nome=pts e só atualiza o arquivo quando a nova pontuação supera a anterior. O DataLoader de outros projetos não existe aqui; o jogo cria os arquivos de configuração em tempo de execução.
Particularidades e pontos que revisaria
-
Debug espalhado no código final. O jogo imprime julgamento a cada nota com
printf(PERFECT,GREAT,MISS,LONG COMPLETA,PAUSADO em: %.4fe váriosfflush), e ainda existe um atalho de debug que pula a música 10 segundos (KEY_F, comentado como “DEBUG: skip na música”). Isso não deveria estar no build de entrega. -
Estado global pesado e limites fixos.
gameStateé global, earray_notas[1000]comtotal_de_notassãoexterncompartilhados entre módulos. A leitura do beatmap para em 1000 notas sem avisar, então uma música com mapa maior seria truncada em silêncio. Funciona num jogo acadêmico, mas o acoplamento dificulta testar e evoluir. -
Cast de enum e falta de validação no mapa. Para ler o tipo com
%deu faço(int*)&array_notas[i].tipo, o que depende do enum serint, e não valido faixa, tipo nem a ordem dos tempos na leitura. Uma linha comlaneoutipoinválido só se manifesta mais tarde (o desenho ignora faixas fora de 0 a 2). -
Geometria derivada do tempo (decisão que eu manteria). A posição de cada nota é uma projeção do tempo da música, o que deixa o visual estável independente de frame. O preço é que o julgamento inteiro depende do
GetMusicTimePlayedda raylib; se o áudio dessincronizar, viram erros de julgamento. -
Configuração em texto na raiz.
configuracoes.cfgehighscores.cfgsão escritos no diretório de trabalho atual; o parse de recordes usa%99[^=]=%de compara o nome com a lista de músicas, o que é frágil se o arquivo for editado na mão.
Pontos de melhoria
- Remover os
printfde debug e o atalhoKEY_F, ou isolá-los atrás de um#define DEBUG. - Validar o beatmap na leitura (faixa, tipo, tempos crescentes, duração) e avisar quando estourar
MAX_NOTAS, ou alocar dinamicamente. - Gravar configurações e recordes em um diretório padrão (ex.:
~/.astrobeat) em vez da raiz do executável. - Reduzir o acoplamento por globais, passando estado por parâmetro e isolando a lógica de julgamento para poder testá-la.
Gabriel Rocha