Stăpânirea camerelor 2D în Unity: Un tutorial pentru dezvoltatorii de jocuri

Publicat: 2022-03-11

Pentru un dezvoltator, camera este una dintre pietrele de temelie ale procesului de dezvoltare a jocului. De la doar afișarea vizualizării jocului într-o aplicație de șah până la dirijarea cu măiestrie a mișcării camerei într-un joc 3D AAA pentru a obține efecte cinematografice, camerele sunt folosite în principiu în orice joc video realizat vreodată, chiar înainte de a fi numite „camere”.

În acest articol, voi explica cum să proiectați un sistem de cameră pentru jocuri 2D și, de asemenea, voi explica câteva puncte despre cum să-l implementez într-unul dintre cele mai populare motoare de joc, Unity.

De la 2D la 2.5D: un sistem de cameră extensibil

Sistemul de camere pe care îl vom proiecta împreună este modular și extensibil. Are un nucleu de bază format din mai multe componente care vor asigura funcționalitatea de bază, iar apoi diverse componente/efecte care pot fi utilizate opțional, în funcție de situația la îndemână.

Sistemul de camere pe care îl construim aici este destinat jocurilor cu platforme 2D, dar poate fi extins cu ușurință la alte tipuri de jocuri 2D, jocuri 2.5D sau chiar jocuri 3D.

Stăpânirea camerei 2D în Unity: Un tutorial pentru dezvoltatorii de jocuri

Stăpânirea camerei 2D în Unity: Un tutorial pentru dezvoltatorii de jocuri
Tweet

Voi împărți funcționalitatea camerei în două grupuri principale: urmărirea camerei și efectele camerei.

Urmărire

Cea mai mare parte a mișcării camerei pe care o vom face aici se va baza pe urmărire. Aceasta este capacitatea unui obiect, în acest caz a camerei, de a urmări alte obiecte pe măsură ce se mișcă în scena jocului. Tipurile de urmărire pe care le vom implementa vor rezolva unele scenarii obișnuite întâlnite în jocurile cu platformă 2D, dar pot fi extinse cu noi tipuri de urmărire pentru alte scenarii particulare pe care le-ați putea avea.

Efecte

Vom implementa câteva efecte interesante, cum ar fi tremuratul camerei, zoomul camerei, estomparea camerei și suprapunerea de culoare.

Noțiuni de bază

Creați un nou proiect 2D în Unity și importați elemente standard, în special personajul RobotBoy. Apoi, creați o casetă de bază și adăugați o instanță de caracter. Ar trebui să poți să mergi și să sari cu personajul tău în scena ta actuală. Asigurați-vă că camera este setată pe modul ortografic (este setată implicit la Perspectivă).

Urmărirea unei ținte

Următorul script va adăuga un comportament de urmărire de bază la camera noastră principală. Scriptul trebuie să fie atașat ca componentă la camera principală din scena dvs. și expune un câmp pentru alocarea unui obiect țintă de urmărit. Apoi, scriptul asigură că coordonatele x și y ale camerei sunt aceleași cu obiectul pe care îl urmărește. Toată această procesare se face în timpul etapei Actualizare.

 [SerializeField] protected Transform trackingTarget; // ... void Update() { transform.position = new Vector3(trackingTarget.position.x, trackingTarget.position.y, transform.position.z); }

Trageți personajul RobotBoy din ierarhia scenei dvs. peste câmpul „Ținta de urmărire” expus de următorul comportament pentru a permite urmărirea personajului principal.

Adăugarea de offset

Toate bune, dar putem vedea o limitare de la început: personajul este întotdeauna în centrul scenei noastre. Putem vedea multe în spatele personajului, care sunt de obicei lucruri care nu ne interesează, și vedem prea puțin din ceea ce este înaintea personajului nostru, ceea ce ar putea fi dăunător jocului.

Pentru a rezolva acest lucru, adăugăm câteva câmpuri noi în script care vor permite poziționarea camerei la un decalaj față de țintă.

 [SerializeField] float xOffset; [SerializeField] float yOffset; // ... void Update() { transform.position = new Vector3(trackingTarget.position.x + xOffset, trackingTarget.position.y + yOffset, transform.position.z); }

Mai jos puteți vedea o posibilă configurație pentru cele două câmpuri noi:

Netezirea lucrurilor

Mișcarea camerei este destul de rigidă și va produce, de asemenea, amețeli la unii jucători din cauza mișcării constante percepute a mediului. Pentru a remedia acest lucru, vom adăuga o întârziere în urmărirea camerei folosind interpolarea liniară și un nou câmp pentru a controla cât de repede intră camera în poziție după ce personajul începe să își schimbe poziția.

 [SerializeField] protected float followSpeed; // ... protected override void Update() { float xTarget = trackingTarget.position.x + xOffset; float yTarget = trackingTarget.position.y + yOffset; float xNew = Mathf.Lerp(transform.position.x, xTarget, Time.deltaTime * followSpeed); float yNew = Mathf.Lerp(transform.position.y, yTarget, Time.deltaTime * followSpeed); transform.position = new Vector3(xNew, yNew, transform.position.z); } 

Opriți amețelile: blocarea axei

Deoarece nu este plăcut pentru creierul tău să privească camera urcând și coborând tot timpul împreună cu personajul, introducem blocarea axelor. Aceasta înseamnă că putem limita urmărirea la o singură axă. Apoi vom separa codul nostru de urmărire în urmărire independentă de axă și vom lua în considerare noile steaguri de blocare.

 [SerializeField] protected bool isXLocked = false; [SerializeField] protected bool isYLocked = false; // ... float xNew = transform.position.x; if (!isXLocked) { xNew = Mathf.Lerp(transform.position.x, xTarget, Time.deltaTime * followSpeed); } float yNew = transform.position.y; if (!isYLocked) { yNew = Mathf.Lerp(transform.position.y, yTarget, Time.deltaTime * followSpeed); } 

Sistemul de benzi

Acum că camera urmărește playerul doar pe orizontală, suntem limitați la înălțimea unui ecran. Dacă personajul urcă pe o scară sau sare mai sus de aceasta, trebuie să urmăm. Modul în care facem acest lucru este prin utilizarea unui sistem de benzi.

Imaginează-ți următorul scenariu:

Personajul se află inițial pe banda de jos. În timp ce personajul rămâne în limitele acestei benzi, camera se va mișca doar orizontal pe decalajul de înălțime specific benzii pe care îl putem seta.

De îndată ce personajul intră pe altă bandă, camera va trece pe acea bandă și va continua să se miște orizontal de acolo până când are loc următoarea schimbare de bandă.

Trebuie avut grijă la proiectarea benzii pentru a preveni schimbarea rapidă a benzii în timpul unor acțiuni precum sărituri, care pot crea confuzie pentru jucător. O bandă ar trebui schimbată numai dacă personajul jucătorului va rămâne pe ea pentru o perioadă.

Nivelurile Lanes se pot schimba pe tot parcursul jocului în funcție de nevoile specifice ale designerului sau pot fi întrerupte cu totul și un alt sistem de urmărire a camerei le poate lua locul. Prin urmare, avem nevoie de niște limitatoare pentru specificarea zonelor de benzi.

Implementarea

O posibilă implementare este adăugarea benzilor ca obiecte simple în scenă. Vom folosi coordonatele poziției Y asociate cu decalajul Y în scriptul de urmărire de mai sus pentru a implementa sistemul. Prin urmare, poziționarea lor pe coordonatele X și Z nu contează.

Adăugați clasa LaneSystem la cameră, împreună cu clasa de urmărire și atribuiți obiectele benzii matricei furnizate. De asemenea, atribuiți caracterul jucătorului câmpului de referință. Deoarece referința este poziționată între o bandă și o altă bandă, cea inferioară dintre cele două va fi folosită pentru a poziționa camera.

Iar clasa LaneSystem se ocupă de mutarea camerei între benzi, pe baza poziției de referință. FollowSpeed ​​este folosit din nou aici pentru interpolarea poziției, pentru a preveni ca schimbarea benzii să fie prea bruscă:

 [SerializeField] Transform reference; [SerializeField] List<Transform> lanes; [SerializeField] float followSpeed = 5f; // ... void Update() { float targetYCoord = transform.position.y; if (lanes.Count > 1) { int i = 0; for (i = 0; i < lanes.Count - 1; ++i) { if ((reference.position.y > lanes[i].position.y) && (reference.position.y <= lanes[i + 1].position.y)) { targetYCoord = lanes[i].position.y; break; } } if (i == lanes.Count - 1) targetYCoord = lanes[lanes.Count - 1].position.y; } else { targetYCoord = lanes[0].position.y; } float yCoord = Mathf.Lerp(transform.position.y, targetYCoord, Time.deltaTime * followSpeed); transform.position = new Vector3(transform.position.x, yCoord, transform.position.z); }

