paint-brush
Cómo crear un controlador de personajes 2D en Unity: Parte 2por@deniskondratev
108 lecturas

Cómo crear un controlador de personajes 2D en Unity: Parte 2

por Denis Kondratev17m2024/12/08
Read on Terminal Reader

Demasiado Largo; Para Leer

Este artículo muestra cómo mejorar un controlador de personajes Unity 2D, cubriendo una nueva configuración del sistema de entrada, mecánicas de salto mejoradas y seguimiento de cámara sin interrupciones.
featured image - Cómo crear un controlador de personajes 2D en Unity: Parte 2
Denis Kondratev HackerNoon profile picture

En este artículo, continuamos desarrollando un controlador de personaje para un juego de plataformas 2D en Unity, examinando exhaustivamente cada paso de configuración y optimización de los controles.


En el artículo anterior, “ Cómo crear un controlador de personajes 2D en Unity: Parte 1 ”, analizamos en detalle cómo crear la base del personaje, incluido su comportamiento físico y movimiento básico. Ahora, es momento de pasar a aspectos más avanzados, como el manejo de entradas y el seguimiento dinámico de la cámara.


En este artículo, profundizaremos en la configuración del nuevo sistema de entrada de Unity, la creación de acciones activas para controlar el personaje, la habilitación de saltos y la garantía de respuestas adecuadas a los comandos del jugador.

Si quieres implementar tú mismo todos los cambios descritos en este artículo, puedes descargar la rama del repositorio “ Character Body ”, que contiene la base de este artículo. Alternativamente, puedes descargar la rama “ Character Controller ” con el resultado final.

Configuración del sistema de entrada

Antes de empezar a escribir el código para controlar a nuestro personaje, debemos configurar el sistema de entrada en el proyecto. Para nuestro juego de plataformas, hemos elegido el nuevo sistema de entrada de Unity, introducido hace unos años, que sigue siendo relevante debido a sus ventajas sobre el sistema tradicional.


El sistema de entrada ofrece un enfoque más modular y flexible para el manejo de entrada, lo que permite a los desarrolladores configurar fácilmente controles para varios dispositivos y admitir escenarios de entrada más complejos sin sobrecarga de implementación adicional.


En primer lugar, instale el paquete Input System. Abra el Administrador de paquetes desde el menú principal seleccionando Ventana → Administrador de paquetes. En la sección Registro de Unity, busque el paquete "Input System" y haga clic en "Instalar".

A continuación, acceda a la configuración del proyecto a través del menú Editar → Configuración del proyecto. Seleccione la pestaña Reproductor, busque la sección Manejo de entrada activa y configúrela en "Paquete del sistema de entrada (nuevo)".

Luego de completar estos pasos, Unity te pedirá que reinicies. Una vez reiniciado, todo estará listo para configurar los controles de nuestro capitán.

Creación de acciones de entrada

En la carpeta Configuración , cree acciones de entrada a través del menú principal: Activos → Crear → Acciones de entrada . Nombre el archivo "Controles".

El sistema de entrada de Unity es una herramienta de gestión de entrada potente y flexible que permite a los desarrolladores configurar controles para personajes y elementos del juego. Admite varios dispositivos de entrada. Las acciones de entrada que crea proporcionan una gestión de entrada centralizada, lo que simplifica la configuración y hace que la interfaz sea más intuitiva.


Haga doble clic en el archivo Controles para abrirlo y editarlo y agregue un Mapa de acción para el control del personaje llamado "Personaje".

Un mapa de acciones en Unity es una colección de acciones que se pueden vincular a varios controladores y teclas para realizar tareas específicas en el juego. Es una forma eficiente de organizar los controles, lo que permite a los desarrolladores asignar y ajustar las entradas sin tener que reescribir el código. Para obtener más detalles, consulte la documentación oficial del sistema de entrada .


