El objetivo#
Un arma con cadencia (no dispara infinitamente rápido), cargador limitado, recarga que tarda un tiempo, y munición de reserva. Con eventos para que la UI muestre "24 / 90".
La receta#
using System;
using System.Collections;
using UnityEngine;
public class Arma : MonoBehaviour
{
[Header("Munición")]
[SerializeField] private int capacidadCargador = 30;
[SerializeField] private int municionReserva = 90;
[Header("Disparo")]
[SerializeField] private float cadencia = 0.1f; // segundos entre disparos
[SerializeField] private int daño = 20;
[SerializeField] private float alcance = 100f;
[SerializeField] private Camera camara;
[Header("Recarga")]
[SerializeField] private float tiempoRecarga = 1.5f;
private int enCargador;
private float proximoDisparo;
private bool recargando;
// La UI escucha esto: (cargador, reserva)
public event Action<int, int> AlCambiarMunicion;
void Awake()
{
enCargador = capacidadCargador;
AvisarUI();
}
void Update()
{
// Disparo automático mientras se mantiene el botón
if (Input.GetButton("Fire1") && Time.time >= proximoDisparo)
Disparar();
if (Input.GetKeyDown(KeyCode.R))
StartCoroutine(Recargar());
}
void Disparar()
{
if (recargando) return;
if (enCargador <= 0) { StartCoroutine(Recargar()); return; } // recarga sola al vaciarse
proximoDisparo = Time.time + cadencia;
enCargador--;
AvisarUI();
// Disparo hitscan por raycast (ver artículo de raycasting)
Ray rayo = new Ray(camara.transform.position, camara.transform.forward);
if (Physics.Raycast(rayo, out RaycastHit hit, alcance))
{
if (hit.collider.TryGetComponent<IDañable>(out var objetivo))
objetivo.RecibirDaño(daño);
}
}
IEnumerator Recargar()
{
if (recargando || enCargador == capacidadCargador || municionReserva <= 0) yield break;
recargando = true;
yield return new WaitForSeconds(tiempoRecarga);
int necesita = capacidadCargador - enCargador;
int aRecargar = Mathf.Min(necesita, municionReserva); // solo lo que hay en reserva
enCargador += aRecargar;
municionReserva -= aRecargar;
recargando = false;
AvisarUI();
}
void AvisarUI() => AlCambiarMunicion?.Invoke(enCargador, municionReserva);
}Por qué así#
Time.time >= proximoDisparo: la cadencia sin corrutinas. Cada disparo fija el siguiente instante permitido. Simple y preciso.- La recarga es una corrutina: tarda
tiempoRecargasin congelar el juego, y bloquea disparar mientras dura. Mathf.Min(necesita, reserva): solo recargas lo que cabe y lo que te queda en reserva; nada de munición mágica.IDañable: reutiliza el sistema de vida y daño — el arma no sabe a qué dispara.
El error clásico es sumar capacidadCargador a la reserva sin mirar. Lo correcto: recargas solo lo que falta en el cargador y solo si lo tienes en reserva. Por eso el Mathf.Min. Así 5 balas en reserva no llenan un cargador de 30.
Variantes#
- Proyectil en vez de raycast: instancia una bala (mejor con object pooling si disparas mucho).
- Disparo semiautomático: usa
Input.GetButtonDownen vez deGetButton(un disparo por clic). - Dispersión (spread): añade un pequeño ángulo aleatorio a la dirección del rayo.
- Retroceso y feedback: screen shake, fogonazo, sonido, casquillo.
Errores frecuentes#
- Recargar sumando el cargador completo sin descontar de la reserva.
- Permitir disparar durante la recarga (bloquéalo con la bandera
recargando). - Cadencia con corrutinas cuando
Time.timees más simple y preciso. - No avisar a la UI por evento y acabar consultando la munición cada frame.
- Leer el disparo en
FixedUpdate(va enUpdate).
Resumen#
Controla la cadencia con Time.time >= proximoDisparo, gestiona cargador y reserva por separado (recarga solo lo que falta y tienes, con Mathf.Min), haz la recarga con una corrutina que bloquea el disparo, y avisa a la UI por evento. El daño reutiliza IDañable del sistema de vida. Amplíalo con proyectiles, dispersión y feedback.