Saltar a contenido

Ejercicios de C# · Refuerzo 3

La Odiseus despliega su flota de drones ante un asteroide

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».

  1. Crea un script vacío con el nombre Dron. La clase tendrá:
    • Las propiedades Nombre (tipo string), Bateria (tipo int) y Consumo (tipo int: la batería que gasta cada vez que trabaja). Desde fuera de la clase solo se pueden leer, pero Bateria también debe poder modificarse desde las clases que hereden de Dron.
    • Un constructor que reciba los tres datos.
    • Un método PuedeTrabajar que devuelva true si al dron le queda batería suficiente para trabajar una vez más.
    • Un método Trabajar, marcado como virtual, que reste el consumo a la batería y muestre un mensaje como Argos explora la zona (batería: 30).
  2. Crea un script vacío con el nombre DronMinero. La clase hereda de Dron y tendrá:
    • Una propiedad Mineral (tipo int) 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 como Pico extrae mineral (batería: 45, mineral: 5).
  3. Crea un script vacío con el nombre Hangar. La clase tendrá:
    • Una lista privada de drones.
    • Un método Guardar que reciba un dron y lo añada a la lista.
    • Un método LanzarTodos que recorra la lista: los drones que puedan trabajar, trabajan; de los que no, se muestra un mensaje como Mentor se queda en el hangar. El método devuelve cuántos drones han trabajado.
  4. Crea un script EjercicioR31 y asígnalo a la nave. En el método Start:
    • 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.
Salida esperada
--- 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
Dron.cs
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 + ")");
    }
}
DronMinero.cs
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 + ")");
    }
}
Hangar.cs
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;
    }
}
EjercicioR31.cs
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.

  1. Crea un script vacío con el nombre Arma. La clase tendrá dos propiedades que solo pueda modificar la propia clase, Nombre (tipo string) y Potencia (tipo int), un constructor que reciba los dos datos y un método Disparar, marcado como virtual, que devuelva el daño causado: la potencia del arma.
  2. Crea un script vacío con el nombre Misil. La clase hereda de Arma y añade una propiedad Unidades (tipo int) con los misiles que quedan. Su constructor recibe el nombre, la potencia y las unidades. Su versión de Disparar gasta un misil y devuelve la potencia si quedan unidades, y devuelve 0 si no queda ninguna.
  3. Crea un script EjercicioR32 y asígnalo a la nave. En el método Start:
    • 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.
Salida esperada
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
Arma.cs
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;
    }
}
Misil.cs
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;
    }
}
EjercicioR32.cs
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.

  1. Crea un script vacío con el nombre Motor. La clase tendrá:
    • Dos propiedades que solo pueda modificar la propia clase: Empuje (tipo float) y Encendido (tipo bool).
    • Un constructor que reciba el empuje (el motor empieza apagado).
    • Dos métodos, Encender y Apagar.
    • Un método EmpujeActual que devuelva el empuje si el motor está encendido, o 0 si está apagado.
  2. Crea un script vacío con el nombre Propulsion. Esta clase se compone de dos motores, guardados en dos variables privadas, izquierdo y derecho. Tendrá:
    • Un constructor que reciba el empuje de cada motor y cree los dos motores.
    • Tres métodos: EncenderTodos, ApagarIzquierdo y ApagarDerecho.
    • Dos métodos, IzquierdoEncendido y DerechoEncendido, que devuelvan true si el motor correspondiente sigue encendido.
    • Un método Velocidad que devuelva la suma del empuje actual de los dos motores.
  3. Crea un script EjercicioR33 y asígnalo a la nave. En el método Start, crea un sistema de propulsión con motores de empuje 0.75, enciéndelos y muestra la velocidad.
  4. 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.
Salida esperada
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
Motor.cs
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;
    }
}
Propulsion.cs
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();
    }
}
EjercicioR33.cs
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.

  1. Crea un script vacío con el nombre Alarma. La clase tendrá:
    • Una propiedad Intervalo (tipo float) con los segundos entre dos cambios de la luz, y otra Cambios (tipo int) con los cambios que lleva. Las dos solo las puede modificar la propia clase.
    • Un campo privado de tipo float para 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 Actualizar que 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.
  2. Crea un script EjercicioR34 y asígnalo a la nave. Tendrá una variable pública nivel (2), una constante con el número total de cambios de la luz (8) y una variable de clase para la alarma.
  3. 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.
  4. En el método Update, mientras no se hayan hecho todos los cambios, actualiza la alarma con Time.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.
