Ejercicios de C# · Refuerzo 3
Instrucciones generales
Tercera y última tanda de refuerzo, con las mismas reglas que la primera y la segunda: los ejercicios son opcionales, no se entregan y cada uno termina con una posible solución oculta. En esta tanda se combinan varias clases en un mismo programa, con herencia y composición.
Crea un script para cada ejercicio (EjercicioR31, EjercicioR32…) y asígnalo a la nave. Crea cada clase nueva en su propio script vacío (Assets → Create → Scripting → Empty C# Script), con el mismo nombre que la clase.
Recuerda que la nave debe empezar cada ejercicio en la posición (0, 0, 0), sin rotación y con escala 1. Tienes la tabla con las órdenes para moverla, girarla, ocultarla o cambiarla de color en las instrucciones de la primera tanda.
Ejercicio R-31. El hangar de drones
herencia override composición List while Dificultad ★★★
Diario de a bordo
La Odiseus ha llegado a un asteroide rico en minerales. En el hangar guardas varios drones: unos exploran el terreno y otros, además, extraen mineral. Quieres lanzarlos una y otra vez hasta que se queden sin batería. Telémaco les ha puesto nombre a todos y ya tiene un favorito: Pico, el minero, «porque es el único que trabaja de verdad».
- Crea un script vacío con el nombre
Dron. La clase tendrá:- Las propiedades
Nombre(tipostring),Bateria(tipoint) yConsumo(tipoint: la batería que gasta cada vez que trabaja). Desde fuera de la clase solo se pueden leer, peroBateriatambién debe poder modificarse desde las clases que hereden deDron. - Un constructor que reciba los tres datos.
- Un método
PuedeTrabajarque devuelvatruesi al dron le queda batería suficiente para trabajar una vez más. - Un método
Trabajar, marcado comovirtual, que reste el consumo a la batería y muestre un mensaje comoArgos explora la zona (batería: 30).
- Las propiedades
- Crea un script vacío con el nombre
DronMinero. La clase hereda deDrony tendrá:- Una propiedad
Mineral(tipoint) con las toneladas de mineral que lleva recogidas. - Un constructor que reciba solo el nombre y la batería: los drones mineros siempre tienen un consumo de 35.
- Su propia versión del método
Trabajar: resta el consumo a la batería, suma 5 toneladas de mineral y muestra un mensaje comoPico extrae mineral (batería: 45, mineral: 5).
- Una propiedad
- Crea un script vacío con el nombre
Hangar. La clase tendrá:- Una lista privada de drones.
- Un método
Guardarque reciba un dron y lo añada a la lista. - Un método
LanzarTodosque recorra la lista: los drones que puedan trabajar, trabajan; de los que no, se muestra un mensaje comoMentor se queda en el hangar. El método devuelve cuántos drones han trabajado.
- Crea un script
EjercicioR31y asígnalo a la nave. En el métodoStart:- Crea un hangar y guarda en él dos drones normales, Argos (batería 50, consumo 20) y Mentor (batería 30, consumo 20), y un dron minero, Pico (batería 80).
- Con un bucle
while, lanza todos los drones ronda tras ronda, hasta que haya una ronda en la que no trabaje ninguno. - Al terminar, muestra el mineral que ha recogido Pico.
--- Ronda 1 ---
Argos explora la zona (batería: 30)
Mentor explora la zona (batería: 10)
Pico extrae mineral (batería: 45, mineral: 5)
Drones en activo: 3
--- Ronda 2 ---
Argos explora la zona (batería: 10)
Mentor se queda en el hangar
Pico extrae mineral (batería: 10, mineral: 10)
Drones en activo: 2
--- Ronda 3 ---
Argos se queda en el hangar
Mentor se queda en el hangar
Pico se queda en el hangar
Drones en activo: 0
Mineral recogido: 10 toneladas
Nota
Fíjate en que el hangar no sabe qué tipo de dron tiene en cada posición de la lista: se limita a llamar a Trabajar y cada dron ejecuta su propia versión del método.
Pista
Como DronMinero hereda de Dron, un dron minero se puede guardar en una lista de tipo Dron. Para poder consultar al final el mineral de Pico, guarda el dron minero en una variable antes de meterlo en el hangar. Repasa también el ejercicio 4-7 para ver cómo se llama al constructor de la clase padre.
Solución
using UnityEngine;
public class Dron
{
public string Nombre { get; private set; }
public int Bateria { get; protected set; }
public int Consumo { get; private set; }
public Dron(string nombre, int bateria, int consumo)
{
Nombre = nombre;
Bateria = bateria;
Consumo = consumo;
}
public bool PuedeTrabajar()
{
return Bateria >= Consumo;
}
public virtual void Trabajar()
{
Bateria -= Consumo;
Debug.Log(Nombre + " explora la zona (batería: " + Bateria + ")");
}
}
using UnityEngine;
public class DronMinero : Dron
{
const int ConsumoMinero = 35;
const int MineralPorViaje = 5;
public int Mineral { get; private set; }
public DronMinero(string nombre, int bateria) : base(nombre, bateria, ConsumoMinero)
{
Mineral = 0;
}
public override void Trabajar()
{
Bateria -= Consumo;
Mineral += MineralPorViaje;
Debug.Log(Nombre + " extrae mineral (batería: " + Bateria + ", mineral: " + Mineral + ")");
}
}
using System.Collections.Generic;
using UnityEngine;
public class Hangar
{
private List<Dron> drones = new List<Dron>();
public void Guardar(Dron dron)
{
drones.Add(dron);
}
public int LanzarTodos()
{
int activos = 0;
foreach (var dron in drones)
{
if (dron.PuedeTrabajar())
{
dron.Trabajar();
activos++;
}
else
{
Debug.Log(dron.Nombre + " se queda en el hangar");
}
}
return activos;
}
}
using UnityEngine;
public class EjercicioR31 : MonoBehaviour
{
void Start()
{
Hangar hangar = new Hangar();
DronMinero pico = new DronMinero("Pico", 80);
hangar.Guardar(new Dron("Argos", 50, 20));
hangar.Guardar(new Dron("Mentor", 30, 20));
hangar.Guardar(pico);
int ronda = 0;
int activos = 1;
while (activos > 0)
{
ronda++;
Debug.Log("--- Ronda " + ronda + " ---");
activos = hangar.LanzarTodos();
Debug.Log("Drones en activo: " + activos);
}
Debug.Log("Mineral recogido: " + pico.Mineral + " toneladas");
}
}
Ejercicio R-32. El arsenal
herencia virtual override List while Dificultad ★★☆
Diario de a bordo
Una fragata enemiga te corta el paso con su escudo de energía desplegado. Tienes un láser, que nunca se agota, y una lanzadera con solo dos misiles. Hay que disparar con todo hasta tumbar ese escudo. Thais, por una vez, ha llegado a su puesto a tiempo.
- Crea un script vacío con el nombre
Arma. La clase tendrá dos propiedades que solo pueda modificar la propia clase,Nombre(tipostring) yPotencia(tipoint), un constructor que reciba los dos datos y un métodoDisparar, marcado comovirtual, que devuelva el daño causado: la potencia del arma. - Crea un script vacío con el nombre
Misil. La clase hereda deArmay añade una propiedadUnidades(tipoint) con los misiles que quedan. Su constructor recibe el nombre, la potencia y las unidades. Su versión deDisparargasta un misil y devuelve la potencia si quedan unidades, y devuelve 0 si no queda ninguna. - Crea un script
EjercicioR32y asígnalo a la nave. En el métodoStart:- Crea una lista de armas con un láser (potencia 10) y unos misiles (potencia 40, 2 unidades).
- Crea una variable con la energía del escudo enemigo (120).
- Con un bucle
while, dispara ronda tras ronda mientras el escudo aguante. En cada ronda, recorre la lista y dispara cada arma, siempre que el escudo siga en pie. Si el arma causa daño, réstalo del escudo (sin bajar de 0) y muéstralo; si no, muestra que no le queda munición. - Al terminar, muestra en cuántas rondas se ha destruido el escudo.
Ronda 1
Láser: 10 de daño (escudo enemigo: 110)
Misiles: 40 de daño (escudo enemigo: 70)
Ronda 2
Láser: 10 de daño (escudo enemigo: 60)
Misiles: 40 de daño (escudo enemigo: 20)
Ronda 3
Láser: 10 de daño (escudo enemigo: 10)
Misiles: sin munición
Ronda 4
Láser: 10 de daño (escudo enemigo: 0)
Escudo enemigo destruido en 4 rondas
Pista
La lista es de tipo Arma, pero puede guardar también objetos Misil, porque un misil es un arma. Al llamar a Disparar, cada objeto ejecuta su propia versión del método. En la ronda 4, los misiles no llegan a disparar porque el escudo ya ha caído: compruébalo con un if antes de cada disparo.
Solución
public class Arma
{
public string Nombre { get; private set; }
public int Potencia { get; private set; }
public Arma(string nombre, int potencia)
{
Nombre = nombre;
Potencia = potencia;
}
public virtual int Disparar()
{
return Potencia;
}
}
public class Misil : Arma
{
public int Unidades { get; private set; }
public Misil(string nombre, int potencia, int unidades) : base(nombre, potencia)
{
Unidades = unidades;
}
public override int Disparar()
{
if (Unidades > 0)
{
Unidades--;
return Potencia;
}
return 0;
}
}
using System.Collections.Generic;
using UnityEngine;
public class EjercicioR32 : MonoBehaviour
{
void Start()
{
List<Arma> armas = new List<Arma>();
armas.Add(new Arma("Láser", 10));
armas.Add(new Misil("Misiles", 40, 2));
int escudo = 120;
int rondas = 0;
while (escudo > 0)
{
rondas++;
Debug.Log("Ronda " + rondas);
foreach (var arma in armas)
{
if (escudo > 0)
{
int golpe = arma.Disparar();
if (golpe > 0)
{
escudo -= golpe;
if (escudo < 0)
{
escudo = 0;
}
Debug.Log(arma.Nombre + ": " + golpe + " de daño (escudo enemigo: " + escudo + ")");
}
else
{
Debug.Log(arma.Nombre + ": sin munición");
}
}
}
}
Debug.Log("Escudo enemigo destruido en " + rondas + " rondas");
}
}
Ejercicio R-33. Motores gemelos
composición clases Update Time.deltaTime bool Dificultad ★★☆
Diario de a bordo
La Odiseus tiene dos motores, uno en cada ala, y la velocidad de la nave es la suma del empuje de los dos. Los dos son de segunda mano y llevan días haciendo ruidos raros; Nono les ha puesto cinta americana «de forma provisional» hace ya tres meses. Hoy, en plena maniobra, van a fallar.
- Crea un script vacío con el nombre
Motor. La clase tendrá:- Dos propiedades que solo pueda modificar la propia clase:
Empuje(tipofloat) yEncendido(tipobool). - Un constructor que reciba el empuje (el motor empieza apagado).
- Dos métodos,
EncenderyApagar. - Un método
EmpujeActualque devuelva el empuje si el motor está encendido, o 0 si está apagado.
- Dos propiedades que solo pueda modificar la propia clase:
- Crea un script vacío con el nombre
Propulsion. Esta clase se compone de dos motores, guardados en dos variables privadas,izquierdoyderecho. Tendrá:- Un constructor que reciba el empuje de cada motor y cree los dos motores.
- Tres métodos:
EncenderTodos,ApagarIzquierdoyApagarDerecho. - Dos métodos,
IzquierdoEncendidoyDerechoEncendido, que devuelvantruesi el motor correspondiente sigue encendido. - Un método
Velocidadque devuelva la suma del empuje actual de los dos motores.
- Crea un script
EjercicioR33y asígnalo a la nave. En el métodoStart, crea un sistema de propulsión con motores de empuje 0.75, enciéndelos y muestra la velocidad. - En el método
Update, lleva la cuenta del tiempo y mueve la nave hacia arriba según la velocidad del sistema de propulsión:transform.Translate(new Vector2(0, velocidad * Time.deltaTime));. Además:- A los 2 segundos, se avería el motor derecho: apágalo, tiñe la nave de amarillo y muestra la nueva velocidad.
- A los 4 segundos, se avería también el izquierdo: apágalo, tiñe la nave de rojo y muestra que la nave queda a la deriva.
Motores encendidos. Velocidad: 1.5
¡Avería en el motor derecho! Velocidad: 0.75
¡Avería en el motor izquierdo! Velocidad: 0. La nave queda a la deriva
La nave debe subir a buen ritmo durante 2 segundos, ponerse amarilla y seguir subiendo a la mitad de velocidad, y a los 4 segundos detenerse de color rojo, cerca de y = 4.5.
Pista
Cada avería debe ocurrir una sola vez, pero Update se ejecuta muchas veces por segundo. Antes de apagar cada motor, comprueba con DerechoEncendido o IzquierdoEncendido si sigue encendido: así, en los fotogramas siguientes la condición ya no se cumple. Desde el script no puedes acceder a los motores directamente, porque son privados: tienes que pasar por los métodos de Propulsion.
Solución
public class Motor
{
public float Empuje { get; private set; }
public bool Encendido { get; private set; }
public Motor(float empuje)
{
Empuje = empuje;
Encendido = false;
}
public void Encender()
{
Encendido = true;
}
public void Apagar()
{
Encendido = false;
}
public float EmpujeActual()
{
if (Encendido)
{
return Empuje;
}
return 0f;
}
}
public class Propulsion
{
private Motor izquierdo;
private Motor derecho;
public Propulsion(float empujePorMotor)
{
izquierdo = new Motor(empujePorMotor);
derecho = new Motor(empujePorMotor);
}
public void EncenderTodos()
{
izquierdo.Encender();
derecho.Encender();
}
public void ApagarIzquierdo()
{
izquierdo.Apagar();
}
public void ApagarDerecho()
{
derecho.Apagar();
}
public bool IzquierdoEncendido()
{
return izquierdo.Encendido;
}
public bool DerechoEncendido()
{
return derecho.Encendido;
}
public float Velocidad()
{
return izquierdo.EmpujeActual() + derecho.EmpujeActual();
}
}
using UnityEngine;
public class EjercicioR33 : MonoBehaviour
{
const float AveriaDerecho = 2f;
const float AveriaIzquierdo = 4f;
Propulsion propulsion;
float tiempo = 0f;
void Start()
{
propulsion = new Propulsion(0.75f);
propulsion.EncenderTodos();
Debug.Log("Motores encendidos. Velocidad: " + propulsion.Velocidad());
}
void Update()
{
tiempo += Time.deltaTime;
if (tiempo >= AveriaDerecho && propulsion.DerechoEncendido())
{
propulsion.ApagarDerecho();
GetComponent<SpriteRenderer>().color = Color.yellow;
Debug.Log("¡Avería en el motor derecho! Velocidad: " + propulsion.Velocidad());
}
if (tiempo >= AveriaIzquierdo && propulsion.IzquierdoEncendido())
{
propulsion.ApagarIzquierdo();
GetComponent<SpriteRenderer>().color = Color.red;
Debug.Log("¡Avería en el motor izquierdo! Velocidad: " + propulsion.Velocidad() + ". La nave queda a la deriva");
}
transform.Translate(new Vector2(0, propulsion.Velocidad() * Time.deltaTime));
}
}
Ejercicio R-34. Alarma general
clases constructores switch Update bool Dificultad ★★☆
Diario de a bordo
Shirka quiere un sistema de alarma con tres niveles de gravedad: cuanto más grave es el peligro, más rápido parpadea la nave en rojo. Así sabrás de un vistazo si hay que correr o basta con terminarse el café. Tú has pedido un cuarto nivel, «alarma, pero sin prisa», y Shirka ha hecho como que no te oía.
- Crea un script vacío con el nombre
Alarma. La clase tendrá:- Una propiedad
Intervalo(tipofloat) con los segundos entre dos cambios de la luz, y otraCambios(tipoint) con los cambios que lleva. Las dos solo las puede modificar la propia clase. - Un campo privado de tipo
floatpara el cronómetro. - Un constructor que reciba el nivel de la alarma y, con una sentencia
switch, calcule el intervalo: 1 segundo para el nivel 1, 0.5 para el nivel 2 y 0.2 para cualquier otro. - Un método
Actualizarque reciba los segundos transcurridos desde el fotograma anterior y los sume al cronómetro. Cuando el cronómetro alcance el intervalo, lo pondrá a 0, sumará un cambio y devolverátrue, indicando que toca cambiar la luz. En el resto de casos devolveráfalse.
- Una propiedad
- Crea un script
EjercicioR34y asígnalo a la nave. Tendrá una variable públicanivel(2), una constante con el número total de cambios de la luz (8) y una variable de clase para la alarma. - En el método
Start, crea la alarma con el nivel indicado, tiñe la nave de rojo y muestra el nivel y el intervalo. - En el método
Update, mientras no se hayan hecho todos los cambios, actualiza la alarma conTime.deltaTime. Cada vez que toque cambiar la luz, oculta la nave si se veía o muéstrala si estaba oculta. Tras el último cambio, devuelve a la nave su color original y muestra que la alarma se ha desactivado.
Alarma de nivel 2: un cambio cada 0.5 segundos
Alarma desactivada
La nave debe parpadear en rojo cada medio segundo durante 4 segundos y quedar visible, con sus colores originales. Cambia el nivel en el inspector, a 1 y a 3, antes de pulsar play (la alarma se crea en Start, así que cambiarlo durante la partida no tiene efecto) y comprueba la diferencia.
Pista
Esta vez el cronómetro está dentro de la clase Alarma: el script solo pregunta, en cada fotograma, si toca cambiar la luz (if (alarma.Actualizar(Time.deltaTime))). Compáralo con el ejercicio R-14, donde el script tenía que llevar la cuenta del tiempo él mismo.
Solución
public class Alarma
{
private float tiempo = 0f;
public float Intervalo { get; private set; }
public int Cambios { get; private set; }
public Alarma(int nivel)
{
switch (nivel)
{
case 1:
Intervalo = 1f;
break;
case 2:
Intervalo = 0.5f;
break;
default:
Intervalo = 0.2f;
break;
}
}
public bool Actualizar(float segundos)
{
tiempo += segundos;
if (tiempo >= Intervalo)
{
tiempo = 0f;
Cambios++;
return true;
}
return false;
}
}
using UnityEngine;
public class EjercicioR34 : MonoBehaviour
{
const int TotalCambios = 8;
public int nivel = 2;
Alarma alarma;
void Start()
{
alarma = new Alarma(nivel);
GetComponent<SpriteRenderer>().color = Color.red;
Debug.Log("Alarma de nivel " + nivel + ": un cambio cada " + alarma.Intervalo + " segundos");
}
void Update()
{
if (alarma.Cambios < TotalCambios)
{
if (alarma.Actualizar(Time.deltaTime))
{
if (GetComponent<SpriteRenderer>().enabled)
{
GetComponent<SpriteRenderer>().enabled = false;
}
else
{
GetComponent<SpriteRenderer>().enabled = true;
}
if (alarma.Cambios == TotalCambios)
{
GetComponent<SpriteRenderer>().enabled = true;
GetComponent<SpriteRenderer>().color = Color.white;
Debug.Log("Alarma desactivada");
}
}
}
}
}
Ejercicio R-35. Especialistas a bordo
herencia virtual override List for Dificultad ★★☆
Diario de a bordo
Al final de cada turno, el ordenador de a bordo redacta un informe con lo que ha hecho cada tripulante. Todos ayudan, pero algunos tienen una especialidad, y el informe debería reflejarlo. (Telémaco ha pedido que su especialidad sea «capitán», y la respuesta ha sido que no.)
- Crea un script vacío con el nombre
Especialista. La clase tendrá:- Una propiedad
Nombre(tipostring) que solo pueda modificar la propia clase, y un constructor que la reciba. - Un método
Tarea, marcado comovirtual, que devuelva el texto"ayuda en lo que puede". - Un método
Informe(sinvirtual) que devuelva el nombre seguido de la tarea, por ejemploTelémaco ayuda en lo que puede.
- Una propiedad
- Crea dos clases que hereden de
Especialista, cada una en su script:Navegante, cuya tarea es"traza la ruta".Mecanico, que además lleva la cuenta de las reparaciones que ha hecho. Cada vez que se le pide su tarea, suma una reparación y devuelve un texto como"repara el motor (reparaciones: 1)".
- Crea un script
EjercicioR35y asígnalo a la nave. En el métodoStart, crea una lista de especialistas con Telémaco (sin especialidad), Ulises (navegante: es decir, tú) y Nono (mecánico). - Con un bucle
for, simula dos turnos. En cada turno, muestra el número de turno y el informe de cada tripulante.
Turno 1
Telémaco ayuda en lo que puede
Ulises traza la ruta
Nono repara el motor (reparaciones: 1)
Turno 2
Telémaco ayuda en lo que puede
Ulises traza la ruta
Nono repara el motor (reparaciones: 2)
Pista
Las clases hijas no necesitan su propio Informe: lo heredan de Especialista. Cuando Informe llama a Tarea, se ejecuta la versión de la clase a la que pertenece el objeto, aunque Informe esté escrito en la clase padre.
Solución
public class Especialista
{
public string Nombre { get; private set; }
public Especialista(string nombre)
{
Nombre = nombre;
}
public virtual string Tarea()
{
return "ayuda en lo que puede";
}
public string Informe()
{
return Nombre + " " + Tarea();
}
}
public class Navegante : Especialista
{
public Navegante(string nombre) : base(nombre)
{
}
public override string Tarea()
{
return "traza la ruta";
}
}
public class Mecanico : Especialista
{
public int Reparaciones { get; private set; }
public Mecanico(string nombre) : base(nombre)
{
Reparaciones = 0;
}
public override string Tarea()
{
Reparaciones++;
return "repara el motor (reparaciones: " + Reparaciones + ")";
}
}
using System.Collections.Generic;
using UnityEngine;
public class EjercicioR35 : MonoBehaviour
{
const int Turnos = 2;
void Start()
{
List<Especialista> tripulacion = new List<Especialista>();
tripulacion.Add(new Especialista("Telémaco"));
tripulacion.Add(new Navegante("Ulises"));
tripulacion.Add(new Mecanico("Nono"));
for (int turno = 1; turno <= Turnos; turno++)
{
Debug.Log("Turno " + turno);
foreach (var tripulante in tripulacion)
{
Debug.Log(tripulante.Informe());
}
}
}
}
Ejercicio R-36. La bodega de carga
composición List for RemoveAt métodos Dificultad ★★☆
Diario de a bordo
La bodega de la Odiseus aguanta 100 kilos de carga; ni uno más. La última vez que alguien se pasó, la nave no consiguió despegar y hubo que tirar por la escotilla la colección de piedras lunares de Telémaco.
- Crea un script vacío con el nombre
Bulto, con dos propiedades que solo pueda modificar la propia clase,Nombre(tipostring) yPeso(tipoint, en kilos), y un constructor que reciba los dos datos. - Crea un script vacío con el nombre
BodegaCarga. La clase tendrá:- Una propiedad
Capacidad(tipoint) que solo pueda modificar la propia clase, y un constructor que la reciba. - Una lista privada de bultos.
- Un método
PesoTotalque devuelva la suma del peso de todos los bultos. - Un método
Cargarque reciba un bulto y lo añada a la lista solo si cabe. Devolverátruesi lo ha cargado. - Un método
Descargarque reciba el nombre de un bulto, lo busque en la lista y lo elimine. Devolverátruesi lo ha encontrado. - Un método
Contenidoque devuelva un texto con los nombres de todos los bultos.
- Una propiedad
- Crea un script
EjercicioR36y asígnalo a la nave. En él, crea dos métodos auxiliares,IntentarCargareIntentarDescargar, que llamen a los métodos de la bodega y muestren el resultado. - En el método
Start, crea una bodega de 100 kilos y haz estas operaciones: carga agua (40 kg), carga un motor (50 kg), carga oro (30 kg), descarga el motor, carga de nuevo el oro y descarga plata. - Al terminar, muestra el contenido de la bodega y su peso total.
Cargado: Agua (40 kg). Peso total: 40 kg
Cargado: Motor (50 kg). Peso total: 90 kg
No cabe: Oro (30 kg)
Descargado: Motor
Cargado: Oro (30 kg). Peso total: 70 kg
No está en la bodega: Plata
Contenido de la bodega: Agua Oro
Peso total: 70 de 100 kg
Reto extra
Añade a BodegaCarga un método MasPesado que devuelva el bulto más pesado de la bodega, o null si está vacía, y úsalo para saber qué tirar primero por la escotilla en caso de emergencia.
Pista
Para buscar un bulto por su nombre necesitas su posición en la lista, así que recórrela con un for en lugar de un foreach: cuando lo encuentres, elimínalo con RemoveAt(i) y devuelve true en ese mismo momento. Si el bucle termina sin encontrarlo, devuelve false.
Solución
public class Bulto
{
public string Nombre { get; private set; }
public int Peso { get; private set; }
public Bulto(string nombre, int peso)
{
Nombre = nombre;
Peso = peso;
}
}
using System.Collections.Generic;
public class BodegaCarga
{
private List<Bulto> bultos = new List<Bulto>();
public int Capacidad { get; private set; }
public BodegaCarga(int capacidad)
{
Capacidad = capacidad;
}
public int PesoTotal()
{
int total = 0;
foreach (var bulto in bultos)
{
total += bulto.Peso;
}
return total;
}
public bool Cargar(Bulto bulto)
{
if (PesoTotal() + bulto.Peso <= Capacidad)
{
bultos.Add(bulto);
return true;
}
return false;
}
public bool Descargar(string nombre)
{
for (int i = 0; i < bultos.Count; i++)
{
if (bultos[i].Nombre == nombre)
{
bultos.RemoveAt(i);
return true;
}
}
return false;
}
public string Contenido()
{
string texto = "";
foreach (var bulto in bultos)
{
texto += " " + bulto.Nombre;
}
return texto;
}
}
using UnityEngine;
public class EjercicioR36 : MonoBehaviour
{
void Start()
{
BodegaCarga bodega = new BodegaCarga(100);
IntentarCargar(bodega, new Bulto("Agua", 40));
IntentarCargar(bodega, new Bulto("Motor", 50));
IntentarCargar(bodega, new Bulto("Oro", 30));
IntentarDescargar(bodega, "Motor");
IntentarCargar(bodega, new Bulto("Oro", 30));
IntentarDescargar(bodega, "Plata");
Debug.Log("Contenido de la bodega:" + bodega.Contenido());
Debug.Log("Peso total: " + bodega.PesoTotal() + " de " + bodega.Capacidad + " kg");
}
void IntentarCargar(BodegaCarga bodega, Bulto bulto)
{
if (bodega.Cargar(bulto))
{
Debug.Log("Cargado: " + bulto.Nombre + " (" + bulto.Peso + " kg). Peso total: " + bodega.PesoTotal() + " kg");
}
else
{
Debug.Log("No cabe: " + bulto.Nombre + " (" + bulto.Peso + " kg)");
}
}
void IntentarDescargar(BodegaCarga bodega, string nombre)
{
if (bodega.Descargar(nombre))
{
Debug.Log("Descargado: " + nombre);
}
else
{
Debug.Log("No está en la bodega: " + nombre);
}
}
}
Ejercicio R-37. Oleada enemiga
herencia override List while null Dificultad ★★★
Diario de a bordo
Una oleada de naves enemigas se lanza contra la Odiseus. Tu cañón dispara siempre al primer enemigo que sigue en pie. El problema es el acorazado: su blindaje reduce a la mitad el daño de cada disparo. Shirka ha calculado cuántos disparos harán falta, pero quiere ver si llegas tú solo a la misma cifra.
- Crea un script vacío con el nombre
Enemigo. La clase tendrá:- Las propiedades
Nombre(tipostring) yVida(tipoint). Desde fuera solo se pueden leer, peroVidatambién la pueden modificar las clases hijas. - Un constructor que reciba los dos datos.
- Un método
RecibirImpacto, marcado comovirtual, que reciba la potencia del disparo y la reste a la vida, sin bajar de 0. - Un método
Destruidoque devuelvatruesi la vida ha llegado a 0.
- Las propiedades
- Crea un script vacío con el nombre
EnemigoBlindado. La clase hereda deEnemigoy su versión deRecibirImpactosolo resta a la vida la mitad de la potencia, también sin bajar de 0. - Crea un script
EjercicioR37y asígnalo a la nave. En él, crea un método que reciba una lista de enemigos y devuelva el primero que no esté destruido, onullsi lo están todos. - En el método
Start, crea una lista con la oleada: un caza (vida 30), un acorazado blindado (vida 40) y un explorador (vida 20). Crea también una constante con la potencia del cañón (20). - Con un bucle
while, dispara al primer enemigo en pie mientras quede alguno. Muestra cada disparo y, si el enemigo cae, muéstralo también. - Al terminar, muestra cuántos disparos han hecho falta.
Disparo 1: Caza (vida: 10)
Disparo 2: Caza (vida: 0)
¡Caza destruido!
Disparo 3: Acorazado (vida: 30)
Disparo 4: Acorazado (vida: 20)
Disparo 5: Acorazado (vida: 10)
Disparo 6: Acorazado (vida: 0)
¡Acorazado destruido!
Disparo 7: Explorador (vida: 0)
¡Explorador destruido!
Oleada eliminada con 7 disparos
Pista
Pide el primer enemigo en pie antes del bucle y vuelve a pedirlo al final de cada vuelta: el bucle continúa mientras el resultado no sea null. En la clase EnemigoBlindado, recuerda que Vida debe ser protected set en la clase padre para poder modificarla.
Solución
public class Enemigo
{
public string Nombre { get; private set; }
public int Vida { get; protected set; }
public Enemigo(string nombre, int vida)
{
Nombre = nombre;
Vida = vida;
}
public virtual void RecibirImpacto(int potencia)
{
Vida -= potencia;
if (Vida < 0)
{
Vida = 0;
}
}
public bool Destruido()
{
return Vida == 0;
}
}
public class EnemigoBlindado : Enemigo
{
public EnemigoBlindado(string nombre, int vida) : base(nombre, vida)
{
}
public override void RecibirImpacto(int potencia)
{
Vida -= potencia / 2;
if (Vida < 0)
{
Vida = 0;
}
}
}
using System.Collections.Generic;
using UnityEngine;
public class EjercicioR37 : MonoBehaviour
{
const int Potencia = 20;
void Start()
{
List<Enemigo> oleada = new List<Enemigo>();
oleada.Add(new Enemigo("Caza", 30));
oleada.Add(new EnemigoBlindado("Acorazado", 40));
oleada.Add(new Enemigo("Explorador", 20));
int disparos = 0;
Enemigo objetivo = PrimerEnemigoEnPie(oleada);
while (objetivo != null)
{
disparos++;
objetivo.RecibirImpacto(Potencia);
Debug.Log("Disparo " + disparos + ": " + objetivo.Nombre + " (vida: " + objetivo.Vida + ")");
if (objetivo.Destruido())
{
Debug.Log("¡" + objetivo.Nombre + " destruido!");
}
objetivo = PrimerEnemigoEnPie(oleada);
}
Debug.Log("Oleada eliminada con " + disparos + " disparos");
}
Enemigo PrimerEnemigoEnPie(List<Enemigo> enemigos)
{
foreach (var enemigo in enemigos)
{
if (enemigo.Destruido() == false)
{
return enemigo;
}
}
return null;
}
}
Ejercicio R-38. Ruta por puntos de paso
arrays Update Time.deltaTime if-else constantes Dificultad ★★★
Diario de a bordo
La Odiseus sale del astillero recién pintada y el capataz te ha dejado muy claro que, si le haces un solo arañazo, la vuelves a pintar tú. Para salir hay que seguir un recorrido marcado por cuatro puntos de paso. Los motores de maniobra solo empujan en horizontal o en vertical, así que la nave llegará a cada punto moviéndose primero en horizontal y después en vertical.
- Crea dos arrays de tipo
floatcomo variables de clase, con las coordenadas de los puntos de paso: uno con lasx(3, 3, -2 y -2) y otro con lasy(0, 2, 2 y -1). El primer punto es (3, 0), el segundo (3, 2), y así sucesivamente. - Crea una variable pública
velocidad(2), una constanteMargen(0.1) y una variable de clase con la posición del siguiente punto de paso. - En el método
Update, mientras queden puntos por visitar, compara la posición de la nave con la del siguiente punto:- Si la
xde la nave es menor que la del punto (descontando el margen), muévela hacia la derecha; si es mayor (sumando el margen), hacia la izquierda. - Si la
xya coincide, haz lo mismo con lay: hacia arriba o hacia abajo. - Si las dos coinciden, el punto se ha alcanzado: muéstralo y pasa al siguiente. Tras el último, muestra que la ruta se ha completado y tiñe la nave de verde.
- Si la
En cada fotograma, la nave se mueve velocidad * Time.deltaTime unidades, por ejemplo con transform.Translate(new Vector2(velocidad * Time.deltaTime, 0)); para ir hacia la derecha.
Punto 1 alcanzado: (3, 0)
Punto 2 alcanzado: (3, 2)
Punto 3 alcanzado: (-2, 2)
Punto 4 alcanzado: (-2, -1)
Ruta completada
La nave debe recorrer los cuatro tramos uno detrás de otro y detenerse de color verde cerca del punto (-2, -1).
Para pensar
¿Por qué hace falta un margen? La nave avanza a saltos, un poco en cada fotograma, así que es casi imposible que su posición coincida exactamente con la del punto. Prueba también a poner velocidad a 50 desde el inspector: cada salto es más grande que el margen, así que la nave se pasa del punto y da bandazos adelante y atrás.
Pista
Encadena las cuatro comprobaciones con else if y deja el punto alcanzado para el else final: así, en cada fotograma la nave hace una sola cosa. Una coordenada «coincide» cuando está entre punto - Margen y punto + Margen.
Solución
using UnityEngine;
public class EjercicioR38 : MonoBehaviour
{
const float Margen = 0.1f;
public float velocidad = 2f;
float[] puntosX = new float[] {3, 3, -2, -2};
float[] puntosY = new float[] {0, 2, 2, -1};
int siguiente = 0;
void Update()
{
if (siguiente < puntosX.Length)
{
float x = transform.position.x;
float y = transform.position.y;
float paso = velocidad * Time.deltaTime;
if (x < puntosX[siguiente] - Margen)
{
transform.Translate(new Vector2(paso, 0));
}
else if (x > puntosX[siguiente] + Margen)
{
transform.Translate(new Vector2(-paso, 0));
}
else if (y < puntosY[siguiente] - Margen)
{
transform.Translate(new Vector2(0, paso));
}
else if (y > puntosY[siguiente] + Margen)
{
transform.Translate(new Vector2(0, -paso));
}
else
{
Debug.Log("Punto " + (siguiente + 1) + " alcanzado: (" + puntosX[siguiente] + ", " + puntosY[siguiente] + ")");
siguiente++;
if (siguiente == puntosX.Length)
{
Debug.Log("Ruta completada");
GetComponent<SpriteRenderer>().color = Color.green;
}
}
}
}
}
Ejercicio R-39. Camuflaje óptico
clases propiedades Update bool Color Dificultad ★★★
Diario de a bordo
En el último mercado compraste un camuflaje óptico de dudosa procedencia. Cuando está activo, la nave se vuelve casi transparente, pero gasta energía muy deprisa y, cuando se agota, tarda el doble en recargarse. El vendedor aseguró que «casi nunca falla». Toca probarlo.
- Crea un script vacío con el nombre
Camuflaje. La clase tendrá:- Tres constantes: la energía máxima (100), lo que se gasta por segundo con el camuflaje activo (40) y lo que se recarga por segundo cuando está inactivo (20).
- Dos propiedades que solo pueda modificar la propia clase:
Energia(tipofloat, empieza en el máximo) yActivo(tipobool). - Un método
Cargado, que devuelvatruesi la energía está al máximo. - Un método
Activar, que activa el camuflaje solo si la energía está al máximo. - Un método
Actualizar, que recibe los segundos transcurridos. Si el camuflaje está activo, gasta energía y, al llegar a 0, se desactiva solo. Si está inactivo, recarga energía sin pasar del máximo.
- Crea un script
EjercicioR39y asígnalo a la nave. Tendrá una constante con el número de pruebas que se harán (2) y variables de clase para el camuflaje, el número de activaciones y el estado del camuflaje en el fotograma anterior. - En el método
Start, crea el camuflaje y actívalo. - En el método
Update, actualiza el camuflaje conTime.deltaTime. Si está inactivo, con la energía al máximo y quedan pruebas por hacer, actívalo de nuevo. Cada vez que cambie el estado del camuflaje respecto al fotograma anterior, muestra un mensaje y cambia el aspecto de la nave: semitransparente si está activo; con su aspecto normal si no lo está.
Hacer la nave transparente
Además de los colores con nombre, se puede crear un color con new Color(rojo, verde, azul, alfa), con valores entre 0 y 1. El cuarto valor, el alfa, es la opacidad: 1 es totalmente opaco y 0, totalmente transparente.
GetComponent<SpriteRenderer>().color = new Color(1, 1, 1, 0.3f);
Camuflaje activado
Camuflaje agotado: recargando
Camuflaje activado
Camuflaje agotado: recargando
Pruebas terminadas
La nave debe estar semitransparente unos 2.5 segundos, recuperar su aspecto durante unos 5 segundos mientras se recarga, volver a camuflarse otros 2.5 segundos y quedarse visible.
Pista
Para detectar el cambio, guarda en una variable de clase el valor de Activo en el fotograma anterior y compáralo con el actual con !=. Al final de cada Update, actualiza esa variable. Como en Start ya se activa el camuflaje, el primer mensaje y el primer cambio de aspecto puedes hacerlos ahí.
Solución
public class Camuflaje
{
const float EnergiaMaxima = 100f;
const float GastoPorSegundo = 40f;
const float RecargaPorSegundo = 20f;
public float Energia { get; private set; } = EnergiaMaxima;
public bool Activo { get; private set; } = false;
public void Activar()
{
if (Energia == EnergiaMaxima)
{
Activo = true;
}
}
public bool Cargado()
{
return Energia == EnergiaMaxima;
}
public void Actualizar(float segundos)
{
if (Activo)
{
Energia -= GastoPorSegundo * segundos;
if (Energia <= 0)
{
Energia = 0;
Activo = false;
}
}
else
{
Energia += RecargaPorSegundo * segundos;
if (Energia > EnergiaMaxima)
{
Energia = EnergiaMaxima;
}
}
}
}
using UnityEngine;
public class EjercicioR39 : MonoBehaviour
{
const int Pruebas = 2;
Camuflaje camuflaje;
int activaciones = 0;
bool estabaActivo = false;
void Start()
{
camuflaje = new Camuflaje();
camuflaje.Activar();
activaciones++;
Debug.Log("Camuflaje activado");
GetComponent<SpriteRenderer>().color = new Color(1, 1, 1, 0.3f);
estabaActivo = true;
}
void Update()
{
camuflaje.Actualizar(Time.deltaTime);
if (camuflaje.Activo == false && camuflaje.Cargado() && activaciones < Pruebas)
{
camuflaje.Activar();
activaciones++;
}
if (camuflaje.Activo != estabaActivo)
{
if (camuflaje.Activo)
{
Debug.Log("Camuflaje activado");
GetComponent<SpriteRenderer>().color = new Color(1, 1, 1, 0.3f);
}
else
{
Debug.Log("Camuflaje agotado: recargando");
GetComponent<SpriteRenderer>().color = Color.white;
if (activaciones == Pruebas)
{
Debug.Log("Pruebas terminadas");
}
}
}
estabaActivo = camuflaje.Activo;
}
}
Ejercicio R-40. El taller de mejoras
clases List while métodos return Dificultad ★★★
Diario de a bordo
Has cobrado 300 créditos por un transporte y en el taller de la estación ofrecen mejoras para la nave. Cada mejora se puede comprar varias veces, pero cada nivel cuesta más que el anterior. Tu estrategia: comprar siempre la más barata, hasta que no te llegue para ninguna. Tu primera idea, gastarlo todo en un portavasos de lujo para el puesto de pilotaje, perdió la votación por cuatro a uno.
- Crea un script vacío con el nombre
Mejora. La clase tendrá:- Las propiedades
Nombre(tipostring),CosteBaseyNivel(tipoint; el nivel empieza en 0). Las tres solo las puede modificar la propia clase. - Un constructor que reciba el nombre y el coste base.
- Un método
Costeque devuelva lo que cuesta el siguiente nivel: el coste base multiplicado por el nivel siguiente (con nivel 0 cuesta el coste base; con nivel 1, el doble…). - Un método
Subir, que suma un nivel.
- Las propiedades
- Crea un script
EjercicioR40y asígnalo a la nave. En él, crea un método que reciba una lista de mejoras y devuelva la que tenga el coste más bajo (si hay empate, la primera). - En el método
Start, crea una variable con los créditos (300) y una lista con estas mejoras: motor (coste base 40), escudo (60) y láser (50). - Con un bucle
while, compra la mejora más barata mientras haya créditos para pagarla. Muestra cada compra. - Al terminar, muestra el nivel de cada mejora y los créditos que sobran, y haz que la nave crezca un 10 % por cada nivel comprado.
Motor sube a nivel 1 por 40 créditos (quedan 260)
Láser sube a nivel 1 por 50 créditos (quedan 210)
Escudo sube a nivel 1 por 60 créditos (quedan 150)
Motor sube a nivel 2 por 80 créditos (quedan 70)
Motor: nivel 2
Escudo: nivel 1
Láser: nivel 1
Créditos sobrantes: 70
Nota
Con 4 niveles comprados, la nave debe acabar con escala 1.4.
Pista
Antes de subir el nivel, guarda el coste en una variable: después de llamar a Subir, Coste() ya devuelve el precio del nivel siguiente. Para la escala, cuenta los niveles comprados en una variable y calcula 1 + niveles * 0.1f.
Solución
public class Mejora
{
public string Nombre { get; private set; }
public int CosteBase { get; private set; }
public int Nivel { get; private set; }
public Mejora(string nombre, int costeBase)
{
Nombre = nombre;
CosteBase = costeBase;
Nivel = 0;
}
public int Coste()
{
return CosteBase * (Nivel + 1);
}
public void Subir()
{
Nivel++;
}
}
using System.Collections.Generic;
using UnityEngine;
public class EjercicioR40 : MonoBehaviour
{
void Start()
{
int creditos = 300;
List<Mejora> mejoras = new List<Mejora>();
mejoras.Add(new Mejora("Motor", 40));
mejoras.Add(new Mejora("Escudo", 60));
mejoras.Add(new Mejora("Láser", 50));
int nivelesComprados = 0;
Mejora candidata = MasBarata(mejoras);
while (candidata.Coste() <= creditos)
{
int coste = candidata.Coste();
creditos -= coste;
candidata.Subir();
nivelesComprados++;
Debug.Log(candidata.Nombre + " sube a nivel " + candidata.Nivel + " por " + coste + " créditos (quedan " + creditos + ")");
candidata = MasBarata(mejoras);
}
foreach (var mejora in mejoras)
{
Debug.Log(mejora.Nombre + ": nivel " + mejora.Nivel);
}
Debug.Log("Créditos sobrantes: " + creditos);
float escala = 1 + nivelesComprados * 0.1f;
transform.localScale = new Vector3(escala, escala, 1);
}
Mejora MasBarata(List<Mejora> mejoras)
{
Mejora masBarata = mejoras[0];
foreach (var mejora in mejoras)
{
if (mejora.Coste() < masBarata.Coste())
{
masBarata = mejora;
}
}
return masBarata;
}
}
Ejercicio R-41. Vuelo en cuadrado
Update switch Time.deltaTime Rotate constantes Dificultad ★★★
Diario de a bordo
Para calibrar los sensores nuevos, el manual pide volar describiendo un cuadrado: avanzar un rato, girar 90° a la derecha, y así cuatro veces, hasta volver al punto de partida. Telémaco pregunta si no hay ninguna figura más emocionante. El manual dice que no.
Como en el ejercicio R-30, el programa pasa por varios estados. Guárdalos en una variable de clase de tipo int y utiliza una sentencia switch en Update:
| Estado | Qué hace la nave |
|---|---|
| 0 | Avanza en la dirección en la que mira. Al cabo de 2 segundos ha completado un lado: si era el cuarto, pasa al estado 2; si no, al 1 |
| 1 | Gira 90° en el sentido de las agujas del reloj y vuelve al estado 0 |
| 2 | Se queda quieta |
- Crea dos constantes, la duración de cada lado (2) y el número de lados (4), una variable pública
velocidad(1.5) y tres variables de clase: el estado, un cronómetro y el número de lados completados. - En el estado 0, haz avanzar la nave con
transform.Translate(new Vector2(0, velocidad * Time.deltaTime));y lleva la cuenta del tiempo. Al completar cada lado, pon el cronómetro a 0 y muestra un mensaje. - En el estado 1, gira la nave con
transform.Rotate(new Vector3(0, 0, -90));. - Al completar el cuarto lado, tiñe la nave de verde y muestra que el cuadrado se ha completado.
Lado 1 completado
Lado 2 completado
Lado 3 completado
Lado 4 completado
Cuadrado completado
La nave debe subir, girar hacia la derecha, avanzar, girar hacia abajo… y acabar, de color verde, aproximadamente en el punto de partida.
Pista
Fíjate en que la nave avanza siempre con la misma orden, Translate(new Vector2(0, …)): como Translate se mueve respecto a la orientación de la nave, basta con girarla para que avance en otra dirección. La nave no vuelve exactamente al origen porque el último fotograma de cada lado puede pasarse un poco de los 2 segundos.
Solución
using UnityEngine;
public class EjercicioR41 : MonoBehaviour
{
const float DuracionLado = 2f;
const int TotalLados = 4;
public float velocidad = 1.5f;
int estado = 0;
float tiempo = 0f;
int lados = 0;
void Update()
{
switch (estado)
{
case 0:
transform.Translate(new Vector2(0, velocidad * Time.deltaTime));
tiempo += Time.deltaTime;
if (tiempo >= DuracionLado)
{
tiempo = 0f;
lados++;
Debug.Log("Lado " + lados + " completado");
if (lados == TotalLados)
{
estado = 2;
GetComponent<SpriteRenderer>().color = Color.green;
Debug.Log("Cuadrado completado");
}
else
{
estado = 1;
}
}
break;
case 1:
transform.Rotate(new Vector3(0, 0, -90));
estado = 0;
break;
}
}
}
Ejercicio R-42. Hundir la flota
arrays bidimensionales for anidados switch métodos while Dificultad ★★★
Diario de a bordo
Una flota enemiga se esconde en una nube de polvo cósmico dividida en una cuadrícula de 4 × 4 sectores. Gracias a un chivatazo, conoces la posición de sus naves y tienes preparada una lista de disparos. Telémaco dice que esto ya lo ha jugado con papel y lápiz, y que siempre gana.
-
Crea dos constantes con el número de filas y de columnas (4), y un array bidimensional de enteros como variable de clase, con el mapa de la flota. En él, un 0 es un sector vacío y un 1, un sector con una nave enemiga. Las naves enemigas son pequeñas: cada una ocupa un solo sector, así que un disparo certero la hunde.
int[,] tablero = { { 0, 1, 0, 0 }, { 0, 1, 0, 0 }, { 0, 0, 0, 1 }, { 1, 0, 0, 0 } }; -
Crea también una variable de clase con las naves enemigas que quedan. En
Start, cuéntalas recorriendo el tablero con dos buclesforanidados y muestra el resultado. - Crea un método
Dispararque reciba una fila y una columna, y devuelva el resultado del disparo. Con una sentenciaswitchsobre el valor de esa casilla:- Si es 0: cámbialo por 2 (agua ya disparada) y devuelve
"Agua". - Si es 1: cámbialo por 3 (nave hundida), resta una a las naves que quedan y devuelve
"¡Tocado y hundido!". - En cualquier otro caso, devuelve
"Casilla repetida".
- Si es 0: cámbialo por 2 (agua ya disparada) y devuelve
- Crea dos arrays de enteros con las coordenadas de los disparos: las filas (0, 1, 1, 3, 2, 0, 2 y 3) y las columnas (0, 1, 1, 0, 2, 1, 3 y 3).
- Con un bucle
while, dispara mientras queden disparos y queden naves enemigas. Muestra el resultado de cada disparo y, al terminar, si se ha hundido toda la flota.
Naves enemigas: 4
Disparo a (0, 0): Agua
Disparo a (1, 1): ¡Tocado y hundido!
Disparo a (1, 1): Casilla repetida
Disparo a (3, 0): ¡Tocado y hundido!
Disparo a (2, 2): Agua
Disparo a (0, 1): ¡Tocado y hundido!
Disparo a (2, 3): ¡Tocado y hundido!
¡Flota enemiga hundida con 7 disparos!
Fíjate en que el último disparo de la lista no llega a hacerse, porque ya no quedan naves.
Pista
El tablero y el contador de naves deben ser variables de clase, porque los usan tanto Start como Disparar. El método modifica el tablero: por eso, al disparar dos veces a la misma casilla, la segunda vez ya no vale 1 sino 3.
Solución
using UnityEngine;
public class EjercicioR42 : MonoBehaviour
{
const int Filas = 4;
const int Columnas = 4;
int[,] tablero = {
{ 0, 1, 0, 0 },
{ 0, 1, 0, 0 },
{ 0, 0, 0, 1 },
{ 1, 0, 0, 0 }
};
int navesRestantes = 0;
void Start()
{
for (int fila = 0; fila < Filas; fila++)
{
for (int columna = 0; columna < Columnas; columna++)
{
if (tablero[fila, columna] == 1)
{
navesRestantes++;
}
}
}
Debug.Log("Naves enemigas: " + navesRestantes);
int[] filasDisparo = new int[] {0, 1, 1, 3, 2, 0, 2, 3};
int[] columnasDisparo = new int[] {0, 1, 1, 0, 2, 1, 3, 3};
int disparos = 0;
while (disparos < filasDisparo.Length && navesRestantes > 0)
{
int fila = filasDisparo[disparos];
int columna = columnasDisparo[disparos];
Debug.Log("Disparo a (" + fila + ", " + columna + "): " + Disparar(fila, columna));
disparos++;
}
if (navesRestantes == 0)
{
Debug.Log("¡Flota enemiga hundida con " + disparos + " disparos!");
}
else
{
Debug.Log("Sin disparos. Quedan " + navesRestantes + " naves enemigas");
}
}
string Disparar(int fila, int columna)
{
switch (tablero[fila, columna])
{
case 0:
tablero[fila, columna] = 2;
return "Agua";
case 1:
tablero[fila, columna] = 3;
navesRestantes--;
return "¡Tocado y hundido!";
default:
return "Casilla repetida";
}
}
}
Ejercicio R-43. El convoy
herencia composición override List Update Dificultad ★★★
Diario de a bordo
Para cruzar la ruta de los corsarios, la Odiseus se une a un convoy de cargueros protegidos por naves de escolta. El convoy debe avanzar unido, así que nunca puede ir más rápido que su nave más lenta. Tú, que solo sabes pilotar a toda velocidad, llevas todo el viaje resoplando.
- Crea un script vacío con el nombre
NaveConvoy. La clase tendrá:- Dos propiedades que solo pueda modificar la propia clase,
Nombre(tipostring) yVelocidad(tipoint), y un constructor que reciba los dos datos. - Un método
Carga, marcado comovirtual, que devuelva 0. - Un método
Descripcion, marcado comovirtual, que devuelva un texto con el nombre y la velocidad.
- Dos propiedades que solo pueda modificar la propia clase,
- Crea dos clases que hereden de
NaveConvoy, cada una en su script:Carguero, con una propiedadToneladas. SuCargadevuelve las toneladas y suDescripcion, un texto comoCarguero Ceres: 500 toneladas, velocidad 3.Escolta, con una propiedadCanones. SuDescripciondevuelve un texto comoEscolta Ares: 4 cañones, velocidad 9.
- Crea un script vacío con el nombre
Convoy. La clase se compone de una lista privada de naves y tendrá:- Un método
Incorporar, que añade una nave a la lista. - Un método
MostrarInforme, que muestra la descripción de cada nave. - Un método
CargaTotal, que devuelve la suma de la carga de todas las naves. - Un método
Velocidad, que devuelve la velocidad de la nave más lenta.
- Un método
-
Crea un script
EjercicioR43y asígnalo a la nave. En el métodoStart, crea el convoy con estas naves, muestra el informe, la carga total y la velocidad del convoy:Tipo Nombre Velocidad Toneladas o cañones Carguero Ceres 3 500 toneladas Escolta Ares 9 4 cañones Carguero Vesta 2 800 toneladas Escolta Atenea 8 2 cañones -
En el método
Update, mueve la nave hacia la derecha a la velocidad del convoy:transform.Translate(new Vector2(convoy.Velocidad() * Time.deltaTime, 0));.
Carguero Ceres: 500 toneladas, velocidad 3
Escolta Ares: 4 cañones, velocidad 9
Carguero Vesta: 800 toneladas, velocidad 2
Escolta Atenea: 2 cañones, velocidad 8
Carga total: 1300 toneladas
Velocidad del convoy: 2
Después, la nave debe avanzar hacia la derecha, a 2 unidades por segundo, hasta salir de la pantalla.
Pista
Escolta no necesita reescribir Carga: hereda la versión de NaveConvoy, que devuelve 0. Para la velocidad del convoy, empieza con la velocidad de la primera nave y quédate con la menor. La variable del convoy debe ser de clase, porque la usan Start y Update.
Solución
public class NaveConvoy
{
public string Nombre { get; private set; }
public int Velocidad { get; private set; }
public NaveConvoy(string nombre, int velocidad)
{
Nombre = nombre;
Velocidad = velocidad;
}
public virtual int Carga()
{
return 0;
}
public virtual string Descripcion()
{
return Nombre + ", velocidad " + Velocidad;
}
}
public class Carguero : NaveConvoy
{
public int Toneladas { get; private set; }
public Carguero(string nombre, int velocidad, int toneladas) : base(nombre, velocidad)
{
Toneladas = toneladas;
}
public override int Carga()
{
return Toneladas;
}
public override string Descripcion()
{
return "Carguero " + Nombre + ": " + Toneladas + " toneladas, velocidad " + Velocidad;
}
}
public class Escolta : NaveConvoy
{
public int Canones { get; private set; }
public Escolta(string nombre, int velocidad, int canones) : base(nombre, velocidad)
{
Canones = canones;
}
public override string Descripcion()
{
return "Escolta " + Nombre + ": " + Canones + " cañones, velocidad " + Velocidad;
}
}
using System.Collections.Generic;
using UnityEngine;
public class Convoy
{
private List<NaveConvoy> naves = new List<NaveConvoy>();
public void Incorporar(NaveConvoy nave)
{
naves.Add(nave);
}
public void MostrarInforme()
{
foreach (var nave in naves)
{
Debug.Log(nave.Descripcion());
}
}
public int CargaTotal()
{
int total = 0;
foreach (var nave in naves)
{
total += nave.Carga();
}
return total;
}
public int Velocidad()
{
int minima = naves[0].Velocidad;
foreach (var nave in naves)
{
if (nave.Velocidad < minima)
{
minima = nave.Velocidad;
}
}
return minima;
}
}
using UnityEngine;
public class EjercicioR43 : MonoBehaviour
{
Convoy convoy;
void Start()
{
convoy = new Convoy();
convoy.Incorporar(new Carguero("Ceres", 3, 500));
convoy.Incorporar(new Escolta("Ares", 9, 4));
convoy.Incorporar(new Carguero("Vesta", 2, 800));
convoy.Incorporar(new Escolta("Atenea", 8, 2));
convoy.MostrarInforme();
Debug.Log("Carga total: " + convoy.CargaTotal() + " toneladas");
Debug.Log("Velocidad del convoy: " + convoy.Velocidad());
}
void Update()
{
transform.Translate(new Vector2(convoy.Velocidad() * Time.deltaTime, 0));
}
}
Ejercicio R-44. El latido del reactor
clases propiedades bool Update localScale Dificultad ★★☆
Diario de a bordo
Tras la reparación, el reactor de la Odiseus «late»: se expande y se contrae mientras se estabiliza. Shirka ha decidido que toda la nave lata con él, para que nadie olvide que no hay que tocar nada hasta que termine. Nono asegura que es «perfectamente normal». Nadie le cree.
- Crea un script vacío con el nombre
Pulso. La clase tendrá:- Tres propiedades que solo pueda modificar la propia clase,
Minimo,MaximoyVelocidad(tipofloat), y un constructor que las reciba. - Una propiedad
Valor(tipofloat), que empieza valiendo el mínimo, y otraLatidos(tipoint), con los latidos completados. Las dos solo las puede modificar la propia clase. - Un campo privado de tipo
boolque indique si el valor está creciendo. - Un método
Actualizar, que recibe los segundos transcurridos. Mientras el valor crece, le suma la velocidad por los segundos; al llegar al máximo, empieza a decrecer. Mientras decrece, se los resta; al llegar al mínimo, vuelve a crecer y cuenta un latido completado.
- Tres propiedades que solo pueda modificar la propia clase,
- Crea un script
EjercicioR44y asígnalo a la nave. Tendrá una constante con el número de latidos (5) y dos variables de clase: el pulso y el último latido mostrado. - En el método
Start, crea un pulso con mínimo 1, máximo 1.5 y velocidad 2. - En el método
Update, mientras no se hayan completado todos los latidos, actualiza el pulso conTime.deltaTimey aplica su valor a la escala de la nave. Cada vez que se complete un latido, muéstralo. Tras el último, tiñe la nave de verde y muestra que el reactor está estable.
Latido 1
Latido 2
Latido 3
Latido 4
Latido 5
Reactor estable
La nave debe crecer y encogerse cinco veces, cada vez durante medio segundo, y quedarse con su tamaño original y de color verde.
Cambiar el tamaño de la nave
transform.localScale = new Vector3(escala, escala, 1); cambia el tamaño de la nave: 1 es el tamaño original y 1.5, una vez y media.
Pista
Cuando el valor se pase del máximo (o baje del mínimo), ajústalo al límite antes de cambiar de sentido: así nunca se sale del rango. Como al completar un latido el valor queda exactamente en el mínimo, la nave termina con escala 1.
Solución
public class Pulso
{
private bool creciendo = true;
public float Minimo { get; private set; }
public float Maximo { get; private set; }
public float Velocidad { get; private set; }
public float Valor { get; private set; }
public int Latidos { get; private set; }
public Pulso(float minimo, float maximo, float velocidad)
{
Minimo = minimo;
Maximo = maximo;
Velocidad = velocidad;
Valor = minimo;
Latidos = 0;
}
public void Actualizar(float segundos)
{
if (creciendo)
{
Valor += Velocidad * segundos;
if (Valor >= Maximo)
{
Valor = Maximo;
creciendo = false;
}
}
else
{
Valor -= Velocidad * segundos;
if (Valor <= Minimo)
{
Valor = Minimo;
creciendo = true;
Latidos++;
}
}
}
}
using UnityEngine;
public class EjercicioR44 : MonoBehaviour
{
const int TotalLatidos = 5;
Pulso pulso;
int ultimoLatido = 0;
void Start()
{
pulso = new Pulso(1f, 1.5f, 2f);
}
void Update()
{
if (pulso.Latidos < TotalLatidos)
{
pulso.Actualizar(Time.deltaTime);
transform.localScale = new Vector3(pulso.Valor, pulso.Valor, 1);
if (pulso.Latidos != ultimoLatido)
{
ultimoLatido = pulso.Latidos;
Debug.Log("Latido " + ultimoLatido);
if (ultimoLatido == TotalLatidos)
{
GetComponent<SpriteRenderer>().color = Color.green;
Debug.Log("Reactor estable");
}
}
}
}
}
Ejercicio R-45. El regreso a Ítaca
clases List while switch métodos bool Dificultad ★★★
Diario de a bordo
Después de tantas aventuras, por fin pones rumbo a casa, al planeta Ítaca. Quedan cinco etapas de viaje y cada una tiene sus peligros. Antes de salir, Shirka simulará el viaje completo para saber si la Odiseus llegará de una pieza. Toda la tripulación se ha reunido en el puente para ver el resultado; hasta Thais ha llegado puntual.
- Crea un script vacío con el nombre
Etapa. La clase tendrá cuatro propiedades que solo pueda modificar la propia clase,Nombre(tipostring),Distancia(tipoint),Peligro(tipostring) yRepostaje(tipoint: el combustible que se puede cargar al final de la etapa), y un constructor que reciba los cuatro datos. - Crea un script vacío con el nombre
NaveOdiseus. La clase tendrá:- Dos constantes: el combustible máximo (100) y el consumo por unidad de distancia (2).
- Dos propiedades que solo pueda modificar la propia clase,
CombustibleyCasco(tipoint), que empiezan en 100. - Un método
Viajarque reciba una distancia. Si hay combustible para recorrerla, lo gasta y devuelvetrue; si no, devuelvefalsesin gastar nada. - Un método
SufrirAveria, que reste al casco los puntos indicados, sin bajar de 0. - Un método
Repostar, que sume al combustible los litros indicados, sin pasar del máximo. - Un método
Destruida, que devuelvatruesi el casco ha llegado a 0.
- Crea un script
EjercicioR45y asígnalo a la nave. En él, crea un método que reciba un peligro y devuelva los puntos de casco que hace perder, con una sentenciaswitch: 20 los asteroides, 35 los piratas, 10 las tormentas y 0 cualquier otro. -
En el método
Start, coloca la nave en la posición (-4, 0) y crea una lista con las etapas:Nombre Distancia Peligro Repostaje Cinturón de asteroides 20 asteroides 0 Estación Eolia 10 ninguno 50 Nebulosa de las Sirenas 25 tormenta 0 Estrecho de Escila 10 piratas 0 Ítaca 10 ninguno 0 -
Con un bucle
while, recorre las etapas mientras queden etapas y el viaje pueda continuar. En cada etapa:- Si no hay combustible para recorrerla, muestra un aviso y detén el viaje.
- Si lo hay, aplica la avería que cause su peligro. Si la nave queda destruida, muéstralo y detén el viaje.
- Si la nave sigue entera, reposta, muestra el combustible y el casco, y mueve la nave 1.5 unidades hacia la derecha.
- Al terminar, si se han completado todas las etapas, muestra que la nave ha llegado a Ítaca y tíñela de verde; si no, tíñela de rojo.
Cinturón de asteroides: combustible 60, casco 80
Estación Eolia: combustible 90, casco 80
Nebulosa de las Sirenas: combustible 40, casco 70
Estrecho de Escila: combustible 20, casco 35
Ítaca: combustible 0, casco 35
¡Por fin en casa! La Odiseus ha llegado a Ítaca
Nota
La nave debe acabar en la posición x = 3.5, y = 0, de color verde. Llega con el depósito vacío: prueba a cambiar el repostaje de la estación Eolia a 40 y comprueba que la simulación detecta que el combustible no alcanza.
Reto extra
Añade entre la Nebulosa de las Sirenas y el Estrecho de Escila una etapa más: el «Remolino de Caribdis» (distancia 5, peligro "remolino"), que hace perder 40 puntos de casco. ¿Llega la Odiseus a Ítaca? Si no llega, ¿qué falla antes, el combustible o el casco?
Pista
Utiliza una variable de tipo bool (por ejemplo, enViaje) que empiece valiendo true y ponla a false cuando el viaje no pueda continuar; el bucle se repite mientras queden etapas y enViaje sea true. Al terminar, si el índice de la etapa ha llegado al número de etapas, es que se han completado todas.
Solución
public class Etapa
{
public string Nombre { get; private set; }
public int Distancia { get; private set; }
public string Peligro { get; private set; }
public int Repostaje { get; private set; }
public Etapa(string nombre, int distancia, string peligro, int repostaje)
{
Nombre = nombre;
Distancia = distancia;
Peligro = peligro;
Repostaje = repostaje;
}
}
public class NaveOdiseus
{
const int CombustibleMaximo = 100;
const int ConsumoPorUnidad = 2;
public int Combustible { get; private set; } = CombustibleMaximo;
public int Casco { get; private set; } = 100;
public bool Viajar(int distancia)
{
int consumo = distancia * ConsumoPorUnidad;
if (consumo > Combustible)
{
return false;
}
Combustible -= consumo;
return true;
}
public void SufrirAveria(int puntos)
{
Casco -= puntos;
if (Casco < 0)
{
Casco = 0;
}
}
public void Repostar(int litros)
{
Combustible += litros;
if (Combustible > CombustibleMaximo)
{
Combustible = CombustibleMaximo;
}
}
public bool Destruida()
{
return Casco == 0;
}
}
using System.Collections.Generic;
using UnityEngine;
public class EjercicioR45 : MonoBehaviour
{
const float AvancePorEtapa = 1.5f;
void Start()
{
transform.position = new Vector2(-4, 0);
List<Etapa> etapas = new List<Etapa>();
etapas.Add(new Etapa("Cinturón de asteroides", 20, "asteroides", 0));
etapas.Add(new Etapa("Estación Eolia", 10, "ninguno", 50));
etapas.Add(new Etapa("Nebulosa de las Sirenas", 25, "tormenta", 0));
etapas.Add(new Etapa("Estrecho de Escila", 10, "piratas", 0));
etapas.Add(new Etapa("Ítaca", 10, "ninguno", 0));
NaveOdiseus nave = new NaveOdiseus();
int i = 0;
bool enViaje = true;
while (i < etapas.Count && enViaje)
{
Etapa etapa = etapas[i];
if (nave.Viajar(etapa.Distancia) == false)
{
Debug.Log("No hay combustible para llegar a " + etapa.Nombre);
enViaje = false;
}
else
{
nave.SufrirAveria(Danyo(etapa.Peligro));
if (nave.Destruida())
{
Debug.Log("La Odiseus no ha sobrevivido a " + etapa.Nombre);
enViaje = false;
}
else
{
nave.Repostar(etapa.Repostaje);
Debug.Log(etapa.Nombre + ": combustible " + nave.Combustible + ", casco " + nave.Casco);
transform.Translate(new Vector2(AvancePorEtapa, 0));
i++;
}
}
}
if (i == etapas.Count)
{
Debug.Log("¡Por fin en casa! La Odiseus ha llegado a Ítaca");
GetComponent<SpriteRenderer>().color = Color.green;
}
else
{
GetComponent<SpriteRenderer>().color = Color.red;
}
}
int Danyo(string peligro)
{
switch (peligro)
{
case "asteroides":
return 20;
case "piratas":
return 35;
case "tormenta":
return 10;
default:
return 0;
}
}
}
Fin del viaje
La Odiseus aterriza en Ítaca con el depósito vacío, el casco abollado y la tripulación entera. Tú ya estás buscando una cafetería, Nono pide una manta y Telémaco quiere saber cuándo empieza el siguiente viaje.
Ya sabes manejar variables, bucles, colecciones, clases, herencia y composición: todo lo que hace falta para programar una nave entera. El siguiente viaje, en Unity.