La primera acción se llamará "Mover". Esta acción definirá la dirección del movimiento del personaje. Establezca el tipo de acción en "Valor" y el tipo de control en "Vector2" para habilitar el movimiento en cuatro direcciones.

Asigne enlaces a esta acción seleccionando Agregar compuesto arriba/abajo/derecha/izquierda y asignando las conocidas teclas WASD a sus respectivas direcciones.

No olvides guardar tu configuración haciendo clic en Guardar activo . Esta configuración garantiza que puedas reasignar asignaciones para la acción "Mover", por ejemplo, a las teclas de flecha o incluso a un joystick de gamepad.


A continuación, agregue una nueva acción: "Saltar". Mantenga el tipo de acción como "Botón", pero agregue una nueva interacción : "Presionar", y configure el comportamiento del disparador como "Presionar y soltar", ya que necesitamos capturar tanto la presión como la liberación del botón.

Con esto se completa el esquema de control de caracteres. El siguiente paso es escribir un componente para gestionar estas acciones.

Mover el personaje hacia la izquierda y hacia la derecha

Es hora de vincular las acciones de entrada que creamos para el control del personaje al componente CharacterBody , permitiendo que el personaje se mueva activamente por la escena de acuerdo con nuestros comandos de control.


Para ello, crearemos un script responsable del control de movimiento y lo llamaremos CharacterController para mayor claridad. En este script, primero definiremos algunos campos básicos. Agregaremos una referencia al componente CharacterBody , _characterBody , que será controlado directamente por el script.


También estableceremos parámetros para la velocidad de movimiento del personaje ( _speed ) y la altura del salto ( _jumpHeight ). Además, definiremos el propósito del campo _stopJumpFactor .


Es posible que hayas notado que en muchos juegos de plataformas en 2D se puede controlar la altura del salto. Cuanto más tiempo se mantenga presionado el botón de salto, más alto saltará el personaje. Básicamente, se aplica una velocidad ascendente inicial al comienzo del salto, y esta velocidad se reduce cuando se suelta el botón. El _stopJumpFactor determina cuánto disminuye la velocidad ascendente al soltar el botón de salto.


Aquí hay un ejemplo del código que escribiremos:


 // CharacterController.cs public class CharacterController : MonoBehaviour { [SerializeField] private CharacterBody _characterBody; [Min(0)] [SerializeField] private float _speed = 5; [Min(0)] [SerializeField] private float _jumpHeight = 2.5f; [Min(1)] [SerializeField] private float _stopJumpFactor = 2.5f; }


A continuación, implementaremos la capacidad de mover al personaje hacia la izquierda y hacia la derecha. Al mantener presionado el botón de movimiento, el personaje debe mantener la velocidad de movimiento especificada independientemente de los obstáculos. Para lograr esto, agregaremos una variable en el script para almacenar la velocidad de movimiento actual a lo largo de la superficie (o simplemente horizontalmente cuando el personaje está en el aire):


 // CharacterController.cs private float _locomotionVelocity;


En el componente CharacterBody , presentaremos un método para establecer esta velocidad:


 // CharacterBody.cs public void SetLocomotionVelocity(float locomotionVelocity) { Velocity = new Vector2(locomotionVelocity, _velocity.y); }


Dado que nuestro juego no tiene superficies inclinadas, este método es bastante simple. En escenarios más complejos, tendríamos que tener en cuenta el estado del cuerpo y la pendiente de la superficie. Por ahora, simplemente conservamos el componente vertical de la velocidad y modificamos solo la coordenada x horizontal.


A continuación, estableceremos este valor en el método Update en cada cuadro:


 // CharacterController.cs private void Update() { _characterBody.SetLocomotionVelocity(_locomotionVelocity); }


Definiremos un método para manejar las señales de la acción de entrada Move :


 // CharacterController.cs public void OnMove(InputAction.CallbackContext context) { var value = context.ReadValue<Vector2>(); _locomotionVelocity = value.x * _speed; }