Această implementare nu este una WYSIWYG și este lăsată ca atare ca un exercițiu pentru cititor.

Lock Node System

A avea camera în mișcare pe benzi este grozav, dar uneori avem nevoie ca camera să fie fixată pe ceva, un punct de interes (POI) în scena jocului.

Acest lucru poate fi realizat prin configurarea unor astfel de puncte de interes în scenă și atașarea unui colisionator de declanșare la ele. Ori de câte ori personajul intră în acel colisionar de declanșare, mișcăm camera și rămânem pe POI. Pe măsură ce personajul se mișcă și apoi părăsește ciocnitorul de declanșare al POI, revenim la un alt tip de urmărire, de obicei comportamentul de urmărire standard.

Comutarea urmăririi camerei la un nod de blocare și înapoi se poate face fie printr-un simplu comutator, fie printr-un sistem de stivă, pe care modurile de urmărire sunt apăsate și deschise.

Implementarea

Pentru a configura un nod de blocare, trebuie doar să creați un obiect (poate fi gol sau ca în captura de ecran de mai jos, un sprite) și să atașați o componentă mare Circle Collider 2D, astfel încât să marcheze zona în care se va afla jucătorul când camera va fi focalizați nodul. Puteți alege orice tip de ciocnitor, eu aleg Circle ca exemplu aici. De asemenea, creați o etichetă pe care o puteți verifica cu ușurință, cum ar fi „CameraNode” și atribuiți-o acestui obiect.

Adăugați următoarea proprietate la scriptul de urmărire de pe camera dvs.:

 public Transform TrackingTarget { get { return trackingTarget; } set { trackingTarget = value; } }