Salida esperada
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
Alarma.cs
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;
    }
}
EjercicioR34.cs
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.)

  1. Crea un script vacío con el nombre Especialista. La clase tendrá:
    • Una propiedad Nombre (tipo string) que solo pueda modificar la propia clase, y un constructor que la reciba.
    • Un método Tarea, marcado como virtual, que devuelva el texto "ayuda en lo que puede".
    • Un método Informe (sin virtual) que devuelva el nombre seguido de la tarea, por ejemplo Telémaco ayuda en lo que puede.
  2. 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)".
  3. Crea un script EjercicioR35 y asígnalo a la nave. En el método Start, crea una lista de especialistas con Telémaco (sin especialidad), Ulises (navegante: es decir, tú) y Nono (mecánico).
  4. Con un bucle for, simula dos turnos. En cada turno, muestra el número de turno y el informe de cada tripulante.
Salida esperada
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
Especialista.cs
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();
    }
}
Navegante.cs
public class Navegante : Especialista
{
    public Navegante(string nombre) : base(nombre)
    {
    }

    public override string Tarea()
    {
        return "traza la ruta";
    }
}
Mecanico.cs
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 + ")";
    }
}
EjercicioR35.cs
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.

  1. Crea un script vacío con el nombre Bulto, con dos propiedades que solo pueda modificar la propia clase, Nombre (tipo string) y Peso (tipo int, en kilos), y un constructor que reciba los dos datos.
  2. Crea un script vacío con el nombre BodegaCarga. La clase tendrá:
    • Una propiedad Capacidad (tipo int) que solo pueda modificar la propia clase, y un constructor que la reciba.
    • Una lista privada de bultos.
    • Un método PesoTotal que devuelva la suma del peso de todos los bultos.
    • Un método Cargar que reciba un bulto y lo añada a la lista solo si cabe. Devolverá true si lo ha cargado.
    • Un método Descargar que reciba el nombre de un bulto, lo busque en la lista y lo elimine. Devolverá true si lo ha encontrado.
    • Un método Contenido que devuelva un texto con los nombres de todos los bultos.
  3. Crea un script EjercicioR36 y asígnalo a la nave. En él, crea dos métodos auxiliares, IntentarCargar e IntentarDescargar, que llamen a los métodos de la bodega y muestren el resultado.
  4. 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.
  5. Al terminar, muestra el contenido de la bodega y su peso total.
Salida esperada
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
Bulto.cs
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;
    }
}
BodegaCarga.cs
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;
    }
}
EjercicioR36.cs
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.

  1. Crea un script vacío con el nombre Enemigo. La clase tendrá:
    • Las propiedades Nombre (tipo string) y Vida (tipo int). Desde fuera solo se pueden leer, pero Vida también la pueden modificar las clases hijas.
    • Un constructor que reciba los dos datos.
    • Un método RecibirImpacto, marcado como virtual, que reciba la potencia del disparo y la reste a la vida, sin bajar de 0.
    • Un método Destruido que devuelva true si la vida ha llegado a 0.
  2. Crea un script vacío con el nombre EnemigoBlindado. La clase hereda de Enemigo y su versión de RecibirImpacto solo resta a la vida la mitad de la potencia, también sin bajar de 0.
  3. Crea un script EjercicioR37 y 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, o null si lo están todos.
  4. 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).
  5. 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.
  6. Al terminar, muestra cuántos disparos han hecho falta.
Salida esperada
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
Enemigo.cs
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;
    }
}
EnemigoBlindado.cs
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;
        }
    }
}
EjercicioR37.cs
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.

  1. Crea dos arrays de tipo float como variables de clase, con las coordenadas de los puntos de paso: uno con las x (3, 3, -2 y -2) y otro con las y (0, 2, 2 y -1). El primer punto es (3, 0), el segundo (3, 2), y así sucesivamente.
  2. Crea una variable pública velocidad (2), una constante Margen (0.1) y una variable de clase con la posición del siguiente punto de paso.
  3. En el método Update, mientras queden puntos por visitar, compara la posición de la nave con la del siguiente punto:
    • Si la x de 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 x ya coincide, haz lo mismo con la y: 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.

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.

Salida esperada
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
EjercicioR38.cs
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.

  1. 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 (tipo float, empieza en el máximo) y Activo (tipo bool).
    • Un método Cargado, que devuelva true si 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.
  2. Crea un script EjercicioR39 y 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.
  3. En el método Start, crea el camuflaje y actívalo.
  4. En el método Update, actualiza el camuflaje con Time.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);
