Creare un Neon Vector Shooter in jMonkeyEngine: Nemici e Suono
() translation by (you can also view the original English article)
Nella prima parte di questa serie sulla crazione di un gioco ispirato a Geometry-Wars con jMonkeyEngine, abbiamo implementato l'astronave del giocatore e la abbiamo fatta muovere e sparare. Questa volta, aggiungeremo i nemici e gli effetti sonori.
Panoramica
Ecco a cosa lavoreremo per tutta la serie:
... sd ecco a cosa arriveremo alla fine di questa parte:
Per implementare le nuove caratteristiche, avremo bisogno di alcune nuove classi.
-
SeekerControl
: Questa è una classe per il comportamento del "nemico ricercatore". -
WandererControl
: Anche questa è una classe per il comportamento, questa volta per il "nemico nomade". -
Sound
: Gestiremo il caricamento e la riproduzione di effetti sonori e musica con questa classe.
Come si può immaginare, aggiungeremo due tipi di nemici. Il primo si chiama seeker (cercatore); inseguirà attivamente il giocatore fino a quando non muore. L'altro, il wanderer (viandante, che vagabonda), si aggira intorno allo schermo in maniera casuale.
Aggiunta dei nemici
Creeremo i nemici in posizioni casuali sullo schermo. Al fine di dare al giocatore un certo tempo per reagire, il nemico non sarà attivo immediatamente, bensì si esaurirà lentamente. Dopo che è sbiadito completamente, inizierà a muoversi nel mondo. Quando si scontra con il giocatore, il giocatore muore; quando si scontra con un proiettile, muore lui.
Generazione dei nemici
Prima di tutto, abbiamo bisogno di creare alcune variabili nuove nella classe MonkeyBlasterMain
:
1 |
|
2 |
private long enemySpawnCooldown; |
3 |
private float enemySpawnChance = 80; |
4 |
|
5 |
private Node enemyNode; |
Arriveremo ad usare i primi due abbastanza presto. Prima, dobbiamo inizializzare enemyNode
nella simpleInitApp()
:
1 |
|
2 |
// set up the enemyNode
|
3 |
enemyNode = new Node("enemies"); |
4 |
guiNode.attachChild(enemyNode); |
Okay, ora andiamo sul codice di generazione dei nemici: sovrascriviamo simpleUpdate(float tpf)
. Questo metodo viene chiamato dal motore più e più volte, e semplicemente continua la sua funzione di produrre nemici fintanto che il giocatore è vivo. (Avevamo già impostato i dati utente alive
a true
nell'ultimo tutorial.)
1 |
|
2 |
@Override
|
3 |
public void simpleUpdate(float tpf) { |
4 |
if ((Boolean) player.getUserData("alive")) { |
5 |
spawnEnemies(); |
6 |
}
|
7 |
}
|
E questo è come generiamo i nemici:
1 |
|
2 |
private void spawnEnemies() { |
3 |
if (System.currentTimeMillis() - enemySpawnCooldown >= 17) { |
4 |
enemySpawnCooldown = System.currentTimeMillis(); |
5 |
|
6 |
if (enemyNode.getQuantity() < 50) { |
7 |
if (new Random().nextInt((int) enemySpawnChance) == 0) { |
8 |
createSeeker(); |
9 |
}
|
10 |
if (new Random().nextInt((int) enemySpawnChance) == 0) { |
11 |
createWanderer(); |
12 |
}
|
13 |
}
|
14 |
//increase Spawn Time
|
15 |
if (enemySpawnChance >= 1.1f) { |
16 |
enemySpawnChance -= 0.005f; |
17 |
}
|
18 |
}
|
19 |
}
|
Non lasciatevi confondere dalla variabile enemySpawnCooldown
. Non è lì per fare generare nemici con una frequenza discreta di 17ms, sarebbe troppo breve come intervallo.
enemySpawnCooldown
è in realtà lì per garantire che la quantità di nuovi nemici sia la stessa su ogni macchina. Su computer più veloci, simpleUpdate(float tpf)
viene chiamata molto più spesso rispetto a quelli più lenti. Con questa variabile, controlliamo circa ogni 17ms, se dobbiamo generare nuovi nemici.
Ma vogliamo generare nemici ogni 17ms? Noi in realtà vogliamo generarli casualmente, quindi introduciamo un'istruzione if
:
1 |
|
2 |
if (new Random().nextInt((int) enemySpawnChance) == 0) { |
Minore è il valore di enemySpawnChance
, più è probabile che un nuovo nemico si riproduca in questo intervallo di 17ms e quindi più nemici che il giocatore dovrà affrontare. Ecco perché sottraiamo un pò di enemySpawnChance
ad ogni tick (n.d.a. unità che indica un singolo frame aggiornato): significa che il gioco otterrà più difficile nel corso del tempo.
Creare seekers e wanderers è simile alla creazione di qualsiasi altro oggetto:
1 |
|
2 |
private void createSeeker() { |
3 |
Spatial seeker = getSpatial("Seeker"); |
4 |
seeker.setLocalTranslation(getSpawnPosition()); |
5 |
seeker.addControl(new SeekerControl(player)); |
6 |
seeker.setUserData("active",false); |
7 |
enemyNode.attachChild(seeker); |
8 |
}
|
9 |
|
10 |
private void createWanderer() { |
11 |
Spatial wanderer = getSpatial("Wanderer"); |
12 |
wanderer.setLocalTranslation(getSpawnPosition()); |
13 |
wanderer.addControl(new WandererControl()); |
14 |
wanderer.setUserData("active",false); |
15 |
enemyNode.attachChild(wanderer); |
16 |
}
|
Creiamo uno spatial, lo muoviamo, aggiungiamo un controllo personalizzato, lo impostiamo come non-attivo, e lo attribuiamo al nostro enemyNode
. Cosa? Perché non è attivo? Questo perché non vogliamo che il nemico inizi ad inseguire il giocatore non appena si genera; vogliamo dare al giocatore un pò di tempo per reagire.
Prima di addentrarci nei controlli, abbiamo bisogno di implementare il metodo di getSpawnPosition()
. Il nemico deve generarsi in modo casuale, ma non proprio accanto al giocatore:
1 |
|
2 |
private Vector3f getSpawnPosition() { |
3 |
Vector3f pos; |
4 |
do { |
5 |
pos = new Vector3f(new Random().nextInt(settings.getWidth()), new Random().nextInt(settings.getHeight()),0); |
6 |
} while (pos.distanceSquared(player.getLocalTranslation()) < 8000); |
7 |
return pos; |
8 |
}
|
Calcoliamo una nuova posizione casuale pos
. Se è troppo vicino al giocatore, si calcola una nuova posizione e ripetiamo il tutto finché non è ad una distanza decente.
Ora abbiamo solo bisogno che i nemici si attivino iniziando a muoversi. Lo faremo nei loro controlli.
Controllo del comportamento dei Nemici
Ce ne occuperemo prima con il SeekerControl
:
1 |
|
2 |
public class SeekerControl extends AbstractControl { |
3 |
private Spatial player; |
4 |
private Vector3f velocity; |
5 |
private long spawnTime; |
6 |
|
7 |
public SeekerControl(Spatial player) { |
8 |
this.player = player; |
9 |
velocity = new Vector3f(0,0,0); |
10 |
spawnTime = System.currentTimeMillis(); |
11 |
}
|
12 |
|
13 |
@Override
|
14 |
protected void controlUpdate(float tpf) { |
15 |
if ((Boolean) spatial.getUserData("active")) { |
16 |
//translate the seeker
|
17 |
Vector3f playerDirection = player.getLocalTranslation().subtract(spatial.getLocalTranslation()); |
18 |
playerDirection.normalizeLocal(); |
19 |
playerDirection.multLocal(1000f); |
20 |
velocity.addLocal(playerDirection); |
21 |
velocity.multLocal(0.8f); |
22 |
spatial.move(velocity.mult(tpf*0.1f)); |
23 |
|
24 |
// rotate the seeker
|
25 |
if (velocity != Vector3f.ZERO) { |
26 |
spatial.rotateUpTo(velocity.normalize()); |
27 |
spatial.rotate(0,0,FastMath.PI/2f); |
28 |
}
|
29 |
} else { |
30 |
// handle the "active"-status
|
31 |
long dif = System.currentTimeMillis() - spawnTime; |
32 |
if (dif >= 1000f) { |
33 |
spatial.setUserData("active",true); |
34 |
}
|
35 |
|
36 |
ColorRGBA color = new ColorRGBA(1,1,1,dif/1000f); |
37 |
Node spatialNode = (Node) spatial; |
38 |
Picture pic = (Picture) spatialNode.getChild("Seeker"); |
39 |
pic.getMaterial().setColor("Color",color); |
40 |
}
|
41 |
}
|
42 |
|
43 |
@Override
|
44 |
protected void controlRender(RenderManager rm, ViewPort vp) {} |
45 |
}
|
Concentriamoci sucontrolUpdate(float tpf)
:
In primo luogo, abbiamo bisogno di verificare se il nemico è attivo. Se non lo è, dobbiamo dissolverlo in ingresso lentamente (n.d.a. "fade out"=dissolvenza, "fade in"=assolvenza ma non si usa).
Poi controlliamo il tempo trascorso da quando abbiamo generato il nemico e, se è abbastanza a lungo, lo impostiamo come attivo.
Indipendentemente dal fatto che è stato attivato, abbiamo bisogno di regolarne il colore. La variabile locale spatial
contiene lo spaziale a cui il controllo è stato attaccato, ma dovreste ricordare che non abbiamo agganciato il controllo all'immagine attuale, l'immagine è un figlio del nodo a cui abbiamo allegato il controllo. (Se non sapete di cosa sto parlando, date un'occhiata al metodo di getSpatial (String name)
che abbiamo implementato nello scorso tutorial.)
Così; otteniamo l'immagine come figlio di spatial
, otteniamo il suo materiale ed impostiamo il suo colore al valore appropriato. Niente di speciale, una volta che vi sarete abituati agli spatials, materiali e nodi.
1
nel codice). Non vogliamo un un nemico giallo e uno rosso?E' perché il materiale mescola il colore del materiale stesso con i colori della texture, quindi se vogliamo visualizzare la trama del nemico così com'è, abbiamo bisogno di mescolarlo ad uno sfondo bianco.
Ora abbiamo bisogno di dare un'occhiata a quello che facciamo quando il nemico è attivo. Questo controllo è chiamato SeekerControl
per un motivo: vogliamo che i nemici con questo controllo collegato inseguano il giocatore.
A questo fine, calcoliamo la direzione dal seeker al giocatore e aggiungiamo questo valore alla velocità. Dopo di che, riduciamo la sua velocità del 80% in modo che non cresca all'infinito e spostiamo il seeker di conseguenza.
La rotazione non è niente di speciale: se il seeker non è ancora in piedi, lo ruotiamo nella direzione del giocatore. Quindi lo ruotarlo un pò di più, perché il seeker nel Seeker.png
non è rivolto verso l'alto, ma a destra.
rotateUpTo(Vector3f direction)
di Spatial
ruota uno spaziale in modo che il suo asse y punti nella direzione data.Questo per il primo nemico. Il codice del secondo nemico, il wanderer (errante, viandante), non è molto diverso:
1 |
|
2 |
public class WandererControl extends AbstractControl { |
3 |
private int screenWidth, screenHeight; |
4 |
|
5 |
private Vector3f velocity; |
6 |
private float directionAngle; |
7 |
private long spawnTime; |
8 |
|
9 |
public WandererControl(int screenWidth, int screenHeight) { |
10 |
this.screenWidth = screenWidth; |
11 |
this.screenHeight = screenHeight; |
12 |
|
13 |
velocity = new Vector3f(); |
14 |
directionAngle = new Random().nextFloat() * FastMath.PI * 2f; |
15 |
spawnTime = System.currentTimeMillis(); |
16 |
}
|
17 |
|
18 |
@Override
|
19 |
protected void controlUpdate(float tpf) { |
20 |
if ((Boolean) spatial.getUserData("active")) { |
21 |
// translate the wanderer
|
22 |
|
23 |
// change the directionAngle a bit
|
24 |
directionAngle += (new Random().nextFloat() * 20f - 10f) * tpf; |
25 |
System.out.println(directionAngle); |
26 |
Vector3f directionVector = MonkeyBlasterMain.getVectorFromAngle(directionAngle); |
27 |
directionVector.multLocal(1000f); |
28 |
velocity.addLocal(directionVector); |
29 |
|
30 |
// decrease the velocity a bit and move the wanderer
|
31 |
velocity.multLocal(0.8f); |
32 |
spatial.move(velocity.mult(tpf*0.1f)); |
33 |
|
34 |
// make the wanderer bounce off the screen borders
|
35 |
Vector3f loc = spatial.getLocalTranslation(); |
36 |
if (loc.x screenWidth || loc.y > screenHeight) { |
37 |
Vector3f newDirectionVector = new Vector3f(screenWidth/2, screenHeight/2,0).subtract(loc); |
38 |
directionAngle = MonkeyBlasterMain.getAngleFromVector(newDirectionVector); |
39 |
}
|
40 |
|
41 |
// rotate the wanderer
|
42 |
spatial.rotate(0,0,tpf*2); |
43 |
} else { |
44 |
// handle the "active"-status
|
45 |
long dif = System.currentTimeMillis() - spawnTime; |
46 |
if (dif >= 1000f) { |
47 |
spatial.setUserData("active",true); |
48 |
}
|
49 |
|
50 |
ColorRGBA color = new ColorRGBA(1,1,1,dif/1000f); |
51 |
Node spatialNode = (Node) spatial; |
52 |
Picture pic = (Picture) spatialNode.getChild("Wanderer"); |
53 |
pic.getMaterial().setColor("Color",color); |
54 |
}
|
55 |
}
|
56 |
|
57 |
@Override
|
58 |
protected void controlRender(RenderManager rm, ViewPort vp) {} |
59 |
}
|
Prima la roba facile; la dissolvenza in ingresso del nemico è la stesso del controllo del seeker (ricercatore). Nel costruttore, abbiamo scelto una direzione casuale per il wanderer, verso cui volerà una volta attivato.
EnemyControl
che gestirebbe tutto quello quello che i nemici hanno in comune: il movimento del nemico, la dissolvenza in ingresso, l'attivazione ...Ora le principali differenze:
Quando il nemico è attivo, prima cambiamo un pò la sua direzione, in modo che il wanderer non si muova in linea retta per tutto il tempo. Lo facciamo cambiando directAngle
un pò e aggiungendo la directionVector
alla velocity
. Poi applichiamo la velocità, proprio come facciamo nel SeekerControl
.
Dobbiamo verificare se il wanderer è al di fuori dei confini dello schermo e in caso affermativo, cambiamo il directionAngle
in una più appropriata in modo che venga applicata al prossimo aggiornamento.
Infine, ruotiamo un pò il wanderer. Solo perché un nemico ruotante è più carino.
Ora che abbiamo finito l'implementazione dei due nemici, è possibile avviare il gioco e giocarci un pò. Ti dà un pò l'idea di come sarà il gioco, anche se ancora non possiamo uccidere i nemici e loro non possono uccidere te. Aggiungiamo il prossimo.
Rilevamento Collisioni
Per rendere i nemici capaci di uccidere il giocatore, abbiamo bisogno di sapere se collidono tra loro. Per questo aggiungeremo un nuovo metodo, handleCollisions
, chiamato nel simpleUpdate(float tpf)
:
1 |
|
2 |
@Override
|
3 |
public void simpleUpdate(float tpf) { |
4 |
if ((Boolean) player.getUserData("alive")) { |
5 |
spawnEnemies(); |
6 |
handleCollisions(); |
7 |
}
|
8 |
}
|
E ora il metodo:
1 |
|
2 |
private void handleCollisions() { |
3 |
// should the player die?
|
4 |
for (int i=0; i<enemyNode.getQuantity(); i++) { |
5 |
if ((Boolean) enemyNode.getChild(i).getUserData("active")) { |
6 |
if (checkCollision(player,enemyNode.getChild(i))) { |
7 |
killPlayer(); |
8 |
}
|
9 |
}
|
10 |
}
|
11 |
}
|
Dobbiamo iterare tutti i nemici avendo contato i figli del nodo e raggiungendoli uno ad uno. Dobbiamo solo controllare se il nemico uccide il giocatore quando il nemico stesso è attivo. Se non lo è, non ci interessa. Quindi, se è attivo, controlliamo se il giocatore e il nemico si scontrano. Questo lo facciamo in un altro metodo, checkCollision(Spatial a, Spatial b)
:
1 |
|
2 |
private boolean checkCollision(Spatial a, Spatial b) { |
3 |
float distance = a.getLocalTranslation().distance(b.getLocalTranslation()); |
4 |
float maxDistance = (Float)a.getUserData("radius") + (Float)b.getUserData("radius"); |
5 |
return distance <= maxDistance; |
6 |
}
|
Il concetto è abbastanza semplice: in primo luogo, si calcola la distanza tra i due spatials. Abbiamo bisogno di sapere quanto vicino devono essere i due spatials per considerarli in collisione, quindi otteniamo i raggi di ogni spaziali e li sommiamo. (Abbiamo impostato tra i dati utente "radius" nel getSpatial (String name)
nel precedente tutorial.) Quindi, se la distanza effettiva è inferiore o uguale a questa distanza massima, il metodo restituisce true
, il che significa che si sono scontrati.
E adesso? Abbiamo bisogno di uccidere il giocatore. Creiamo un altro metodo:
1 |
|
2 |
private void killPlayer() { |
3 |
player.removeFromParent(); |
4 |
player.getControl(PlayerControl.class).reset(); |
5 |
player.setUserData("alive", false); |
6 |
player.setUserData("dieTime", System.currentTimeMillis()); |
7 |
enemyNode.detachAllChildren(); |
8 |
}
|
9 |
}
|
In primo luogo, rimuoviamo il player dal suo nodo padre, questo lo rimuove automaticamente dalla scena. Quindi, abbiamo bisogno di ripristinare il movimento nel PlayerControl
, altrimenti, il giocatore potrebbe ancora muoversi quando si rigenera di nuovo.
Quindi impostiamo il dato utente alive
su false
e creariamo un nuovo dato utente dieTime
. (Ne abbiamo bisogno per il respawn del giocatore quando è morto)
Infine, stacchiamo tutti i nemici, poiché per il giocatore sarebbe difficile combattere subito con nemici già esistenti appena viene rigenerato.
Oltre ai già citati respawning, cerchiamo di gestire il prossimo. Ci sarà, ancora una volta, da modificare il metodo simpleUpdate(float tpf)
:
1 |
|
2 |
@Override |
3 |
public void simpleUpdate(float tpf) { |
4 |
if ((Boolean) player.getUserData("alive")) { |
5 |
spawnEnemies(); |
6 |
handleCollisions(); |
7 |
} else if (System.currentTimeMillis() - (Long) player.getUserData("dieTime") > 4000f && !gameOver) { |
8 |
// spawn player |
9 |
player.setLocalTranslation(500,500,0); |
10 |
guiNode.attachChild(player); |
11 |
player.setUserData("alive",true); |
12 |
} |
13 |
} |
Quindi, se il giocatore non è vivo ed è rimasto morto abbastanza a lungo, impostiamo la sua posizione al centro dello schermo, lo aggiungiamo alla scena, e, infine, impostiamo nuovamente il suo dato utente (userdata) alive
a true
!
Ora potrebbe essere un buon momento per avviare il gioco e testare le nostre nuove funzionalità. Per circa venti secondi avrete difficoltà, perché lo sparo è inutile, quindi cerchiamo di fare qualcosa a riguardo.
Al fine di rendere i proiettili capaci di uccidere i nemici, aggiungeremo del codice del handleCollisions()
:
1 |
|
2 |
//should an enemy die?
|
3 |
int i=0; |
4 |
while (i < enemyNode.getQuantity()) { |
5 |
int j=0; |
6 |
while (j < bulletNode.getQuantity()) { |
7 |
if (checkCollision(enemyNode.getChild(i),bulletNode.getChild(j))) { |
8 |
enemyNode.detachChildAt(i); |
9 |
bulletNode.detachChildAt(j); |
10 |
break; |
11 |
}
|
12 |
j++; |
13 |
}
|
14 |
i++; |
15 |
}
|
La procedura per uccidere i nemici è più o meno la stesso che per uccidere il giocatore; iterare tutti i nemici e tutti i proiettili, verificare se si scontrano e, se lo fanno, li sganciamo entrambe.
Ora eseguiamo il gioco e vediamo quanto lontano andiamo!
Con il gameplay principale abbiamo finito. Andremo ad implementare i buchi neri, a visualizzare il punteggio e la vita del giocatore, poi per rendere il gioco più divertente ed emozionante aggiungeremo alcuni effetti sonori ed una grafica migliore. Queste ultime cose le otterremo con un filtro di trasformazione bloom post processing, alcuni effetti particellari ed un effetto al background molto bello.
Prima di considerare questa parte della serie finita, aggiungeremo un pò di audio e l'effetto bloom.
Riproduzione di Suoni e Musica
Per l'audio del vostro gioco creeremo una nuova classe, chiamata semplicemente Sound
:
1 |
|
2 |
public class Sound { |
3 |
private AudioNode music; |
4 |
private AudioNode[] shots; |
5 |
private AudioNode[] explosions; |
6 |
private AudioNode[] spawns; |
7 |
|
8 |
private AssetManager assetManager; |
9 |
|
10 |
public Sound(AssetManager assetManager) { |
11 |
this.assetManager = assetManager; |
12 |
shots = new AudioNode[4]; |
13 |
explosions = new AudioNode[8]; |
14 |
spawns = new AudioNode[8]; |
15 |
|
16 |
loadSounds(); |
17 |
}
|
18 |
|
19 |
private void loadSounds() { |
20 |
music = new AudioNode(assetManager,"Sounds/Music.ogg"); |
21 |
music.setPositional(false); |
22 |
music.setReverbEnabled(false); |
23 |
music.setLooping(true); |
24 |
|
25 |
for (int i=0; i<shots.length; i++) { |
26 |
shots[i] = new AudioNode(assetManager,"Sounds/shoot-0"+(i+1)+".wav"); |
27 |
shots[i].setPositional(false); |
28 |
shots[i].setReverbEnabled(false); |
29 |
shots[i].setLooping(false); |
30 |
}
|
31 |
|
32 |
for (int i=0; i<explosions.length; i++) { |
33 |
explosions[i] = new AudioNode(assetManager,"Sounds/explosion-0"+(i+1)+".wav"); |
34 |
explosions[i].setPositional(false); |
35 |
explosions[i].setReverbEnabled(false); |
36 |
explosions[i].setLooping(false); |
37 |
}
|
38 |
|
39 |
for (int i=0; i<spawns.length; i++) { |
40 |
spawns[i] = new AudioNode(assetManager,"Sounds/spawn-0"+(i+1)+".wav"); |
41 |
spawns[i].setPositional(false); |
42 |
spawns[i].setReverbEnabled(false); |
43 |
spawns[i].setLooping(false); |
44 |
}
|
45 |
}
|
46 |
|
47 |
public void startMusic() { |
48 |
music.play(); |
49 |
}
|
50 |
|
51 |
public void shoot() { |
52 |
shots[new Random().nextInt(shots.length)].playInstance(); |
53 |
}
|
54 |
|
55 |
public void explosion() { |
56 |
explosions[new Random().nextInt(explosions.length)].playInstance(); |
57 |
}
|
58 |
|
59 |
public void spawn() { |
60 |
spawns[new Random().nextInt(spawns.length)].playInstance(); |
61 |
}
|
62 |
}
|
Qui cominciamo impostando la variabile AudioNode
e inizializzando gli array.
Quindi, carichiamo i suoni, facendo la stessa cosa più o meno per ogni suono. Creiamo un nuovo AudioNode
, con l'aiuto del assetManager
. Poi, lo impostiamo non posizionale e disabilitiamo il riverbero. (Non abbiamo bisogno che il suono sia posizionale perché non abbiamo uscita stereo nel nostro gioco 2D, anche se sarebbe possibile implementarlo, se vi piace.) La disattivazione del riverbero rende la riproduzione il suono così come è nel file; se attivo potremmo fare suonare a jME l'audio come se ci trovassimo in una grotta o in un sotterraneo, Dopo di che, abbiamo impostato il looping a true
per la musica e a false
per qualsiasi altro suono.
Suonare i suoni è piuttosto semplice: ci basta chiamare soundX.play()
.
play()
su qualche suono, questo riproduce il suono. Ma a volte vogliamo giocare lo stesso suono due volte o anche più volte contemporaneamente. Questo è ciò che playInstance()
fa: crea una nuova istanza per ogni suono in modo che lo si possa suonare lo stesso più volte allo stesso tempo. Lascio il resto del lavoro a voi: è necessario chiamare startMusic
, shoot()
, explosion()
(per uccidere i nemici), e spawn()
nei punti appropriati della nostra classe principale MonkeyBlasterMain()
.
Quando avrete finito, vedrete che il gioco è ora molto più divertente; quei pochi effetti sonori aggiungono davvero molto all'atmosfera. Ma cerchiamo di pulire un pò di più la grafica.
L'aggiunta del filtro Bloom post-processing
L'attivazione del boom è molto semplice in jMonkeyEngine, poiché tutto il codice e gli shader necessari sono già implementati. Basta proseguire ed incollare queste righe in simpleInitApp()
:
1 |
|
2 |
FilterPostProcessor fpp=new FilterPostProcessor(assetManager); |
3 |
BloomFilter bloom=new BloomFilter(); |
4 |
bloom.setBloomIntensity(2f); |
5 |
bloom.setExposurePower(2); |
6 |
bloom.setExposureCutOff(0f); |
7 |
bloom.setBlurScale(1.5f); |
8 |
fpp.addFilter(bloom); |
9 |
guiViewPort.addProcessor(fpp); |
10 |
guiViewPort.setClearColor(true); |
Ho configurato il BloomFilter
un pò; se volete sapere che cosa fanno queste impostazioni, dovreste dare un'occhiata al tutorial jME sul bloom.
Conclusioni
Congratulazioni per aver finito la seconda parte. Ci sono ancora altre tre parti da affrontare, quindi non distraetevi giocando troppo! La prossima volta aggiungeremo l'interfaccia grafica e i buchi neri.