Opprette et enkelt 3D Endless Runner-spill ved hjelp av Three.js

Hva du skal skape

Nettplattformen har hatt en enorm vekst i nyere tid ved hjelp av HTML5, WebGL og den økte kraften i den nåværende generasjonen av enheter. Nå er mobile enheter og nettlesere i stand til å levere høypresterende innhold både i 2D og 3D. Forståelsen av JavaScript (JS) som skriptspråk har også vært en drivende faktor etter at Flash-plattformen har gått ned.. 

De fleste webutviklere er godt klar over hvor komplisert JS økosystemet er med alle de ulike rammene og standardene som er tilgjengelige, noe som noen ganger kan være overveldende for en ny utvikler. Men når det gjelder 3D, er valgene enkle, takk til Mr.Doob. Hans Three.js er for tiden det beste alternativet der ute for å skape høyverdig 3D WebGL-innhold. Et annet kraftig alternativ er Babylon.js, som også kan brukes til å lage 3D-spill.

I denne opplæringen lærer du å lage et enkelt, uendelig runner-stil, innfødt 3D-spill med det kraftige Three.js-rammeverket. Du vil bruke piltastene til å styre en snøball som ruller nedover en fjellside for å unnvike trærne i veien. Det er ingen kunst involvert, og alle visualer er opprettet i kode.

1. Grunnleggende 3D-scene

Envato Tuts + har allerede noen opplæringsprogrammer som kan komme i gang med Three.js. Her er noen av dem for å komme i gang.

  • En Noobs guide til Three.js
  • WebGL med Three.js: Grunnleggende
  • Three.js for spillutvikling

La oss lage en grunnleggende 3D-scene først, som vist her hvor det er en roterende terning. Du kan bruke musen til å bane rundt kuben.

Enhver grafikk som vises på en todimensjonal skjerm, er praktisk talt 2D i naturen, med noen få viktige elementer som gir 3D-illusjonen: belysningen, skyggen, skyggene og 3D til 2D-projeksjonens magi som skjer via kameraet. I den ovennevnte scenen aktiverer vi effektiv belysning ved hjelp av disse kodelinjene.

kamera = nytt THREE.PerspectiveCamera (60, scenevidde / sceneHeight, 0,1, 1000); // perspektiv kamera renderer = nytt THREE.WebGLRenderer (ala: true); // renderer med gjennomsiktig bakgrunn renderer.shadowMap.enabled = true; // aktiver skygge renderer.shadowMap.type = THREE.PCFSoftShadowMap; // ... hero = nytt THREE.Mesh (heroGeometry, heroMaterial); hero.castShadow = true; hero.receiveShadow = false; // ... ground.receiveShadow = true; ground.castShadow = false; // ... sol = ny THREE.DirectionalLight (0xffffff, 0.8); sun.position.set (0,4,1); sun.castShadow = true; scene.add (sol); // Sett opp skyggeegenskaper for sollyset sun.shadow.mapSize.width = 256; sun.shadow.mapSize.height = 256; sun.shadow.camera.near = 0.5; sun.shadow.camera.far = 50;

De renderer må ha shadowMap aktivert, scenen må ha et lys med kaste skygge aktivert, og alle 3D-objekter trenger kaste skygge og receiveShadow eiendommer satt riktig. For riktig skygge skal skje, bør vi også bruke MeshStandardMaterial eller et mer funksjonsrikt materiale for våre 3D-objekter. Kameraet styres ved hjelp av det skarpe OrbitControls-skriptet. Jeg vil anbefale å spille med den grunnleggende 3D-scenen ved å legge til flere primitive figurer eller spille med belysningen, etc. før du fortsetter med opplæringen.

2. Endless Runner Concept

Det finnes mange typer uendelige løpespill, og vår er en "endeløs rulle". Vi vil skape et spill hvor en snøball ruller ned en uendelig fjellside der vi bruker piltastene til å unnvike de innkommende trærne. En interessant ting er at dette enkle spillet ikke vil involvere noen kunstverdier, da alle komponentene vil bli opprettet med kode. Her er hele spillet å leke rundt.

3. Komponenter av spillet

