Overview
The game loop in Una Aventura Inesperada follows a classic game architecture pattern: initialize hardware, set up game state, and enter the main loop that processes input, updates game logic, and synchronizes with the display’s vertical blank.
Main Function Structure
The main() function (main.c:157) serves as the entry point and primary game loop:
int main(void)
{
// Hardware initialization
REG_POWERCNT = POWER_ALL_2D;
REG_DISPCNT_SUB = MODE_0_2D | DISPLAY_BG0_ACTIVE;
VRAM_C_CR = VRAM_ENABLE | VRAM_C_SUB_BG;
// Framebuffer setup for menu
VRAM_A_CR = VRAM_ENABLE | VRAM_A_LCD;
VRAM_B_CR = VRAM_ENABLE | VRAM_B_LCD;
fb = VRAM_A;
// Tile and map memory pointers
tileMemory = (u8*) BG_TILE_RAM_SUB(1);
mapMemory = (u16*) BG_MAP_RAM_SUB(0);
// Initialize palette (26 colors)
BG_PALETTE_SUB[0] = RGB15(0, 0, 0);
BG_PALETTE_SUB[1] = RGB15(15, 4, 6);
// ... (palette continues)
InicializarTeselas();
ConfigurarInterrupciones();
tiempo = 0;
record = 1000;
CrearMenu(menuTitulo, menuPrincipalBitmap, menuCreditosBitmap);
// Main loop - handles level restart
u32 keys;
while(1) {
if(esActivoBotonReinicio == true) {
scanKeys();
keys = keysCurrent();
if(keys & KEY_TOUCH && esActivoBotonesDialogos == true) {
touchRead(&posicionXY);
if((posicionXY.px >= puntosReinicioNivelBoton[0].x &&
posicionXY.px <= puntosReinicioNivelBoton[1].x) &&
(posicionXY.py >= puntosReinicioNivelBoton[0].y &&
posicionXY.py <= puntosReinicioNivelBoton[1].y)) {
esJuegoReiniciado = true;
ConsultarSistemaDialogo();
}
}
swiWaitForVBlank();
}
}
}
Game State Variables
The game uses several boolean flags to manage state transitions (main.c:91-99):
Controls whether the game is in an active play state. Set to false during gameplay, true in menus.
Tracks whether the player character is active in the current level.
Prevents player movement when interacting with enemies or obstacles.
Enables the level restart button during gameplay.
Prevents rapid dialog advancement through debouncing touch input.
Indicates whether the player has started the game from the main menu.
Toggles between animation frames (F1/F2) for sprites.
Update Cycle
The game follows a standard Input → Logic → Render pattern:
Player input is handled through IRQ_KEYS interrupts (see Interrupts). The TeclasJugador() callback (main.c:271) processes directional input:
void TeclasJugador() {
// Check for UP movement
if (movimientosJugador > 0 &&
REG_KEYINPUT == 0x03BF &&
posJugFila > 0 &&
esPartidaAcabada == false &&
mapMemory[(posJugFila-1)*32+posJugColumna] != 18 &&
mapMemory[(posJugFila-1)*32+posJugColumna] != 28) {
// Check for box collision
if(mapMemory[(posJugFila-2)*32+posJugColumna] == 19) {
MoverObstaculo(0);
}
// Check for enemy collision
else if(mapMemory[(posJugFila-2)*32+posJugColumna] == 5 ||
mapMemory[(posJugFila-2)*32+posJugColumna] == 42) {
int enemActual = -1;
for (int i=0; i<numeroDeEnemigos && enemActual == -1; i++) {
if(posJugFila-2 == posicionesEnemigo[i].y &&
posJugColumna == posicionesEnemigo[i].x) {
enemActual = i;
}
}
MoverEnemigo(0, enemActual);
}
// Update player position if path is clear
if(mapMemory[(posJugFila-2)*32+posJugColumna] != 19 &&
mapMemory[(posJugFila-2)*32+posJugColumna] != 5 &&
puedeJugadorMoverse == true) {
// ... position update code
}
movimientosJugador--;
ActualizarBarraMovimientos();
}
}
The game uses a 2x2 tile grid for all entities. Movement checks look 2 tiles ahead, and collision detection only examines diagonal corners.
2. Game Logic
Logic updates include:
- Movement validation - Checking tile indices for walls (17, 18, 24, 26-28), boxes (19), and enemies (5, 42)
- Collision resolution - Pushing boxes, eliminating enemies, or blocking player movement
- Stamina tracking - Decrementing
movimientosJugador and updating the HUD bar
- Win condition - Reaching tile 23 triggers
ConsultarSistemaDialogo() (main.c:476)
3. Rendering
Rendering is handled through:
- Tile map updates - Modifying
mapMemory to change displayed tiles
- Framebuffer drawing - Direct pixel manipulation for the stamina bar (main.c:809-813)
- DMA transfers - Loading full-screen images for menus/dialogs
Frame Synchronization
The game synchronizes with the display using vertical blank waiting:
This function appears in:
- Main loop (main.c:232) - Prevents screen tearing during level restart checks
- Menu loop (main.c:927) - Ensures smooth menu navigation
- Dialog loop (main.c:962) - Synchronizes dialog rendering
All rendering loops call swiWaitForVBlank() to prevent screen tearing. The NDS refreshes at 60 Hz, so this effectively caps the game at 60 FPS.
State Transitions
Triggered by the “Start Game” button in CrearMenu() (main.c:906):
if((posicionXY.px >= puntosComenzaPartidaBoton[0].x &&
posicionXY.px <= puntosComenzaPartidaBoton[1].x) &&
(posicionXY.py >= puntosComenzaPartidaBoton[0].y &&
posicionXY.py <= puntosComenzaPartidaBoton[1].y)) {
// Play intro cinematics
CrearDialogo(CinematicaInicioF1Bitmap, 2);
CrearDialogo(CinematicaInicioF2Bitmap, 2);
CrearDialogo(CinematicaInicioF3Bitmap, 2);
// Load level 1
mapaAcutal = nivel1;
GenerarNivel(nivel1, HUDBitmap, MOVIMIENTOS_NIV1);
esJuegoComenzado = true;
}
Gameplay → Dialog
When the player reaches the exit (tile 23), ConsultarSistemaDialogo() is called (main.c:658). This function:
- Disables movement interrupts:
REG_KEYCNT = 0x3FFF;
- Presents quiz questions via
CrearDialogo()
- Advances to next level or restarts current level
- Re-enables movement:
REG_KEYCNT = 0x7FFF; (in GenerarNivel())
Level Progression
The game tracks progress through nivelActual (0-4) and uses a switch statement (main.c:660) to handle each level’s quiz:
switch(nivelActual) {
case 0: // Level 1
if(esJuegoReiniciado == false &&
CrearDialogo(Pregunta1_1Bitmap, 1) &&
CrearDialogo(Pregunta1_2Bitmap, 0) &&
CrearDialogo(Pregunta1_3Bitmap, 0)) {
nivelActual++;
CrearDialogo(PreguntasAcertadasBitmap, 2);
mapaAcutal = nivel2;
GenerarNivel(nivel2, HUDBitmap, MOVIMIENTOS_NIV2);
} else {
if(esJuegoReiniciado == false) {
CrearDialogo(PreguntaFallidaBitmap, 2);
}
esJuegoReiniciado = false;
GenerarNivel(nivel1, HUDBitmap, MOVIMIENTOS_NIV1);
}
break;
// ... cases 1-4 for other levels
}
Level Generation
The GenerarNivel() function (main.c:792) initializes a new level:
void GenerarNivel(u16 mapa[], unsigned int imagen[], int numeroMovimientosMax) {
timerUnpause(1); // Resume animations
REG_KEYCNT = 0x7FFF; // Enable input interrupts
esActivoBotonReinicio = true;
esPartidaAcabada = false;
jugadorVivo = true;
// Load HUD to main screen
dmaCopy(imagen, VRAM_A, 256*192*2);
REG_DISPCNT = MODE_FB0;
movimientosJugador = numeroMovimientosMax;
maximoMovimientosJugador = numeroMovimientosMax;
// Draw stamina bar
for(int lin = COMIENZO_LINEA_BARRA_ESTAMINA;
lin < ALTO_BARRA_ESTAMINA; lin++) {
for(int col = COMIENZO_COLUMNA_BARRA_ESTAMINA;
col < ANCHO_BARRA_ESTAMINA; col++) {
fb[lin*256+col] = RGB15(0, 30, 0);
}
}
// Load tile map and track entity positions
int fila, columna, contEnemigos = 0;
numeroDeEnemigos = 0;
pos_mapData = 0;
for(fila = 0; fila < 24; fila++) {
for(columna = 0; columna < 32; columna++) {
pos_mapMemory = fila*32 + columna;
mapMemory[pos_mapMemory] = mapa[pos_mapData];
// Track player position (tile 0)
if(mapMemory[pos_mapMemory] == 0) {
posJugColumna = columna;
posJugFila = fila;
}
// Track NPC position (tile 38)
else if(mapMemory[pos_mapMemory] == 38) {
posNpcColumna = columna;
posNpcFila = fila;
}
// Track enemy positions (tiles 5, 42)
else if(mapMemory[pos_mapMemory] == 5 ||
mapMemory[pos_mapMemory] == 42) {
posicionesEnemigo[contEnemigos].x = columna;
posicionesEnemigo[contEnemigos].y = fila;
posicionesEnemigo[contEnemigos].vivo = true;
contEnemigos++;
numeroDeEnemigos++;
}
pos_mapData++;
}
}
}
Target Frame Rate: 60 FPS (synchronized with VBlank)Update Frequency:
- Player input: Interrupt-driven (instant response)
- Animations: Timer-driven at ~0.5 Hz (TIMER_DATA = 32768, DIV_1024)
- Stamina bar: Updated only on player movement
The game minimizes per-frame work by:
- Using interrupt-driven input instead of polling
- Only updating tiles that change (no full screen refresh)
- Leveraging hardware DMA for large transfers
- Pausing animation timers during menus/dialogs