En este episodio crearemos y añadiremos un “Enemigo 3D” a nuestro “Primer Juego 3D” en Unity 🙂
Para ser más precisos, crearemos un Enemigo que patrulle moviéndose en direcciones aleatorias, pero que al vernos nos comience a perseguir. Si nos toca nos causará daño pero si saltamos y le caemos encima lo podremos derrotar.
Tutorial de Unity Nivel: Principiante – Intermedio.
5.1 Los Preparativos.
Antes de comenzar con los códigos vamos a realizar los siguientes ajustes que permitirán a nuestro “Enemigo” funcionar adecuadamente.
Es necesario crear los siguientes “Tags“:
- Wall
- Enemy
- Ground
Estos tags le ayudaran a nuestro Enemigo a poder moverse por el suelo y a distinguir si está chocando contra una pared o contra nosotros.
A continuación, hay que asignar el tag “Ground” al suelo (así tendrá el nombre “MyGround” con tag “Ground” y layer “ground3D”) y el tag “Wall” a todos los bloques que representan obstáculos y muros ( que para este tutorial se llaman “Rock” y tendrán el tag “Wall” y layer “ground3D”).
5.2 El Enemigo.
Para crear a nuestro Enemigo debemos hacer lo siguiente:
- Crea un nuevo objeto color anaranjado en la escena y nómbralo como “Enemy” (nosotros usamos cubos pero tu puedes usar el objeto que desees).
- Asígnale el tag “Enemy” y la capa “Enemy” (todo en el se llamará “Enemy” 😀 ).
- El “Collider” del Enemigo será un collider normal (no es necesario que sea un trigger).
- Añade el componente “Rigidbody“. Asegúrate que tenga activa la propiedad “Use Gravity” y los ejes “X, Z” de “Freeze Rotation” (dentro de “Constraints“).
- Añade el scrip “DamageDealer“. Desactiva “Destroy On Hit” y asigna la capa “Player” en “Target Layer“.
- Añade el script “HealthSystem“. Desactiva la regeneración y la invulnerabilidad. En el evento “On Death” dá clic en el ícono de “más” (+) y añade este mismo objeto “Enemy” y selecciona la función “SetActive” (GameObject→SetActive); esto hará que el enemigo se desactive (desaparezca de la escena) automáticamente cuando muera.