Hovedkomponentene eller elementene i spillet er: 

  • den rullende snøball
  • tilfeldige trær
  • rullende bakken
  • avstandsdåpen
  • kollisjonseffekten

Vi vil utforske hver av disse en etter en i følgende avsnitt.

Tåken

De tåke er en egenskap av 3D-scenen i Tre. Det er alltid et praktisk triks å bruke for å simulere dybden eller vise en horisont. Tåkenes farge er viktig for at illusjonen skal fungere skikkelig og avhenger av fargene på scenen og belysningen. Som du kan se i koden nedenfor, setter vi også inn renderer's clearColor verdi å være nær fargen på tåke.

scene = nytt THREE.Scene (); scene.fog = nytt THREE.FogExp2 (0xf0fff0, 0,14); kamera = nytt THREE.PerspectiveCamera (60 sceneVidth / sceneHeight, 0,1, 1000); // perspektiv kamera renderer = nytt THREE.WebGLRenderer (ala: true); // renderer med gjennomsiktig bakgrunn renderer.setClearColor (0xfffafa, 1) ; 

For å matche atmosfæren bruker vi også lignende fargeverdier til lysene som brukes i scenen. Hver omgivende farge er en annen nyanse av hvitt som geler sammen for å skape den nødvendige effekten.

var hemisphereLight = new THREE.HemisphereLight (0xfffafa, 0x000000, .9) scene.add (hemisphereLight); sol = nytt tre DIREKTE LYS (0xcdc1c5, 0,9); solstilling (12,6, -7); sun.castShadow = true; scene.add (sol);

The Snowball

Vår snøball er en DodecahedronGeometry tre primitiv form opprettet som vist nedenfor.

var sphereGeometry = ny THREE.DodecahedronGeometry (heroRadius, 1); var sphereMaterial = nytt THREE.MeshStandardMaterial (color: 0xe5f2f2, shading: THREE.FlatShading) heroSphere = nytt THREE.Mesh (sphereGeometry, sphereMaterial);

For alle 3D-elementene i dette spillet bruker vi THREE.FlatShading for å få ønsket lavpoly-utseende.

The Scrolling Mountain

Den rullende bakken heter rollingGroundSphere er en stor SphereGeometry primitiv, og vi roterer den på x akse for å lage den bevegelige bakken illusjonen. Snøballen ruller egentlig ikke over noe; Vi skaper bare illusjonen ved å holde bakken sfærende og samtidig holde snøballen stasjonær. 

En normal sfære primitiv vil se veldig jevnt ut og vil derfor ikke gi den nødvendige robustheten som trengs for fjellhellingen. Så vi gjør noen vertex manipulasjoner for å forandre glatt overflate til et robust terreng. Her er den tilhørende koden etterfulgt av en forklaring.

