Lag en asteroiderlignende skjermomslagseffekt med enhet

Ta en titt på demoen under og la oss komme i gang!

Klikk på demoen for å gi den fokus, bruk piltastene til å flytte skipet.

Som du kan se, er det to måter å gjøre dette på. Den første er enklere å vikle hodet rundt. Den andre løsningen er ikke mye mer komplisert, men krever litt out-of-the-box tenkning. Vi skal dekke dem begge.

Sette opp scenen

La oss sette opp scenen først. Brann opp enhet, start et nytt prosjekt, og still inn kameraposisjonen til x = 0, y = 0 (z kan være hva du vil). Du vil sannsynligvis at kameraet skal være i ortografisk modus; vår skjermpakke vil fungere i perspektivmodus, men det ser kanskje ikke ut som du vil. Du er velkommen til å eksperimentere.

Redaktørens notat: Se Clemens kommentar for informasjon om å gjøre dette arbeidet i perspektivmodus.

Legg til et objekt som skal vikle rundt og få det til å bevege seg. Du kan bruke ShipMovementBehaviour skript fra demo-kilden.

I mitt tilfelle har jeg et enkelt romskip (en kjegle) med asteroider-lignende bevegelse. Som du kan se på bildet, foretrekker jeg å ha nettverket foreldret til hovedobjektet. Enten du gjør det sånn eller ikke, om du har ett eller flere masker, spiller det ingen rolle. Vi skal lage et fint skjermomslag som fungerer i alle fall.

Enkel innpakning

Den grunnleggende ideen bak skjerminnpakningen er dette:

  1. Kryss av om objektet gikk på skjermen.
  2. Finne ut hvor det gikk på skjermen. Gikk det over venstre kant eller høyre? Toppen eller bunnen?
  3. Teleporter objektet rett bak motsatte kanten av skjermen. For eksempel, hvis den går over den venstre kanten, teleporterer vi den bak høyre kant. Vi teleporterer objektet bak den motsatte kanten, slik at den faktisk ser ut som om den er omsluttet i stedet for å bli teleportert.

Gjør dette i enhet

Så det første vi vil gjøre er å sjekke om objektet gikk helt utenom skjermen. En enkel måte å gjøre dette på i Unity er ved å sjekke om objektets renderere er synlige. Hvis de ikke er det, betyr det at objektet er helt off-kamera og dermed off-screen.

La oss hente renderers på Start() og gjør en verktøyfunksjon for å sjekke dem:

Renderer [] renderers; void Start () renderers = GetComponentsInChildren ();  bool CheckRenderers () foreach (var renderer i renderers) // Hvis minst en gjengivelse er synlig, returner sann hvis (renderer.isVisible) return true;  // Ellers er objektet usynlig retur falsk; 

Vi kan nå fortelle om objektet vårt gikk av skjermen, men vi må fortsatt finne ut hvor det gikk av, og teleporterte det til motsatt side. For å gjøre dette kan vi se på aksene separat. For eksempel, hvis vårt skips x-posisjon er utenfor skjermgrensene, betyr det at det gikk av enten til venstre eller til høyre.

Den enkleste måten å sjekke på er å først konvertere skipets verdensposisjon til å se posisjonen, og deretter sjekke. På denne måten vil det fungere om du bruker et ortografisk eller et perspektivkamera.

var cam = Camera.main; var viewportPosition = cam.WorldToViewportPoint (transform.position);

For å gjøre ting klarere, la meg forklare viewport koordinater. Viewport plass er i forhold til kameraet. Koordinatene varierer fra 0 til 1 for alt som er på skjermen, noe som betyr:

  • x = 0 er koordinaten til venstre kant av skjermen.
  • x = 1 er koordinaten til høyre kant av skjermen.

på samme måte,

  • y = 0 er den nederste skjermkanten koordinat.
  • y = 1 er toppskjermkoordinaten.

Dette betyr at hvis et objekt er utenfor skjermen, vil det enten ha en negativ koordinat (mindre enn 0) eller en koordinat større enn 1.

Siden kameraets posisjon er på x = 0, y = 0, scenen er lagt ut som et speil. Alt til høyre har positive x-koordinater; alt til venstre, negativt. Alt i topphalvdelen har positive y-koordinater; alt i den nederste halvdelen, negativ. Så, for å plassere vårt objekt på motsatt side av skjermen, dreier vi bare om sin posisjon langs riktig akse. For eksempel:

  • Hvis vårt skip flytter til høyre og dets posisjon er (20, 0), blir det (-20, 0).
  • Hvis vårt skip beveger seg over den nederste kanten og posisjonen er (0, -15) blir den (0, 15).

Legg merke til at vi forvandler skipets forvandle posisjon, ikke dens view stilling.


I kode ser det slik ut:

