In questa serie di tutorial, spiegherò come creare un gioco ispirato a Geometry Wars, utilizzando jMonkeyEngine. JMonkeyEngine (in breve "jME") è un motore di gioco 3D Java open source, scopri di più sul loro sito web o nella nostra guida Come imparare jMonkeyEngine.
Mentre jMonkeyEngine è intrinsecamente un motore di gioco 3D, è anche possibile creare giochi 2D con esso.
Post correlatiI cinque capitoli del tutorial saranno dedicati a determinati componenti del gioco:
Come un piccolo assaggio visivo, ecco l'esito finale dei nostri sforzi:
... E qui ci sono i nostri risultati dopo questo primo capitolo:
La musica e gli effetti sonori che puoi sentire in questi video sono stati creati da RetroModular e puoi leggere come lo ha fatto qui.
Gli sprite sono di Jacob Zinman-Jeanes, il nostro tutor + designer residente. Tutte le opere d'arte possono essere trovate nel file zip di download del file sorgente.
Il tutorial è progettato per aiutarti a imparare le basi del jMonkeyEngine e creare il tuo primo gioco con esso. Mentre sfrutteremo le funzionalità del motore, non useremo strumenti complicati per migliorare le prestazioni. Ogni volta che c'è uno strumento più avanzato per implementare una funzionalità, collegherò alle esercitazioni di jME appropriate, ma seguirò il modo semplice nel tutorial stesso. Quando guardi più su jME, in seguito sarai in grado di sviluppare e migliorare la tua versione di MonkeyBlaster.
Eccoci qui!
Il primo capitolo includerà il caricamento delle immagini necessarie, la gestione degli input e il trasferimento e la sparizione della nave del giocatore.
Per ottenere questo, avremo bisogno di tre classi:
MonkeyBlasterMain
: La nostra classe principale contenente il loop di gioco e il gameplay di base.PlayerControl
: Questa classe determinerà come si comporta il giocatore.BulletControl
: Simile a quanto sopra, questo definisce il comportamento dei nostri proiettili.Durante il corso del tutorial, inseriremo il codice di gioco generale MonkeyBlasterMain
e gestire gli oggetti sullo schermo principalmente attraverso controlli e altre classi. Anche le funzioni speciali, come il suono, avranno le loro classi.
Se non hai ancora scaricato l'SDK di jME, è giunto il momento! Puoi trovarlo nella homepage di jMonkeyEngine.
Crea un nuovo progetto nell'SDK di jME. Genererà automaticamente la classe principale, che apparirà simile a questa:
pacchetto scimmiablaster; import com.jme3.app.SimpleApplication; import com.jme3.renderer.RenderManager; public class MonkeyBlasterMain estende SimpleApplication public static void main (String [] args) Main app = new Main (); app.start (); @Override public void simpleInitApp () @Override public void simpleUpdate (float tpf) @Override public void simpleRender (RenderManager rm)
Inizieremo ignorando simpleInitApp ()
. Questo metodo viene chiamato all'avvio dell'applicazione. Questo è il posto dove impostare tutti i componenti:
@Override public void simpleInitApp () // setup camera per giochi 2D cam.setParallelProjection (true); cam.setLocation (new Vector3f (0,0,0.5f)); getFlyByCamera () setEnabled (false).; // disattiva la visualizzazione delle statistiche (puoi lasciarla attiva, se vuoi) setDisplayStatView (false); setDisplayFps (falsi);
Per prima cosa dovremo aggiustare un po 'la telecamera dato che jME è fondamentalmente un motore di gioco 3D. La visualizzazione delle statistiche nel secondo paragrafo può essere molto interessante, ma è così che la disattivate.
Quando inizi il gioco ora, puoi vedere ... niente.
Bene, dobbiamo caricare il giocatore nel gioco! Creeremo un piccolo metodo per gestire il caricamento delle nostre entità:
private Spatial getSpatial (nome stringa) nodo nodo = nuovo nodo (nome); // carica immagine Picture pic = new Picture (nome); Texture2D tex = (Texture2D) assetManager.loadTexture ("Texture /" + nome + ". Png"); pic.setTexture (assetManager, tex, true); // regola l'immagine float width = tex.getImage (). getWidth (); float height = tex.getImage (). getHeight (); pic.setWidth (larghezza); pic.setHeight (altezza); pic.move (-width / 2f, -altezza / 2f, 0); // aggiungi un materiale all'immagine Materiale picMat = new Material (assetManager, "Common / MatDefs / Gui / Gui.j3md"); . PicMat.getAdditionalRenderState () setBlendMode (BlendMode.AlphaAdditive); node.setMaterial (picMat); // imposta il raggio dello spazio // (usando la larghezza solo come semplice approssimazione) node.setUserData ("radius", width / 2); // collega l'immagine al nodo e la restituisce node.attachChild (pic); nodo di ritorno;
All'inizio creiamo un nodo che conterrà la nostra immagine.
Mancia: Il grafico della scena jME è composto da spatials (nodi, immagini, geometrie e così via). Ogni volta che aggiungi qualcosa spaziale alguiNode
, diventa visibile nella scena. Useremo il guiNode
perché stiamo creando un gioco 2D. Puoi collegare gli spazi ad altre superfici e quindi organizzare la scena. Per diventare un vero maestro del grafico della scena, ti consiglio questo tutorial sul grafico delle scene di jME. Dopo aver creato il nodo, carichiamo l'immagine e applichiamo la texture appropriata. Applicare la giusta dimensione all'immagine è carina è facile da capire, ma perché è necessario spostarlo?
Quando carichi un'immagine in jME, il centro di rotazione non è nel mezzo, ma piuttosto in un angolo dell'immagine. Ma possiamo spostare l'immagine di metà della sua larghezza a sinistra e della metà della sua altezza verso l'alto e aggiungerla a un altro nodo. Quindi, quando ruotiamo il nodo genitore, l'immagine stessa viene ruotata attorno al suo centro.
Il prossimo passo è aggiungere un materiale alla foto. Un materiale determina come verrà visualizzata l'immagine. In questo esempio, usiamo il materiale GUI predefinito e impostiamo il BlendMode
a AlphaAdditive
. Ciò significa che le parti trasparenti sovrapposte di più immagini diventeranno più luminose. Questo sarà utile in seguito per rendere le esplosioni più "splendenti".
Infine, aggiungiamo la nostra immagine al nodo e la restituiamo.
Ora dobbiamo aggiungere il giocatore al guiNode
. Ci estenderemo simpleInitApp
un po 'di più:
// imposta il player player = getSpatial ("Player"); player.setUserData ( "vivo", true); player.move (settings.getWidth () / 2, settings.getHeight () / 2, 0); guiNode.attachChild (giocatore);
In breve: carichiamo il lettore, configuriamo alcuni dati, lo spostiamo al centro dello schermo e lo colleghiamo al guiNode
per renderlo visibile.
Dati utente
sono semplicemente alcuni dati che puoi allegare a qualsiasi spazio. In questo caso, aggiungiamo un booleano e lo chiamiamo vivo
, in modo che possiamo controllare se il giocatore è vivo. Lo useremo più tardi.
Ora esegui il programma! Dovresti essere in grado di vedere il giocatore nel mezzo. Al momento è piuttosto noioso, lo ammetto. Quindi aggiungiamo un po 'di azione!
L'input di jMonkeyEngine è piuttosto semplice una volta che lo hai fatto una volta. Iniziamo implementando un Listener di azioni:
public class MonkeyBlasterMain extends SimpleApplication implementa ActionListener
Ora, per ogni chiave, aggiungeremo la mappatura di input e l'ascoltatore simpleInitApp ()
:
inputManager.addMapping ("left", new KeyTrigger (KeyInput.KEY_LEFT)); inputManager.addMapping ("right", new KeyTrigger (KeyInput.KEY_RIGHT)); inputManager.addMapping ("up", nuovo KeyTrigger (KeyInput.KEY_UP)); inputManager.addMapping ("down", new KeyTrigger (KeyInput.KEY_DOWN)); inputManager.addMapping ("return", new KeyTrigger (KeyInput.KEY_RETURN)); inputManager.addListener (this, "left"); inputManager.addListener (this, "right"); inputManager.addListener (this, "up"); inputManager.addListener (this, "down"); inputManager.addListener (this, "return");
Ogni volta che qualcuno di questi tasti viene premuto o rilasciato, il metodo onAction
è chiamato. Prima di entrare in cosa effettivamente fare quando viene premuto un tasto, è necessario aggiungere un controllo al nostro lettore.
FightControl
e un IdleControl
a un'IA nemica. A seconda della situazione, è possibile attivare e disattivare o collegare e scollegare i controlli. Nostro PlayerControl
si occuperà semplicemente di spostare il lettore ogni volta che viene premuto un tasto, ruotandolo nella direzione corretta e assicurandosi che il lettore non lasci lo schermo.
Ecco qui:
public class PlayerControl estende AbstractControl private int screenWidth, screenHeight; // è il giocatore attualmente in movimento? pubblico booleano su, giù, sinistra, destra; // velocità della velocità di flottazione privata del giocatore = 800f; // lastRotation del giocatore private float lastRotation; public PlayerControl (int width, int height) this.screenWidth = width; this.screenHeight = height; @Override protected void controlUpdate (float tpf) // sposta il player in una determinata direzione // se non è fuori dallo schermo if (up) if (spatial.getLocalTranslation (). Y < 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 ("radius")) spatial.move (0, tpf * -speed, 0); spatial.rotate (0,0, -lastRotation + FastMath.PI * 1.5f); lastRotation = FastMath.PI * 1.5f; else if (left) if (spatial.getLocalTranslation (). x> (Float) spatial.getUserData ("radius")) spatial.move (tpf * -speed, 0,0); spatial.rotate (0,0, -lastRotation + FastMath.PI); lastRotation = FastMath.PI; else if (right) if (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;
Va bene; ora, diamo un'occhiata al codice pezzo per pezzo.
private int screenWidth, screenHeight; // è il giocatore attualmente in movimento? pubblico booleano su, giù, sinistra, destra; // velocità della velocità di flottazione privata del giocatore = 800f; // lastRotation del giocatore private float lastRotation; public PlayerControl (int width, int height) this.screenWidth = width; this.screenHeight = height;
Innanzitutto, inizializziamo alcune variabili, definendo in che direzione e quanto velocemente il giocatore si sta muovendo e quanto lontano è ruotato. Quindi, impostiamo il screenwidth
e ScreenHeight
, di cui avremo bisogno nel prossimo grande metodo.
controlUpdate (float tpf)
viene automaticamente chiamato da jME ogni ciclo di aggiornamento. La variabile TPF
indica il tempo trascorso dall'ultimo aggiornamento. Questo è necessario per controllare la velocità: se alcuni computer impiegano il doppio del tempo per calcolare un aggiornamento come altri, il lettore dovrebbe spostarsi il doppio in un singolo aggiornamento su quei computer.
Ora al primo Se
dichiarazione:
if (su) if (spatial.getLocalTranslation (). y < screenHeight - (Float)spatial.getUserData("radius")) spatial.move(0,tpf*speed,0);
Controlliamo se il giocatore sta salendo e, in tal caso, controlliamo se può salire ulteriormente. Se è abbastanza lontano dal confine, lo spostiamo semplicemente un po '.
Ora sulla rotazione:
spatial.rotate (0,0, -lastRotation + FastMath.PI / 2); lastRotation = FastMath.PI / 2;
Ruotiamo il lettore di nuovo lastRotation
per affrontare la sua direzione originale. Da questa direzione, possiamo ruotare il giocatore nella direzione in cui vogliamo che guardi. Infine, salviamo la rotazione effettiva.
Usiamo lo stesso tipo di logica per tutte e quattro le direzioni. Il reset()
il metodo è solo qui per riportare tutti i valori a zero, da usare quando si respawning il giocatore.
Quindi, finalmente abbiamo il controllo per il nostro giocatore. È ora di aggiungerlo allo spazio reale. Basta aggiungere la seguente riga al simpleInitApp ()
metodo:
player.addControl (new PlayerControl (settings.getWidth (), settings.getHeight ()));
L'oggetto impostazioni
è incluso nella classe SimpleApplication
. Contiene dati sulle impostazioni di visualizzazione del gioco.
Se iniziamo ora il gioco, non sta ancora accadendo nulla. Dobbiamo dire al programma cosa fare quando viene premuto uno dei tasti mappati. Per fare ciò, ignoreremo il onAction
metodo:
public void onAction (String name, boolean isPressed, float tpf) if ((Boolean) player.getUserData ("alive")) if (name.equals ("up")) player.getControl (PlayerControl.class). up = isPressed; else if (name.equals ("down")) player.getControl (PlayerControl.class) .down = isPressed; else if (name.equals ("left")) player.getControl (PlayerControl.class) .left = isPressed; else if (name.equals ("right")) player.getControl (PlayerControl.class) .right = isPressed;
Per ogni tasto premuto, diciamo al PlayerControl
il nuovo stato della chiave. Ora è finalmente giunto il momento di iniziare il gioco e vedere qualcosa che si muove sullo schermo!
Quando sei felice di aver compreso le basi dell'input e della gestione del comportamento, è ora di fare di nuovo la stessa cosa, questa volta, per i proiettili.
Se vogliamo averne vero azione in corso, dobbiamo essere in grado di sparare alcuni nemici. Seguiremo la stessa procedura di base del passaggio precedente: gestione dell'input, creazione di alcuni punti elenco e aggiunta di un comportamento.
Per gestire l'input del mouse, implementeremo un altro listener:
public class MonkeyBlasterMain estende SimpleApplication implementa ActionListener, AnalogListener
Prima che succeda qualcosa, dobbiamo aggiungere la mappatura e l'ascoltatore come abbiamo fatto l'ultima volta. Lo faremo nel simpleInitApp ()
metodo, insieme all'altra inizializzazione dell'input:
inputManager.addMapping ("mousePick", nuovo MouseButtonTrigger (MouseInput.BUTTON_LEFT)); inputManager.addListener (this, "mousePick");
Ogni volta che clicchiamo con il mouse, il metodo onAnalog
viene chiamato Prima di entrare nel tiro vero e proprio, dobbiamo implementare un piccolo metodo di supporto, Vector3f getAimDirection ()
, che ci darà la direzione per sparare sottraendo la posizione del giocatore da quella del mouse:
private Vector3f getAimDirection () Mouse Vector2f = inputManager.getCursorPosition (); Vector3f playerPos = player.getLocalTranslation (); Vector3f dif = new Vector3f (mouse.x-playerPos.x, mouse.y-playerPos.y, 0); return dif.normalizeLocal ();Mancia: Quando si collegano oggetti al
guiNode
, le loro unità di traduzione locali sono uguali a un pixel. Questo ci facilita il calcolo della direzione, poiché la posizione del cursore è specificata anche in unità pixel. Ora che abbiamo una direzione in cui sparare, implementiamo lo scatto effettivo:
public void onAnalog (String name, float value, float tpf) if ((Boolean) player.getUserData ("alive")) if (name.equals ("mousePick")) // spara Bullet if (System.currentTimeMillis () - bulletCooldown> 83f) bulletCooldown = System.currentTimeMillis (); Vector3f aim = getAimDirection (); Vector3f offset = new 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 (trans); bullet.addControl (nuovo BulletControl (aim, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (proiettile); // init bullet 2 Spatial bullet2 = getSpatial ("Bullet"); finalOffset = aim.add (offset.negate ()). mult (30); trans = player.getLocalTranslation (). add (finalOffset); bullet2.setLocalTranslation (trans); bullet2.addControl (nuovo BulletControl (aim, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (bullet2);
Okay, passiamo a questo:
if (System.currentTimeMillis () - bulletCooldown> 83f) bulletCooldown = System.currentTimeMillis (); Vector3f aim = getAimDirection (); Vector3f offset = new Vector3f (aim.y / 3, -aim.x / 3,0);
Se il giocatore è vivo e si fa clic sul pulsante del mouse, il nostro codice controlla prima se l'ultimo colpo è stato sparato almeno 83 ms fa (bulletCooldown
è una variabile lunga che inizializziamo all'inizio della classe). Se è così, allora siamo autorizzati a sparare e calcoliamo la giusta direzione per il puntamento e l'offset.
// init bullet 1 Spatial bullet = getSpatial ("Bullet"); Vector3f finalOffset = aim.add (offset) .mult (30); Vector3f trans = player.getLocalTranslation (). Add (finalOffset); bullet.setLocalTranslation (trans); bullet.addControl (nuovo BulletControl (aim, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (proiettile); // init bullet 2 Spatial bullet2 = getSpatial ("Bullet"); finalOffset = aim.add (offset.negate ()). mult (30); trans = player.getLocalTranslation (). add (finalOffset); bullet2.setLocalTranslation (trans); bullet2.addControl (nuovo BulletControl (aim, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (bullet2);
Vogliamo generare due pallottole gemelle, una accanto all'altra, quindi dovremo aggiungere un po 'di offset a ciascuna di esse. Un offset appropriato è ortogonale alla direzione della mira, che può essere facilmente ottenuta cambiando la X
e y
valori e negando uno di essi. Il secondo sarà semplicemente una negazione del primo.
// init bullet 1 Spatial bullet = getSpatial ("Bullet"); Vector3f finalOffset = aim.add (offset) .mult (30); Vector3f trans = player.getLocalTranslation (). Add (finalOffset); bullet.setLocalTranslation (trans); bullet.addControl (nuovo BulletControl (aim, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (proiettile); // init bullet 2 Spatial bullet2 = getSpatial ("Bullet"); finalOffset = aim.add (offset.negate ()). mult (30); trans = player.getLocalTranslation (). add (finalOffset); bullet2.setLocalTranslation (trans); bullet2.addControl (nuovo BulletControl (aim, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (bullet2);
Il resto dovrebbe sembrare abbastanza familiare: inizializziamo il proiettile usando il nostro getSpatial
metodo dall'inizio Poi lo traduciamo nel posto giusto e lo colleghiamo al nodo. Ma aspetta, quale nodo?
Organizzeremo le nostre entità in nodi specifici, quindi ha senso creare un nodo in cui saremo in grado di allegare tutti i nostri proiettili. Per visualizzare i figli di quel nodo, dovremo collegarlo al guiNode
.
L'inizializzazione in simpleInitApp ()
è piuttosto semplice:
// imposta il bulletNode bulletNode = new Node ("bullet"); guiNode.attachChild (bulletNode);
Se vai avanti e inizi il gioco, sarai in grado di vedere i proiettili che appaiono, ma non si stanno muovendo! Se vuoi metterti alla prova, metti in pausa la lettura e pensa di persona cosa dobbiamo fare per farli muovere.
...
L'hai capito??
Dobbiamo aggiungere un controllo a ciascun proiettile che si prenderà cura del suo movimento. Per fare ciò, creeremo un'altra classe chiamata BulletControl
:
BulletControl di classe pubblica estende AbstractControl private int screenWidth, screenHeight; velocità di navigazione privata = 1100f; direzione pubblica di Vector3f; rotazione del galleggiante privato; BulletControl pubblico (direzione Vector3f, int screenWidth, int screenHeight) this.direction = direction; this.screenWidth = screenWidth; this.screenHeight = screenHeight; @Override protected void controlUpdate (float tpf) // movement spatial.move (direction.mult (speed * tpf)); // rotation float actualRotation = MonkeyBlasterMain.getAngleFromVector (direction); if (actualRotation! = rotation) spatial.rotate (0,0, actualRotation - rotation); rotation = actualRotation; // controlla i contorni Vector3f loc = spatial.getLocalTranslation (); if (loc.x> screenWidth || loc.y> screenHeight || loc.x < 0 || loc.y < 0) spatial.removeFromParent(); @Override protected void controlRender(RenderManager rm, ViewPort vp)
Una rapida occhiata alla struttura della classe mostra che è abbastanza simile al PlayerControl
classe. La differenza principale è che non abbiamo alcuna chiave da controllare, e noi abbiamo a direzione
variabile. Spostiamo semplicemente il proiettile nella sua direzione e lo ruotiamo di conseguenza.
Vector3f loc = spatial.getLocalTranslation (); if (loc.x> screenWidth || loc.y> screenHeight || loc.x < 0 || loc.y < 0) spatial.removeFromParent();
Nell'ultimo blocco, controlliamo se il proiettile è al di fuori dei limiti dello schermo e, in tal caso, lo rimuoviamo dal suo nodo genitore, che cancellerà l'oggetto.
Potresti aver preso questa chiamata al metodo:
MonkeyBlasterMain.getAngleFromVector (direzione);
Si riferisce a un breve metodo di supporto matematico statico nella classe principale. Ne ho creati due, uno convertendo un angolo in un vettore nello spazio 2D e l'altro convertendo tali vettori in un valore angolare.
public static float getAngleFromVector (Vector3f vec) Vector2f vec2 = new Vector2f (vec.x, vec.y); return vec2.getAngle (); public static Vector3f getVectorFromAngle (float angle) return new Vector3f (FastMath.cos (angle), FastMath.sin (angle), 0);Mancia: Se ti senti abbastanza confuso da tutte quelle operazioni vettoriali, allora fai un favore a te stesso e approfondisci alcuni tutorial sulla matematica vettoriale. È essenziale sia nello spazio 2D che in quello 3D. Mentre ci sei, dovresti anche cercare la differenza tra gradi e radianti. E se vuoi imparare di più nella programmazione di giochi 3D, anche i quaternioni sono fantastici ...
Ora torniamo alla panoramica principale: abbiamo creato un listener di input, inizializzato due punti elenco e creato a BulletControl
classe. L'unica cosa rimasta è aggiungere un BulletControl
ad ogni proiettile durante l'inizializzazione:
bullet.addControl (nuovo BulletControl (aim, settings.getWidth (), settings.getHeight ()));
Ora il gioco è molto più divertente!
Mentre non è esattamente difficile volare e sparare alcuni proiettili, puoi almeno fare qualcosa. Ma non disperare: dopo il prossimo tutorial avrai difficoltà a sfuggire alla crescente orda di nemici!