I den här tutorialserien ska jag förklara hur man skapar ett spel inspirerat av Geometry Wars, med jMonkeyEngine. JMonkeyEngine ("jME" för kort) är en open source 3D Java-spelmotor - ta reda på mer på deras hemsida eller i vår Hur lära sig jMonkeyEngine guide.
Medan jMonkeyEngine i grunden är en 3D-spelmotor, är det också möjligt att skapa 2D-spel med det.
relaterade inläggDe fem kapitlen i handledningen kommer att vara avsedda för vissa delar av spelet:
Som en liten visuell försmak är här det slutliga resultatet av våra ansträngningar:
... Och här är våra resultat efter det här första kapitlet:
Musik och ljud effekter du kan höra i dessa videoklipp skapades av RetroModular, och du kan läsa om hur han gjorde det här.
De sprites är av Jacob Zinman-Jeanes, vår invånare Tuts + designer. Allt konstverket kan hittas i källfilens nedladdningsladd.
Handledningen är utformad för att hjälpa dig att lära dig grunderna i jMonkeyEngine och skapa ditt första spel med det. Medan vi kommer att dra nytta av motorns funktioner, kommer vi inte använda komplicerade verktyg för att förbättra prestanda. När det finns ett mer avancerat verktyg för att implementera en funktion, länkar jag till lämpliga jME-handledning, men håller sig till det enkla sättet i självstudiet. När du tittar på jME mer kan du senare bygga vidare på och förbättra din version av MonkeyBlaster.
Nu kör vi!
Det första kapitlet kommer att inkludera att ladda de nödvändiga bilderna, hanteringången och göra spelarens skepp flytta och skjuta.
För att uppnå detta behöver vi tre klasser:
MonkeyBlasterMain
: Vår huvudklass som innehåller spelet loop och den grundläggande gameplayen.PlayerControl
: Den här klassen bestämmer hur spelaren beter sig.BulletControl
: Liksom ovanstående definierar detta beteendet för våra kulor.Under tutorialet kommer vi att kasta den allmänna spelkoden i MonkeyBlasterMain
och hantera objekten på skärmen, huvudsakligen genom kontroller och andra klasser. Särskilda funktioner, som ljud, kommer också att ha sina egna klasser.
Om du inte har laddat ner jME SDK ännu är det dags! Du hittar den på jMonkeyEngines hemsida.
Skapa ett nytt projekt i jME SDK. Det kommer automatiskt att generera huvudklassen, som kommer att likna den här:
paket monkeyblaster; importera com.jme3.app.SimpleApplication; importera com.jme3.renderer.RenderManager; public class MonkeyBlasterMain utökar SimpleApplication public static void main (String [] args) Huvud app = ny Huvud (); app.start (); @Override public void simpleInitApp () @Override public void simpleUpdate (float tpf) @Override public void simpleRender (RenderManager rm)
Vi börjar med att överstyra simpleInitApp ()
. Denna metod kallas när applikationen startar. Det här är platsen för att ställa in alla komponenter:
@Override public void simpleInitApp () // setup kamera för 2D spel cam.setParallelProjection (true); cam.setLocation (ny Vector3f (0,0,0,5f)); getFlyByCamera () setEnabled (false).; // stäng av statistikvyn (du kan lämna den om du vill) setDisplayStatView (false); setDisplayFps (falska);
Först måste vi justera kameran lite eftersom jME är i grunden en 3D-spelmotor. Statistikvisningen i andra stycket kan vara mycket intressant, men det här är hur du stänger av det.
När du börjar spelet nu kan du se ... ingenting.
Tja, vi måste ladda spelaren i spelet! Vi skapar en liten metod för hantering av våra enheter:
privat rumslig getSpatial (strängnamn) Node node = ny nod (namn); // ladda bild Bild pic = ny Bild (namn); Texture2D tex = (Texture2D) assetManager.loadTexture ("Textures /" + name + ".png"); pic.setTexture (assetManager, tex, true); // justera bild float width = tex.getImage (). getWidth (); float height = tex.getImage (). getHeight (); pic.setWidth (bredd); pic.setHeight (höjd); pic.move (-width / 2f, -Höjd / 2f, 0); // lägg till material till bilden Material picMat = nytt material (assetManager, "Common / MatDefs / Gui / Gui.j3md"); . PicMat.getAdditionalRenderState () setBlendMode (BlendMode.AlphaAdditive); node.setMaterial (picMat); // ställa in radiusen för den rumsliga // (använd bredden endast som en enkel approximation) node.setUserData ("radie", bredd / 2); // bifoga bilden till noden och returnera den node.attachChild (pic); returnod;
I början skapar vi en nod som innehåller vår bild.
Tips: JME-scenen består av spatials (noder, bilder, geometrier, och så vidare). Närhelst du lägger till en rumslig något tillguiNode
, det blir synligt i scenen. Vi kommer att använda guiNode
för att vi skapar ett 2D-spel. Du kan bifoga rum till andra rumsliga platser och organisera därför din scen. För att bli en sann mästare i scengrafen rekommenderar jag denna tutorials för jME-scenen. Efter att ha skapat noden laddar vi bilden och tillämpar lämplig textur. Att tillämpa rätt storlek på bilden är ganska lätt att förstå, men varför behöver vi flytta den?
När du laddar upp en bild i jME är rotationscentret inte i mitten, utan snarare i ett hörn av bilden. Men vi kan flytta bilden med hälften bredden till vänster och halva höjden uppåt och lägga till den till en annan nod. Då, när vi roterar föräldernoden roteras själva bilden runt sitt centrum.
Nästa steg är att lägga till ett material på bilden. Ett material bestämmer hur bilden ska visas. I det här exemplet använder vi standard GUI-material och anger Blandningsläge
till AlphaAdditive
. Det betyder att överlappande transparenta delar av flera bilder blir ljusare. Detta kommer att vara användbart senare för att göra explosioner "shinier".
Slutligen lägger vi till vår bild till noden och returnerar den.
Nu måste vi lägga till spelaren till guiNode
. Vi kommer att förlängas simpleInitApp
lite mer:
// Ställ in spelarens spelare = getSpatial ("Player"); player.setUserData ( "levande", true); player.move (settings.getWidth () / 2, settings.getHeight () / 2, 0); guiNode.attachChild (spelare);
Kort sagt: Vi laddar spelaren, konfigurerar vissa data, flyttar den till mitten av skärmen och bifogar den till guiNode
för att få det att visas.
Användardata
är helt enkelt några data du kan bifoga till någon rumslig. I det här fallet lägger vi till en booleska och kallar den Levande
, så att vi kan leta upp om spelaren lever. Vi använder det senare.
Nu kör programmet! Du borde kunna se spelaren i mitten. För tillfället är det ganska tråkigt, jag ska erkänna. Så låt oss lägga till några åtgärder!
jMonkeyEngine-ingången är ganska enkel när du har gjort det en gång. Vi börjar med att genomföra en Action Listener:
offentlig klass MonkeyBlasterMain utökar SimpleApplication implementerar ActionListener
Nu, för varje nyckel, lägger vi till inmatningskort och lyssnare i simpleInitApp ()
:
inputManager.addMapping ("left", ny KeyTrigger (KeyInput.KEY_LEFT)); inputManager.addMapping ("right", ny KeyTrigger (KeyInput.KEY_RIGHT)); inputManager.addMapping ("up", ny KeyTrigger (KeyInput.KEY_UP)); inputManager.addMapping ("ner", ny KeyTrigger (KeyInput.KEY_DOWN)); inputManager.addMapping ("return", ny KeyTrigger (KeyInput.KEY_RETURN)); inputManager.addListener (detta, "vänster"); inputManager.addListener (detta, "rätt"); inputManager.addListener (detta, "upp"); inputManager.addListener (detta, "ner"); inputManager.addListener (this, "return");
När någon av dessa nycklar trycks eller släpps, är metoden onAction
kallas. Innan vi kommer in i vad som egentligen ska do när en knapp trycks in måste vi lägga till en kontroll till vår spelare.
FightControl
och en IdleControl
till en fiende AI. Beroende på situationen kan du aktivera och inaktivera eller bifoga och ta bort kontroller. Vår PlayerControl
kommer helt enkelt att ta hand om att flytta spelaren när en tangent trycks in, rotera den i rätt riktning och se till att spelaren inte lämnar skärmen.
Här har du:
offentlig klass PlayerControl utökar AbstractControl private int screenWidth, screenHeight; // går spelaren för närvarande? offentliga boolean upp, ner, vänster, höger; // hastighet på spelaren privat flythastighet = 800f; // lastRotation av spelaren privat float lastRotation; allmän PlayerControl (int bredd, int höjd) this.screenWidth = width; this.screenHeight = height; @Override protected void controlUpdate (float tpf) // flytta spelaren i en viss riktning // om han inte är ute av skärmen om (upp) om (spatial.getLocalTranslation (). < screenHeight - (Float)spatial.getUserData("radius")) spatial.move(0,tpf*speed,0); spatial.rotate(0,0,-lastRotation + FastMath.PI/2); lastRotation=FastMath.PI/2; else if (down) if (spatial.getLocalTranslation().y > (Float) spatial.getUserData ("radie")) spatial.move (0, tpf * -speed, 0); spatial.rotate (0,0, -lastRotation + FastMath.PI * 1.5f); lastRotation = FastMath.PI * 1.5f; annars om (vänster) om (spatial.getLocalTranslation () .x> (Float) spatial.getUserData ("radius")) spatial.move (tpf * -hastighet, 0,0); spatial.rotate (0,0, -lastRotation + FastMath.PI); lastRotation = FastMath.PI; annars om (höger) om (spatial.getLocalTranslation (). x < screenWidth - (Float)spatial.getUserData("radius")) spatial.move(tpf*speed,0,0); spatial.rotate(0,0,-lastRotation + 0); lastRotation=0; @Override protected void controlRender(RenderManager rm, ViewPort vp) // reset the moving values (i.e. for spawning) public void reset() up = false; down = false; left = false; right = false;
Okej; nu, låt oss ta en titt på kodstycket för bit.
privat int skärmvidd, skärmvåg; // går spelaren för närvarande? offentliga boolean upp, ner, vänster, höger; // hastighet på spelaren privat flythastighet = 800f; // lastRotation av spelaren privat float lastRotation; allmän PlayerControl (int bredd, int höjd) this.screenWidth = width; this.screenHeight = height;
Först initierar vi några variabler, definierar i vilken riktning och hur snabbt spelaren rör sig, och hur långt den roteras. Sedan satte vi screenWidth
och screenHeight
, som vi behöver i nästa stora metod.
controlUpdate (float tpf)
kallas automatiskt av jME varje uppdateringscykel. Variabeln TPF
anger tiden sedan den senaste uppdateringen. Detta behövs för att styra hastigheten: Om vissa datorer tar dubbelt så lång tid för att beräkna en uppdatering som andra, ska spelaren flytta två gånger så långt i en enda uppdatering på de datorerna.
Nu till den första om
påstående:
om (upp) om (spatial.getLocalTranslation (). y < screenHeight - (Float)spatial.getUserData("radius")) spatial.move(0,tpf*speed,0);
Vi kontrollerar om spelaren går upp och, om så är fallet, kontrollerar vi om det kan gå längre. Om det är tillräckligt långt bort från gränsen, flyttar vi helt enkelt upp lite.
Nu på rotationen:
spatial.rotate (0,0, -lastRotation + FastMath.PI / 2); lastRotation = FastMath.PI / 2;
Vi roterar spelaren tillbaka med lastRotation
att möta sin ursprungliga riktning. Från denna riktning kan vi rotera spelaren i den riktning vi vill att den ska titta på. Slutligen sparar vi själva rotationen.
Vi använder samma typ av logik för alla fyra riktningarna. De återställa()
Metoden är just här för att ställa in alla värden till noll igen, för användning när spelaren respekteras.
Så vi har äntligen kontrollen för vår spelare. Det är dags att lägga till det till det aktuella rumsliga. Lägg bara till följande rad till simpleInitApp ()
metod:
player.addControl (nya PlayerControl (settings.getWidth (), settings.getHeight ()));
Objektet inställningar
ingår i klassen SimpleApplication
. Den innehåller data om spelets skärminställningar.
Om vi börjar spelet nu är det fortfarande inget som händer än. Vi måste berätta för programmet vad man ska göra när en av de mappade tangenterna trycks in. För att göra detta kommer vi att åsidosätta onAction
metod:
public void onAction (strängnamn, boolean isPressed, float tpf) if ((Boolean) player.getUserData ("live")) if (name.equals ("up")) player.getControl (PlayerControl.class). up = isPressed; annars om (name.equals ("down")) player.getControl (PlayerControl.class) .down = isPressed; annars om (name.equals ("left")) player.getControl (PlayerControl.class) .left = isPressed; annars om (name.equals ("right")) player.getControl (PlayerControl.class) .right = isPressed;
För varje tryckt tangent berättar vi PlayerControl
Nyckeln till nyckeln. Nu är det äntligen dags att starta vårt spel och se något som rör sig på skärmen!
När du är glad att du förstår grunderna för inmatning och beteendehantering är det dags att göra samma sak igen - den här gången, för kulorna.
Om vi vill ha lite verklig åtgärd som pågår måste vi kunna skjuta några fiender. Vi ska följa samma grundläggande procedur som i föregående steg: hantera inmatning, skapa några kulor och lägga till ett beteende för dem.
För att hantera musingången implementerar vi en annan lyssnare:
offentlig klass MonkeyBlasterMain utökar SimpleApplication implementerar ActionListener, AnalogListener
Innan det händer något måste vi lägga till kartläggningen och lyssnaren som vi gjorde förra gången. Vi gör det i simpleInitApp ()
metod, tillsammans med den andra ingångsinitialiseringen:
inputManager.addMapping ("mousePick", ny MouseButtonTrigger (MouseInput.BUTTON_LEFT)); inputManager.addListener (detta, "mousePick");
När vi klickar med musen, metoden onAnalog
kallas. Innan vi går in i själva skottet måste vi genomföra en liten hjälparmetod, Vector3f getAimDirection ()
, vilket ger oss rätten att skjuta på genom att subtrahera spelarens position från musens
privat Vector3f getAimDirection () Vector2f mouse = inputManager.getCursorPosition (); Vector3f playerPos = player.getLocalTranslation (); Vector3f dif = ny Vector3f (mouse.x-playerPos.x, mouse.y-playerPos.y, 0); returnera dif.normalizeLocal ();Tips: När du bifogar objekt till
guiNode
, deras lokala översättningsenheter är lika med en pixel. Detta gör det enkelt för oss att beräkna riktningen, eftersom markörpositionen också anges i pixelenheter. Nu när vi har en riktning att skjuta på, låt oss genomföra den faktiska fotograferingen:
(ifall (namnet.equals ("mousePick")) // skjut Bullet if (System.currentTimeMillis () - bulletCooldown> 83f) bulletCooldown = System.currentTimeMillis (); Vector3f aim = getAimDirection (); Vector3f offset = ny Vector3f (aim.y / 3, -aim.x / 3,0); // init bullet 1 Spatial bullet = getSpatial ("Bullet"); Vector3f finalOffset = aim.add (offset) .mult (30); Vector3f trans = player.getLocalTranslation (). Add (finalOffset); bullet.setLocalTranslation (träns); bullet.addControl (nya BulletControl (aim, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (bullet); // init bullet 2 Spatial bullet2 = getSpatial ("Bullet"); finalOffset = aim.add (offset.negate ()). mult (30); trans = player.getLocalTranslation (). add (finalOffset); bullet2.setLocalTranslation (träns); bullet2.addControl (nya BulletControl (aim, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (bullet2);
Okej så, låt oss gå igenom det här:
om (System.currentTimeMillis () - bulletCooldown> 83f) bulletCooldown = System.currentTimeMillis (); Vector3f aim = getAimDirection (); Vector3f offset = ny Vector3f (aim.y / 3, -aim.x / 3,0);
Om spelaren lever och musknappen klickar, kontrollerar vår kod först om det sista skottet avfyras minst 83 ms sedan (bulletCooldown
är en lång variabel vi initierar i början av klassen). Om så är fallet får vi skjuta, och vi beräknar rätt riktning för sikten och offset.
// init bullet 1 Spatial bullet = getSpatial ("Bullet"); Vector3f finalOffset = aim.add (offset) .mult (30); Vector3f trans = player.getLocalTranslation (). Add (finalOffset); bullet.setLocalTranslation (träns); bullet.addControl (nya BulletControl (aim, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (bullet); // init bullet 2 Spatial bullet2 = getSpatial ("Bullet"); finalOffset = aim.add (offset.negate ()). mult (30); trans = player.getLocalTranslation (). add (finalOffset); bullet2.setLocalTranslation (träns); bullet2.addControl (nya BulletControl (aim, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (bullet2);
Vi vill krossa dubbla kulor, en bredvid varandra, så vi måste lägga en liten motgång till var och en av dem. En lämplig förskjutning är ortogonal mot sikten, vilket lätt uppnås genom att byta x
och y
värden och negera en av det. Den andra kommer helt enkelt att vara en negation av den första.
// init bullet 1 Spatial bullet = getSpatial ("Bullet"); Vector3f finalOffset = aim.add (offset) .mult (30); Vector3f trans = player.getLocalTranslation (). Add (finalOffset); bullet.setLocalTranslation (träns); bullet.addControl (nya BulletControl (aim, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (bullet); // init bullet 2 Spatial bullet2 = getSpatial ("Bullet"); finalOffset = aim.add (offset.negate ()). mult (30); trans = player.getLocalTranslation (). add (finalOffset); bullet2.setLocalTranslation (träns); bullet2.addControl (nya BulletControl (aim, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (bullet2);
Resten ska verka ganska bekant: Vi initierar kulan genom att använda vår egen getSpatial
metod från början. Då översätter vi det till rätt ställe och bifogar det till noden. Men vänta, vilken nod?
Vi organiserar våra enheter i specifika noder, så det är vettigt att skapa en nod där vi kan fästa alla våra kulor till. För att visa barnen till den noden måste vi bifoga den till guiNode
.
Initialiseringen i simpleInitApp ()
är ganska enkelt:
// ställa in bulletNode bulletNode = ny nod ("kulor"); guiNode.attachChild (bulletNode);
Om du går vidare och börjar spelet kan du se kulorna som visas, men de rör sig inte! Om du vill testa dig själv, pausa läsning och tänka själv vad vi behöver göra för att få dem att röra sig.
...
Fann du ut det?
Vi måste lägga till en kontroll för varje kula som tar hand om sin rörelse. För att göra detta ska vi skapa en annan klass som heter BulletControl
:
offentlig klass BulletControl utökar AbstractControl private int screenWidth, screenHeight; privat flythastighet = 1100f; allmän vektor3f riktning; privat flottörrotation; offentlig BulletControl (Vector3f riktning, int skärmvidd, int screenHeight) this.direction = riktning; this.screenWidth = screenWidth; this.screenHeight = screenHeight; @Override protected void controlUpdate (float tpf) // movement spatial.move (direction.mult (hastighet * tpf)); // rotation float actualRotation = MonkeyBlasterMain.getAngleFromVector (riktning); om (actualRotation! = rotation) spatial.rotate (0,0, actualRotation - rotation); rotation = actualRotation; // kontrollera gränser Vector3f loc = spatial.getLocalTranslation (); om (loc.x> screenWidth || loc.y> screenHeight || loc.x < 0 || loc.y < 0) spatial.removeFromParent(); @Override protected void controlRender(RenderManager rm, ViewPort vp)
En snabb blick på klassens struktur visar att den är ganska lik den PlayerControl
klass. Huvudskillnaden är att vi inte har några nycklar som ska kontrolleras, och vi har a riktning
variabel. Vi flyttar helt enkelt kulan i dess riktning och roterar den i enlighet därmed.
Vector3f loc = spatial.getLocalTranslation (); om (loc.x> screenWidth || loc.y> screenHeight || loc.x < 0 || loc.y < 0) spatial.removeFromParent();
I det sista blocket kontrollerar vi om kulan ligger utanför gränserna för skärmen och om så är fallet tar vi bort det från dess moderkod, vilket kommer att ta bort objektet.
Du kan ha fångat det här metoden samtalet:
MonkeyBlasterMain.getAngleFromVector (riktning);
Det hänvisar till en kort statisk matematisk hjälparmetod i huvudklassen. Jag skapade två av dem, en omvandlar en vinkel i en vektor i 2D-utrymme och den andra omvandlar sådana vektorer tillbaka i ett vinkelvärde.
offentlig statisk float getAngleFromVector (Vector3f vec) Vector2f vec2 = ny Vector2f (vec.x, vec.y); returnera vec2.getAngle (); statisk statisk vektor3f getVectorFromAngle (floatvinkel) returnera ny Vector3f (FastMath.cos (vinkel), FastMath.sin (vinkel), 0);Tips: Om du känner dig ganska förvirrad av alla dessa vektoroperationer, gör dig själv en tjänst och gräva till några handledningar om vektormatematik. Det är viktigt i både 2D och 3D-utrymme. Medan du är i det, bör du också se upp skillnaden mellan grader och radianer. Och om du vill få mer in i 3D-spelprogrammering är quaternions också fantastiska ...
Nu tillbaka till huvudöversikten: Vi skapade en ingångslyttare, initierade två kulor och skapade a BulletControl
klass. Det enda som kvar är att lägga till en BulletControl
till varje kula när den initialiseras:
bullet.addControl (nya BulletControl (aim, settings.getWidth (), settings.getHeight ()));
Nu är spelet mycket roligare!
Även om det inte är exakt utmanande att flyga runt och skjuta några kulor, kan du åtminstone do något. Men förtvivla inte - efter nästa handledning har du svårt att försöka undkomma de växande horderna av fiender!