var newPosition = transform.position; hvis (viewportPosition.x> 1 || viewportPosition.x < 0)  newPosition.y = -newPosition.y;  if (viewportPosition.y > 1 || viewportPosition.y < 0)  newPosition.y = -newPosition.y;  transform.position = newPosition;

Hvis du kjører prosjektet nå, vil det fungere fint mesteparten av tiden. Men noen ganger kan objektet ikke vikle rundt. Dette skjer fordi vårt objekt hele tiden bytter posisjoner mens det ikke er på skjermen, i stedet for bare en gang. Vi kan forhindre dette ved å legge til et par kontrollvariabler:

bool isWrappingX = false; bool isWrappingY = false;

Alt skal fungere perfekt nå, og den endelige skjermomslagskoden skal se slik ut:

void ScreenWrap () var isVisible = CheckRenderers (); hvis (erVisible) isWrappingX = false; isWrappingY = false; komme tilbake;  hvis (isWrappingX && isWrappingY) return;  var cam = Camera.main; var viewportPosition = cam.WorldToViewportPoint (transform.position); var newPosition = transform.position; hvis (! isWrappingX && (viewportPosition.x> 1 || viewportPosition.x < 0))  newPosition.x = -newPosition.x; isWrappingX = true;  if (!isWrappingY && (viewportPosition.y > 1 || viewportPosition.y < 0))  newPosition.y = -newPosition.y; isWrappingY = true;  transform.position = newPosition; 

Avansert innpakning

Den enkle innpakningen fungerer bra, men det kan se bedre ut. I stedet for at objektet går på skjermen før det pakkes rundt, kan du ha perfekt innpakning, som i bildet nedenfor:


Den enkleste måten å gjøre dette på er å jukse litt og ha flere skip på scenen. På denne måten vil vi skape en illusjon av et enkelt skip som omsluttes. Vi skal ha åtte ekstra skip (jeg skal ringe til dem spøkelser): en for hver kant og en for hvert hjørne av skjermen.

Vi vil at disse spøkelsesskipene skal være synlige bare når spilleren kommer til en kant. For å gjøre det, må vi posisjonere dem på enkelte avstander fra hovedskipet:

  • To skip plasserte en skjermbredde vekk til venstre og til høyre, henholdsvis.
  • To skip plasserte en skjermhøyde bort over og under, henholdsvis.
  • Fire hjørneskip plassert en skjermbredde vekk horisontalt og en skjermhøyde vekk vertikalt.

Gjør dette i enhet

Vi må hente skjermstørrelsen først, slik at vi kan posisjonere våre spøkelseskip. Saken er, vi trenger skjermstørrelsen i verdens koordinater i forhold til spillerskipet. Det spiller ingen rolle om vi bruker et ortografisk kamera, men med perspektivvisning er det svært viktig for spøkelseskipene å være på samme z-koordinat som hovedskipet. 

Så, for å gjøre dette på en fangst-all måte, skal vi forvandle visningsportkoordinatene til de øverste og nederste venstre skjermhjørnene til verdenskoordinatene som ligger på samme z-akse som hovedskipet. Vi bruker disse koordinatene til å beregne skjermbredden og høyden i verdensenheter i forhold til vårt skips posisjon.

Erklære screenWidth og screenHeight som klassevariabler og legg til dette til Start():

var cam = Camera.main; var screenBottomLeft = cam.ViewportToWorldPoint (ny Vector3 (0, 0, transform.position.z)); var screenTopRight = cam.ViewportToWorldPoint (ny Vector3 (1, 1, transform.position.z)); screenWidth = screenTopRight.x - screenBottomLeft.x; screenHeight = screenTopRight.y - screenBottomLeft.y;

Nå som vi kan plassere dem riktig, la oss gyte spøkelsesskipene. Vi bruker en matrise for å lagre dem:

Transform [] ghosts = new Transform [8];

Og la oss lage en funksjon som vil gjøre gytingen. Jeg skal klone hovedskipet for å skape spøkelsene, og så fjerner jeg ScreenWrapBehaviour fra dem. Hovedskipet er det eneste som burde ha ScreenWrapBehaviour, fordi det kan ha full kontroll over spøkelsene, og vi vil ikke at spøkelsene skal gyte sine egne spøkelser. Du kan også ha en egen prefab for spøkelseskipene og instantiere det; Dette er veien å gå hvis du vil at spøkelsene skal ha noen spesiell oppførsel.

void CreateGhostShips () for (int i = 0; i < 8; i++)  ghosts[i] = Instantiate(transform, Vector3.zero, Quaternion.identity) as Transform; DestroyImmediate(ghosts[i].GetComponent());  

Vi plasserer deretter spøkelsene som i bildet ovenfor:

