Grada Racing en Java 17: así está organizado un videojuego Android desde su código fuente
Serie de desarrollo Grada Racing — Artículo 1 de 4. Arquitectura, clases, ciclo de vida, representación gráfica y decisiones técnicas.
¿Cómo se construye un juego de coches retro para Android sin recurrir a Unity, Unreal u otro motor de videojuegos? En Grada Racing el núcleo está escrito en Java 17: la conducción, el progreso, los menús, el audio sintetizado y la imagen pixel art se coordinan con un motor propio. Android aporta la ventana de juego, los controles táctiles, el acelerómetro y la reproducción de sonido.
En este artículo no me limitaré a enumerar tecnologías. Voy a recorrer fragmentos reales del código que forman parte del proyecto, explicar qué hace cada uno y cómo encajan entre sí. El propósito es mostrar las decisiones de desarrollo y también sus compromisos técnicos.
Nota de lectura: todos los bloques identificados como «archivo real» proceden de la versión del código fuente revisada el 28 de octubre de 2026. Son extractos literales, no ejemplos de una aplicación ficticia. Algunos fragmentos, al estar separados de sus clases, no se compilan individualmente.
1. Qué es Grada Racing y qué se ha programado
Grada Racing es un arcade de conducción contrarreloj, con estética de recreativa de los años ochenta y ocho escenarios inspirados en ciudades españolas: Barcelona, Valencia, Bilbao, Madrid, A Coruña, Granada, Sevilla y Málaga. El jugador esquiva tráfico, toma curvas, gestiona la velocidad y trata de terminar cada etapa antes de agotar el cronómetro.
El juego presenta dos formas de progresión. En Vuelta a España se recorren cuatro etapas con bifurcaciones: al finalizar una, el jugador selecciona el siguiente destino en el mapa. En Etapa libre se pueden repetir los circuitos desbloqueados y mejorar sus récords. También existe personalización del coche, guardado y recuperación de partidas, radio chiptune, aceleración automática opcional y controles por botones o inclinación del teléfono.
Un juego así necesita resolver varios problemas distintos: calcular el movimiento del vehículo, representar el escenario, dibujar el marcador, detectar pulsaciones, almacenar el progreso y respetar el ciclo de vida de Android. Si todo se concentra en una sola clase de actividad, mantenerlo se vuelve difícil. La arquitectura elegida intenta separar esas responsabilidades.
2. Configuración real: Java 17, Android nativo y Gradle
El archivo app/build.gradle confirma un proyecto Android nativo con identificador org.informatoli.gradaracing, nivel mínimo de API 24, compilación con API 36 y compatibilidad de código fuente Java 17. La configuración de compilación no demuestra por sí sola que el juego haya superado todas las pruebas de publicación: identifica el entorno para el que se ha preparado.
Archivo real: app/build.gradle (líneas 11–23).
android {
namespace 'org.informatoli.gradaracing'
compileSdk 36
defaultConfig {
applicationId 'org.informatoli.gradaracing'
minSdk 24
targetSdk 36
versionCode 1
versionName '1.0'
}
La instrucción compileSdk indica contra qué SDK se compila; targetSdk expresa el nivel de comportamiento de Android al que se orienta la aplicación; y minSdk fija el nivel mínimo declarado para instalarla. El código Java se compila con las opciones sourceCompatibility y targetCompatibility, recogidas en el mismo archivo:
Archivo real: app/build.gradle (líneas 46–49).
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
La carpeta raíz incluye settings.gradle, gradlew y las configuraciones del plugin de Android. Las clases nativas viven en app/src/main/java/. El motor core es un paquete Java dentro del módulo Android, no un segundo módulo independiente de Gradle. Esta distinción importa porque la separación es lógica y de dependencias, no una publicación de librería aparte.
3. Mapa de clases: quién hace cada trabajo
GradaRacing/
├── app/
│ ├── build.gradle
│ └── src/main/java/org/informatoli/gradaracing/
│ ├── MainActivity.java Actividad, ciclo de vida y servicios Android
│ ├── GameView.java SurfaceView, bucle y controles táctiles
│ ├── AudioOut.java Salida de audio en Android
│ └── core/
│ ├── Game.java Estados, reglas, progresión y menús
│ ├── Stage.java Datos de una etapa
│ ├── Stages.java Escenarios y conexiones entre etapas
│ ├── Track.java Trazado, curvas, decoración y tráfico
│ ├── Raster.java Búfer de píxeles ARGB
│ ├── Draw.java Primitivas y tipografía pixel art
│ ├── Backdrop.java Paisajes y fondos
│ ├── Props.java Objetos del decorado
│ ├── CarArt.java Dibujo de los vehículos
│ ├── Synth.java Síntesis de canciones
│ └── Mixer.java Mezcla de música y efectos
├── desktop/
│ ├── ejecutar.bat / ejecutar.sh
│ └── src/.../desktop/
│ ├── DesktopMain.java Adaptación para escritorio
│ └── StoreAssets.java Recursos gráficos de tienda
└── store/ Capturas y materiales de publicación
En este esquema, Game coordina la partida; Track genera el recorrido; Raster y las clases de dibujo construyen la imagen; GameView entrega esa imagen a la pantalla Android; y MainActivity se conecta con servicios del sistema operativo. El audio también está dividido entre generación de muestras y salida física.
Esta organización permite razonar sobre el videojuego por capas. No evita toda dependencia entre clases, pero sí reduce el acoplamiento más importante: el motor no necesita importar android.app.Activity, SurfaceView ni SharedPreferences.
4. La decisión clave: una interfaz para separar juego y plataforma
Una pieza pequeña de código explica gran parte del diseño. La clase Game declara una interfaz Platform. El motor expresa lo que necesita del entorno —leer, guardar, abrir un enlace, consultar la inclinación y salir— sin decidir cómo lo realizará Android o el ordenador de escritorio.
Archivo real: app/src/main/java/org/informatoli/gradaracing/core/Game.java (líneas 16–24).
public interface Platform {
String load(String key);
void save(String key, String value);
void openUrl(String url);
/** Inclinación del móvil normalizada -1..1 (0 si no hay sensor). */
float tilt();
void quit();
}
La interfaz actúa como contrato. Para guardar, Game llama a plat.save(...); en Android, la implementación usa SharedPreferences; en la versión de escritorio, un archivo .properties. El motor puede reutilizar la misma lógica porque conoce las operaciones, no su implementación concreta.
El constructor recibe ese contrato mediante inyección de dependencias por constructor. Inicializa las preferencias guardadas, el tamaño lógico y la demostración del menú antes de aplicar los ajustes de música:
Archivo real: app/src/main/java/org/informatoli/gradaracing/core/Game.java (líneas 139–146).
public Game(Platform p) {
this.plat = p;
loadProgress();
resize(1920, 1080);
startDemo();
mixer.musicOn = musicOn;
mixer.sfxOn = sfxOn;
}
Esta idea resulta útil más allá de los videojuegos. El mismo patrón puede aplicarse a una aplicación de escritorio: la lógica de negocio trabaja con una interfaz y no con una implementación concreta de archivos, base de datos o ventana.
5. Android solo construye el entorno del motor
El siguiente fragmento procede de MainActivity, el punto de entrada Android. La actividad prepara las preferencias, crea el motor Java, construye la vista, enlaza el audio y obtiene el sensor de aceleración cuando está disponible.
Archivo real: app/src/main/java/org/informatoli/gradaracing/MainActivity.java (líneas 38–48).
@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
prefs = getSharedPreferences("gradaracing", MODE_PRIVATE);
game = new Game(this);
view = new GameView(this, game);
setContentView(view);
audio = new AudioOut(game.mixer);
sensors = (SensorManager) getSystemService(SENSOR_SERVICE);
accel = sensors != null ? sensors.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) : null;
No hay una jerarquía de pantallas Android separadas para cada menú o etapa. El juego utiliza una actividad principal que dibuja su propia interfaz. Por eso los menús de «Tu coche», selección de ruta y pausa son pantallas lógicas del motor, no actividades Android independientes.
Hay una consecuencia importante: MainActivity controla los recursos específicos de Android y la sincronización con el motor, pero no necesita conocer cómo se calcula una curva de la carretera ni cómo se dibuja un edificio.
6. Una máquina de estados para organizar menús y carrera
Los videojuegos tienen situaciones mutuamente distintas: título, menú, selección de etapa, cuenta atrás, carrera, pausa, final, garaje y otras. En Grada Racing estos modos se representan mediante constantes enteras. La clase Game utiliza state para saber qué comportamiento debe ejecutar.
Archivo real: app/src/main/java/org/informatoli/gradaracing/core/Game.java (líneas 34–36).
static final int S_TITLE = 0, S_MENU = 1, S_RADIO = 2, S_SELECT = 3, S_COUNT = 4, S_PLAY = 5, S_PAUSE = 6,
S_SLOTS = 7, S_CLEAR = 8, S_MAP = 9, S_OVER = 10, S_END = 11, S_OPTIONS = 12, S_CREDITS = 13, S_CONFIRM = 14,
S_GARAGE = 15, S_TEXT = 16;
Para cambiar de estado existe un método centralizado. No se limita a asignar el número: también reinicia el tiempo interno del estado, el foco del menú y el botón presionado, y recalcula la disposición visual:
Archivo real: app/src/main/java/org/informatoli/gradaracing/core/Game.java (líneas 176–182).
void go(int s) {
state = s;
st = 0;
focus = -1;
pressed = null;
layout();
}
En el método update(float dt), un switch separa la lógica de la cuenta atrás, la carrera, las transiciones y la pausa. Por ejemplo, durante la carrera se actualiza el mundo, disminuye el tiempo disponible y se comprueba si el jugador ha llegado a la meta o si el cronómetro se ha agotado.
Archivo real: app/src/main/java/org/informatoli/gradaracing/core/Game.java (líneas 278–288).
case S_PLAY:
updateWorld(dt, autopilot ? 1 : 0);
timeLeft -= dt;
if (timeLeft <= 0) {
timeLeft = 0;
deleteAutosave();
go(S_OVER);
} else if (position + playerZ >= track.goalZ) {
stageClear();
}
break;
Se trata de un extracto dentro de un switch, no de un método autónomo. La solución es sencilla y explícita, adecuada a un proyecto de tamaño contenido. Si el número de estados y transiciones creciese mucho, podría estudiarse una estructura de objetos por estado, pero no resulta necesario introducirla solo por seguir un patrón más complejo.
7. Resolución virtual: dibujar pocos píxeles y ampliarlos
Las capturas muestran una estética de píxel grande y bordes marcados. El proyecto no dibuja todos los objetos directamente sobre un lienzo de 1920 × 1080 píxeles. Primero calcula una resolución virtual en función de las proporciones de pantalla, y luego escala el resultado.
Archivo real: app/src/main/java/org/informatoli/gradaracing/core/Game.java (líneas 149–167).
/** Calcula una resolución virtual retro (240 px de alto) con la proporción de la pantalla real. */
public void resize(int screenW, int screenH) {
float aspect = (float) screenW / Math.max(1, screenH);
int nw, nh;
if (aspect >= 1.25f) {
nh = 240;
nw = Math.min(620, Math.round(240 * aspect));
} else {
nw = 320;
nh = Math.min(700, Math.round(320 / aspect));
}
if (raster == null || nw != W || nh != H) {
W = nw;
H = nh;
raster = new Raster(W, H);
layout();
}
}
En orientación apaisada, el código emplea 240 píxeles de alto y calcula la anchura a partir de la relación de aspecto, con un límite de 620. En orientación estrecha utiliza otra rama de cálculo. Esto reduce el trabajo del rasterizador y favorece la estética retro. La contrapartida es que las coordenadas táctiles y los márgenes seguros deben transformarse al mismo espacio virtual.
El recálculo de Raster solo se realiza si cambian las dimensiones efectivas. Evitar recrearlo continuamente reduce asignaciones innecesarias durante el juego.
8. Raster.java: un lienzo ARGB programado en Java
El objeto Raster contiene un arreglo lineal de enteros. Cada entero representa el color de un píxel ARGB. Esto permite crear la imagen desde Java puro, antes de que intervenga Bitmap en Android.
Archivo real: app/src/main/java/org/informatoli/gradaracing/core/Raster.java (líneas 9–21).
*/
public final class Raster {
public final int w, h;
public final int[] pix;
private int col = 0xFF000000, a = 255;
private int cx0, cy0, cx1, cy1;
private final float[] xs = new float[64];
public Raster(int w, int h) {
this.w = w;
this.h = h;
this.pix = new int[w * h];
unclip();
La inicialización reserva exactamente w × h posiciones. La operación de limpieza de pantalla rellena el arreglo con un color:
Archivo real: app/src/main/java/org/informatoli/gradaracing/core/Raster.java (líneas 28–31).
public void clear(int c) {
Arrays.fill(pix, c);
}
Para dibujar se utilizan operaciones de más nivel, como rectángulos y polígonos. Un detalle técnico interesante es span: trabaja sobre filas y aplica recorte; cuando el color contiene transparencia, combina los canales con el píxel de destino mediante una operación alfa realizada en CPU.
Archivo real: app/src/main/java/org/informatoli/gradaracing/core/Raster.java (líneas 51–68).
private void span(int y, int x0, int x1) {
if (x0 < cx0) x0 = cx0;
if (x1 > cx1) x1 = cx1;
if (x0 >= x1) return;
int o = y * w;
if (a == 255) {
Arrays.fill(pix, o + x0, o + x1, col);
} else {
int sr = (col >> 16) & 255, sg = (col >> 8) & 255, sb = col & 255, ia = 255 - a;
for (int i = o + x0, e = o + x1; i < e; i++) {
int d = pix[i];
int r = (sr * a + ((d >> 16) & 255) * ia) / 255;
int g = (sg * a + ((d >> 8) & 255) * ia) / 255;
int b = (sb * a + (d & 255) * ia) / 255;
pix[i] = 0xFF000000 | (r << 16) | (g << 8) | b;
}
}
}
No es una llamada mágica a la GPU. Es un rasterizador por software y su coste debe valorarse según el tamaño del búfer, el número de elementos dibujados y el dispositivo. Para este proyecto existe una razón estética y arquitectónica clara: el mismo conjunto de píxeles puede presentarse tanto en Android como en un ordenador.
9. GameView: del búfer Java a la pantalla Android
GameView hereda de SurfaceView, implementa SurfaceHolder.Callback y utiliza un hilo dedicado. La vista no calcula la ruta del jugador; recibe la imagen ya dibujada por el motor y la transfiere a la superficie.
En el constructor desactiva el filtrado y el antialiasing de Paint, lo que ayuda a conservar los bordes de la imagen ampliada:
Archivo real: app/src/main/java/org/informatoli/gradaracing/GameView.java (líneas 35–44).
public GameView(Context ctx, Game game) {
super(ctx);
this.game = game;
paint.setFilterBitmap(false);
paint.setAntiAlias(false);
getHolder().addCallback(this);
setFocusable(true);
setKeepScreenOn(true);
}
El bucle es uno de los fragmentos centrales del artículo. Usa un acumulador de tiempo real, ejecuta actualizaciones de paso fijo y, después, solicita al juego que produzca el siguiente fotograma:
Archivo real: app/src/main/java/org/informatoli/gradaracing/GameView.java (líneas 117–138).
public void run() {
long last = System.nanoTime();
float acc = 0;
while (running) {
long now = System.nanoTime();
acc += (now - last) / 1e9f;
last = now;
if (acc > 0.25f) acc = 0.25f;
int w, h;
synchronized (game) {
while (acc >= STEP) {
game.update(STEP);
acc -= STEP;
}
game.render();
w = game.W;
h = game.H;
if (bitmap == null || bitmap.getWidth() != w || bitmap.getHeight() != h) {
bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
}
bitmap.setPixels(game.raster.pix, 0, w, 0, 0, w, h);
}
Con STEP = 1f / 60f, la intención es avanzar la simulación en intervalos regulares. El acumulador permite ejecutar varias actualizaciones cuando el tiempo transcurrido lo requiere; limitarlo a 0,25 segundos evita que el motor trate de recuperar sin límite una pausa prolongada. Esto no garantiza 60 FPS constantes: medir el rendimiento real requeriría instrumentación y pruebas en diferentes teléfonos.
Luego la aplicación obtiene un Canvas de la superficie y presenta el Bitmap con el escalado definido. El fragmento siguiente es el tramo real de presentación; la liberación de la superficie se controla con unlockCanvasAndPost:
Archivo real: app/src/main/java/org/informatoli/gradaracing/GameView.java (líneas 139–157).
SurfaceHolder holder = getHolder();
Canvas c = null;
try {
c = Build.VERSION.SDK_INT >= 26 ? holder.lockHardwareCanvas() : holder.lockCanvas();
if (c != null) {
dst.set(0, 0, c.getWidth(), c.getHeight());
c.drawBitmap(bitmap, null, dst, paint);
}
} catch (IllegalStateException | IllegalArgumentException ignored) {
// la superficie se está destruyendo
} finally {
if (c != null) {
try {
holder.unlockCanvasAndPost(c);
} catch (IllegalStateException | IllegalArgumentException ignored) {
// nada
}
}
}
También hay una cuestión de concurrencia: el bloque synchronized(game) coordina actualización, rasterizado y lectura del búfer con las operaciones que pueden llegar de otros hilos. El mismo objeto se utiliza para proteger acciones de ciclo de vida y eventos del usuario.
10. Escenarios como datos: Stages y Stage
Otra decisión útil consiste en representar las etapas como conjuntos de parámetros. La clase Stage almacena identificador, ciudad, barrio, rutas disponibles, colores, elementos del escenario, dificultad, tráfico y características del recorrido. Stages reúne las ocho definiciones.
La definición de Barcelona muestra cómo una misma estructura parametriza un escenario costero, sin crear una clase completamente distinta para cada ciudad:
Archivo real: app/src/main/java/org/informatoli/gradaracing/core/Stages.java (líneas 22–34).
// ---------------------------------------------------------------- Barcelona
s = new Stage(BARCELONA, "Barcelona", "La Barceloneta");
s.mapLon = 2.17f; s.mapLat = 41.39f; s.next = new int[]{VALENCIA, BILBAO};
s.skyTop = 0x1C6FE8; s.skyBottom = 0x9AD4FF; s.fog = 0xA9DAFF; s.clouds = true;
s.roadL = 0x6F6F72; s.roadD = 0x67676A; s.grassL = 0xEBDDB8; s.grassD = 0xE2D2AA;
s.rumbleL = 0xF4F4F4; s.rumbleD = 0xCF3B2E; s.lane = 0xF4F4F4;
s.farType = Stage.FAR_SKYLINE; s.farColor = 0x86A0BE; s.farColor2 = 0x9DB6D1;
s.nearType = Stage.NEAR_SEA; s.nearColor = 0x1E6FD6;
s.weights(Props.PALM, 10, Props.UMBRELLA, 4, Props.BUSH, 2, Props.BILLBOARD, 0);
s.lampEvery = 20; s.density = 0.30f;
s.billboards = new String[]{"INFORMATOLI.ORG", "CHIRINGUITO", "HORCHATA", "BARCELONETA", "PAELLA"};
s.curvy = 0.30f; s.hilly = 0.15f; s.segments = 2400; s.traffic = 22; s.difficulty = 1;
ALL[BARCELONA] = s;
La ruta se configura mediante next. En Barcelona, las siguientes opciones son Valencia o Bilbao. Los colores del cielo, la niebla, el mar y la carretera cambian el ambiente; los pesos de Props favorecen determinados objetos; segments, traffic, curvy y hilly definen parámetros del recorrido.
Este enfoque hace que la ampliación de contenido sea principalmente un trabajo de definir datos y combinaciones. La implementación concreta de la carretera, su perspectiva y la proyección de objetos la analizaremos en detalle en el artículo 2.
La transición entre etapas tampoco es solo una pantalla decorativa: al elegir destino, el juego lo incorpora a la ruta, desbloquea el escenario y suma tiempo al contador existente.
Archivo real: app/src/main/java/org/informatoli/gradaracing/core/Game.java (líneas 465–477).
void chooseRoute(int id) {
level++;
route.add(id);
unlock(id);
scoreAtStageStart = score;
loadStage(id);
float extra = stage.time();
timeLeft += extra;
msg = "¡TIEMPO EXTRA! +" + (int) extra;
msgTime = 4.5f;
go(S_COUNT);
autosave();
}
11. Entrada táctil: transformar coordenadas antes de jugar
La resolución virtual obliga a convertir las coordenadas de los dedos. GameView calcula escalas horizontales y verticales y envía al motor las pulsaciones en su sistema de coordenadas, no en los píxeles físicos del móvil.
Archivo real: app/src/main/java/org/informatoli/gradaracing/GameView.java (líneas 173–203).
public boolean onTouchEvent(MotionEvent e) {
int action = e.getActionMasked();
float sx = (float) game.W / Math.max(1, getWidth());
float sy = (float) game.H / Math.max(1, getHeight());
synchronized (game) {
switch (action) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN: {
int i = e.getActionIndex();
game.touch(e.getPointerId(i) % 10, e.getX(i) * sx, e.getY(i) * sy, Game.T_DOWN);
break;
}
case MotionEvent.ACTION_MOVE:
for (int i = 0; i < e.getPointerCount(); i++)
game.touch(e.getPointerId(i) % 10, e.getX(i) * sx, e.getY(i) * sy, Game.T_MOVE);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP: {
int i = e.getActionIndex();
game.touch(e.getPointerId(i) % 10, e.getX(i) * sx, e.getY(i) * sy, Game.T_UP);
break;
}
case MotionEvent.ACTION_CANCEL:
for (int i = 0; i < 10; i++) game.touch(i, -1, -1, Game.T_UP);
break;
default:
break;
}
}
if (action == MotionEvent.ACTION_UP) performClick();
return true;
El código contempla pulsación, movimiento, liberación, varias pulsaciones simultáneas y cancelación. La reducción del identificador con % 10 se relaciona con los diez huecos de entrada que mantiene el motor. Es una implementación concreta de multitáctil, no una abstracción de controles independiente de las limitaciones de ese arreglo.
Los controles por inclinación siguen una ruta distinta: MainActivity lee el acelerómetro, normaliza el valor y devuelve la inclinación a través de Platform.tilt(). La integración detallada de sensores y acciones Android se reservará para la tercera entrega.
12. Persistencia: el motor decide qué guardar y Android decide dónde
Al diseñar un videojuego móvil hay que asumir que el usuario puede cambiar de aplicación o cerrar la partida. Grada Racing conserva ajustes, desbloqueos, récords y distintas instantáneas de partida. El motor almacena estos datos en cadenas de texto con claves, y la plataforma concreta decide dónde persistirlos.
En Android, la implementación de Game.Platform utiliza SharedPreferences:
Archivo real: app/src/main/java/org/informatoli/gradaracing/MainActivity.java (líneas 133–142).
// ------------------------------------------------------------ Game.Platform
@Override
public String load(String key) {
return prefs.getString(key, null);
}
@Override
public void save(String key, String value) {
prefs.edit().putString(key, value).apply();
}
En el núcleo, la función de lectura incorpora valores por defecto y límites para índices de canciones y colores. No se presupone que todos los valores guardados existan o sean válidos:
Archivo real: app/src/main/java/org/informatoli/gradaracing/core/Game.java (líneas 729–743).
void loadProgress() {
HashMap<String, String> m = parse(plat.load("progress"));
unlocked = gi(m, "u", 1) | 1;
for (int i = 0; i < Stages.COUNT; i++) best[i] = gi(m, "b" + i, 0);
bestVuelta = gi(m, "bv", 0);
musicOn = gi(m, "m", 1) == 1;
sfxOn = gi(m, "s", 1) == 1;
tiltCtl = gi(m, "t", 0) == 1;
autoGas = gi(m, "a", 0) == 1;
song = Math.max(0, Math.min(2, gi(m, "song", 0)));
shirt = Math.max(0, Math.min(KIT.length - 1, gi(m, "cs", 2)));
pants = Math.max(0, Math.min(KIT.length - 1, gi(m, "cp", 2)));
stripes = gi(m, "st", 0) == 1;
carText = cleanText(m.get("tx"));
}
En cuanto al autoguardado, se genera una instantánea cuando la partida se encuentra en estados apropiados. El método onBackground() provoca una pausa y desactiva los sonidos dinámicos del motor:
Archivo real: app/src/main/java/org/informatoli/gradaracing/core/Game.java (líneas 594–607).
void pause() {
if (state == S_PLAY || state == S_COUNT) {
autosave();
go(S_PAUSE);
}
}
/** La app pasa a segundo plano: pausa y autoguarda. */
public void onBackground() {
pause();
mixer.engine = -1;
mixer.skid = 0;
}
MainActivity.onPause() delega esa transición al motor bajo sincronización y detiene vista y audio:
Archivo real: app/src/main/java/org/informatoli/gradaracing/MainActivity.java (líneas 79–87).
protected void onPause() {
if (accel != null) sensors.unregisterListener(this);
synchronized (game) {
game.onBackground();
}
view.pause();
audio.pause();
super.onPause();
}
Esta separación es una solución concreta al problema de persistir el progreso sin llenar las clases gráficas de llamadas directas al almacenamiento. La implementación usa datos locales; no equivale a un sistema de sincronización entre dispositivos ni a una copia en la nube.
13. Audio por capas: sintetizar y reproducir
La banda sonora se genera mediante Synth, y Mixer reúne música, motor, derrapes y efectos. El diseño vuelve a separar las muestras de audio producidas por el juego de su reproducción física en Android.
Por ejemplo, Mixer declara las señales principales y prepara las canciones en un método sincronizado:
Archivo real: app/src/main/java/org/informatoli/gradaracing/core/Mixer.java (líneas 9–24).
public static final int SFX_CRASH = 0, SFX_BEEP_LO = 1, SFX_BEEP_HI = 2, SFX_BONUS = 3, SFX_CLICK = 4;
private static final int[] BONUS_NOTES = {1047, 1319, 1568, 2093};
private final short[][] songs = new short[Synth.SONGS.length][];
private volatile int song = -1;
private int musicPos;
public volatile boolean musicOn = true, sfxOn = true;
public volatile float engine = -1; // -1 = motor apagado; 0..1 = velocidad
public volatile float skid = 0;
public volatile boolean gas;
private final int[] voiceType = new int[8];
private final int[] voiceT = new int[8];
private final boolean[] voiceOn = new boolean[8];
private volatile int pendingMask;
En Android, la clase AudioOut solicita un búfer de muestras al mezclador y las escribe con AudioTrack. El detalle de síntesis chiptune, la generación de ondas y el ajuste de controles merecen un apartado propio en el artículo 3, dedicado a integración Android y audio.
14. Una ventaja poco visible: probar el motor en el PC
La carpeta desktop no contiene otro juego escrito desde cero. Su lanzador compila directamente el código del paquete core y lo combina con un adaptador de escritorio. En Windows, el procedimiento de compilación usa javac:
Archivo real: desktop/ejecutar.bat (líneas 4–10).
if not exist desktop\build mkdir desktop\build
dir /s /b app\src\main\java\org\informatoli\gradaracing\core\*.java desktop\src\org\informatoli\gradaracing\desktop\*.java > desktop\build\sources.txt
javac -encoding UTF-8 -d desktop\build @desktop\build\sources.txt || exit /b 1
if "%1"=="graficos" (
java -cp desktop\build org.informatoli.gradaracing.desktop.StoreAssets .
) else (
java -cp desktop\build org.informatoli.gradaracing.desktop.DesktopMain
DesktopMain implementa el mismo contrato Game.Platform, pero con recursos de Java para PC. El siguiente extracto muestra el almacenamiento de ajustes en un archivo de propiedades del usuario:
Archivo real: desktop/src/org/informatoli/gradaracing/desktop/DesktopMain.java (líneas 34–53).
PcPlatform() {
try (FileInputStream in = new FileInputStream(file)) { props.load(in); } catch (Exception ignored) { }
}
public String load(String key) { return props.getProperty(key); }
public void save(String key, String value) {
props.setProperty(key, value);
try (FileOutputStream out = new FileOutputStream(file)) { props.store(out, "Grada Racing - informatoli.org"); } catch (Exception ignored) { }
}
public void openUrl(String url) {
try { Desktop.getDesktop().browse(new URI(url)); } catch (Exception ignored) { }
}
public float tilt() { return 0; }
public void quit() { System.exit(0); }
}
Además, utiliza una ventana JFrame y un BufferedImage para presentar los píxeles del mismo Raster. El motor no necesita saber si la imagen acaba en la pantalla de un móvil o en una ventana de ordenador.
El beneficio práctico es poder depurar pantallas, navegación, comportamiento y reglas sin instalar un APK tras cada modificación. Sigue siendo imprescindible validar después la entrada táctil, los sensores, el rendimiento, el audio y las transiciones de aplicación en el dispositivo Android real.
15. Lo que esta arquitectura resuelve —y lo que no garantiza
| Decisión | Ventaja | Límite o precaución |
|---|---|---|
Interfaz Platform | Reutilización del núcleo | Cada plataforma necesita un adaptador correcto |
| Raster de baja resolución | Pixel art uniforme | Coste de renderizado CPU y escalado |
| Paso fijo de simulación | Actualización temporal ordenada | No asegura 60 FPS estables |
| Etapas parametrizadas | Facilita crear nuevos escenarios | Hay que comprobar variedad y equilibrio |
| Guardado local | Continuidad sin servidor | Sin sincronización cloud automática |
| Versión de escritorio | Iteración más rápida del motor | No sustituye pruebas Android |
No he efectuado aquí una prueba de rendimiento, una batería automática de tests ni una auditoría completa del APK. El análisis se basa en las clases y recursos del código fuente aportado. Esta distinción entre código existente, comportamiento previsto y resultado medido es importante para documentar un desarrollo con precisión profesional.
16. Competencias técnicas aplicadas y utilidad para el portfolio
Más allá de la estética retro, Grada Racing permite enseñar trabajo en varios frentes de ingeniería: uso de Java 17 y su biblioteca estándar, programación orientada a objetos, diseño de interfaces, organización de estados, separación de responsabilidades, cálculo geométrico, dibujo sobre un búfer ARGB, hilos, sincronización, ciclo de vida Android, persistencia, entrada táctil y compilación con Gradle.
También deja un aprendizaje importante para otros proyectos: separar una parte esencial de la lógica del framework permite probarla y reutilizarla. Es un criterio aplicable a aplicaciones empresariales y herramientas técnicas, no solo a videojuegos.
17. Próximo artículo: cómo se construye la carretera pseudo-3D
Este primer recorrido ha explicado la arquitectura general. En la segunda entrega analizaremos específicamente Track.java, la generación por segmentos, la perspectiva, las curvas y las cuestas; y cómo Backdrop, Props y CarArt completan los escenarios de la carretera.
En las entregas posteriores abordaremos Android nativo, audio, controles y guardado; y finalmente el empaquetado, las pruebas y el proceso de publicación en Google Play. Cada parte partirá de la implementación real en lugar de recetas genéricas.
InformatOLI — Desarrollo de software y ciberseguridad desde 1996.
Referencias técnicas (APA 7)
- Android Developers. (s. f.). Java versions in Android builds. https://developer.android.com/build/jdks
- Android Developers. (s. f.). SurfaceView. https://developer.android.com/reference/android/view/SurfaceView
- Android Developers. (s. f.). SurfaceHolder. https://developer.android.com/reference/android/view/SurfaceHolder
- Android Developers. (s. f.). The activity lifecycle. https://developer.android.com/guide/components/activities/activity-lifecycle
- Android Developers. (s. f.). Save simple data with SharedPreferences. https://developer.android.com/training/data-storage/shared-preferences
- Gradle. (s. f.). Structuring and organizing Gradle projects. https://docs.gradle.org/current/userguide/organizing_gradle_projects.html
