An Infinite World
"An Infinite World" è il nome di un progetto che sto portando avanti da qualche settimana nel tempo libero. È sostanzialmente un esperimento che ho fatto con XNA per vedere cosa sarei riuscito a tirarne fuori con le mie scarse competenze di grafica. Questo progetto mira a creare un semplice giochino in stile platform, dove una pallina si muove e salta da una piattaforma all'altra per arrivare alla fine del livello: un concept molto semplice per una semplice prova di programmazione. Dato che ho implementato diverse interessanti features in quello che definire un game engine è decisamente eccessivo, ho voluto dedicare una sezione separata a questo progetto, sperando che, magari, qualcuno mi aiutasse a scrivere qualche nuovo livello. Ecco una breve anteprima delle funzionalità che ho integrato.
Download
Fisica di base
Il progetto comprende una piccolissima parte dedicata alla fisica di base (ma proprio base base). Dopo aver implementato semplici metodi
di collision detection tra bounding spheres e bounding boxes di tipo axis-aligned, ho scritto qualcosa di molto banale per gestire le forze
del mondo di gioco. La pallina viene spinta da una forza applicata dal giocatore tramite i pulsanti WASD. Essa è anche dotata di massa,
perciò agisce una forza peso verso il basso. Ogni piattaforma ha un proprio
coefficiente di attrito ed esercita una forza di attrito in verso opposto al movimento della pallina. I campi di forza, i buchi neri ed
altri oggetti di gioco esercitano a loro volta altre forze in altre direzioni
sul giocatore. La risultante di queste forze dà luogo ad un'accelerazione; la velocità e la posizione della pallina vengono
calcolate di conseguenza.Dopo svariate e disperate riscritture, sono riuscito a perfezionare le fisica con queste features:
- Collisioni sempre funzionanti, anche nel caso di rotazioni delle piattaforme. Dato che questi oggetti di gioco sono incapsulati in un Bounding Box di tipo axis-aligned, è un problema calcolare collisioni esatte quando sono presenti delle rotazioni, poiché ruotando la bounding box si ottiene ancora un parallelepipedo allineato agli assi, che non è per nulla coerente con la posizione della piattaforma. Ho risolto ruotando la pallina relativamente alla piattaforma (questo approccio c'era già da prima, ma a causa di un errore di calcolo con le matrici non funzionava correttamente);
- Gravità variabile;
- Forze di attrito coerenti: la definizione data poco sopra risultava in un calcolo che forniva in output una forza opposta alla direzione del moto avente anche una componente verticale, o, più in generale, normale alla superficie delle piattaforme. Ciò è scorretto in quanto esso dovrebbe essere una componente tangente della reazione vincolare. Ho corretto questo bug proiettando la velocità su un piano parallelo alla superficie di contatto, e calcolando l'attrito di conseguenza;
- Reazioni vincolari coerenti: sono passato da una reazione solo verticale a una normale al piano d'appoggio;
- Urti semi-elastici: quando viene rintracciata una collisione, il programma calcola un versore normale al piano di impatto. Tale versore è coerente anche nel caso si vada a sbattere contro spigoli o vertici dell'oggetto. Mediante questo versore, calcola anche una possibile nuova direzione della pallina dopo l'urto con la formula del vettore riflessione. Dato che non si tratta di urti completamente elastici, parte dell'energia viene persa nell'urto (altrimenti la pallina continuerebbe a rimbalzare): la componente di energia viene dissipata solo nella direzione parallela alla normale, cosicché il giocatore non sia rallentato nel procedere in avanti quando rimbalza;
Oggetti di gioco e livelli
Quasi ogni oggetto di gioco è rappresentato da una classe GameObject. Modificandone le proprietà è possibile spostare,
ruotare, muovere, trasformare o disabilitare l'oggetto.
Oltre alla pallina, sono definiti come oggetti di gioco anche piattaforme, gemme, monete, campi di forza, checkpoints, buchi neri. Ecco un breve elenco delle classi definite:
- GameObject : la classe base per tutti gli oggetti di gioco. Oltre a definire proprietà base come posizione, velocità, accelerazione, rotazione, velocità angolare, modello, textures, fattore di scala (per il modello), massa e id, fornisce anche alcuni metodi base per l'interazione con il mondo di gioco, quali Draw (con alcune varianti in overload, permette di disegnare l'oggetto nella scena 3D), Update (aggiorna lo stato dell'oggetto, ricalcolandone la dinamica) e CollidesWith (fornisce un test di base per le collisioni, molto poco accurato). Inoltre espone l'evento CollisionDetected, generato dall'engine del gioco quando viene rintracciata una collisione;
- SphericalObject : classe derivata da GameObject che rappresenta un oggetto approssimabile a una sfera. Su questa ipotesi ridefinisce i metodi per le collisioni, rendendoli più accurati, con le funzioni TestCollision, GetCollisionInfo e ApplyStaticCollisionEffect. La prima verifica le collisioni, la seconda calcola le normali, mentre la terza applica gli effetti dell'urto all'oggetto;
- BoxObject : classe derivata da GameObject che rappresenta un oggetto approssimabile a un parallelepipedo. Su questa ipotesi ridefinisce i metodi per le collisioni, rendendoli più accurati con la funzione TestCollision. Espone come proprietà ExtensionX, ExtensionY ed ExtensionZ, che restituiscono l'estensione dell'oggetto lungo gli assi (tenendo conto della rotazione);
- Coin : classe derivata da SphericalObject che rappresenta una moneta. Nel gioco esistono vari tipi di monete:
Le Infinity Coins (sulla sinistra) azzerano istantaneamente la velocità del
giocatore:
sono molto utili per atterare senza conseguenze dopo essere stati scaraventati da qualche parte a causa, ad esempio, di un
campo di forta o di un buco nero. Le Warp Coins (sulla destra), invece, teletrasportano il giocatore in un altro punto del livello.
- Switch : classe derivata da SphericalObject che rappresenta un interruttore. Uno switch serve per attivare o disattivare certi fenomeni,
o per modificare il livello. Oltre a poter assumere i due stati accesso e spento, può essere di quattro tipi: normale, se è
possibile attivare o disattivarlo a piacere in qualsiasi momento; periodico, se una volta attivato rimane attivo per un certo tempo prima
di ritornare allo stato precedente; statico, se può essere attivato solo una volta; continuo (ancora non implementato correttamente),
se è necessaria la presenza del giocatore su di esso per farlo rimanere attivo;


- Ball : classe derivata da SphericalObject che rappresenta la pallina;
- Platform : classe derivata da BoxObject che rappresenta una piattaforma;
- ForceField : classe derivata da BoxObject che rappresenta un campo di forza. Questi oggetti non hanno un modello 3D, ma emettono
particelle (gestite da un emettitore) nello stesso verso della forza che applicano. Più particelle vengono emesse, e più
velocemente, più è intensa la forza applicata. In genere forze molto intense vengono applicate per un periodo di tempo esiguo.
È possibile regolare il comportamento del campo tramite le proprietà Force, EmittingPeriod (periodo di funzionamento del campo),
EmittingDirection e Color:

- BlackHole : classe derivata da SphericalObject che rappresenta un buco nero. I buchi neri sono oggetti dotati di massa "molto" elevata (nel gioco, solo 400 volte la massa della pallina, per i casi più piccoli), che attraggono il giocatore verso il proprio centro: anche questi non hanno modello 3D, ma sono rappresentati da un sistema di particelle nere che convergono verso il centro del buco nero;
I modelli, poiché molto semplici, li ho fatti io con blender; anche la mappatura delle coordinate texture è stata fatta con blender. Le texture sono prese dalla rete. Ringrazio particolarmente Hipshot per le sue magnifiche sky-textures.
Sistemi di particelle
Nell'engine è anche implementato un semplice sistema di gestione degli effetti particellari, che permette di gestire effetti grafici
come fumo, fuoco e semplici animazioni particellari. E' possibile determinare il moto di ogni particella grazie ad alcuni delegati:
Le particelle vengono disegnati come point sprite con una semplice texture semitrasparente a punto, e la loro dimensione viene scalata con la distanza dalla telecamera. Il colore, il movimento, la texture e le modalità di emissione possono essere cambiate a piacimento. Ad ogni sistema è associato un oggetto Emitter che gestisce il refresh e il rendering delle particelle.
Post-processing
È possibile abilitare alcuni effetti di tipo bloom, creati grazie agli shader forniti da Microsoft in alcuni esempi.
Se sei interessato a partecipare al level design di questo piccolo gioco, puoi scaricare il gioco (sorgente + eseguibile) da qui. Requisiti minimi:
- XNA Framework 3.0
- .NET Framework 3.5, poiché nel progetto vengono usate alcune features peculiari della 3.5, quali Linq to Xml, Linq to Object e l'inferenza di tipo;
- Almeno 256MB di RAM
- Scheda grafica con almeno 128MB di memoria video dedicata, che supporti il pixel shader versione 2.0
Istruzioni editor livelli
Potete accedere all'editor di livelli dal menù principale, o premendo T in qualsiasi momento di un livello (questa funzionalità
verrà rimossa presto). In modalità editor, la pallina è di dimensioni minori e indica il cursore. Per spostare il cursore
si usano i pulsanti WASD, mentre per spostare la telecamera di usano le frecce direzionali. Altri pulsanti:
- G : ritorna al gioco (potete usarlo per testare il livello)
- Esc : ritorna al menù principale
- P : aggiunge una piattaforma alla posizione del cursore
- I : aggiunge un campo di forza
- O : aggiunge una moneta
- K : aggiunge uno switch
- J : aggiunge una gemma
- H : aggiunge un checkpoint
- B : aggiunge un buco nero
- Invio : entra in modalità linea di comando
- select : seleziona l'oggetto più vicino. Nella parte bassa della finestra apparirà il tipo dell'oggetto, la sua posizione e il suo ID
- delete : elimina l'oggetto selezionato
- move : sposta l'oggetto selezionato alla posizione del cursore
- goto : sposta il cursore alla posizione dell'oggetto selezionato
- goto id : sposta il cursore alla posizione dell'oggetto con dato id (numero intero)
- goto x y z : sposta il cursore alle coordinate specificate (il separatore decimale è la virgola)
- save : salva il livello. Di default, viene salvato tutto in lev0.xml
- save n : salva il livello in un file denominato levn.xml, dove n è un numero intero
- Acceleration : vector3
- AngularSpeed : vector3
- Enabled : boolean
- ForcesResult : vector3
- ID : integer
- Mass : single (float)
- Model : model
- Position : vector3
- Rotation : vector3
- Scale : vector3
- Speed : vector3
- Texture : texture2D
- Unmovable : boolean
In questa versione la gestione degli errori è pressoché assente, quindi... state attenti XD
Per pubblicare un livello in questa sezione, mandatemi un'e-mail con allegato il file xml e le eventuali risorse aggiuntive usate.
