:: Samashooter ::
Sviluppo del Samashooter
Questa pagina ospita appunti, linee guida etc. relative allo sviluppo del Samashooter.
Organizzazione dei files
L'organizzazione più adatta sembra essere questa:
dir progetto/
data/
scripts/
lib/
shooter.jar
dove:
- data/ conterrà tutti i dati, eventualmente divisi per argomento (immagini, musiche, fonts etc.) e per livello (= sottodirectory)
- scripts/ conterrà tutti gli script
- lib/ conterrà tutte le librerie necessarie
- shooter.jar è l'"eseguibile" principale
Librerie
Le librerie necessarie sono tutte quelle della directory lib di Slick, e js.jar di Rhino. In particolare, occorre mettere nella directory lib anche tutti i files nativi di Slick, ovvero le .dll o le .so che servono per .
Se si crea un progetto (parlo per Netbeans, ma credo che con Eclipse la faccenda sia simile) si può creare la cartella lib, e specificare di andare a pescare tutte le librerie da dentro lì. Inoltre, quando si sceglie di compilare l'intero progetto, Netbeans provvederà automaticamente a copiare la cartella lib nella cartella dist, così da poter essere copiata dove si vuole.
A questo punto, è possibile eseguire il gioco così:
java -Djava.library.path=./lib -jar shooter.jar
e siamo tutti felici.
Il PRUEngine
Si chiama PRUEngine perché è nato cercando di snasare la Patty in modo virtuale, ma altro non è che un'interfaccia miniminiminimale una cui implementazione va esportata in JS:
public interface PRUEngine {
public void Esegui(String s);
public String PercorsoApplicazione();
}
Occorre poi una classe che implementi PRUEngine, e che abbia qualche collegamento con il motore Rhino:
- il metodo Esegui(String s) serve per caricare da script un altro script
- il metodo string PercorsoApplicazione() serve per restituire allo script la directory base dell'applicazione
La necessità di questo secondo comando è presto detta:
this.img = new Image(PRUEngine.PercorsoApplicazione + "/Data/Images/Sama.png")
Per bindare una classe in JS, vedi la pagina JavaScript in Java.
L'interfaccia di tutti i viventi
Slick ha già pensato a dare un'interfaccia generica, che ho scoperto andare bene praticamente per tutto, e può essere estesa facilmente. Essa si compone dei metodi init, update e render, ai quali aggiungerei checkCollision e destroy:
public void init(GameContainer c);
public void update(GameContainer c, int delta);
public void render(GameContainer c, Graphics g);
public boolean checkCollision(Shape s);
public void destroy();
Eccoli spiegati:
- init(GameContainer c) serve per inizializzare tutte le cose di cui il vivente avrà bisogno: immagini, altri viventi che dipendono da lui, e così via
- update(GameContainer c, int delta) serve per implementare la logica del gioco, e viene chiamato da Slick prima della render
- render(GameContainer c, Graphics g) invece disegna effettivamente su schermo
- checkCollision(Shape s) restituisce vero se la Shape (interfaccia di Slick) passata come argomento collide con il vivente in questione; falso altrimenti
- destroy() invece chiama la destroy di tutte le risorse che il vivente ha allocato, così da pulire la memoria
Tutte le cose, ovvero l'intero gioco, i livelli, i layer, i singoli sprite e così via, possono implementare questa interfaccia. Il vivente primordiale, cioè il gioco, propagherà la chiamata init, update o render a tutti i suoi sottoviventi, e così via in modo ricorsivo.
Esempio:
LivelloCespugli.update = function(container, delta) {
for each (p in this.ListaLayer) {
p.update(container, delta)
}
}
e via ricorrendo. Idem per render e per init.
Chi carica cosa
Ogni singolo livello, nella sua init, si occupa di caricare le proprie immagini. Le immagini degli sprite, ad esempio, vengono caricate una volta sola dal livello, e poi saranno passate come parametro ai vari sprite che vi faranno ricorso.
this.sprite = new SpriteSheet(path + "/data/spritepatty.png", 200, 173)
var p = new parabolicPatty(this.sprite, this.counter, this)
this.ListaPatty[this.counter] = p
In questo modo si evita di caricare la roba mille volte.
Sprite autocancellanti
Un vivente, finita la sua vita utile, deve poter informare il suo proprietario che non ha più nessuna ragione di vivere.
Nell'esempio qui sopra, passo il this.counter alla Patty, così che essa sa il proprio ID all'interno della ListaPatty. Quando poi la singola Patty decide di uscire di scena, chiamerà il metodo deleteID del layer cui appartiene. Questo metodo è così scritto:
LayerPatty1.deleteID = function(id) {
delete this.ListaPatty[id]
}
Avendo passato anche un this alla Patty, essa può salvare via il proprio owner, e quando vuole scomparire fa questa chiamata:
this.owner.deleteID(this.ID)
Un layer, quindi, rispetto al vivente generico, se ospita sprite dinamici deve implementare anche questo metodo.
Dimensioni corrette delle immagini
Se si vuole evitare di dover forzare una determinata risoluzione dello schermo, non si possono usare i singoli pixel come riferimenti assoluti. Esempio: il punto (200,200) in uno schermo di 800x600 sarà più vicino al centro rispetto allo stesso punto in uno schermo (1024x768). Slick però prende come parametro i pixel.
La mia idea è che i singoli sprite ragionano in pixel: vai da qui a là e poi muori, per fare un esempio. Questi parametri gli vengono passati da chi li gestisce, e sarà lui a smazzarsi le diverse risoluzioni. Le dimensioni vengono salvate in float che vanno da 0.0 a 1.0, e poi moltiplicate per la risoluzione attuale dello schermo.
Se stabiliamo ad esempio che una certa immagine deve essere larga il 25% dello schermo, possiamo dire così:
this.Scale = (container.getHeight() / 4) / this.immagine.getHeight()
e poi fare
this.immagine.draw(this.x, this.y, this.Scale)
NOTA: la scala è un valore che viene applicato a tutte le dimensioni dell'oggetto. C'è però il problema che il rapporto tra i lati dello schermo non è omogeneo. Ad esempio, 800/600 = 1.3333, 1280/1024 = 1.25 e così via, e poi ci sono gli schermi panoramici etc. etc. Occorre trovare un modo standard per gestire tutti questi aspetti, incluso anche il fregarsene e fissare 1024x768 e via:)
Programmazione