Proyecto de práctica de Videojuegos para Dispositivos Móviles: un Tower Defense desarrollado en Java con arquitectura separada por módulos para poder ejecutar el mismo GameLogic tanto en Desktop/PC como en Android.
Este README está pensado como guía rápida de repaso: dónde está cada parte, qué mejoras se han añadido y qué archivos tocar si en un examen práctico se pide ampliar el juego.
TowerDefense/
├── Engine/ # Interfaces comunes del motor
├── DesktopEngine/ # Implementación del motor para PC
├── AndroidEngine/ # Implementación del motor para Android
├── GameLogic/ # Lógica común del juego
├── DesktopGame/ # Main de PC
├── AndroidGame/ # MainActivity de Android
└── data/ # Fuentes, sonidos y sprites
La idea principal es:
DesktopGame / AndroidGame
↓
DesktopEngine / AndroidEngine
↓
Engine interfaces comunes
↓
GameLogic común
GameLogic no debería depender directamente de Android ni de AWT/Swing. Para gráficos, sonido, input o almacenamiento usa las interfaces del módulo Engine.
MainActivity Desktop / MainActivity Android
↓
DesktopEngine / AndroidEngine
↓
SceneManager
↓
IntroScene → Menu → Game → EndScene
↓
Achievements
↓
OptionsMenu
Cambio de escena típico:
((SceneManager) this.engine.getScene()).changeScene(new OtraScene());Con fade:
startFadeOut(0.5f, () -> {
((SceneManager) this.engine.getScene()).changeScene(new OtraScene());
});Archivo:
GameLogic/src/main/java/com/example/gamelogic/Scenes/Achievements.java
Muestra:
- Enemigos eliminados.
- Oleadas superadas totales.
- Torres construidas.
- Récord de modo infinito.
Se accede desde:
GameLogic/src/main/java/com/example/gamelogic/Scenes/IntroScene.java
Clase principal:
GameLogic/src/main/java/com/example/gamelogic/Resources/GameStats.java
Archivos persistidos:
enemiesKilled.txt
totalWaves.txt
towersBuilt.txt
bestInfinite.txt
isMuted.txt
bestTime.txt
partida_a_continuar.txt
La lógica usa engine.getStorage(), así que sirve tanto para PC como para Android.
Interfaz común:
Engine/src/main/java/com/vdm_g13/engine/Storage.java
Implementaciones:
DesktopEngine/src/main/java/com/example/desktopengine/DesktopStorage.java
AndroidEngine/src/main/java/com/example/androidengine/AndroidStorage.java
Interfaz:
public interface Storage {
void save(String filename, String content);
String load(String filename);
boolean exists(String filename);
void delete(String filename);
}Regla importante:
GameLogic nunca debería usar File, Context, FileOutputStream, etc.
GameLogic debe usar engine.getStorage().
Escena:
GameLogic/src/main/java/com/example/gamelogic/Scenes/OptionsMenu.java
Interfaz común:
Engine/src/main/java/com/vdm_g13/engine/Audio.java
Métodos añadidos:
void setMuted(boolean muted);
boolean isMuted();Implementaciones:
DesktopEngine/src/main/java/com/example/desktopengine/DesktopAudio.java
AndroidEngine/src/main/java/com/example/androidengine/AndroidAudio.java
Idea:
GameLogic pide reproducir sonido.
Audio decide si lo reproduce o lo ignora según el mute.
Archivo principal:
GameLogic/src/main/java/com/example/gamelogic/Scenes/Menu.java
Comprueba:
this.engine.getStorage().exists("partida_a_continuar.txt");Formato guardado:
vidas;dinero;oleada;
La partida se reconstruye con:
new Game(8, waves, lives, money);Botón localizado en:
GameLogic/src/main/java/com/example/gamelogic/Scenes/Menu.java
Lanza:
new Game(5, 120);Es decir:
- 5 oleadas.
- 120 segundos.
Lógica en:
GameLogic/src/main/java/com/example/gamelogic/Scenes/Game.java
Variables:
private boolean hasTime;
private float amountOfTimeLeft;
private TextLogic timeLeftText;Archivo:
GameLogic/src/main/java/com/example/gamelogic/Maps/Grid.java
Se separa:
private int[][] grid;
private int[][] specialGrid;Constantes:
public static final int NO_SPECIAL = 0;
public static final int SPECIAL_RED = 1;
public static final int SPECIAL_GREEN = 2;Métodos importantes:
createRandomSpecialCells()
createFixedSpecialCells()
isNextToPath(...)
getSpecialCell(...)
drawSpecialCells(...)La aplicación del bonus está en:
GameLogic/src/main/java/com/example/gamelogic/Scenes/Game.java
Método:
private void applySpecialCellBonus(TowerBase tower, int specialCell)Bonuses:
Casilla verde:
rango x1.5
Casilla roja:
fuego → daño x1.25
hielo → ralentización x1.25
rayo → dos enemigos
Archivos:
GameLogic/src/main/java/com/example/gamelogic/Towers/PoisonTower.java
GameLogic/src/main/java/com/example/gamelogic/Towers/Towers.java
GameLogic/src/main/java/com/example/gamelogic/Scenes/Game.java
Para añadir otra torre:
1. Añadir enum en Towers.java.
2. Crear botón en Game.createTowerButtons().
3. Instanciarla en Game.createTower(...).
4. Crear clase hija de TowerBase.
Archivos:
GameLogic/src/main/java/com/example/gamelogic/Enemies/FastEnemy.java
GameLogic/src/main/java/com/example/gamelogic/Enemies/TankEnemy.java
GameLogic/src/main/java/com/example/gamelogic/Enemies/Wave.java
El spawn se decide en:
private void spawnEnemy()| Qué quiero cambiar | Dónde mirar primero |
|---|---|
| Cambiar de pantalla | Scenes/SceneManager.java |
| Añadir pantalla nueva | Scenes/ + heredar de SceneBase |
| Añadir botón en menú inicial | Scenes/IntroScene.java |
| Añadir modo de juego | Scenes/Menu.java + constructor de Game |
| Gameplay principal | Scenes/Game.java |
| Oleadas | Enemies/WaveManager.java |
| Spawn de enemigos | Enemies/Wave.java |
| Enemigo base | Enemies/EnemyBase.java |
| Nueva torre | Towers/Towers.java, Game.java, nueva clase |
| Bonus de torres | Towers/TowerBase.java y torres concretas |
| Mapa/casillas | Maps/Grid.java |
| Camino de enemigos | Maps/Path.java |
| Estadísticas | Resources/GameStats.java |
| Persistencia | Engine/Storage.java, DesktopStorage, AndroidStorage |
| Audio/mute | Engine/Audio.java, DesktopAudio, AndroidAudio |
public class NuevaPantalla extends SceneBase {
private Button volver;
@Override
public void init(Engine engine, int width, int height) {
super.init(engine, width, height);
// crear textos, botones, imágenes...
}
@Override
public void update(double deltaTime) {
super.update(deltaTime);
if (!fadingIn && !fadingOut && volver.isPressed()) {
startFadeOut(0.5f, () -> {
((SceneManager) engine.getScene()).changeScene(new IntroScene());
});
}
}
}Button boton = new Button(engine.getAudio(), x, y, w, h);
boton.drawCentered();
boton.setText("Texto", font);
entities.add(boton);En update(...):
if (boton.isPressed()) {
// acción
}engine.getStorage().save("archivo.txt", contenido);
String contenido = engine.getStorage().load("archivo.txt");Patrón recomendado:
Grid guarda la información de la celda
Game consulta la celda cuando el jugador interactúa
Tower/Enemy recibe el efecto concreto
Towers.java
Game.createTowerButtons()
Game.createTower(...)
NuevaTorre extends TowerBase
EnemyBase.java
NuevoEnemigo.java
Wave.java
Tocar:
Engine/
DesktopEngine/
AndroidEngine/
No resolverlo solo dentro de GameLogic.
Evitar medir texto con Graphics dentro de init() de una escena.
En Desktop, Graphics2D puede no estar activo en mitad de un cambio de escena durante update(). Es más seguro usar posiciones proporcionales o constantes:
new TextLogic("Defense", this.getWidth() / 2, this.getHeight() / 5 + 110, color);[ ] Funciona en Desktop.
[ ] Funciona en Android.
[ ] Entrar y salir de cada escena varias veces no rompe.
[ ] El botón nuevo funciona más de una vez.
[ ] La persistencia carga aunque el archivo no exista.
[ ] La persistencia no machaca valores por error.
[ ] Se han creado los archivos alumno y examen en raíz.
[ ] examen explica qué se ha tocado y dónde.
[ ] Se han borrado carpetas build/.
[ ] Se ha borrado .gradle/.
[ ] El ZIP no pesa innecesariamente.
Pantalla nueva → Scenes/
Botón nuevo → escena concreta + Button
Cambio de escena → SceneManager.changeScene(...)
Guardar/cargar → engine.getStorage()
Stats globales → GameStats
Mute/audio → Audio + DesktopAudio + AndroidAudio
Casillas/mapa → Grid + Game.applySpecialCellBonus(...)
Nueva torre → Towers enum + Game + TowerBase/hija
Nuevo enemigo → EnemyBase/hija + Wave
Modo nuevo → Menu + constructor Game + condición de victoria/derrota