Dado que la acción Move se define como un Vector2 , el contexto proporcionará un valor vectorial según las teclas que se presionen o suelten. Por ejemplo, si se presiona la tecla D , el método OnMove recibirá el vector (1, 0). Si se presionan D y W simultáneamente, se obtendrá (1, 1). Si se sueltan todas las teclas, se activará OnMove con el valor (0, 0).


Para la tecla A , el vector será (-1, 0). En el método OnMove , tomamos el componente horizontal del vector recibido y lo multiplicamos por la velocidad de movimiento especificada, _speed .

Enseñar al personaje a saltar

Primero, debemos enseñarle al componente CharacterBody a manejar los saltos. Para ello, agregaremos un método responsable del salto:


 // CharacterBody.cs public void Jump(float jumpSpeed) { Velocity = new Vector2(_velocity.x, jumpSpeed); State = CharacterState.Airborne; }


En nuestro caso, este método es sencillo: establece la velocidad vertical y cambia inmediatamente el estado del personaje a Airborne .


A continuación, debemos determinar la velocidad a la que debe saltar el personaje. Ya hemos definido la altura del salto y sabemos que la gravedad actúa constantemente sobre el cuerpo. En base a esto, la velocidad inicial del salto se puede calcular mediante la fórmula:



Donde h es la altura del salto y g es la aceleración gravitacional. También tendremos en cuenta el multiplicador de gravedad presente en el componente CharacterBody . Agregaremos un nuevo campo para definir la velocidad de salto inicial y la calcularemos de la siguiente manera:


 // CharacterController.cs private float _jumpSpeed; private void Awake() { _jumpSpeed = Mathf.Sqrt(2 * Physics2D.gravity.magnitude * _characterBody.GravityFactor * _jumpHeight); }


Necesitaremos otro campo para rastrear si el personaje está saltando actualmente, para que podamos limitar la velocidad del salto en el momento apropiado.


Además, si el jugador mantiene presionado el botón de salto hasta aterrizar, nosotros mismos deberíamos restablecer esta bandera. Esto se hará en el método Update :


 // CharacterController.cs private bool _isJumping; private void Update() { if (_characterBody.State == CharacterState.Grounded) { _isJumping = false; } //... }


Ahora, escribamos el método para manejar la acción Jump :


 // CharacterController.cs public void OnJump(InputAction.CallbackContext context) { if (context.started) { Jump(); } else if (context.canceled) { StopJumping(); } }


Como la acción Jump es un botón, podemos determinar a partir del contexto si la pulsación del botón ha comenzado ( context.started ) o finalizado ( context.canceled ). En función de esto, iniciamos o detenemos el salto.


Aquí está el método para ejecutar el salto:


 // CharacterController.cs private void Jump() { if (_characterBody.State == CharacterState.Grounded) { _isJumping = true; _characterBody.Jump(_jumpSpeed); } }


Antes de saltar, comprobamos si el personaje está en el suelo. Si es así, activamos el indicador _isJumping y hacemos que el cuerpo salte con _jumpSpeed .

Ahora, implementemos el comportamiento de dejar de saltar:


 // CharacterController.cs private void StopJumping() { var velocity = _characterBody.Velocity; if (_isJumping && velocity.y > 0) { _isJumping = false; _characterBody.Velocity = new Vector2( velocity.x, velocity.y / _stopJumpFactor); } }


Detenemos el salto solo si el indicador _isJumping está activo. Otra condición importante es que el personaje debe estar moviéndose hacia arriba. Esto evita limitar la velocidad de caída si se suelta el botón de salto mientras se mueve hacia abajo. Si se cumplen todas las condiciones, reiniciamos el indicador _isJumping y reducimos la velocidad vertical por un factor de _stopJumpFactor .

Configuración del personaje

