<< Fumo e stati di gioco | Giochino Sottomarino | Suoni >>
:: Giochino del Sottomarino - Ottobre Rosso ::
Affondiamo l'Ottobre Rosso!
L'Ottobre Rosso
Dal momento che si tratta di un gioco di sottomarini, non possiamo astenerci dall'infilarci l'Ottobre Rosso:) Questo sarà il boss di fine gioco (uhm dopo ben un livello il gioco già finisce). Sarà più complesso rispetto ai sottomarini normali, oltre ad essere più grosso.
Comportamento
L'Ottobre Rosso esce improvvisamente da un lato dello schermo e si dirige a gran velocità verso l'altro lato. Ogni tanto decide di sparare missili verso la superficie, cercando di colpire la Nave. Se la colpisce, il giocatore perde tot punti (no, non mettiamo le vite alla Nave). Il giocatore deve colpire tre volte l'Ottobre Rosso, prima di affondarlo. Quando viene colpito, l'Ottobre Rosso lampeggia come da tradizione videoludica. Infine, quando è stato affondato, si inabisserà lentamente nell'Abisso Laurenziano.
Comunicazione con lo stato di gioco
L'Ottobre Rosso lancerà dei missili, ma spetta allo stato di gioco visualizzarli e gestirne la collisione con la Nave. Dobbiamo quindi inventare un metodo per permettere all'Ottobre Rosso di inviare messaggi allo stato di gioco
ManagerOttobreRosso
Per ottenere questo, inventiamo una nuova interfaccia che chiamiamo ManagerOttobreRosso. Lo StatoBoss implementerà quest'interfaccia.
public interface ManagerOttobreRosso {
public void lanciaMissile(float x, float y);
public void affondamentoCompleto();
}
- lanciaMissile dice allo StatoBoss di lanciare un missile in prossimità di quelle coordinate;
- affondamentoCompleto comunica che ha finito di affondare, così che lo StatoGioco può passare al prossimo stato.
Robe standard
Anche l'Ottobre Rosso è una Entity. Qui ci sono le variabili standard che fanno parte dell'Ottobre Rosso. I metodi soliti li lascio a voi.
private Rectangle box;
private Image ottobreDx;
private Image ottobreSx;
private boolean direction;
private boolean active = false;
private float speed = 200.0f / 1000.0f;
A queste dobbiamo aggiungere un po' di robe
private ManagerOttobreRosso myGestore;
Viene passato al costruttore dell'Ottobre Rosso:
public OttobreRosso(ManagerOttobreRosso lm) throws SlickException {
this.myGestore = lm;
ottobreDx = new Image("./data/ottobrerosso.png");
ottobreSx = ottobreDx.getFlippedCopy(true, false);
box = new Rectangle(0, 0, ottobreDx.getWidth(), ottobreSx.getHeight());
}
In StatoBoss costruiremo nel metodo init l'Ottobre Rosso:
ottobreRosso = new OttobreRosso(this);
Come dicevo sopra, StatoBoss deve implementare l'interfaccia ManagerOttobreRosso. Per ora lasciamo i due metodi vuoti.
Occorrono però anche altre variabili per gestire vari aspetti dell'Ottobre Rosso.
Partenza
L'Ottobre Rosso parte ad intervalli regolari da un bordo dello schermo, e si dirige verso l'altro bordo. Per fare questo ci serve un Timer:
private Timer partenzaTimer;
Lo inizializziamo nel costruttore:
partenzaTimer = new Timer(new Runnable() {
public void run() {
partenza();
}
}, 2 * 1000);
Questo vuol dire che l'Ottobre Rosso rimarrà "fuori" dallo schermo per due secondi, prima di decidere di partire.
Nella run() viene invocato il metodo partenza, in cui facciamo partire effettivamente il sottomarino:
private void partenza() {
float depth = (float) (300 + Math.random() * (screenHeight - 300 -
box.getHeight()));
double caso = Math.random() * 100;
if (caso > 50) {
direction = true;
box.setLocation(0 - box.getWidth(), depth);
} else {
direction = false;
box.setLocation(screenWidth, depth);
}
active = true;
}
La variabile active è un booleano che viene impostato a false nel costruttore. Sta ad indicare se l'Ottobre Rosso sta facendo qualcosa oppure riposa ai lati dello schermo.
Le variabili screenWidth e screenHeight sono le dimensioni dello schermo. Per averle, occorre che venga passato il GameContainer al costruttore, e da lì le trarremo mediante container.getWidth() e container.getHeight().
Andiamo quindi in update. La prima cosa è aggiornare il timer della partenza:
partenzaTimer.update(delta);
e poi controllare il movimento secondo il solito schema:
if (active) {
int multi = direction ? 1 : -1;
float x = box.getX() + this.speed * delta * multi;
box.setLocation(x, box.getY());
Dobbiamo anche controllare che il sottomarino non sia uscito dai bordi. Se è così, allora dobbiamo disattivarlo e far ripartire il partenzaTimer:
if ((direction && box.getX() > container.getWidth())
|| (!direction && box.getX() < -box.getWidth())) {
active = false;
// Faccio ripartire il timer
partenzaTimer.start();
}
In render quindi dobbiamo disegnare il sottomarino:
if (active) {
if (direction) {
ottobreDx.draw(box.getX(), box.getY());
} else {
ottobreSx.draw(box.getX(), box.getY());
}
}
E quindi abbiamo già l'Ottobre Rosso che scorrazza per lo schermo di gran carriera:)
Missili
Ci vuole un Timer:
private Timer missileTimer;
e nel costruttore lo tiriamo in piedi:
missileTimer = new Timer(new Runnable() {
public void run() {
double caso = Math.random() * 100;
if (caso > 40) {
myGestore.lanciaMissile(box.getX() + box.getWidth() / 2, box.getY());
}
missileTimer.stop();
missileTimer.start();
}
}, 1300);
La variabile caso mi dà la probabilità che il missile venga effettivamente lanciato, confrontandola con il 40. Viene chiamato il metodo lanciaMissile del ManagerOttobreRosso, ovvero lo StatoBoss.
Dobbiamo però anche ricordare che i missili possono essere lanciati solo se il sottomarino è attivo, cioè sta girando per lo schermo. Subito dopo, quindi, nel costruttore, chiamiamo
missileTimer.stop();
per far sì che questo Timer non prosegua.
In update lo aggiorniamo:
missileTimer.update(delta);
Quando il sottomarino parte, cioè in partenza, facciamo partire anche il timer:
missileTimer.start();
Quando il sottomarino esce dallo schermo, lo fermiamo:
missileTimer.stop();
La classe Missile
Il missile
Il Missile viene gestito dallo StatoBoss, allo stesso modo in cui vengono gestite anche le altre entità, cioè infilandolo in un'ArrayList e così via.
Stabiliamo però che il Missile arriverà al massimo alla superficie dell'acqua, e quando arriva qui verificherà se è entrato in collisione con la Nave. Anche in questo caso, il Missile deve poter comunicare con lo StatoBoss, e utilizziamo il solito sistema di interfacce, creandone una appositamente che verrà implementata da StatoBoss.
public interface CheckMissileNave {
public void check(float sx, float ex);
}
Questo metodo viene invocato dal Missile quando si accorge di essere finito fuori "range". sx è il valore sulle ascisse del lato sinistro della bounding box del Missile, mentre ex è il valore del lato destro sempre sullo stesso asse. Se uno dei due estremi è incluso tra gli estremi della bounding box della Nave allora vuol dire che questa è stata colpita.
Andiamo quindi subito ad implementare il metodo check in StatoBoss:
public void check(float sx, float ex) {
Rectangle playerBox = player.getBoundingBox();
if ((sx >= playerBox.getX() && sx <= playerBox.getX() + playerBox.getWidth())
|| (ex >= playerBox.getX() && ex <= playerBox.getX() + playerBox.getWidth()))
{
Log.info("Il missile ha colpito la nave!");
attivaEsplosione(playerBox.getCenterX(), playerBox.getCenterY());
Globali.punti -= 200;
}
}
Non so se l'avevo detto precedentemente, comunque si suppone che lo StatoBoss sia in pratica una replica di StatoGioco, e che quindi abbia anche lui un player e così via.
Missile
Veniamo dunque al Missile. Quando lo costruiamo, gli diciamo quale deve essere il suo limite superiore. Quando arriva al limite chiamerà il check che abbiamo appena visto.
private boolean active = false;
private Image missile;
private Rectangle box;
private float upperLimit;
private float speed = 150.0f / 1000.0f;
private CheckMissileNave checkMissileNave;
public Missile(float upperLimit, CheckMissileNave cmn) throws SlickException {
this.upperLimit = upperLimit;
this.checkMissileNave = cmn;
missile = new Image("./data/missile.png");
box = new Rectangle(0, 0, missile.getWidth(), missile.getHeight());
}
Ci mettiamo anche il metodo start, che fa le seguenti cose:
public void start(float x, float y) {
active = true;
box.setLocation(x, y);
}
Questo vuol dire che in StatoBoss possiamo finalmente implementare il metodo lanciaMissile di ManagerOttobreRosso:
public void lanciaMissile(float x, float y) {
for (Missile m : missili) {
if (!m.isActive()) {
m.start(x, y);
break;
}
}
}
Tornando al Missile, il metodo update è il solito, con l'aggiunta della chiamata a checkMissileNave quando termina la vita utile:
public void update(GameContainer container, StateBasedGame game, int delta) {
if (active) {
float y = box.getY() - speed * delta;
if (y < upperLimit) {
active = false;
checkMissileNave.check(box.getX(), box.getX() + box.getWidth());
}
box.setLocation(box.getX(), y);
}
}
E anche render scade nella banalità:)
public void render(GameContainer container, StateBasedGame game, Graphics g) {
if (active) {
missile.draw(box.getX(), box.getY());
}
}
E con questo anche il Missile è funzionante! Quando l'OttobreRosso decide di sparare un Missile, si rivolge al suo manager, che nel nostro caso è ancora StatoBoss. A sua volta il Missile, quando arriva alla superficie, si rivolge al suo manager per dirgli di eseguire il controllo per l'eventuale collisione con la Nave. Così facendo si eseguono controlli solo quando strettamente necessario. Non che siano costosi, sti calcoli, ma può essere una tecnica interessante:)
Lampeggiamento dell'Ottobre Rosso
Come dicevamo sopra, quando viene colpito l'Ottobre Rosso deve "lampeggiare", cioè diventare trasparente ad intermittenza, come succedeva nei giochi di una volta. Costruiamo quindi un Timer chiamato lampeggiaTimer in OttobreRosso, e creiamo anche una variabile boolean hit. Il Timer lo inizializziamo così:
lampeggiaTimer = new Timer(new Runnable() {
public void run() {
hit = false;
}
}, 1 * 1000);
e ricordiamoci di aggiornarlo in update. Ci serve anche un LFO, che chiamiamo hitLFO, e lo inizalizziamo così:
hitLFO = new LFO(10, 0, 1);
Vogliamo che l'Ottobre Rosso lampeggi 10 volte al secondo.
La variabile hit viene messa a true nel metodo hit di OttobreRosso, che a sua volta viene chiamato da StatoBoss quando si verifica una collisione tra una Bomba e il sottomarinone rosso. Il metodo hit è fatto così:
public void hit() {
life--;
hit = true;
hitLFO.restart();
lampeggiaTimer.start();
}
In update mettiamo anche:
if (hit) {
hitLFO.update(delta);
}
ma la cosa interessante viene in render:
Image i = direction ? ottobreDx : ottobreSx;
if (!hit || hitLFO.getValue() >= 0.0f) {
i.draw(box.getX(), box.getY());
}
Con questo codice facciamo in modo che, quando l'Ottobre Rosso viene colpito, allora usiamo l'LFO per sapere se disegnare o meno l'immagine. Facciamo così "lampeggiare" l'Ottobre Rosso.
Sequenza di affondamento
L'ultimo comportamento dell'Ottobre Rosso riguarda il suo affondamento. Quando viene colpito 3 volte, allora deve affondare. La sequenza di affondamento consiste nel sottomarino che ruota la propria prua verso il fondo del mare, e scompare. Ci serve una variabile di stato booleana che chiamiamo affondamento. Per non sbagliare la mettiamo a true in start:
public void start() {
partenzaTimer.start();
life = 3;
affondamento = false;
angolo = 0;
ottobreDx.setRotation(0.0f);
ottobreSx.setRotation(0.0f);
}
Ci servono poi anche le variabili per gestire la rotazione e l'affondamento del sottomarino:
private float angolo = 0;
private float velAngolare = 360.0f / 10000.0f;
private float velAffondamento = 50.0f / 1000.0f;
La velAngolare è la velocità di rotazione, mentre la velAffondamento è la velocità lineare con cui il sottomarino si inabissa.
Aggiungiamo il metodo kill:
public void kill() {
affondamento = true;
missileTimer.stop();
}
che verrà chiamato dallo StatoBoss quando, controllando che le vite dell'Ottobre Rosso sono diventate 0, deciderà di ucciderlo.
Sia update che render andranno modificati, perché il sottomarino deve avere comportamenti diversi a seconda della "fase" in cui si trova. Se sta funzionando normalmente, deve andare di qua e di là e lanciare missili etc. Se invece sta affondando, deve fare tutt'altro, ovvero ruotare puntando la prua verso il basso e poi scomparire, e quando è finito sotto lo schermo deve chiamare il metodo affondamentoCompleto del suo ManagerOttobreRosso. Ecco quindi l'intero metodo update riscritto in luce di queste nuove scoperte:
public void update(GameContainer container, StateBasedGame game, int delta) {
partenzaTimer.update(delta);
lampeggiaTimer.update(delta);
missileTimer.update(delta);
if (active) {
if (affondamento) {
angolo += velAngolare * delta;
if (angolo > 90.0f) {
angolo = 90.0f;
}
float y = box.getY() + velAffondamento * delta;
box.setLocation(box.getX(), y);
if (y >= container.getHeight() + box.getWidth() / 2) {
myGestore.affondamentoCompleto();
affondamento = false;
}
} else {
int multi = direction ? 1 : -1;
float x = box.getX() + this.speed * delta * multi;
box.setLocation(x, box.getY());
if (hit) {
hitLFO.update(delta);
}
if ((direction && box.getX() > container.getWidth())
|| (!direction && box.getX() < -box.getWidth())) {
active = false;
missileTimer.stop();
// Faccio ripartire il timer
partenzaTimer.start();
}
}
}
}
ed ecco anche il metodo render:
public void render(GameContainer container, StateBasedGame game, Graphics g) {
if (active) {
if (affondamento) {
if (direction) {
ottobreDx.setRotation(angolo);
ottobreDx.draw(box.getX(), box.getY());
} else {
ottobreSx.setRotation(-angolo);
ottobreSx.draw(box.getX(), box.getY());
}
} else {
Image i = direction ? ottobreDx : ottobreSx;
if (!hit || hitLFO.getValue() >= 0.0f) {
i.draw(box.getX(), box.getY());
}
}
}
}
Con questo, l'Ottobre Rosso è concluso e questa lunga e faticosa pagina è terminata!:)
<< Fumo e stati di gioco | Giochino Sottomarino | Suoni >>
Guide