Salida esperada
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
Camuflaje.cs
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;
            }
        }
    }
}
EjercicioR39.cs
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.

  1. Crea un script vacío con el nombre Mejora. La clase tendrá:
    • Las propiedades Nombre (tipo string), CosteBase y Nivel (tipo int; 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 Coste que 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.
  2. Crea un script EjercicioR40 y 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).
  3. 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).
  4. Con un bucle while, compra la mejora más barata mientras haya créditos para pagarla. Muestra cada compra.
  5. 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.
Salida esperada
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
Mejora.cs
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++;
    }
}
EjercicioR40.cs
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
  1. 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.
  2. 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.
  3. En el estado 1, gira la nave con transform.Rotate(new Vector3(0, 0, -90));.
  4. Al completar el cuarto lado, tiñe la nave de verde y muestra que el cuadrado se ha completado.
Salida esperada
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
EjercicioR41.cs
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.

  1. 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 }
    };
    
  2. Crea también una variable de clase con las naves enemigas que quedan. En Start, cuéntalas recorriendo el tablero con dos bucles for anidados y muestra el resultado.

  3. Crea un método Disparar que reciba una fila y una columna, y devuelva el resultado del disparo. Con una sentencia switch sobre 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".
  4. 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).
  5. 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.
Salida esperada
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
EjercicioR42.cs
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.

  1. Crea un script vacío con el nombre NaveConvoy. La clase tendrá:
    • Dos propiedades que solo pueda modificar la propia clase, Nombre (tipo string) y Velocidad (tipo int), y un constructor que reciba los dos datos.
    • Un método Carga, marcado como virtual, que devuelva 0.
    • Un método Descripcion, marcado como virtual, que devuelva un texto con el nombre y la velocidad.
  2. Crea dos clases que hereden de NaveConvoy, cada una en su script:
    • Carguero, con una propiedad Toneladas. Su Carga devuelve las toneladas y su Descripcion, un texto como Carguero Ceres: 500 toneladas, velocidad 3.
    • Escolta, con una propiedad Canones. Su Descripcion devuelve un texto como Escolta Ares: 4 cañones, velocidad 9.
  3. 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.
  4. Crea un script EjercicioR43 y asígnalo a la nave. En el método Start, 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
  5. 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));.

Salida esperada
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
NaveConvoy.cs
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;
    }
}
Carguero.cs
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;
    }
}
Escolta.cs
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;
    }
}
Convoy.cs
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;
    }
}
EjercicioR43.cs
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.

  1. Crea un script vacío con el nombre Pulso. La clase tendrá:
    • Tres propiedades que solo pueda modificar la propia clase, Minimo, Maximo y Velocidad (tipo float), y un constructor que las reciba.
    • Una propiedad Valor (tipo float), que empieza valiendo el mínimo, y otra Latidos (tipo int), con los latidos completados. Las dos solo las puede modificar la propia clase.
    • Un campo privado de tipo bool que 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.
  2. Crea un script EjercicioR44 y 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.
  3. En el método Start, crea un pulso con mínimo 1, máximo 1.5 y velocidad 2.
  4. En el método Update, mientras no se hayan completado todos los latidos, actualiza el pulso con Time.deltaTime y 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.
Salida esperada
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
Pulso.cs
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++;
            }
        }
    }
}
EjercicioR44.cs
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.

  1. Crea un script vacío con el nombre Etapa. La clase tendrá cuatro propiedades que solo pueda modificar la propia clase, Nombre (tipo string), Distancia (tipo int), Peligro (tipo string) y Repostaje (tipo int: el combustible que se puede cargar al final de la etapa), y un constructor que reciba los cuatro datos.
  2. 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, Combustible y Casco (tipo int), que empiezan en 100.
    • Un método Viajar que reciba una distancia. Si hay combustible para recorrerla, lo gasta y devuelve true; si no, devuelve false sin 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 devuelva true si el casco ha llegado a 0.
  3. Crea un script EjercicioR45 y 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 sentencia switch: 20 los asteroides, 35 los piratas, 10 las tormentas y 0 cualquier otro.
  4. 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
  5. 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.
  6. 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.
Salida esperada
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
Etapa.cs
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;
    }
}
NaveOdiseus.cs
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;
    }
}
EjercicioR45.cs
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.