Ahora que todos los componentes están listos, agrega los componentes PlayerInput y CharacterController al objeto Captain en la escena. Asegúrate de seleccionar el componente CharacterController que creamos, no el componente estándar de Unity diseñado para controlar personajes 3D.


Para CharacterController , asigne el componente CharacterBody existente del personaje. Para PlayerInput , configure los controles creados previamente en el campo Actions .

A continuación, configure el componente PlayerInput para llamar a los métodos adecuados desde CharacterController. Expanda las secciones Eventos y Personaje en el editor y vincule los métodos correspondientes a las acciones Mover y Saltar.

Ahora, todo está listo para ejecutar el juego y probar cómo funcionan juntos todos los componentes configurados.


Movimiento de cámara

Ahora, necesitamos que la cámara siga al personaje a donde quiera que vaya. Unity ofrece una herramienta poderosa para la gestión de cámaras: Cinemachine .


Cinemachine es una solución revolucionaria para el control de cámaras en Unity que ofrece a los desarrolladores una amplia gama de capacidades para crear sistemas de cámara dinámicos y bien ajustados que se adaptan a las necesidades del juego. Esta herramienta facilita la implementación de técnicas de cámara complejas, como el seguimiento de personajes, el ajuste automático del enfoque y mucho más, agregando vitalidad y riqueza a cada escena.


Primero, ubique el objeto Cámara principal en la escena y agréguele el componente CinemachineBrain .

A continuación, crea un nuevo objeto en la escena llamado CaptainCamera . Esta será la cámara que seguirá al capitán, como un camarógrafo profesional. Agrega el componente CinemachineVirtualCamera . Establece el campo Follow en el capitán, elige Framing Transposer para el campo Body y establece el parámetro Lens Ortho Size en 4.


Además, necesitaremos otro componente para definir el desplazamiento de la cámara en relación con el personaje: CinemachineCameraOffset . Establezca el valor Y en 1,5 y el valor Z en -15.

Ahora, probemos cómo la cámara sigue a nuestro personaje.



Creo que quedó bastante bien. Noté que la cámara a veces se traba un poco. Para solucionarlo, establecí el campo Método de actualización de mezcla del objeto Cámara principal en Actualización fija.

Mejorando los saltos

Probemos la mecánica actualizada. Prueba a correr y saltar continuamente. Los jugadores experimentados pueden notar que los saltos no siempre se registran. En la mayoría de los juegos, esto no es un problema.


Resulta que es difícil predecir el momento exacto en el que se debe presionar el botón de salto nuevamente. Necesitamos hacer que el juego sea más indulgente y permitir que los jugadores presionen el botón de salto ligeramente antes de aterrizar y que el personaje salte inmediatamente después de aterrizar. Este comportamiento se alinea con lo que los jugadores están acostumbrados.


Para implementar esto, introduciremos una nueva variable, _jumpActionTime , que representa la ventana de tiempo durante la cual aún se puede activar un salto si surge la oportunidad.


 // CharacterController.cs [Min(0)] [SerializeField] private float _jumpActionTime = 0.1f;


Agregué un campo _jumpActionEndTime , que marca el final de la ventana de acción de salto. En otras palabras, hasta que se alcance _jumpActionEndTime , el personaje saltará si surge la oportunidad. Actualicemos también el controlador de acción Jump .


 // CharacterController.cs private float _jumpActionEndTime; public void OnJump(InputAction.CallbackContext context) { if (context.started) { if (_characterBody.State == CharacterState.Grounded) { Jump(); } else { _jumpActionEndTime = Time.unscaledTime + _jumpActionTime; } } else if (context.canceled) { StopJumping(); } }


Al pulsar el botón de salto, si el personaje está en el suelo, salta inmediatamente. De lo contrario, almacenamos el intervalo de tiempo durante el cual aún puede realizar el salto.


Eliminaremos la comprobación del estado Grounded del propio método Jump .


 // CharacterController.cs private void Jump() { _isJumping = true; _characterBody.Jump(_jumpSpeed); }