var sider = 40; var tiers = 40; var sphereGeometry = new THREE.SphereGeometry (worldRadius, sides, tiers); var sphereMaterial = nytt THREE.MeshStandardMaterial (color: 0xfffafa, shading: THREE.FlatShading) var vertexIndex; var vertexVector = nytt THREE.Vector3 (); var nextVertexVector = nytt THREE.Vector3 (); var firstVertexVector = nytt THREE.Vector3 (); var offset = nytt THREE.Vector3 (); var currentTier = 1; var lerpValue = 0,5; var heightValue; var maxHeight = 0,07; for (var j = 1; j

Vi lager en primitiv sfære med 40 horisontale segmenter (sider) og 40 vertikale segmenter (lag). Hvert toppunkt av en tre geometri kan nås via toppunkter array eiendom. Vi går gjennom alle tiers mellom de ekstreme topp- og ekstreme bunnpunktene for å gjøre våre vertexmanipulasjoner. Hvert nivå av kule geometrien inneholder nøyaktig sider Antall kryss, som danner en lukket ring rundt sfæren. 

Det første trinnet er å rotere hver merkelig ring av vertices for å bryte enhetligheten av overflatekonturene. Vi beveger hvert toppunkt i ringen med en tilfeldig fraksjon mellom 0,25 og 0,75 av avstanden til neste toppunkt. Som et resultat av dette er kuglens vertikale hjørner ikke justert i en rett linje lenger, og vi får en fin sikksagkontur. 

Som det andre trinnet gir vi hvert toppunkt en tilfeldig høydejustering som er justert i forhold til normal i vertexet, uavhengig av hvilken nivå den tilhører. Dette resulterer i en ujevn og robust overflate. Jeg håper vektormatematikken som brukes her, er grei når du vurderer at senterets senter betraktes som opprinnelsen (0,0).

Trærne

Trærne ser ut utenfor vårt rullende spor for å legge dybde til verden, og innvendig som hindringer. Å lage treet er litt mer komplisert enn den robuste bakken, men følger den samme logikken. Vi bruker en ConeGeometry primitiv for å lage den øverste grønne delen av treet og a CylinderGeometry å lage den nederste delen av kofferten. 

For den øverste delen går vi gjennom hvert lag av krysser og utvider ringenes ring etterfulgt av krymping ned neste ring. Følgende kode viser blowUpTree Metode som brukes til å utvide den alternative ringen av vinkler utover og tightenTree Metoden brukes til å krympe ned neste ring av hjørner.

funksjon createTree () var sides = 8; var tiers = 6; var scalarMultiplier = (Math.random () * (0,25-0,1)) + 0,05; var midPointVector = nytt THREE.Vector3 (); var vertexVector = nytt THREE.Vector3 (); var treeGeometry = nye THREE.ConeGeometry (0,5, 1, sider, nivåer); var treeMaterial = nytt THREE.MeshStandardMaterial (color: 0x33ff33, shading: THREE.FlatShading); var offset; midPointVector = treeGeometry.vertices [0] .clone (); var currentTier = 0; var vertexIndex; blowUpTree (treeGeometry.vertices, sider, 0, scalarMultiplier); tightenTree (treeGeometry.vertices, sider, 1); blowUpTree (treeGeometry.vertices, sider, 2, scalarMultiplier * 1,1, true); tightenTree (treeGeometry.vertices, sider, 3); blowUpTree (treeGeometry.vertices, sider, 4, scalarMultiplier * 1,2); tightenTree (treeGeometry.vertices, sider, 5); var treeTop = nytt THREE.Mesh (treeGeometry, treeMaterial); treeTop.castShadow = true; treeTop.receiveShadow = false; treeTop.position.y = 0,9; treeTop.rotation.y = (Math.random () * (Math.PI)); var treeTrunkGeometry = nytt THREE.CylinderGeometry (0,1, 0,1,0,5); var trunkMaterial = nytt THREE.MeshStandardMaterial (color: 0x886633, shading: THREE.FlatShading); var treeTrunk = nytt THREE.Mesh (treeTrunkGeometry, trunkMaterial); treeTrunk.position.y = 0,25; var tree = nytt THREE.Object3D (); tree.add (treetrunk); tree.add (tretopp); tilbake treet;  funksjon blowUpTree (vertices, sides, currentTier, scalarMultiplier, oddetall) var vertexIndex; var vertexVector = nytt THREE.Vector3 (); var midPointVector = vertices [0] .clone (); var offset; for (var i = 0; i

De blowUpTree Metoden skyver ut hvert alternativ toppunkt i en ring av hjørner, samtidig som de andre kryssene i ringen holdes i en mindre høyde. Dette skaper spisse grener på treet. Hvis vi bruker de ulike sporene i ett nivå, bruker vi de samme punktene i neste nivå slik at ensartetheten blir ødelagt. Når det komplette treet er dannet, gir vi det en tilfeldig rotasjon på y-aksen for å gjøre det litt annerledes.

Eksplosjonseffekten

Blokkepixeleksplosjonseffekten er ikke den mest elegante vi kan bruke, men det fungerer sikkert godt. Denne spesielle partikkel-effekten er faktisk en 3D-geometri som manipuleres til å se ut som en effekt ved hjelp av THREE.Points klasse. 

funksjon addExplosion () particleGeometry = new THREE.Geometry (); for (var i = 0; i < particleCount; i ++ )  var vertex = new THREE.Vector3(); particleGeometry.vertices.push( vertex );  var pMaterial = new THREE.ParticleBasicMaterial( color: 0xfffafa, size: 0.2 ); particles = new THREE.Points( particleGeometry, pMaterial ); scene.add( particles ); particles.visible=false;  function explode() particles.position.y=2; particles.position.z=4.8; particles.position.x=heroSphere.position.x; for (var i = 0; i < particleCount; i ++ )  var vertex = new THREE.Vector3(); vertex.x = -0.2+Math.random() * 0.4; vertex.y = -0.2+Math.random() * 0.4 ; vertex.z = -0.2+Math.random() * 0.4; particleGeometry.vertices[i]=vertex;  explosionPower=1.07; particles.visible=true;  function doExplosionLogic()//called in update if(!particles.visible)return; for (var i = 0; i < particleCount; i ++ )  particleGeometry.vertices[i].multiplyScalar(explosionPower);  if(explosionPower>1.005) eksplosjonPower- = 0.001;  ellers particles.visible = false;  particleGeometry.verticesNeedUpdate = true; 

De addExplosion Metoden legger til 20 hjørner til toppunkter utvalg av particleGeometry. De eksplodere Metoden kalles når vi trenger effekten til å løpe, som tilfeldigvis posisjonerer hvert toppunkt av geometrien. De doExplosionLogic blir kalt i Oppdater Metode hvis partikkelobjektet er synlig, hvor vi beveger hvert toppunkt utover. Hvert toppunkt i a punkter objekt blir gjengitt som en firkantet blokk.

4. Gameplay

Nå som vi vet hvordan du lager hver av elementene som trengs for spillet, la oss komme inn i spillet. De viktigste spillelementene er:

  • spillsløyfen
  • Plasseringen av trærne
  • brukerinteraksjonen
  • kollisjonsdeteksjonen

La oss analysere dem i detalj.

The Game Loop

Alt kjernemekanikeren skjer i spillsløyfen, som i vårt tilfelle er Oppdater metode. Vi kaller det for første gang fra i det metode, som blir kalt på vinduets last. Etter dette hooks det på dokumentet gjengeløkken ved hjelp av requestAnimationFrame metode slik at det blir kalt flere ganger. 

funksjon oppdatering () rollingGroundSphere.rotation.x + = rollingSpeed; heroSphere.rotation.x - = heroRollingSpeed; if (heroSphere.position.y<=heroBaseY) jumping=false; bounceValue=(Math.random()*0.04)+0.005;  heroSphere.position.y+=bounceValue; heroSphere.position.x=THREE.Math.lerp(heroSphere.position.x,currentLane, 2*clock.getDelta());//clock.getElapsedTime()); bounceValue-=gravity; if(clock.getElapsedTime()>treeReleaseInterval) clock.start (); addPathTree (); hvis (! harCollided) score + = 2 * treeReleaseInterval; scoreText.innerHTML = score.toString ();  doTreeLogic (); doExplosionLogic (); render (); requestAnimationFrame (update); // request next update funksjon gjengivelse () renderer.render (scene, kamera); // draw

Oppdater, vi kaller gjengi metode, som bruker renderer å tegne scenen. Vi kaller doTreeLogic metode, som sjekker for kollisjon og fjerner også trærne når de har gått ut av syn. 

Snøballet og bakken sfærer blir rotert mens vi også legger til en tilfeldig hoppende logikk til snøballet. Nye trær plasseres i banen ved å ringe addPathTree etter at en forhåndsdefinert tid er gått. Tiden spores ved hjelp av a THREE.Clock gjenstand. Vi oppdaterer også poengsum med mindre en kollisjon har oppstått.

Plassering av trærne

Ett sett med trær er plassert utenfor rullestien for å skape verden ved hjelp av addWorldTrees metode. Alle trær blir lagt til som barn av rollingGroundSphere slik at de også beveger seg når vi roterer sfæren. 

funksjon addWorldTrees () var numTrees = 36; var gap = 6,28 / 36; for (var i = 0; i

Å plante verdenstrær, vi kaller addTree Metode ved å overføre verdier rundt omkretsen av vår jordkule. De sphericalHelper verktøyet hjelper oss med å finne stillingen på overflaten av en sfære.

For å plante trær på stien, vil vi gjøre bruk av et basseng av trær som er opprettet ved å begynne å bruke createTreesPool metode. Vi har også forhåndsdefinerte vinkelverdier for hver bane på sfæren som er lagret i pathAngleValues matrise.

pathAngleValues ​​= [1.52,1.57,1.62]; // ... funksjon createTreesPool () var maxTreesInPool = 10; var newTree; for (var i = 0; i0,5) lane = Math.floor (Math.random () * 2); addTree (sant, opsjoner [kjørefelt]); 

De addPathTree Metoden kalles fra oppdatering når det er nok tid siden etter planting av det siste treet. Det kalles i sin tur addTree metode vist tidligere med et annet sett med parametre hvor treet blir plassert i den valgte banen. De doTreeLogic Metoden vil returnere treet til bassenget når det går ut av visningen.

Brukerinteraksjon

Vi legger til en lytter til dokumentet for å se etter relevante tastaturhendelser. De handleKeyDown Metoden setter currentLane verdi hvis høyre eller venstre piltast trykkes eller setter bounceValue verdi hvis opp pil er trykket.

document.onkeydown = handleKeyDown; // ... funksjon handleKeyDown (keyEvent) hvis (hopping) tilbake; var validMove = true; hvis (keyEvent.keyCode === 37) // left if (currentLane == middleLane) currentLane = leftLane;  annet hvis (currentLane == rightLane) currentLane = middleLane;  else validMove = false;  annet hvis (keyEvent.keyCode === 39) // rett hvis (currentLane == middleLane) currentLane = rightLane;  annet hvis (currentLane == leftLane) currentLane = middleLane;  else validMove = false;  annet hvis (keyEvent.keyCode === 38) // opp, hopp bounceValue = 0,1; hopping = true;  validMove = false;  hvis (validMove) jumping = true; bounceValue = 0,06; 

I Oppdater, de x posisjonen til vår snøball økes sakte for å nå currentLane plasser det ved å bytte baner. 

Kollisjonsdeteksjon

Det er ingen ekte fysikk involvert i dette spillet, selv om vi kunne bruke ulike fysikkrammer for vårt kollisjonssensjonsformål. Men som du er klar over, legger en fysikkmotor mye ytelse overfor spillet vårt, og vi bør alltid prøve å se om vi kan unngå det. 

I vårt tilfelle beregner vi bare avstanden mellom vår snøball og hvert tre for å utløse en kollisjon hvis de er veldig nært. Dette skjer i doTreeLogic metode, som blir kalt fra Oppdater.

funksjon doTreeLogic () var oneTree; var treePos = nytt THREE.Vector3 (); treesInPath.forEach (funksjon (element, indeks) oneTree = treesInPath [index]; treePos.setFromMatrixPosition (oneTree.matrixWorld); hvis (treePos.distanceTo (heroSphere.position)<=0.6) console.log("hit"); hasCollided=true; explode();  ); //… 

Som du kanskje har lagt merke til, lagres alle trær som er tilstede i vår vei, i treesInPath array. De doTreeLogic Metoden fjerner også trærne fra skjerm og inn i bassenget når de går ut av vår visning ved hjelp av koden som er vist nedenfor.

var treesToRemove = []; trærInPath.forEach (funksjon (element, indeks) oneTree = treesInPath [indeks]; treePos.setFromMatrixPosition (oneTree.matrixWorld); hvis (treePos.z> 6 && oneTree.visible) // gått ut av vår visningssone treesToRemove.push (oneTree);); var fra hvor; treesToRemove.forEach (funksjon (element, indeks) oneTree = treesToRemove [index]; fromWhere = treesInPath.indexOf (oneTree); treesInPath.splice (fromWhere, 1); treesPool.push (oneTree); oneTree.visible = false; .log ("remove tree"););

Konklusjon

Å lage et 3D-spill er en komplisert prosess hvis du ikke bruker et visuelt verktøy som Unity. Det kan virke skremmende eller overveldende, men la meg forsikre deg om at når du får tak i det, vil du føle deg mye kraftigere og kreativere. Jeg vil at du skal utforske videre ved hjelp av de ulike fysikkrammene eller partikkelsystemene eller de offisielle eksemplene.