En aquest article, continuem desenvolupant un controlador de personatges per a un joc de plataformes 2D a Unity, examinant a fons cada pas de configuració i optimització dels controls.
A l'article anterior, " Com crear un controlador de personatges 2D a Unity: Part 1 ", vam comentar detalladament com crear la base del personatge, inclòs el seu comportament físic i el seu moviment bàsic. Ara és el moment de passar a aspectes més avançats, com ara el maneig d'entrada i el seguiment dinàmic de la càmera.
En aquest article, aprofundirem en la configuració del nou sistema d'entrada d'Unity, creant accions actives per controlar el personatge, permetre saltar i garantir les respostes adequades a les ordres dels jugadors.
Si voleu implementar vosaltres mateixos tots els canvis descrits en aquest article, podeu descarregar la branca del repositori " Cos del caràcter ", que conté la base d'aquest article. Alternativament, podeu descarregar la branca " Controlador de caràcters " amb el resultat final.
Abans de començar a escriure codi per controlar el nostre personatge, hem de configurar el sistema d'entrada al projecte. Per al nostre joc de plataformes, hem escollit el nou sistema d'entrada d'Unity, introduït fa uns anys, que segueix sent rellevant pels seus avantatges respecte al sistema tradicional.
El sistema d'entrada ofereix un enfocament més modular i flexible per al maneig d'entrada, que permet als desenvolupadors configurar fàcilment els controls per a diversos dispositius i donar suport a escenaris d'entrada més complexos sense sobrecàrrec d'implementació addicional.
Primer, instal·leu el paquet del sistema d'entrada. Obriu el Gestor de paquets des del menú principal seleccionant Finestra → Gestor de paquets. A la secció Registre Unity, cerqueu el paquet "Sistema d'entrada" i feu clic a "Instal·la".
A continuació, aneu a la configuració del projecte mitjançant el menú Edita → Configuració del projecte. Seleccioneu la pestanya Reproductor, cerqueu la secció Gestió activa d'entrada i configureu-la a "Paquet del sistema d'entrada (nou).
Després de completar aquests passos, Unity us demanarà que reinicieu. Un cop reiniciat, tot estarà a punt per configurar els controls del nostre capità.
A la carpeta Configuració , creeu Accions d'entrada mitjançant el menú principal: Actius → Crea → Accions d'entrada . Anomena el fitxer "Controls".
El sistema d'entrada d'Unity és una eina de gestió d'entrada potent i flexible que permet als desenvolupadors configurar controls per als personatges i els elements del joc. Admet diversos dispositius d'entrada. Les accions d'entrada que creeu proporcionen una gestió centralitzada d'entrada, simplificant la configuració i fent que la interfície sigui més intuïtiva.
Feu doble clic al fitxer de controls per obrir-lo per editar-lo i afegiu un mapa d'acció per al control de caràcters anomenat "Caràcter".
Un mapa d'acció a Unity és una col·lecció d'accions que es poden enllaçar amb diversos controladors i claus per realitzar tasques específiques del joc. És una manera eficient d'organitzar els controls, que permet als desenvolupadors assignar i ajustar les entrades sense reescriure el codi. Per obtenir més detalls, consulteu la documentació oficial del sistema d'entrada .
La primera acció s'anomenarà "Mou". Aquesta acció definirà la direcció del moviment del personatge. Estableix el tipus d'acció a "Valor" i el tipus de control a "Vector2" per permetre el moviment en quatre direccions.
Assigneu enllaços a aquesta acció seleccionant Afegeix composició amunt/avall/dreta/esquerra i assignant les tecles WASD conegudes a les seves direccions respectives.
No us oblideu de desar la configuració fent clic a Desa l'actiu . Aquesta configuració garanteix que podeu reassignar els enllaços per a l'acció "Mou", per exemple, a les tecles de fletxa o fins i tot a un joystick del gamepad.
A continuació, afegiu una acció nova: "Saltar". Mantingueu el tipus d'acció com a "Botó", però afegiu una nova interacció : "Premeu" i configureu el comportament de l'activador a "Premeu i deixeu anar", ja que hem de capturar tant el botó com el botó.
Això completa l'esquema de control de caràcters. El següent pas és escriure un component per gestionar aquestes accions.
És hora d'enllaçar les accions d'entrada que hem creat per al control de personatges al component CharacterBody
, permetent que el personatge es mogui activament per l'escena segons les nostres ordres de control.
Per fer-ho, crearem un script responsable del control del moviment i l'anomenarem CharacterController
per a més claredat. En aquest script, primer definirem alguns camps bàsics. Afegirem una referència al component CharacterBody
, _characterBody
, que serà controlat directament per l'script.
També establirem paràmetres per a la velocitat de moviment del personatge ( _speed
) i l'alçada de salt ( _jumpHeight
). A més, definirem el propòsit del camp _stopJumpFactor
.
És possible que hàgiu notat que en molts jocs de plataformes 2D, l'alçada del salt es pot controlar. Com més temps estigui premut el botó de salt, més alt saltarà el personatge. Essencialment, s'aplica una velocitat inicial ascendent a l'inici del salt, i aquesta velocitat es redueix quan es deixa anar el botó. El _stopJumpFactor
determina quant disminueix la velocitat ascendent en deixar anar el botó de salt.
Aquí teniu un exemple del codi que escriurem:
// 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ó, implementarem la capacitat de moure el personatge cap a l'esquerra i la dreta. En mantenir premut el botó de moviment, el personatge hauria de mantenir la velocitat de moviment especificada independentment dels obstacles. Per aconseguir-ho, afegirem una variable a l'script per emmagatzemar la velocitat de moviment actual al llarg de la superfície (o simplement horitzontalment quan el personatge està a l'aire):
// CharacterController.cs private float _locomotionVelocity;
Al component CharacterBody
, introduirem un mètode per establir aquesta velocitat:
// CharacterBody.cs public void SetLocomotionVelocity(float locomotionVelocity) { Velocity = new Vector2(locomotionVelocity, _velocity.y); }
Com que el nostre joc no té superfícies inclinades, aquest mètode és bastant senzill. En escenaris més complexos, hauríem de tenir en compte l'estat del cos i el pendent de la superfície. De moment, simplement conservem la component vertical de la velocitat mentre modifiquem només la coordenada x
horitzontal.
A continuació, establirem aquest valor al mètode Update
a cada fotograma:
// CharacterController.cs private void Update() { _characterBody.SetLocomotionVelocity(_locomotionVelocity); }
Definirem un mètode per gestionar els senyals de l'acció d'entrada de Move
:
// CharacterController.cs public void OnMove(InputAction.CallbackContext context) { var value = context.ReadValue<Vector2>(); _locomotionVelocity = value.x * _speed; }
Com que l'acció Move
es defineix com a Vector2
, el context proporcionarà un valor vectorial en funció de quines tecles es premeu o allibereu. Per exemple, si premeu la tecla D
, el mètode OnMove
rebi el vector (1, 0). Si premeu D
i W
simultàniament, es produirà (1, 1). Alliberar totes les tecles activarà OnMove
amb el valor (0, 0).
Per a la clau A
, el vector serà (-1, 0). En el mètode OnMove
, prenem la component horitzontal del vector rebut i la multipliquem per la velocitat de moviment especificada, _speed
.
Primer, hem d'ensenyar el component CharacterBody
a manejar els salts. Per fer-ho, afegirem un mètode responsable del salt:
// CharacterBody.cs public void Jump(float jumpSpeed) { Velocity = new Vector2(_velocity.x, jumpSpeed); State = CharacterState.Airborne; }
En el nostre cas, aquest mètode és senzill: estableix la velocitat vertical i canvia immediatament l'estat del personatge a Airborne
.
A continuació, hem de determinar la velocitat a la qual ha de saltar el personatge. Ja hem definit l'alçada del salt i sabem que la gravetat actua constantment sobre el cos. A partir d'això, la velocitat de salt inicial es pot calcular mitjançant la fórmula:
On h és l'alçada del salt i g l'acceleració gravitatòria. També comptarem amb el multiplicador de gravetat present al component CharacterBody
. Afegirem un camp nou per definir la velocitat de salt inicial i la calcularem de la següent manera:
// CharacterController.cs private float _jumpSpeed; private void Awake() { _jumpSpeed = Mathf.Sqrt(2 * Physics2D.gravity.magnitude * _characterBody.GravityFactor * _jumpHeight); }
Necessitarem un altre camp per fer un seguiment de si el personatge està saltant actualment, de manera que podem limitar la velocitat de salt en el moment adequat.
A més, si el jugador manté premut el botó de salt fins a aterrar, hauríem de restablir aquesta bandera nosaltres mateixos. Això es farà amb el mètode Update
:
// CharacterController.cs private bool _isJumping; private void Update() { if (_characterBody.State == CharacterState.Grounded) { _isJumping = false; } //... }
Ara, escrivim el mètode per gestionar l'acció Jump
:
// CharacterController.cs public void OnJump(InputAction.CallbackContext context) { if (context.started) { Jump(); } else if (context.canceled) { StopJumping(); } }
Com que l'acció Jump
és un botó, podem determinar a partir del context si la pressió del botó s'ha iniciat ( context.started
) o finalitzada ( context.canceled
). En base a això, iniciem o aturem el salt.
Aquest és el mètode per executar el salt:
// CharacterController.cs private void Jump() { if (_characterBody.State == CharacterState.Grounded) { _isJumping = true; _characterBody.Jump(_jumpSpeed); } }
Abans de saltar, comprovem si el personatge està a terra. Si és així, posem la bandera _isJumping
i fem que el cos salti amb la _jumpSpeed
.
Ara, implementem el comportament de parar-salt:
// 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); } }
Aturem el salt només si la bandera _isJumping
està activa. Una altra condició important és que el personatge s'hagi de moure cap amunt. Això evita limitar la velocitat de caiguda si es deixa anar el botó de salt mentre es mou cap avall. Si es compleixen totes les condicions, restablirem la bandera _isJumping
i reduïm la velocitat vertical en un factor de _stopJumpFactor
.
Ara que tots els components estan preparats, afegiu els components PlayerInput
i CharacterController
a l'objecte Captain de l'escena. Assegureu-vos de seleccionar el component CharacterController
que hem creat, no el component Unity estàndard dissenyat per controlar personatges 3D.
Per al CharacterController
, assigneu el component CharacterBody
existent del personatge. Per a PlayerInput
, establiu els controls creats anteriorment al camp Actions
.
A continuació, configureu el component PlayerInput per cridar els mètodes adequats des de CharacterController. Amplieu les seccions Esdeveniments i Caràcter a l'editor i enllaçeu els mètodes corresponents a les accions Moure i Saltar.
Ara, tot està preparat per executar el joc i provar com funcionen tots els components configurats junts.
Ara, hem de fer que la càmera segueixi el personatge allà on vagi. Unity proporciona una potent eina per a la gestió de càmeres: Cinemachine .
Cinemachine és una solució revolucionària per al control de càmeres a Unity que ofereix als desenvolupadors una àmplia gamma de capacitats per crear sistemes de càmeres dinàmics i ben ajustats que s'adaptin a les necessitats de joc. Aquesta eina facilita la implementació de tècniques complexes de càmera, com ara el seguiment de personatges, l'ajust automàtic de l'enfocament i molt més, afegint vitalitat i riquesa a cada escena.
Primer, localitzeu l'objecte de la càmera principal a l'escena i afegiu-hi el component CinemachineBrain
.
A continuació, creeu un objecte nou a l'escena anomenat CaptainCamera . Aquesta serà la càmera que segueixi el capità, com un càmera professional. Afegiu-hi el component CinemachineVirtualCamera
. Establiu el camp Seguiu al capità, seleccioneu Framing Transposer per al camp Body i configureu el paràmetre Lens Ortho Size a 4.
A més, necessitarem un altre component per definir el desplaçament de la càmera en relació amb el personatge: CinemachineCameraOffset
. Estableix el valor Y a 1,5 i el valor Z a -15.
Ara, anem a provar com la càmera segueix el nostre personatge.
Crec que va sortir força bé. Em vaig adonar que la càmera de tant en tant tartamudeja lleugerament. Per solucionar-ho, he definit el camp Mètode d'actualització de combinació de l'objecte de la càmera principal a FixedUpdate.
Anem a provar la mecànica actualitzada. Intenta córrer i saltar contínuament. Els jugadors experimentats poden notar que els salts no sempre es registren. A la majoria de jocs, això no és un problema.
Resulta que és difícil predir l'hora exacta d'aterratge per tornar a prémer el botó de salt. Hem de fer que el joc sigui més indulgent permetent als jugadors que premeu el salt lleugerament abans d'aterrar i que el personatge salti immediatament després d'aterrar. Aquest comportament s'alinea amb el que els jugadors estan acostumats.
Per implementar-ho, introduirem una nova variable, _jumpActionTime
, que representa la finestra de temps durant la qual encara es pot activar un salt si es presenta l'oportunitat.
// CharacterController.cs [Min(0)] [SerializeField] private float _jumpActionTime = 0.1f;
He afegit un camp _jumpActionEndTime
, que marca el final de la finestra d'acció de salt. En altres paraules, fins que s'arribi _jumpActionEndTime
, el personatge saltarà si es presenta l'oportunitat. També actualitzem el controlador d'accions 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(); } }
Quan es prem el botó de salt, si el personatge està a terra, salta immediatament. En cas contrari, emmagatzemem la finestra de temps durant la qual encara es pot realitzar el salt.
Eliminem la comprovació d'estat Grounded
del mètode Jump
.
// CharacterController.cs private void Jump() { _isJumping = true; _characterBody.Jump(_jumpSpeed); }
També adaptarem el mètode de stop-jumping. Si el botó es va deixar anar abans d'aterrar, no s'hauria de produir cap salt, de manera que restablirem _jumpActionEndTime
.
// CharacterController.cs private void StopJumping() { _jumpActionEndTime = 0; //... }
Quan hem de comprovar que el personatge ha aterrat i desencadenar un salt? L'estat CharacterBody
es processa a FixedUpdate
, mentre que el processament de l'acció es produeix més tard. Independentment de si és Update
o FixedUpdate
, es pot produir un retard d'un fotograma entre l'aterratge i el salt, cosa que és notable.
Afegirem un esdeveniment StateChanged
a CharacterBody
per respondre a l'instant a l'aterratge. El primer argument serà l'estat anterior i el segon serà l'estat actual.
// CharacterBody.cs public event Action<CharacterState, CharacterState> StateChanged;
Ajustarem la gestió de l'estat per activar l'esdeveniment de canvi d'estat i tornarem a escriure 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); } } }
També vaig perfeccionar com es gestiona surfaceHit
a 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; }
A CharacterController
, ens subscriurem a l'esdeveniment StateChanged
i afegirem 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(); } }
Eliminarem la comprovació de l'estat Grounded
Update
i la traslladarem a OnGrounded
.
// CharacterController.cs private void Update() { _characterBody.SetLocomotionVelocity(_locomotionVelocity); } private void OnGrounded() { _isJumping = false; }
Ara, afegiu el codi per comprovar si s'ha d'activar un salt.
// CharacterController.cs private void OnGrounded() { _isJumping = false; if (_jumpActionEndTime > Time.unscaledTime) { _jumpActionEndTime = 0; Jump(); } }
Si _jumpActionEndTime
és més gran que l'hora actual, vol dir que el botó de salt s'ha premut recentment, de manera que restablirem _jumpActionEndTime
i realitzem el salt.
Ara, prova de saltar contínuament amb el personatge. Notareu que el botó de salt se sent més sensible i controlar el personatge es fa més suau. Tanmateix, vaig observar que en determinades situacions, com ara la cantonada que es mostra a la il·lustració següent, l'estat Grounded
experimenta un lleuger retard, interrompent la cadena de salt.
Per solucionar-ho, he establert el camp Surface Anchor
al component CharacterBody
a 0,05 en lloc de 0,01. Aquest valor representa la distància mínima a una superfície perquè el cos entri a l'estat Grounded
.
És possible que hagis notat que intentar saltar mentre corres per superfícies verticals no sempre funciona. De vegades pot semblar que el botó de salt no respon.
Aquesta és una de les subtileses del desenvolupament d'un controlador de personatges per a plataformes 2D. Els jugadors necessiten la capacitat de saltar fins i tot quan arribin una mica tard en prémer el botó de salt. Tot i que aquest concepte pot semblar estrany, és com funcionen la majoria de plataformes. El resultat és un personatge que sembla expulsar l'aire, com es demostra a l'animació següent.
Implementem aquesta mecànica. Introduirem un camp nou per emmagatzemar la finestra de temps (en segons) durant la qual el personatge encara pot saltar després de perdre l'estat Grounded
.
// CharacterController.cs [Min(0)] [SerializeField] private float _rememberGroundTime = 0.1f;
També afegirem un altre camp per emmagatzemar la marca de temps després del qual s'"oblida" l'estat Grounded
.
// CharacterController.cs private float _lostGroundTime;
Aquest estat es farà un seguiment mitjançant l'esdeveniment CharacterBody
. Ajustarem el controlador OnStateChanged
per a aquest propòsit.
// CharacterController.cs private void OnStateChanged(CharacterState previousState, CharacterState state) { if (state == CharacterState.Grounded) { OnGrounded(); } else if (previousState == CharacterState.Grounded) { _lostGroundTime = Time.unscaledTime + _rememberGroundTime; } }
És important distingir si el personatge va perdre l'estat Grounded
a causa d'un salt intencionat o per un altre motiu. Ja tenim el senyalador _isJumping
, que es desactiva cada vegada que es crida StopJumping
per evitar accions redundants.
Vaig decidir no introduir una altra bandera ja que la cancel·lació de salt redundant no afecta el joc. No dubteu a experimentar. La bandera _isJumping
ara només s'esborrarà quan el personatge caigui després de saltar. Actualitzem el codi en conseqüència.
// 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); } }
Finalment, revisarem el mètode 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(); } }
Ara, saltar de superfícies verticals ja no pertorba el ritme de joc i se sent molt més natural, malgrat el seu aparent absurd. El personatge pot, literalment, expulsar l'aire, anant més lluny del que sembla lògic. Però això és exactament el que es necessita per al nostre joc de plataformes.
El toc final és fer que el personatge s'enfronti a la direcció del moviment. Ho implementarem de la manera més senzilla: canviant l'escala del personatge al llarg de l'eix x. Establir un valor negatiu farà que el nostre capità s'enfronti en sentit contrari.
En primer lloc, emmagatzemem l'escala original per si difereix d'1.
// CharacterController.cs public class CharacterController : MonoBehaviour { //... private Vector3 _originalScale; private void Awake() { //... _originalScale = transform.localScale; } }
Ara, quan ens movem cap a l'esquerra o la dreta, aplicarem 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; } } }
Anem a provar el resultat.
Aquest article va resultar bastant detallat, però vam aconseguir cobrir tots els aspectes essencials del control dels personatges en un joc de plataformes 2D. Com a recordatori, podeu consultar el resultat final a la branca " Controlador de caràcters " del dipòsit.
Si us ha agradat o us ha semblat útil aquest article i l'anterior, us agrairia els m'agrada i les estrelles a GitHub. No dubteu a posar-vos en contacte si teniu cap problema o trobeu errors. Gràcies per la vostra atenció!