I serien hittills har vi kodat spelet, tillfogade fiender och kryddade saker med blom och partikeleffekter. I den här sista delen kommer vi att skapa ett dynamiskt, krusande bakgrundsrör.
Den här videon visar nätet i åtgärd:
Vi ska göra nätet med en fjädersimulering: vid varje skärning av gallret lägger vi en liten vikt (en punktmassa), och vi kopplar samman dessa vikter med hjälp av fjädrar. Dessa fjädrar kommer bara att dra och aldrig trycka, som ett gummiband. För att hålla nätet på plats, kommer massorna runt gridens gräns att förankras på plats.
Nedan är ett diagram över layouten.
Vi skapar en klass som heter Rutnät
för att skapa denna effekt. Men innan vi arbetar på själva nätet måste vi göra två hjälparklasser: Vår
och PointMass
.
De PointMass
klassen representerar de massor som vi kommer att fästa fjädrarna på. Fjädrar kopplas aldrig direkt till andra fjädrar. I stället tillämpar de en kraft på de massor de ansluter, vilket i sin tur kan sträcka andra fjädrar.
allmän klass PointMass privat Vector3f position; privat Vector3f hastighet = Vector3f.ZERO; privat float inverseMass; privat Vector3f acceleration = Vector3f.ZERO; privat floatdämpning = 0,98f; allmän PointMass (Vector3f position, float inverseMass) this.position = position; this.inverseMass = inverseMass; public void applyForce (Vector3f force) acceleration.addLocal (force.mult (inverseMass)); offentligt tomrumsökningDampning (flytfaktor) dämpning * = faktor; public void update (float tpf) hastighet.addLokal (acceleration.mult (1f)); position.addLocal (velocity.mult (0.6f)); acceleration = Vector3f.ZERO.clone (); om (hastighet.längdSquared () < 0.0001f) velocity = Vector3f.ZERO.clone(); velocity.multLocal(damping); damping = 0.98f; damping = 0.8f; position.z *= 0.9f; if (position.z < 0.01) position.z = 0; public Vector3f getPosition() return position; public Vector3f getVelocity() return velocity;
Det finns några intressanta punkter om den här klassen. Först märker du att det lagrar omvänd av massan, 1 / massa
. Det här är ofta en bra idé i fysiksimuleringar, eftersom fysikekvationer tenderar att använda massens inversare oftare, och eftersom det ger oss ett enkelt sätt att representera oändligt tunga, obotliga föremål genom att ställa in den omvända massan till noll.
Klassen innehåller också en dämpning variabel, vilket verkar för att gradvis sakta ner massan. Detta används ungefär som friktion eller luftmotstånd. Detta bidrar till att nätet i slutändan kommer att vila och ökar också stabiliteten i vårens simulering.
De Uppdatering()
Metoden gör arbetet med att flytta punktmassan varje ram. Det börjar med att göra en symplektisk Euler-integration, vilket bara innebär att vi lägger till accelerationen till hastigheten och sedan lägger till den uppdaterade hastigheten på positionen. Detta skiljer sig från standard Euler integration där vi skulle uppdatera hastigheten efter uppdatering av positionen.
Efter uppdatering av hastighet och position kontrollerar vi om hastigheten är väldigt liten och, om så är fallet, ställer vi den till noll. Detta kan vara viktigt för prestanda på grund av arten av denormaliserade flytpunkten.
De IncreaseDamping ()
Metoden används för att tillfälligt öka mängden dämpning. Vi kommer att använda detta senare för vissa effekter.
En fjäder ansluter två punktmassor och, om den sträcker sig över sin naturliga längd, applicerar en kraft som drar massorna samman. Springs följer en modifierad version av Hooke's Law med dämpning:
\ [f = -kx - bv \]
Koden för Vår
klassen är som följer:
offentlig klass våren privat PointMass end1; privat PointMass end2; privat float targetLength; privat float styvhet; privat floatdämpning; offentlig vår (PointMass end1, PointMass end2, float styvhet, float dämpning, Node gridNode, booleskt synligt, Geometry defaultLine) this.end1 = end1; this.end2 = end2; this.stiffness = styvhet; this.damping = dämpning; targetLength = end1.getPosition () .avstånd (end2.getPosition ()) * 0.95f; om (synlig) defaultLine.addControl (ny LineControl (end1, end2)); gridNode.attachChild (defaultLine); public void update (float tpf) Vector3f x = end1.getPosition (). subtrahera (end2.getPosition ()); floatlängd = x.length (); om (längd> mållängd) x.normalizeLocal (); x.multLocal (längd-targetLength); Vector3f dv = end2.getVelocity (). Subtrahera (end1.getVelocity ()); Vector3f force = x.mult (styvhet); force.subtract (dv.mult (dämpning / 10f)); end1.applyForce (force.negate ()); end2.applyForce (kraft);
När vi skapar en fjäder sätter vi vårens naturliga längd på bara något mindre än avståndet mellan de två ändpunkterna. Detta håller gallret stramt, även i vila och förbättrar utseendet något.
De Uppdatering()
Metoden kontrollerar först om fjädern sträcker sig bortom sin naturliga längd. Om det inte sträcker sig, händer ingenting. Om det är så använder vi den modifierade Hooke's Law för att hitta kraften från våren och tillämpa den på de två anslutna massorna.
Det finns en annan klass som vi behöver skapa för att visa linjerna korrekt. De LineControl
kommer att ta hand om att flytta, skala och rotera linjerna:
allmän klass LineControl utökar AbstractControl privat PointMass end1, end2; allmän LineControl (PointMass end1, PointMass end2) this.end1 = end1; this.end2 = end2; @Override protected void controlUpdate (float tpf) // rörelse spatial.setLocalTranslation (end1.getPosition ()); // skala Vector3f dif = end2.getPosition (). subtrahera (end1.getPosition ()); spatial.setLocalScale (dif.length ()); // rotation spatial.lookAt (end2.getPosition (), ny Vector3f (1,0,0)); @Override protected void controlRender (RenderManager rm, ViewPort vp)
Nu när vi har nödvändiga kapslade klasser, är vi redo att skapa nätet. Vi börjar med att skapa PointMass
objekt vid varje korsning på gallret. Vi skapar också ett antal fasta ankar PointMass
föremål för att hålla nätet på plats. Vi kopplar sedan massorna samman med fjädrar:
public class Grid private Node gridNode; privata våren [] fjädrar; privata PointMass [] [] poäng; privat geometri defaultLine; privat geometri thickLine; public grid (rektangelstorlek, Vector2f-mellanslag, nod guiNode, AssetManager assetManager) gridNode = ny nod (); guiNode.attachChild (gridNode); defaultLine = createLine (1f, assetManager); thickLine = createLine (3f, assetManager); ArrayList springList = new ArrayList (); flytstivhet = 0,28f; flytdämpning = 0,06f; int numColumns = (int) (size.width / spacing.x) + 2; int numRows = (int) (size.height / spacing.y) + 2; poäng = nya PointMass [numColumns] [numRows]; PointMass [] [] fixedPoints = new PointMass [numColumns] [numRows]; // skapa punktmassorna float xCoord = 0, yCoord = 0; för (int rad = 0; rad < numRows; row++) for (int column = 0; column < numColumns; column++) points[column][row] = new PointMass(new Vector3f(xCoord,yCoord,0),1); fixedPoints[column][row] = new PointMass(new Vector3f(xCoord,yCoord,0),0); xCoord += spacing.x; yCoord += spacing.y; xCoord = 0; // link the point masses with springs Geometry line; for (int y=0; y0) om (y% 3 == 0) line = thickLine; annars line = defaultLine; springList.add (ny vår (poäng [x-1] [y], poäng [x] [y], styvhet, dämpning, gridNode, true, line.clone ())); om (y> 0) if (x% 3 == 0) line = thickLine; annars line = defaultLine; springList.add (ny vår (poäng [x] [y-1], poäng [x] [y], styvhet, dämpning, gridNode, true, line.clone ()));
Den första för
slinga skapar både vanliga massor och obevekliga massor vid varje korsning av gallret. Vi kommer inte att använda alla de oförmögna massorna, och de oanvända massorna kommer helt enkelt att vara skräp samlade vid någon tidpunkt efter att konstruktören slutar. Vi kan optimera genom att undvika att skapa onödiga objekt, men eftersom rutnätet vanligtvis bara skapas en gång kommer det inte att göra stor skillnad.
Förutom att använda ankarpunktsmassor runt gridens gräns, kommer vi också att använda några ankermassor inuti gallret. Dessa kommer att användas för att försiktigt hjälpa till att dra tillbaka gallret till dess ursprungliga läge efter att deformeras.
Eftersom ankarpunkterna aldrig rör sig behöver de inte uppdateras varje ram. Vi kan helt enkelt koppla upp dem till fjädrarna och glömma dem. Därför har vi ingen medlemsvariabel i Rutnät
klass för dessa massor.
Det finns ett antal värden du kan tweak i skapandet av rutnätet. De viktigaste är fjädrarnas styvhet och dämpning. Stivheten och dämpningen av gränsankarna och inre ankare ställs oberoende av huvudfjädrarna. Högre styvhetsvärden gör att fjädrarna svänger snabbare och högre dämpningsvärden leder till att fjädrarna saktar fortare.
Det finns en sista sak att nämnas: createLine ()
metod.
privat geometri createLine (flyt tjocklek, AssetManager assetManager) Vector3f [] vertices = ny Vector3f (0,0,0), ny Vector3f (0,0,1); int [] index = 0,1; Mesh lineMesh = nytt Mesh (); lineMesh.setMode (Mesh.Mode.Lines); lineMesh.setLineWidth (tjocklek); lineMesh.setBuffer (VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer (vertices)); lineMesh.setBuffer (VertexBuffer.Type.Index, 1, BufferUtils.createIntBuffer (index)); lineMesh.updateBound (); Geometri lineGeom = ny geometri ("lineMesh", lineMesh); Material matWireframe = nytt material (assetManager, "Common / MatDefs / Misc / Unshaded.j3md"); . MatWireframe.getAdditionalRenderState () setFaceCullMode (RenderState.FaceCullMode.Off); matWireframe.setColor ("Color", ny ColorRGBA (0.118f, 0.118f, 0.545f, 0.25f)); . MatWireframe.getAdditionalRenderState () setBlendMode (BlendMode.AlphaAdditive); lineGeom.setMaterial (matWireframe); returlinjeGeom;
Här skapar vi i grund och botten en linje genom att ange linjens spetsar och ordningen av knutpunkterna, skapa ett nät, lägga till ett blått material och så vidare. Om du vill förstå processen med linjeupprättandet exakt kan du alltid titta på jME-handledningarna.
Varför måste linjekonstruktionen vara så komplicerad - är det inte bara en enkel linje? Ja, det är, men du måste titta på vad jME avser att vara. Vanligtvis i 3D-spel har du inte enstaka linjer eller trianglar i spelet, utan snarare modeller med texturer och animeringar. Så medan det är möjligt att generera en enda rad i jME är huvudfokuset på att importera modeller som har genererats med annan programvara, till exempel Blender.För att nätet ska kunna flyttas måste vi uppdatera det varje ram. Det här är väldigt enkelt, eftersom vi redan gjorde allt det hårda arbetet i PointMass
och Vår
klasser.
public void update (float tpf) för (int i = 0; iNu lägger vi till några metoder som manipulerar nätet. Du kan lägga till metoder för vilken typ av manipulation du kan tänka dig. Vi kommer att genomföra tre typer av manipuleringar här: trycka en del av gallret i en given riktning, trycka gallret utåt från en viss punkt och dra in gallret in i någon punkt. Alla tre kommer att påverka nätet inom en given radie från någon målpunkt.
Nedan följer några bilder av dessa manipulationer:
Våg skapad genom att trycka gallret längs z-axeln.
Kulor avvisar gallret utåt.
Suger gallret inåt.Och här är metoderna för effekterna:
public void applyDirectedForce (Vector3f kraft, Vector3f position, floatradie) for (int x = 0; xAnvända gallret i Shape Blaster
Nu är det dags att använda gallret i vårt spel. Vi börjar med att förklara en
Rutnät
variabel iMonkeyBlasterMain
och initialisera den isimpleInitApp ()
:Rektangelstorlek = ny rektangel (0, 0, settings.getWidth (), settings.getHeight ()); Vector2f avstånd = ny Vector2f (25,25); rutnät = nytt rutnät (storlek, mellanslag, guiNode, assetManager);Då måste vi ringa
grid.update (float tpf)
frånsimpleUpdate
metod:@Override public void simpleUpdate (float tpf) om ((Boolean) player.getUserData ("live")) spawnEnemies (); spawnBlackHoles (); handleCollisions (); handleGravity (TPF); annars om (System.currentTimeMillis () - (Lång) player.getUserData ("dieTime")> 4000f &&! gameOver) // spawn player player.setLocalTranslation (500.500,0); guiNode.attachChild (spelare); player.setUserData ( "levande", true); sound.spawn (); grid.update (tpf); hud.update ();Därefter måste vi ringa effektmetoderna från rätt ställe i vårt spel.
Den första, som skapar en våg när spelaren spelar, är ganska lätt-vi förlänger bara den plats där vi spelar spelaren i
simpleUpdate (float tpf)
med följande rad:grid.applyDirectedForce (ny Vector3f (0,0,5000), player.getLocalTranslation (), 100);Observera att vi tillämpar en kraft i z-riktningen. Vi kan ha ett 2D-spel men eftersom jME är en 3D-motor kan vi enkelt använda 3D-effekter också. Om vi skulle rotera kameran skulle vi se rutnätet studsa inåt och utåt.
Den andra och tredje effekten måste hanteras i kontroller. När kulor flyger genom spelet kallar de den här metoden i
controlUpdate (float tpf)
:grid.applyExplosiveForce (direction.length () * (18f), spatial.getLocalTranslation (), 80);Detta kommer att göra kulor avstänger gallret proportionellt till deras hastighet. Det var ganska enkelt.
Det liknar de svarta hålen:
grid.applyImplosiveForce (FastMath.sin (sprayAngle / 2) * 10 +20, spatial.getLocalTranslation (), 250);Detta gör att det svarta hålet suger i gallret med en varierande mängd kraft. Jag återanvända
sprayAngle
variabel, vilket kommer att orsaka kraften på gallret att pulsera i synk med vinkeln som sprutar partiklar (även om den är halva frekvensen beroende på uppdelningen med två). Kraften som passerar in varierar sinusformigt mellan10
och30
.För att göra detta arbete, får du inte glömma att skicka
rutnät
tillBulletControl
ochBlackHoleControl
.
Interpolation
Vi kan optimera nätet genom att förbättra den visuella kvaliteten för ett visst antal fjädrar utan att avsevärt öka prestandakostnaden.
Vi kommer att göra nätet tätare genom att lägga till linjesegment i de befintliga rutorna. Vi gör det genom att rita linjer från mittpunkten på den ena sidan av cellen till mittpunkten på motsatt sida. Bilden nedan visar de nya interpolerade linjerna i rött:
Vi kommer att skapa de extra linjerna i vår konstruktör
Rutnät
klass. Om du tittar på det ser du tvåför
slingor där vi knyter punktmassorna med fjädrarna. Lägg bara in det här kvarteret där:om (x> 0 && y> 0) Geometry additionalLine = defaultLine.clone (); additionalLine.addControl (nya AdditionalLineControl (poäng [x-1] [y], poäng [x] [y], poäng [x-1] [y-1], poäng [x] [y-1])); gridNode.attachChild (additionalLine); Geometri additionalLine2 = defaultLine.clone (); additionalLine2.addControl (nya AdditionalLineControl (poäng [x] [y-1], poäng [x] [y], poäng [x-1] [y-1], poäng [x-1] [y])); gridNode.attachChild (additionalLine2);Men som du vet är skapande av objekt inte det enda vi behöver göra. vi måste också lägga till en kontroll för dem för att få dem att fungera korrekt. Som du kan se ovan,
AdditionalLineControl
får fyra poängmassor skickas så att den kan beräkna sin position, rotation och skala:offentlig klass AdditionalLineControl utökar AbstractControl privat PointMass end11, end12, end21, end22; offentlig AdditionalLineControl (PointMass end11, PointMass end12, PointMass end21, PointMass end22) this.end11 = end11; this.end12 = end12; this.end21 = end21; this.end22 = end22; @Override protected void controlUpdate (float tpf) // rörelse spatial.setLocalTranslation (position1 ()); // skala Vector3f dif = position2 (). subtrahera (position1 ()); spatial.setLocalScale (dif.length ()); // rotation spatial.lookAt (position2 (), ny Vector3f (1,0,0)); Privat Vector3f position1 () returnera ny Vector3f (). interpolera (end11.getPosition (), end12.getPosition (), 0.5f); Privat Vector3f position2 () returnera ny Vector3f (). interpolera (end21.getPosition (), end22.getPosition (), 0.5f); @Override protected void controlRender (RenderManager rm, ViewPort vp)
Vad kommer härnäst?
Vi har de grundläggande spel och effekter som genomförts. Det är upp till dig att göra det till ett komplett och polerat spel med din egen smak. Prova att lägga till några intressanta nya mekaniker, några coola nya effekter eller en unik historia. Om du inte är säker på var du ska börja, här är några förslag:
- Skapa nya fiendetyper som ormar eller exploderande fiender.
- Skapa nya vapentyper som att söka efter missiler eller en blixtpistol.
- Lägg till en titelskärm och huvudmeny.
- Lägg till en hög poäng tabell.
- Lägg till några power-ups, till exempel en sköld eller bomber. För bonuspoäng, bli kreativ med dina power-ups. Du kan göra power-ups som manipulerar gravitation, förändra tid eller växa som organismer. Du kan fästa en jätte, fysikbaserad slypboll till skeppet för att krossa fiender. Experimentera med att hitta power-ups som är roliga och hjälpa ditt spel att sticka ut.
- Skapa flera nivåer. Hårdare nivåer kan införa tuffare fiender och mer avancerade vapen och powerups.
- Tillåt en andra spelare att gå med i en gamepad.
- Låt arenan rulla så att den kan vara större än spelfönstret.
- Lägg till miljöfaror som lasrar.
- Lägg till en butik eller nivelleringssystem, och låta spelaren vinna uppgraderingar.
Tack för att du läser!