El código que se encargará de controlar a nuestro enemigo se llamará “Enemy.cs” y es el siguiente:
using UnityEngine;
using System.Collections;
// Asegura que el GameObject tenga un componente Rigidbody al ser agregado este script
[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(HealthSystem))] // Asegurar que tenga HealthSystem
public class Enemy : MonoBehaviour
{
// Estados del enemigo
private enum State { Patrol, Chase }
private State currentState = State.Patrol;
// Configuración de patrullaje
[Header("Configuración de Patrullaje")]
[Tooltip("Radio máximo del área donde el enemigo puede moverse aleatoriamente")]
public float patrolRadius = 10f;
[Tooltip("Tiempo en segundos entre cambios de dirección durante el patrullaje")]
public float patrolChangeInterval = 3f;
[Tooltip("Velocidad de movimiento durante el patrullaje")]
public float patrolSpeed = 2f;
// Configuración de persecución
[Header("Configuración de Persecución")]
[Tooltip("Distancia máxima a la que el enemigo detecta al jugador para comenzar a perseguirlo")]
public float chaseRange = 15f;
[Tooltip("Velocidad de movimiento durante la persecución")]
public float chaseSpeed = 4f;
[Tooltip("Distancia a la que el enemigo deja de perseguir al jugador")]
public float stopChaseDistance = 20f;
// Configuración de Aplastamiento
[Header("Configuración de Aplastamiento")]
[Tooltip("¿El enemigo muere instantáneamente al ser aplastado?")]
public bool dieOnStomp = true;
[Tooltip("Daño recibido al ser aplastado (si no muere instantáneamente)")]
public float stompDamage = 20f;
private HealthSystem healthSystem; // Referencia al HealthSystem
// Referencias a componentes y objetos
private Rigidbody rb; // Referencia al componente Rigidbody del enemigo
private Transform player; // Referencia a la transformación del jugador
private Vector3 patrolDirection; // Dirección actual de movimiento durante el patrullaje
private Vector3 initialPosition; // Posición inicial del enemigo (centro de su área de patrullaje)
/// <summary>
/// Método llamado al iniciar el script. Inicializa referencias y variables.
/// </summary>
void Start()
{
// Obtiene el componente Rigidbody del enemigo
rb = GetComponent<Rigidbody>();
healthSystem = GetComponent<HealthSystem>(); // Obtener el HealthSystem
if (healthSystem == null)
{
Debug.LogError("Enemy necesita un componente HealthSystem!", this);
}
// Guarda la posición inicial como centro del área de patrullaje
initialPosition = transform.position;
// Busca el objeto con etiqueta "Player" en la escena
GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
// Si se encuentra el jugador, guarda su transform
if (playerObj != null)
player = playerObj.transform;
// Inicia la corrutina para actualizar la dirección de patrullaje
StartCoroutine(UpdatePatrolDirection());
}
/// <summary>
/// Método llamado cada frame. Maneja la lógica de cambio de estados y movimiento.
/// </summary>
void Update()
{
// Verificar distancia al jugador si existe
if (player != null)
{
// Calcula la distancia entre el enemigo y el jugador
float distanceToPlayer = Vector3.Distance(transform.position, player.position);
// Si el jugador está dentro del rango de persecución y no se está persiguiendo
if (distanceToPlayer <= chaseRange && currentState != State.Chase)
{
// Cambia al estado de persecución
currentState = State.Chase;
// Detiene la corrutina de cambio de dirección de patrullaje
StopCoroutine(UpdatePatrolDirection());
}
// Si el jugador se aleja más allá de la distancia de parada y no se está patrullando
else if (distanceToPlayer > stopChaseDistance && currentState != State.Patrol)
{
// Cambia al estado de patrullaje
currentState = State.Patrol;
// Reinicia la corrutina de cambio de dirección de patrullaje
StartCoroutine(UpdatePatrolDirection());
}
}
// Actualizar movimiento según el estado actual del enemigo
switch (currentState)
{
case State.Patrol:
// Ejecuta el movimiento de patrullaje
PatrolMovement();
break;
case State.Chase:
// Ejecuta el movimiento de persecución
ChaseMovement();
break;
}
}
/// <summary>
/// Corrutina que actualiza periódicamente la dirección de patrullaje.
/// </summary>
/// <returns>IEnumerator para el funcionamiento de la corrutina</returns>
IEnumerator UpdatePatrolDirection()
{
// Bucle infinito para actualizar continuamente la dirección
while (true)
{
// Generar una dirección aleatoria en un círculo (ángulo aleatorio)
float randomAngle = Random.Range(0, 360);
// Convierte el ángulo a un vector de dirección en X y Z (plano horizontal)
patrolDirection = new Vector3(Mathf.Cos(randomAngle), 0, Mathf.Sin(randomAngle));
// Normaliza el vector para que tenga magnitud 1
patrolDirection = patrolDirection.normalized;
// Opcional: Si se ha salido del radio, dirigirse hacia el centro
if (Vector3.Distance(initialPosition, transform.position) > patrolRadius)
{
patrolDirection = (initialPosition - transform.position).normalized;
}
// Espera el intervalo especificado antes de la próxima actualización
yield return new WaitForSeconds(patrolChangeInterval);
}
}
/// <summary>
/// Controla el movimiento del enemigo durante el estado de patrullaje.
/// </summary>
void PatrolMovement()
{
// Aplica velocidad en la dirección de patrullaje
rb.linearVelocity = new Vector3(patrolDirection.x * patrolSpeed, rb.linearVelocity.y, patrolDirection.z * patrolSpeed);
// Opcional: Si se ha salido del radio, ajustar dirección hacia el centro
if (Vector3.Distance(initialPosition, transform.position) > patrolRadius)
{
patrolDirection = (initialPosition - transform.position).normalized;
}
}
/// <summary>
/// Controla el movimiento del enemigo durante el estado de persecución.
/// </summary>
void ChaseMovement()
{
// Si no hay jugador referenciado, salir del método
if (player == null) return;
// Calcular dirección hacia el jugador
Vector3 directionToPlayer = (player.position - transform.position).normalized;
// Mantener movimiento en plano horizontal (ignorar diferencia de altura)
directionToPlayer.y = 0;
// Aplicar velocidad en dirección al jugador
rb.linearVelocity = new Vector3(directionToPlayer.x * chaseSpeed, rb.linearVelocity.y, directionToPlayer.z * chaseSpeed);
}
/// <summary>
/// Método llamado en el editor para dibujar ayudas visuales (gizmos).
/// Solo se muestra cuando el objeto está seleccionado.
/// </summary>
void OnDrawGizmosSelected()
{
// Dibuja el radio de patrullaje en color amarillo
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, patrolRadius);
// Dibuja el radio de inicio de persecución en color rojo
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, chaseRange);
// Dibuja el radio de fin de persecución en color azul
Gizmos.color = Color.blue;
Gizmos.DrawWireSphere(transform.position, stopChaseDistance);
}
/// <summary>
/// Método llamado cuando el jugador salta sobre el enemigo.
/// </summary>
public void Stomp()
{
if (healthSystem == null || healthSystem.IsDead) return; // No hacer nada si no hay sistema de salud o ya está muerto
if (dieOnStomp)
{
// Matar al enemigo instantáneamente
healthSystem.TakeDamage(healthSystem.MaxHealth); // O simplemente llamar a Die() si es público/privado
// healthSystem.Die();
}
else
{
// Aplicar daño específico por aplastamiento
healthSystem.TakeDamage(stompDamage);
}
}
/// <summary>
/// Método llamado cuando el enemigo muere.
/// </summary>
private void OnDeath()
{
// Aquí puedes agregar efectos visuales, sonidos, puntuación, etc.
Debug.Log($"{gameObject.name} ha muerto por aplastamiento.");
// Destruir el GameObject del enemigo
Destroy(gameObject);
}
// Opcional: Si el enemigo también puede dañar al jugador por contacto
void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.CompareTag("Player"))
{
// Verificar si el contacto NO es desde arriba (para evitar daño al aplastar)
ContactPoint contact = collision.contacts[0];
// Si la normal del contacto en el jugador apunta hacia arriba significativamente,
// probablemente sea un aplastamiento, no un daño.
// Asumimos que el jugador está en collision.collider y el enemigo en this.collider
if (contact.normal.y < 0.5f) // El contacto en el jugador no es principalmente desde abajo
{
// El enemigo daña al jugador
DamageDealer damageDealer = GetComponent<DamageDealer>();
if (damageDealer == null)
{
// Si no tiene DamageDealer, podemos crear uno temporal o usar HealthSystem directamente
HealthSystem playerHealth = collision.gameObject.GetComponent<HealthSystem>();
if(playerHealth != null)
{
playerHealth.TakeDamage(10f); // Daño fijo, ajusta según necesites
Debug.Log("El jugador recibió daño del enemigo.");
}
}
// Si tiene DamageDealer, este se encargará automáticamente si está configurado
// correctamente para afectar al jugador.
}
} // Si colisiona con una pared o el suelo durante el patrullaje
else if (collision.gameObject.CompareTag("Wall") || collision.gameObject.CompareTag("Ground"))
{
// Invertir dirección con reflexión basada en la normal de contacto
patrolDirection = Vector3.Reflect(patrolDirection, collision.contacts[0].normal);
// Mantener dirección en plano horizontal
patrolDirection = new Vector3(patrolDirection.x, 0, patrolDirection.z).normalized;
}
}
}
Asigna este código a nuestro Enemigo y asegúrate de que la opción “Die On Stomp” esté activa.
Coloca al Enemigo sobre el suelo en el área donde quieras que esté patrullando. Puedes dar “Play” solo para probar que este se mueve y que no hay errores (pero solo eso porque aún nos falta más 🙂 ).
5.3 Ajustes para Dañar
Para que nuestro enemigo pueda causarle daño al personaje cuando lo toque es necesario modificar el código “DamageDealer.cs” porque actualmente este código trabaja con colliders tipo trigger pero nuestro enemigo tiene un collider normal; por ello incluiremos el siguiente fragmento de código:
using UnityEngine;
// Clase para objetos que pueden causar daño a otras entidades del juego
public class DamageDealer : MonoBehaviour
{
//CÓDIGO EXISTENTE...
//CÓDIGO EXISTENTE...
// Método que se ejecuta cuando este objeto (con collider trigger) entra en contacto con otro collider
//private void OnTriggerEnter(Collider other) { }
// Método que se ejecuta cuando este objeto (con collider normal) colisiona físicamente con otro objeto
private void OnCollisionEnter(Collision collision)
{
// Verifica si el objeto con el que colisionó está en las capas objetivo
if (IsTargetLayer(collision.gameObject))
{
// Inflige daño al objeto objetivo
DealDamage(collision.gameObject);
// Reproduce efectos visuales y de audio del impacto
HandleHitEffects();
}
}
// Método privado que se encarga de aplicar el daño al objetivo
//private void DealDamage(GameObject target) { }
//CÓDIGO EXISTENTE...
//CÓDIGO EXISTENTE...
}
El código de arriba muestra la sección donde se debe añadir las modificaciones necesarias para que nuestro Enemigo pueda causarle daño al personaje. Recuerda guardar estos cambios y esperar a que el motor de Unity los compile antes de dar Play y probar que el Enemigo resta salud al personaje (solo eso porque aún falta más 🙂 ).
5.4 Ajustes para el Personaje.
Para que nuestro personaje pueda saltar, caer encima del enemigo y derrotarlo, es necesario modificar el script “PlayerController.cs” de la siguiente manera:
// ... (using statements y atributos existentes) ...
[RequireComponent(typeof(CharacterController))]
public class PlayerController : MonoBehaviour
{
// ... (variables existentes) ...
// === NUEVA VARIABLE PARA APLASTAMIENTO ===
private float lastGroundedTime = 0f; // Para verificar si el jugador estaba en el aire recientemente
void Start()
{
// ... (código existente) ...
}
void Update()
{
// ... (código existente) ...
HandleMovement();
HandleJump();
HandleDash();
HandleWallRun();
HandleGlide();
// === NUEVA LÓGICA PARA APLASTAMIENTO ===
// Actualizar el tiempo de última vez que estuvo en el suelo
if (isGrounded)
{
lastGroundedTime = Time.time;
}
// Verificar aplastamiento si está cayendo y no está en una plataforma
if (!isGrounded && velocity.y < 0 && platform == null)
{
CheckStomp();
}
// === MOVIMIENTO DE PLATAFORMA ===
// ... (código existente) ...
}
// ... (resto de métodos existentes) ...
// === NUEVO MÉTODO PARA DETECTAR APLASTAMIENTO ===
private void CheckStomp()
{
// Definir una pequeña esfera ligeramente por debajo del jugador para detectar enemigos
Vector3 stompCheckPosition = transform.position + Vector3.down * (controller.height / 2 + 0.1f);
float stompCheckRadius = controller.radius * 0.8f; // Un poco más pequeño que el controlador
Collider[] hitColliders = Physics.OverlapSphere(stompCheckPosition, stompCheckRadius);
foreach (var hitCollider in hitColliders)
{
// Verificar si el objeto es un enemigo (por etiqueta o componente)
Enemy enemy = hitCollider.GetComponent<Enemy>();
if (enemy != null)
{
// Verificar que el jugador esté cayendo (y no subiendo)
if (velocity.y < 0)
{
// Aplastar al enemigo
enemy.Stomp();
// Aplicar rebote al jugador (similar al salto, pero basado en la velocidad actual)
// Esto hace que el rebote sea más fuerte si caía desde mayor altura
velocity.y = Mathf.Sqrt(jumpForce * -2f * risingGravity);
// O una fuerza fija: velocity.y = Mathf.Sqrt(jumpForce * 0.7f * -2f * risingGravity);
Debug.Log("¡Enemigo aplastado!");
break; // Solo aplastar al primer enemigo detectado
}
}
}
}
// ... (resto de métodos existentes) ...
// Opcional: Visualizar la esfera de detección de aplastamiento en el editor
void OnDrawGizmosSelected()
{
if (controller != null)
{
// Dibujar la esfera de detección de aplastamiento
Gizmos.color = Color.magenta;
Vector3 stompCheckPosition = transform.position + Vector3.down * (controller.height / 2 + 0.1f);
float stompCheckRadius = controller.radius * 0.8f;
Gizmos.DrawWireSphere(stompCheckPosition, stompCheckRadius);
}
// ... (otras visualizaciones existentes si las hubiera) ...
}
}
El código de arriba muestra la sección donde se debe añadir las modificaciones necesarias para que nuestro personaje pueda derrotar al Enemigo. Recuerda guardar estos cambios y esperar a que el motor de Unity los compile.
Finalmente, en el componente “Health System” del personaje es necesario activar la invulnerabilidad con una duración de dos segundos (2) para que de esta forma cuando el enemigo te toque, te baje salud una vez y no te pueda dañar nuevamente sino hasta que transcurran dos segundos y te dé oportunidad de escapar 🙂 .
¡Ahora sí, el Momento ha llegado, Oprime Play y disfruta tu Enemigo 3D en tu Primer Juego 3D 🙂 !
Ejercicios.
Para reforzar lo aprendido, es necesario practicarlo, por ello intenta realizar los siguientes ejercicios:
- Prueba las opciones de “Invulnerabilidad” y “Regeneración” presentes en el “Health System” de tu personaje.
- Añade efectos especiales y clips de audio para probar las funcionalidades del Enemigo que daña al personaje.
- Crea un Enemigo que muera de tres saltos.
- Modifica el código “Enemy.cs” para generar los efectos especiales al momento de morir (como lo hacen los objetos del tutorial anterior).
Este Tutorial de Unity termina aquí. Acompáñanos en el siguiente tutorial donde añadiremos más funcionalidades a “Tu Primer Juego 3D”.
Siguiente Tutorial de Unity: “6. Dinero y Triggers“
Unity Tutorial: “Tu Primer Juego 3D“
1. Creando un Juego 3D
2. Supercargando el Juego
3. Añadiendo Plataformas
4. Salud y Daño
5. Enemigos 3D
6. Dinero y Triggers
Ver más Tutoriales






















