Uno de los elementos clave de cualquier juego de plataformas 2D es el personaje principal. La forma en que se mueve y se controla da forma significativa a la atmósfera del juego, ya sea un juego acogedor de la vieja escuela o un slasher dinámico. Por lo tanto, la creación de un controlador de personaje es una etapa inicial importante en el desarrollo de un juego de plataformas. En este artículo, examinaremos a fondo el proceso de creación de un personaje desde cero, enseñándole a moverse por el nivel respetando las leyes de la física. Incluso si ya tiene experiencia en la creación de controladores de personajes, le interesará conocer las innovaciones de Unity 2023. Para mi sorpresa, se ha agregado un método tan esperado para el componente , que simplifica enormemente la escritura de un controlador de personajes al permitiendo el uso de en modo cinemático de manera más efectiva. Anteriormente, toda esta funcionalidad debía implementarse manualmente. Slide Rigidbody2D Rigidbody2D Si no solo quieres leer el artículo sino también probarlo en la práctica, te recomiendo descargar una plantilla de nivel del repositorio de GitHub , donde ya se incluyen los recursos necesarios y un nivel listo para probar a tu personaje. Treasure Hunters Sentando las bases de nuestro carácter Para nuestro juego de plataformas, hemos establecido la regla de que el juego solo tendrá superficies verticales y horizontales, y la fuerza de gravedad se dirigirá estrictamente hacia abajo. Esto simplifica enormemente la creación de un juego de plataformas en la etapa inicial, especialmente si no quieres profundizar en las matemáticas vectoriales. En el futuro, es posible que me desvíe de estas reglas en mi proyecto Treasure Hunters, donde exploro la creación de mecánicas para un juego de plataformas 2D en Unity. Pero ese será el tema de otro artículo. En base a nuestra decisión de que el movimiento se realizará a lo largo de superficies horizontales, la base de nuestro personaje será rectangular. El uso de superficies inclinadas requeriría desarrollar un colisionador en forma de cápsula y mecanismos adicionales como el deslizamiento. Primero, crea un objeto vacío en la escena y llámalo Capitán; este será nuestro personaje principal. Agregue los componentes y al objeto. Establezca el tipo en Cinemático para que podamos controlar el movimiento del personaje mientras seguimos utilizando las capacidades físicas integradas de Unity. Además, bloquea la rotación del personaje a lo largo del eje Z activando la opción Congelar rotación Z. Rigidbody2D BoxCollider2D Rigidbody2D Su configuración debería verse como la ilustración a continuación. Ahora agreguemos una apariencia a nuestro capitán. Busque la textura en Recursos/Texturas/Cazadores de tesoros/Capitán Nariz de payaso/Sprites/Capitán Nariz de payaso/Capitán Nariz de payaso con espada/09-Idle Sword/Idle Sword 01.png y establezca su valor de Píxel por unidad en 32. A menudo lo haremos Utilice este valor en este curso porque nuestras texturas se crean con esta resolución. Establezca el Modo Sprite en Único y, para mayor comodidad, establezca el Pivote en Abajo. No olvide aplicar los cambios haciendo clic en el botón Aplicar. Asegúrese de que todas las configuraciones se realicen correctamente. En este artículo, no tocaremos la animación de personajes, así que por ahora solo usaremos un objeto. En el objeto Capitán, cree un objeto anidado llamado Apariencia y agréguele un componente Sprite Renderer, especificando el sprite configurado previamente. Cuando acerqué la imagen del capitán, noté que estaba bastante borrosa debido a una configuración incorrecta de los sprites. Para solucionar este problema, seleccione la textura Idle Sword 01 en la ventana Proyecto y establezca el Modo de filtro en Punto (sin filtro). Ahora la imagen se ve mucho mejor. Configuración del colisionador de personajes Por el momento, la posición del colisionador y la imagen de nuestro capitán no coinciden, y el colisionador resulta demasiado grande para nuestras necesidades. Arreglemos esto y también posicionemos correctamente el pivote del personaje en la parte inferior. Aplicaremos esta regla a todos los objetos del juego para facilitar su colocación en el mismo nivel sin importar su tamaño. Para facilitar la gestión de este proceso, utilice la herramienta Alternar posición del mango con conjunto de pivote, como se muestra a continuación. El siguiente paso es ajustar el colisionador de nuestro héroe para que su punto de pivote esté exactamente en la parte inferior central. El tamaño del colisionador debe coincidir exactamente con las dimensiones del personaje. Ajuste los parámetros de Desplazamiento y Tamaño del colisionador, así como la posición del objeto de Apariencia anidado para lograr la precisión necesaria. Preste especial atención al parámetro Offset.X del colisionador: su valor debe ser estrictamente 0. Esto asegurará la ubicación simétrica del colisionador con respecto al centro del objeto, lo cual es extremadamente importante para las rotaciones posteriores de caracteres hacia la izquierda y hacia la derecha, donde el valor de Transform.Scale.X se cambia a -1 y 1. El colisionador debe permanecer en su lugar y la rotación visual debe verse natural. Introducción al comportamiento físico de los personajes En primer lugar, es esencial enseñar a los personajes (ya sea el héroe principal, los PNJ o los enemigos) a interactuar con el mundo físico. Por ejemplo, los personajes deberían poder caminar sobre una superficie plana, saltar de ella y estar sujetos a la fuerza de la gravedad, empujándolos hacia el suelo. Unity tiene un motor de física incorporado que controla el movimiento de los cuerpos, maneja colisiones y agrega el efecto de fuerzas externas sobre los objetos. Todo esto se implementa utilizando el componente . Sin embargo, para los personajes, es útil tener una herramienta más flexible que interactúe con el mundo físico y al mismo tiempo brinde a los desarrolladores más control sobre el comportamiento del objeto. Rigidbody2D Como mencioné anteriormente, en versiones anteriores de Unity, los desarrolladores tenían que implementar toda esta lógica ellos mismos. Sin embargo, en Unity 2023, se agregó un nuevo método al componente , que permite un control flexible del objeto físico y al mismo tiempo proporciona toda la información necesaria sobre el movimiento realizado. Slide Rigidbody2D Comencemos creando una clase , que contendrá la lógica básica para mover personajes en el mundo físico. Esta clase usará en modo , que ya hemos agregado a nuestro personaje. Entonces, lo primero es agregar una referencia a este componente. CharacterBody Rigidbody2D Kinematic public class CharacterBody : MonoBehaviour { [SerializeField] private Rigidbody2D _rigidbody; } En ocasiones, para la dinámica del movimiento de los personajes, es necesario que la gravedad actúe con más fuerza de lo habitual. Para lograrlo agregaremos un factor de influencia de la gravedad con un valor inicial de 1, y este factor no puede ser menor que 0. [Min(0)] [field: SerializeField] public float GravityFactor { get; private set; } = 1f; También necesitamos definir qué objetos se considerarán superficies intransitables. Para ello crearemos un campo que nos permitirá especificar las capas necesarias. [SerializeField] private LayerMask _solidLayers; Limitaremos la velocidad del personaje para evitar que desarrolle velocidades excesivamente altas, por ejemplo, debido a alguna influencia externa, incluida la gravedad. Establecemos el valor inicial en 30 y limitamos la capacidad de establecer un valor inferior a 0 en el inspector. [Min(0)] [SerializeField] private float _maxSpeed = 30; Al movernos por una superficie, queremos que el personaje siempre se aferre a ella si la distancia entre ellos es lo suficientemente pequeña. [Min(0)] [SerializeField] private float _surfaceAnchor = 0.01f; Aunque decidimos que las superficies de nuestro juego solo serían horizontales o verticales, por si acaso especificaremos el ángulo de inclinación máximo de la superficie sobre el que el personaje puede pararse de manera estable, con un valor inicial de 45º. [Range(0, 90)] [SerializeField] private float _maxSlop = 45f; A través del inspector, también quiero ver la velocidad actual del personaje y su estado, así que agregaré dos campos con el atributo . SerializeField [SerializeField] private Vector2 _velocity; [field: SerializeField] public CharacterState State { get; private set; } Sí, aquí presenté una entidad nueva, aún no definida, . Discutiremos esto más a fondo. CharacterState Estados de carácter Para simplificar el desarrollo de nuestro juego de plataformas, definamos solo dos estados de los personajes principales. El primero es , un estado en el que el personaje se encuentra de forma segura en la superficie. En este estado, el personaje puede moverse libremente por la superficie y saltar de ella. Grounded El segundo es , un estado de caída libre, en el que el personaje se encuentra en el aire. El comportamiento del personaje en este estado puede variar según las características específicas del juego de plataformas. En un caso general cercano a la realidad, el personaje se mueve bajo la influencia del impulso inicial y no puede afectar su comportamiento. Sin embargo, en los juegos de plataformas, la física a menudo se simplifica en favor de la comodidad y la dinámica del juego: por ejemplo, en muchos juegos, incluso en caída libre, podemos controlar el movimiento horizontal del personaje. En nuestro caso esto también es posible, además de la popular mecánica del doble salto, que permite un salto adicional en el aire. Airborne Representemos los estados de nuestro personaje en el código: /// <summary> /// Describes the state of <see cref="CharacterBody"/>. /// </summary> public enum CharacterState { /// <summary> /// The character stays steady on the ground and can move freely along it. /// </summary> Grounded, /// <summary> /// The character is in a state of free fall. /// </summary> Airborne } Vale la pena señalar que puede haber muchos más estados. Por ejemplo, si el juego incluye superficies inclinadas, el personaje puede estar en un estado de deslizamiento en el que no puede moverse libremente hacia la derecha o hacia la izquierda, pero puede deslizarse cuesta abajo. En tal estado, el personaje también puede saltar, empujándose desde la pendiente, pero sólo en la dirección de la pendiente. Otro posible caso es deslizarse a lo largo de una pared vertical, donde la influencia de la gravedad se debilita y el personaje puede empujarse horizontalmente. Limitación de velocidad de movimiento Ya hemos definido un campo privado , pero necesitamos poder obtener y establecer este valor desde afuera mientras limitamos la velocidad máxima del personaje. Esto requiere comparar el vector de velocidad dado con la velocidad máxima permitida. _velocity Esto se puede hacer calculando la longitud del vector velocidad o, en términos matemáticos, su magnitud. La estructura ya contiene una propiedad que nos permite hacer esto. Por lo tanto, si la magnitud del vector que pasa excede la velocidad máxima permitida, debemos mantener la dirección del vector pero limitar su magnitud. Para esto, multiplicamos por el vector de velocidad normalizado (un vector normalizado es un vector con la misma dirección pero con una magnitud de 1). Vector2 magnitude _maxSpeed Así es como se ve en el código: public Vector2 Velocity { get => _velocity; set => _velocity = value.magnitude > _maxSpeed ? value.normalized * _maxSpeed : value; } Ahora echemos un vistazo de cerca a cómo se calcula la magnitud de un vector. Está definido por la fórmula: Calcular la raíz cuadrada es una operación que requiere muchos recursos. Aunque en la mayoría de casos la velocidad no superará el máximo, igual debemos realizar esta comparación al menos una vez por ciclo. Sin embargo, podemos simplificar significativamente esta operación si comparamos el cuadrado de la magnitud del vector con el cuadrado de la velocidad máxima. Para ello introducimos un campo adicional para almacenar el cuadrado de la velocidad máxima, y lo calculamos una vez en el método : Awake private float _sqrMaxSpeed; private void Awake() { _sqrMaxSpeed = _maxSpeed * _maxSpeed; } Ahora el ajuste de la velocidad se puede realizar de forma más óptima: public Vector2 Velocity { get => _velocity; set => _velocity = value.sqrMagnitude > _sqrMaxSpeed ? value.normalized * _maxSpeed : value; } Así, evitamos cálculos innecesarios y mejoramos el rendimiento del procesamiento de la velocidad de movimiento del personaje. Método de movimiento de cuerpo rígido Como mencioné anteriormente, Unity ha agregado un nuevo método , que simplificará enormemente el desarrollo de nuestro . Sin embargo, antes de utilizar este método, es necesario definir las reglas según las cuales el objeto se moverá en el espacio. Este comportamiento lo establece la estructura . Slide() CharacterBody Rigidbody2D.SlideMovement Introduzcamos un nuevo campo y establezcamos sus valores. _slideMovement private Rigidbody2D.SlideMovement _slideMovement; private void Awake() { _sqrMaxSpeed = _maxSpeed * _maxSpeed; _slideMovement = CreateSlideMovement(); } private Rigidbody2D.SlideMovement CreateSlideMovement() { return new Rigidbody2D.SlideMovement { maxIterations = 3, surfaceSlideAngle = 90, gravitySlipAngle = 90, surfaceUp = Vector2.up, surfaceAnchor = Vector2.down * _surfaceAnchor, gravity = Vector2.zero, layerMask = _solidLayers, useLayerMask = true, }; } Es importante explicar que determina cuántas veces un objeto puede cambiar de dirección como resultado de una colisión. Por ejemplo, si el personaje está en el aire junto a una pared y el jugador intenta moverla hacia la derecha mientras la gravedad actúa sobre ella. Por lo tanto, para cada llamada al método , se establecerá un vector de velocidad dirigido hacia la derecha y hacia abajo. Al golpear la pared, el vector de movimiento se recalcula y el objeto continuará moviéndose hacia abajo. maxIterations Slide() En tal situación, si el valor se estableciera en 1, el objeto golpearía la pared, se detendría y efectivamente se quedaría atascado allí. maxIterations Los valores para y se definieron previamente. Para obtener información más detallada sobre los otros campos, consulte . maxIterations layerMask la documentación oficial de la estructura Finalmente, moviendo el personaje Ahora todo está listo para que el Capitán se mueva. Haremos esto en , una devolución de llamada en Unity diseñada para manejar la física. En los últimos años, el equipo de Unity ha mejorado significativamente el manejo de la física 2D. Actualmente, el procesamiento se puede realizar en la devolución de llamada o incluso llamando al método requerido por su cuenta. FixedUpdate Update Sin embargo, en este ejemplo, utilizaremos el método tradicional y probado . Antes de continuar, vale la pena mencionar algunas palabras sobre el valor de . FixedUpdate Time.fixedDeltaTime Para garantizar la previsibilidad en la física del juego, la simulación se lleva a cabo en iteraciones en intervalos de tiempo fijos. Esto garantiza que los cambios en FPS o retrasos no afecten el comportamiento de los objetos. Al comienzo de cada ciclo, tendremos en cuenta el efecto de la gravedad sobre el objeto. Dado que la gravedad viene dada por el vector de aceleración de caída libre, podemos calcular el cambio en la velocidad del objeto a lo largo del tiempo mediante la fórmula: Δv Δt donde es la aceleración constante del objeto. En nuestro caso, es la aceleración debida a la gravedad, considerando el coeficiente que introdujimos: . Por tanto, se puede calcular de la siguiente manera: a Physics2D.gravity * GravityFactor Δv Time.fixedDeltaTime * GravityFactor * Physics2D.gravity El resultado final, donde cambiamos la velocidad, se ve así: Velocity += Time.fixedDeltaTime * GravityFactor * Physics2D.gravity; Ahora podemos realizar el movimiento de cuerpo rígido del personaje: var slideResults = _rigidbody.Slide( _velocity, Time.fixedDeltaTime, _slideMovement); La variable es un valor de la estructura y almacena los resultados del movimiento. Los campos principales de este resultado para nosotros son , el resultado de la colisión con la superficie durante el movimiento, y , el resultado de un lanzamiento hacia abajo, que ayudará a determinar si el personaje está parado sobre una superficie estable. slideResults SlideResults slideHit surfaceHit Manejo de colisiones Al chocar con superficies, es crucial limitar la velocidad del personaje dirigida hacia esa superficie. Un ejemplo simple es si el personaje está parado en el suelo, no debe continuar ganando velocidad bajo la influencia de la gravedad. Al final de cada ciclo, su velocidad debe ser cero. De manera similar, al moverse hacia arriba y golpear el techo, el personaje debería perder toda velocidad vertical y comenzar a moverse hacia abajo. Los resultados de las colisiones, y , están representados por valores de la estructura , que incluye la normal de la superficie de colisión. slideHit surfaceHit RaycastHit2D La limitación de velocidad se puede calcular restando la proyección del vector de velocidad original sobre la normal de colisión del propio vector de velocidad. Esto se hace usando el . Escribamos un método que realizará esta operación: producto escalar private static Vector2 ClipVector(Vector2 vector, Vector2 hitNormal) { return vector - Vector2.Dot(vector, hitNormal) * hitNormal; } Ahora integremos este método en nuestro . Aquí, para , solo limitaremos la velocidad si se dirige hacia abajo, ya que el lanzamiento que determina si el objeto está en la superficie siempre se realiza para verificar el contacto con el suelo. FixedUpdate surfaceHit private void FixedUpdate() { Velocity += Time.fixedDeltaTime * GravityFactor * Physics2D.gravity; var slideResults = _rigidbody.Slide( _velocity, Time.fixedDeltaTime, _slideMovement); if (slideResults.slideHit) { _velocity = ClipVector(_velocity, slideResults.slideHit.normal); } if (_velocity.y <= 0 && slideResults.surfaceHit) { var surfaceHit = slideResults.surfaceHit; _velocity = ClipVector(_velocity, surfaceHit.normal); } } Esta implementación permite gestionar correctamente el movimiento del personaje, evitando aceleraciones no deseadas al chocar con varias superficies y manteniendo el movimiento de los personajes en el juego predecible y fluido. Determinar el estado del personaje Al final de cada ciclo, es necesario determinar si el personaje se encuentra sobre una superficie sólida (estado a tierra) o en caída libre (o, como lo definimos, caída controlada: estado en el aire). Para considerar que el personaje está en el estado Conectado, principalmente, su velocidad vertical debe ser cero o negativa, que determinamos por el valor de . _velocity.y Otro criterio importante es la presencia de una superficie debajo de los pies del personaje, que identificamos a partir de los resultados del movimiento de Rigidbody, es decir, a través de la presencia de . surfaceHit El tercer factor es el ángulo de inclinación de la superficie, que analizamos en función de la normal de esta superficie, es decir, el valor de . Es necesario comparar este ángulo con , el ángulo máximo posible de la superficie sobre la que el personaje puede permanecer estable. surfaceHit.normal _maxSlop Para una superficie completamente vertical, la normal será estrictamente horizontal, es decir, su valor vectorial será (1, 0) o (-1, 0). Para una superficie horizontal, el valor de la normal será (0, 1). Cuanto menor sea el ángulo de inclinación, mayor será el valor de . Para el ángulo , este valor se puede calcular como: y alpha Dado que nuestro ángulo está dado en grados y la función $\cos$ requiere radianes, la fórmula se transforma en: Para ello, introduzcamos un nuevo campo y calculemoslo en el método . Awake private float _minGroundVertical; private void Awake() { _minGroundVertical = Mathf.Cos(_maxSlop * Mathf.PI / 180f); //... } Ahora actualicemos nuestro código en , verificando todas las condiciones anteriores. FixedUpdate if (_velocity.y <= 0 && slideResults.surfaceHit) { var surfaceHit = slideResults.surfaceHit; Velocity = ClipVector(_velocity, surfaceHit.normal); State = surfaceHit.normal.y >= _minGroundVertical ? CharacterState.Grounded : CharacterState.Airborne; } else { State = CharacterState.Airborne; } Esta lógica nos permitirá determinar con precisión cuándo el personaje está en el suelo y responder correctamente a los cambios en su estado. Agregar CharacterBody al Capitán Ahora que nuestro componente CharacterBody está listo, el paso final es agregarlo a nuestro Capitán. En la escena, seleccione el objeto Capitán y agréguele el componente . CharacterBody No olvide configurar Rigidbody como se muestra en la ilustración anterior. Establezca el Factor de gravedad en 3 y seleccione la opción Predeterminada para Capa sólida. Ahora puedes comenzar el juego y experimentar estableciendo diferentes valores de Velocidad para observar cómo se mueve nuestro personaje por la escena. Concluyendo por ahora Por supuesto, todavía necesitamos agregar controles de personajes. Sin embargo, este artículo ya se ha vuelto bastante largo, por lo que detallaré el control del personaje usando el nuevo sistema de entrada en el próximo artículo: "Creación de un controlador de personaje 2D en Unity: Parte 2". Puedes descargar el proyecto completo descrito en este artículo aquí: y comprobarlo todo en la práctica si encuentras alguna dificultad. Desarrollar un controlador de personaje es un aspecto clave en la creación de un juego de plataformas 2D, ya que determina el desarrollo posterior del juego. Afecta la facilidad con la que se agregarán nuevas características al comportamiento del héroe principal o de los enemigos. Por eso, es muy importante entender los conceptos básicos para poder desarrollar tu propio juego de forma independiente. Treasure Hunters