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: Principiante-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í. Acompáñanos en el siguiente tutorial donde añadiremos más funcionalidades a “Tu Primer Juego 3D”.
Siguiente Tutorial de Unity: “3. Añadiendo Plataformas“
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






