void PositionGhostShips () // Alle spøkelsesstillinger vil være i forhold til skipene (dette) transformere, / / ​​så la oss stjerne med det. var ghostPosition = transform.position; // Vi plasserer spøkelsene med klokken bak kantene på skjermen. // La oss starte med langt til høyre. ghostPosition.x = transform.position.x + screenWidth; ghostPosition.y = transform.position.y; spøkelser [0] .position = ghostPosition; // Bottom-right ghostPosition.x = transform.position.x + screenWidth; ghostPosition.y = transform.position.y - screenHeight; spøkelser [1] .position = ghostPosition; // Bottom ghostPosition.x = transform.position.x; ghostPosition.y = transform.position.y - screenHeight; spøkelser [2] .position = ghostPosition; // nederst til venstre ghostPosition.x = transform.position.x - screenWidth; ghostPosition.y = transform.position.y - screenHeight; spøkelser [3] .position = ghostPosition; // Venstre ghostPosition.x = transform.position.x - screenWidth; ghostPosition.y = transform.position.y; spøkelser [4] .position = ghostPosition; // Topp venstre ghostPosition.x = transform.position.x - screenWidth; ghostPosition.y = transform.position.y + screenHeight; spøkelser [5] .position = ghostPosition; // Topp ghostPosition.x = transform.position.x; ghostPosition.y = transform.position.y + screenHeight; spøkelser [6] .position = ghostPosition; // Topp-høyre ghostPosition.x = transform.position.x + screenWidth; ghostPosition.y = transform.position.y + screenHeight; spøkelser [7] .position = ghostPosition; // Alle spøkelsesskip skal ha samme rotasjon som hovedskipet for (int i = 0; i < 8; i++)  ghosts[i].rotation = transform.rotation;  

Kjør prosjektet ditt og prøv det. Hvis du ser på scenevisningen, vil du se at alle spøkelsesskipene flytter med hovedskipet og snu når det blir. Vi kodes ikke eksplisitt dette, men det fungerer fortsatt. Har du en ide hvorfor?

Ghost skip er kloner av hovedskipet uten ScreenWrappingBehaviour. De skal fortsatt ha den separate bevegelsesatferden, og siden de alle får samme inngang, beveger de alle de samme. Hvis du vil gyte spøkelsene fra en prefab, ikke glem å ta med en bevegelseskomponent eller et annet skript som vil synkronisere bevegelsen med hovedskipet.

Alt ser ut til å fungere bra nå, ikke sant? Vel, nesten. Hvis du fortsetter å gå i en retning, vil første gang det brytes, det fungerer fint, men når du kommer til kanten igjen, vil det ikke være et skip på den andre siden. Fornuftig, siden vi ikke gjør noen teleportering denne gangen. La oss fikse det.

Når hovedskipet går utenfor kanten, vil et spøkelseskip være på skjermen. Vi må bytte posisjoner og deretter plassere spøkelsesskipene rundt hovedskipet. Vi har allerede en rekke spøkelser, vi trenger bare å avgjøre hvilken av dem som er på skjermen. Da gjør vi bytte og reposisjonering. I kode:

void SwapShips () foreach (var spøkelse i spøkelser) if (ghost.position.x < screenWidth && ghost.position.x > -skjermbredde og ghost.position.y < screenHeight && ghost.position.y > -skjermhøyde) transform.position = ghost.position; gå i stykker;  PositionGhostShips (); 

Prøv det nå, og alt skal fungere perfekt.

Siste tanker

Du har nå en komponent for arbeidsskjermer. Om dette er nok for deg, avhenger av spillet du lager og hva du prøver å oppnå.

Enkel innpakning er ganske enkel å bruke: Bare fest den til et objekt, og du trenger ikke å bekymre deg for dens oppførsel. På den annen side må du være forsiktig hvis du bruker avansert innpakning. Tenk deg en situasjon hvor en kule eller en asteroide rammer et spøkelseskip: du må forplante kollisjonshendelser til hovedskipet, eller et eksternt kontrollerobjekt.

Du vil kanskje også at spillobjektene dine skal vikle sammen bare en akse. Vi gjør allerede separate sjekker for hver akse, så det er bare et spørsmål om å legge til et par boolesker til koden.

En mer interessant ting å vurdere: hva om du ville at kameraet skal bevege seg litt i stedet for å bli løst i rommet? Kanskje du vil ha en arena som er større enn skjermen. I så fall kan du fortsatt bruke det samme innpakningsskriptet. Du trenger bare en separat oppførsel som styrer begrenser kameraets bevegelse. Siden koden vår er basert på visningsposisjon, spiller kameraets plassering i spillet ingen rolle.

Du har sikkert noen ideer av deg selv nå. Så fortsett, prøv dem og gjør noen spill!

referanser

  • Bildetekst: Raket av William J. Salvador fra Noun-prosjektet