Apoi atașați următorul script la player, care îi va permite să comute temporar ținta camerei la nodul de blocare pe care l-ați setat. Scriptul își va aminti, de asemenea, ținta anterioară, astfel încât să poată reveni la ea când jucătorul iese din zona de declanșare. Puteți merge mai departe și transforma acest lucru într-o stivă completă dacă aveți nevoie de asta, dar pentru scopul nostru, deoarece nu suprapunem mai multe noduri de blocare, acest lucru va fi bine. De asemenea, vă rugăm să rețineți că puteți modifica poziția Circle Collider 2D sau puteți adăuga din nou orice alt tip de ciocnitor pentru a declanșa blocarea camerei, acesta este doar un simplu exemplu.

 public class LockBehavior : MonoBehaviour { #region Public Fields [SerializeField] Camera camera; [SerializeField] string tag; #endregion #region Private private Transform previousTarget; private TrackingBehavior trackingBehavior; private bool isLocked = false; #endregion // Use this for initialization void Start() { trackingBehavior = camera.GetComponent<TrackingBehavior>(); } void OnTriggerEnter2D(Collider2D other) { if (other.tag == tag && !isLocked) { isLocked = true; PushTarget(other.transform); } } void OnTriggerExit2D(Collider2D other) { if (other.tag == tag && isLocked) { isLocked = false; PopTarget(); } } private void PushTarget(Transform newTarget) { previousTarget = trackingBehavior.TrackingTarget; trackingBehavior.TrackingTarget = newTarget; } private void PopTarget() { trackingBehavior.TrackingTarget = previousTarget; } } 

Zoom camera

Zoomul camerei poate fi executat fie la intrarea utilizatorului, fie ca o animație atunci când dorim să ne concentrăm pe ceva precum un POI sau o zonă mai restrânsă dintr-un nivel.

Zoomul camerei 2D în Unity 3D poate fi realizat prin manipularea dimensiunii ortografice a camerei. Atașarea următorului script ca componentă la o cameră și utilizarea metodei SetZoom pentru a modifica factorul de zoom va produce efectul dorit. 1.0 înseamnă fără zoom, 0,5 înseamnă mărire de două ori, 2 înseamnă micșorare de două ori și așa mai departe.

 [SerializeField] float zoomFactor = 1.0f; [SerializeField] float zoomSpeed = 5.0f; private float originalSize = 0f; private Camera thisCamera; // Use this for initialization void Start() { thisCamera = GetComponent<Camera>(); originalSize = thisCamera.orthographicSize; } // Update is called once per frame void Update() { float targetSize = originalSize * zoomFactor; if (targetSize != thisCamera.orthographicSize) { thisCamera.orthographicSize = Mathf.Lerp(thisCamera.orthographicSize, targetSize, Time.deltaTime * zoomSpeed); } } void SetZoom(float zoomFactor) { this.zoomFactor = zoomFactor; }

Tremurarea ecranului

Ori de câte ori trebuie să arătăm un cutremur, o explozie sau orice alt efect în jocul nostru, un efect de tremurare a camerei este util.

Un exemplu de implementare a modului de a face acest lucru este disponibil pe GitHub: gist.github.com/ftvs/5822103. Implementarea este destul de simplă. Spre deosebire de celelalte efecte pe care le-am acoperit până acum, se bazează pe puțină aleatorie.

Fade & Overlay

Când nivelul nostru începe sau se termină, un efect de atenuare sau de ieșire este plăcut. Putem implementa acest lucru adăugând o textură UI neinteracabilă într-un panou care se întinde pe tot ecranul nostru. Inițial transparent, îl putem umple cu orice culoare și opacitate sau îl putem anima pentru a obține efectul dorit.

Iată un exemplu al acestei configurații, vă rugăm să rețineți că obiectul UI Panel este atribuit fiului „Camera Overlay” al obiectului principal al camerei. Camera Overlay expune un script numit Overlay care include următoarele:

 [SerializeField] Image overlay; // ... public void SetOverlayColor(Color color) { overlay.color = color; } 

Pentru a avea un efect de fade-in, schimbați scriptul Overlay adăugând o interpolare la o culoare țintă pe care o setați cu SetOverlayColor ca în scriptul următor și setați culoarea inițială a panoului nostru la Negru (sau Alb) și culoarea țintă. până la culoarea finală a suprapunerii. Puteți schimba fadeSpeed ​​la orice se potrivește nevoilor dvs., cred că 0.8 este unul bun pentru început. Valoarea fadeSpeed ​​funcționează ca un modificator de timp. 1.0 înseamnă că se va întâmpla pe mai multe cadre, dar într-un interval de timp de 1 secundă. 0,8 înseamnă că va dura de fapt 1/0,8 = 1,25 secunde pentru finalizare.

 public class Overlay : MonoBehaviour { #region Fields [SerializeField] Image overlay; [SerializeField] float fadeSpeed = 5f; [SerializeField] Color targetColor; #endregion void Update() { if (overlay.color != targetColor) { overlay.color = Color.Lerp(overlay.color, targetColor, Time.deltaTime * fadeSpeed); } } #region Public public void SetOverlayColor(Color color) { targetColor = color; } #endregion }

Învelire

În acest articol am încercat să demonstrez componentele de bază necesare pentru a avea un sistem modular de camere 2D la locul jocului dvs. și, de asemenea, care este mentalitatea necesară pentru proiectarea acestuia. Desigur, toate jocurile au nevoile lor speciale, dar cu urmărirea de bază și cu efectele simple descrise aici puteți parcurge un drum lung și, de asemenea, aveți un plan pentru implementarea propriilor efecte. Apoi puteți merge și mai departe și împachetați totul într-un pachet reutilizabil Unity 3D pe care îl puteți transfera și în alte proiecte.

Sistemele de camere sunt foarte importante pentru a transmite atmosfera potrivită pentru jucătorii tăi. O comparație bună pe care îmi place să o folosesc este atunci când te gândești la diferența dintre teatrul clasic și filme. Camerele și filmul în sine au adus atât de multe posibilități în scenă încât au evoluat în cele din urmă într-o artă de la sine, așa că dacă nu intenționați să implementați un alt joc „Pong”, camerele avansate ar trebui să fie instrumentul dvs. de alegere în orice proiect de joc pe care îl aveți. mă voi întreprinde de acum înainte.

Înrudit: Unitate cu MVC: Cum să-ți ridici nivelul de dezvoltare a jocului