También adaptaremos el método de parada de salto. Si se soltó el botón antes de aterrizar, no debería producirse ningún salto, por lo que reiniciamos _jumpActionEndTime .


 // CharacterController.cs private void StopJumping() { _jumpActionEndTime = 0; //... }


¿Cuándo debemos comprobar que el personaje ha aterrizado y activar un salto? El estado CharacterBody se procesa en FixedUpdate , mientras que el procesamiento de la acción se produce más tarde. Independientemente de si se trata de Update o FixedUpdate , puede producirse un retraso de un fotograma entre el aterrizaje y el salto, lo cual es notable.


Agregaremos un evento StateChanged a CharacterBody para responder instantáneamente al aterrizaje. El primer argumento será el estado anterior y el segundo será el estado actual.


 // CharacterBody.cs public event Action<CharacterState, CharacterState> StateChanged;


Ajustaremos la gestión del estado para activar el evento de cambio de estado y reescribiremos FixedUpdate .


 // CharacterBody.cs [field: SerializeField] private CharacterState _state; public CharacterState State { get => _state; private set { if (_state != value) { var previousState = _state; _state = value; StateChanged?.Invoke(previousState, value); } } }


También perfeccioné cómo se maneja surfaceHit en FixedUpdate .


 // CharacterBody.cs private void FixedUpdate() { //... if (_velocity.y <= 0 && slideResults.surfaceHit) { var surfaceHit = slideResults.surfaceHit; Velocity = ClipVector(_velocity, surfaceHit.normal); if (surfaceHit.normal.y >= _minGroundVertical) { State = CharacterState.Grounded; return; } } State = CharacterState.Airborne; }


En CharacterController , nos suscribiremos al evento StateChanged y agregaremos un controlador.


 // CharacterController.cs private void OnEnable() { _characterBody.StateChanged += OnStateChanged; } private void OnDisable() { _characterBody.StateChanged -= OnStateChanged; } private void OnStateChanged(CharacterState previousState, CharacterState state) { if (state == CharacterState.Grounded) { OnGrounded(); } }


Eliminaremos la verificación del estado Grounded de Update y la moveremos a OnGrounded .


 // CharacterController.cs private void Update() { _characterBody.SetLocomotionVelocity(_locomotionVelocity); } private void OnGrounded() { _isJumping = false; }


Ahora, agregue el código para verificar si se debe activar un salto.


 // CharacterController.cs private void OnGrounded() { _isJumping = false; if (_jumpActionEndTime > Time.unscaledTime) { _jumpActionEndTime = 0; Jump(); } }


Si _jumpActionEndTime es mayor que la hora actual, significa que el botón de salto se presionó recientemente, por lo que reiniciamos _jumpActionEndTime y realizamos el salto.


Ahora, intenta saltar continuamente con el personaje. Notarás que el botón de salto responde mejor y que controlar al personaje se vuelve más fluido. Sin embargo, observé que en ciertas situaciones, como la esquina que se muestra en la ilustración a continuación, el estado Grounded experimenta un ligero retraso, lo que interrumpe la cadena de saltos.

Para solucionar este problema, establecí el campo Surface Anchor en el componente CharacterBody en 0,05 en lugar de 0,01. Este valor representa la distancia mínima a una superficie para que el cuerpo entre en el estado Grounded .

Salto desde el acantilado

Es posible que hayas notado que intentar saltar mientras corres por superficies verticales no siempre funciona. A veces, puedes sentir que el botón de salto no responde.


Esta es una de las sutilezas del desarrollo de un controlador de personajes para plataformas 2D. Los jugadores necesitan la capacidad de saltar incluso cuando se demoran un poco en presionar el botón de salto. Si bien este concepto puede parecer extraño, es así como funcionan la mayoría de los juegos de plataformas. El resultado es un personaje que parece impulsarse en el aire, como se muestra en la animación a continuación.



