El héroe
Parte de la sección Arquitectura de juegos en Unity del Journey de Unity de Coddy. Lección 41 de 50.
Este capítulo crea Arena Survivor, el último juego del recorrido. Oleadas de enemigos irrumpen en una arena amurallada y tú los repeles con una espada: soldados rasos que caminan hacia ti y orbes que esperan y luego se abalanzan. Una oleada derrotada deja una mejora, un HUD muestra tu salud y la oleada, y cuando se agota tu salud, la partida termina y se guarda tu mejor partida. El juego utiliza las herramientas de esta sección cuando encajan: un Enemy abstracto con dos tipos, IDamageable para todo lo que pueda recibir daño, las oleadas como recurso ScriptableObject, objetos recogibles detrás de una interfaz, un HUD que solo escucha eventos de C# y un guardado en JSON. Los doce scripts están en el proyecto desde el principio. Solo Hero tiene código por ahora, y cada lección añade más.
La espada del héroe golpea lo que tiene delante, así que el ataque necesita comprobar si está «delante». Vector3.Dot(a, b) multiplica las partes correspondientes de dos vectores y las suma: a.x * b.x + a.y * b.y + a.z * b.z. Para dos direcciones de longitud 1, eso es el coseno del ángulo entre ellas: 1 cuando apuntan en la misma dirección, 0.866 a 30 grados, 0.707 a 45, 0 en ángulos rectos y -1 cuando apuntan en direcciones opuestas. Así que «a menos de 30 grados de hacia donde estoy mirando» se convierte en «un producto escalar de al menos 0.866», sin tener que calcular ningún ángulo. Este robot centinela da la alarma cuando el jugador está a menos de 10 unidades y dentro de su campo de visión de 30 grados:
public Transform player;
public float range = 10f;
void Update()
{
Vector3 toPlayer = player.position - transform.position;
toPlayer.y = 0f;
bool near = toPlayer.magnitude <= range;
bool inView = Vector3.Dot(transform.forward, toPlayer.normalized) >= 0.866f;
if (near && inView)
{
Debug.Log("Alarm!");
}
}Ambas direcciones deben tener longitud 1. transform.forward siempre la tiene, pero toPlayer tiene la longitud de la distancia, así que primero se normaliza; de lo contrario, un jugador que estuviera el doble de lejos produciría un producto escalar el doble de grande. Su valor y se establece en 0 antes de eso, de modo que la posición de un jugador en una pasarela más alta se determina por dónde se encuentra en el mapa, no por su altura. El signo distingue delante de detrás: un producto escalar mayor que 0 significa que está en cualquier parte de la mitad delantera; menor que 0, en cualquier parte detrás.
Desafío
IntermedioInicia Arena Survivor con la espada del héroe. El desplazamiento se escribe así: la acción Move desplaza al héroe con SimpleMove y lo gira para que mire en la dirección en que se desplaza. Cinco muñecos de entrenamiento etiquetados como Enemy están alrededor. El Inspector asigna a Hero un reach de (2) y un swingCooldown de (0.5), así como el efecto de corte que muestra durante un golpe.
- La barra espaciadora o un clic izquierdo ejecutan un golpe, como máximo una vez cada
swingCooldownsegundos: si pulsas antes de que termine el tiempo de espera, no ocurre nada. Lee las teclas conKeyboard.currentyMouse.current, y llama aSwing(). - En
Swing(), después de mostrar el efecto de corte, destruye todos los objetos etiquetados comoEnemyque estén dentro dereachy delante del héroe. Estar dentro del alcance significa que cualquier colisionador quePhysics.OverlapSphereencuentre alrededor del héroe con un radio dereach. Estar delante significa que el producto escalar entre la dirección hacia delante del héroe y la dirección al objeto a lo largo del suelo sea de al menos 0.5: hasta 60 grados a cualquiera de los lados.
Deja los demás scripts tal como están. Pulsa Play, acércate a los muñecos y ejecuta un golpe; después, pulsa Check.
Pruébalo tú mismo
using System.Collections;
using UnityEngine;
using UnityEngine.InputSystem;
public class Hero : MonoBehaviour
{
[SerializeField] private float speed = 5f;
[SerializeField] private float reach = 2f;
[SerializeField] private float swingCooldown = 0.5f;
[SerializeField] private GameObject slash;
private CharacterController controller;
private InputAction moveAction;
void Start()
{
controller = GetComponent<CharacterController>();
moveAction = InputSystem.actions.FindAction("Move");
}
void Update()
{
Vector2 input = moveAction.ReadValue<Vector2>();
Vector3 move = new Vector3(input.x, 0f, input.y);
controller.SimpleMove(move * speed);
if (move != Vector3.zero)
{
transform.rotation = Quaternion.LookRotation(move);
}
// La barra espaciadora o un clic izquierdo ejecuta un ataque, como máximo una vez cada swingCooldown segundos
}
void Swing()
{
StartCoroutine(ShowSlash());
// destruye todo lo que tenga la etiqueta Enemy, esté al alcance y delante del héroe
}
IEnumerator ShowSlash()
{
slash.SetActive(true);
yield return new WaitForSeconds(0.1f);
slash.SetActive(false);
}
}
Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.
Todas las lecciones de Arquitectura de juegos en Unity
1Componentes, no jerarquías
Componentes pequeñosUn script base para enemigosComponentes abstractosComponentes que trabajan juntosRepaso: torreta modular4Datos serializados
Clases serializablesListas en el InspectorEnumeraciones en el InspectorRepaso: tabla de botín7Máquinas de estados
Estados como clasesUna máquina de estadosTransicionesCerebros de enemigosRepaso: estados de puertas10Proyecto: Superviviente de la arena
El héroeTipos de enemigos2Interfaces en Unity
IDamageableInterfaces y TryGetComponentGetComponentsRepaso: tecla de interacción5ScriptableObjects
Recursos de datosEstadísticas de armasConfiguración compartidaOleadas como datosRepaso: base de datos de objetos