Hoy vamos a Supercargar nuestro “Primer Juego 3D” en Unity 🙂
Para ser más precisos, vamos a mejorar nuestro código “Player Controller” para añadirle “Habilidades Especiales” a nuestro personaje como “Dash“, “Doble Salto“, “Correr“, “Planear“, “Correr por los Muros” y “Salto en Muro“.
Tutorial de Unity Nivel: Intermedio.
2.1 Cambiando el Código.
Alerta de Spoiler “Se viene muchísimo Código” 🙁
Con la intención de añadirle a nuestro personaje las habilidades de “doble salto”, “correr”, “correr y saltar por los muros”, “planear” y hacer “dash”(además de mejorar los movimientos básicos); vamos a “modificar completamente” nuestro código “PlayerController” para que quede como se muestra a continuación (hemos añadido todos los comentarios posibles para que ayuden a explicar que es lo que hace cada parte). Así que, comencemos:
using UnityEngine; using UnityEngine.InputSystem; [RequireComponent(typeof(CharacterController))] public class PlayerController : MonoBehaviour { // ================================================= // VARIABLES EDITABLES EN EL INSPECTOR // ================================================= [Header("General Config")] public float moveSpeed = 5f; // Velocidad base de movimiento public float jumpForce = 5f; // Fuerza del salto normal public float risingGravity = -15f; // Fuerza de gravedad al saltar public float fallingGravity = -25f; // Fuerza de gravedad al caer public Transform cameraTransform; // Transform de la cámara principal public float groundCheckRadius = 0.4f; // Radio para detección de suelo [Header("Components")] public Transform groundCheck; // Objeto para detectar suelo public LayerMask groundMask; // Capas consideradas como suelo [Header("Abilities")] public bool canDoubleJump = false; // Doble salto public bool canDash = false; // Dash o movimiento rápido public bool canWallJump = false; // Salto en pared public bool canWallRun = false; // Correr en paredes public bool canGlide = false; // Planear public bool canSprint = false; // Sprint o Carrera acelerada [Header("Abilities Values")] public float sprintSpeed = 10f; // Velocidad durante el sprint public float dashSpeed = 30f; // Velocidad del dash public float dashDuration = 0.15f; // Duración del dash public float dashCooldown = 0.75f; // Tiempo entre dashes public bool allowAirDash = true; // ¿Permitir dash en el aire? public float wallJumpForce = 15f; // Fuerza del salto en pared public float wallRunGravity = 1f; // Gravedad reducida en wall run public float wallRunSpeed = 11f; // Velocidad durante wall run public float glideFallSpeed = -1.75f; // Velocidad de caída al planear // ================================================= // VARIABLES PRIVADAS // ================================================= private CharacterController controller; // Componente CharacterController private Vector3 velocity; // Velocidad actual del jugador private bool isGrounded; // ¿Está tocando el suelo? private int jumpCount = 0; // Contador para doble salto private bool isDashing = false; // ¿Actualmente en dash? private float dashTimer = 0f; // Temporizador para el dash private bool isWallRunning = false; // ¿Actualmente en wall run? private bool isGliding = false; // ¿Actualmente planeando? private Vector3 wallNormal; // Normal de la pared actual private bool isTouchingWall = false; // ¿Tocando una pared? private float lastDashTime = -10f; // Tiempo del último dash private bool hasAirDashed = false; // ¿Ya se usó dash en el aire? // ================================================= // INICIALIZACIÓN // ================================================= void Start() { //Asignamos el componente Character Controller a una variable para poder manipularlo controller = GetComponent<CharacterController>(); //Hacemos que el cursor sea invisible Cursor.lockState = CursorLockMode.Locked; Cursor.visible = false; } // ================================================= // BUCLE PRINCIPAL // ================================================= void Update() { // Verificar estados básicos CheckGround(); CheckWalls(); // Manejar habilidades HandleMovement(); HandleJump(); HandleDash(); HandleWallRun(); HandleGlide(); } // ================================================= // MÉTODOS DE DETECCIÓN // ================================================= /// Verifica si el jugador está tocando el suelo private void CheckGround() { // Detectar colisión con el suelo usando una esfera física // - Parámetros: // * Posición: Usa la posición del objeto groundCheck (pies del personaje) // * Radio: groundCheckRadius (tamaño de la esfera de detección) // * Capa: groundMask (solo detecta colisiones con capas definidas como suelo) isGrounded = Physics.CheckSphere(groundCheck.position, groundCheckRadius, groundMask); // Si está en el suelo, resetear variables de estado if (isGrounded) { // Resetear contador de saltos (permite nuevo salto básico) jumpCount = 0; // Reactivar dash aéreo (permite usar dash en el próximo salto) hasAirDashed = false; // Resetear velocidad horizontal solo si NO está en dash // - Evita interrumpir el impulso del dash // - Mantiene momentum si está dashando en el suelo if(!isDashing){ velocity.x = 0; // Detener movimiento en eje X velocity.z = 0; // Detener movimiento en eje Z } } } /// Detecta paredes cercanas en 4 direcciones principales y almacena su normal usando SphereCast private void CheckWalls() { isTouchingWall = false; // Inicializar estado de contacto con pared float detectionDistance = 1.2f; // Configurar parámetros de detección // Direcciones a verificar (frontal, derecha, izquierda, trasera) Vector3[] directions = new Vector3[] { transform.forward, // Dirección frontal (Z positivo) transform.right, // Dirección derecha (X positivo) -transform.right, // Dirección izquierda (X negativo) -transform.forward // Dirección trasera (Z negativo) }; // Recorrer todas las direcciones definidas RaycastHit hit; foreach(Vector3 dir in directions) { // Realizar SphereCast en dirección actual if(Physics.SphereCast( transform.position, // Origen en centro del personaje 0.5f, // Radio de la esfera (mitad del ancho del personaje) dir, // Dirección actual del chequeo out hit, // Información de la colisión detectionDistance, // Distancia máxima de detección groundMask // Solo detectar capas definidas como suelo/pared ) && Vector3.Angle(hit.normal, Vector3.up) > 50f) // Excluir pisos // Filtrar superficies planas (pisos/techos) // - Calcula ángulo entre la normal de la superficie y el eje vertical // - Si >50°, se considera pared (evita detectar pisos inclinados) { isTouchingWall = true; // Indicar contacto con pared wallNormal = hit.normal; // Almacenar normal de la pared return; // Salir del método tras primera detección válida } } } // ================================================= // SISTEMA DE MOVIMIENTO // ================================================= /// Maneja el movimiento básico y el sprint private void HandleMovement() { if (isDashing) return; // Ignorar movimiento normal durante el dash // Obtener inputs de ejes horizontales/verticales (teclas WASD o joystick y Valores entre [-1, 1]) float horizontal = Input.GetAxis("Horizontal"); float vertical = Input.GetAxis("Vertical"); // Calcular dirección de movimiento relativa a la cámara // - Convierte inputs en vector 3D orientado según la rotación de la cámara Vector3 moveDirection = CalculateCameraMovement(horizontal, vertical); // Determinar velocidad actual considerando sprint // - Usa sprintSpeed si se mantiene el botón de sprint y está habilitado // - Mantiene moveSpeed como valor base en otros casos float currentSpeed = canSprint && Input.GetButton("Sprint") ? sprintSpeed : moveSpeed; // Aplicar movimiento horizontal solo si no está en wall run // - Evita conflicto entre sistemas de movimiento if(!isWallRunning) { // Mueve el CharacterController en la dirección calculada // - Multiplica por deltaTime para hacerlo frame-rate independent controller.Move(moveDirection * currentSpeed * Time.deltaTime); } // Calcular gravedad según estado (ascenso/descenso) // - fallingGravity: Gravedad fuerte durante caída o en el aire // - risingGravity: Gravedad suave durante ascenso float currentGravity = (velocity.y < 0 || !isGrounded) ? fallingGravity : risingGravity ; // Aplicar gravedad solo si no está en wall run/glide if(!isWallRunning && !isGliding) { velocity.y += currentGravity * Time.deltaTime; } // Aplicar velocidad vertical acumulada // - Movimiento independiente del horizontal para mejor control controller.Move(velocity * Time.deltaTime); } /// Calcula dirección de movimiento relativa a la cámara private Vector3 CalculateCameraMovement(float horizontal, float vertical) { // Obtener vectores direccionales base de la cámara Vector3 cameraForward = cameraTransform.forward; // Vector hacia donde apunta la cámara Vector3 cameraRight = cameraTransform.right; // Vector derecho de la cámara // Aplanar vectores para ignorar componente vertical (eje Y) // - Evita movimiento no deseado al mirar hacia arriba/abajo // Mantener solo componentes X y Z cameraForward.y = 0; cameraRight.y = 0; // Normalizar vectores para mantener velocidad constante // - Evita que movimientos diagonales sean más rápidos cameraForward.Normalize(); // Longitud = 1 cameraRight.Normalize(); // Combinar direcciones según input del jugador // - Vertical: Controla movimiento adelante/atrás (eje Z) // - Horizontal: Controla movimiento lateral (eje X) Vector3 combinedDirection = (cameraForward * vertical) + (cameraRight * horizontal); // Normalizar resultado final para: // - Mantener velocidad constante en todas las direcciones // - Evitar valores mayores a 1 en movimiento diagonal return combinedDirection.normalized; } // ================================================= // SISTEMA DE SALTO // ================================================= /// Maneja todos los tipos de salto private void HandleJump() { // Detectar que el botón de Salto ha sido oprimido if(Input.GetButtonDown("Jump")) { isGliding = false; // Cancelar cualquier estado de planeo activo // Salto normal/doble salto if(isGrounded || (canDoubleJump && jumpCount < 1)) { // Fórmula física para altura de salto: v = √(2 * fuerza * gravedad) // - risingGravity se usa para cálculo consistente con el sistema de gravedad velocity.y = Mathf.Sqrt(jumpForce * -2 * risingGravity); jumpCount++; // Incrementar contador de saltos aéreos } // Wall jump else if(canWallJump && isTouchingWall) { // Calcular dirección óptima (alejarse de la pared + vertical) // - Vector.up * 2: Impulso vertical predominante // - wallNormal: Dirección perpendicular a la pared para empuje horizontal Vector3 jumpDirection = (Vector3.up * 2f + wallNormal).normalized; // Aplicar fuerza con impulso velocity = jumpDirection * wallJumpForce; // Resetear estados jumpCount = 0; hasAirDashed = false; // Reactivar dash aéreo // Rotar personaje para mirar en dirección opuesta a la pared // - LookRotation(-wallNormal): Crea rotación alejándose de la pared transform.rotation = Quaternion.LookRotation(-wallNormal); } } } // ================================================= // SISTEMA DE DASH // ================================================= /// Maneja el movimiento de dash private void HandleDash() { // ¿Puede dashar en el aire? (Habilidad activa + en aire + no ha usado dash aéreo) bool canAirDash = allowAirDash && !isGrounded && !hasAirDashed; // ¿Puede dashar en suelo? (En suelo + cooldown completado) bool canGroundDash = isGrounded && Time.time >= lastDashTime + dashCooldown; // Iniciar Dash si se oprime el botón y se cumplen las condiciones if (canDash && Input.GetButtonDown("Fire2") && (canGroundDash || canAirDash)) { // Configurar variables isDashing = true; // Activar flag de dash dashTimer = dashDuration; // Iniciar temporizador lastDashTime = Time.time; // Registrar momento del último dash // Registrar dash aéreo (solo una vez por salto) if(!isGrounded) hasAirDashed = true; // Calcular dirección del dash Vector3 dashDirection = GetDashDirection(); // Aplicar fuerza del dash (combinar con velocidad vertical existente) velocity = dashDirection * dashSpeed; // Velocidad en dirección calculada velocity.y = isGrounded ? 0 : velocity.y; // En suelo: dash puramente horizontal } // Ejecutar lógica durante el dash if (isDashing) { dashTimer -= Time.deltaTime; // Reducir temporizador // Finalizar dash cuando el tiempo expire if (dashTimer <= 0) { isDashing = false; velocity = Vector3.zero; // Eliminar velocidad residual } //Aplicar movimiento del dash (Movimiento frame-rate independent) controller.Move(velocity * Time.deltaTime); } } // Calcula la dirección del dash basada en el input y la cámara private Vector3 GetDashDirection() { // Obtener input RAW (sin suavizado de Unity) para dirección inmediata float horizontal = Input.GetAxisRaw("Horizontal"); // Valores: -1, 0, 1 float vertical = Input.GetAxisRaw("Vertical"); // Valores: -1, 0, 1 // Calcular dirección relativa a la cámara (igual que el movimiento normal) Vector3 cameraForward = cameraTransform.forward; Vector3 cameraRight = cameraTransform.right; // Ignorar componente vertical para movimiento horizontal cameraForward.y = 0f; cameraRight.y = 0f; // Normalizar vectores para mantener velocidad constante cameraForward.Normalize(); cameraRight.Normalize(); // Combinar direcciones según input y normalizar para evitar velocidad extra en diagonales Vector3 dashDirection = (cameraForward * vertical + cameraRight * horizontal).normalized; // Si no hay input, usar dirección frontal de la cámara if(dashDirection == Vector3.zero) { dashDirection = cameraForward; // Dash hacia adelante por defecto } return dashDirection; } // ================================================= // SISTEMA DE WALL RUN // ================================================= /// Maneja el movimiento en paredes private void HandleWallRun() { // Iniciar el wall run si se oprime el botón correspondiente y se cumplen las condiciones if(canWallRun && !isGrounded && isTouchingWall && Input.GetButton("Sprint")) { isWallRunning = true; // Rotación automática hacia la pared: // - Interpola suavemente la rotación actual hacia la dirección opuesta a la normal de la pared // - Quaternion.LookRotation(-wallNormal): Crea rotación mirando en dirección contraria a la pared // - 10f * Time.deltaTime: Velocidad de rotación (10 grados/segundo) transform.rotation = Quaternion.Lerp( transform.rotation, Quaternion.LookRotation(-wallNormal), 10f * Time.deltaTime ); // Calcular direcciones de movimiento: // Dirección "frontal" paralela a la pared (90° respecto a la normal) Vector3 wallForward = Vector3.Cross(wallNormal, Vector3.up); // Proyectar dirección de la cámara sobre el plano de la pared // - Vector3.ProjectOnPlane: Elimina componente perpendicular a la pared // - .normalized: Mantiene velocidad constante Vector3 cameraRelative = Vector3.ProjectOnPlane(cameraTransform.forward,wallNormal).normalized; // Mover en dirección combinada (pared + input) float horizontal = Input.GetAxis("Horizontal"); float vertical = Input.GetAxis("Vertical"); // Combinar direcciones: // - vertical * cameraRelative: Movimiento hacia adelante/atrás relativo a la cámara // - horizontal * transform.right: Movimiento lateral relativo a la rotación del personaje Vector3 moveDirection = (cameraRelative * vertical + transform.right * horizontal) * wallRunSpeed; // Aplicar movimiento: controller.Move(moveDirection * Time.deltaTime); // Movimiento horizontal // Configurar gravedad vertical personalizada: // - Valor negativo para mantener al personaje "pegado" a la pared // - wallRunGravity controla la velocidad de deslizamiento velocity.y = -wallRunGravity; } else { isWallRunning = false; // Desactivar wall run si no se cumplen las condiciones } } // ================================================= // SISTEMA DE PLANEO // ================================================= /// Maneja el planeo aéreo private void HandleGlide() { //Iniciar el planeo si se mantiene oprimido el botón correspondiente y se cumplen las condiciones requeridas if (canGlide && !isGrounded && Input.GetButton("Jump") && !isGliding && velocity.y<0) { isGliding = true; velocity.y = glideFallSpeed; //Se modifica la velocidad en el eje vertical para usar el valor definido } // Cancelar planeo al tocar el suelo o soltar el botón else if (isGrounded || Input.GetButtonUp("Jump")) { isGliding = false; } } }
2.2 Configurando Controles
Como pudiste ver en la sección anterior, las habilidades especiales se activan al oprimir botones específicos:
Botón “Jump” → Activa “Saltar, Doble Salto, Salto en Muro y Planear” → Oprimiendo “Barra Espaciadora” (en teclado) o “Botón Y” (en gamepad).
Botón “Sprint” → Activa “Correr y Correr en Muro” → Oprimiendo “Shift Izquierdo” (en teclado) o “Botón del Joystick Izquierdo” (en gamepad).
Botón “Fire2” → Activa “Dash” → Oprimiendo “Alt Izquierdo” (en teclado) o “Botón B” (en gamepad).
Tanto “Jump” como “Fire2” ya están previamente configurados en Unity 6. A continuación veremos como configurar “Sprint“:
Primero, vamos a abrir los “Project Settings” de Unity (Edit → Project Settings).
Dentro de “Project Settings” hacemos clic en “Input Manager“. Dentro de “Input Manager” buscamos el parámetro “Size” y cambiamos el valor actual para añadirle “dos unidades más“, es decir, si actualmente el valor es “30“, lo vamos a cambiar a “32“. Esto creará dos elementos más al final de la lista los cuales serán usados para configurar “Sprint“.
A los dos elementos los vamos a llamar “Sprint” y cada uno tendrá la siguiente configuración:
2.3 Últimos ajustes.
Antes de probar nuestro juego es necesario que asignes los componentes y capas requeridas en el componente de “PlayerController” como se hizo en el tutorial anterior y, también es necesario que añadas más obstáculos de distintos tamaños que te permitan hacer uso de las nuevas habilidades. Así que, “Hazlo Yá” 😀
Por cierto, no olvides asignarles a los nuevos obstáculos la capa “ground3D” para que el juego los pueda reconocer como objetos en los cuales se activen las habilidades especiales.
¡Ahora sí, el Momento ha llegado, Oprime Play y disfruta tu Primer Juego 3D Supercargado 🙂 !
Ejercicios.
Para reforzar lo aprendido, es necesario practicarlo, por ello intenta realizar los siguientes ejercicios:
- Modifica valores en el componente “Player Controller” para que tu personaje se mueva más rápido, salte más alto y caiga más lento al planear.
- Modifica valores en los componentes “Cinemachine Orbital Follow” y “Cinemachine Rotation Composer” que se encuentran en el objeto “FreeLook Camera” para que experimentes con distintos enfoques de tu cámara en tercera persona.
- Crea una nueva escena con muchos tipos de obstáculos para que puedas probar todas las habilidades como en “Los Juegos de Plataformas“.
- Crea más elementos dentro del “Input Manager” para modificar los controles del juego (por ejemplo, que puedas planear usando otro botón diferente al del salto).
Este Tutorial de Unity termina aquí. Esperamos que te haya sido de mucha utilidad y que hayas creado “Tu Primer Juego 3D”.
Unity Tutorial: “Tu Primer Juego 3D“
1. Creando un Juego 3D
2. Supercargando el Juego
Ver más Tutoriales