Implementaremos esta mecánica. Introduciremos un nuevo campo para almacenar la ventana de tiempo (en segundos) durante la cual el personaje aún puede saltar después de perder el estado Grounded .


 // CharacterController.cs [Min(0)] [SerializeField] private float _rememberGroundTime = 0.1f;


También agregaremos otro campo para almacenar la marca de tiempo después de la cual se "olvida" el estado Grounded .


 // CharacterController.cs private float _lostGroundTime;


Este estado se rastreará mediante el evento CharacterBody . Ajustaremos el controlador OnStateChanged para este propósito.


 // CharacterController.cs private void OnStateChanged(CharacterState previousState, CharacterState state) { if (state == CharacterState.Grounded) { OnGrounded(); } else if (previousState == CharacterState.Grounded) { _lostGroundTime = Time.unscaledTime + _rememberGroundTime; } }


Es importante distinguir si el personaje perdió el estado Grounded debido a un salto intencional o por otra razón. Ya tenemos el indicador _isJumping , que se desactiva cada vez que se llama StopJumping para evitar acciones redundantes.


Decidí no introducir otra bandera, ya que la cancelación redundante de saltos no afecta la jugabilidad. Siéntete libre de experimentar. La bandera _isJumping ahora solo se borrará cuando el personaje aterrice después de saltar. Actualicemos el código en consecuencia.


 // CharacterController.cs private void StopJumping() { _jumpActionEndTime = 0; var velocity = _characterBody.Velocity; if (_isJumping && velocity.y > 0) { _characterBody.Velocity = new Vector2( velocity.x, velocity.y / _stopJumpFactor); } }


Por último, revisaremos el método OnJump .


 // CharacterController.cs public void OnJump(InputAction.CallbackContext context) { if (context.started) { if (_characterBody.State == CharacterState.Grounded || (!_isJumping && _lostGroundTime > Time.unscaledTime)) { Jump(); } else { _jumpActionEndTime = Time.unscaledTime + _jumpActionTime; } } else if (context.canceled) { StopJumping(); } }


Ahora, saltar desde superficies verticales ya no interrumpe el ritmo del juego y resulta mucho más natural, a pesar de su aparente absurdo. El personaje puede literalmente impulsarse desde el aire, yendo más lejos de lo que parece lógico. Pero esto es exactamente lo que se necesita para nuestro juego de plataformas.

Cambio de personaje

El toque final es hacer que el personaje mire en la dirección del movimiento. Lo haremos de la forma más sencilla: cambiando la escala del personaje a lo largo del eje x. Si se establece un valor negativo, nuestro capitán mirará en la dirección opuesta.

Primero, almacenemos la escala original en caso de que difiera de 1.


 // CharacterController.cs public class CharacterController : MonoBehaviour { //... private Vector3 _originalScale; private void Awake() { //... _originalScale = transform.localScale; } }


Ahora, al movernos hacia la izquierda o hacia la derecha, aplicaremos una escala positiva o negativa.


 // CharacterController.cs public class CharacterController : MonoBehaviour { public void OnMove(InputAction.CallbackContext context) { //... // Change character's direction. if (value.x != 0) { var scale = _originalScale; scale.x = value.x > 0 ? _originalScale.x : -_originalScale.x; transform.localScale = scale; } } }


Probemos el resultado.


Terminando

Este artículo resultó ser bastante detallado, pero logramos cubrir todos los aspectos esenciales del control de personajes en un juego de plataformas en 2D. Como recordatorio, puedes consultar el resultado final en la rama “ Controlador de personajes ” del repositorio.


Si disfrutaste o te resultó útil este artículo y el anterior, agradecería que me dieras "Me gusta" y que le dieras estrellas en GitHub. No dudes en comunicarte conmigo si tienes algún problema o encuentras errores. ¡Gracias por tu atención!