XNA - ubertini

MASSIMO UBERTINI
XNA
WWW.UBERTINI.IT
MODULO 1
XNA
XNA
Installazione
Prima applicazione
Mondo 2D
Rimbalzi
Random
Input
File
Menu
Librerie
Mondo 3D
Telecamera
Terreno
XACT
Progetti
XNA
um 1 di 196
XNA (XNA ‘S NOT ACRONYMED)
INTRODUZIONE
XNA è un acronimo ricorsivo, in altre parole un acronimo che contiene se stesso; nel logo,
la parte arancione, è la parola XNA in codice morse: linea – punto – punto – linea.
ZUNE
È il nome della linea di prodotti multimediali di Microsoft che include un lettore
multimediale, un client S/W e il negozio online Zune Marketplace.
È l’unico prodotto Microsoft della linea di media devices portatili.
Inizialmente fu rilasciato nel novembre del 2006 come un lettore basato su hard disk da 30
GB in grado di riprodurre musica, video e radio FM (Frequency Modulation).
Xbox 360
È una console per videogiochi di sesta generazione prodotta da Microsoft.
Rappresenta il tentativo di entrare nel mercato delle console da parte di Microsoft, dopo
aver collaborato con SEGA (SErvice GAmes) nel convertire Windows CE (Compact
Edition) per la console Dreamcast.
È stata immessa sul mercato nell’autunno del 2005.
La console è venduta in più modelli: Arcade, Pro e Elite.
1. CPU (Central Processing Unit).
1.1. Processore Tricore IBM PowerPC Xenon a 3.2 GHz da 170 milioni di transistor.
1.2. Un MB di memoria cache L2 condivisa fra i 3 core.
1.3. Ogni core è corredato di 2 thread H/W.
1.3.1. Un’unità per il calcolo d’interi.
1.3.2. Un’unità per il calcolo in virgola mobile.
1.3.3. Un’unità vettoriale VMX-128.
XNA
um 2 di 196
2.
3.
4.
5.
6.
7.
1.3.4. 128 registri VMX-128 per thread H/W.
1.3.5. 64 KB memoria cache L1.
1.4. Capacità matematica totale della CPU: 9.6 miliardi di operazioni al secondo.
1.5. Performance complessiva: 115 GigaFLOPS.
Memoria.
2.1. GDDR3 (Graphics Double Data Rate) 512 MB 500 MHz unificata: sistema e video.
System Bandwidth.
3.1. Bus memoria 23.4 Gbps.
3.2. FSB (Front Side Bus) 22.6 Gbps.
3.3. Performance complessiva :1 TeraFLOPS.
GPU (Graphics Processing Unit).
4.1. Processore ATI (Array Technologies Incorporated) Xenos da 337 milioni di
transistor.
4.2. ATI Xenos basic core stabilizzato ad una frequenza di clock di 500 MHz a 160
operazioni per ciclo.
4.3. eDRAM (embedded Dynamic Random Access Memory ) di 10 MB.
4.4. NEC (Nippon Electric Company) EDRAM Core da 120 Milioni di transistor.
4.5. Fill rate in pixel da 16 gigasample al secondo con MSAA 4x (Multi Sampling Anti
Aliasing).
4.6. Architettura di ombreggiatura unificata per pixel e vertex shaders, costituita da 48
ALU (Arithmetic Logic Unit) operanti su vector4+1scalar.
4.7. Rendering grafico di picco pari a 1.5 miliardi di vertici al secondo, 500 milioni di
triangoli.
4.8. Risoluzione: 50/60/120 Hz, 4:3, 16:9, 16:10, 480p, 720p, 1080i, 1080p.
Storage.
5.1. Disco rigido 2.5” SATA (Serial Advanced Technology Attachment) da
20/60/120/250 GB.
5.2. DVD-ROM 12X dual-layer.
5.3. Memory Unit da 64/256/512 MB.
5.4. Pendrive USB (Universal Serial Bus) da 1/2/4/8/16 GB per ogni slot.
I/O.
6.1. Quattro porte per controller wireless operativi a 2.4 GHz.
6.2. Cinque porte USB 2.0.
6.3. Due porte per Memory Unit.
6.4. Porta Kinect autoalimentata.
6.5. Uscite video: SCART (Syndicat des Constructeurs d’Appareils Radiorécepteurs et
Téléviseurs) avanzato, composito, S-video, VGA (Video Graphics Array), HDMI
v1.2 (High-Definition Multimedia Interface).
Audio2.
7.1. Dolby Digital Surround 5.1, 7.1 solo film, anche su cavo HDMI.
7.2. DTS (Digital Theater System).
7.3. LPCM.
7.4. Supporto audio a 64 bit a 48 KHz.
7.5. 320 canali a decompressione indipendenti.
7.6. Elaborazione audio a 64 bit.
7.7. Più di 256 canali audio.
AltiVec è un insieme d’istruzioni SIMD (Single Instruction Multiple Data) in virgola mobile
sviluppato da Apple Computer, IBM (International Business Machines) e Motorola e
implementato sulle ultime versioni dei processori PowerPC.
AltiVec è un marchio registrato di Motorola.
Apple definisce l’unità dedita alla gestione di queste istruzioni Velocity Engine.
IBM utilizza la sigla VMX per identificare questo gruppo d’istruzioni.
XNA
um 3 di 196
STORIA
La programmazione di videogiochi per Windows inizia con Game SDK (Software
Development Kit) che fu poi ribattezzato come DirectX e si può far risalire all’uscita di
Windows 95.
La versione 1.0 disponeva solo di DirectDraw, DirectInput e DirectSound.
La versione 7.0 permetteva di programmare le API (Application Programming Interface)
non solo con il linguaggio Visual C ma anche con il Visual Basic.
La versione 9.0 aveva un componente S/W disegnato secondo le specifiche della
piattaforma .NET chiamato Managed DirectX che permise di programmare le API con il
linguaggio Visual C#, gli sviluppatori fondarono un team che progettò, nel 2006, la
versione 1.0 del framework XNA.
La versione 2.0 del 2007 introdusse il supporto al networking via Xbox LIVE e permise di
utilizzarne la relativa estensione XNA Game Studio per tutte le versioni dell’IDE
(Integrated Development Environment) Visual Studio 2005.
La versione 3.0 del 2008 aggiunse il supporto per la piattaforma Zune e la possibilità di
pubblicare e vendere le applicazioni mediante il servizio Microsoft Xbox LIVE.
La versione 3.1 del 2009 aggiunse il supporto a Zune HD, gli Avatar e il playback dei
video.
La versione 4.0 del 2010 aggiunse il supporto al microfono, audio dinamico, profili H/W,
incluso il supporto alla piattaforma per smartphone Windows Phone.
Consente un livello di astrazione più elevato, permettendo ai programamtori di
concentrarsi sul contenuto e la logica del gioco, piuttosto che sui dettagli implementativi di
basso livello.
Possibilità di creare propri shaders, oltre a quelli messi a disposizione dal framework,
utilizzando l’HLSL (High Level Shader Language) per la realizzazione di applicazioni 3D di
grande qualità.
XNA
um 4 di 196
INSTALLAZIONE
INTRODUZIONE
Installare il WPDT (Windows Phone Developer Tools) che include anche l’emulatore dello
smartphone.
Verificare che nel menu Start appaiano le seguenti voci.
XNA include i seguenti oggetti.
Xbox LIVE
È un servizio online offerto da Microsoft per scaricare demo, trailer o versioni in digital
delivery dei giochi, per chattare con gli amici o scaricare a pagamento musica e film.
È utilizzabile gratuitamente con una sottoscrizione di un account di tipo Xbox Live Free
oppure a pagamento per avere servizi aggiuntivi quali l’accesso a Facebook e il
multiplayer.
Xbox Live Indie Games
Sono i giochi creati con XNA da sviluppatori indipendenti per la Xbox 360 e rilasciati sul
relativo marketplace mediante la piattaforma Xbox LIVE.
XNA Game Development Framework
È un insieme di librerie di classi managed, specializzate per lo sviluppo di videogiochi per
Windows, Xbox 360 e Windows Phone.
XNA Game Studio
È un’estensione all’IDE Visual Studio che contiene, oltre al framework, dei template di
progetto e dei tool di ausilio.
È possibile scegliere tra una serie predefinita di template, distinti in base alla piattaforma
su cui si vuole sviluppare: Windows Phone, Windows o Xbox 360.
Alla natura del progetto, a seconda che si realizzi il gioco vero, un eseguibile per una delle
XNA
um 5 di 196
tre piattaforme disponibili oppure creare una libreria da referenziare nei progetti.
XNA permette un’alta portabilità di un’applicazione da una piattaforma ad un’altra.
In pratica, è sufficiente cliccare con il tasto destro sul tipo di piattaforma nella finestra
Esplora soluzione che s’intende convertire e scegliere la piattaforma su cui si desidera
“migrare”, lasciando al framework il compito di apportare le necessarie modifiche.
App Hub
È un portale http://create.msdn.com esplicitamente pensato per offrire tool, esempi di
codice, risorse e tutorial utili a sviluppare applicazioni e giochi per Windows Phone e Xbox
360.
Con una sottoscrizione di un abbonamento annuale, App Hub membership, è offerta la
possibilità di pubblicare la propria creazione sul marketplace di Windows Phone o della
Xbox 360.
MONOXNA
È l’alternativa Open Source di XNA che ha il fine di effettuare il porting su altre
piattaforme.
 GNU/Linux (GNU is Not Unix).
 Apple iOS.
 Google Android.
http://www.monoxna.org/
http://monogame.codeplex.com/
XNA
um 6 di 196
PRIMA APPLICAZIONE
INTRODUZIONE
Fare clic su File/Nuovo progetto… (CTRL+N), selezionare in Modelli installati/Altri
linguaggi/Visual C#/XNA game Studio 4.0.
L’IDE genera una soluzione composta da due progetti.
1. WindowsGame1
Costituisce il gioco vero e proprio e, come tale, è destinato a ospitare il codice e la logica
necessari all’applicazione per funzionare.
2. WindowsGame1Content (Content)
Contraddistinto dal suffisso Content aggiunto automaticamente al nome del progetto
principale, è invece finalizzato a raccogliere, importare e processare, tramite una Content
pipeline tutte le risorse “esterne”, definite asset che si utilizzano nel gioco, quali ad
esempio immagini e texture 2D, modelli 3D, suoni e audio.
L’importer: ha il compito di “tradurre” questi file in un formato standard.
Il processor interpreta questi file in modo da arrivare ad una forma che sia facilmente
“consumabile” dal run-time.
XNA ricorre ad una content pipeline, per incrementare le prestazioni, altrimenti le risorse
dovrebbero essere caricate nel loro formato originario e il progetto, prima di poterle
utilizzare, dovrebbe convertirle a run-time in una forma che possa essere utilizzata
all’interno dell’applicazione, aumentando molto i tempi di attesa.
La content pipeline rimedia a questo problema, anticipando la conversione degli asset al
momento della compilazione dei sorgenti.
XNA
um 7 di 196
Un progetto di tipo Content è associato al progetto principale tramite la cartella
Riferimenti contenuto.
È possibile condividere lo stesso progetto Content fra giochi diversi, consentendo il
riutilizzo di sprite, modelli e suoni tra diverse applicazioni, così come è possibile avere più
progetti Content per uno stesso gioco, nel caso, ad esempio che due giochi condividano
alcune risorse ma necessitino anche di risorse ulteriori diverse per ciascun gioco.
In XNA, logica e contenuti si presentano nettamente separati, non solo da un punto di
vista concettuale ma anche dal punto di vista architetturale, dal momento che danno vita a
progetti distinti e, in una certa misura, autonomi.
Properties
Contiene i file che descrivono il tipo di applicazione e che, di conseguenza, variano a
seconda della piattaforma prescelta.
Per esempio, scegliendo di realizzare un gioco per Windows Phone, i file sono tre.
File ASSEMBLYINFO.CS
Contiene le informazioni sull’assembly: informazioni generali dell’applicazione, il nome del
progettista, il Copyright ed altre informazioni generali.
File APPMANIFEST.XML
Descrive le caratteristiche della distribuzione dell’applicazione.
File WMAPPMANIFEST.XML
Contiene la definizione dei nomi delle risorse, come le immagini, del titolo dell’app, del
titolo da usare nella schermata principale, la versione applicativa, il tipo di run-time e il
ProductID.
In figura, il gioco è per Windows per cui c’è un solo file ASSEMBLYINFO.CS.
Riferimenti
Contiene le librerie esterne che utilizzerà il gioco.
File GAME.ICO
Icona che identifica il gioco una volta creato.
File GAMETHUMBNAIL.PNG
Immagine che identifica il gioco.
XNA
um 8 di 196
File PROGRAM.CS
Contiene l’entry point dell’applicazione all’interno della quale il corrispettivo metodo Main
crea un’istanza della classe principale dell’applicazione Game1, creata di default dal
framework e definita nel file GAME1.CS ed esegue il metodo Run.
I progettisti di XNA hanno posizionato il metodo Main in questo file, così da non rendere
troppo pesante il file principale su cui si deve lavorare, per cui questo file non si deve
mai modificare, inoltre, il namespace è ovviamente lo stesso del file Game1.cs.
using System;
namespace WindowsGame1 {
#if WINDOWS || XBOX
static class Program {
/// <summary>
/// Punto di ingresso principale dell’applicazione.
/// </summary>
static void Main(string[] args) {
using (Game1 game = new Game1())
{
game.Run();
}
}
}
#endif
}
File GAME1.CS
Contiene la definizione della classe Game1 che eredita dalla classe base
Microsoft.Xna.Framework.Game e fornisce i metodi base per l’inizializzazione delle
funzioni grafiche del dispositivo, per la gestione della logica del gioco e per il codice che si
occuperà del rendering grafico.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
namespace WindowsGame1 {
// Questo è il tipo principale per il gioco
public class Game1 : Microsoft.Xna.Framework.Game {
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
public Game1() {
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
// Consente di eseguire tutte le inizializzazioni necessarie prima dell’avvio del gioco.
// Qui è possibile eseguire query dei servizi necessari e caricare contenuto correlato
// non grafico.
XNA
um 9 di 196
// La chiamata a base.Initialize comporta l’enumerazione di tutti i componenti
// e la loro inizializzazione.
protected override void Initialize() {
// TODO: aggiungere qui la logica di inizializzazione
base.Initialize();
}
// LoadContent verrà chiamato una volta per gioco e costituisce il punto in cui caricare
// tutto il contenuto.
protected override void LoadContent() {
// Creare un nuovo SpriteBatch che potrà essere utilizzato per disegnare trame.
spriteBatch = new SpriteBatch(GraphicsDevice);
// TODO: utilizzare this.Content per caricare qui il contenuto del gioco
}
// UnloadContent verrà chiamato una volta per gioco e costituisce il punto in cui
// scaricare tutto il contenuto.
protected override void UnloadContent() {
// TODO: scaricare qui tutto il contenuto non gestito da ContentManager
}
// Consente al gioco di eseguire la logica per, ad esempio, aggiornare il mondo,
// controllare l’esistenza di conflitti, raccogliere l’input e riprodurre l’audio.
// <param name="gameTime">
// Fornisce uno snapshot dei valori di temporizzazione.</param>
protected override void Update(GameTime gameTime) {
// Consente di uscire dal gioco
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
// TODO: aggiungere qui la logica di aggiornamento
base.Update(gameTime);
}
// Viene chiamato quando il gioco deve disegnarsi.
// <param name="gameTime">
// Fornisce uno snapshot dei valori di temporizzazione.</param>
protected override void Draw(GameTime gameTime) {
GraphicsDevice.Clear(Color.CornflowerBlue);
// TODO: aggiungere qui il codice di disegno
base.Draw(gameTime);
}
}
}
La classe Game1 possiede i seguenti oggetti.
L’oggetto graphics di tipo GraphicsDeviceManager rappresenta la porta di accesso verso il
corrispondente dispositivo grafico: la scheda video.
L’oggetto spriteBatch di tipo SpriteBatch rappresenta l’elemento essenziale per disegnare
immagini 2D, chiamate sprite.
C’è l’indicazione della cartella, Root, in cui andare a cercare le risorse multimediali
aggiunte al progetto Content e, nel caso di un’applicazione per Windows Phone, di fissare
il framerate a 30 frame per secondo tramite la proprietà TargetElapsedTime.
Nel metodo Initialize si scrive il codice per l’inizializzazione di variabili e oggetti associati
con l’applicazione, per esempio inizializzare i valori del punteggio del gioco.
Nel metodo LoadContent, invocato dopo il metodo Initialize, oppure ogni volta che il
contenuto grafico del gioco dev’essere ricaricato, si scrive il codice per il caricamento di
tutte le risorse di cui il gioco necessita per poter funzionare: sprite, modelli 3D, shader,
suoni e font.
XNA
um 10 di 196
Nel metodo Update, invocato dopo il metodo LoadContent, si scrive il codice per la
gestione della logica del gioco, per esempio la posizione degli sprite sullo schermo, il
rilevamento delle collisioni tra gli oggetti, la gestione dell’input della tastiera e del mouse.
Mediante il parametro gameTime è possibile determinare l’intervallo di tempo trascorso dal
precedente Update o dall’inizio del gioco e quindi intraprendere azioni come uscire dal
gioco se il tempo a disposizione del giocatore fosse esaurito.
Nel metodo Draw, invocato dopo il metodo Update, si scrive il codice per disegnare sullo
schermo utilizzando il device grafico, di default, ad ogni chiamata il metodo provvede a
cancellare lo schermo, GraphicsDevice.Clear, prima di qualunque altra operazione di
scrittura a video e a impostare il colore del background, di default CornflowerBlue; al
termine della sua esecuzione è invocato nuovamente il metodo Update.
Nel metodo UnloadContent, invocato dopo il metodo Update, all’uscita del “game loop” del
gioco, si scrive il codice per scaricare dalla memoria gli oggetti precedentemente allocati
che richiedono una loro gestione personalizzata e indipendente da quella che effettua il
GC (Garbage Collector).
I metodi elencati sono invocati in modo sequenziale fino al metodo Update e quest’ultimo
invoca il metodo Draw, il quale invoca nuovamente il metodo Update; tale modalità
d’invocazione si ripete in modo ciclico, in quella che è definita nella programmazione dei
videogiochi come il game loop.
Inoltre, per ogni iterazione del loop, dal metodo Update, si verifica se è presente una
condizione per l’uscita dal gioco e, in caso positivo, si esce dal game loop e il sistema
invoca il metodo UnloadContent.
Compilare ed eseguire la soluzione, senza modificare il codice inserito, si ottiene la
seguente finestra.
GAME LOOP
Il game loop è l’elemento fondamentale di un gioco, ossia il suo “cuore pulsante”.
È la la continua iterazione fra l’aggiornamento della logica di gioco ad esempio, la
posizione e lo stato del giocatore e dei nemici, la posizione e l’orientamento della
telecamera, la ricerca d’input da parte dell’utente e le operazioni di disegno a schermo.
XNA
um 11 di 196
Questa struttura ciclica è connaturata con l’essenza stessa di gioco che deve potersi
svolgere anche a prescindere da o addirittura in assenza d’input da parte dell’utente.
Il game loop è un ciclo formato da metodi che sono invocati ininterrottamente e in una
determinata sequenza, finchè il gioco non termina.
Nel framework XNA essi sono rappresentati dai metodi Update e Draw e il relativo game
loop è ripetuto 60 volte al secondo.
Frame
È una singola scena o fotogramma facente parte di una sequenza di scene o fotogrammi
che formano un’animazione.
La frequenza, o velocità temporale, con cui avviene il cambiamento di scena, ovvero il
passaggio da un frame ad un altro, è definita come frame rate ed è indicata FPS (Frame
Per Secondo).
In XNA ogni 16 millisecondi, 60 volte al secondo, lo schermo è cancellato e una nuova
scena è nuovamente disegnata.
Per default, si lavora con una risoluzione standard di 800X600 in modalità finestra.
In XNA, il compito d’iterare continuamente tra i diversi momenti di gioco, in particolare
quello della logica e quello del disegno è interamente demandato al framework che vi
provvede invocando i diversi metodi esposti dalla classe base Game secondo un preciso
ordine, i cui passaggi principali possono essere così riassunti.
Questi sono gli stessi metodi che si trovano anche nella classe Game1 che provvede alla
loro concreta implementazione, tramite ovveride.
I metodi Initialize, LoadContent e UnloadContent sono invocati una sola volta, all’inizio o
subito prima dell’uscita dal gioco.
Il loop vero e proprio riguarda due soli metodi: Update e Draw, chiamati ininterrottamente
dal framework a intervalli di tempo più o meno regolari.
XNA
um 12 di 196
Chiamare i due metodi “a passo fisso”: fixed-step game loop
Se il carico di lavoro dovesse rendere impossibile rispettare l’intervallo prefissato, XNA
provvederà a regolare l’iterazione tra i due metodi, saltando, ad esempio, alcune
operazioni di disegno a schermo.
XNA chiama il metodo Update al raggiungimento del periodo temporale espresso dalla
proprietà TargetElapsedTime che, di default, è settata a 16 millisecondi.
Dopo tale invocazione, se non è giunto ancora il momento d’invocare nuovamente il
metodo Update, XNA invoca il metodo Draw e successivamente, se non è giunto ancora il
tempo d’invocare nuovamente il metodo Update, il sistema pone il gioco in attesa,
altrimenti invoca tale metodo.
Se, tuttavia, il metodo Update impiega troppo tempo a compiere le sue elaborazioni, XNA
imposta a true la proprietà IsRunningSlowly e invoca il metodo Update nuovamente, senza
però richiamare il relativo metodo Draw.
In pratica, si possono potenzialmente avere numerose invocazioni del metodo Update,
rispetto ad un’invocazione del metodo Draw.
Chiamare i due metodi “a passo variabile”: variable-step game loop
XNA invoca i metodi Update e Draw in un loop continuo, senza riguardo della proprietà
TargetElapsedTime, significando con ciò che invoca una volta il metodo Update, una volta
il metodo Draw e poi ripete tale sequenza finché non si esce dal gioco.
Sono chiamati alternativamente nel mimimo intervallo di tempo possibile rispetto alla
configurazione H/W del PC e con conseguente fluttuazione del framerate in base al carico
di lavoro di CPU e scheda grafica, in pratica, in quest’ultimo caso, l’ammontare del tempo
che passa tra i frame successivi non è costante.
È possibile scegliere il tipo di game loop, impostando la proprietà IsFixedTimeStep della
classe Game, essa accetta il valore true che è quello di default, per un loop a step
prefissato e false per un loop a step variabile.
Nel game loop si scrive, dunque, del codice che serve sia a gestire la logica del gioco sia
a disegnare gli oggetti che lo compongono.
In pratica si fa sempre qualcosa, aggiornare le animazioni, muovere gli oggetti, verificare
le collisioni e ciò avviene indipendentemente se l’utente stia compiendo delle operazioni.
Questo pone in risalto una caratteristica della programmazione dei videogiochi che si
differenzia dalla programmazione di un’applicazione tradizionale dotata di una GUI
(Graphic User Interface) ed event-driven.
Un’applicazione tradizionale è guidata dagli eventi che un utente compie e che sono stati
registrati sui relativi componenti e che dunque il sistema genera al fine di farli intercettare
e gestire, per esempio in un form l’applicazione rimane in attesa che l’utente faccia clic
con il mouse oppure digiti sulla tastiera.
Un’applicazione orientata ai videogiochi, piuttosto che registrare ed essere in ascolto per
l’accadimento dei relativi eventi, effettua un polling degli stessi; ossia elabora una
continua richiesta al sistema per verificare se è accaduto qualcosa d’interessante che
deve gestire e nel contempo continuare a compiere anche altre operazioni
indipendentemente dall’input dell’utente.
Ad esempio, il metodo Update, ad ogni sua invocazione, chiede al sistema se l’utente ha
premuto il pulsante BACK della Xbox 360 e in caso affermativo invoca il metodo Exit della
classe Game che farà terminare il gioco.
XNA
um 13 di 196
Esempio, progettare una finestra con lo sfondo che cambia colore ad un intervallo di
tempo regolare.
File GAME1.CS
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
namespace RandomBackground {
public class Game1 : Microsoft.Xna.Framework.Game {
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Random rnd;
Color[] colors = {Color.AliceBlue, Color.Beige, Color.Blue, Color.CornflowerBlue,
Color.DarkRed, Color.ForestGreen, Color.LightCoral, Color.MediumBlue,
Color.Olive, Color.Orchid, Color.PeachPuff, Color.Silver, Color.Teal};
int current_index;
String color_values;
public Game1() {
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize() {
// TODO: Add your initialization logic here
rnd = new Random();
TargetElapsedTime = TimeSpan.FromSeconds(1);
base.Initialize();
}
protected override void LoadContent() {
spriteBatch = new SpriteBatch(GraphicsDevice);
}
protected override void UnloadContent() {
}
protected override void Update(GameTime gameTime) {
XNA
um 14 di 196
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
current_index = rnd.Next(0, 13);
color_values = colors[current_index].ToString();
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime) {
GraphicsDevice.Clear(colors[current_index]);
Window.Title = color_values;
base.Draw(gameTime);
}
}
}
Il metodo Initialize inizializza la variabile rnd di tipo Random che serve per ottenere un
numero casuale da utilizzare per estrarre, dall’array colors, il colore di sfondo da impiegare
per la finestra dell’applicazione.
Esso, inoltre, imposta ad un secondo di tempo la proprietà TargetElapsedTime della
classe Game che permetterà d’invocare il metodo Update ogni volta che passerà
quell’intervallo di tempo stabilito.
Il metodo Update che, ad ogni sua invocazione, ottiene un valore numerico differente tra 0
e 12 utilizzabile per assegnare, alla stringa color_values, i valori RGBA (Red, Green, Blue,
Alpha) del relativo colore estratto.
Infine, nel metodo Draw, si utilizza il metodo Clear della classe GraphicsDevice che
cancella qualsiasi cosa si trova nella finestra e la riempie con il colore casuale all’uopo
ottenuto.
FONT
È possibile scrivere del testo ma a differenza di una normale applicazione Windows, XNA
non può utilizzare i font installati, bisogna prima creare un oggetto di classe SpriteFont che
è una rappresentazione grafica del carattere.
Aggiungere al progetto Content la cartella FONT, quindi Aggiungi\Nuovo elemento…
Selezionare Tipo di carattere Sprite e nella casella Nome: Font1.spritefont.
XNA
um 15 di 196
XNA non crea il font ma un file XML (eXtensible Markup Language) che contiene le
istruzioni per il compilatore.
Basta ora modificare le caratteristiche del carattere da importare, quindi essenzialmente il
nome e la dimensione.
Per esempio, per creare un oggetto SpriteFont per il font Courier New, di dimensione 20,
basterà modificare come segue le corrispondenti sezioni del file.
<FontName>Segoe UI Mono</FontName>
<Size>24</Size>
Molti font sono sottoposti a licenza commerciale, quindi non sono liberamente utilizzabili in
un videogame XNA.
Microsoft ha messo a disposizione diversi font redistribuibili liberamente con i giochi.
File FONT1.SPRITEFONT
<?xml version="1.0" encoding="utf-8"?>
<!-This file contains an xml description of a font, and will be read by the XNA
Framework Content Pipeline. Follow the comments to customize the appearance
of the font in your game, and to change the characters which are available to draw
with.
-->
<XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics">
<Asset Type="Graphics:FontDescription">
<!-Modify this string to change the font that will be imported.
-->
<FontName>Segoe UI Mono</FontName>
<!-Size is a float value, measured in points. Modify this value to change
the size of the font.
-->
<Size>24</Size>
<!-Spacing is a float value, measured in pixels. Modify this value to change
the amount of spacing in between characters.
-->
<Spacing>0</Spacing>
<!-UseKerning controls the layout of the font. If this value is true, kerning information
will be used when placing characters.
-->
<UseKerning>true</UseKerning>
<!-Style controls the style of the font. Valid entries are "Regular", "Bold", "Italic",
and "Bold, Italic", and are case sensitive.
-->
<Style>Regular</Style>
<!-If you uncomment this line, the default character will be substituted if you draw
or measure text that contains characters which were not included in the font.
-->
<!-- <DefaultCharacter>*</DefaultCharacter> -->
XNA
um 16 di 196
<!-CharacterRegions control what letters are available in the font. Every
character from Start to End will be built and made available for drawing. The
default range is from 32, (ASCII space), to 126, (‘~’), covering the basic Latin
character set. The characters are ordered according to the Unicode standard.
See the documentation for more information.
-->
<CharacterRegions>
<CharacterRegion>
<Start>&#32;</Start>
<End>&#126;</End>
</CharacterRegion>
</CharacterRegions>
</Asset>
</XnaContent>
XNA
um 17 di 196
MONDO 2D
INTRODUZIONE
Lo sprite è una figura bidimensionale che può essere spostata rispetto allo sfondo.
Vector2 posizione = new Vector2(0f, 0f);
È una coppia di valori di tipo float che identificano un punto sullo schermo, rispettivamente
la X e la Y della posizione, è possibile impostare la posizione di un oggetto
bidimensionale, come un’immagine, un testo o uno sprite animato.
Se per esempio, si volesse porre uno sprite, un oggetto 2D, al centro dello schermo,
lavorando con una risoluzione di 800X600, la sua posizione sarebbe la seguente.
X=400
Y=300
Il Vector2 si dichiara con il codice seguente.
posizione1 = new Vector2(400, 300);
Se si volesse, per esempio che lo sprite si collocasse in alto a destra, si deve impostare.
posizione1 = new Vector2(790, 10);
A questo punto basta modificare la X o la Y per vederlo muovere.
Infatti, se all’interno del metodo Update si aumentasse di 1, un pixel, la X dello sprite, lo
vedremmo muoversi verso destra, muovendosi alla velocità di un pixel a fotogramma.
VISUALIZZARE IMMAGINI E SUONI
XNA permette di caricare e gestire suoni, musiche e immagini.
Aggiungere al progetto Content uno sprite in formato PNG (Portable Network Graphics)
per sfruttarne la trasparenza, ove presente e un effetto sonoro, in formato MP3 e poi
utilizzare il metodo Content.Load per creare degli oggetti.
Per riprodurre il suono si utilizza il metodo Play che permette di determinare il volume,
l’altezza del suono e il bilanciamento stereo.
Play(0.75f, 0.2f, 0.0f);
Il primo parametro rappresenta il volume, compreso tra 0.0f (silenzio) e 1.0f (volume
massimo), 1.0f corrisponde al volume massimo relativo a SoundEffect.MasterVolume.
Il secondo parametro è il pitch del suono, volume, compreso tra -1.0f (silenzio, 1 ottava in
basso) e 1.0f (volume massimo, 1 ottava in alto), 0.0f è il tono unitario (normale).
Il terzo parametro è il bilanciamento, panoramica, compreso tra -1.0f (completamente a
sinistra) e 1.0f (completamente a destra), 0.0f è centrata.
Aggiungere, al progetto Content le cartelle IMMAGINI e MUSICA, uno sprite in formato
PNG per sfruttarne la trasparenza, ove presente, un effetto sonoro in formato WAV
(WAVEform audio file format) ed una canzone in formato MP3.
Per importare un asset nella soluzione è sufficiente cliccare con il tasto destro sul progetto
di tipo WindowsGame1Content (Content), selezionare Aggiungi/Elemento esistente…
(CTRL+D) e scegliere il file da aggiungere, oppure trascinare il file direttamente nel
XNA
um 18 di 196
progetto.
Sarà lo stesso framework a scegliere il tipo d’importer e di processor adeguati.
Se si fa clic anche sull’immagine PNG importata, si vede nella finestra Proprietà una
proprietà indicata come Nome asset che riporta il nome della risorsa che per default è
sempre il nome del file importato e che rappresenta l’identificativo da usare per il suo
utilizzo.
Quindi, una volta importati all’interno del progetto, gli asset assumono come nome di
default quello del file originale ma privo della relativa estensione, visto che sono stati
importati e processati dalla Content Pipeline.
Da questo momento in poi, sarà solo il nome dell’asset a identificare le risorse utilizzate
all’interno della soluzione.
A livello di classe, aggiungere una variabile per la sprite a due dimensioni e una per
l’effetto sonoro.
La variabile di tipo Vector2(float x, float y) memorizza le coordinate dello sprite sullo
schermo, inizialmente posizionato all’origine degli assi, nonché una seconda variabile
XNA
um 19 di 196
dello stesso tipo per settare la velocità con cui muovere lo sprite.
Texture2D sprite;
SoundEffect beep;
Song concerto; // musica da suonare
Vector2 position = Vector2.Zero;
Vector2 speed = new Vector2(1.0f, 3.0f);
Nel metodo LoadContent, caricare in memoria i relativi asset.
spriteFont = Content.Load<SpriteFont>("Font/font1");
sprite = Content.Load<Texture2D>("Immagini/light");
beep = Content.Load<SoundEffect>("Musica/ball");
concerto = Content.Load<Song>("Musica/concerto");
Adesso bisogna animare lo sprite, lo si fa muovere lungo lo schermo a velocità costante.
Nel momento in cui lo sprite raggiungerà un bordo, lo si fa rimbalzare, invertendo la
velocità lungo l’asse corrispondente e riprodurre il suono per indicare l’avvenuto contatto
con il bordo.
La logica è definita nel metodo Update.
position += speed;
if (position.X <= 0 || position.X >= GraphicsDevice.Viewport.Width - sprite.Width)
speed.X *= -1; beep.Play();
if (position.Y <= 0 || position.Y >= GraphicsDevice.Viewport.Height - sprite.Height)
speed.Y *= -1; beep.Play();
Si usano le proprietà GraphicsDevice.Viewport.Width e GraphicsDevice.Viewport.Height
per recuperare le dimensioni dello schermo.
Fatto questo, si può procedere a disegnare lo sprite a schermo, mediante il metodo Draw
esposto dalla classe SpriteBatch, ossia la classe del framework che si occupa proprio di
disegnare sprite a due dimensioni, per il testo, invece, occorre usare il metodo DrawString.
Come parametri si passa la texture dello sprite, la sua posizione sullo schermo e il colore.
Ogni chiamata al metodo spriteBatch.Draw dev’essere necessariamente preceduta da una
chiamata al metodo spriteBatch.Begin che permette d’impostare alcune proprietà relative,
ad esempio, alla gestione delle trasparenze e all’ordine con il quale gli sprite devono
essere disegnati, nell’esempio, si utilizzano le impostazioni di default, senza effettuare
alcun overload, e deve concludersi con una chiamata al metodo spriteBatch.End che
provvede a resettare il device al suo stato originario.
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
spriteBatch.Draw(sprite, position, Color.White);
// stringa da stampare
spriteBatch.DrawString(spriteFont, info, new Vector2(200, 100), Color.White);
spriteBatch.End();
File GAME1.CS
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
XNA
um 20 di 196
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using System.Text;
namespace WindowsGame1 {
public class Game1 : Microsoft.Xna.Framework.Game {
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
SpriteFont spriteFont;
Texture2D sprite;
SoundEffect beep;
Song concerto; // musica da suonare
Vector2 position = Vector2.Zero;
Vector2 speed = new Vector2(1.0f, 3.0f);
string info;
// stringa da stampare
public Game1() {
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize() {
// ripete la musica
MediaPlayer.IsRepeating = true;
base.Initialize();
}
protected override void LoadContent() {
spriteFont = Content.Load<SpriteFont>("Font/font1");
sprite = Content.Load<Texture2D>("Immagini/light");
beep = Content.Load<SoundEffect>("Musica/ball");
concerto = Content.Load<Song>("Musica/concerto");
MediaPlayer.Play(concerto);
spriteBatch = new SpriteBatch(GraphicsDevice);
}
protected override void UnloadContent() {
spriteFont = null;
beep=null;
concerto = null;
}
protected override void Update(GameTime gameTime) {
// stringa da stampare
StringBuilder sb = new StringBuilder();
sb.AppendLine("Ciao, mondo!");
position += speed;
if (position.X <= 0 || position.X >= GraphicsDevice.Viewport.Width - sprite.Width) {
speed.X *= -1; beep.Play();
}
if (position.Y <= 0 || position.Y >= GraphicsDevice.Viewport.Height - sprite.Height) {
speed.Y *= -1; beep.Play();
}
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
// stringa da stampare
info = sb.ToString();
base.Update(gameTime);
XNA
um 21 di 196
}
protected override void Draw(GameTime gameTime) {
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
spriteBatch.Draw(sprite, position, Color.White);
// stringa da stampare
spriteBatch.DrawString(spriteFont, info, new Vector2(200, 100), Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
}
}
XNA
um 22 di 196
Esempio.
File GAME1.CS
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
namespace Sprite {
public class Game1 : Microsoft.Xna.Framework.Game {
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
// i nostri sprite
Texture2D boy, princess_girl, tree_tall, rock;
Texture2D background;
Rectangle window;
Vector2 boy_position, rock_position, tree_position,princess_position;
float boy_speed, princess_speed;
public Game1() {
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize() {
window = Window.ClientBounds;
window.X = 0;
window.Y = 0;
boy_position = new Vector2(1, 300);
rock_position = new Vector2(50, 290);
XNA
um 23 di 196
tree_position = new Vector2(490, 280);
princess_position = new Vector2(600, 300);
boy_speed = 1.2f;
princess_speed = 2.4f;
base.Initialize();
}
protected override void LoadContent() {
spriteBatch = new SpriteBatch(GraphicsDevice);
boy = Content.Load<Texture2D>(@"immagini\Boy");
princess_girl = Content.Load<Texture2D>(@"immagini\Princess Girl");
tree_tall = Content.Load<Texture2D>(@"immagini\Tree Tall");
rock = Content.Load<Texture2D>(@"immagini\Rock");
background = Content.Load<Texture2D>(@"immagini\sfondo");
}
protected override void UnloadContent() {
}
protected override void Update(GameTime gameTime) {
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
boy_position.X += boy_speed;
if (boy_position.X > 85 || boy_position.X < 1)
boy_speed *= -1;
princess_position.X -= princess_speed;
if (princess_position.X < 400 || princess_position.X > 700)
princess_speed *= -1;
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime) {
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
spriteBatch.Draw(background, window, Color.White);
spriteBatch.Draw(boy, boy_position, Color.White);
spriteBatch.Draw(rock, rock_position, Color.White);
spriteBatch.Draw(tree_tall, tree_position, Color.White);
spriteBatch.Draw(princess_girl, princess_position, Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
}
}
L’oggetto spriteBatch di tipo SpriteBatch che consente di renderizzare un gruppo
d’immagini 2D sullo schermo, ha i seguenti principali metodi.
1. void Begin()
Marca l’inizio delle operazioni di disegno delle immagini.
Esso ha un metodo in overload con la seguente firma.
void Begin(SpriteSortMode sortMode, BlendState blendState)
Per decidere l’ordine cui disegnare le immagini, layer depth e la modalità con cui i colori
degli sprite sono “fusi” con i colori di background.
2. void End()
Marca la fine delle operazioni di disegno delle immagini e le invia al display grafico
XNA
um 24 di 196
nell’ordine cui sono state descritte.
3. void Draw (Texture2D texture, Rectangle destinationRectangle,Color color)
Disegna lo sprite rappresentato da texture in un rettangolo di destinazione e con un
determinato colore.
4. void Draw (Texture2D texture, Rectangle destinationRectangle,Nullable<Rectangle>
sourceRectangle,Color color)
Come il precedente ma in più consente di scegliere la porzione dell’immagine sorgente da
disegnare, il valore null disegnerà l’intera immagine.
5. void Draw (Texture2D texture, Rectangle destinationRectangle,Nullable<Rectangle>
sourceRectangle,Color color, float rotation, Vector2 origin,SpriteEffects effects, float
layerDepth)
Come il punto 4 ma in più consente, per l’immagine, di ruotarla, capovolgerla e applicarle
un indice d’ordinamento.
6. void Draw (Texture2D texture, Vector2 position,Color color)
Disegna lo sprite alla posizione e con il colore indicati.
7. void Draw (Texture2D texture, Vector2 position,Nullable<Rectangle> sourceRectangle,
Color color)
Come il punto 6 ma in più consente di scegliere la porzione dell’immagine sorgente da
disegnare.
8. void Draw (Texture2D texture, Vector2 position,Nullable<Rectangle> sourceRectangle,
Color color,float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth)
Come il punto 5, si differenzia perché vi sono: il vettore di posizionamento al posto del
rettangolo di destinazione; il parametro scale che permette di ridimensionare l’immagine
secondo di un fattore di scala.
9. void Draw (Texture2D texture, Vector2 position, Nullable<Rectangle> sourceRectangle,
Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float
layerDepth)
Come il punto 8 ma il fattore di scala è espresso tramite un vettore.
Per l’esempio, nel metodo Draw della classe Game1, si è usato il metodo Draw della
classe SpriteBatch del punto 3, per applicare un’immagine di background in un rettangolo
di destinazione che è grande quando tutta l’area occupata dalla finestra,
Window.ClientBounds e quello del punto 6, per disegnare gli altri sprite e posizionarli a
partire dalle coordinate X e Y definite nei relativi oggetti vettori, Vector2.
Per XNA, nel 2D, le coordinate dell’area di visualizzazione partono dai valori 0 per X,
angolo a sinistra e 0 per Y, angolo in alto e crescono in positivo per X per spostamenti alla
sua destra, mentre crescono in positivo per Y per spostamenti verso il basso.
Esaminando i vari metodi Draw della classe SpriteBatch, si nota come tutti hanno in
comune un parametro di tipo Texture2D che rappresenta una sorta di “contenitore” dentro
il quale caricare uno sprite prelevato dalla Content Pipeline attraverso il metodo Load della
classe ContentManager.
Infatti, tutti gli sprite, nel metodo LoadContent della classe Game1, sono caricati, ciascuno
nel proprio oggetto di tipo Texture2D, con il metodo Load, al quale è passato come
parametro il path e il nome dell’asset della relativa immagine.
Per muovere gli sprite si usa il metodo Update della classe Game1 dove ad ogni
invocazione dello stesso, le posizioni, la coordinata X, degli sprite sono incrementate o
decrementate rispetto ai valori posti nelle variabili di tipo float chiamate come boy_speed e
XNA
um 25 di 196
princess_speed.
Ciò significa, in pratica che ad ogni iterazione del game loop il metodo Update cambia il
valore della coordinata X mentre il metodo Draw disegna lo sprite con quel valore
cambiato che, di fatto, ne causa lo spostamento.
ORDINAMENTO DEGLI SPRITE
Nell’esempio si è usato un ordinamento di renderizzazione per gli sprite, detto Z order,
secondo cui l’ultima immagine disegnata è sempre posta prima delle altre.
Tuttavia, questa tecnica non è indicata se si devono disegnare tanti sprite, dove ciascuno
deve avere un raffinamento del suo ordinamento.
A tal fine, si deve usare il metodo Begin della classe SpriteBatch e passargli come primo
parametro il valore SpriteSortMode.BackToFront, per far disegnare, prima di un altro, lo
sprite con il valore del parametro layerDepth del metodo Draw più vicino allo 0 e il valore
SpriteSortMode.FrontToBack per far disegnare, prima di un altro, lo sprite con il valore del
parametro layerDepth del metodo Draw più vicino all’uno.
Infine, è utile precisare che i valori di ordinamento sono di tipo float e vanno da 0.0 a 1.0.
Ad esempio, il codice seguente mostra come decidere che lo sprite della roccia debba
essere disegnato dietro lo sprite del ragazzo, posto che al metodo Begin sia stato passato
il parametro SpriteSortMode.BackToFront.
spriteBatch.Draw(boy,boy_position, null, Color.White,0,Vector2 Zero,
1,SpriteEffects.None,0.1f);
// più vicino allo 0 allora disegnalo sopra…
spriteBatch.Draw(rock,rock_position, null,Color.White, 0, Vector2.Zero, 1,
SpriteEffects.None,0.2f);
// più lontano dallo 0 disegnalo sotto…
XNA
um 26 di 196
Esempio, visualizzare una Texture2D di sfondo a due dimensioni.
Cartella CONTENT/IMMAGINI/AGGIUNGI/ELEMENTO ESISTENTE/IMG.JPG.
File GAME1.CS
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace PrimoGioco
{ public class Game1 : Microsoft.Xna.Framework.Game
{ GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
// dichiarazione della texture
Texture2D img;
// dichiarazione della posizione della texture
Vector2 posizione;
public Game1()
{ graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize()
{ spriteBatch = new SpriteBatch(GraphicsDevice);
// punto in cui si vuole visualizzare l’immagine
posizione = new Vector2(0f, 0f);
base.Initialize();
}
protected override void LoadContent()
{ // caricare il file
img = Content.Load<Texture2D>("Immagini/img");
}
protected override void UnloadContent() { }
protected override void Update(GameTime gameTime)
{ base.Update(gameTime); }
protected override void Draw(GameTime gameTime)
{ graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
// inizia la stampa
spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Deferred,
SaveStateMode.SaveState);
// il metodo Draw stampa a video l’immagine, i tre parametri indicano la texture
// la sua posizione e il colore
spriteBatch.Draw(img, posizione, Color.White);
// finisce la stampa
spriteBatch.End();
base.Draw(gameTime);
}
}
}
XNA
um 27 di 196
L’immagine è visualizzata a sinistra e in alto, perché la posizione scelta è (0,0), il punto
dell’immagine su cui XNA si basa per posizionarla, si chiama punto d’origine.
Ogni sprite, texture o elemento 2D ha il suo punto d’origine e, se non è specificato
diversamente, corrisponde all’angolo in alto e a sinistra dell’elemento 2D, il punto (0,0).
Color
L’immagine ha dimensioni minori di 800X600, quindi rimane dello spazio vuoto intorno ad
essa, del classico colore di default di XNA, celeste.
La riga che imposta questo colore è la seguente.
graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
S’imposta un colore, quello dello sfondo, l’istruzione è la seguente.
sfondo = new Color(
Appena si apre la parentesi per impostare i parametri da assegnare a questo oggetto della
classe Color, sono suggeriti 5 modi differenti (con parametri differenti e di numero
differente), per impostare il colore tra cui, uno che utilizza 4 variabili di tipo byte.
Questo sarà il modo che si utilizza, il quinto.
Dal commento che fornisce Visual Studio, s’intuisce che la prima variabile di tipo byte che
si deve inserire imposterà il colore rosso, la seconda il verde e la terza il blu.
La quarta variabile definisce l’alpha dell’immagine, la componente alpha definisce la
trasparenza globale di un’immagine.
Il modo descritto, il quinto permette d’impostare anche questo valore.
XNA
um 28 di 196
Il tipo di dato byte è un numero che va da 0 a 255, per impostare un colore specificando le
componenti rosso, verde e blu, si devono definire queste 3 variabili utilizzando un numero
che va da 0 a 255 dove 255 rappresenta il massimo per quella componente.
Per esempio, per creare il colore blu, si deve impostare a 0 la componente rossa e verde e
impostare al massimo la componente blu.
colore_sfondo = new Color(0, 0, 255);
In questo esempio, si crea un nuovo oggetto della classe Color di colore blu senza però
impostare l’alpha, omettendo la variabile che identifica l’alpha essa sarà come se fosse
impostata al massimo, in pratica a 255, infatti, scrivere Color(0,0,255); equivale a scrivere
Color(0,0,255,255).
Oltre ad impostare i colori mescolando le componenti RGB (Red, Green, Blue), la classe
Color mette a disposizione una serie di colori preimpostati.
colore= Color.
Notare la mancanza della parola chiave new perché non si crea un nuovo oggetto ma si
usa uno già impostato, inoltre, si vede una lista di colori tra cui si può scegliere e si vede
una finestra che elenca anche come è composto questo colore.
Riga che imposta il colore di sfondo e i diversi modi in cui si può scrivere.
1. graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
2. graphics.GraphicsDevice.Clear(new Color (100,100,100));
3. Color = sfondo;sfondo = new Color (100,100,100);
graphics.GraphicsDevice.Clear(sfondo);
VISUALIZZARE UN’IMMAGINE AL CENTRO DELLO SCHERMO
Visualizzare, al centro dello schermo, una Texture2D di sfondo a due dimensioni.
Si usa la seguente istruzione.
spriteBatch.Draw(img, posizione, Color.White);
Basta cambiare il punto d’origine dell’immagine modificandolo da (0,0), default, nel centro
dell’immagine.
In questo modo si può stampare l’immagine e fare sì che il punto centrale dell’immagine
corrisponda al punto centrale dello schermo.
Per fare questo si deve usare, in un altro modo, il metodo Draw della classe SpriteBatch.
spriteBatch.Draw(img, posizione, null, Color.White, 0, centro_immagine, 1,
SpriteEffects.None, 1);
Il metodo ha 9 parametri, quelli che interessano sono i seguenti.
XNA
um 29 di 196
Primo parametro: la texture che rimarrà quella di prima.
Secondo parametro: la posizione sullo schermo.
Sesto parametro: il punto d’origine dell’immagine.
Centro dell’immagine
centro_immagine = new Vector2(img.Width / 2, img.Height / 2);
Basta creare un nuovo Vector2 che ha le coordinate X, Y nel punto centrale della
Texture2D.
Width significa larghezza in pixel di tipo int.
Height significa altezza in pixel di tipo int.
Creando un nuovo Vector2 che ha come X la metà della larghezza dell’immagine
(img.Width/2) e come Y la metà dell’altezza dell’immagine (img.Heigth/2), si crea il punto
che identifica il centro esatto dell’immagine.
File GAME1.CS
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace PrimoGioco
{ public class Game1 : Microsoft.Xna.Framework.Game
{ GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Texture2D img;
Vector2 posizione;
// il punto d’origine dell’immagine
Vector2 centro_immagine;
public Game1()
{ graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize()
{ spriteBatch = new SpriteBatch(GraphicsDevice);
// si deve cambiare il punto posizione da (0,0) al centro della finestra
// in 800*600 il centro è (400,300)
// posizione = new Vector2(400, 300);
// ma cambiando la risoluzione non si è più al centro!!!!
// bisogna fare la stessa cosa che si è fatto per l’immagine
// dividendo la larghezza e l’altezza della finestra per 2.
posizione = new Vector2(graphics.PreferredBackBufferWidth / 2,
graphics.PreferredBackBufferHeight / 2);
base.Initialize();
}
protected override void LoadContent()
{ img = Content.Load<Texture2D>("Immagini/img");
// calcolo il punto d’origine dell’immagine e lo salvo in centro_immagine
centro_immagine = new Vector2(img.Width / 2, img.Height / 2);
}
protected override void UnloadContent()
{ }
XNA
um 30 di 196
protected override void Update(GameTime gameTime)
{ base.Update(gameTime); }
protected override void Draw(GameTime gameTime)
{ graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Deferred,
SaveStateMode.SaveState);
spriteBatch.Draw(img, posizione, null, Color.White, 0, centro_immagine, 1,
SpriteEffects.None, 1);
spriteBatch.End();
base.Draw(gameTime);
}
}
}
L’immagine è al centro dello schermo, qualsiasi siano le sue dimensioni o la risoluzione
che si usa.
Per testare se questo è effettivamente così, si possono scrivere queste righe di codice che
modificano la risoluzione dell’applicazione.
graphics.PreferredBackBufferWidth = 1024;
graphics.PreferredBackBufferHeight = 800;
L’impostazione della risoluzione sarà effettuata nel costruttore della classe.
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
graphics.PreferredBackBufferWidth = 1024;
XNA
um 31 di 196
graphics.PreferredBackBufferHeight = 800;
}
Per vedere il gioco a tutto schermo si usa un valore booleano.
graphics.IsFullScreen = true;
In conclusione, scrivendo true se si vuole che l’applicazione sia esegua a tutto schermo e
false se la si vuole in finestra.
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
graphics.PreferredBackBufferWidth = 1024;
graphics.PreferredBackBufferHeight = 800;
graphics.IsFullScreen = true;
}
SPOSTARE L’IMMAGINE CON LA TASTIERA
La classe KeyboardState ha il compito di controllare lo stato della tastiera, in pratica
controlla se un tasto è stato premuto o rilasciato.
Dichiarazione e inizializzazione della classe.
KeyboardState stato_tastiera;
stato_tastiera = Keyboard.GetState();
La classe Keyboard possiede il metodo GetState che fornisce lo stato della tastiera che
serve a controllare se è premuto un tasto.
Per fare questo si deve eseguire questo controllo ad ogni fotogramma, ripetutamente, in
ogni momento dell’applicazione, per questo si utilizza il metodo Update perché è eseguito
ad ogni fotogramma.
protected override void Update(GameTime gameTime)
{ stato_tastiera = Keyboard.GetState();
Così la variabile stato_tastiera sarà sempre aggiornata e le azioni dell’utente sulla tastiera
saranno sempre intercettate
Come tasto si sceglie la lettera D.
if (stato_tastiera.IsKeyDown(Keys.D))
IsKeyDown è un metodo della classe KeyboardState che controlla se il tasto è premuto.
Keys è un’ulteriore classe che permette di specificare un determinato tasto.
Cosa succede se il tasto D è premuto?
if (stato_tastiera.IsKeyDown(Keys.D))
posizione.X++;
posizione .X, è un Vector2, se il tasto D è premuto, la X del Vector2 posizione; dev’essere
aumentare di 1.
Siccome questo controllo è eseguito in ogni momento del gioco, non appena si preme il
tasto D sulla tastiera, la X del Vector2 che identifica la posizione dello sprite, aumenterà di
1 pixel.
Al fotogramma successivo sarà effettuato di nuovo il controllo e se il tasto è ancora
premuto la X dello sprite aumenterà di nuovo di 1 pixel e così via finché è tenuto premuto il
tasto A: questo è il modo con cui lo sprite si muove.
XNA
um 32 di 196
File GAME1.CS
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace PrimoGioco
{ public class Game1 : Microsoft.Xna.Framework.Game
{ GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Texture2D img;
Vector2 posizione;
Vector2 centro_immagine;
KeyboardState stato_tastiera;
public Game1()
{ graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize()
{ spriteBatch = new SpriteBatch(GraphicsDevice);
posizione = new Vector2(graphics.PreferredBackBufferWidth / 2,
graphics.PreferredBackBufferHeight / 2);
base.Initialize();
}
protected override void LoadContent()
{ img = Content.Load<Texture2D>("Immagini/img");
centro_immagine = new Vector2(img.Width / 2, img.Height / 2);
}
protected override void UnloadContent()
{ }
protected override void Update(GameTime gameTime)
{ // teniamo sempre aggiornato il controllo dello stato della tastiera
stato_tastiera = Keyboard.GetState();
//controlliamo se è premuto il tasto D
if (stato_tastiera.IsKeyDown(Keys.D))
{ /*Se la condizione D è premuto è vera, la X del Vector2 posizione
aumenterà di 1 pixel ad ogni fotogramma verso destra */
posizione.X++;
}
//controlliamo se è premuto il tasto S per andare a sinistra
if (stato_tastiera.IsKeyDown(Keys.S))
{ posizione.X--;}
/*Se la condizione B è premuto è vera, la Y del Vector2 posizione
aumenterà di 1 pixel ad ogni fotogramma verso il basso*/
if (stato_tastiera.IsKeyDown(Keys.B))
{ posizione.Y++; }
/*Se la condizione A è premuto è vera, la Y del Vector2 posizione
diminuirà di 1 pixel ad ogni fotogramma verso l’alto*/
if (stato_tastiera.IsKeyDown(Keys.A))
{ posizione.Y--; }
base.Update(gameTime);
}
XNA
um 33 di 196
protected override void Draw(GameTime gameTime)
{ graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Deferred,
SaveStateMode.SaveState);
spriteBatch.Draw(img, posizione, null, Color.White, 0, centro_immagine, 1,
SpriteEffects.None, 1);
spriteBatch.End();
base.Draw(gameTime);
}
}
}
Se lo sprite si muove lentamente, si può aumentare la sua velocità.
File GAME1.CS
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace PrimoGioco
{ public class Game1 : Microsoft.Xna.Framework.Game
{ GraphicsDeviceManager graphics;
XNA
um 34 di 196
SpriteBatch spriteBatch;
Texture2D img;
Vector2 posizione;
Vector2 centro_immagine;
KeyboardState stato_tastiera;
// velocità dello sprite
float velocità;
public Game1()
{ graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize()
{ spriteBatch = new SpriteBatch(GraphicsDevice);
// impostiamo la velocità
velocità = 3f;
posizione = new Vector2(graphics.PreferredBackBufferWidth / 2,
graphics.PreferredBackBufferHeight / 2);
base.Initialize();
}
protected override void LoadContent()
{ img = Content.Load<Texture2D>("Immagini/img");
centro_immagine = new Vector2(img.Width / 2, img.Height / 2);
}
protected override void UnloadContent()
{
}
protected override void Update(GameTime gameTime)
{ stato_tastiera = Keyboard.GetState();
if (stato_tastiera.IsKeyDown(Keys.D))
{ posizione.X +=velocità; }
if (stato_tastiera.IsKeyDown(Keys.S))
{ posizione.X -= velocità;;}
if (stato_tastiera.IsKeyDown(Keys.B))
{ posizione.Y +=velocità;}
if (stato_tastiera.IsKeyDown(Keys.A))
{ posizione.Y -= velocità; }
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{ graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Deferred,
SaveStateMode.SaveState);
spriteBatch.Draw(img, posizione, null, Color.White, 0, centro_immagine, 1,
SpriteEffects.None, 1);
spriteBatch.End();
base.Draw(gameTime);
}
}
}
CLASSE
È buona regola di programmazione OO (Object Oriented) dividere un progetto in tanti file
CS e in tante classi in modo da poter tenere sempre in ordine il progetto e lavorare su un
XNA
um 35 di 196
determinato aspetto del gioco alla volta, inoltre, nel caso di un team di programmatori
ognuno potrà dedicarsi ad un aspetto del gioco.
Su un gioco, di norma, esisteranno tante classi, una per il personaggio, una o più per gli
oggetti, e altre classi, ognuna per ogni aspetto del gioco.
Esempio, progettare una nuova classe chiamata MouseObject dedicata al controllo del
mouse.
In Esplora soluzioni fare clic con il tasto destro sul nome del progetto,
Aggiungi/Classe….
Si apre la finestra seguente.
File MIE_VARIABILI.CS
Ha il codice seguente già inserito.
È una nuova classe che non fa nulla ed è priva di metodi.
using System;
using System.Collections.Generic;
XNA
um 36 di 196
using System.Linq;
using System.Text;
namespace PrimoGioco {
class Mie_variabili {
}
}
Record
Per passare le variabili, come velocità e posizione, si può usare il passaggio parametri.
In un gioco le variabili in comune tra le diverse classi sono numerose, allora per evitare di
dover passare tanti parametri, è meglio progettare un record come contenitore di variabili
e la renderlo pubblico.
Modificare il codice nel modo seguente.
File MIE_VARIABILI.CS
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace PrimoGioco {
public struct Mie_variabili
{ // il record è accessibile a tutto il codice perché è public
public static Vector2 posizione;
public static float velocità;
}
}
Per dichiarare una variabile che si trova all’interno di un record, si deve scrivere.
Mie_variabili.posizione=7f;
Per richiamarla si deve scrivere.
Mie_variabili.posizione;
Chiudere il file MIE_VARIABILI.CS che contiene il record perché non si userà più.
In Esplora soluzioni fare clic con il tasto destro sul nome del progetto,
Aggiungi/Classe/Input.cs.
Ha il codice seguente già inserito, è una nuova classe che non fa nulla ed è priva di
metodi.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace PrimoGioco {
class Input {
}
}
Aprire il file INPUT.CS e all’interno della classe Input scrivere il metodo che controlla la
XNA
um 37 di 196
tastiera e fa muovere l’oggetto, si chiamerà movimenti.
File INPUT.CS
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace PrimoGioco
{ class Input
{ KeyboardState stato_tastiera;
// il costruttore della classe che,anche se vuoto, è sempre necessario
public Input(Game game)
{ }
// il metodo dev’essere public per essere richiamato
// e void perchè non ritorna nessun valore
public void movimenti()
{ stato_tastiera = Keyboard.GetState();
//controlliamo se è premuto il tasto D
if (stato_tastiera.IsKeyDown(Keys.D))
{ Mie_variabili.posizione.X += Mie_variabili.velocità; }
//controlliamo se è premuto il tasto S per andare a sinistra
if (stato_tastiera.IsKeyDown(Keys.S))
{ Mie_variabili.posizione.X -= Mie_variabili.velocità; }
//controlliamo se è premuto il tasto B
if (stato_tastiera.IsKeyDown(Keys.B))
{ Mie_variabili.posizione.Y += Mie_variabili.velocità; }
//controlliamo se è premuto il tasto A
if (stato_tastiera.IsKeyDown(Keys.A))
{ Mie_variabili.posizione.Y -= Mie_variabili.velocità; }
}
}
}
Aprire il file GAME1.CS e modificarlo.
File GAME1.CS
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace PrimoGioco
{ public class Game1 : Microsoft.Xna.Framework.Game
{ GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Texture2D img;
Vector2 posizione;
Vector2 centro_immagine;
// dichiariamo di usare la classe Input
XNA
um 38 di 196
Input prova_input;
public Game1()
{ graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize()
{ spriteBatch = new SpriteBatch(GraphicsDevice);
// impostiamo la velocità
Mie_variabili.velocità = 3f;
Mie_variabili.posizione = new Vector2(graphics.PreferredBackBufferWidth / 2,
graphics.PreferredBackBufferHeight / 2);
// inzializziamo un nuovo oggetto della classe Input
prova_input = new Input(this);
base.Initialize();
}
protected override void LoadContent()
{ img = Content.Load<Texture2D>("Immagini/img");
centro_immagine = new Vector2(img.Width / 2, img.Height / 2);
}
protected override void UnloadContent()
{ }
protected override void Update(GameTime gameTime)
{ // all’interno del metodo Update si deve solo richiamare il metodo
// dell’oggetto Input che fa muovere l’oggetto
//questo metodo sarà eseguito ad ogni fotogramma
prova_input.movimenti();
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{ graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Deferred,
SaveStateMode.SaveState);
// notare che anche nel Draw dev’essere specificato che la variabile
// posizione è collocata all’interno della struttura Mie_variabili
spriteBatch.Draw(img, Mie_variabili.posizione, null, Color.White, 0,
centro_immagine, 1, SpriteEffects.None, 1);
spriteBatch.End();
base.Draw(gameTime);
}
}
}
DrawableGameComponent
Sono classi “autonome”, in altre parole possiedono tutti i metodi Initialize, LoadContent,
Draw tipici della classe principale del gioco.
Quando si devono aggiungere nuovi elementi stampabili a schermo, non si useranno più
le solite classi ma i DrawableGameComponent.
In questo modo si ha un componente per ogni sprite modello o elemento qualsiasi che si
troverà in un file CS che lo gestisce e sarà diviso dal CS principale.
In Internet si può fare il download di componenti creati da utenti di XNA, questa
caratteristica crea una grande comunità di programmatori che lavorano in simbiosi,
ognuno su un aspetto di un gioco e poi lo mette a disposizione di tutti.
I componenti risolvono molti problemi evitando di dover specificare la stampa sul file
principale GAME1.CS e di riempire di codice questo file.
XNA
um 39 di 196
Un gioco che avrà centinaia di sprite ed elementi a video, scrivendo tutta l’applicazione in
un unico file, si avrà una gestione del codice difficile.
Aggiungere un nuovo componente al progetto, creare un nuovo file CS di nome
PERSONAGGIO1.CS.
Per specificare che la nuova classe sarà un DrawableGameComponent, basterà scriverlo
sulla sua intestazione dopo i due punti.
public class Personaggio1 : DrawableGameComponent
Ora basta inserire tutto ciò che riguarda lo sprite, spostandolo dalla classe GAME1.CS al
componente.
File PERSONAGGIO1.CS.
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace PrimoGioco
{ public class Personaggio1 : DrawableGameComponent
{ // specifichiamo una nuova variabile di tipo Game
Game game;
SpriteBatch spriteBatch;
Vector2 centro_immagine;
Texture2D img;
public Personaggio1(Game par_game): base(par_game)
{ // inizializiamo la variabile game
game = par_game;
// [1]
}
protected override void LoadContent()
{ spriteBatch = new SpriteBatch(GraphicsDevice);
img = game.Content.Load<Texture2D>("Immagini/img");
centro_immagine = new Vector2(img.Width / 2, img.Height / 2);
base.LoadContent();
}
public override void Draw(GameTime gameTime)
{ spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Deferred,
SaveStateMode.SaveState);
spriteBatch.Draw(img, Mie_variabili.posizione, null, Color.White, 0,
centro_immagine, 1, SpriteEffects.None, 1);
spriteBatch.End();
base.Draw(gameTime);
}
}
}
[1] Questa riga è utile per gestire i parametri spediti ad una classe e renderli utilizzabili
nella classe stessa, in questo caso si ha solo un parametro, di tipo Game.
Anche nella classe Input si ha lo stesso parametro (this) spedito al costruttore della classe
ma non serve a nulla.
In questo caso si deve utilizzare nella riga che caricherà lo sprite il codice seguente.
XNA
um 40 di 196
sprite1 = game.Content.Load<Texture2D>("Immagini/img");
Questo perché il componente dovrà sapere a quale classe principale Game faccia
riferimento.
Quando s’inviano dei parametri ad una classe, è dentro al costruttore che sarà ricevuta e,
per usarla, si deve renderla locale salvandola all’interno di una variabile.
In questo caso, si salva il parametro par_game all’interno di game che è una variabile di
tipo Game.
Come si vede dal costruttore, esso riceve una variabile di tipo Game chiamata par_game.
public Personaggio1(Game par_game): base(par_game)
{ // inizializiamo la variabile game
game = par_game;
}
Con la riga al suo interno si renderà la variabile dichiarata, game, uguale a quella spedita
al costruttore, par_game, potendola così utilizzare senza problemi.
In questo modo avremo dato il valore della variabile spedita come parametro, alla variabile
dichiarata all’inizio del codice.
Riprendere il file GAME1.CS per eliminare le righe inutili e per aggiungere l’inizializzazione
del componente.
Si possono togliere tutti i riferimenti allo SpriteBatch già inseriti nel componente.
Inserire prima la dichiarazione dell’oggetto della classe/componente Personaggio1 che si
chiamerà mio_personaggio.
Poi, nel metodo Inizialize inizializzare il componente e quindi aggiungerlo con Add.
Components.Add(mio_personaggio);
Grazie alle innumerevoli opzioni per gestire un componente, in seguito, si può renderlo
invisibile, spostare la sua profondità rispetto agli altri sprites e altre cose che, se non si
fosse usato un componente, sarebbero state più complesse da implementare.
File GAME1.CS
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace PrimoGioco
{ public class Game1 : Microsoft.Xna.Framework.Game
{ GraphicsDeviceManager graphics;
Input prova_input;
// dichiariamo di voler usare il componente
Personaggio1 mio_personaggio;
public Game1()
{ graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize()
{ prova_input = new Input(this);
Mie_variabili.velocità = 3f;
Mie_variabili.posizione = new Vector2(graphics.PreferredBackBufferWidth / 2,
graphics.PreferredBackBufferHeight / 2);
XNA
um 41 di 196
// inizializziamo il componente inviadogli il parametro this
mio_personaggio = new Personaggio1(this);
// aggiungiamo il componente
Components.Add(mio_personaggio);
base.Initialize();
}
protected override void Update(GameTime gameTime)
{ prova_input.movimenti();
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{ graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
base.Draw(gameTime);
}
}
}
Il metodo Draw non contiene più la stampa dello sprite perché è fatta nel componente.
Se ora si volesse aggiungere un altro sprite, non si deve fare altro che aggiungere un
nuovo componente per la sua gestione.
EVITARE CHE L’IMMAGINE ESCA DALLA FINESTRA
Si devono progettare dei limiti al movimento dell’immagine che si muove per evitare che lo
sprite esca fuori dai limiti dello schermo/finestra.
if (stato_tastiera.IsKeyDown(Keys.D) && Mie_variabili.posizione.X<800))
Mie_variabili.posizione.X += Mie_variabili.velocità;
Problema: non siamo sicuri che l’utente giocherà con la risoluzione di 800X600.
Come fatto per trovare il centro dello schermo, si usano le istruzioni seguenti.
graphics.PreferredBackBufferWidth
graphics.PreferredBackBufferHeight
Restituiscono la larghezza e l’altezza della finestra, dunque la risoluzione.
Dichiarare due variabili di tipo int che identificano la larghezza e l’altezza della finestra, nel
file seguente.
File MIE_VARIABILI.CS.
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace PrimoGioco
{ public struct Mie_variabili
{ // il record è accessibile a tutto il codice perché è public
public static Vector2 posizione;
public static float velocità;
public static int larghezza_fin; // larghezza della finestra
public static int altezza_fin; // altezza della finestra
XNA
um 42 di 196
}
}
File GAME1.CS
Si devono inizializzare le due nuove variabili.
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace PrimoGioco
{ public class Game1 : Microsoft.Xna.Framework.Game
{ GraphicsDeviceManager graphics;
Input prova_input;
// dichiariamo di voler usare il componente
Personaggio1 mio_personaggio;
public Game1()
{ graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize()
{ prova_input = new Input(this);
Mie_variabili.velocità = 3f;
// impostiamo la larghezza
Mie_variabili.larghezza_fin = graphics.PreferredBackBufferWidth;
// impostiamo l’altezza
Mie_variabili.altezza_fin = graphics.PreferredBackBufferHeight;
//Ora che abbiamo queste due nuove variabili
// potremmo modificare anche la riga che trova il centro dello schermo
Mie_variabili.posizione = new Vector2(Mie_variabili.larghezza_fin / 2,
Mie_variabili.altezza_fin / 2);
mio_personaggio = new Personaggio1(this);
Components.Add(mio_personaggio);
base.Initialize();
}
protected override void Update(GameTime gameTime)
{ prova_input.movimenti();
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{ graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
base.Draw(gameTime);
}
}
}
File INPUT.CS
Modificare tutti gli if aggiungendo la condizione che delimita lo spostamento all’interno
della finestra.
using System;
XNA
um 43 di 196
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace PrimoGioco
{ class Input
{ KeyboardState stato_tastiera;
// il costruttore della classe che,anche se vuoto, è sempre necessario
public Input(Game game)
{ }
// il metodo dev’essere public per essere richiamato
// e void perchè non ritorna nessun valore
public void movimenti()
{ stato_tastiera = Keyboard.GetState();
//controlliamo se è premuto il tasto D
if (stato_tastiera.IsKeyDown(Keys.D)&& Mie_variabili.posizione.X <
Mie_variabili.larghezza_fin)
{ Mie_variabili.posizione.X += Mie_variabili.velocità; }
//controlliamo se è premuto il tasto S per andare a sinistra
if (stato_tastiera.IsKeyDown(Keys.S)&& Mie_variabili.posizione.X >0)
{ Mie_variabili.posizione.X -= Mie_variabili.velocità; }
//controlliamo se è premuto il tasto B
if (stato_tastiera.IsKeyDown(Keys.B)&& Mie_variabili.posizione.Y <
Mie_variabili.altezza_fin)
{ Mie_variabili.posizione.Y += Mie_variabili.velocità; }
//controlliamo se è premuto il tasto A
if (stato_tastiera.IsKeyDown(Keys.A)&& Mie_variabili.posizione.Y > 0)
{ Mie_variabili.posizione.Y -= Mie_variabili.velocità; }
}
}
}
Lo sprite esce di metà fuori dallo schermo perché il controllo che verifica la posizione dello
sprite è effettuato sul suo punto d’origine che si è impostato nel centro dello sprite.
È ovvio, quindi che si fermerà solo quando il suo punto centrale si troverà al limite dello
schermo.
Per ovviare a questo problema si deve aggiungere e sottrarre la metà della dimensione
dello sprite ad ogni controllo negli if a seconda del lato della finestra che si controlla.
Anche in questo caso si utilizza il file MIE_VARIABILI.CS per avere a disposizione la
variabile Texture2D sprite1 così da controllare le sue dimensioni in qualunque parte del
gioco.
XNA
um 44 di 196
File MIE_VARIABILI.CS
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace PrimoGioco
{ public struct Mie_variabili
{ // il record è accessibile a tutto il codice perché è public
public static Vector2 posizione;
public static float velocità;
public static int larghezza_fin; // larghezza della finestra
public static int altezza_fin; // altezza della finestra
// aggiungiamo la Texture2D nel record
public static Texture2D img;
}
}
File PERSONAGGIO1.CS
Cancellare la dichiarazione della texture perché ora è dichiarata nel record, modificare
anche tutte le chiamate alla Texture2D sprite1 specificando che questa variabile ora si
trova nel record.
XNA
um 45 di 196
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace PrimoGioco
{ public class Personaggio1 : DrawableGameComponent
{ // specifichiamo una nuova variabile di tipo Game
Game game;
SpriteBatch spriteBatch;
Vector2 centro_immagine;
public Personaggio1(Game par_game): base(par_game)
{ // inizializiamo la variabile game
game = par_game;
}
protected override void LoadContent()
{ spriteBatch = new SpriteBatch(GraphicsDevice);
// la variabile è salvata nella struttura
Mie_variabili.img = game.Content.Load<Texture2D>("Immagini/img");
centro_immagine = new Vector2(Mie_variabili.img.Width / 2,
Mie_variabili.img.Height / 2);
base.LoadContent();
}
public override void Draw(GameTime gameTime)
{ spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Deferred,
SaveStateMode.SaveState);
spriteBatch.Draw(Mie_variabili.img, Mie_variabili.posizione, null, Color.White, 0,
centro_immagine, 1, SpriteEffects.None, 1);
spriteBatch.End();
base.Draw(gameTime);
}
}
}
File INPUT.CS
Si deve aggiungere la metà delle dimensioni dalla Texture2D sprite1.
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace PrimoGioco
{ class Input
{ KeyboardState stato_tastiera;
// il costruttore della classe che,anche se vuoto, è sempre necessario
public Input(Game game)
{ }
// il metodo dev’essere public per essere richiamato
// e void perchè non ritorna nessun valore
XNA
um 46 di 196
public void movimenti()
{ stato_tastiera = Keyboard.GetState();
//controlliamo se è premuto il tasto D
if (stato_tastiera.IsKeyDown(Keys.D) && Mie_variabili.posizione.X <
Mie_variabili.larghezza_fin - Mie_variabili.img.Width / 2)
{ Mie_variabili.posizione.X += Mie_variabili.velocità; }
//controlliamo se è premuto il tasto S per andare a sinistra
if (stato_tastiera.IsKeyDown(Keys.S) && Mie_variabili.posizione.X > 0 +
Mie_variabili.img.Width / 2)
{ Mie_variabili.posizione.X -= Mie_variabili.velocità; }
//controlliamo se è premuto il tasto B
if (stato_tastiera.IsKeyDown(Keys.B) && Mie_variabili.posizione.Y <
Mie_variabili.altezza_fin - Mie_variabili.img.Height / 2)
{ Mie_variabili.posizione.Y += Mie_variabili.velocità; }
//controlliamo se è premuto il tasto A
if (stato_tastiera.IsKeyDown(Keys.A) && Mie_variabili.posizione.Y > 0 +
Mie_variabili.img.Height / 2)
{ Mie_variabili.posizione.Y -= Mie_variabili.velocità; }
}
}
}
XNA
um 47 di 196
GRAVITÀ DELL’IMMAGINE
Per fare questo occorre creare una nuova classe GRAVITÀ.CS, si usa il passaggio
parametri per poter applicare la gravità a qualsiasi oggetto specificando a quale sprite si
vuole applicarla.
Dichiarare 2 nuove variabili di tipo float.
float forza_grav;
Più sarà alta e maggiore sarà l’attrazione verso il basso.
float incrementoY;
Serve per vedere la gravità aumentare esponenzialmente, infatti, la velocità della caduta
dell’oggetto non sarà costante ma aumenterà fino a che non toccherà terra.
public Vector2 applica_grav(Vector2 posizione, Texture2D sprite)
Questo metodo applica la gravità a qualsiasi oggetto, ha 2 parametri.
Vector2
Rappresenta la posizione dell’oggetto.
Texture2D
È l’oggetto stesso, serve solo per calcolare la metà della sua altezza per evitare che esso
vada oltre il limite della finestra.
Il valore di ritorno di tipo Vector2, dopo aver fatto tutti i calcoli, si ha la stessa variabile
posizione ma modificata dall’applicazione della gravità.
if (posizione.Y < Mie_variabili.altezza_fin - sprite.Height / 2) {
È il controllo che verifica che la posizione dell’oggetto non vada oltre il limite basso della
finestra, si applica la gravità solo se questa condizione è vera.
forza_grav = 0.05f;
incrementoY += forza_grav;
posizione.Y += incrementoY;
La prima riga inizializza la variabile forza_grav che sarà la forza con cui sarà attratto lo
sprite dal terreno.
La seconda riga aumenterà la variabile incrementoY di 0.05 ad ogni frame.
La terza riga aumenterò la posizione.Y dello sprite d’incrementoY, ad ogni frame.
Cosa succede a ogni frame e perché la velocità di discesa aumenterà ad ogni
fotogramma?
1. Al primo fotogramma, incrementoY è 0 e sarà aumentata di 0.05, dunque diventerà
0.05, la Y della posizione sarà aumentata di incrementoY che è 0.05 e dunque,
ipotizzando che l’oggetto parta per esempio dalla posizione Y 100, diventerà 100.05.
2. Al secondo fotogramma incrementoY non sarà più 0 ma 0.05 e dunque, aumentando di
0.05, diventerà 0.1; la Y della posizione sarà aumentata di nuovo d’incrementoY che
questa volta è 0.1, dunque aumenterà più velocemente del fotogramma precedente
che aumentava di 0.05, da 100.05 a cui era, passerà a 100.15.
3. Al terzo fotogramma, incrementoY sarà 0.1 e aumentando sempre di 0.05 diventerà
0.15, la Y della posizione sarà aumentata di nuovo d’incrementoY che adesso è di 0.15
XNA
um 48 di 196
e dunque aumenterà ancora più velocemente di prima e passerà da 100.15 a 100.30.
Al primo fotogramma la posizione aumenterà di 0.05, al secondo di 0.15, al terzo di 0.30 e
cosi via sempre ad aumentare, in questo modo la velocità di discesa risentirà di
un’accelerazione esponenziale.
}
else {
forza_grav = 0;
incrementoY = 0;
}
Altrimenti si azzera sia la forza di gravità sia l’incremento in modo che il calcolo della
gravità ricominci da capo.
return posizione;
}
File GRAVITÀ.CS
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Input;
namespace PrimoGioco
{ class Gravità
{ float forza_grav;
float incrementoY;
public Vector2 applica_grav(Vector2 posizione, Texture2D sprite)
{ if (posizione.Y < Mie_variabili.altezza_fin - sprite.Height / 2)
{ forza_grav = 0.05f;
incrementoY += forza_grav;
posizione.Y += incrementoY;
}
else
{ forza_grav = 0;
incrementoY = 0;
}
return posizione;
}
}
}
Nel file GAME1.CS bisogna chiamare il metodo della classe Gravità inviandogli i parametri
opportuni.
Per usare il metodo applica_grav della classe Gravità, si deve prima dichiarare ed
inizializzare un nuovo oggetto della classe Gravità.
Gravità gravità;
gravità = new Gravità();
Questa riga andrà posta nel metodo Update affinché sia eseguita ad ogni fotogramma.
Mie_variabili.posizione = gravità.applica_grav(Mie_variabili.posizione, Mie_variabili.img);
XNA
um 49 di 196
In questo modo si aggiorna la variabile Mie_variabili.posizione applicandogli la gravità.
File GAME1.CS
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace PrimoGioco
{ public class Game1 : Microsoft.Xna.Framework.Game
{ GraphicsDeviceManager graphics;
Input prova_input;
Personaggio1 mio_personaggio;
Gravità gravità;
public Game1()
{ graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize()
{ prova_input = new Input(this);
Mie_variabili.velocità = 3f;
// impostiamo la larghezza
Mie_variabili.larghezza_fin = graphics.PreferredBackBufferWidth;
// impostiamo l’altezza
Mie_variabili.altezza_fin = graphics.PreferredBackBufferHeight;
//Ora che abbiamo queste due nuove variabili
// potremmo modificare anche la riga che trova il centro dello schermo
Mie_variabili.posizione = new Vector2(Mie_variabili.larghezza_fin / 2,
Mie_variabili.altezza_fin / 2);
mio_personaggio = new Personaggio1(this);
Components.Add(mio_personaggio);
gravità = new Gravità();
base.Initialize();
}
protected override void Update(GameTime gameTime)
{ prova_input.movimenti();
Mie_variabili.posizione = gravità.applica_grav(Mie_variabili.posizione,
Mie_variabili.img);
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{ graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
base.Draw(gameTime);
}
}
}
XNA
um 50 di 196
RIMBALZI
RIMBALZI1
Nel progetto ci sono i seguenti file.
Nella cartella CONTENT, creare la cartella IMMAGINI, quindi aggiungere il file
MYTEXTURE.TGA.
File GAME1.CS
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
namespace Gioco1
{ public class Game1 : Microsoft.Xna.Framework.Game
{ GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
public Game1()
{ graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize()
{ base.Initialize();}
// questa è la texture
Texture2D myTexture;
// coordinate dello sprite
Vector2 spritePosition = Vector2.Zero;
protected override void LoadContent()
{ // crea un nuovo SpriteBatch, usando la texture
XNA
um 51 di 196
spriteBatch = new SpriteBatch(GraphicsDevice);
myTexture = Content.Load<Texture2D>("Immagini/mytexture");
}
protected override void UnloadContent()
{}
// memorizza i movimenti dello sprite
Vector2 spriteSpeed = new Vector2(50.0f, 50.0f);
protected override void Update(GameTime gameTime)
{ // muove lo sprite
UpdateSprite(gameTime);
base.Update(gameTime);
}
void UpdateSprite(GameTime gameTime)
{ spritePosition +=
spriteSpeed * (float)gameTime.ElapsedGameTime.TotalSeconds;
int MaxX =
graphics.GraphicsDevice.Viewport.Width - myTexture.Width;
int MinX = 0;
int MaxY =
graphics.GraphicsDevice.Viewport.Height - myTexture.Height;
int MinY = 0;
// controlla i rimbalzi
if (spritePosition.X > MaxX)
{ spriteSpeed.X *= -1;
spritePosition.X = MaxX;
}
else if (spritePosition.X < MinX)
{ spriteSpeed.X *= -1;
spritePosition.X = MinX;
}
if (spritePosition.Y > MaxY)
{ spriteSpeed.Y *= -1;
spritePosition.Y = MaxY;
}
else if (spritePosition.Y < MinY)
{ spriteSpeed.Y *= -1;
spritePosition.Y = MinY;
}
}
protected override void Draw(GameTime gameTime)
{ graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
// disegna lo sprite
spriteBatch.Begin(SpriteBlendMode.AlphaBlend);
spriteBatch.Draw(myTexture, spritePosition, Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
}
}
XNA
um 52 di 196
RIMBALZI2
Nel progetto ci sono i seguenti file.
Nella cartella CONTENT, creare la cartella IMMAGINI, quindi aggiungere il file
OBJECT.TGA.
File GAMEPLAYOBJECT.CS
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
XNA
um 53 di 196
using Microsoft.Xna.Framework;
namespace Rimbalzo
{ public enum ObjectStatus
{ Active,
Dying,
Dead
}
public class GameplayObject
{
#region Status Data
ObjectStatus status;
public ObjectStatus Status
{ get { return status; }
}
#endregion
#region Graphics Data
Texture2D texture;
public Texture2D Texture
{ get { return texture; }
set { texture = value; }
}
Rectangle rectangle;
public Rectangle Rectangle
{ get { return rectangle; }
}
public Vector2 Origin
{ get {return new Vector2(texture.Width / 2.0f, texture.Height / 2.0f); }
}
float opacity = 1.0f;
byte Alpha
{ get { return (byte)(opacity * 255); }
}
Color color = Color.White;
protected Color Color
{ get { return new Color(color.R, color.G, color.B, Alpha); }
set { color = value; }
}
#endregion
#region Physics Data
Vector2 position = Vector2.Zero;
public Vector2 Position
{ get { return position; }
set { position = value; }
}
Vector2 velocity = Vector2.Zero;
public Vector2 Velocity
{ get { return velocity; }
set { velocity = value; }
}
Vector2 acceleration = Vector2.Zero;
public Vector2 Acceleration
{ get { return acceleration; }
set { acceleration = value; }
XNA
um 54 di 196
}
float rotation = 0f;
public float Rotation
{ get { return rotation; }
set { rotation = value; }
}
float speed = 0.0f;
public float Speed
{ get { return speed; }
set { speed = value; }
}
float scale = 1.0f;
public float Scale
{ get { return scale; }
set { scale = value; }
}
#endregion
#region Die Data
TimeSpan dieTime = TimeSpan.Zero;
public TimeSpan DieTime
{ get { return dieTime; }
set { dieTime = value; }
}
float diePercent = 0.0f;
#endregion
#region Initialization Methods
public virtual void Initialize()
{ if (!(status == ObjectStatus.Active)) status = ObjectStatus.Active; }
#endregion
#region Update and Draw Methods
public virtual void Update(GameTime gameTime)
{ if (status == ObjectStatus.Active)
{ velocity += Vector2.Multiply(acceleration,
(float)gameTime.ElapsedGameTime.TotalSeconds);
position += Vector2.Multiply(velocity,
(float)gameTime.ElapsedGameTime.TotalSeconds);
if (texture != null)
rectangle = new Rectangle((int)position.X, (int)position.Y, texture.Width,
texture.Height);
}
else if (status == ObjectStatus.Dying)
{ Dying(gameTime); }
else if (status == ObjectStatus.Dead)
{ Dead(gameTime); }
}
public virtual void Dying(GameTime gameTime)
{ if (diePercent >= 1) status = ObjectStatus.Dead;
else
{ float dieDelta = (float)(gameTime.ElapsedGameTime.TotalMilliseconds /
dieTime.TotalMilliseconds);
diePercent += dieDelta;
}
}
public virtual void Dead(GameTime gameTime) { }
XNA
um 55 di 196
public virtual void Collision(GameplayObject target) { }
public void Die()
{ if (status == ObjectStatus.Active)
{ if (dieTime != TimeSpan.Zero) status = ObjectStatus.Dying;
else status = ObjectStatus.Dead;
}
}
public virtual void Draw(GameTime gameTime, SpriteBatch spriteBatch)
{ if ((texture != null) && (spriteBatch != null))
spriteBatch.Draw(texture, position, null, Color, rotation, Origin, scale,
SpriteEffects.None, 0.0f);
}
#endregion
}
}
File GAME1.CS
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
namespace Rimbalzo
{ public class Game1 : Microsoft.Xna.Framework.Game
{ GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Vector2 friction;
GameplayObject box;
KeyboardState keyboardState;
public Game1()
{ Window.Title = "Rimbalzi";
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize()
{ box = new GameplayObject();
box.Position = new Vector2(GraphicsDevice.Viewport.Width / 2,
GraphicsDevice.Viewport.Height / 2);
box.Acceleration = new Vector2(0, 60);
friction = new Vector2(5, 0);
base.Initialize();
}
protected override void LoadContent()
{ spriteBatch = new SpriteBatch(GraphicsDevice);
box.Texture = Content.Load<Texture2D>("Immagini/object");
box.Scale = 1.0f;
}
XNA
um 56 di 196
protected override void UnloadContent()
{box.Texture = null;}
protected override void Update(GameTime gameTime)
{ keyboardState = Keyboard.GetState();
if (keyboardState.IsKeyDown(Keys.Left))
box.Velocity = Vector2.Add(box.Velocity, new Vector2(-30, 0));
if (keyboardState.IsKeyDown(Keys.Right))
box.Velocity = Vector2.Add(box.Velocity, new Vector2(30, 0));
PerformPhysics(box);
box.Update(gameTime);
base.Update(gameTime);
}
private void PerformPhysics(GameplayObject smiley)
{ if (smiley.Velocity.X < 0)
{ smiley.Velocity = Vector2.Add(smiley.Velocity, friction);
if (smiley.Velocity.X > 0)
smiley.Velocity = new Vector2(0, smiley.Velocity.Y);
}
else if (smiley.Velocity.X > 0)
{ smiley.Velocity = Vector2.Subtract(smiley.Velocity, friction);
if (smiley.Velocity.X < 0)
smiley.Velocity = new Vector2(0, smiley.Velocity.Y);
}
if ((smiley.Position.Y > GraphicsDevice.Viewport.Height - (smiley.Texture.Height smiley.Origin.Y) * smiley.Scale) && Math.Abs(smiley.Velocity.Y) > 1)
{ smiley.Position = new Vector2(smiley.Position.X,
GraphicsDevice.Viewport.Height - (smiley.Texture.Height - smiley.Origin.Y) *
smiley.Scale);
smiley.Velocity = new Vector2(smiley.Velocity.X, -smiley.Velocity.Y / 2);
}
if (smiley.Position.X < 0 + (smiley.Texture.Width - smiley.Origin.X) * smiley.Scale)
{ smiley.Position = new Vector2((float)Math.Ceiling((smiley.Texture.Width smiley.Origin.X) * smiley.Scale),
smiley.Position.Y);
smiley.Velocity = new Vector2(0, smiley.Velocity.Y);
}
else if (smiley.Position.X > GraphicsDevice.Viewport.Width - (smiley.Texture.Width
- smiley.Origin.X) * smiley.Scale)
{ smiley.Position = new Vector2((float)Math.Floor(GraphicsDevice.Viewport.Width
- (smiley.Texture.Width - smiley.Origin.X) * smiley.Scale), smiley.Position.Y);
smiley.Velocity = new Vector2(0, smiley.Velocity.Y);
}
}
protected override void Draw(GameTime gameTime)
{ GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
box.Draw(gameTime, spriteBatch);
spriteBatch.End();
base.Draw(gameTime);
}
}
}
XNA
um 57 di 196
XNA
um 58 di 196
RANDOM
INTRODUZIONE
Nel progetto ci sono i seguenti file.
Nella cartella CONTENT, creare la cartella IMMAGINI, quindi aggiungere i file
ENEMY.PNG e PLAYER.PNG.
File GAME1.CS
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
namespace Sprite
{ public class Game1 : Microsoft.Xna.Framework.Game
{ GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Texture2D texture, s1, s2;
Vector2 position, velocity;
TimeSpan delayTimer;
float maxTime = 2.0f;
Random rand;
public Game1()
{ graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize()
{ rand = new Random();
XNA
um 59 di 196
position = new Vector2(0, (float)(rand.NextDouble() *
GraphicsDevice.Viewport.Height));
velocity = new Vector2(10, 0);
delayTimer = TimeSpan.FromSeconds(maxTime);
base.Initialize();
}
protected override void LoadContent()
{ spriteBatch = new SpriteBatch(GraphicsDevice);
s1 = Content.Load<Texture2D>("Immagini/player");
s2 = Content.Load<Texture2D>("Immagini/enemy");
GenerateRandomSprite();
}
protected override void UnloadContent()
{ texture = null;
s1 = null;
s2 = null;
}
protected override void Update(GameTime gameTime)
{ position += velocity;
delayTimer = delayTimer.Subtract(gameTime.ElapsedGameTime);
if (delayTimer.TotalSeconds <= 0)
{ GenerateRandomSprite();
delayTimer = TimeSpan.FromSeconds(maxTime);
}
base.Update(gameTime);
}
private void GenerateRandomSprite()
{ int r = rand.Next(0, 100);
if (r % 2 == 0)
{ texture = s1;}
else
{texture = s2; }
position = new Vector2(0, (float)(rand.NextDouble() *
GraphicsDevice.Viewport.Height));
if (position.Y > GraphicsDevice.Viewport.Height - texture.Height)
position.Y = GraphicsDevice.Viewport.Height - texture.Height;
}
protected override void Draw(GameTime gameTime)
{ GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
spriteBatch.Draw(texture, position, Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
}
}
XNA
um 60 di 196
XNA
um 61 di 196
INPUT
INTRODUZIONE
L’interazione dell’utente è una componente fondamentale di qualsiasi videogame.
Windows Phone è dotato di pannello touch screen ed è quindi questo il principale
meccanismo d’input usato dall’utente.
Inoltre, alcuni dispositivi potrebbero essere dotati anche di tastiera che permetterebbero
per esempio di usare dei tasti come le frecce direzionali per interagire con il gioco.
La classe Keyboard permette di leggere i tasti premuti, mediante il suo unico metodo
GetState.
Per verificare che il tasto premuto sia uno delle frecce e aggiornare di conseguenza le
coordinate, scrivere qualcosa del tipo seguente.
Nella cartella CONTENT, creare la cartella IMMAGINI, quindi aggiungere il file
PLAYER.PNG.
File GAME1.CS
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
namespace Input
{ public class Game1 : Microsoft.Xna.Framework.Game
{ GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Texture2D player;
Vector2 position;
InputSystem input;
float speed;
XNA
um 62 di 196
public Game1()
{ graphics = new GraphicsDeviceManager(this);
graphics.PreferredBackBufferWidth = 800;
graphics.PreferredBackBufferHeight = 600;
Content.RootDirectory = "Content";
}
protected override void Initialize()
{ speed = 10;
input = new InputSystem();
base.Initialize();
}
protected override void LoadContent()
{ spriteBatch = new SpriteBatch(GraphicsDevice);
player = Content.Load<Texture2D>("Immagini/player");
position = CenterTexture(player);
}
private Vector2 CenterTexture(Texture2D player)
{ return new Vector2((GraphicsDevice.Viewport.Width / 2) - (player.Width / 2),
(GraphicsDevice.Viewport.Height / 2) - (player.Height / 2));
}
protected override void UnloadContent()
{ player = null; }
protected override void Update(GameTime gameTime)
{ input.Update(gameTime);
if (input.QuitGame)
this.Exit();
if (input.MoveUp) position.Y -= speed;
if (input.MoveDown) position.Y += speed;
if (input.MoveLeft) position.X -= speed;
if (input.MoveRight) position.X += speed;
if (input.CenterSprite) position = CenterTexture(player);
position.X += input.Move.X * speed;
position.Y -= input.Move.Y * speed;
CheckBounds();
base.Update(gameTime);
}
private void CheckBounds()
{
if (position.X < 0) position.X = 0;
else if (position.X > GraphicsDevice.Viewport.Width - player.Width)
position.X = GraphicsDevice.Viewport.Width - player.Width;
if (position.Y < 0) position.Y = 0;
else if (position.Y > GraphicsDevice.Viewport.Height - player.Height)
position.Y = GraphicsDevice.Viewport.Height - player.Height;
}
protected override void Draw(GameTime gameTime)
{ GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
spriteBatch.Draw(player, position, Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
}
}
XNA
um 63 di 196
File INPUTSYSTEM.CS
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework;
namespace Input {
public class InputSystem {
#region Fields and Properties
KeyboardState currentKeyboardState, previousKeyboardState;
GamePadState currentGamePadState, previousGamePadState;
public bool QuitGame
{ get { return IsNewKeyPress(Keys.Escape) || IsNewButtonPress(Buttons.Back); } }
public bool MoveUp
{ get { return IsHeldKey(Keys.Up); } }
public bool MoveDown
{ get { return IsHeldKey(Keys.Down); } }
public bool MoveLeft
{ get { return IsHeldKey(Keys.Left); } }
public bool MoveRight
{ get { return IsHeldKey(Keys.Right); } }
public bool CenterSprite
{ get { return IsNewKeyPress(Keys.Enter)
|| IsNewButtonPress(Buttons.A); }
}
public Vector2 Move
{ get { return currentGamePadState.ThumbSticks.Left; } }
#endregion
#region Methods
private bool IsNewKeyPress(Keys key)
{ return currentKeyboardState.IsKeyDown(key) &&
previousKeyboardState.IsKeyUp(key); }
private bool IsHeldKey(Keys key)
{ return currentKeyboardState.IsKeyDown(key); }
private bool IsNewButtonPress(Buttons button)
{ return currentGamePadState.IsButtonDown(button) &&
previousGamePadState.IsButtonUp(button);
}
private bool IsHeldButton(Buttons button)
{ return currentGamePadState.IsButtonDown(button);
}
public void Update(GameTime gameTime)
{ previousGamePadState = currentGamePadState;
previousKeyboardState = currentKeyboardState;
currentKeyboardState = Keyboard.GetState();
currentGamePadState = GamePad.GetState(PlayerIndex.One);
}
#endregion
}
}
XNA
um 64 di 196
Esempio, gestione tastiera
File GAME1.CS
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
using System.Text;
namespace Audio {
public class Game1 : Microsoft.Xna.Framework.Game {
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
SpriteFont spriteFont;
KeyboardState keyboardState, previousKeyboardState;
// effetto sonoro growl
SoundEffect growl;
// tema di Forest
XNA
um 65 di 196
Song forestTheme;
// info visualizza le informazioni
string info;
public Game1() {
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize() {
// ripete la musica
MediaPlayer.IsRepeating = true;
base.Initialize();
}
protected override void LoadContent() {
spriteBatch = new SpriteBatch(GraphicsDevice);
spriteFont = Content.Load<SpriteFont>("Font/font");
growl = Content.Load<SoundEffect>("Audio/Dark Boss Growl");
forestTheme = Content.Load<Song>("Audio/ForestTheme");
MediaPlayer.Play(forestTheme);
}
protected override void UnloadContent() {
spriteFont = null;
growl = null;
forestTheme = null;
}
protected override void Update(GameTime gameTime) {
previousKeyboardState = keyboardState;
keyboardState = Keyboard.GetState();
if (keyboardState.IsKeyDown(Keys.P) &&
previousKeyboardState.IsKeyUp(Keys.P) &&
MediaPlayer.State == MediaState.Playing)
MediaPlayer.Pause();
else if (keyboardState.IsKeyDown(Keys.P) &&
previousKeyboardState.IsKeyUp(Keys.P) &&
MediaPlayer.State == MediaState.Paused)
MediaPlayer.Resume();
if (keyboardState.IsKeyDown(Keys.F) &&
previousKeyboardState.IsKeyUp(Keys.F))
growl.Play();
StringBuilder sb = new StringBuilder();
sb.AppendLine("Musica: Tema di Forest, " +
MediaPlayer.PlayPosition.Minutes.ToString()
+ ":" + MediaPlayer.PlayPosition.Seconds.ToString() + "/" +
forestTheme.Duration.Minutes.ToString()
+ ":" + forestTheme.Duration.Seconds.ToString());
sb.AppendLine("Premere P per fermare e per far ripartire la musica");
if (MediaPlayer.State == MediaState.Paused)
sb.AppendLine("Stato della Musica: Pausa");
else
sb.AppendLine("Stato della Musica: Suona");
if (MediaPlayer.IsRepeating)
sb.AppendLine("Loop: Abilitato");
else
sb.AppendLine("Loop: Disabilitato");
sb.AppendLine("\nEffetti Sonori: Dark Boss Growl");
XNA
um 66 di 196
sb.AppendLine("Premere F per ascoltare l’effetto sonoro");
info = sb.ToString();
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime) {
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
spriteBatch.DrawString(spriteFont, info, new Vector2(200, 100), Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
}
}
XNA
um 67 di 196
MOUSE
Nel progetto Content, creare la cartella IMMAGINI, quindi aggiungere i file ENEMY.PNG
(sprite che si vede facendo clic con il pulsante destro), MOUSE.PNG, PLAYER.PNG;
creare la cartella FONT, quindi creare il file FONT.SPRITEFONT.
File MOUSEOBJECT.CS
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;
namespace MouseTest {
public class MouseObject {
// memorizza gli stati del mouse
MouseState previousMouseState, currentMouseState;
// memorizza la posizione corrente del puntatore del mouse
Vector2 position;
public Vector2 Position {
get { return position; }
}
// memorizza la texture
Texture2D texture;
// posizione dell’oggetto più la dimensione
Rectangle rectangle;
public Rectangle Rectangle {
XNA
um 68 di 196
get { return rectangle; }
}
// è premuto il pulsante sinitro?
public bool LeftClick {
get { return currentMouseState.LeftButton == ButtonState.Pressed; }
}
// è premuto di nuovo il pulsante sinistro?
public bool NewLeftClick {
get { return LeftClick && previousMouseState.LeftButton == ButtonState.Released; }
}
// il pulsante sinistro è premuto ma è rilasciato
public bool LeftRelease {
get { return !LeftClick && previousMouseState.LeftButton == ButtonState.Pressed; }
}
// è premuto il pulsante destro?
public bool RightClick {
get { return currentMouseState.RightButton == ButtonState.Pressed; }
}
// è premuto di nuovo il pulsante destro?
public bool NewRightClick {
get {return RightClick && previousMouseState.RightButton==ButtonState.Released;}
}
// il pulsante destro è premuto ma è rilasciato
public bool RightRelease {
get {return !RightClick && previousMouseState.RightButton==ButtonState.Pressed;}
}
// crea un nuovo oggettoMouse
public MouseObject() { }
// il parametro passato è una texture
public MouseObject(Texture2D texture) {
this.texture = texture;
}
/* crea un nuovo oggettoMouse, sono passati content manager (si trova in game1.cs
o ScreenManager</param> )e assetname per caricare la texture che si usa */
public MouseObject(ContentManager content, string assetName) {
texture = content.Load<Texture2D>(assetName);
}
// Update per lo stato del mouse e della posizione
public void Update() {
previousMouseState = currentMouseState;
currentMouseState = Mouse.GetState();
position = new Vector2(currentMouseState.X, currentMouseState.Y);
rectangle=new Rectangle((int)position.X,(int)position.Y,texture.Width,texture.Height);
}
// disegna il mouse sullo schermo, se esiste la texture
public void Draw(SpriteBatch spriteBatch) {
if (texture != null) {
spriteBatch.Begin();
spriteBatch.Draw(texture, position, Color.White);
spriteBatch.End();
}
}
}
}
XNA
um 69 di 196
File GAMEPLAYOBJECTIMPLEMENTATION.CS
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework;
namespace MouseTest {
public class GameplayObjectImplementation:ClickableGameplayObject {
Texture2D rightClickedTexture;
public Texture2D RightClickedTexture {
get { return rightClickedTexture; }
set { rightClickedTexture = value; }
}
Texture2D normalTexture;
public Texture2D NormalTexture {
get { return normalTexture; }
set { normalTexture = value; }
}
public GameplayObjectImplementation() { }
public GameplayObjectImplementation(ContentManager content, string clickedAsset,
string releasedAsset) {
rightClickedTexture = content.Load<Texture2D>(clickedAsset);
Texture = NormalTexture = content.Load<Texture2D>(releasedAsset);
}
public override void OnRightClick(MouseObject mouse) {
if (RightClickedState == ClickedState.Released) {
Texture = rightClickedTexture;
Texture2D temp = rightClickedTexture;
rightClickedTexture = normalTexture;
normalTexture = temp;
}
base.OnRightClick(mouse);
}
public override void LeftClickUpdate(GameTime gameTime) {
Position = ActiveMouse.Position;
}
}
}
File GAMEPLAYOBJECT.CS
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
namespace MouseTest
{ public enum ObjectStatus
{ // stati dell’oggetto
Active,
Dying,
Dead
XNA
um 70 di 196
}
public class GameplayObject
{
#region Status Data
ObjectStatus status;
public ObjectStatus Status
{ get { return status; }
}
#endregion
#region Graphics Data
// la texture corrente del gioco
Texture2D texture;
public Texture2D Texture
{ get { return texture; }
set { texture = value; }
}
// rettangolo dell’oggetto usato per le collisioni
Rectangle rectangle;
public Rectangle Rectangle
{ get { return rectangle; }
}
/// Origin property used for the center of the game object
public Vector2 Origin
{ get { return new Vector2(texture.Width / 2.0f, texture.Height / 2.0f); }
}
/// Opacity and alpha of the object
float opacity = 1.0f;
byte Alpha
{ get { return (byte)(opacity * 255); }
}
// colore di default White
Color color = Color.White;
protected Color Color
{ get { return new Color(color.R, color.G, color.B, Alpha); }
set { color = value; }
}
#endregion
#region Physics Data
// localizzazione dell’oggettodi gioco nel mondo del gioco
Vector2 position = Vector2.Zero;
public Vector2 Position
{ get { return position; }
set { position = value; }
}
// velocità dell’oggetto
Vector2 velocity = Vector2.Zero;
public Vector2 Velocity
{ get { return velocity; }
set { velocity = value; }
}
// accelerazione dell’oggetto
Vector2 acceleration = Vector2.Zero;
public Vector2 Acceleration
{ get { return acceleration; }
XNA
um 71 di 196
set { acceleration = value; }
}
// dove si visualizza l’oggetto
float rotation = 0f;
public float Rotation
{ get { return rotation; }
set { rotation = value; }
}
// velocità dell’oggetto
float speed = 0.0f;
public float Speed
{ get { return speed; }
set { speed = value; }
}
#endregion
#region Die Data
TimeSpan dieTime = TimeSpan.Zero;
public TimeSpan DieTime
{ get { return dieTime; }
set { dieTime = value; }
}
float diePercent = 0.0f;
#endregion
#region Initialization Methods
public virtual void Initialize()
{ if (!(status == ObjectStatus.Active)) status = ObjectStatus.Active;
}
#endregion
#region Update and Draw Methods
public virtual void Update(GameTime gameTime)
{ if (status == ObjectStatus.Active)
{ velocity += Vector2.Multiply(acceleration,
(float)gameTime.ElapsedGameTime.TotalSeconds);
position += Vector2.Multiply(velocity,
(float)gameTime.ElapsedGameTime.TotalSeconds);
if (texture != null)
rectangle = new Rectangle((int)position.X, (int)position.Y, texture.Width,
texture.Height);
}
else if (status == ObjectStatus.Dying)
{ Dying(gameTime); }
else if (status == ObjectStatus.Dead)
{ Dead(gameTime); }
}
public virtual void Dying(GameTime gameTime)
{ if (diePercent >= 1) status = ObjectStatus.Dead;
else
{ float dieDelta = (float)(gameTime.ElapsedGameTime.TotalMilliseconds /
dieTime.TotalMilliseconds);
diePercent += dieDelta;
}
}
public virtual void Dead(GameTime gameTime) { }
public virtual void Collision(GameplayObject target) { }
XNA
um 72 di 196
public void Die()
{ if (status == ObjectStatus.Active)
{ if (dieTime != TimeSpan.Zero) status = ObjectStatus.Dying;
else status = ObjectStatus.Dead;
}
}
public virtual void Draw(GameTime gameTime, SpriteBatch spriteBatch)
{ if ((texture != null) && (spriteBatch != null))
spriteBatch.Draw(texture, position, null, Color, rotation, Origin, 1.0f,
SpriteEffects.None, 0.0f);
}
#endregion
}
}
File CLICKABLEGAMEPLAYOBJECT.CS
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
namespace MouseTest
{ public enum ClickedState
{ Clicked,
Released
}
public class ClickableGameplayObject:GameplayObject
{ ClickedState leftClickedState;
public ClickedState LeftClickedState
{ get { return leftClickedState; }
}
ClickedState rightClickedState;
public ClickedState RightClickedState
{ get { return rightClickedState; }
}
MouseObject activeMouse;
public MouseObject ActiveMouse
{ get { return activeMouse; }
}
public ClickableGameplayObject()
{ leftClickedState = ClickedState.Released;
rightClickedState = ClickedState.Released;
}
public virtual void OnLeftClick(MouseObject mouse)
{ if (leftClickedState == ClickedState.Released)
leftClickedState = ClickedState.Clicked;
activeMouse = mouse;
}
public virtual void OnLeftRelease()
{ if (leftClickedState == ClickedState.Clicked)
leftClickedState = ClickedState.Released;
activeMouse = null;
}
public virtual void OnRightClick(MouseObject mouse)
XNA
um 73 di 196
{ if (rightClickedState == ClickedState.Released)
rightClickedState = ClickedState.Clicked;
activeMouse = mouse;
}
public virtual void OnRightRelease()
{ if (rightClickedState == ClickedState.Clicked)
rightClickedState = ClickedState.Released;
activeMouse = null;
}
public override void Update(GameTime gameTime)
{ base.Update(gameTime);
if (leftClickedState == ClickedState.Clicked)
LeftClickUpdate(gameTime);
if (rightClickedState == ClickedState.Clicked)
RightClickUpdate(gameTime);
}
public virtual void LeftClickUpdate(GameTime gameTime)
{ }
public virtual void RightClickUpdate(GameTime gameTime)
{ }
}
}
File GAME1.CS
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
namespace MouseTest
{ public class Game1 : Microsoft.Xna.Framework.Game
{ GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
SpriteFont spriteFont;
GameplayObjectImplementation player;
MouseObject mouse;
public Game1()
{ Window.Title = "Mouse";
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize()
{ player = new GameplayObjectImplementation();
player.Velocity = new Vector2(50, 50);
player.Rotation = MathHelper.ToRadians(-90);
base.Initialize();
}
XNA
um 74 di 196
protected override void LoadContent()
{ spriteBatch = new SpriteBatch(GraphicsDevice);
spriteFont = Content.Load<SpriteFont>("Font/font");
player.Texture = player.NormalTexture =
Content.Load<Texture2D>("Immagini/player");
player.RightClickedTexture = Content.Load<Texture2D>("Immagini/enemy");
mouse = new MouseObject(Content, "Immagini/mouse");
}
protected override void UnloadContent()
{ player.Texture = null; }
protected override void Update(GameTime gameTime)
{ if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
mouse.Update();
if (mouse.NewLeftClick)
PerformLeftMouseClick();
else if (mouse.LeftRelease)
PreformLeftRelease();
if (mouse.NewRightClick)
PerformRightMouseClick();
else if (mouse.RightRelease)
PreformRightRelease();
player.Update(gameTime);
base.Update(gameTime);
}
private void PerformLeftMouseClick()
{ if (player.Rectangle.Intersects(mouse.Rectangle))
player.OnLeftClick(mouse);
}
private void PreformLeftRelease()
{ player.OnLeftRelease(); }
private void PerformRightMouseClick()
{ player.OnRightClick(mouse); }
private void PreformRightRelease()
{ player.OnRightRelease(); }
protected override void Draw(GameTime gameTime)
{ GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
player.Draw(gameTime, spriteBatch);
spriteBatch.DrawString(spriteFont, "Posizione mouse: " +
mouse.Position.ToString()
+ "; Posizione sprite: " + player.Position.ToString(), Vector2.Zero, Color.White);
spriteBatch.End();
mouse.Draw(spriteBatch);
base.Draw(gameTime);
}
}
}
XNA
um 75 di 196
XNA
um 76 di 196
FILE
INTRODUZIONE
Nel progetto ci sono i seguenti file.
Nella cartella CONTENT, creare la cartella FONT e il file FONT.SPRITEFONT.
File SHIP.CS
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Font {
[Serializable]
public class Ship
{ string name;
public string Name
{ get { return name; }
set { name = value; }
}
int hull;
public int Hull
{ get { return hull; }
set { hull = value; }
}
int hullDamage;
int shield;
public int Shield
{ get { return shield; }
set { shield = value; }
}
int shieldDamage;
int armor;
public int Armor
{ get { return armor; }
}
public Ship()
XNA
um 77 di 196
{ hull = 100;
hullDamage = 0;
shield = 100;
shieldDamage = 0;
armor = 5;
}
public void Hit(int damage)
{ if (shield <= 0) shieldDamage = 0;
else
shieldDamage = damage;
shield -= shieldDamage;
if (hull < 0) hullDamage = 0;
else
hullDamage = (int)((armor + shield) * (0.02 * damage));
hull -= hullDamage;
}
public override string ToString()
{ StringBuilder sb = new StringBuilder();
sb.AppendLine("Nome: " + name);
sb.Append("Colpito: " + hull);
if (hullDamage > 0)
sb.Append(" (-" + hullDamage + ")\n");
else
sb.Append("\n");
sb.Append("Difesa: " + shield);
if (shieldDamage > 0)
sb.Append(" (-" + shieldDamage + ")\n");
else
sb.Append("\n");
sb.AppendLine("Blindati: " + armor);
return sb.ToString();
}
}
}
File GAME1.CS
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
using System.Text;
using System.IO;
using System.Xml.Serialization;
namespace Font {
public class Game1 : Microsoft.Xna.Framework.Game
{ GraphicsDeviceManager graphics;
XNA
um 78 di 196
SpriteBatch spriteBatch;
SpriteFont spriteFont;
Ship player;
KeyboardState currentKeyboardState, previousKeyboardState;
IAsyncResult result;
// oggetto per la memorizzazione dei dati
StorageDevice device;
string info;
public Game1()
{ Window.Title ="Gestione File";
graphics = new GraphicsDeviceManager(this);
graphics.PreferredBackBufferWidth = 800;
graphics.PreferredBackBufferHeight = 600;
Content.RootDirectory = "Content";
}
protected override void Initialize()
{ player = new Ship();
player.Name = "Orione";
base.Initialize();
}
protected override void LoadContent()
{ spriteBatch = new SpriteBatch(GraphicsDevice);
spriteFont = Content.Load<SpriteFont>("Font/font");
}
protected override void UnloadContent()
{ }
protected override void Update(GameTime gameTime)
{ previousKeyboardState = currentKeyboardState;
currentKeyboardState = Keyboard.GetState();
if (currentKeyboardState.IsKeyDown(Keys.H) &&
previousKeyboardState.IsKeyUp(Keys.H))
player.Hit(5);
if (currentKeyboardState.IsKeyDown(Keys.S) &&
previousKeyboardState.IsKeyUp(Keys.S))
{ result = Guide.BeginShowStorageDeviceSelector(PlayerIndex.One, null, null);
SaveGame();
}
if (currentKeyboardState.IsKeyDown(Keys.L) &&
previousKeyboardState.IsKeyUp(Keys.L))
{
result = Guide.BeginShowStorageDeviceSelector(PlayerIndex.One, null, null);
LoadGame();
}
StringBuilder sb = new StringBuilder();
if (player != null)
sb.AppendLine(player.ToString());
sb.AppendLine("Premi H per colpire l’oggetto");
sb.AppendLine("Premi S(ave) per salvare il gioco");
sb.AppendLine("Premi L(oad) per caricare il gioco");
info = sb.ToString();
base.Update(gameTime);
}
private void SaveGame()
{ if (result.IsCompleted)
XNA
um 79 di 196
{ device = Guide.EndShowStorageDeviceSelector(result);
if (device.IsConnected)
{ using (StorageContainer container = device.OpenContainer("GameSave"))
{ string fileName = Path.Combine(container.Path, "savedgame.zzz");
FileStream stream = File.Open(fileName, FileMode.OpenOrCreate);
XmlSerializer serializer = new XmlSerializer(typeof(Ship));
serializer.Serialize(stream, player);
stream.Close();
container.Dispose();
}
}
}
}
private void LoadGame()
{ if (result.IsCompleted)
{ device = Guide.EndShowStorageDeviceSelector(result);
if (device.IsConnected)
{ using (StorageContainer container = device.OpenContainer("GameSave"))
{ string fileName = Path.Combine(container.Path, "savedgame.zzz");
if (!File.Exists(fileName))
return;
FileStream stream = File.Open(fileName, FileMode.OpenOrCreate,
FileAccess.Read);
XmlSerializer serializer = new XmlSerializer(typeof(Ship));
player = (Ship)serializer.Deserialize(stream);
stream.Close();
container.Dispose();
}
}
}
}
protected override void Draw(GameTime gameTime)
{ GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
spriteBatch.DrawString(spriteFont, info, new Vector2(200, 100), Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
}
}
Il file è salvato nella cartella seguente.
XNA
um 80 di 196
File SAVEDGAME.ZZZ
<?xml version="1.0"?>
<Ship xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Name>Orione</Name>
<Hull>64</Hull>
<Shield>80</Shield>
</Ship>
XNA
um 81 di 196
MENU
INTRODUZIONE
Nel progetto ci sono i seguenti file.
Nella cartella CONTENT, creare la cartella FONT e creare il file FONT.SPRITEFONT.
Nella cartella CONTENT, creare la cartella IMMAGINI, quindi aggiungere i file
PHSLOGO.PNG e SINGLEPIXEL.PNG.
Cartella SCREENMANAGER
File GAMESCREEN.CS
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace MenuSystem
{ public enum ScreenState
{ TransitionOn,
Active,
TransitionOff,
Hidden,
Frozen,
Inactive,
}
XNA
um 82 di 196
public abstract class GameScreen
{
#region Fields and Properties
// è un meu popup
public bool IsPopup
{ get { return isPopup; }
set { isPopup = value; }
}
private bool isPopup = false;
public TimeSpan TransitionOnTime
{ get { return transitionOnTime; }
protected set { transitionOnTime = value; }
}
TimeSpan transitionOnTime = TimeSpan.Zero;
public TimeSpan TransitionOffTime
{ get { return transitionOffTime; }
protected set { transitionOffTime = value; }
}
TimeSpan transitionOffTime = TimeSpan.Zero;
public float TransitionPercent
{ get { return transitionPercent; }
}
float transitionPercent = 0.00f;
public float TransitionSpeed
{ get { return transitionSpeed; }
}
float transitionSpeed = 1.5f;
public int TransitionDirection
{ get { return transitionDirection; }
}
int transitionDirection = 1;
public byte ScreenAlpha
{ get { return (byte)(transitionPercent * 255); }
}
public ScreenState ScreenState
{ get { return screenState; }
set { screenState = value; }
}
ScreenState screenState = ScreenState.TransitionOn;
public ScreenManager ScreenManager
{ get { return screenManager; }
internal set { screenManager = value; }
}
ScreenManager screenManager;
public bool IsExiting
{ get { return isExiting; }
protected set
{ isExiting = value;
if (isExiting && (Exiting != null))
{ Exiting(this, EventArgs.Empty); }
}
}
bool isExiting = false;
public bool IsActive
XNA
um 83 di 196
{ get
{ return (screenState == ScreenState.TransitionOn
|| screenState == ScreenState.Active);
}
}
public event EventHandler Entering;
public event EventHandler Exiting;
#endregion
#region Initialization
public virtual void LoadContent() { }
public virtual void UnloadContent() { }
#endregion
#region Update and Draw
// metodo che inizializza gli oggetti e le variabili
public virtual void Initialize() { }
public virtual void Update(GameTime gameTime, bool covered)
{ if (IsExiting)
{ screenState = ScreenState.TransitionOff;
if (!ScreenTransition(gameTime, transitionOffTime, -1))
{ this.Remove(); }
}
else if (covered)
{ if (ScreenTransition(gameTime, transitionOffTime, 1))
{ screenState = ScreenState.TransitionOff; }
else
{ screenState = ScreenState.Hidden; }
}
else if(screenState != ScreenState.Active)
{ if (ScreenTransition(gameTime, transitionOffTime, 1))
{ screenState = ScreenState.TransitionOn; }
else
{ screenState = ScreenState.Active; }
}
}
// rimuove lo screen dal manager
public virtual void Remove()
{ screenManager.RemoveScreen(this); }
// transizione dello screen
private bool ScreenTransition(GameTime gameTime, TimeSpan transitionTime, int
direction)
{ float transitionDelta;
if (transitionTime == TimeSpan.Zero)
transitionDelta = 1;
else
transitionDelta = (float)(gameTime.ElapsedGameTime.TotalMilliseconds /
transitionTime.TotalMilliseconds);
transitionPercent += transitionDelta * direction * transitionSpeed;
if ((transitionPercent <= 0) || (transitionPercent >= 1))
{ transitionPercent = MathHelper.Clamp(transitionPercent, 0, 1);
return false;
}
return true;
}
XNA
um 84 di 196
public virtual void HandleInput()
{ if (screenState != ScreenState.Active) return; }
public abstract void Draw(GameTime gameTime);
#endregion
#region Methods
public virtual void ExitScreen()
{ IsExiting = true;
if (transitionOffTime == TimeSpan.Zero)
this.Remove();
}
public void FreezeScreen()
{screenState = ScreenState.Frozen;}
#endregion
}
}
File INTROSCREEN.CS
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
namespace MenuSystem
{ public class IntroScreen:GameScreen
{
#region Fields and Properties
Texture2D texture;
public Texture2D Texture
{ get { return texture; }
set { texture = value; }
}
Texture2D pixel;
public Texture2D Pixel
{ get { return pixel; }
set { pixel = value; }
}
TimeSpan screenTime;
public TimeSpan ScreenTime
{ get { return screenTime; }
set { screenTime = value; }
}
// uso l’effetto fade
public byte Alpha
{ get { return (byte)(TransitionPercent * 255); }
}
// opacità?
float fadeOpacity;
public float FadeOpacity
{ get { return fadeOpacity; }
set { fadeOpacity = value; }
}
// colore di background
Color fadeColor;
XNA
um 85 di 196
public Color FadeColor
{ get { return fadeColor; }
set { fadeColor = value; }
}
#endregion
public IntroScreen()
{ TransitionOnTime = TimeSpan.FromSeconds(2.5);
TransitionOffTime = TimeSpan.FromSeconds(2.5);
}
// Unload la texture e il pixel se esistono
public override void UnloadContent()
{ if(texture != null) texture = null;
if (pixel != null) pixel = null;
}
public override void Update(GameTime gameTime, bool covered)
{ if (ScreenState == ScreenState.Active)
{ screenTime = screenTime.Subtract(gameTime.ElapsedGameTime);
if (screenTime.TotalSeconds <= 0) ExitScreen();
}
base.Update(gameTime, covered);
}
public override void Draw(GameTime gameTime)
{ SpriteBatch spriteBatch = ScreenManager.SpriteBatch;
Viewport viewport = ScreenManager.Game.GraphicsDevice.Viewport;
// centra la texture sullo schermo
Vector2 centerTexture = new Vector2((viewport.Width / 2) - (texture.Width / 2),
(viewport.Height / 2) - (texture.Height / 2));
spriteBatch.Begin();
if (texture.Width < viewport.Width || texture.Height < viewport.Height)
DrawFade(spriteBatch, viewport);
spriteBatch.Draw(texture, centerTexture, new Color(Color.White, Alpha));
spriteBatch.End();
}
private void DrawFade(SpriteBatch spriteBatch, Viewport viewport)
{ if (pixel != null)
spriteBatch.Draw(pixel, new Rectangle(0, 0, viewport.Width, viewport.Height),
new Color(fadeColor, (byte)(fadeOpacity * 255)));
}
}
}
File MENUSCREEN.CS
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace MenuSystem
{ public abstract class MenuScreen : GameScreen {
#region Fields and Properties
// lista del menu
List<string> menuEntries = new List<string>();
public List<string> MenuEntries
XNA
um 86 di 196
{ get { return menuEntries; }
}
// font che usa il menu
SpriteFont spriteFont;
public SpriteFont SpriteFont
{ get { return spriteFont; }
set { spriteFont = value; }
}
// la posizione della prima voce di menu
Vector2 startPosition;
public Vector2 StartPosition
{ get { return startPosition; }
set { startPosition = value; }
}
// la posizione delle altre voci di menu
Vector2 position;
public Vector2 Position
{ get { return position; }
set { position = value; }
}
// il colore della voce quando è selezionata
Color selected;
public Color Selected
{ get { return selected; }
set { selected = value; }
}
// colore del testo quando non è selezionato
Color nonselected;
public Color NonSelected
{ get { return nonselected; }
set { nonselected = value; }
}
// seleziono la voce di menu
int selectedEntry = 0;
#endregion
#region Menu Operations
// metodo quando la voce è selezionata
public abstract void MenuSelect(int selectedItem);
// metodo quando si cancella il menu
public abstract void MenuCancel();
#endregion
public MenuScreen()
{TransitionOnTime = TransitionOffTime = TimeSpan.FromSeconds(1.5);}
// Unload del font
public override void UnloadContent()
{ if (SpriteFont != null) SpriteFont = null;}
// muove tra le diverse voci di menu
public override void HandleInput()
{ InputSystem input = ScreenManager.InputSystem;
// se si muove up o down, seleziona una diversa voce
if (input.MenuUp)
{ selectedEntry--;
if (selectedEntry < 0)selectedEntry = menuEntries.Count - 1;
}
XNA
um 87 di 196
if (input.MenuDown)
{ selectedEntry++;
if (selectedEntry >= menuEntries.Count)selectedEntry = 0;
}
// se si clicca, chiama il metodo selezionato
if (input.MenuSelect)
{ MenuSelect(selectedEntry); }
// se si preme cancella, cjìhiama il metodo
if (input.MenuCancel)
{ MenuCancel(); }
}
public override void Update(GameTime gameTime, bool covered)
{ position = new Vector2(startPosition.X, startPosition.Y);
base.Update(gameTime, covered);
if (ScreenState == ScreenState.TransitionOn || ScreenState ==
ScreenState.TransitionOff)
{ Vector2 acceleration = new Vector2((float)Math.Pow(TransitionPercent - 1, 2), 0);
acceleration.X *= TransitionDirection * -150;
position += acceleration;
}
}
// disegna le voci di menu, incrementa la posizione Y per ogni nuova voce
public override void Draw(GameTime gameTime)
{ SpriteBatch spriteBatch = ScreenManager.SpriteBatch;
spriteBatch.Begin();
for (int i = 0; i < menuEntries.Count; i++)
{ bool isSelected = (i == selectedEntry);
DrawEntry(spriteBatch, gameTime, menuEntries[i], position, isSelected);
position.Y += spriteFont.LineSpacing;
}
spriteBatch.End();
}
// disegna la voce di menu
private void DrawEntry(SpriteBatch spriteBatch, GameTime gameTime,
string entry, Vector2 position, bool isSelected)
{ Vector2 origin = new Vector2(0, spriteFont.LineSpacing / 2);
Color color = isSelected ? selected : nonselected;
color = new Color(color, ScreenAlpha);
float pulse = (float)(Math.Sin(gameTime.TotalGameTime.TotalSeconds * 3) + 1);
float scale = isSelected ? (1 + pulse * 0.05f) : 0.8f;
spriteBatch.DrawString(spriteFont, entry, position, color, 0, origin,
scale, SpriteEffects.None, 0);
}
}
}
File SCREENMANAGER.CS
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;
XNA
um 88 di 196
namespace MenuSystem {
public class ScreenManager : DrawableGameComponent {
#region Fields
// lista degli screen correnti nel manager
List<GameScreen> screens = new List<GameScreen>();
// altra lista degli screen che sono modifcati nel gioco corrente
List<GameScreen> screensToUpdate = new List<GameScreen>();
SpriteBatch spriteBatch;
InputSystem inputSystem;
// lo screen manager è inizializzato?
bool isInitialized;
#endregion
#region Properties
public SpriteBatch SpriteBatch
{ get { return spriteBatch; }
}
public InputSystem InputSystem
{ get { return inputSystem; }
}
#endregion
#region Initialization
public ScreenManager(Game game)
: base(game)
{ base.Initialize();
inputSystem = new InputSystem();
isInitialized = true;
}
protected override void LoadContent()
{ ContentManager content = Game.Content;
spriteBatch = new SpriteBatch(GraphicsDevice);
// carica lo screen
foreach (GameScreen screen in screens)
screen.LoadContent();
}
protected override void UnloadContent()
{ foreach (GameScreen screen in screens)
screen.UnloadContent();
}
#endregion
#region Update and Draw
public override void Update(GameTime gameTime)
{ inputSystem.Update();
screensToUpdate.Clear();
if (screens.Count == 0)
this.Game.Exit();
foreach (GameScreen screen in screens)
screensToUpdate.Add(screen);
bool screenIsCovered = false;
bool firstScreen = true;
if (!Game.IsActive)
{ // pausa
}
else
{ while (screensToUpdate.Count > 0)
XNA
um 89 di 196
{ GameScreen screen = screensToUpdate[screensToUpdate.Count - 1];
screensToUpdate.RemoveAt(screensToUpdate.Count - 1);
//Update the screen
screen.Update(gameTime, screenIsCovered);
if (screen.IsActive)
{ if (firstScreen)
{ screen.HandleInput();
firstScreen = false;
}
if (!screen.IsPopup)
screenIsCovered = true;
}
}
}
}
public override void Draw(GameTime gameTime)
{ foreach (GameScreen screen in screens)
{ if (screen.ScreenState == ScreenState.Hidden)
continue;
screen.Draw(gameTime);
}
}
#endregion
#region Methods
public void AddScreen(GameScreen screen)
{ screen.ScreenManager = this;
if (this.isInitialized)
{ screen.LoadContent();
screen.Initialize();
}
screens.Add(screen);
}
public void RemoveScreen(GameScreen screen)
{ if (this.isInitialized)
{screen.UnloadContent();}
screens.Remove(screen);
screensToUpdate.Remove(screen);
}
#endregion
}
}
Cartella SCREENS
File LOGOSCREEN.CS
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
namespace MenuSystem
{ // logo
public class LogoScreen:IntroScreen
XNA
um 90 di 196
{ public LogoScreen()
{ // proprietà
ScreenTime = TimeSpan.FromSeconds(3);
FadeColor = Color.Black;
FadeOpacity = 0.9f;
}
public override void LoadContent()
{ // carico il logo e il pixel
ContentManager content = ScreenManager.Game.Content;
Texture = content.Load<Texture2D>("Immagini/phslogo");
Pixel = content.Load<Texture2D>("Immagini/singlePixel");
}
public override void Remove()
{ ScreenManager.AddScreen(new MainMenuScreen());
base.Remove();
}
}
}
File MAINMENUSCREEN.CS
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
namespace MenuSystem
{ public class MainMenuScreen : MenuScreen
{ public MainMenuScreen()
{ // aggiungo le voci di menu
MenuEntries.Add("Prima voce di menu 1");
MenuEntries.Add("Seconda voce di menu 2");
MenuEntries.Add("Terza voce di menu 3");
MenuEntries.Add("Quarta voce di menu 4");
// seleziono (non seleziono) il colore
Selected = Color.Yellow;
NonSelected = Color.White;
// posizione del menu
StartPosition = new Vector2(100, 200);
}
public override void LoadContent()
{ // carico il font che usa il menu
ContentManager content = ScreenManager.Game.Content;
SpriteFont = content.Load<SpriteFont>("Font/font");
}
public override void Remove()
{ base.Remove();
MenuEntries.Clear();
}
public override void MenuSelect(int selectedItem)
{ // selezione il menu desiderato
// case 0 = menu 1
// case 1 = menu 2
XNA
um 91 di 196
// case 2 = menu 3
// case 3 = menu 4
switch (selectedItem)
{ case 0: ExitScreen(); break;
case 1: ExitScreen(); break;
case 2: ExitScreen(); break;
case 3: ExitScreen(); break;
}
}
public override void MenuCancel()
{ // esco con il tasto ESC
ExitScreen();
}
}
}
File INPUTSYSTEM.CS
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Input;
namespace MenuSystem
{ public class InputSystem {
#region Fields and Properties
KeyboardState currentKeyboardState;
KeyboardState previousKeyboardState;
// stato del mouse e del GamePad
#endregion
#region Properties
public bool MenuUp
{ get { return IsNewPressedKey(Keys.Up); }
}
public bool MenuDown
{ get { return IsNewPressedKey(Keys.Down); }
}
public bool MenuSelect
{ get { return IsNewPressedKey(Keys.Enter); }
}
public bool MenuCancel
{ get { return IsNewPressedKey(Keys.Escape); }
}
#endregion
#region Input System Methods
public void Update()
{ previousKeyboardState = currentKeyboardState;
currentKeyboardState = Keyboard.GetState();
}
private bool IsNewPressedKey(Keys key)
{ return previousKeyboardState.IsKeyUp(key) &&
currentKeyboardState.IsKeyDown(key);
}
private bool IsPressedKey(Keys key)
{ return currentKeyboardState.IsKeyDown(key);
XNA
um 92 di 196
}
#endregion
}
}
File GAME1.CS
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
namespace MenuSystem
{ public class Game1 : Microsoft.Xna.Framework.Game
{ GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
ScreenManager screenManager;
public Game1()
{ Window.Title = "Menu";
graphics = new GraphicsDeviceManager(this);
graphics.PreferredBackBufferWidth = 800;
graphics.PreferredBackBufferHeight = 600;
Content.RootDirectory = "Content";
screenManager = new ScreenManager(this);
Components.Add(screenManager);
}
protected override void Initialize()
{ base.Initialize();
screenManager.AddScreen(new LogoScreen());
}
protected override void LoadContent()
{ spriteBatch = new SpriteBatch(GraphicsDevice);}
protected override void UnloadContent()
{ }
protected override void Update(GameTime gameTime)
{ if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{ GraphicsDevice.Clear(Color.CornflowerBlue);
base.Draw(gameTime);
}
}
}
XNA
um 93 di 196
XNA
um 94 di 196
LIBRERIE
INTRODUZIONE
Aprire un progetto, Windows Game Library (3.0).
Nel progetto ci sono i seguenti file.
File EMITTER.CS
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace ParticleSystemLibrary {
public class Emitter:Icloneable {
XNA
um 95 di 196
#region Fields and Properties
string name;
public string Name
{ get { return name; }
set { name = value; }
}
Texture2D texture;
public Texture2D Texture
{ get { return texture; }
set { texture = value; }
}
Vector2 position = Vector2.Zero;
public Vector2 Position
{ get { return position; }
set { position = value; }
}
Vector2 radius = Vector2.Zero;
public Vector2 Radius
{ get { return radius; }
set { radius = value; }
}
Vector2 velocity = Vector2.Zero;
public Vector2 Velocity
{ get { return velocity; }
set { velocity = value; }
}
Vector2 gravity = Vector2.Zero;
public Vector2 Gravity
{ get { return gravity; }
set { gravity = value; }
}
float minimumParticleAngle = 0;
public float MinimumParticleAngle
{ get { return minimumParticleAngle; }
set { minimumParticleAngle = value; }
}
float maximumParticleAngle = 355;
public float MaximumParticleAngle
{ get { return maximumParticleAngle; }
set { maximumParticleAngle = value; }
}
float minimumAngle = MathHelper.ToRadians(0);
public float MinimumAngle
{ get { return minimumAngle; }
set { minimumAngle = value; }
}
float maximumAngle = MathHelper.ToRadians(359);
public float MaximumAngle
{ get { return maximumAngle; }
set { maximumAngle = value; }
}
Random randomizer;
#endregion
public Emitter() { }
XNA
um 96 di 196
public Emitter(Texture2D texture)
{ this.texture = texture;
randomizer = new Random();
}
public virtual Particle GenerateParticle(ParticleSystem system)
{ Particle p = new Particle(texture);
Vector2 particleRadius = new Vector2(radius.X * (float)(randomizer.NextDouble() * 2 - 1),
radius.Y * (float)(randomizer.NextDouble() * 2 - 1));
p.Position = Vector2.Add(position, particleRadius);
p.MaxLifeTime = system.ParticleLongevity;
float randAngle = minimumAngle + (float)randomizer.NextDouble() *
(maximumAngle - minimumAngle);
float speed = (float)(velocity.Length() * randomizer.NextDouble());
p.Velocity = new Vector2((float)Math.Cos(randAngle) * speed,
(float)Math.Sin(randAngle) * speed);
p.Acceleration = new Vector2(gravity.X, gravity.Y);
p.Color = new Color(system.BirthColor);
p.Opacity = system.BirthOpacity;
p.Color = new Color(p.Color, p.Opacity);
p.Rotation = randomizer.Next((int)minimumParticleAngle,
(int)maximumParticleAngle);
p.Size = system.BirthSize;
p.AngularVelocity = system.DeltaRotation * (float)(randomizer.NextDouble() * 2 - 1);
p.Initialize(system.ParticleLongevity);
return p;
}
#region ICloneable Members
public object Clone()
{ Emitter emitter = new Emitter();
emitter.gravity = new Vector2(gravity.X, gravity.Y);
emitter.maximumAngle = maximumAngle;
emitter.maximumParticleAngle = maximumParticleAngle;
emitter.minimumAngle = minimumAngle;
emitter.minimumParticleAngle = minimumParticleAngle;
emitter.name = name;
emitter.position = new Vector2(position.X, position.Y);
emitter.radius = new Vector2(radius.X, radius.Y);
emitter.randomizer = new Random();
emitter.texture = texture;
emitter.velocity = new Vector2(velocity.X, velocity.Y);
return emitter;
}
#endregion
}
}
File PARTICLE.CS
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
namespace ParticleSystemLibrary {
XNA
um 97 di 196
public class Particle {
#region Fields and Properties
bool active = true;
public bool Active
{ get { return active; }
set { active = value; }
}
Texture2D texture;
public Texture2D Texture
{ get { return texture; }
set { texture = value; }
}
float lifetime = 0;
public float LifeTime
{ get { return lifetime; }
set { lifetime = value; }
}
float maxLifeTime = 0;
public float MaxLifeTime
{ get { return maxLifeTime; }
set { maxLifeTime = value; }
}
Vector2 position = Vector2.Zero;
public Vector2 Position
{
get { return position; }
set { position = value; }
}
Vector2 velocity = Vector2.Zero;
public Vector2 Velocity
{ get { return velocity; }
set { velocity = value; }
}
Vector2 acceleration = Vector2.Zero;
public Vector2 Acceleration
{ get { return acceleration; }
set { acceleration = value; }
}
Color color = Color.White;
public Color Color
{ get { return color; }
set { color = value; }
}
float opacity = 1;
public float Opacity
{ get { return opacity; }
set { opacity = value; }
}
float rotation = 0;
public float Rotation
{ get { return rotation; }
set { rotation = value; }
}
float angularVelocity = 0;
XNA
um 98 di 196
public float AngularVelocity
{ get { return angularVelocity; }
set { angularVelocity = value; }
}
float size = 1;
public float Size
{ get { return size; }
set { size = value; }
}
#endregion
public Particle() { }
public Particle(Texture2D texture)
{ this.texture = texture; }
public virtual void Initialize(float lifetime)
{ maxLifeTime = lifetime; }
public virtual void Update(GameTime gameTime, Vector4 deltaColor,
float deltaSize, float deltaOpacity)
{ float seconds = (float)gameTime.ElapsedGameTime.TotalSeconds;
lifetime += seconds;
if (lifetime >= maxLifeTime) { active = false;
return;
}
velocity += acceleration * seconds;
position += velocity * seconds;
color = new Color(Vector4.Add(color.ToVector4(), Vector4.Multiply(deltaColor,
seconds)));
opacity += deltaOpacity * seconds;
color = new Color(color, opacity);
rotation += angularVelocity * seconds;
size += deltaSize * seconds;
}
public virtual void Update(GameTime gameTime, ParticleSystem system)
{ Update(gameTime, system.DeltaColor, system.DeltaSize, system.DeltaOpacity); }
public virtual void Draw(GameTime gameTime, SpriteBatch spriteBatch)
{ spriteBatch.Draw(texture,position,null, color, rotation, new Vector2(texture.Width / 2,
texture.Height / 2), size, SpriteEffects.None, 0.0f);
}
}
}
File PARTICLESYSTEM.CS
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
XNA
um 99 di 196
namespace ParticleSystemLibrary {
public class ParticleSystem:ICloneable {
#region Fields and Properties
bool active;
public bool Active
{ get { return active; }
set { active = value; }
}
string name;
public string Name
{ get { return name; }
set { name = value; }
}
string textureName;
public string TextureName
{ get { return textureName; }
set { textureName = value; }
}
Vector2 origin = Vector2.Zero;
public Vector2 Origin
{ get { return origin; }
set { origin = value; }
}
SpriteBlendMode blendMode = SpriteBlendMode.AlphaBlend;
public SpriteBlendMode BlendMode
{ get { return blendMode; }
set { blendMode = value; }
}
Emitter emitter;
public Emitter Emitter
{ get { return emitter; }
set { emitter = value; }
}
List<Particle> particlesToUpdate;
List<Particle> particles;
public List<Particle> Particles
{ get { return particles; }
set { particles = value; }
}
float systemTimer = float.MaxValue;
public float SystemTimer
{ get { return systemTimer; }
set { systemTimer = value; }
}
int birthRate = 2;
public int BirthRate
{ get { return birthRate; }
set { birthRate = value; }
}
float releaseRate = 0.5f;
float releaseTimer = 0;
float particleLongevity = 5;
public float ParticleLongevity
{ get { return particleLongevity; }
XNA
um 100 di 196
set { particleLongevity = value; }
}
Vector4 birthColor = Color.White.ToVector4();
public Vector4 BirthColor
{ get { return birthColor; }
set { birthColor = value; }
}
Vector4 deathColor = Color.White.ToVector4();
public Vector4 DeathColor
{ get { return deathColor; }
set { deathColor = value; }
}
float birthSize = 1;
public float BirthSize
{ get { return birthSize; }
set { birthSize = value; }
}
float deathSize = 1;
public float DeathSize
{ get { return deathSize; }
set { deathSize = value; }
}
float birthOpacity = 1;
public float BirthOpacity
{ get { return birthOpacity; }
set { birthOpacity = value; }
}
float deathOpacity = 1;
public float DeathOpacity
{ get { return deathOpacity; }
set { deathOpacity = value; }
}
float birthRevolutions = 0;
public float BirthRevolutions
{ get { return birthRevolutions; }
set { birthRevolutions = value; }
}
float deathRevolutions = 0;
public float DeathRevolutions
{ get { return deathRevolutions; }
set { deathRevolutions = value; }
}
Vector2 velocityMinimum = Vector2.Zero;
public Vector2 VelocityMinimum
{ get { return velocityMinimum; }
set { velocityMinimum = value; }
}
Vector2 velocityMaximum = Vector2.Zero;
public Vector2 VelocityMaximum
{ get { return velocityMaximum; }
set { velocityMaximum = value; }
}
Vector4 deltaColor = Vector4.Zero;
public Vector4 DeltaColor
XNA
um 101 di 196
{ get { return deltaColor; }
set { deltaColor = value; }
}
float deltaOpacity = 0;
public float DeltaOpacity
{ get { return deltaOpacity; }
set { deltaOpacity = value; }
}
float deltaRotation = 0;
public float DeltaRotation
{ get { return deltaRotation; }
set { deltaRotation = value; }
}
float deltaSize = 0;
public float DeltaSize
{ get { return deltaSize; }
set { deltaSize = value; }
}
#endregion
#region Initialization
public ParticleSystem() { }
public virtual void Initialize()
{ if (birthRate > 150)
birthRate = 150;
releaseRate = 1.0f / (float)birthRate;
particles = new List<Particle>();
particlesToUpdate = new List<Particle>();
deltaColor = Vector4.Multiply(
new Vector4(deathColor.X - birthColor.X, deathColor.Y - birthColor.Y,
deathColor.Z - birthColor.Z, deathOpacity - birthOpacity), (1.0f / particleLongevity));
deltaOpacity = (deathOpacity - birthOpacity) / particleLongevity;
deltaRotation = (deathRevolutions - birthRevolutions) / particleLongevity;
deltaSize = (deathSize - birthSize) / particleLongevity;
active = true;
}
public virtual void LoadContent()
{ }
#endregion
#region Update and Draw
public virtual void Update(GameTime gameTime)
{ if (!active)
return;
systemTimer -= (float)gameTime.ElapsedGameTime.TotalSeconds;
if (systemTimer <= 0 && particles.Count == 0)
{ active = false;
return;
}
particlesToUpdate.Clear();
foreach (Particle p in particles)
particlesToUpdate.Add(p);
foreach (Particle p in particlesToUpdate)
{ if (!p.Active)
{ particles.Remove(p);
continue;
XNA
um 102 di 196
}
p.Update(gameTime, this);
}
releaseTimer += (float)gameTime.ElapsedGameTime.TotalSeconds;
while (releaseTimer >= releaseRate && systemTimer > 0)
{ particles.Add(emitter.GenerateParticle(this));
releaseTimer -= releaseRate;
}
}
public virtual void Reset()
{ active = true;
systemTimer = float.MaxValue;
releaseTimer = 0;
particlesToUpdate.Clear();
particles.Clear();
}
public virtual void Draw(GameTime gameTime, SpriteBatch spriteBatch)
{ spriteBatch.Begin(blendMode);
foreach (Particle p in particles)
p.Draw(gameTime, spriteBatch);
spriteBatch.End();
}
public virtual void Draw(GameTime gameTime, SpriteBatch spriteBatch, Matrix
transform)
{ spriteBatch.Begin(blendMode, SpriteSortMode.Deferred, SaveStateMode.None,
transform);
foreach (Particle p in particles)
p.Draw(gameTime, spriteBatch);
spriteBatch.End();
}
#endregion
#region ICloneable Members
public object Clone()
{ ParticleSystem system = new ParticleSystem();
system.active = active;
system.birthColor = new Vector4(birthColor.X, birthColor.Y, birthColor.Z, birthColor.W);
system.birthOpacity = birthOpacity;
system.birthRate = birthRate;
system.birthRevolutions = birthRevolutions;
system.birthSize = birthSize;
system.blendMode = blendMode;
system.deathColor=new Vector4(deathColor.X,deathColor.Y,deathColor.Z, deathColor.W);
system.deathOpacity = deathOpacity;
system.deathRevolutions = deathRevolutions;
system.deathSize = deathSize;
system.deltaColor = new Vector4(deltaColor.X, deltaColor.Y, deltaColor.Z, deltaColor.W);
system.deltaOpacity = deltaOpacity;
system.deltaRotation = deltaRotation;
system.deltaSize = deltaSize;
system.name = name;
system.origin = new Vector2(origin.X, origin.Y);
system.particleLongevity = particleLongevity;
system.particles = new List<Particle>();
foreach (Particle p in particles)
XNA
um 103 di 196
system.particles.Add(p);
system.particlesToUpdate = new List<Particle>();
foreach (Particle p in particlesToUpdate)
system.particlesToUpdate.Add(p);
system.releaseRate = releaseRate;
system.releaseTimer = releaseTimer;
system.systemTimer = systemTimer;
system.textureName = textureName;
system.velocityMaximum = new Vector2(velocityMaximum.X, velocityMaximum.Y);
system.velocityMinimum = new Vector2(velocityMinimum.X, velocityMinimum.Y);
return system;
}
#endregion
}
}
Compilare il progetto, se si esegue, Visual Studio risponde con questo messaggio.
DLL (DYNAMIC LINK LIBRARY)
È possibile importare proprie DLL oppure programmate da altri programmatori.
Esistono due metodi.
1. Importando un nuovo progetto all’interno della soluzione e poi aggiungendolo ai
Riferimenti, è possibile effettuare eventuali modifiche alla classe del progetto.
2. Importando le DLL aggiungendole ai Riferimenti, il codice è protetto e più veloce.
In Esplora soluzioni aggiungere un nuovo riferimento.
Attenzione ad aggiungere la DLL nella cartella Riferimenti del progetto, non quella
presente nella cartella Content.
Si apre una finestra da dove si sceglie che tipo di riferimento si vuole importare.
Fare clic sulla scheda Sfoglia così da poter scegliere un riferimento di tipo DLL presente
nella cartella.
I:\ESERCIZI\XNA\PARTICLESYSTEMIMPLEMENTATION\BIN\X86\DEBUG
XNA
um 104 di 196
Aprire un progetto, Windows Game (3.0) con i seguenti file.
File GAME1.CS
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
XNA
um 105 di 196
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
using ParticleSystemLibrary;
namespace ParticleSystemImplementation {
public class Game1 : Microsoft.Xna.Framework.Game {
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
SpriteFont spriteFont;
ParticleSystem system, secondarySystem;
Emitter emitter;
KeyboardState currentKeyboardState, previousKeyboardState;
int blendModeSelector = 1;
string blendModeMessage,changeBlendModeMessage,followMessage,killMessage,
currentTexture;
bool follow = false;
public Game1()
{ graphics = new GraphicsDeviceManager(this);
graphics.PreferredBackBufferWidth = 800;
graphics.PreferredBackBufferHeight = 600;
Content.RootDirectory = "Content";
system = new ParticleSystem();
}
protected override void Initialize()
{ system.SystemTimer = 1;
system.ParticleLongevity = 8f;
system.TextureName = "Immagini/fadingParticle";
system.BlendMode = SpriteBlendMode.AlphaBlend;
system.BirthRate = 150;
system.BirthSize = 0.0f;
system.DeathSize = 0.8f;
system.BirthRevolutions = 0;
system.DeathRevolutions = 0.5f;
system.BirthColor = Color.White.ToVector4();
system.DeathColor = Color.Black.ToVector4();
system.BirthOpacity = 1.0f;
system.DeathOpacity = 0.0f;
system.Initialize();
blendModeMessage = "Corrente Blend Mode: ";
changeBlendModeMessage = "Premi Invio per cambiare il blend mode";
followMessage = "Premi F per proseguire";
killMessage = "Premi K per terminare";
currentTexture = "";
base.Initialize();
}
protected override void LoadContent()
{ spriteBatch = new SpriteBatch(GraphicsDevice);
spriteFont = Content.Load<SpriteFont>("Font/font");
emitter = new Emitter(Content.Load<Texture2D>(system.TextureName));
emitter.Radius = new Vector2(0, 0);
XNA
um 106 di 196
emitter.Position=new Vector2(GraphicsDevice.Viewport.Width/2,
GraphicsDevice.Viewport.Height / 2);
emitter.Velocity = new Vector2(10, 10);
emitter.Gravity = new Vector2(0, 0);
system.Emitter = emitter;
}
protected override void UnloadContent()
{ system.Emitter.Texture = null; }
protected override void Update(GameTime gameTime)
{ previousKeyboardState = currentKeyboardState;
currentKeyboardState = Keyboard.GetState();
Vector2 move = Vector2.Zero;
if (currentKeyboardState.IsKeyDown(Keys.Left))
move.X -= 10;
if (currentKeyboardState.IsKeyDown(Keys.Right))
move.X += 10;
if (currentKeyboardState.IsKeyDown(Keys.Up))
move.Y -= 10;
if (currentKeyboardState.IsKeyDown(Keys.Down))
move.Y += 10;
emitter.Position = Vector2.Add(emitter.Position, move);
if (currentKeyboardState.IsKeyDown(Keys.Enter) &&
previousKeyboardState.IsKeyUp(Keys.Enter))
{ blendModeSelector++;
if (blendModeSelector > 2)
blendModeSelector = 0;
switch (blendModeSelector)
{ case 0: system.BlendMode = SpriteBlendMode.Additive;break;
case 1: system.BlendMode = SpriteBlendMode.AlphaBlend; break;
case 2: system.BlendMode = SpriteBlendMode.None; break;
}
}
if (currentKeyboardState.IsKeyDown(Keys.F)&&previousKeyboardState.IsKeyUp(Keys.F))
{ follow = !follow; }
if (currentKeyboardState.IsKeyDown(Keys.K)&&previousKeyboardState.IsKeyUp(Keys.K))
system.SystemTimer = 0;
system.Update(gameTime);
if (!system.Active)
{ system.Reset();
system.Emitter.Position = new Vector2(GraphicsDevice.Viewport.Width / 2,
GraphicsDevice.Viewport.Height / 2);
}
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{ GraphicsDevice.Clear(Color.CornflowerBlue);
if (follow)
{ Vector2 center = new Vector2(system.Emitter.Position.X GraphicsDevice.Viewport.Width / 2, system.Emitter.Position.Y GraphicsDevice.Viewport.Height / 2);
Matrix transform = Matrix.CreateTranslation(new Vector3(-center.X, -center.Y, 0));
system.Draw(gameTime, spriteBatch, transform);
}
else system.Draw(gameTime, spriteBatch);
XNA
um 107 di 196
spriteBatch.Begin();
spriteBatch.DrawString(spriteFont, blendModeMessage +
system.BlendMode.ToString(), new Vector2(10, 10), Color.White);
spriteBatch.DrawString(spriteFont, changeBlendModeMessage, new
Vector2(GraphicsDevice.Viewport.Width-200-spriteFont.MeasureString(followMessage).X,
10), Color.White);
spriteBatch.DrawString(spriteFont, followMessage,new
Vector2(GraphicsDevice.Viewport.Width-200-spriteFont.MeasureString(followMessage).X,
10 + spriteFont.LineSpacing), Color.White);
spriteBatch.DrawString(spriteFont, killMessage, new
Vector2(GraphicsDevice.Viewport.Width - 200 spriteFont.MeasureString(followMessage).X, 10 + spriteFont.LineSpacing * 2),
Color.White);
spriteBatch.DrawString(spriteFont, currentTexture, new
Vector2(GraphicsDevice.Viewport.Width - 200 spriteFont.MeasureString(followMessage).X, 10 + spriteFont.LineSpacing * 3),
Color.White);
spriteBatch.Draw(system.Emitter.Texture,new
Vector2(GraphicsDevice.Viewport.Width+175-spriteFont.MeasureString(followMessage).X,
10 + spriteFont.LineSpacing * 3), Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
}
}
XNA
um 108 di 196
MONDO 3D
INTRODUZIONE
Vector3
posizione = new Vector3(0f, 0f, 0f);
È l’insieme di 3 variabili di tipo float che identificano una posizione nello spazio
tridimensionale, è in pratica un punto nello spazio 3D, concettualmente come un Vector2
ma con una variabile di tipo float in più che identifica la coordinata Z, ogni oggetto
possiede una posizione nello spazio identificabile tramite le tre coordinate del mondo
tridimensionale: X,Y, Z.
posizione1 = new Vector3(X, Y, Z);
Nel caso delle coordinate 3D, il punto (0,0,0) corrisponde al punto che nell’immagine si
trova all’incrocio degli assi.
Se non si dovesse specificare diversamente, ogni oggetto, luce o telecamera inserita nel
nostro mondo, si troverà nel punto (0,0,0).
Per muovere un oggetto in avanti si deve allontanarlo dal nostro punto di vista, in pratica,
si deve diminuire la terza variabile float (Z) dell’oggetto, oppure, si può muovere il nostro
punto di vista tirandolo indietro, allontanando la telecamera dall’oggetto, facendo
aumentare la terza variabile float (Z) della telecamera.
Per spostare l’oggetto verso l’alto non si deve fare altro che aumentare la variabile float Y
dell’oggetto.
Per spostare a destra/sinistra l’oggetto, si deve lavorare sulla variabile float X.
Per le immagini si usa il termine pixel, per gli oggetti 3D si usa il VOXEL (VOlumetric
piXEL) che rappresenta un singolo valore su una griglia regolare nello spazio
tridimensionale, in pratica esprime la risoluzione del modello scansionato.
Matrix
Le matrici, sono indispensabili per effettuare trasformazioni di ogni tipo sui modelli 3D o
sulla telecamera, per esempio per assegnare una posizione ad un oggetto.
Per trasformazioni s’intende, lo spostamento, CreateTraslation, la scala, CreateScale, le
rotazioni.
Ogni tipo di trasformazione ha la sua sintassi, per esempio, con Vector3 si può spostare il
modello 3D nel punto posizione1, utilizzando questa sintassi.
posizione1 = new Vector3(X, Y, Z);
matrice = Matrix.CreateTranslation(posizione1);
XNA
um 109 di 196
Una volta assegnata questa matrice ad un oggetto, basterà cambiare i float del Vector3
per vederlo muoversi in tempo reale.
Ad esempio, per andare in avanti, in pratica allontanarlo dalla vista del giocatore, si lavora
sulla coordinata Z dell’oggetto.
Model
myModel = Content.Load<Model>("Cartella/modello");
Identifica un modello 3D da caricare dalla cartella del gioco il percorso e il nome del file è
scritto come stringa e nel nome del file non si specifica l’estensione perché, importandolo,
XNA lo riconosce, basta importarlo nell’Esplora soluzioni ed esso sarà pronto per l’uso.
Testxture2D
sfondo1 = Content.Load<Texture2D>("Cartella/immagine");
Identifica un’immagine, per texture s’intendono le immagini che sono “attaccate” ai modelli
per dare la sensazione di materiali particolari o disegni particolari sugli oggetti 3D.
Per Texture2D s’intende qualsiasi immagine a due dimensioni, una foto, un’immagine di
sfondo o un personaggio a due dimensioni.
Il codice serve per caricare il file nel gioco, non serve specificare l’estensione del file
perché XNA riconosce il tipo di file e che sia JPG (Joint Photographic Experts Group),
BMP (Windows BitMaP) o altro formato grafico.
PRIMA APPLICAZIONE
Ci sono due tipi di modelli 3D che si possono usare.
1. File con estensione X, si creano con applicazioni di modellazione come 3DS Max,
Softimage, Blender.
2. File con estensione FBX.
Creare una nuova cartella e chiamarla MODELLI, al suo interno importare il file Head.X.
File GAME1.CS
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
namespace PrimoGioco3D {
public class Game1 : Microsoft.Xna.Framework.Game {
GraphicsDeviceManager graphics;
Model mio_modello;
Vector3 posizione_modello;
float rotazione;
// gestisce la rotazione del modello
/* mio_modello è il modello che si carica nel LoadContent come se fosse un’immagine
posizione_modello è il Vector3 che identificherà la posizione del modello
funziona come un Vector2 ma questa volta si ha la terza dimensione, Z */
public Game1()
XNA
um 110 di 196
{ graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize()
{ posizione_modello = new Vector3(0, 0, 0);
base.Initialize();
}
/* il metodo Inizialize dove s’impostano le variabili: posizione_modello è un Vector3 che
imposta le coordinare del modello e per adesso lo mettiamo nel punto 0,0,0 */
protected override void LoadContent()
{ mio_modello = Content.Load<Model>("modelli/head"); }
/* come succedeva con le Texture2D, nel metodo LoadContent si caricano i file
in questo caso un modello che si chiama mio_modello */
protected override void Draw(GameTime gameTime)
{ graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
rotazione += 0.01f;
foreach (ModelMesh mesh in mio_modello.Meshes)
/* ogni ModelMesh è una classe di XNA che identifica le mesh che sono i pezzi di cui è
formato un modello, in questo caso il modello è un’unica mesh, è cioè tutto attaccato in un
unico pezzo ma sarebbe stato lo stesso se esso fosse stato diviso in più mesh */
{ foreach (BasicEffect effect in mesh.Effects)
/* ciclo che in questo caso applica un BasicEffect ad ogni parte del modello
BasicEffect è l’effetto base di XNA, lo shader che XNA utilizza per renderizzare gli
oggetti, per effetto shader s’intende il modo di ricreare i materiali, l’effetto delle luci su di
esso ed altri particolari che si possono impostare anche su un BasicEffect
è però possibile creare gli shader personalizzati che renderizzeranno come vorremo i
modelli che simuleranno i diversi materiali, con effetti visivi particolari */
{ effect.EnableDefaultLighting();
/* abilita le luci di base, è una delle caratteristiche del metodo BasicEffect */
effect.World = Matrix.CreateTranslation(posizione_modello) *
Matrix.CreateRotationY(rotazione);
effect.View = Matrix.CreateLookAt(new Vector3(100, 150, 200),
posizione_modello, Vector3.Up);
effect.Projection = Matrix.CreatePerspectiveFieldOfView(1f, 1.33f, 10f, 1000);
}
/* imposta e stampa a video il modello, utilizzando le matrici Word, View e Projection
View e Projection sono le due matrici che gestiranno il punto di vista
la telecamera ha diversi parametri da impostare e lo si farà con queste due matrici
Word è la terza matrice che imposta le trasformazioni del modello
dunque ogni modello avrà la propria matrice World */
mesh.Draw(SaveStateMode.SaveState);
/* è questa riga a fare il Draw del modello, a stamparlo sul video, è un metodo di una
classe di XNA (ModelMesh), anche questo chiamato Draw come per lo SpriteBatch
}
base.Draw(gameTime);
}
}
}
XNA
um 111 di 196
MESH
Sono una rappresentazione discretizzata di oggetti 3D e ciascuno è un insieme di vertici,
spigoli e facce che definiscono, con le loro coordinate 3D, la forma dell’oggetto.
Formati per il salvataggio delle mesh: PLY, OBJ e STL.
È possibile suddividere le mesh in due gruppi.
1. Statiche.
2. Animate, a loro volta possono essere divise in due sotto gruppi.
2.1. Rigide: si possono applicare solo trasformazioni “standard”, quindi traslazioni,
rotazioni, scale uniformi, proiezioni.
2.2. Deformabli.
MATRICI
Matrix è una classe di XNA con i suoi metodi dedicati all’impostazione della telecamera e
alle trasformazioni, spostamenti, scala e rotazioni, degli oggetti.
In ogni applicazione 3D si hanno 3 matrici indispensabili già create e pronte per l’uso.
1. World: definisce le varie trasformazioni, posizione, scala, rotazione, di un modello, per
cui ogni modello avrà una sua matrice World.
2. View: lavora sulla telecamera, in altre parole il punto di vista.
3. Projection: lavora sulla telecamera, in altre parole il punto di vista.
In presenza di una sola telecamera, si avrà solo una View e una Projection.
In ogni applicazione 3D si avrà almeno una telecamera che vorrà alcuni parametri, come
la sua posizione, il punto in cui deve guardare.
effect.View = Matrix.CreateLookAt(new Vector3(100, 150, 200), posizione_modello,
Vector3.Up);
effect.Projection = Matrix.CreatePerspectiveFieldOfView(1f, 1.33f, 10f, 10000)
XNA
um 112 di 196
View e Projection sono le due matrici che gestiranno il punto di vista.
La telecamera ha diversi parametri da impostare e lo si farà con due variabili di tipo Matrix.
View
Per impostare la matrice View (vista) si deve usare la sintassi seguente.
View = Matrix.CreateLookAt(posizione_telecamera , punto_in_cui_guardiamo,
Vector3.Up);
Con questa riga si assegnano i 3 parametri alla matrice View della telecamera.
1. cameraPosition, il punto nello spazio in cui si trova la telecamera di tipo Vector3.
2. cameraTarget, il punto nello spazio in cui guarda la telecamera di tipo Vector3.
3. cameraUpVector, il suo orientamento che sarà sempre Up, in pratica, sarà allineata
con l’orizzonte di tipo Vector3.
View richiede 3 variabili di tipo Vector3, è semplicemente un punto nello spazio
tridimensionale, come accade per il Vector2 che definisce un punto in uno spazio
bidimensionale.
Nell’esempio con il modello HEAD.FBX, si è scritto su cameraTarget la variabile
posizione_modello, in questo modo il punto inquadrato dalla telecamera sarà il modello.
Projection
Lavora sulla telecamera e imposta la prospettiva del punto di vista.
Projection = Matrix.CreatePerspectiveFieldOfView(profondità di vista, aspetto_schermo,
inzio_vista, fine_vista);
La matrice Projection richiede 4 parametri.
1. fieldOfView: la profondità di vista di tipo float.
2. aspectRatio: l’aspetto dell’immagine in uscita, larghezza/altezza, di tipo float.
3. nearPlaneDistance: piano vicino, la distanza da cui iniziare a visualizzare il mondo 3D,
di tipo float.
4. farPlaneDistance: piano lontano, la distanza in cui finire di visualizzare il mondo 3D, di
tipo float.
Per avere un’idea di come cambi la visualizzazione cambiando questi parametri, si devono
fare degli esempi.
Primo parametro della matrice Projection: fieldOfView
Si tratta della stessa scena, vista dallo stesso identico punto, l’unica cosa che cambia è il
parametro fieldOfView.
Per fieldOfView si usa sempre il numero 1f che simula una prospettiva normale.
effect.Projection = Matrix.CreatePerspectiveFieldOfView(1f, 1.33f, 10f, 1000);
effect.Projection = Matrix.CreatePerspectiveFieldOfView(0.2f, 1.33f, 10f, 1000);
XNA
um 113 di 196
La figura illustra il risultato di un fieldOfView che vale 0.2.
Secondo parametro della matrice Projection: aspectRatio
È il risultato della divisione tra le due dimensioni della finestra/schermo, larghezza diviso
altezza.
Facendo questa operazione, XNA saprà come visualizzare correttamente il mondo, senza
deformazioni dovute alle diverse risoluzioni.
Per esempio una risoluzione quadrata 800X800, aspectRatio sarà uguale a 1 perché
800/800 = 1, mentre la risoluzione standard 800X600 o 1024X768, sarà uguale a 1.33
perché sia 800/600 sia 1024/768 = 1.33.
Per avere sempre un aspectRatio corretto, qualsiasi risoluzione si usi, basterà fare la
seguente operazione.
aspectRatio = graphics.PreferredBackBufferWidth / graphics.PreferredBackBufferHeight;
Sapendo che PreferredBackBufferWidth ritorna la larghezza della finestra e
PreferredBackBufferHeight ritorna l’altezza, con questa operazione si avrà sempre un
aspectRatio corretto.
effect.Projection = Matrix.CreatePerspectiveFieldOfView(f1, 1.33f, 10f, 1000);
effect.Projection = Matrix.CreatePerspectiveFieldOfView(1f, 0.3f, 10f, 1000);
XNA
um 114 di 196
La figura illustra il risultato di un aspectRatio che vale 0.3.
Terzo e quarto parametro della matrice Projection: farPlaneDistance e nearPlaneDistance
Impostano le distanze minime e massime di renderizzazione (visualizzazione) del mondo
3D limitando i calcoli della CPU/GPU allo spazio necessario.
XNA
um 115 di 196
In pratica tutto ciò che si troverà prima del nearPlane e oltre il farPlane non sarà
renderizzato.
Questo eviterà alla GPU di dover calcolare elementi 3D tanto lontani o tanto vicini dalla
telecamera.
nearPlaneDistance è la distanza in cui inizia il rendering con un valore sempre basso, 10f.
farPlaneDistance è la distanza dove finirà il rendering con un valore di 10000.
effect.Projection = Matrix.CreatePerspectiveFieldOfView(1f, 1.33f, 10f, 1000);
effect.Projection = Matrix.CreatePerspectiveFieldOfView(1f, 0.3f, 10f, 260);
La figura illustra il risultato di un farPlane che vale 260.
World
Non lavora sulla telecamera ma sui modelli.
Ci sono tanti tipi di matrici già impostate e pronte per essere usate per impostare la
matrice World.
Una della caratteristiche delle matrici è quella di poter essere moltiplicate tra loro così da
poter impostare molte trasformazioni semplicemente moltiplicandole tra di loro.
Infatti per creare la matrice World non si deve fare altro che moltiplicare le matrici che
interessano.
World = Matrix.CreateTranslation(posizione_modello) * Matrix.CreateRotationY(rotazione);
In questo caso si usa CreateTranslation e CreateRotationY, la prima per spostare il
modello in posizione_modello e la seconda per ruotare il modello di un valore pari alla
XNA
um 116 di 196
variabile rotazione che si sa che nell’esempio, aumenta ad ogni fotogramma.
Le matrici che interesseranno maggiormente sono le seguenti.
 CreateTranslation imposta la posizione e gli spostamenti.
 CreateRotationX crea la rotazione sull’asse X.
 CreateRotationY crea la rotazione sull’asse Y.
 CreateRotationZ crea la rotazione sull’asse X.
 CreateScale crea la scala.
L’effetto finale varierà a seconda dell’ordine in cui le si usa.
Esempio.
World = Matrix.CreateTranslation(posizione_modello) * Matrix.CreateRotationY(rotazione);
È diverso dallo scrivere il codice seguente.
World = Matrix.CreateRotationY(rotazione) * Matrix.CreateTranslation(posizione_modello);
L’effetto finale sul modello sarà diverso perché un conto è prima spostarlo e poi ruotarlo e
un conto è prima ruotarlo e poi spostarlo.
Quando si effettuano queste moltiplicazioni fra matrici di trasformazione, è giusto sapere
anche che la prima trasformazione che è eseguita è quella scritta per ultima.
World = Matrix.trasformazioneA * Matrix.trasformazioneB * Matrix.trasformazioneC;
Sarà eseguita nel modo seguente.
Matrix.trasformazioneC * Matrix.trasformazioneB * Matrix.trasformazioneA;
In pratica, saranno eseguite con un ordine inverso.
DrawableGameComponent
Progettare un DrawableGameComponent che stampi qualsiasi modello sia passato come
parametro.
Progettare un record, come fatto nel 2D, per contenere solo le matrici View e Projection
che s’imposteranno una volta sola e che non si modificheranno più.
File MIE_VARIABILI.CS
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace PrimoGioco3D {
public struct Mie_variabili
{ public static Matrix View;
public static Matrix Projection;
}
}
Progettare ora il DrawableGameComponent che conterrà il codice per stampare a video
qualsiasi modello passato come parametro.
XNA
um 117 di 196
File STAMPA_MODELLI.CS
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Storage;
using Microsoft.Xna.Framework.Content;
namespace PrimoGioco3D {
public class Stampa_Modelli : DrawableGameComponent {
Game game;
string cartella_nome;
Vector3 posizione_modello;
Model mio_modello;
public Stampa_Modelli(Game par_game, string par_cartella_nome, Vector3
par_posizione_modello)
: base(par_game)
{ game = par_game;
cartella_nome = par_cartella_nome;
posizione_modello = par_posizione_modello;
}
protected override void LoadContent()
{ mio_modello = game.Content.Load<Model>(cartella_nome);
base.LoadContent();
}
public override void Draw(GameTime gameTime)
{ foreach (ModelMesh mesh in mio_modello.Meshes)
{ foreach (BasicEffect effect in mesh.Effects)
{ effect.EnableDefaultLighting();
effect.World = Matrix.CreateTranslation(posizione_modello);
effect.View = Mie_variabili.View;
effect.Projection = Mie_variabili.Projection;
}
mesh.Draw(SaveStateMode.SaveState);
}
base.Draw(gameTime);
}
}
}
Le prime righe dichiarano l’utilizzo dei namespace di XNA.
Aperta la nuova classe si trovano la dichiarazione delle variabili.
Game game;
string cartella_nome;
Vector3 posizione_modello;
Model mio_modello;
Tre di queste variabili sono inizializzate nel costruttore che riceve i valori tramite i
parametri che si passeranno con l’inizializzazione del componente, nel file GAME1.CS da
assegnare a ognuna di queste variabili.
La variabile di tipo Game è necessaria nel LoadContent per caricare il modello.
XNA
um 118 di 196
La variabile di tipo string è la stringa che identifica il percorso dove si trova il file.
Il Vector3 identifica la posizione del modello nello spazio 3D.
Il nome del modello che s’inizializza in questa classe e sarà sempre mio_modello.
public Stampa_Modelli(Game par_game, string par_cartella_nome, Vector3
par_posizione_modello) : base(par_game)
{ game = par_game;
cartella_nome = par_cartella_nome;
posizione_modello = par_posizione_modello;
}
Questo è il costruttore che riceve i parametri con i quali imposta le variabili dichiarate.
La variabile game diventa uguale al parametro par_game, la variabile cartella_nome
diventa uguale al parametro par_cartella_nome e la variabile posizione_modello diventa
uguale al parametro par_posizione_modello.
In questo modo si sono ricevuti i parametri e si usano per inizializzare le variabili che si
usano in questa classe.
protected override void LoadContent()
{ mio_modello = game.Content.Load<Model>(cartella_nome);
base.LoadContent();
}
Il metodo LoadContent carica i file, questa volta però non si scrive qui la stringa che
identifica il percorso del file ma la s’inserisce nella variabile cartella_nome che sarà uguale
al parametro che riceverà dall’inizializzazione del componete nel file GAME1.CS.
effect.World = Matrix.CreateTranslation(posizione_modello);
effect.View = Mie_variabili.View;
effect.Projection = Mie_variabili.Projection;
S’inizializza la matrice World perché le altre due sono uguali alle matrici inserite nel record
e saranno inizializzate nel file GAME1.CS.
In questo caso s’imposta solo la posizione del modello eliminando la rotazione che si
aveva prima, ora i modelli non ruoteranno più.
Per impostare la posizione si usa il metodo CreateTraslation della classe Matrix che
accetta un solo parametro che sarà il Vector3 che specifica la posizione del modello.
In seguito, per muovere l’oggetto basterà modificare le componenti X,Y,Z di Vector3.
Le matrici View e Projection le imposteremo uguali alle variabili all’interno della struttura.
File GAME1.CS
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
namespace PrimoGioco3D {
public class Game1 : Microsoft.Xna.Framework.Game {
GraphicsDeviceManager graphics;
XNA
um 119 di 196
Vector3 punto_inquadrato;
Stampa_Modelli modello1;
// dichiariamo la nuova classe/componente che stamperà i nostri modelli
public Game1()
{ graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize()
{ // queste tre righe imposteranno la telecamera
// impostiamo il punto inquadrato e le matrici View e Projection
punto_inquadrato = new Vector3(0, 0, 0);
Mie_variabili.View = Matrix.CreateLookAt(new Vector3(100, 150, 200),
punto_inquadrato, Vector3.Up);
Mie_variabili.Projection=Matrix.CreatePerspectiveFieldOfView(1,1.33f, 10f, 10000);
/* Le due righe sottostanti inizializzano e aggiungono il componente
Con la prima riga inviamo inizializziamo il componente inviando i parametri che
serviranno al componente dove il primo parametro è this, il secondo è la stringa
che identifica la cartella e il nome del modello e il terzo è il Vector3 che identifica la
posizione del modello in questione.
La seconda riga aggiunge il componente al gioco.*/
modello1 = new Stampa_Modelli(this, "modelli/head", new Vector3(0, 0, 0));
Components.Add(modello1);
base.Initialize();
}
protected override void LoadContent() { }
protected override void Draw(GameTime gameTime)
{ graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
base.Draw(gameTime);
}
}
}
L’effetto su schermo sarà esattamente uguale a prima ma è possibile aggiungere altri
modelli senza dover riscrivere altro codice che stampa a video un nuovo modello.
Esempio, aggiungere un altro modello alla scena.
Importare il nuovo modello nella cartella MODELLI, insieme al modello già importato.
File GAME1.CS
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
namespace PrimoGioco3D {
public class Game1 : Microsoft.Xna.Framework.Game {
GraphicsDeviceManager graphics;
Vector3 punto_inquadrato;
Stampa_Modelli modello1;
XNA
um 120 di 196
Stampa_Modelli modello2;
// nuovo modello
public Game1()
{ graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize()
{ punto_inquadrato = new Vector3(0, 0, 0);
Mie_variabili.View = Matrix.CreateLookAt(new Vector3(100, 150, 200),
punto_inquadrato, Vector3.Up);
Mie_variabili.Projection=Matrix.CreatePerspectiveFieldOfView(0.2f,1.33f,10f,10000);
modello1 = new Stampa_Modelli(this, "modelli/head", new Vector3(0, 0, 0));
Components.Add(modello1);
// inzializziamo il secondo modello, cambiamo la posizione spostandolo un po’
// verso sinistra (diminuiamo la X a -20) e cambiamo anche il nome del modello
modello2 = new Stampa_Modelli(this, "modelli/earth", new Vector3(-20, 0, 0));
Components.Add(modello2);
base.Initialize();
}
protected override void LoadContent() { }
protected override void Draw(GameTime gameTime)
{ graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
base.Draw(gameTime);
}
}
}
XNA
um 121 di 196
SPOSTARE IL MONDO 3D CON LA TASTIERA
Per renderizzare il modello giocante, in altre parole il modello principale, non si usa la
classe Stampa_modelli che si utilizzava solo per gli oggetti statici di contorno.
Per il personaggio principale si deve progettare un’altra classe chiamata Pg (personaggio
giocante), questo perché, in futuro, in un altro gioco, il Pg avrà tanti metodi e
caratteristiche particolari, differenti dal resto degli oggetti.
Progettare un altro DrawableGameComponent di nome Pg e copiare il codice di
STAMPA_MODELLI.CS con le seguenti modifiche.
File PG.CS
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Storage;
using Microsoft.Xna.Framework.Content;
namespace PrimoGioco3D {
public class Pg : DrawableGameComponent {
Game game;
string modello_pg;
Model mio_modello;
Vector3 tmp_posizione_pg;
public Pg(Game par_game, string par_modello_pg)
: base(par_game)
{ game = par_game;
modello_pg = par_modello_pg;
}
protected override void LoadContent()
{ mio_modello = game.Content.Load<Model>(modello_pg);
base.LoadContent();
}
public override void Draw(GameTime gameTime)
{ foreach (ModelMesh mesh in mio_modello.Meshes)
{ foreach (BasicEffect effect in mesh.Effects)
{ effect.EnableDefaultLighting();
effect.World = Matrix.CreateTranslation(Mie_variabili.posizione_pg);
effect.View = Mie_variabili.View;
effect.Projection = Mie_variabili.Projection;
}
mesh.Draw(SaveStateMode.SaveState);
}
base.Draw(gameTime);
}
}
}
Questa classe è quasi identica a quella per i modelli statici ma si devono modificare alcuni
parametri.
La posizione del Pg sarà dichiarata, come per le matrici View e Projection, nel record.
XNA
um 122 di 196
File MIE_VARIABILI.CS
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace PrimoGioco3D {
public struct Mie_variabili
{ public static Matrix View;
public static Matrix Projection;
public static Vector3 posizione_pg;
}
}
Così si ha la variabile posizione_pg sempre a disposizione e richiamabile senza passaggi
di parametri.
Le variabili View e Projection rimangono le stesse come tutto il resto del codice.
Si usa la classe Input usata per il 2D, quello che serve è la classe KeyboardState, il
costruttore e il metodo movimenti.
File INPUT.CS
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
namespace PrimoGioco3D {
class Input {
KeyboardState stato_tastiera;
public Input(Game game)
{}
public void movimenti()
{ stato_tastiera = Keyboard.GetState();
//Spostamento a destra
if (stato_tastiera.IsKeyDown(Keys.Right))
{ Mie_variabili.posizione_pg.X++; }
//Spostamento a sinistra
if (stato_tastiera.IsKeyDown(Keys.Left))
{ Mie_variabili.posizione_pg.X--;}
//Spostamento in avanti
if (stato_tastiera.IsKeyDown(Keys.Up))
{ Mie_variabili.posizione_pg.Z--;}
//Spostamento indietro
if (stato_tastiera.IsKeyDown(Keys.Down))
{ Mie_variabili.posizione_pg.Z++; }
}
}
}
Come per l’esempio del 2D, si sono progettati gli spostamenti dell’oggetto, questa volta ci
sono i possibili movimenti sull’asse X e l’asse Z.
XNA
um 123 di 196
File GAME1.CS
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
namespace PrimoGioco3D {
public class Game1 : Microsoft.Xna.Framework.Game {
GraphicsDeviceManager graphics;
Vector3 punto_inquadrato;
Stampa_Modelli modello2;
Input prova_input;
// la classe che implementa i movimenti
Pg personaggio_giocante;
// il nuovo componente Pg
public Game1()
{ graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize()
{ punto_inquadrato = new Vector3(0, 0, 0);
Mie_variabili.View = Matrix.CreateLookAt(new Vector3(0, 500, 0.1f),
punto_inquadrato, Vector3.Up);
// Mie_variabili.View = Matrix.CreateLookAt(new Vector3(0, 300, 800),
punto_inquadrato, Vector3.Up);
Mie_variabili.Projection = Matrix.CreatePerspectiveFieldOfView(0.2f, 1.33f, 10f,
1000);
modello2 = new Stampa_Modelli(this, "modelli/head", new Vector3(-20, 0, 0));
Components.Add(modello2);
personaggio_giocante = new Pg(this, "modelli/earth"); // il personaggio che
muoveremo
Components.Add(personaggio_giocante);
prova_input = new Input(this);
base.Initialize();
}
protected override void Draw(GameTime gameTime)
{ graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
prova_input.movimenti();
// il metodo che muove il personaggio
base.Draw(gameTime);
}
}
}
COLLISIONI CON LE BOUNDINGSPHERE
Rilevare le collisioni è uno dei problemi più difficili per chi progetta un videogioco in 3D, le
BoundingSphere sono delle classi di XNA create per rilevare le collisioni tra i modelli.
In altre parole, sono delle sfere che contengono i modelli e rilevano la collisione tra di loro,
non è il metodo più accurato ma è il più semplice da implementare.
Esistono anche i BoundigBox che sono l’equivalente delle BoundingSphere ma a forma di
parallelepipedo.
XNA
um 124 di 196
Queste classi possiedono un metodo chiamato Intersects che restituisce un valore
booleano nel caso di collisione, la sua sintassi è la seguente.
BS_1.Intersects(BS_2)
BS_1 è una BoundingSphere e BS_2 è la seconda BoundingSphere.
Questa riga controlla la collisione tra le due BoundingSphere e restituirà true se esse si
toccano.
Ponendo il centro delle BoundingSphere uguale a quello dei modelli di cui si vuole
controllare la collisione, in questo modo si controlla quando i modelli si toccano.
Rilevare le collisioni in questo modo non è preciso al millimetro perché di norma i modelli
non hanno una forma sferica.
La collisione è rilevata anche se in realtà gli oggetti ancora non si toccano.
Per ovviare a questo problema si possono usare più BoundingSphere per ogni modello
così da rendere più precisa la rilevazione.
La sintassi per impostare una BoundingSphere è la seguente.
BoundingSphere_1 = new BoundingSphere(posizione_sfera, raggio_sfera);
Il primo parametro, posizione_sfera è un Vector3 che identifica la posizione della
BoundingSphere, mentre il secondo parametro è il raggio della sfera.
Allora, se esiste una collisione, la posizione del modello deve tornare uguale a quella che
aveva nel fotogramma precedente, per fare questo si deve salvare la posizione che aveva
il Pg nel fotogramma precedente a quello che si sta vivendo.
XNA
um 125 di 196
Questo sistema è utile anche per evitare che la pressione di un tasto sia rilevata più di una
volta, anche qui si deve controllare il fotogramma precedente.
Per esempio, se si dovesse “sparare” un missile, un proiettile, anche nel caso che si
premesse un pulsante per un istante, si vedranno partire decine di colpi, questo perché,
per quanto si è veloci a premere e rilasciare un tasto, il PC sarà sempre più veloce e
verificherà che il controllo “tasto premuto” sia vero, almeno una decina di volte nell’istante
che si tiene premuto il pulsante ed eseguirà decine di volte l’istruzione dentro l’if.
Per sapere il valore di una variabile nel fotogramma precedente a quello che si sta
vivendo, si deve scrivere il seguente codice.
vecchia_posizione_pg = tmp_posizione_pg;
tmp_posizione_pg = posizione_pg;
Esempio, la posizione del Pg.
vecchia_posizione_PG è la variabile che si vuole trovare, in pratica quella nel fotogramma
precedente.
tmp_posizione_PG è una variabile temporanea che servirà di passaggio.
posizione_PG è la variabile odierna, la posizione del modello in questo momento.
In questo modo, la variabile vecchia_posizione_PG sarà uguale a posizione_PG solo nel
fotogramma successivo a quello in cui è in quel momento, dunque ritarderà ad aggiornarsi
di un fotogramma.
Con queste due righe si è impostata la variabile vecchia_posizione_PG con la posizione
del Pg che aveva nel fotogramma precedente.
Mie_variabili.vecchia_posizione_pg = tmp_posizione_pg;
tmp_posizione_pg = Mie_variabili.posizione_pg;
File MIE_VARIABILI.CS
Aggiungere la variabile vecchia_posizione_pg e la variabile di tipo BoundingSphere di
nome BS_pg identifica la sfera del personaggio giocante, nel record.
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace PrimoGioco3D
{ public struct Mie_variabili
{ public static Matrix View;
public static Matrix Projection;
public static Vector3 posizione_pg;
public static Vector3 vecchia_posizione_pg;
public static BoundingSphere BS_pg;
}
}
File PG.CS
Inizializzare la sfera che contiene il personaggio che si deve muovere.
Per far si che la sfera segua sempre il modello, si deve inizializzare la BoundingSphere
all’interno di un metodo che è eseguito ad ogni fotogramma.
Si usa il metodo Draw oppure si riscrive il metodo Update.
Aggiungere questa riga di codice.
XNA
um 126 di 196
Mie_variabili.BS_pg = new BoundingSphere(Mie_variabili.posizione_pg, 20f);
Inizializzare la variabile BS_pg di tipo BoundingSphere impostando il centro della sfera sul
centro del modello, Mie_variabili.posizione_pg e impostando il raggio della sfera a 20f.
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Storage;
using Microsoft.Xna.Framework.Content;
namespace PrimoGioco3D {
public class Pg : DrawableGameComponent {
Game game;
string modello_pg;
Model mio_modello;
Vector3 tmp_posizione_pg;
public Pg(Game par_game, string par_modello_pg)
: base(par_game)
{ game = par_game;
modello_pg = par_modello_pg;
}
protected override void LoadContent()
{ mio_modello = game.Content.Load<Model>(modello_pg);
base.LoadContent();
}
public override void Draw(GameTime gameTime)
{ Mie_variabili.BS_pg = new BoundingSphere(Mie_variabili.posizione_pg, 25f);
Mie_variabili.vecchia_posizione_pg = tmp_posizione_pg;
tmp_posizione_pg = Mie_variabili.posizione_pg;
foreach (ModelMesh mesh in mio_modello.Meshes)
{ foreach (BasicEffect effect in mesh.Effects)
{ effect.EnableDefaultLighting();
effect.World = Matrix.CreateTranslation(Mie_variabili.posizione_pg);
effect.View = Mie_variabili.View;
effect.Projection = Mie_variabili.Projection;
}
mesh.Draw(SaveStateMode.SaveState);
}
base.Draw(gameTime);
}
}
}
File STAMPA_MODELLI.CS
Bisogna creare una BoundingSphere che contenga il modello che si stampa con la classe
Stampa_modelli.
using System;
using System.Collections.Generic;
XNA
um 127 di 196
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Storage;
using Microsoft.Xna.Framework.Content;
namespace PrimoGioco3D {
public class Stampa_Modelli : DrawableGameComponent {
Game game;
string cartella_nome;
Vector3 posizione_modello;
Model mio_modello;
BoundingSphere BS_modello;
public Stampa_Modelli(Game par_game, string par_cartella_nome, Vector3
par_posizione_modello)
: base(par_game)
{ game= par_game;
cartella_nome = par_cartella_nome;
posizione_modello = par_posizione_modello;
}
protected override void LoadContent()
{ mio_modello = game.Content.Load<Model>(cartella_nome);
base.LoadContent();
}
public override void Draw(GameTime gameTime)
{ BS_modello = new BoundingSphere(posizione_modello, 20f);
////////////////////rileva la collisione//////////////////////////
if (Mie_variabili.BS_pg.Intersects(BS_modello))
// ritorna true, in altre parole se c’è una collisione tra la sfera Mie_variabili.BS_pg e la
// sfera "BS_modello".
{Mie_variabili.posizione_pg = Mie_variabili.vecchia_posizione_pg;}
//////////////////////////////////////////////////////////////////
foreach (ModelMesh mesh in mio_modello.Meshes)
{ foreach (BasicEffect effect in mesh.Effects)
{ effect.EnableDefaultLighting();
effect.World = Matrix.CreateTranslation(posizione_modello);
effect.View = Mie_variabili.View;
effect.Projection = Mie_variabili.Projection;
}
mesh.Draw(SaveStateMode.SaveState);
}
base.Draw(gameTime);
}
}
}
File GAME1.CS
modello2 = new Stampa_Modelli(this, "modelli/head", new Vector3(-50, 0, 0));
Modificare la posizione da -20 a -50 e verificare che muovendo la terra non si avvicini alla
testa.
XNA
um 128 di 196
XNA
um 129 di 196
TELECAMERA
PRIMA APPLICAZIONE
Creare un nuovo progetto per un sistema di movimento della telecamera che implementi i
movimenti laterali e la rotazione mediante l’uso del mouse e della classe Mouse.
Creare una nuova cartella e chiamarla MODELLI, al suo interno importare il file
AMBIENTE.X.
File Game1.Cs
Imposta la telecamera.
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace Telecamera {
public class Game1 : Microsoft.Xna.Framework.Game {
GraphicsDeviceManager graphics;
Stampa_Modelli modello1;
Vector3 camera_posizione;
// dichiariamo i 3 float che indentificano il valore
// ruotare, beccheggiare e rollare della telecamera.
float camera_rotazione;
float camera_beccheggio;
float camera_rollio;
Gravità gravità;
float forza_salto;
public Game1()
{ // il titolo della nostra finestra
Window.Title = "Telecamera";
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize()
{ // il modello dell’ambiente
modello1 = new Stampa_Modelli(this, "modelli/ambiente", new Vector3(0, 0, 0));
Components.Add(modello1);
forza_salto = 400f;
gravità = new Gravità();
// imposto la Matrice Projection
Mie_variabili.Projection = Matrix.CreatePerspectiveFieldOfView(1, 1.33f, 10f, 8000);
camera_posizione = new Vector3(0.0f, 100.0f, 0.0f);// posizione iniziale
camera_rotazione = 0f; // rotazione destra-sinistra (rotazione)
camera_beccheggio = 0f; // rotazione alto-basso (beccheggio)
camera_rollio = 0.0f; // rotazione rollio
base.Initialize();
}
protected override void Update(GameTime gameTime)
{ // secondi trascorsi dall’ultimo frame
XNA
um 130 di 196
float elapsedSec = (float)gameTime.ElapsedGameTime.TotalSeconds;
KeyboardState keyState = Keyboard.GetState(); // lo stato della tastiera
MouseState mouseState = Mouse.GetState(); // lo stato del Mouse
// determina l’angolo di rotazione e del beccheggio
camera_rotazione -= (float)(mouseState.X - graphics.PreferredBackBufferWidth /
2) * elapsedSec;
camera_beccheggio += (float)(mouseState.Y - graphics.PreferredBackBufferHeight
/ 2) * elapsedSec;
// riposiziona il mouse al centro del viewport per le successive letture
Mouse.SetPosition(graphics.PreferredBackBufferWidth / 2,
graphics.PreferredBackBufferHeight / 2);
// crea una matrice di rotazione secondo i vari angoli di rotazione
Matrix rot = Matrix.CreateFromYawPitchRoll(camera_rotazione,
camera_beccheggio, camera_rollio);
// calcola i vettori per la matrice di rotazione corrente
Vector3 camUp = Vector3.Transform(Vector3.UnitY, rot);
Vector3 camLook = Vector3.Transform(Vector3.UnitZ, rot);
Vector3 camLeft = Vector3.Cross(camUp, camLook);
// la velocità di movimento
float speed = 600.0f;
// pulsante per uscire dal gioco
if (keyState.IsKeyDown(Keys.Escape))
this.Exit();
// raddoppia la velocità se premuto il tasto lo Shift sinistro
if (keyState.IsKeyDown(Keys.LeftShift))
speed *= 2.0f;
// controlla la pressione dei tasti D S A I per lo spostamento
// moltiplica per elapsedSec per rendere la velocità di spostamento indipendente
// dal framerate.
if (keyState.IsKeyDown(Keys.S))
// sinistra camera
camera_posizione += speed * elapsedSec * camLeft;
if (keyState.IsKeyDown(Keys.D))
// destra camera
camera_posizione -= speed * elapsedSec * camLeft;
if (keyState.IsKeyDown(Keys.A))
// avanti
camera_posizione += speed * elapsedSec * camLook;
if (keyState.IsKeyDown(Keys.I))
// indietro
camera_posizione -= speed * elapsedSec * camLook;
if (keyState.IsKeyDown(Keys.Space)) // il tasto spazio implementa il salto
camera_posizione.Y += forza_salto * elapsedSec;
if (camera_posizione.Y < 10)
camera_posizione.Y = 10;
camera_posizione = gravità.applica_grav(camera_posizione);
// tenendo premuto F1 vedremo l’ambiente in WireFrame
if (keyState.IsKeyDown(Keys.F1))
GraphicsDevice.RenderState.FillMode = FillMode.WireFrame;
else GraphicsDevice.RenderState.FillMode = FillMode.Solid;
// crea la matrice per il punto di vista corrente
// si trova nel metodo Update perchè essa verrà aggiornata ad ogni fotogramma
Mie_variabili.View = Matrix.CreateLookAt(camera_posizione, camera_posizione +
camLook, camUp);
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{ GraphicsDevice.Clear(Color.CornflowerBlue);
XNA
um 131 di 196
base.Draw(gameTime);
}
}
}
camera_posizione = new Vector3(0.0f, 100.0f, 0.0f); // posizione iniziale
camera_rotazione = 0f;
// rotazione destra-sinistra (rotazione)
camera_beccheggio = 0f;
// rotazione alto-basso (beccheggio)
camera_rollio = 0.0f;
// rotazione rollio
Queste variabili lavorano su vari aspetti della telecamera in movimento, identificano la
rotazione, il beccheggio, il rollio e la posizione.
All’inizio saranno poste tutte a 0 e la telecamera guarderà davanti a sé, senza rotazioni.
Poi, premendo i tasti D S A I e muovendo il mouse, queste variabili varieranno e faranno
muovere e ruotare la telecamera.
È nel metodo Update che succede tutto, è necessario che i cambiamenti delle variabili
siano controllati ad ogni fotogramma.
// secondi trascorsi dall’ultimo frame
float elapsedSec = (float)gameTime.ElapsedGameTime.TotalSeconds;
gameTime.ElapsedGameTime.TotalSeconds fornisce i secondi trascorsi dall’ultimo frame,
questo dato serve per rendere il movimento del mondo costante e non legato dal numero
di frame al secondo.
Il numero di FPS è incostante e dipende dal numero di oggetti/triangoli/effetti che sono
visualizzati a schermo in quel momento, quindi questa velocità non sarà quasi mai la
stessa e varierà a seconda di cosa c’è sullo schermo in un determinato fotogramma.
Per evitare brusche accelerazioni dei movimenti e rendere il tutto più costante, si deve
moltiplicare questa variabile per la velocità che si dà alla telecamera.
La conversione in float, è necessaria perché gameTime.ElapsedGameTime.TotalSeconds
ritorna un double e quindi bisogna convertirla in float.
Nel resto del codice sono create le matrici per calcolare le rotazioni e i movimenti della
telecamera, per terminare nella matrice View si determina la sua posizione e il punto dove
essa guarderà.
File MIE_VARIABILI.CS
using Microsoft.Xna.Framework;
namespace Telecamera {
public struct Mie_variabili
{ public static Matrix View;
public static Matrix Projection;
}
}
File STAMPA_MODELLI.CS
using System;
using System.Collections.Generic;
XNA
um 132 di 196
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Storage;
using Microsoft.Xna.Framework.Content;
namespace Telecamera {
public class Stampa_Modelli : DrawableGameComponent {
Game game;
string cartella_nome;
Vector3 posizione_modello;
Model mio_modello;
public Stampa_Modelli(Game par_game, string par_cartella_nome, Vector3
par_posizione_modello)
: base(par_game)
{ game = par_game;
cartella_nome = par_cartella_nome;
posizione_modello = par_posizione_modello;
}
protected override void LoadContent()
{ mio_modello = game.Content.Load<Model>(cartella_nome);
base.LoadContent();
}
public override void Draw(GameTime gameTime)
{ foreach (ModelMesh mesh in mio_modello.Meshes)
{ foreach (BasicEffect effect in mesh.Effects)
{ effect.EnableDefaultLighting();
effect.World = Matrix.CreateTranslation(posizione_modello);
effect.View = Mie_variabili.View;
effect.Projection = Mie_variabili.Projection;
effect.FogEnabled = true;
// uso della nebbia attivo (true)
effect.FogColor = new Vector3(0.5f, 0.7f, 1f);
// imposta il colore tramite un Vector3
effect.FogStart = 2000;
effect.FogEnd = 6000;
// le ultime due righe impostano la distanza d’inizio e di fine della nebbia, ciò significa che
// la nebbia inizia da 2000f dalla telecamera, a 6000f dalla telecamera la nebbia sarà
// massima in pratica gli oggetti saranno completamente coperti
}
mesh.Draw(SaveStateMode.SaveState);
}
base.Draw(gameTime);
}
}
}
I colori s’impostano con la classe Color ma, in questo caso, i metodi di XNA richiedono un
Vector3 che può avere la stessa funzione della classe Color, in questo caso si devono
usare 3 float da 0 a 1 per definire i tre colori RGB, invece dei byte.
Per esempio, per definire il colore bianco con un Vector3 si deve impostare (1f,1f,1f),
mentre per il rosso (1f,0f,0f), per un grigio (0.5f,0.5f,0.5f).
Per evitare che la telecamera non vada oltre il terreno, si può aggiungere questo codice
XNA
um 133 di 196
all’interno del metodo Update.
if (camera_posizione.Y < 10)
camera_posizione.Y = 10;
File GRAVITÀ.CS
using Microsoft.Xna.Framework;
namespace Telecamera {
class Gravità {
float forza_grav;
float incrementoY;
public Vector3 applica_grav(Vector3 posizione)
{ if (posizione.Y > 10)
{ forza_grav = 0.1f;
incrementoY += forza_grav;
posizione.Y -= incrementoY;
}
else
{ forza_grav = 0;
incrementoY = 0;
}
return posizione;
}
}
}
Si lavora con un Vector3 inviando la posizione su cui si vuole lavorare al metodo
applica_grav e ritorna la posizione con la gravità applicata, ora basta invocare il metodo
della classe Gravità all’interno del metodo Update del file GAME1.CS.
Per invocare il metodo della classe Gravità e per usare la nuova variabile di tipo float
forza_salto si deve prima dichiarare ed inizializzare la classe e la variabile.
XNA
um 134 di 196
XNA
um 135 di 196
F1 per vedere l’ambiente in WireFrame.
SECONDA APPLICAZIONE
Nel progetto ci sono i seguenti file.
Nella cartella CONTENT/FONT, creare il font: FONT.SPRITEFONT.
Nella
cartella
CONTENT/IMMAGINI
aggiungere
i
file:
BACKGROUND.JPG.
XNA
PLAYER.PNG
e
um 136 di 196
File GAMEPLAYOBJECT.CS
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
namespace Camera {
public enum ObjectStatus
{ Active,
Dying,
Dead
}
public class GameplayObject {
#region Status Data
ObjectStatus status;
public ObjectStatus Status
{ get { return status; }
}
#endregion
#region Graphics Data
Texture2D texture;
public Texture2D Texture
{ get { return texture; }
set { texture = value; }
}
Rectangle rectangle;
public Rectangle Rectangle
{ get { return rectangle; }
}
public Vector2 Origin
{ get {return new Vector2(texture.Width / 2.0f, texture.Height / 2.0f);}
}
float opacity = 1.0f;
byte Alpha
{ get { return (byte)(opacity * 255); }
}
Color color = Color.White;
protected Color Color
{ get {return new Color(color.R, color.G, color.B, Alpha); }
set { color = value; }
}
#endregion
#region Physics Data
Vector2 position = Vector2.Zero;
public Vector2 Position
{ get { return position; }
set { position = value; }
}
Vector2 velocity = Vector2.Zero;
public Vector2 Velocity
{ get { return velocity; }
set { velocity = value; }
}
XNA
um 137 di 196
Vector2 acceleration = Vector2.Zero;
public Vector2 Acceleration
{ get { return acceleration; }
set { acceleration = value; }
}
float rotation = 0f;
public float Rotation
{ get { return rotation; }
set { rotation = value; }
}
float speed = 0.0f;
public float Speed
{ get { return speed; }
set { speed = value; }
}
#endregion
#region Die Data
TimeSpan dieTime = TimeSpan.Zero;
public TimeSpan DieTime
{ get { return dieTime; }
set { dieTime = value; }
}
float diePercent = 0.0f;
#endregion
#region Initialization Methods
public virtual void Initialize()
{ if (!(status == ObjectStatus.Active))
status = ObjectStatus.Active;
}
#endregion
#region Update and Draw Methods
public virtual void Update(GameTime gameTime)
{ if (status == ObjectStatus.Active)
{ velocity += Vector2.Multiply(acceleration,
(float)gameTime.ElapsedGameTime.TotalSeconds);
position += Vector2.Multiply(velocity,
(float)gameTime.ElapsedGameTime.TotalSeconds);
if (texture != null)
rectangle = new Rectangle((int)position.X, (int)position.Y, texture.Width,
texture.Height);
}
else if (status == ObjectStatus.Dying)
{ Dying(gameTime); }
else if (status == ObjectStatus.Dead)
{ Dead(gameTime); }
}
public virtual void Dying(GameTime gameTime)
{ if (diePercent >= 1)
status = ObjectStatus.Dead;
else
{ float dieDelta = (float)(gameTime.ElapsedGameTime.TotalMilliseconds /
dieTime.TotalMilliseconds);
diePercent += dieDelta;
}
XNA
um 138 di 196
}
public virtual void Dead(GameTime gameTime) { }
public virtual void Collision(GameplayObject target) { }
public void Die()
{ if (status == ObjectStatus.Active)
{ if (dieTime != TimeSpan.Zero)
status = ObjectStatus.Dying;
else
status = ObjectStatus.Dead;
}
}
public virtual void Draw(GameTime gameTime, SpriteBatch spriteBatch)
{ if ((texture != null) && (spriteBatch != null))
spriteBatch.Draw(texture, position, null, Color, rotation, Origin, 1.0f,
SpriteEffects.None, 0.0f);
}
#endregion
}
}
File GAME1.CS
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
using System.Text;
namespace Camera {
public class Game1 : Microsoft.Xna.Framework.Game {
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
SpriteFont spriteFont;
// due gameplayobjects per giocare e per il background
GameplayObject player, bg;
// focus della telecamera
Vector2 cameraPosition;
// tastiera per il controllo della telecamera
KeyboardState currentKeyboardState, previousKeyboardState;
Matrix transform;
// zoom telecamera
float zoom = 1.0f;
//stringa d’informazione per l’utente
string info;
public Game1()
{ graphics = new GraphicsDeviceManager(this);
graphics.PreferredBackBufferWidth = 1280;
graphics.PreferredBackBufferHeight = 720;
XNA
um 139 di 196
Content.RootDirectory = "Content";
}
protected override void Initialize()
{ player = new GameplayObject();
bg = new GameplayObject();
base.Initialize();
}
protected override void LoadContent()
{ spriteBatch = new SpriteBatch(GraphicsDevice);
player.Texture = Content.Load<Texture2D>("Immagini/player");
bg.Texture = Content.Load<Texture2D>("Immagini/Background");
spriteFont = Content.Load<SpriteFont>("Font/font");
}
protected override void UnloadContent()
{ player.Texture = null;
bg.Texture = null;
spriteFont = null;
}
protected override void Update(GameTime gameTime)
{ previousKeyboardState = currentKeyboardState;
currentKeyboardState = Keyboard.GetState();
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed
|| currentKeyboardState.IsKeyDown(Keys.Escape))
this.Exit();
// zoom in/out della telecamera
if (currentKeyboardState.IsKeyDown(Keys.Z))
zoom += 0.002f;
if (currentKeyboardState.IsKeyDown(Keys.X))
zoom -= 0.002f;
/* movimenti della telecamera
* B = basso
* A = alto
* D = destra
* S = sinistra
* */
if (currentKeyboardState.IsKeyDown(Keys.B))
cameraPosition.Y -= 5;
if (currentKeyboardState.IsKeyDown(Keys.A))
cameraPosition.Y += 5;
if (currentKeyboardState.IsKeyDown(Keys.S))
cameraPosition.X -= 5;
if (currentKeyboardState.IsKeyDown(Keys.D))
cameraPosition.X += 5;
// movimenti dello sprite
Vector2 move = Vector2.Zero;
if (currentKeyboardState.IsKeyDown(Keys.Up))
move.Y -= 5;
if (currentKeyboardState.IsKeyDown(Keys.Down))
move.Y += 5;
if (currentKeyboardState.IsKeyDown(Keys.Left))
move.X -= 5;
if (currentKeyboardState.IsKeyDown(Keys.Right))
move.X += 5;
// rotazione
XNA
um 140 di 196
if (currentKeyboardState.IsKeyDown(Keys.Q))
player.Rotation -= MathHelper.ToRadians(1);
if (currentKeyboardState.IsKeyDown(Keys.E))
player.Rotation += MathHelper.ToRadians(1);
player.Position = Vector2.Add(player.Position, move);
// calcola il centro del focus della telecamera.
Vector2 center = new Vector2(cameraPosition.X - GraphicsDevice.Viewport.Width / 2,
cameraPosition.Y - GraphicsDevice.Viewport.Height / 2);
/* matrice di trasformazione di scala, rotazione, e traslazione
transform = Matrix.CreateScale(new Vector3((float)Math.Pow(zoom, 10),
(float)Math.Pow(zoom, 10), 0)) *
Matrix.CreateRotationZ(player.Rotation) *
Matrix.CreateTranslation(new Vector3(-center.X, -center.Y, 0));
StringBuilder sb = new StringBuilder();
sb.AppendLine("------- Controllo Camera ---------------");
sb.AppendLine("Z - Zoom In");
sb.AppendLine("X - Zoom Out");
sb.AppendLine("A - Muove la camera in alto");
sb.AppendLine("B - Muove la camera in basso");
sb.AppendLine("D - Muove la camera a destra");
sb.AppendLine("S - Muove la camera a sinistra");
sb.AppendLine("----------------------------------------");
sb.AppendLine("-------- Controllo dello sprite --------");
sb.AppendLine("Frecce - Muovono");
sb.AppendLine("Q - Ruota a sinistra");
sb.AppendLine("E - Ruota a destra");
sb.AppendLine("----------------------------------------");
info = sb.ToString();
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{ GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Deferred,
SaveStateMode.None, transform);
bg.Draw(gameTime, spriteBatch);
player.Draw(gameTime, spriteBatch);
spriteBatch.End();
spriteBatch.Begin();
spriteBatch.DrawString(spriteFont, info, new Vector2(50, 25), Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
}
}
XNA
um 141 di 196
XNA
um 142 di 196
TERRENO
PRIMA APPLICAZIONE
Creare un terreno, senza importare alcun modello esistente ma generendolo da una
HeightMap.
Si ha la possibilità di accelerare premendo il tasto SHIFT sinistro, di vedere il mondo in
WireFrame premendo il tasto F1 e di saltare premendo il tasto spazio.
Il terreno è formato da due texture che si mescolano, sfumando tra le montagne.
Fino ad ora il mondo era formato da un solo oggetto che rappresentava il “livello”.
Questo metodo però non è il più indicato per creare un terreno, salvo esso non sia
completamente piatto.
Infatti, per calcolare la collisione con esso, si era specificato che la telecamera non
avrebbe dovuto scendere sotto una determinata Y che rimane costante per tutto il “livello”
ma questo non è sufficiente, se si ha, per esempio, un modello per il terreno fatto di
montagne, con dossi, con salite e discese tutto il lavoro non funzionerebbe.
Il miglior modo per creare un terreno e rilevarne le collisioni è usare una HeightMap.
HeightMap
È un’immagine a scala di grigi (in bianco e nero) che rappresenta le altezze di un piano,
con le sue parti più alte (montagne, dossi) rappresentate dal colore bianco e con le parti
più basse (buche, cunette) rappresentate dal colore nero.
Avendo a disposizioni tutte le gradazioni di grigio, si può creare un livello pieno di sali e
scendi a piacimento che simulino montagne e vallate.
La caratteristica fondamentale di questa tecnica sta nella rilevazione delle collisioni.
Usando una HeightMap sarà semplice rilevare la collisione di un oggetto o della
telecamera con il terreno.
XNA non fornisce una classe per creare un terreno e per questo si usa un
DrawableGameComponent che avrà il compito di creare un oggetto 3D partendo da un
immagine 2D a scala di grigi.
Per usare questo componente occorrono 4 file che rappresentano.
1. L’immagine per la HeightMap: TERRENO.PNG.
2. Due immagini per dare un colore al terreno: TEXTURE1.JPG e TEXTURE2.JPG.
3. Un file con estensione FX che ha il compito di creare il materiale applicando le due
texture: SHADER_TERRENO.FX.
File SHADER_TERRENO.FX
Questi file generano gli effetti e sono chiamati shader, determinano il modo di visualizzare
un oggetto, un materiale o un effetto grafico (fuoco, luci, effetti particellari) oppure in 2D,
XNA
um 143 di 196
possono applicare effetti grafici sulla visualizzazione delle immagini o dell’intero schermo.
Fin’ora si è usato il BasicEffect che è lo shader di base di XNA, in questo caso si usa un
file FX, semplice che ha il compito di applicare le due texture al terreno in base
all’inclinazione dei triangoli, in pratica, applicherà la TEXTURE1 sulle le facce orizzontali e
la TEXTURE2 su quelle verticali, sfumando tra loro in base all’inclinazione delle facce,
creando così una diversificazione.
// dichiarazione dei parametri dello shader
float4x4 World
: WORLD;
float4x4 WorldViewProj : WORLDVIEWPROJ;
const float3 LightVec = float3(1, -1, 1);
texture horizontalTex;
texture verticalTex;
// stati dei sampler per le varie texture
sampler horizontalSampler = sampler_state
{
Texture = (horizontalTex);
AddressU = WRAP;
AddressV = WRAP;
AddressW = WRAP;
MIPFILTER =
LINEAR;
MINFILTER =
LINEAR;
MAGFILTER =
LINEAR;
};
sampler verticalSampler = sampler_state
{
Texture = (verticalTex);
AddressU = WRAP;
AddressV = WRAP;
AddressW = WRAP;
MIPFILTER =
LINEAR;
MINFILTER =
LINEAR;
MAGFILTER =
LINEAR;
};
// output del vertex shader
struct AmbientVS_OUTPUT
{
float4 oPos : POSITION;
float2 oTexCoords : TEXCOORD0;
float3 oNormal : TEXCOORD1;
};
// dichiara il vertex shader
AmbientVS_OUTPUT terrainVS(
in float3 Pos : POSITION,
in float3 Normal : NORMAL,
in float2 TexCoords : TEXCOORD0)
{
AmbientVS_OUTPUT Out;
// Trasforma il vertice in coordinate assolute
float4 p = mul(float4(Pos, 1.0), World);
// Proietta il vertice sullo schermo
Out.oPos = mul(p, WorldViewProj);
// Copia le coordinate di texture
Out.oTexCoords = TexCoords;
// Copia la normale del vertice (sarà interpolata dal dalla scheda grafica.
Out.oNormal = Normal;
return Out;
}
XNA
um 144 di 196
// dichiara il pixel shader
float4 terrainPS(
float2 TexCoord : TEXCOORD0,
float3 Normal : TEXCOORD1) : COLOR0
{
// carica i texel
float3 hColor = tex2D(horizontalSampler, TexCoord);
float3 vColor = tex2D(verticalSampler, TexCoord);
// normale del pixel
Normal = normalize(Normal);
// calcola l’illuminazione diffusa
float ndotl = saturate(dot(Normal, normalize(-LightVec)));
// interpola tra la texture orizzontale e la verticale secondo la normale del punto
float3 color = lerp(vColor*1.6, hColor, Normal.y);
// moltiplica il colore per il fattore di illuminazione.
color *= ndotl;
return float4(color, 1.0);
}
// dichiara la tecnica di rendering
technique Terrain
{
pass P0
{
ZEnable = true;
ZWriteEnable = true;
VertexShader = compile vs_2_0 terrainVS();
PixelShader = compile ps_2_0 terrainPS();
}
}
Quando si usa uno shader si deve inviargli i parametri, infatti, le righe all’interno del
foreach sono leseguenti.
foreach (BasicEffect effect in mesh.Effects) {
effect.EnableDefaultLighting();
effect.World = Matrix.CreateTranslation(posizione_modello);
effect.View = Mie_variabili.View;
effect.Projection = Mie_variabili.Projection;
}
Sono tutti i parametri che richiede il BasicEffect per una corretta visualizzazione.
Anche i parametri impostati per la nebbia, sono variabili che impostano l’effetto nebbia che
possiede il BasicEffect.
Le matrici World, View e Projection sono dei parametri essenziali per una corretta
visualizzazione con il BasicEffect.
La grafica di un gioco la fanno gli shader, è quindi un parametro importante nel momento
si desidera avere effetti particolari e personalizzati che si distinguono dal rendering di base
di XNA.
Per creare degli shader personalizzati, oltre a programmarli riga per riga, si possono usare
alcune applicazioni gratuite.
 RenderMonkey della AMD (Advanced Micro Devices) ATI (Array Technologies
Incorporated).
 FX Composer della nVIDIA.
XNA
um 145 di 196
In questa applicazione non si aggiunge il file TERRENO.PNG in Esplora soluzioni, si
carica direttamente da file, senza che sia compilato da XNA, la sintassi è la seguente.
Texture2D nuova_texture= Texture2D.FromFile(GraphicsDevice, "cartella/terreno.png");
In questo modo, una volta compilato il progetto, è possibile fare delle modifiche al file
immagine e vederle applicate nell’applicazione senza dover ricompilare il progetto.
Copiare il file nella cartella dove si trova l’eseguibile del progetto.
I:\ESERCIZI\XNA\TERRENO\TERRENO\BIN\X86\DEBUG
Importare, invece, gli altri 3 file direttamente in Esplora Soluzioni/Content.
File TERRAIN.CS
S’incolla il codice del componente che crea l’oggetto, questo componente richiede 5
parametri che s’invieranno dal GAME1.CS quando s’inizializza il componente.
1. game: di tipo Game, è la classe Game cui appartiene il componente.
2. meshScale: di tipo Vector3, ridimensiona le 3 dimensioni del terreno generato.
3. heightMapFile: di tipo Texture2D, il file immagine che genera la HeightMap.
4. horizTex: di tipo Texture2D, il file immagine per la texturizzazione orizzontale.
5. vertTex: di tipo Texture2D, il file immagine per la texturizzazione verticale.
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
XNA
um 146 di 196
namespace Terreno {
/* classe che si occupa di generare un terreno da una texture in scala di grigi che
rappresenta le altezze, disegnarla a schermo e rilevare eventuali collisioni, lLa classe è
derivata da DrawableGameComponent che permette il caricamento di contenuti grafici,
l’inizializzazione ed il disegno a video utilizzando la struttura a componenti di XNA */
public class Terrain : DrawableGameComponent {
// tipo ti vertice da utilizzare nel VertexBuffer per il rendering
private struct Vertex
{ public Vector3 pos;
public Vector3 normal;
public Vector2 texture;
}
// dichiarazione per la struttura precedente, informando DirectX del tipo di vertici
// che si utilizzeranno.
private VertexElement[] ve = new VertexElement[3]
{
new VertexElement(0, 0, VertexElementFormat.Vector3, VertexElementMethod.Default,
VertexElementUsage.Position, 0),
new VertexElement(0,12, VertexElementFormat.Vector3, VertexElementMethod.Default,
VertexElementUsage.Normal, 0),
new VertexElement(0,24, VertexElementFormat.Vector2, VertexElementMethod.Default,
VertexElementUsage.TextureCoordinate, 0)
};
private VertexDeclaration VertexDecl;
private string m_heightMapFile;
private int m_heightMapWidth, m_heightMapHeight;
private float[][] m_heightData;
private int m_numVerts;
private int m_numTris;
private VertexBuffer m_vb;
private IndexBuffer m_ib;
private string m_vertTex, m_horizTex;
/* texture per i triangoli orizzontali questa e le successive sono interpolate in base
all’inclinazione del triangolo rispetto all’orizzontale */
private Texture2D m_horizontalTex;
/* texture per i triangoli verticali questa e le precedenti sono interpolate in base
all’inclinazione del triangolo rispetto all’orizzontale */
private Texture2D m_verticalTex;
/* shader che disegna il terreno realizzando l’illuminazione ed il mix delle texture
precedenti */
private Effect m_fx;
// dimensioni di un texel della heightmap (XZ) e ridimensionamento valori heightmap (Y)
private Vector3 m_meshScale;
// l’insieme della matrice View e Projection
private Matrix ViewProjMatrix;
// la classe che calcola la gravità
private Gravità gravità;
// costrutture della classe, inizializza alcune variabili
// "game": classe Game cui appartiene il componente
// "meshScale": ridimensionamento della mesh generata
// "heightMapFile": file da cui prelevare dati per l’heightmap
// "horizTex": la texture1 per la parti orizzontali
// "vertTex": la texture2 per le parti verticali
public Terrain(Game game,
XNA
um 147 di 196
Vector3 meshScale,
string heightMapFile,
string horizTex,
string vertTex)
: base(game)
{ m_heightMapFile = heightMapFile;
m_meshScale = meshScale;
m_vertTex = vertTex;
m_horizTex = horizTex;
}
// carico i contenuti grafici e lo shader
// inoltre eseguo il metodo che cre la mesh, CreateMesh();
protected override void LoadContent()
{ // carica lo shader (il file .fx) per il terreno
m_fx = Game.Content.Load<Effect>("shader_terreno");
// carica le due textures
m_horizontalTex = Game.Content.Load<Texture2D>(m_horizTex);
m_verticalTex = Game.Content.Load<Texture2D>(m_vertTex);
VertexDecl = new VertexDeclaration(GraphicsDevice, ve);
// inizializza la classe per la gravità
gravità = new Gravità();
// esegue il metodo che crea la mesh del terreno
CreateMesh();
base.LoadContent();
}
// metodo che crea una mesh partendo da un texture grayscale
private void CreateMesh()
{ // carica la texture per creare il terreno
Texture2D heightMapTex = Texture2D.FromFile(GraphicsDevice,
m_heightMapFile);
// salva le dimensioni della HeightMap
m_heightMapWidth = heightMapTex.Width;
m_heightMapHeight = heightMapTex.Height;
// calcola il numero di vertici e triangoli
m_numVerts = heightMapTex.Width * heightMapTex.Height;
m_numTris = (heightMapTex.Width - 1) * (heightMapTex.Height - 1) * 2;
// alloca l’array per contenere il valore dei texel letti dalla texture
// e rilascia la memoria della texture
Byte[] texData = new Byte[m_numVerts];
heightMapTex.GetData<Byte>(texData);
heightMapTex.Dispose();
// alloca due array temporanei per creare il VertexBuffer e l’IndexBuffer
Vertex[] verts = new Vertex[m_numVerts];
Int32[] indices = new Int32[m_numTris * 3];
// alloca l’array che contiene i valori di altezza effettivi dei vertici della mesh
m_heightData = new float[heightMapTex.Height][];
// riempe l’array dei vertici con i dati per la mesh
int i = 0;
for (int z = 0; z < heightMapTex.Height; ++z)
{ m_heightData[z] = new float[heightMapTex.Width];
for (int x = 0; x < heightMapTex.Width; ++x, ++i)
{ // imposta la posizione e le coordinate di texture del vertice.
verts[i].pos = new Vector3(x, (float)texData[i], z);
verts[i].pos *= m_meshScale;
XNA
um 148 di 196
verts[i].normal = Vector3.UnitY;
verts[i].texture = new Vector2(x / (float)heightMapTex.Width, z /
(float)heightMapTex.Height) * 16.0f;
// salva l’altezza per utilizzarla nel rilevamento delle collizioni
m_heightData[z][x] = verts[i].pos.Y;
}
}
// riempe l’array degli indici con i dati per la mesh
i = 0;
for (int z = 0; z < heightMapTex.Height - 1; ++z)
{ for (int x = 0; x < heightMapTex.Width - 1; ++x, ++i)
{ int idx = x + z * heightMapTex.Width;
// calcola la normale di un vertice utilizzando i vertici adiacenti
// per formare un piano.
Vector3 pv = verts[idx + 1].pos - verts[idx].pos;
Vector3 pu = verts[idx + heightMapTex.Width].pos - verts[idx].pos;
pu.Normalize();
pv.Normalize();
verts[idx].normal = Vector3.Cross(pu, pv);
// ripete il procedimento per creare una normale interpolata con i vertici adiacenti
if (x > 0)
{ Vector3 u = verts[idx - 1].pos - verts[idx].pos;
Vector3 v = verts[idx + heightMapTex.Width].pos - verts[idx].pos;
u.Normalize();
v.Normalize();
verts[idx].normal += Vector3.Cross(u, v);
}
if (z > 0)
{ Vector3 u = verts[idx + 1].pos - verts[idx].pos;
Vector3 v = verts[idx - heightMapTex.Width].pos - verts[idx].pos;
u.Normalize();
v.Normalize();
verts[idx].normal += Vector3.Cross(u, v);
}
// normaliza la normale interpolata
verts[idx].normal.Normalize();
// specifica gli indici che formano i due triangoli tra il vertice corrente e
// quelli adiacenti.
// A *-----* B ABCD = quattro texel disposti in un quadrato 2x2 della heightmap
// | / | A = idx
// | / | B = idx + 1
// | / | C = idx + m_heightMapWidth
// C *-----* D D = idx + 1 + m_heightMapWidth
indices[i * 6 + 0] = idx;
indices[i * 6 + 1] = idx + 1;
indices[i * 6 + 2] = idx + m_heightMapWidth;
indices[i * 6 + 3] = idx + 1;
indices[i * 6 + 4] = idx + 1 + m_heightMapWidth;
indices[i * 6 + 5] = idx + m_heightMapWidth;
}
}
// salva i dati nel VertexBuffer
m_vb = new VertexBuffer(GraphicsDevice, typeof(Vertex), m_numVerts,
BufferUsage.WriteOnly);
XNA
um 149 di 196
m_vb.SetData<Vertex>(verts);
// salva i dati nell’IndexBuffer
m_ib = new IndexBuffer(GraphicsDevice, typeof(Int32), m_numTris * 3,
BufferUsage.WriteOnly);
m_ib.SetData<Int32>(indices);
// modifica il meshScale per utilizzarlo nel calcolo delle collisioni
m_meshScale.Y = 1.0f;
}
// libera la memoria occupata dai contenuti caricati in LoadContent()
protected override void UnloadContent()
{ m_vb.Dispose();
m_ib.Dispose();
m_horizontalTex.Dispose();
m_verticalTex.Dispose();
m_fx.Dispose();
base.UnloadContent();
}
// primo metodo CheckCollisions: BSphere <---> terreno
// controlla se ci sono collisioni tra la BoundingSphere della telecamera ed il terreno
// "boundSphere": la BoundingSphere da testare
// "newPos": la nuova posizione della BoundingSphere da impostare
public bool CheckCollisions(BoundingSphere boundSphere, out Vector3 newPos)
{ // calcola la posizione rispetto alla heightmap
Vector3 hMapPos = boundSphere.Center / m_meshScale;
// calcola le coordinate del texel della heightmap
int hmapX = (int)Math.Floor(hMapPos.X);
int hmapZ = (int)Math.Floor(hMapPos.Z);
// inizializza la nuova posizione dalla vecchia
newPos = boundSphere.Center;
// se all’interno del terreno
if (hmapX >= 0 && hmapZ >= 0 && hmapX < m_heightMapWidth - 1 && hmapZ <
m_heightMapHeight - 1)
{ // calcola i fattori di peso dei vertici adiacenti (da 0.0 a 1.0)
float lerpX = hMapPos.X - hmapX;
float lerpZ = hMapPos.Z - hmapZ;
// array temporaneo che forma un quadrato di 4 vertici così disposti:
// 0 *-----* 1
// | / |
// | / |
// | / |
// 2 *-----* 3
float[] hMapQuad = new float[4];
hMapQuad[0] = m_heightData[hmapZ][hmapX];
hMapQuad[1] = m_heightData[hmapZ][hmapX + 1];
hMapQuad[2] = m_heightData[hmapZ + 1][hmapX];
hMapQuad[3] = m_heightData[hmapZ + 1][hmapX + 1];
// esegue l’interpolazione delle varie altezze secondo le variabili lerpX e lerpY
float hMapY = (hMapQuad[1] * lerpX + hMapQuad[0] * (1.0f - lerpX)) * (1.0f lerpZ) +
(hMapQuad[3] * lerpX + hMapQuad[2] * (1.0f - lerpX)) * lerpZ;
// aggiunge il raggio della BoundingSphere, non è esattissimo per il rilevamento
// delle collisioni perchè per inclinazioni tendenti al verticale bisognerebbe
// spostare la nuova posizione anche su X e su Z
hMapY += boundSphere.Radius;
XNA
um 150 di 196
// calcola la nuova posizione se la posizione Y della telecamera
// si trova sotto quella del terreno
if (boundSphere.Center.Y < hMapY)
{ newPos.Y = hMapY;
return true;
}
}
// nessuna collisione
return false;
}
// secondo metodo CheckCollisions: BBox <---> terreno
// controlla se ci sono collisioni tra un BoundingBOX ed il terreno
// "boundBOX": il BBox da testare
// "newPos": la nuova posizione Y del BoundingBOX da impostare
public bool CheckCollisions(BoundingBox boundBox, out float newPos)
{ Vector3 hMapPos = boundBox.Min / m_meshScale;
// calcola le coordinate del texel della heightmap
int hmapX = (int)Math.Floor(hMapPos.X);
int hmapZ = (int)Math.Floor(hMapPos.Z);
// inizializza la nuova posizione Y dalla vecchia
newPos = boundBox.Min.Y;
// se all’interno del terreno
if (hmapX >= 0 && hmapZ >= 0 && hmapX < m_heightMapWidth - 1 && hmapZ <
m_heightMapHeight - 1)
{ // calcola i fattori di peso dei vertici adiacenti (da 0.0 a 1.0)
float lerpX = hMapPos.X - hmapX;
float lerpZ = hMapPos.Z - hmapZ;
// array temporaneo che forma un quadrato di 4 vertici così disposti:
// 0 *-----* 1
// | / |
// | / |
// | / |
// 2 *-----* 3
float[] hMapQuad = new float[4];
hMapQuad[0] = m_heightData[hmapZ][hmapX];
hMapQuad[1] = m_heightData[hmapZ][hmapX + 1];
hMapQuad[2] = m_heightData[hmapZ + 1][hmapX];
hMapQuad[3] = m_heightData[hmapZ + 1][hmapX + 1];
// esegue l’interpolazione delle varie altezze secondo le variabili lerpX e lerpY
float hMapY = (hMapQuad[1] * lerpX + hMapQuad[0] * (1.0f - lerpX)) * (1.0f lerpZ) +
(hMapQuad[3] * lerpX + hMapQuad[2] * (1.0f - lerpX)) * lerpZ;
// calcola la nuova posizione se la quota e sotto quella del terreno
if (boundBox.Min.Y <= hMapY)
{ newPos = hMapY;
return true;
}
}
// nessuna collisione
return false;
}
// disegna il terreno
public override void Draw(GameTime gameTime)
{ ViewProjMatrix = Mie_variabili.View * Mie_variabili.Projection;
XNA
um 151 di 196
// imposta il formato corretto dei vertici
GraphicsDevice.VertexDeclaration = VertexDecl;
// imposta il VertexBuffer e l’IndexBuffer per disegnare il terreno
GraphicsDevice.Vertices[0].SetSource(m_vb, 0,
VertexDecl.GetVertexStrideSize(0));
GraphicsDevice.Indices = m_ib;
// passa i parametri allo shader
m_fx.Parameters["World"].SetValue(Matrix.Identity);
m_fx.Parameters["WorldViewProj"].SetValue(ViewProjMatrix);
m_fx.Parameters["horizontalTex"].SetValue(m_horizontalTex);
m_fx.Parameters["verticalTex"].SetValue(m_verticalTex);
// gestisce il rendering con lo shader
m_fx.Begin(SaveStateMode.None);
foreach (EffectPass pass in m_fx.CurrentTechnique.Passes)
{ pass.Begin();
GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0,
m_numVerts, 0, m_numTris);
pass.End();
}
m_fx.End();
base.Draw(gameTime);
}
}
}
La gravità adesso è applicata all’interno di questo file, infatti, il componente terrain, oltre a
creare la mesh del terreno, possiede due metodi chiamati CheckCollisions che rilevano,
uno la collisione tra una Bsphere e il terreno e l’altro, la collisione tra un BBox e il terreno.
Per adesso si usa solo il primo metodo, rilevando la collisione delle BSphere della
telecamera con il terreno, restituisce un valore bool, in pratica, se la collisione è rilevata, il
metodo ritornerà true ed è reimpostata la Y della telecamera come uguale alla Y della
faccia del terreno su cui si trova.
Se il metodo restituisce false, la collisione non è avvenuta e significa che la Bsphere della
telecamera non tocca terra.
// calcola la nuova posizione se la posizione Y della telecamera
// si trova sotto quella del terreno
if (boundSphere.Center.Y < hMapY) {
newPos.Y = hMapY;
return true;
}
Quindi, la dichiarazione e l’inizializzazione della classe Gravità non avviene più nel file
GAME1.CS ma nel TERRAIN.CS.
File MIE_VARIABILI.CS
using Microsoft.Xna.Framework;
namespace Terreno {
public struct Mie_variabili
{ public static Matrix View;
public static Matrix Projection;
// variabile di tipo bool
public static bool coll_terreno;
// variabile di tipo Terrain
public static Terrain m_terrain;
XNA
um 152 di 196
}
}
coll_terreno identifica l’eventuale collisione tra la BSphere della telecamera ed il terreno.
m_terrain identifica l’oggetto della classe Terrain, in pratica il terreno di gioco.
Grazie a queste due nuove variabili si ha sempre a disposizione lo stato di collisione della
telecamera e il terreno stesso, per usarle in qualunque parte del codice senza problemi.
File GAME1.CS
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace Terreno {
public class Game1 : Microsoft.Xna.Framework.Game {
GraphicsDeviceManager graphics;
Vector3 camera_posizione;
float forza_salto;
float camera_rotazione;
float camera_beccheggio;
float camera_rollio;
// dichiara la classe che calcola la gravità della telecamera
Gravità gravità;
public Game1()
{ Window.Title = "Telecamera";
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize()
{ // inizializzo un’instanza del componente Terreno:
// XZ dimensioni della griglia, Y fattore di scala per i valori di altezza.
Mie_variabili.m_terrain = new Terrain(this,
new Vector3(64.0f, 6.0f, 64.0f),"terreno.png","texture1","texture2");
/* aggiungo il componente alla collezione in modo che questa classe si occupi in
automatico di aggiornare e disegnare il componente */
Components.Add(Mie_variabili.m_terrain);
// imposto la Matrice Projection
Mie_variabili.Projection = Matrix.CreatePerspectiveFieldOfView(1f, 1.33f, 10f,
100000);
camera_posizione = new Vector3(0.0f, 100.0f, 0.0f); //<-- Posizione iniziale
camera_rotazione = 0f; //<-- rotazione destra-sinistra (rotazione)
camera_beccheggio = 0f; //<-- rotazione alto-basso (beccheggio)
camera_rollio = 0.0f; //<-- rotazione rollio
// abbiamo tolto l’assegnazione del valore della variabile "forza_salto"
// che ora faremo nel metodo Update dove gestiremo il salto
// inizializza la classe per la gravità che andrà applicata alla telecamera
gravità = new Gravità();
base.Initialize();
}
protected override void Update(GameTime gameTime)
{ // secondi trascorsi dall’ultimo frame
float elapsedSec = (float)gameTime.ElapsedGameTime.TotalSeconds;
XNA
um 153 di 196
KeyboardState keyState = Keyboard.GetState(); // lo stato della tastiera
MouseState mouseState = Mouse.GetState(); // lo stato del mouse
// determina l’angolo di rotazione e del beccheggio.
camera_rotazione -= (float)(mouseState.X - graphics.PreferredBackBufferWidth /
2) * elapsedSec;
camera_beccheggio += (float)(mouseState.Y - graphics.PreferredBackBufferHeight
/ 2) * elapsedSec;
// riposiziona il mouse al centro del viewport per le successive letture
Mouse.SetPosition(graphics.PreferredBackBufferWidth / 2,
graphics.PreferredBackBufferHeight / 2);
// crea una matrice di rotazione secondo i vari angoli di rotazione
Matrix rot = Matrix.CreateFromYawPitchRoll(camera_rotazione,
camera_beccheggio, camera_rollio);
// calcola i vettori per la matrice di rotazione corrente
Vector3 camUp = Vector3.Transform(Vector3.UnitY, rot);
Vector3 camLook = Vector3.Transform(Vector3.UnitZ, rot);
Vector3 camLeft = Vector3.Cross(camUp, camLook);
// la velocità di movimento
float speed = 600.0f;
// pulsante per uscire dal gioco ESC
if (keyState.IsKeyDown(Keys.Escape))
this.Exit();
// raddoppia la velocità se premuto il tasto lo Shift sinistro MAIUSC
if (keyState.IsKeyDown(Keys.LeftShift))
speed *= 2.0f;
// controlla la pressione dei tasti D S A I per lo spostamento
// moltiplica per elapsedSec per rendere la velocità di spostamento indipendente
// dal framerate
if (keyState.IsKeyDown(Keys.D)) // destra telecamera
camera_posizione += speed * elapsedSec * camLeft;
if (keyState.IsKeyDown(Keys.S)) // sinistra telecamera
camera_posizione -= speed * elapsedSec * camLeft;
if (keyState.IsKeyDown(Keys.A)) // avanti
camera_posizione += speed * elapsedSec * camLook;
if (keyState.IsKeyDown(Keys.I)) // indietro
camera_posizione -= speed * elapsedSec * camLook;
// tenendo premuto F1 vedremo l’ambiente in WireFrame
if (keyState.IsKeyDown(Keys.F1))
GraphicsDevice.RenderState.FillMode = FillMode.WireFrame;
else
GraphicsDevice.RenderState.FillMode = FillMode.Solid;
// crea la matrice per il punto di vista corrente
// si trova nel metodo Update perchè essa verrà aggiornata ad ogni fotogramma
Mie_variabili.View = Matrix.CreateLookAt(camera_posizione, camera_posizione +
camLook, camUp);
// il metodo che controlla le collisioni col terreno
// salviamo la variabile di ritorno del metodo CheckCollisions (di tipo bool)
// in una variabile che inseriremo nella nostra cara struttura .
Mie_variabili.coll_terreno = Mie_variabili.m_terrain.CheckCollisions(new
BoundingSphere(camera_posizione, 150.0f), out camera_posizione);
// applichiamo la gravità alla telecamera
camera_posizione = gravità.applica_grav(Mie_variabili.coll_terreno,
camera_posizione);
/////////////////// salto ///////////////////
XNA
um 154 di 196
if (Mie_variabili.coll_terreno == true)
{ if (keyState.IsKeyDown(Keys.Space))
{ forza_salto = 200; }
else
{ forza_salto = 0; }
}
camera_posizione.Y += forza_salto * elapsedSec;
///////////////////////////////////////
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{ GraphicsDevice.Clear(Color.CornflowerBlue);
base.Draw(gameTime);
}
}
}
La prima parte del codice in questa nuova versione del GAME.CS è quella che inizializza il
nuovo componente chiamato m_terrain, della classe Terrain che ha il compito di creare il
terreno.
Mie_variabili.m_terrain = new Terrain(this, new Vector3(64.0f, 6.0f,
64.0f),"terreno.png","texture1","texture2");
Quando s’inizializza questa classe si devono inviare 5 parametri.
1. Il primo parametro è sempre il Game cui dovrà fare riferimento il componente.
2. Il secondo parametro gestisce la scala del terreno, con un Vector3 s’imposta la
dimensione X con il primo float (64.0f), l’altezza con la dimensione Y (6.0f) e la
dimensione Z (64.0f) con il terzo float.
3. Il terzo parametro stabilisce la texture a base di grigi, è la stringa che rappresenta il
nome del file da cui creare il terreno.
4. Il quarto parametro è la texture orizzontale.
5. Il quinto è la texture verticale.
Quando si desidera creare delle texture bisogna tenere presente che esse dovranno avere
sempre le dimensioni: potenza di 2, in pratica 2, 4, 16, 32, 64, 128, 256, 512, 1024, sia per
l’altezza sia per la larghezza.
Per esempio, una texture valida potrebbe avere le dimensioni di 128X128 pixel, oppure
256X256 o anche, per esempio, 512X256, combinando le varie dimensioni per altezza e
larghezza.
Un’altra porzione di codice cui dovremmo fare attenzione, è la chiamata al metodo
CheckCollision che controlla le collisioni con il terreno.
Mie_variabili.coll_terreno = Mie_variabili.m_terrain.CheckCollisions(new
BoundingSphere(camera_posizione, 150.0f), out camera_posizione);
// applichiamo la gravità alla telecamera
camera_posizione = gravità.applica_grav(Mie_variabili.coll_terreno, camera_posizione);
Il metodo CheckCollisions della classe Terrain restituisce un valore di tipo bool che è true
se c’è la collisione tra la BoundingSphere inviatagli come primo parametro e il terreno e
false se non c’è la collisione.
Questo metodo ha il compito anche di reimpostare la variabile inviatagli come secondo
parametro, camera_posizione.
Infatti, il primo parametro è di tipo BoundingSphere e rappresenta la BSphere di cui si
vuole controllare la collisione con il terreno, il secondo parametro è la variabile di tipo
XNA
um 155 di 196
Vector3 cui, se il metodo CheckCollisions restituisce true, va a reimpostare la Y uguale
alla Y del triangolo del terreno dove si trova la BSphere.
Si salva il valore che restituisce il metodo CheckCollisions nella variabile di tipo bool,
coll_terreno che si è aggiunta nel record Mie_variabili.
Così si ha questa variabile di tipo bool sempre disponibile che è true quando la telecamera
colliderà con il terreno.
Il metodo applica_grav è cambiato rispetto a prima e ora si deve inviargli 2 parametri.
Oltre alla posizione dell’oggetto cui si vuole applicare la gravità, s’invia anche lo stato della
collisione Mie_variabili.coll_terreno.
In questo modo il metodo applica_grav prende in considerazione se c’è o meno la
collisione con la terra.
Il secondo metodo CheckCollisions, quello che lavora con i Bbox, è diverso da quello che
lavora con le BSphere e gestisce direttamente la Y dell’oggetto, infatti, quando lo si usa, si
deve inviargli un float che sarebbe la Y della posizione dell’oggetto e non tutto il Vector3.
Altro cambiamento riguarda il salto.
if (Mie_variabili.coll_terreno == true) {
if (keyState.IsKeyDown(Keys.Space)) {
forza_salto = 200; }
else
forza_salto = 0;
}
camera_posizione.Y += forza_salto * elapsedSec;
Nell’esempio precedente “Telecamera”, il codice del salto non era perfetto, in effetti, se si
rilascia il tasto “spazio” durante il salto, la telecamera effettua un movimento anomalo,
accelerando la sua caduta, cosa che su un gioco normalmente non accade.
Questo avviene perché la pressione del tasto “spazio” è rilevata sempre, anche quando la
telecamera non tocca terra.
Adesso con la variabile Mie_variabili.coll_terreno, è possibile fare in modo che il salto sia
effettuato solo quando la telecamera è in collisione con il terreno, così da evitare
movimenti anomali, la variabile Mie_variabili.coll_terreno è true solo quando la telecamera
è a terra.
Con questo nuovo sistema si va a mettere il codice che effettua il salto.
camera_posizione.Y += forza_salto * elapsedSec;
Fuori da ogni if così che esso sia effettuato sempre.
Quello che si modifica è la forza del salto che, se il tasto “spazio” è premuto, sarà 200
mentre, quando il tasto non sarà premuto sarà a 0 e dunque la Y della telecamera non
aumenterà.
La rilevazione del tasto “spazio” premuto si trova dentro un altro if che rileva la collisione
con il terreno, così il salto è rilevato solo se la telecamera si trova a contatto con la terra.
File GRAVITÀ.CS
using Microsoft.Xna.Framework;
namespace Terreno {
class Gravità {
float forza_grav;
float incrementoY;
public Vector3 applica_grav(bool collisione, Vector3 posizione)
{ // se non c’è la collisione
if (collisione == false)
{ forza_grav = 0.2f;
XNA
um 156 di 196
incrementoY += forza_grav;
posizione.Y -= incrementoY;
}
else
{ forza_grav = 0;
incrementoY = 0;
}
return posizione;
}
}
}
Ora la forza di gravità sarà applicata solo quando la variabile bool
Mie_variabili.coll_terreno sarà false mentre è azzerata quando sarà true.
Infatti, questo metodo adesso richiede anche il parametro collisione che è true solo
quando l’oggetto cui si applica, la telecamera, tocca con il terreno.
XNA
um 157 di 196
XACT (Cross Platform Audio Creation Tool)
INTRODUZIONE
È un’applicazione di Microsoft che offre la possibilità agli utenti di creare delle librerie
audio che possono essere integrate nei progetti di XNA.
XACT è una parte del DirectX SDK; utilizza Xaudio per implementare le librerie su Xbox,
DirectSound per Windows XP e l’Audio Stack su Windows 7.
XACT organizza i file audio in Wave Banks e Sound Banks e li esegue tramite Cue.
Wave
È un buffer di dati audio, ossia un file che contiene un suono, i formati audio supportati da
XACT sono WAV, AIFF (Audio Interchange File Format) e XMA.
Sound
È insieme di tracce eseguite contemporaneamente, per ciascuna traccia si possono
impostare diverse proprietà tra cui il wave che quella traccia esegue.
Cue
È il suono logico, ossia ciò che è effettivamente usato nel codice per riprodurre un suono.
Ogni cue può contenere uno o più suoni; nel caso in cui esso contenga più suoni, quando
è eseguito, è selezionato uno dei suoni che esso contiene.
Tramite XACT si può decidere com’è effettuata la scelta del suono da eseguire, ad
esempio si può impostare una selezione casuale, oppure che i suoni siano riprodotti
nell’ordine in cui sono stati inseriti.
Wave Bank
È una collezione di waves, in altre parole una collezione di file audio, il formato del file è
XWB.
Per ogni Wave Bank si possono impostare varie proprietà, come ad esempio il metodo di
compressione per i file audio, se la riproduzione è effettuata in streaming o se il file è
tenuto in memoria.
Sound Bank
È una collezione di sounds e cues, il formato del file è XSB.
Il formato del progetto finale è XGS.
PROGETTO
Per creare la libreria audio con XACT, fare clic Start/Tutti i Programmi/Microsoft XNA
Game Studio 3.0/Tools/Microsoft Cross-Platform Audio Creation Tool (XACT).
XNA
um 158 di 196
La prima schermata che si presenterà è la seguente.
Fare clic su File/New Project per creare un nuovo progetto.
La schermata successiva sarà identica ma ora si può lavorare sul progetto.
Creare una Wave Bank e una Sound Bank: dal menu in alto selezionare Wave
Banks/new Wave Bank e Sound Banks/new Sound Bank.
Per ordinare le due finestre appena create, selezionare Window/Tile Horizontally.
Per inserire i file audio, selezionare la finestra Wave Bank, scegliere dal menu in alto
Wave Banks e Insert Wave File(s) (CTRL+W).
Dopo aver completato l’inserimento, i file sono nella Wave Bank, il cui nome risulta rosso
e in corsivo, questo perchè non sono ancora stati inseriti nella Sound Bank.
Selezionar i file audio e trascinarli dalla Wave Bank alla Sound Bank.
XNA
um 159 di 196
Nella Sound Bank si trova una sotto sezione Cue: per avere la possibilità di riprodurre il
file audio nel progetto XNA, si deve spostare i file da Sound Bank alla sottosezione Cue.
Ora la libreria è pronta per essere introdotta nel progetto XNA.
Prima di farlo, però, è possibile modificare alcune impostazioni avanzate per la
riproduzione dei file.
OPZIONI AGGIUNTIVE
È possibile, selezionando un file dalla Sound Bank, modificare le impostazioni avanzate
per la riproduzione del file tramite il menu in basso a sinistra.
In Mixing, si può modificare il volume, il tono (pitch) e la priorità per la riproduzione del file.
In Looping, si può impostare il numero di riproduzioni del file: impostando Infinite, il file
audio sarà riprodotto in continuazione.
Più in basso, è possibile variare le impostazioni trovate in Mixing durante l’esecuzione del
progetto, oppure dopo ogni loop del file audio.
Infine, tramite due menu è possibile aggiungere dei RPC (Runtime Parameter Controls) e
degli effetti ai suoni.
XNA
um 160 di 196
CREARE UNA CUE CONTENTENTE PIÙ DI UN FILE AUDIO
È possibile, mentre si trasferisce dalla Sound Bank alla sotto sezione Cue, unire più di un
file audio in una sola cue: così facendo, però, i due file saranno riprodotti
contemporaneamente.
Tuttavia, se in una cue si sono più suoni, si può impostare il metodo con cui è scelto quale
dei suoni riprodurre quando è eseguita la cue.
INSERIRE UNA LIBRERIA DI XACT NEL PROGETTO XNA
Per utilizzare la libreria creata con XACT nel progetto XNA, occorre inserire i file audio nel
progetto, insieme al file con estensione XAP creato da XACT, contenente le istruzioni per
la riproduzione.
Aprire il progetto XNA: a destra, in Esplora Soluzioni, selezionare la cartella CONTENT,
creare una cartella AUDIO, premere il tasto destro e selezionare Aggiungi/Elemento
Esistente.
Fatto ciò, occorre solo inserire queste righe di codice, per inizializzare la libreria e poterla
utilizzare.
Creare 3 oggetti:
AudioEngine engine;
SoundBank soundBank;
WaveBank waveBank;
In aggiunta si può creare un file cue, per riprodurre il file audio con una variabile
personalizzata.
Cue SoundPrincess;
Cue SoundEnd;
Procedere inizializzandoli nel metodo Initialize.
engine = new AudioEngine("Content\\Audio\\audio.xgs");
soundBank = new SoundBank(engine, "Content\\Audio\\Sound Bank.xsb");
waveBank = new WaveBank(engine, "Content\\Audio\\Wave Bank.xwb");
SoundPrincess = soundBank.GetCue("SoundPrincess");
SoundEnd = soundBank.GetCue("SoundEnd");
Per riprodurre i file, si hanno a disposizione due possibilità.
La prima è attraverso le cue create in precedenza.
XNA
um 161 di 196
SoundPrincess.Play()
SoundPrincess = soundBank.GetCue("SoundPrincess");
Facendo così però, una volta che la cue è stata riprodotta una volta, occorre ricaricarla
con l’istruzione GetCue perché è “consumata”.
La seconda possibilità è recuperando il file direttamente dalla SoundBank, evitando la cue.
soundBank.PlayCue("industry");
Esistono anche metodi che consentono di mettere in pausa un file, riprendere l’ascolto
dopo averlo fermato e, attraverso delle richieste, sapere se il file è in riproduzione o è stato
fermato in precedenza.
XNA
um 162 di 196
PROGETTI
SPACE INVADER
Creare le immagini con Paint: la navicella (ship), i nemici (enemy), i proiettili (cannonball) e
salvarle in formato PNG nel progetto Content cartella SPRITES.
I file audio: EXPLOSION.WAV, WEAPON.WAV, salvarli nella cartella AUDIO.
XNA
um 163 di 196
Creare la cartella FONT, clic con il tasto destro Aggiungi\Nuovo elemento….
Selezionare SpriteFont e nella casella Nome: scoreFont.spritefont.
File GAMEOBJECT.CS
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
namespace SpaceInvaderWin {
class GameObject {
// posizione X e Y
// velocità della navetta
// l’oggetto è da visualizzare?
public Rectangle ObjRectangle
{
// rettangolo dove è definito lo sprite
// costruttore che accetta una texture 2D
public GameObject(Texture2D _texture)
{
…………….
}
}
}
File GAME1.CS
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
namespace SpaceInvaderWin {
public class Game1 : Microsoft.Xna.Framework.Game {
// navetta
// velocità della navetta
// collezione di nemici
// numero massimo di nemici su schermo
// posizione dei nemici
// collection proiettili
// numero massimo di proiettili
// velocità dei proiettili
// effetti audio
// punteggio visualizzato a schermo
// posizione
}
protected override void Initialize()
XNA
um 164 di 196
{ base.Initialize(); }
protected override void LoadContent()
{ // inizializziamo la navetta
// posizione navetta
// assegnazione velocità
// inizializziamo i nemici
// dichiarazione dei nemici
// spostiamo a destra i nemici per evitare la sovrapposizione
}
// inizializziamo i proiettili
// dichiarazione dei proiettili
}
// inizializziamo gli effetti audio
// inizializziamo il fonts
}
protected override void UnloadContent()
// gestiamo l’input dell’utente
// gestiamo la classe Keyboard (PC) e la classe Gamepad (XBOX)
#if !XBOX
// cambiamo posizione alla navetta
// gestiamo lo sparo con il tasto spazio
// abbiamo premuto il tasto spazio and il tasto spazio è stato rilasciato
#endif
// evitiamo che la navetta esca dallo schermo
// ridisegniamo i nemici quando sono stati tutti colpiti
}
// modifichiamo la posizione del proiettile
private void updateCannonBalls() {
{ // se lo schermo non contiene il proiettile Alive = false
}
// gestione collisioni con i nemici
// il nemico è stato colpito perchè c’è stata una collisione
}
}
}
}
// visualizza il proiettile quando spariamo
private void fireCannonBall()
{ // effetto audio dello sparo
}
}
// visualizza gli elementi sullo schermo
protected override void Draw(GameTime gameTime)
{ // tra Begin ed End si mette tutto il codice per disegnare
// disegniamo la navetta
// disegniamo i nemici usando la lambda expression
// e è la variabile, maggiore uguale, condizione
// si entra nel ciclo solo per i nemici con Alive == true
}
// disegniamo i proiettili
}
// disegniamo il punteggio
}
}
XNA
um 165 di 196
}
La classe MathHelper contiene diversi metodi utili per fare calcoli matematici molto comuni
nei videogame.
Ad esempio, la classe contiene il metodo ToRadians che consente di trasformare l’angolo
in gradi calcolato nella proprietà Rotation, nell’angolo in radianti che è voluto dal metodo
Draw di SpriteBatch.
Migliorare il gioco ricordando che la grafica e la musica sono componenti
fondamentali per la riuscita di un videogame professionale.
Gli sparatutto utilizzano anche lo sfondo che si muove al di sotto del personaggio
principale e degli altri protagonisti del gioco: nemici e missili.
Per esempio, due sole immagini che si mostrano una dopo l’altra, di dimensioni
480X2048.
480 pixel è la larghezza dello schermo del dispositivo, mentre l’altezza di 2048 pixel è
maggiore di quella standard dello schermo di 800 pixel, quindi lo sfondo scorrerà in
altezza per un totale di 4096 pixel e poi ricomincerà dalla prima immagine.
XNA
um 166 di 196
SAVE THE PRINCESS
Il giocatore impersona un piccolo quadratino blu, il Principe Quadrazzurro.
Lo scopo del gioco è raggiungere la Principessa Quadrotta, posizionata in alto sullo
schermo, portarla in salvo dall’esercito dei Quadrossi.
La scalata sarà ostacolata da una vera e propria valanga di quadratini rossi che fanno
parte dell'Esercito dei Quadrossi.
PERSONAGGI
Principe Quadrazzurro
Il suo unico obiettivo è salvare la Principessa Quadrotta e sposarla.
Principessa Quadrotta
La più bella del Quadreame, rapita dal malvagio Dr. Q.
XNA
um 167 di 196
Esercito dei Quadrossi
Mercenari al servizio del Dr. Q, difendono con tenacia la gabbia della Principessa.
Dr. Q
Misterioso, astuto e malvagio, progetta sempre qualcosa di pericoloso.
COMANDI
Freccia Su/W
Freccia Giù/S
Freccia Sinistra/A
Freccia Destra/D
ESC
Muove Quadrazzurro verso l’alto.
Muove Quadrazzurro verso il basso.
Muove Quadrazzurro verso sinistra.
Muove Quadrazzurro verso destra.
Esce dal gioco.
FINE DEL GIOCO
Il gioco termina quando si salva la Principessa Quadrotta per un totale di 10 volte.
XNA
um 168 di 196
1. CREAZIONE E POSIZIONAMENTO DEGLI OGGETTI NEL CAMPO DA GIOCO
Dopo aver dichiarato le texture per le immagini utilizzate nel gioco e averle caricate nel
metodo LoadContent, creare un nuovo oggetto Font che serve per visualizzare il numero
del livello e delle collisioni nel corso del gioco.
Per le collisioni, creare dei Rectangle per i 3 componenti del gioco, l’Eroe, la Principessa e
i Nemici; servono per determinare l’area di collisione di un oggetto.
La creazione del Rectangle ha bisogno di 4 variabili: la posizione X, la posizione Y, la
larghezza e l’altezza; devono adattarsi alle dimensioni della texture.
Rectangle heroRectangle = new Rectangle((int)heroPosition.X, (int)heroPosition.Y,
heroTexture.Width, heroTexture.Height);
Rectangle princessRectangle = new Rectangle((int)princessPosition.X,
(int)princessPosition.Y, princessTexture.Width, princessTexture.Height);
Rectangle enemyRectangle = new Rectangle((int)enemyPositions[i].X,
(int)enemyPositions[i].Y, enemyTexture.Width, enemyTexture.Height);
Dopo aver creato i Rectangle, per gestire le collisioni basta la seguente istruzione.
if (heroRectangle.Intersects(enemyRectangle)) {
// istruzioni }
2. ALGORITMO PER LA CREAZIONE E LA CADUTA DEI NEMICI
La creazione dei nemici è gestita dalle seguenti istruzioni.
if (random.NextDouble() < enemySpawnProbability) {
Dove random rappresenta un numero casuale, enemySpawnProbability è la probabilità di
creazione dei nemici che aumenta durante il gioco e Window.ClientBounds.Width
rappresenta la zona del campo di gioco.
La caduta dei nemici, invece, è gestita dalle seguenti istruzioni.
for (int i = 0; i < enemyPositions.Count; i++) {
Dove enemyFallSpeed rappresenta la velocità di caduta dei blocchi.
Anche questa variabile aumenterà avanzando nel gioco.
3. VARIABILI BOOLEANE
Sono state utilizzate nello sviluppo del gioco per avere la possibilità di bloccare il gioco
durante l’introduzione e la fine della partita, oltre che per implementare il menu di pausa.
La più importante è la variabile theend che consente, tramite una semplice struttura di
controllo if, di fermare il gioco in più occasioni.
Per esempio, il disegno delle parti del gioco avviene solamente con theend = false, mentre
con theend = true, quando il livello è uguale a 10, sono eliminate dal campo le parti del
gioco per far spazio alla conclusione della partita.
Le altre variabili booleane utilizzate sono quelle per il menu di pausa e quelle per la
riproduzione dell'audio.
4. IMPLEMENTAZIONE DELLA LIBRERIA AUDIO
Per i file audio, non è disponibile nessun metodo per la loro gestione, come ad esempio la
possibilità di mettere in pausa una canzone.
Utilizzare XACT che permette la creazione di una libreria audio con i relativi metodi di
gestione.
XNA
um 169 di 196
XNA
um 170 di 196
XNA
um 171 di 196
MODULO 2
KINECT
Storia
SDK
SCANNER 3D
OPENKINECT
NITE
XNA
um 172 di 196
STORIA
INTRODUZIONE
È una periferica che sta rivoluzionando il mondo dell’interazione uomo-macchina.
È stato sviluppato da Rare, un’azienda di produzione di videogiochi inglese, fondata negli
anni ‘80 dai fratelli Tim e Chris Stamper, l’azienda si chiamava inizialmente “Ultimate Play
the Game”, specializzata in titoli per computer a 8 bit come Commodore 64.
Nel 1988 l’azienda cambia nome, diventando Rare e comincia la produzione di molti titoli
per console, in particolare per quelle Nintendo; proprio la casa giapponese a partire dal
1994 stringerà un accordo per la produzione di titoli in esclusiva.
Nel 2002, Rare è stata acquisita da Microsoft.
Primo annuncio all’E3 (Electronic Entertainment Expo) nel 2009, dove furono mostrati
alcuni giochi già abilitati a tale sistema d’interazione.
Nel novembre del 2010, questo dispositivo è approdato nei negozi.
La società israeliana PrimeSense ha sviluppato la tecnologia basata su una telecamera in
grado di misurare la distanza di oggetti e superfici in base ad una scansione effettuata
attraverso raggi infrarossi.
La tecnologia si chiama PrimeSensor e risolve il problema del calcolo della distanza in
maniera intuitiva: sono catturate 30 FPS, due immagini differenti da due telecamere VGA
con risoluzione 640X480, una a colori e un’altra nella quale è misurata la profondità della
scena ripresa, quest’ultima correda ogni pixel all’interno dello scatto catturato con un
valore che ne rappresenta la distanza misurata; le due immagini sono poi sovrapposte per
attribuire ai pixel colorati le rispettive profondità.
La misurazione della distanza avviene grazie alla sinergia di un proiettore a infrarossi,
posizionato sulla sinistra del dispositivo che genera una griglia di punti invisibili all’occhio
umano e di un sensore CMOS (Complementary Metal-Oxide Semiconductor)
monocromatico, posizionato a destra che cattura come l’ambiente riflette tali infrarossi e
valuta come tali informazioni ritornano ad essa.
Previa una calibrazione necessaria per far combaciare i pixel della telecamera di motion
capture con quella a colori, il dispositivo scatta una “fotografia” tridimensionale, misura il
tempo necessario perché i singoli raggi luminosi ritornino come avviene per il sonar,
dell’ambiente e cerca d’identificare quei componenti che possano essere parti del corpo
umano: è in grado di riconoscere fino a 6 persone all’interno della scena ripresa ma di
queste solo 2 sono utilizzate per l’analisi del movimento.
L’H/W che gestisce il tutto è un chip chiamato PS1080 progettato da PrimeSense ma le
operazioni di riconoscimento del corpo e del movimento non sono eseguite dal chip ma
sono delegate ad un middleware S/W, quello ufficiale PrimeSense si chiama NITE,
apposito installato all’interno della Xbox e che impegna la CPU meno del 10%.
Il campo di visuale della telecamera è di 58° sull’asse orizzontale, 70° obliquo e 45° su
quello verticale, quest’ultimo varia in un intervallo di 27°, sia in alto sia in basso, grazie al
sistema motorizzato presente alla base della periferica che consente di ruotare
meccanicamente la barra.
La distanza minima dichiarata per un suo corretto funzionamento è di 1.2 metri, mentre
quella massima di 3.5 metri, anche se il sensore, potenzialmente, sarebbe in grado di
XNA
um 173 di 196
arrivare a misurare movimenti posizionati fino a 6 metri.
In termini di libertà di movimento, PrimeSensor ha 307200 gradi di libertà, 640X480,
poiché per ogni punto è registrata la relativa distanza nello spazio.
Microsoft ha rilasciato 22 “campioni” di codice per Kinect con licenza Open Source
Apache.
Riguardano alcune specifiche che vanno dai comandi vocali, al riconoscimento facciale
fino alla rilevazione dei movimenti.
Kinect va oltre il videogioco.
 Applicazioni di danza e di pittura.
 Elaborazione d’immagini.
 Applicazioni mediche.
HARDWARE
Kinect è una periferica costituita da vari sensori e collegabile al PC via porta USB.
Comprende una camera VGA 640X480 che permette di catturare immagini a colori della
scena inquadrata ad una frequenza di 30 Hz.
È dotato di un sensore di profondità, in realtà è composto da una coppia di dispositivi: una
camera sensibile agli infrarossi e un proiettore infrarosso che permette a Kinect di
generare un’immagine di profondità in modalità QVGA (Quarter VGA) 320X240.
Un’immagine di profondità non è altro che un’immagine in cui a ogni pixel non è associato
un colore ma un valore di profondità, in altre parole la misura della distanza tra l’elemento
rappresentato dal pixel e il Kinect calcolato rispetto ad un sistema di assi di riferimento con
origine nel dispositivo stesso.
In sintesi, tanto più un elemento è distante dal Kinect, tanto più questo comparirà con un
valore di profondità elevato nell’immagine.
Ovviamente, anche le immagini di profondità sono accessibili tramite API, mediante uno
stream ad una frequenza di 30 Hz.
Il rilevamento della profondità si basa sulla proiezione sulla scena d’interesse di un pattern
di luce infrarossa, per mezzo del proiettore infrarosso, non visibile dall’occhio umano ma
rilevabile con dispositivi ad hoc.
Tale pattern illumina la scena ed è deformato adagiandosi sulle varie superfici.
La rilevazione della deformazione del pattern, da parte della camera a infrarossi permette
XNA
um 174 di 196
al Kinect di formulare una stima della distanza per ogni punto dell’immagine di profondità.
Questo tipo di tecnica è nota come approccio in luce strutturata.
Il terzo ed ultimo stream di dati fornito dal Kinect è generato da un array di 4 microfoni,
ADC (Analog to Digital Converter) a 24 bit, corredati da un sistema embedded di
cancellazione dell’eco e soppressione del rumore.
Infine, la base del kinect è dotata di un motore elettrico per ottimizzare l’orientamento
verticale inquadrato del Kinect, ±28º.
Kinect SDK DTW (Dynamic Time Warping) Gesture Recognition
È in grado di riconoscere il volto di una persona con ottima precisione ad una distanza di
un metro, tale precisione decresce con l’allontanamento dell’utente.
Inoltre, adopera i controlli vocali, riconoscimento del movimento delle mani e del volto.
Depth Image
I due sensori di profondità a infrarossi funzionano sul principio della stereo visione.
È una tecnica che consente di ottenere informazioni concernenti la profondità da una
coppia d’immagini provenienti da due telecamere che inquadrano la stessa scena da
differenti posizioni.
Avendo a disposizione le posizioni virtuali del punto captate dalle due telecamere, è
possibile, intersecando le due semirette dirette dalle telecamere ai rispettivi punti virtuali,
individuarne l’intersezione che rappresenta la posizione esatta del punto nello spazio.
Questo procedimento è equivalente a quello utilizzato dagli occhi per attribuire profondità
di campo all’immagine osservata.
Skeletal tracking
È in grado, sfruttando le immagini fornite dai suoi sensori, di stimare le posizioni di vari
punti del corpo umano, skeletal joint, di uno o al massimo due persone in piedi di fronte
alla periferica.
Date queste posizioni, è possibile identificare i movimenti e di conseguenza le gestures
che possono essere utilizzate per interagire con un’applicazione o con un videogioco.
Elenco degli skeletal joints, con i rispettivi nomi.
L’implementazione dello skeletal tracking è essenzialmente S/W ed è stata realizzata con
tecniche di machine learning che hanno individuato svariate pose tipiche del corpo umano
e riescono a riconoscere anche posizioni in cui parti del corpo ne nascondono delle altre.
I valori di profondità ottenuti dal Kinect esprimono distanze in millimetri con un minimo di
850 mm fino ad un massimo di 4000 mm.
Un valore pari a 0 indica che Kinect non è riuscita a stimare la profondità per quel punto in
particolare, ad esempio a causa di ombre o cattiva riflettività.
Molteplici possono essere gli utilizzi del Kinect.
In ambito medico è stata creata un’applicazione che permette la visualizzazione di
radiografie e tomografie senza l’impiego delle mani.
Nel supermercato, è stato applicato Kinect ad un carrello della spesa collegato ad un
computer touch screen, il carrello è capace di riconoscere i prodotti mano a mano che
sono inseriti, così da evitare i lunghi tempi di attesa alla cassa e spuntarli in automatico
dalla lista della spesa inserita prima, permettendo di fare il pagamento con carta di credito
XNA
um 175 di 196
senza pagare alla cassa.
SICUREZZA DEGLI OCCHI
L’utente, posizionato di fronte a Kinect, è investito continuamente da fasci d’infrarossi,
PrimeSense dichiara che il fascio LASER (Light Amplification by Stimulated Emission of
Radiation) adoperato è di classe 1 e rispetta le normative IEC (International
Electrotechnical Commission) 60825-1, è comunque sconsigliabile stazionare per troppo
tempo davanti a tale dispositivo e soprattutto senza muoversi.
WINDOWS 8.X
Nasce con le librerie per l’interfacciamento con Kinect.
Ci saranno applicazioni riguardanti la sicurezza, come ad esempio il riconoscimento
facciale automatico per il login o all’autorizzazione di processi in modalità amministratore.
2012
Grazie ad un’ottimizzazione del firmware Kinect si utilizza sul PC.
Presa USB più corta.
Adattatore cui attaccare altre prese USB al PC.
Raggio d’azione: 50 centimetri di distanza, si chiama Near Mode.
XNA
um 176 di 196
SDK
INTRODUZIONE
L’SDK ufficiale di Kinect offre API per la gestione degli stream di dati generati dalla
periferica e per la gestione dell’audio.
È possibile sfruttare le API sia in .NET sia in Visual C++.
Per verificare di aver installato correttamente il Kinect e il relativo SDK, controllare, dal
Pannello di controllo, nella sezione Hardware e suoni, la voce Audio se è presente il
Kinect con la dicitura Microphone Array.
Kinect è riconosciuto dal SO (Sistema Operativo) come una periferica di registrazione
audio ed è possibile utilizzarla anche con l’applicazione Registratore di suoni.
Tramite l’SDK è possibile accedere agli stream di dati della periferica con due modalità.
1. Polling: è necessario aprire lo stream d’interesse con un’apposita chiamata e
richiedere attivamente un frame specificando quanto tempo, in millisecondi, si è
disposti ad attendere; non appena il frame è disponibile o il tempo di attesa è terminato
il controllo ritorna al chiamante.
2. Eventi: è quello più facile, infatti, l’arrivo di un nuovo frame nello stream genera un
evento, al quale è possibile sottoscrivere un metodo handler incaricato di gestire
l’evento stesso.
La procedura d’inizializzazione dell’SDK comprende due passi distinti.
1. La creazione del Runtime.
2. L’inizializzazione con una o più opzioni che indicano quali stream si è interessati a
ricevere dalla periferica.
Esempio, si richiede l’inizializzazione per lo skeletal tracking e l’immagine di profondità.
// includere i namespace necessari al progetto
using Microsoft.Research.Kinect.Nui;
// creare il Runtime di Kinect
Runtime nui = new Runtime();
// inizializzare il Runtime
nui.Initialize(RuntimeOptions.UseSkeletalTracking | RuntimeOptions.UseDepth);
// codice ….
// cleanup
nui.Uninitialize();
Dopo il codice, bisogna deallocare le risorse acquisite dal Runtime chiamando il metodo
Uninitialize.
Per quanto riguarda le opzioni d’inizializzazione, è possibile passare più parametri al
metodo Initialize, mediante l’operatore pipe, in base a quanti e quali stream si è interessati
a ottenere dalla periferica.
L’elenco completo delle possibili opzioni è il seguente.
 UseColor: indica che si utilizza lo stream d’immagini dalla camera a colori.
 UseDepth: significa che si utilizza lo stream di profondità.
 UseDepthAndPlayerIndex: significa che si utilizza lo stream formattato di profondità, in
cui a ogni elemento di ciascun frame è indicato, oltre al valore di profondità, anche se
appartiene ad una delle due persone potenzialmente inquadrate dal Kinect o allo
sfondo, rispettivamente con i valori: 1, 2 e 0.
 UseSkeletalTracking: significa che si utilizza lo stream con le informazioni di Skeletal
Tracking.
XNA
um 177 di 196
ACQUISIRE GLI STREAM DI DATI DAI SENSORI
Progettare un’applicazione WPF (Windows Presentation Foundation) costituita da una
singola finestra che mostra a video i frame ottenuti in tempo reale dallo stream della
camera a colori e da quello di profondità.
Utilizzando la Casella degli strumenti, creare due elementi ti tipo Image di uguale
dimensione, affiancandoli al centro della finestra dell’applicazione WPF, dimensionato
come 350 di altezza per 800 di lunghezza.
File MAINWINDOW.XAML
<Window x:Class="CameraFundamentals.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="800" Loaded="Window_Loaded"
Closed="Window_Closed">
<Grid>
<Image Height="240" HorizontalAlignment="Left" Margin="32,46,0,0"
Name="image1" Stretch="Fill" VerticalAlignment="Top" Width="320" />
<Image Height="240" HorizontalAlignment="Left" Margin="372,46,0,0"
Name="image2" Stretch="Fill" VerticalAlignment="Top" Width="320" />
</Grid>
</Window>
Questo è tutto ciò che ci serve a livello d’interfaccia.
La classe che gestisce la finestra ereditata da Window deve includere le librerie di
XNA
um 178 di 196
supporto.
using Microsoft.Research.Kinect.Nui;
using Coding4Fun.Kinect.Wpf;
Creare una variabile di tipo Runtime.
Runtime nui = new Runtime();
Modificare il metodo Window_Loaded che è stato creato automaticamente da Visual
Studio e che è invocato all’avvio dell’applicazione.
private void Window_Loaded(object sender, RoutedEventArgs e) {
// setup event handlers
nui.VideoFrameReady += new
EventHandler<ImageFrameReadyEventArgs>(nui_VideoFrameReady);
nui.DepthFrameReady += new
EventHandler<ImageFrameReadyEventArgs>(nui_DepthFrameReady);
// inizializzazione immagini Color & Depth
nui.Initialize(RuntimeOptions.UseColor | RuntimeOptions.UseDepth);
// apertura streams
nui.VideoStream.Open(ImageStreamType.Video, 2,
ImageResolution.Resolution640x480, ImageType.Color);
nui.DepthStream.Open(ImageStreamType.Depth, 2,
ImageResolution.Resolution320x240, ImageType.Depth);
}
Le istruzioni di apertura degli stream richiedono vari parametri che indicano la tipologia
delle immagini che si ricevono.
Si deve indicare esplicitamente che gli stream sono di tipo Video di tipo Depth, con un
buffer di dimensione due, con le appropriate risoluzioni di 640X480 e di 320X240.
L’immagine riportata da ogni frame dev’essere rispettivamente di tipo Color e Depth.
Il codice ha due metodi: nui_VideoFrameReady e nui_DepthFrameReady, utilizzati per
gestire ogni nuovo frame che è generato dagli stream.
void nui_DepthFrameReady(object sender, ImageFrameReadyEventArgs e)
{
// Coding4Fun extension method on ImageFrame class
image2.Source = e.ImageFrame.ToBitmapSource();
}
Il metodo permette di mappare direttamente su un’Image il contenuto del frame attraverso
il metodo ToBitMapSource.
In altre parole, tutto quello che si deve fare è accedere al parametro dell’evento che è
passato ed accedere al suo campo ImageFrame, per poi chiamare il metodo
ToBitMapSource ed assegnarne il risultato alla proprietà Source di una delle due immagini
dell’interfaccia WPF, in questo caso Image2.
L’immagine di profondità non è un’immagine vera e propria ma contiene, associati ad ogni
pixel, i valori di distanza dell’elemento rappresentato dal pixel rispetto al Kinect.
Per mostrare quest’immagine a video, si deve darle una rappresentazione grafica.
In questo caso, Condig4Fun mappa automaticamente i valori di profondità contenuti
nell’immagine su una scala di grigi, in modo da ottenere un’immagine in bianco e nero, in
cui gli elementi con minor valore di profondità avranno un colore tendente al bianco,
mentre gli elementi nell’immagine con un elevato valore di profondità, in pratica lontani
rispetto al Kinect, avranno un valore tendente linearmente al nero.
XNA
um 179 di 196
void nui_VideoFrameReady(object sender, ImageFrameReadyEventArgs e)
{
// creazione manuale BitmapSource
PlanarImage imageData = e.ImageFrame.Image;
image1.Source = BitmapSource.Create(imageData.Width, imageData.Height, 96,
96, PixelFormats.Bgr32, null, imageData.Bits, imageData.Width *
imageData.BytesPerPixel);
}
Il secondo metodo trasforma a mano l’immagine di profondità senza usare le librerie di
CODING4FUN.
Salvare in una variabile locale l’immagine del frame che è di tipo PlanarImage e creare
una BitMapSource da associare ad Image1.
In questo caso ci sono svariati parametri da passare, come la dimensione in larghezza e
altezza, il formato e i bit veri propri dell’immagine che sono contenuti in imageData.Bits.
Il parametro con valore null è la palette che in questo caso non interessa specificare, i due
parametri con valore 96 rappresentano invece i DPI (Dot Per Inch) rispettivamente lungo
l’ascisse e l’ordinata dell’immagine.
L’ultimo parametro rappresenta la dimensione totale dell’immagine, ovvero lo stride,
comprendente eventuale padding, è il prodotto di larghezza per altezza.
Inserire, nel metodo pregenerato Window_Closed, la chiamata a nui.Uninitialize() in modo
da rilasciare le risorse in fase di chiusura dell’applicazione.
private void Window_Closed(object sender, EventArgs e)
{ nui.Uninitialize(); }
Nel caso si volesse ottenere il valore effettivo della distanza di un certo elemento presente
nella matrice di profondità che in questo caso non si è manipolata perché gestita
direttamente dall’extension method di CODING4FUN, avremmo dovuto effettuare una
trasformazione.
La matrice di profondità è, infatti, codificata con 2 byte per pixel e, per ottenere il valore di
profondità, è necessario effettuare un bitshift del secondo byte di di 8.
Esempio, farlo sul primo pixel dell’immagine di profondità.
int Distance = (int)(imageData.Bits[0] | imageData.Bits[1] << 8);
Nel caso si dovesse fare su tutta l’immagine, basta inserire l’istruzione in un ciclo.
Nel caso si configuri lo stream di profondità per utilizzare, invece di una ImageType.Depth,
una ImageType.DepthAndPlayerIndex, la trasformazione necessaria per ottenere il valore
di distanza è diversa; richiede infatti 2 shift su entrambi i 2 byte che contengono
l’informazione di profondità.
int Distance = (int) (imageData.Bits[0] >> 3 | image Data.Bits[1] << 5);
XNA
um 180 di 196
KINECT SKELETAL TRACKING
Creare un nuovo progetto in Visual Studio, scegliendo un’applicazione WPF.
Inserire all’interno della finestra principale, sempre di 640X480, 3 immagini,
rispettivamente head, left e rigth e popolare le loro proprietà Source con 3 file immagini.
Creare e inizializzare il Runtime di Kinect con l’eccezione di specificare come opzione
RuntimeOptions.UseSkeletalTracking.
Specificare i seguenti parametri per rendere più fluido il processo di tracking.
private void Window_Loaded(object sender, RoutedEventArgs e)
{
// inizializzazione Kinect
nui.Initialize(RuntimeOptions.UseSkeletalTracking);
#region TransformSmooth
nui.SkeletonEngine.TransformSmooth = true;
var parameters = new TransformSmoothParameters {
Smoothing = 0.75f, Correction = 0.0f, Prediction = 0.0f, JitterRadius = 0.05f,
MaxDeviationRadius = 0.04f
};
nui.SkeletonEngine.SmoothParameters = parameters;
#endregion
// registrazione event handler
nui.SkeletonFrameReady += new
EventHandler<SkeletonFrameReadyEventArgs>(nui_SkeletonFrameReady);
}
void nui_SkeletonFrameReady(object sender, SkeletonFrameReadyEventArgs e)
{
SkeletonFrame allSkeletons = e.SkeletonFrame;
// seleziono primo skeleton
SkeletonData skeleton = (from s in allSkeletons.Skeletons
where s.TrackingState == SkeletonTrackingState.Tracked
select s).FirstOrDefault();
if(skeleton != null) {
Joint leftHand = skeleton.Joints[JointID.HandLeft].ScaleTo(640, 480, .5f, .5f);
XNA
um 181 di 196
Joint rightHand = skeleton.Joints[JointID.HandRight].ScaleTo(640, 480, .5f, .5f);
Joint jointHead = skeleton.Joints[JointID.Head].ScaleTo(640, 480, .5f, .5f);
// posiziono le immagini nell’interfaccia nella posizione in cui sono le mani e la testa
SetPosition(head, jointHead);
SetPosition(left, leftHand);
SetPosition(right, rightHand);
}
}
Il codice accede al parametro passato all’event handler, in particolare alla proprietà
SkeletonFrame.
Da tale proprietà estrae i dati del primo degli skeleton, in altre parole la prima persona
riconosciuta da Kinect che abbia stato Tracked tramite una query LINQ (Language
INtegrated Query).
Nel caso non ce ne siano, la variabile skeleton avrà valore null e l’event handler non farà
nulla.
In caso contrario, si accede ad un dizionario che contiene tutte le informazioni relative alle
posizioni dei joint rilevati da Kinect.
In particolare, interessa conoscere la posizione della testa e delle mani e proprio su questi
dati si applica il metodo ScaleTo, i dalla libreria di CODING4FUN.
Questo metodo scala i valori su di un range 640X480 per adattarli alle dimensioni della
finestra.
Il metodo seguente posiziona sull’interfaccia WPF le 3 immagini, rispettivamente nelle 3
posizioni scalate dei joint.
Queste operazioni sono ripetute per ogni nuovo frame dello stream e di conseguenza si
avrà la sensazione che le 3 immagini si muovano all’interno dell’interfaccia fluidamente,
seguendo i movimenti della testa e delle mani.
private void SetPosition(FrameworkElement element, Joint joint)
{
Canvas.SetLeft(element, joint.Position.X);
Canvas.SetTop(element, joint.Position.Y);
}
XNA
um 182 di 196
SCANNER 3D
INTRODUZIONE
Basta sfruttare la tecnologia di Skeletal tracking per realizzare le scansioni tridimensionali
degli oggetti inquadrati.
Le mappe di profondità fornite da Kinect si prestano a essere interpretate come Point
Cloud: ogni singolo punto del pattern infrarosso diffuso sulla scena fornisce le proprie
coordinate spaziali.
Anche se il formato puntiforme può essere visualizzato direttamente, non sempre è
usabile dai S/W di elaborazione 3D che utilizzano le mesh.
È possibile ottenere le prime scansioni 3D usando le applicazioni installate tramite SDK e
accessibili dal DTB (Developer Toolkit Browser).
Esempi.
 Kinect Fusion Basic e Color: permette di visualizzare la ricostruzione 3D della scena
inquadrata.
 Kinect Fusion Explorer, Multistatic Camera e Head Scanning: specifico per i volti,
permette di salvare i rilievi tridimensionali nei formati PLY, OBJ e STL.
SCANSIONE
H/W: Kinect.
S/W: RGBDEMO.EXE.
L’applicazione RGB-RECONSTRUCTOR.EXE fornisce la point cloud della scena
ricostruita in base al movimento della Kinect.
L’applicazione RGB-SCANTOPVIEW.EXE isola i modelli 3D dallo sfondo, ne calcola il
volume e permette di salvare il tutto.
Bisogna evitare le zone d’ombra dovute al disallineamento di fonte infrarossa e sensore.
Esaminare la scansione ottenuta con un S/W 3D, per esempio MESHLAB che fornisce sia
gli strumenti per visualizzare in 3D la mesh ottenuta sia per correggerla.
L’applicazione RGB-SCAN-MARKERS.EXE salva le scansioni in formato point cloud e la
mesh poligonale si ottiene ricostruendo i punti con MESHLAB.
Dopo la ricostruzione possono essere presenti dei buchi nella mesh, sempre con
MESHLAB correggere i difetti di scansione.
XNA
um 183 di 196
OPENKINECT
INTRODUZIONE
Appena Kinect è arrivato sul mercato, ha avuto inizio una gara a chi sarebbe riuscito prima
degli altri a effettuare reversing su tale dispositivo.
Dopo meno di una settimana Héctor Martín è riuscito a sviluppare dei driver per Linux in
grado di accedere ai dati inviati dalla telecamera a colori e a quella “di profondità” e di
mostrarli a schermo adoperando OpenGL (Open Graphics Library).
Non è hacking poiché non c’è stata alcuna “rottura” di codici o protezioni, è stato reverse
engineering.
Héctor ha carpito il significato di ogni singolo byte che il dispositivo inviava tramite USB,
operazione non possibile se Microsoft non avesse volutamente deciso di omettere
qualunque tipo di protezione nei flussi di comunicazione tra Kinect e console.
È così iniziato un processo di reingegnerizzazione per la realizzazione di una serie di
driver multipiattaforma.
Oltre ai driver Windows, Apple e Linux sono stati realizzati wrapper e librerie creati in
diversi linguaggi di programmazione che consentono di programmare il dispositivo.
Esistono librerie per i seguenti linguaggi di programmazione.
 Python.
 C Synchronous.
 ActionScript, richiede l’esecuzione di un mini server realizzato in linguaggio C.
 Visual C++/Visual C#.
 Java JNI (Java Native Interface).
 JavaScript.
 Common LISP (List Processor).
 OpenCV.
Queste librerie, sono, però, limitate a fornire le informazioni ricevute da Kinect tramite
USB: immagini e suoni.
Non forniscono nessuna delle funzionalità presenti nel S/W realizzato da Microsoft e
PrimeSense, manca il riconoscimento del volto, dello scheletro, il filtraggio del suono e
l’accelerazione tramite GPU.
Gli ingegneri del MIT (Massachusetts Institute of Technology) hanno già realizzato un
prototipo di un’interfaccia multimediale controllata con i movimenti delle mani.
http://www.freenect.com/kinect-minorityreport
INSTALLAZIONE
Per iniziare a sviluppare con OpenKinect è necessario effettuare il download dei driver dal
repository posizionato nel sito GitHub chiamato libfreenect, ci sono le versioni per i 3 SO:
Windows, Apple e Linux.
All’interno dell’archivio trovano posto documentazione tecnica, wrapper, sorgenti,
strumenti di testing e driver.
La procedura da seguire per utilizzare Kinect su Windows è la seguente.
Quando è collegato, è identificato come tre componenti H/W: telecamera, motore e audio,
riconosciuto come Unknow Device perché non sono installati i driver.
XNA
um 184 di 196
Installarli manualmente, in Impostazioni di sistema avanzate, scheda Hardware, fare
clic su Gestioni dispositivi e selezionare la periferica con il punto esclamativo,
visualizzare le proprietà con il tasto destro e selezionando la voce Aggiornamento
software driver….
Per ognuna delle tre periferiche è necessario ripetere la procedura, i driver si trovano
all’interno della cartella PLATFORM della libreria.
Alla fine, nell’elenco delle periferiche si hanno le seguenti voci: Xbox NUI (Natural User
Interface) Audio, Xbox NUI Camera, Xbox NUI Motor.
Per verificare l’installazione e testare i driver, aprire un progetto Visual Studio in Visual C#.
File KINECTLEDSTATUS.CS
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace OpenKinect {
public enum KinectLEDStatus {
Off = 0x0,
Green = 0x1,
Red = 0x2,
XNA
um 185 di 196
Yellow = 0x3,
BlinkingYellow = 0x4,
BlinkingGreen = 0x5,
AlternateRedYellow = 0x6,
AlternateRedGreen = 0x7
}
}
File KINECTMOTOR.CS
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using LibUsbDotNet.Main;
using LibUsbDotNet;
namespace OpenKinect {
public class KinectMotor {
#region Fields
private static UsbDevice MyUsbDevice;
private static UsbDeviceFinder MyUsbFinder;
#endregion
#region Constructors
public KinectMotor()
{ InitDevice();}
#endregion
#region Public Methods
/// <summary>
/// Always returns 0x22 (34) so far
/// </summary>
/// <returns></returns>
public ushort GetInitStatus()
{ UsbSetupPacket setup = new UsbSetupPacket(0xC0, 0x10, 0x0, 0x0, 0x1);
int len = 0;
byte[] buf = new byte[1];
MyUsbDevice.ControlTransfer(ref setup, buf, (ushort)buf.Length, out len);
return buf[0];
}
public void SetLED(KinectLEDStatus status)
{ UsbSetupPacket setup = new UsbSetupPacket(0x40, 0x06, (ushort)status, 0x0,
0x0);
int len = 0;
MyUsbDevice.ControlTransfer(ref setup, IntPtr.Zero, 0, out len);
}
public void SetTilt(sbyte tiltValue)
{ if (!MyUsbDevice.IsOpen)
InitDevice();
ushort mappedValue = (ushort)(0xff00 | (byte)tiltValue);
UsbSetupPacket setup=new UsbSetupPacket(0x40,0x31,mappedValue, 0x0, 0x0);
int len = 0;
MyUsbDevice.ControlTransfer(ref setup, IntPtr.Zero, 0, out len);
}
#endregion
#region Private Methods
private static void InitDevice()
XNA
um 186 di 196
{ MyUsbFinder = new UsbDeviceFinder(0x045E, 0x02B0);
MyUsbDevice = UsbDevice.OpenUsbDevice(MyUsbFinder);
// If the device is open and ready
if (MyUsbDevice == null) throw new Exception("Device Not Found.");
// If this is a "whole" usb device (libusb-win32, linux libusb)
// it will have an IUsbDevice interface. If not (WinUSB) the
// variable will be null indicating this is an interface of a device.
IUsbDevice wholeUsbDevice = MyUsbDevice as IUsbDevice;
if (!ReferenceEquals(wholeUsbDevice, null)) {
// This is a "whole" USB device. Before it can be used,
// the desired configuration and interface must be selected.
// Select config #1
wholeUsbDevice.SetConfiguration(1);
// Claim interface #0.
wholeUsbDevice.ClaimInterface(0);
}
}
#endregion
}
}
File OPENKINECTCONSOLE.CS
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using LibUsbDotNet.Main;
using LibUsbDotNet;
using OpenKinect;
using System.Threading;
namespace OpenKinectConsole {
class OpenKinectConsole {
static void Usage()
{ Console.WriteLine(
"Usage:\n " +
System.IO.Path.GetFileName(Environment.GetCommandLineArgs()[0]) +
" [--horizon | (pos)]"
);
}
static void Main(string[] args)
{ KinectMotor motor = new KinectMotor();
if( args.Length ==1 ) {
if( args[0] == "--horizon" ) {
// Demonstrate horizon following
Console.WriteLine("Press any key to exit");
while(Console.KeyAvailable == false) {
motor.SetTilt(0);
Thread.Sleep(TimeSpan.FromMilliseconds(100));
}
}
else {
// User defined value
sbyte pos = sbyte.Parse(args[0]);
motor.SetTilt(pos);
XNA
um 187 di 196
}
}
else {
Usage();
Console.WriteLine("Demo sequence started");
// Sample Sequence
ExerciseMotor(motor,+50);
ExerciseMotor(motor,-50);
ExerciseMotor(motor,+60);
ExerciseMotor(motor,-60);
ExerciseMotor(motor, 0);
}
Console.WriteLine("done");
}
private static void ExerciseMotor(KinectMotor motor, sbyte pos)
{ motor.SetLED(KinectLEDStatus.AlternateRedYellow);
motor.SetTilt(pos);
Thread.Sleep(TimeSpan.FromSeconds(2));
motor.SetLED(KinectLEDStatus.Green);
}
}
}
XNA
um 188 di 196
NITE
INTRODUZIONE
PrimeSense ha fondato un’organizzazione no-profit chiamata OpenNI (Open Natural
Interaction) il cui intento è quello di agevolare lo sviluppo e promuovere la realizzazione di
una serie di tecnologie certificate per l’interazione uomo-macchina: H/W, S/W e
middleware compatibili tra loro e con lo scopo di consentire una comunicazione
trasparente adoperando standard ben definiti.
OpenNI fornisce un’API per la realizzazione di applicazioni che necessitano di accedere
alle risorse utilizzate per supportare l’interazione uomo-macchina.
Consente di gestire la comunicazione tra dispositivi H/W di “basso” livello come sensori
visivi e acustici ma anche tra differenti middleware.
Non è perciò legato solamente a Kinect, bensì si pone come una soluzione S/W adatta ad
un utilizzo con qualunque tipo di tecnologia di qualsiasi tipo di complessità.
È quindi un livello di astrazione che consente di nascondere ai livelli superiori e
parallelamente tra i moduli interni, la complessità tecnologica dei singoli sensori.
Il middleware NITE, realizzato da PrimeSense; si appoggia su OpenNI e consente di
fornire agli sviluppatori un sistema di riconoscimento gesti e d’identificazione dell’utente.
Perché optare per NITE invece di continuare a operare con i driver Open Source?
Nel caso dei driver Open Source se si sente la necessità di avere pieno controllo su una
certa funzionalità e si hanno le giuste conoscenze si può modificarli in qualunque
momento e installando i driver si ha accesso completo a Kinect.
Optando per NITE si sale di astrazione, poiché è una tecnologia mirata per il supporto
specifico all’interazione attraverso l’utilizzo delle mani con tutti i dispositivi che adoperano
il sensore realizzato da PrimeSense; visto che tale sensore è solo uno dei tanti
componenti di Kinect, il driver fornito non è compatibile con la soluzione Microsoft, sarà
sufficiente però adoperare un driver per Kinect compatibile con NITE per risolvere
immediatamente tale limitazione.
OpenNI è il livello più basso per lo sviluppo di applicazioni d’interazione uomo-macchina,
mentre NITE è un toolbox che consente d’integrare nelle proprie applicazioni l’interazione
con i sensori PrimeSense adoperando un’API disponibile in diversi linguaggi.
XNA
um 189 di 196
STRUTTURA
Per avviare una qualunque identificazione del movimento è sempre necessario effettuare
una procedura d’identificazione, più propriamente di calibrazione, per esempio della mano,
operazione che prende il nome di focus gesture, subito dopo si avrà accesso ai dati
relativi ai suoi movimenti, per la precisione ai punti ad essa associati.
Esistono diverse modalità per eseguire la focus gesture, solamente due, però,
garantiscono la massima precisione di tracking.
1. Click: l’utente con la mano aperta e il palmo in direzione della telecamera muove la
mano verso la telecamera e torna indietro.
2. Wave: l’utente scuote la mano come avviene per un normale saluto almeno 5 volte.
Esistono poi tre procedure che non sono affidabili per quanto riguarda la precisione e il cui
scopo è più quello di essere invocate appena è identificato un determinato movimento.
1. Swipe left/right: spostamento della mano lateralmente rispetto alla telecamera.
2. Raise hand delegate: l’utente alza la mano verso l’altro.
3. Hand candidate moved: l’utente muove la mano in qualunque direzione.
Concetti fondamentali necessari per realizzare un progetto che adoperi NITE.
Sessione
Questo contesto rappresenta un stato di NITE durante il quale i movimenti dei punti della
mano sono analizzati costantemente, tali punti sono identificati da degli ID univoci e
persistenti nel tempo.
XNA
um 190 di 196
Per ogni sessione esistono tre possibili configurazioni.
1. not in session che indica che non è stata ancora avviata la procedura d’identificazione
della mano, focus gesture.
2. in session: successiva alla fase di focus gesture e durante la quale avviene il tracking
dei movimenti della mano.
3. quick refocus: che indica una fase transitoria all’interno della chiamata in session,
indica uno stato d’indecisione perché non è più possibile identificare la mano e spetta
al programmatore decidere se interrompere il tracking dopo un intervallo di non
ricezione di tali dati, oppure ripristinarlo adoperando un’ulteriore procedura di
reidentificazione della mano, quick refocus gesture.
La gestione di questi possibili stati avviene attraverso la classe chiamata
XnVSessionManager.
Oltre ai movimenti riconosciuti da NITE nativamente è possibile definirne dei propri
realizzando delle istanze della classe XnVGesture e inviandole a XnVSessionManager.
Controlli
Sono oggetti che ricevono i movimenti dei punti della mano generalmente dal session
manager, XnVSessionManager, cercano d’identificare un tipo di movimento di tale
estremità del corpo e, in caso affermativo, richiamano uno o più metodi definiti dal
programmatore: eventi.
I controlli possono attendere il verificarsi di eventi quali la comparsa di un nuovo punto
della mano, di un suo movimento e della sua scomparsa; i controlli sono quindi dei
listener, attendono di ricevere informazioni dalla fonte dati a ogni fotogramma e
reagiscono immediatamente.
NITE adopera due termini per identificarli: Detector e SelectableSlider.
Elenco di quelli disponibili.
 Push Detector: cerca d’identificare un movimento avanti-indietro nella direzione della
telecamera, nella pratica è un pugno.
 Swipe Detector: cerca d’identificare un movimento laterale della mano.
 Steady Detector: cerca d’identificare la staticità prolungata di una mano.
 Wave Detector: cerca d’identificare un movimento in cui avviene per ben 4 volte un
cambio di direzione, nella pratica è il gesto del comune saluto.
 CircleDetector: cerca d’identificare un movimento circolare in senso orario o antiorario
completo della mano.
 SelectableSlider1D: cerca d’identificare un movimento lungo uno dei tre assi, è
adoperato, ad esempio, per spostarsi all’interno delle voci di un menu contestuale.
 SelectableSlider2D: cerca d’identificare un movimento prima nel piano X-Y, parallelo
all’immagine ripresa dalla telecamera e poi una selezione effettuando un movimento
lungo l’asse Z, come avviene per il push.
I controlli descritti sono quelli forniti da NITE ma è possibile realizzarne dei propri allo
scopo di ampliare il sistema d’interazione che si desidera realizzare.
Il sistema di assi adoperato in NITE è il seguente.
 X: parallelo alla direzione dei movimenti della mano sinistra-destra guardando la
telecamera, è la direzione che è seguita dai movimenti di tipo swipe.
 Y: parallelo alla direzione dei movimenti della mano sopra-sotto guardando la
telecamera.
 Z: parallelo alla direzione dei movimenti della mano vicino-lontano guardando la
telecamera, è la direzione che è seguita dai movimenti di tipo push.
NITE non invia tutti i punti identificati ai controlli ma solo uno, chiamato punto primario che
è il primo punto che il sistema identifica con la telecamera; nel caso tale informazione
venisse meno, uno degli altri punti, se disponibili, prenderà il suo posto come primario.
Il punto primario ha quindi un’importanza fondamentale per il tracking dei movimenti e a
esso sono associati 4 specifici eventi invece dei 3 adoperati per gli altri punti.
Sono entrata nel campo della telecamera, spostamento, uscita dal campo della
XNA
um 191 di 196
telecamera: per il punto primario si ricevono “creazione del punto primario”,
“aggiornamento”, “distruzione” e “sostituzione con un altro punto”.
 Controllo di flusso: poiché è possibile attivare più controlli e metterli in attesa, è
disponibile una serie di strumenti per decidere con quale modalità sono preprocessate
le informazioni inviate dal session manager a questi controlli e quindi definire un
sistema decisionale, il controllo del flusso è possibile con i seguenti oggetti.
 Flow router: i dati sono inviati ad un unico controllo ridefinibile.
 Broadcaster: i dati sono inviati parallelamente ad n controlli.
Esistono poi due oggetti che, a monte dell’invio al controllo, consentono di filtrare e
applicare particolari operazioni sui dati ricevuti a monte dell’analisi.
Point area
Definendo un’area tridimensionale nello spazio di movimento ripreso dalla telecamera, è
possibile ignorare tutti quei movimenti che non sono contenuti al suo interno.
Point denoiser
Questo oggetto riduce la variazione del movimento dei punti consentendo di ridurre il
tremolio dovuto a normali microspostamenti della mano nei diversi assi.
Virtual coordinates
Identifica un piano parallelo al corpo dell’utente e fa in modo di proiettare tutti i punti
identificati su di esso.
Messaggi (messages)
La comunicazione tra i diversi controlli avviene interamente adoperando messaggi
appartenenti alla classe XmVMessage, al cui interno trovano posto tutte le informazioni
necessarie per una corretta analisi dell’informazione inviata.
XNA
um 192 di 196
POSIZIONE T-POSE
È il nome della posizione che l’utente deve assumere per consentire al framework
d’identificare tutti i singoli punti che compongono il suo corpo, chiamati joint, articolazioni.
Consiste nel piegare le braccia ad angolo retto direzionando i pugni verso l’alto e
attendendo per circa quattro o cinque secondi in tale posizione per consentire la
generazione dello scheletro.
La distanza per effettuarla è di circa un metro e mezzo dalla telecamera; al termine della tpose è effettuato in real-time un mapping tra modello tridimensionale e reale.
In molte applicazioni di controllo di modelli 3D, questa posizione è necessariamente la
prima da effettuare prima di adoperare il S/W, è la controparte più complessa delle focus
gesture della mano, applicata ad un modello più complesso quale è il corpo umano.
Dopo la fase di calibrazione si potrà accedere alle coordinate dei singoli punti che
compongono il corpo umano, si parla delle articolazioni gomito, ginocchio, caviglia, collo,
mano, testa e torso.
INSTALLAZIONE
Installare i driver presenti nel seguente repository.
https://github.com/avin2/SensorKinect/tree/master
Per testare il loro funzionamento è necessario installare OpenNI.
http://www.openni.org/downloadfiles/openni-binaries/21-stable
Installare NITE, per avere la libreria MANAGEDNITE.DLL che s’interfaccia con il
dispositivo.
http://www.openni.org/downloadfiles/openni-compliant-middleware-binaries/34-stable
XNA
um 193 di 196
Esempio, progettare un S/W di controllo remoto del cursore del mouse: muovere il
puntatore con una mano e interagire con la GUI effettuando il doppio clic in base ad un
movimento della mano, una gesture.
Aprire Visual Studio e creare un progetto in linguaggio Visual C# di tipo WPF.
Inserire la libreria MANAGEDNITE.DLL.
L’interfaccia grafica del progetto ha al suo interno un’immagine di sfondo, due immagini
con relativi testi esplicativi contenuti in altrettanti “contenitori”, due stackpanels che
mostrano lo stato del S/W, la cui visibiltà dipende se si è in fase di tracking del movimento
oppure in attesa di avviare il tracking e un’ultima immagine che seguirà il puntatore del
mouse che controlleremo dopo una fase di calibrazione.
La visibilità dei due box contenenti le immagini e i testi si basa sulla rilevazione del cambio
di stato di due variabili booleane, HandDetected e Refocus.
L’immagine con la mano cambia la sua posizione e visibilità in base alla variazione di una
variabile, quella che gestirà le mani rilevate.
File HAND.CS
Inizializza le strutture dati e si pone in attesa di ricevere eventi dal middleware NITE,
adopera un movimento di wave per avviare il tracking della mano; la classe di supporto,
Hand, permette di tenere traccia e aggiornare le posizioni delle mani identificate, un
movimento di push, movimento della mano verso lo schermo, permette di simulare il clic
del tasto sinistro del mouse, sia questo sia lo spostamento del cursore, avvengono
attraverso chiamate a librerie native del sistema.
Si rappresenta ogni mano identificata con un’istanza di una classe chiamata Hand, al cui
interno trovano posto le coordinate rilevate al punto che la rappresenta e l’ID che il
middleware comunica durante la fase di prima rilevazione della stessa.
using System;
using GalaSoft.MvvmLight;
namespace kinectNite {
public class Hand : ViewModelBase {
private int _id = 0;
public int ID
{ get { return _id; }
XNA
um 194 di 196
set {
if (_id == value)
return;
var oldValue = _id;
_id = value;
// Update bindings, no broadcast
RaisePropertyChanged("ID");
}
}
private double _top = 0;
public double Top
{ get { return _top; }
set {
if (_top == value)
return;
var oldValue = _top;
_top = value;
// Update bindings, no broadcast
RaisePropertyChanged("Top");
}
}
private double _left = 0;
public double Left
{ get { return _left; }
set {
if (_left == value)
return;
var oldValue = _left;
_left = value;
// Update bindings, no broadcast
RaisePropertyChanged("Left");
}
}
}
}
File CURSORHANDLER.CS
Per muovere il puntatore del mouse del PC è necessario accedere direttamente a librerie
native del sistema, adoperando INTEROPSERVICES e importando diversi metodi forniti
nella libreria USER32.DLL relativi al controllo del desktop e del mouse.
I due metodi SendMouseLeftClick e MoveMouseTo non fanno altro che agire come
intermediari tra .NET e le librerie native inviando il clic sinistro del mouse e spostando il
cursore.
Questa classe è adoperata in risposta ai movimenti della mano.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.Windows;
namespace kinectNite {
internal class CursorHandler {
#region Constants
XNA
um 195 di 196
private const UInt32 MouseEventfLeftDown = 0x0002;
private const UInt32 MouseEventfLeftUp = 0x0004;
// fattori di conversione tra coordinate
private const double ScreenXConv = 64.0; // 65535 / 1024
private const double ScreenYConv = 85.3; // 65535 / 768
#endregion
#region DllImports
[DllImport("user32.dll")]
private static extern void mouse_event(UInt32 dwFlags, UInt32 dx, UInt32 dy, UInt32
dwData, IntPtr dwExtraInfo);
[DllImport("user32.dll")]
public static extern IntPtr GetDesktopWindow();
[DllImport("user32.dll")]
public static extern IntPtr GetWindowDC(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern IntPtr ReleaseDC(IntPtr hWnd, IntPtr hDC);
[DllImport("user32.dll")]
public static extern void mouse_event(uint dwFlags, long dx,long dy, uint dwData,
IntPtr dwExtraInfo);
[DllImport("user32.dll")]
public static extern void keybd_event(byte bVk, byte bScan,uint dwFlags, IntPtr
dwExtraInfo);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool SetCursorPos(int x, int y);
#endregion
/// <summary>
/// click del pulsante sinistro del mouse
/// </summary>
/// <param name="location">Posizione richiesta.</param>
public static void SendMouseLeftClick(Point location)
{ MoveMouseTo((int)location.X, (int)location.Y);
mouse_event(MouseEventfLeftDown, 0, 0, 0, new IntPtr());
mouse_event(MouseEventfLeftUp, 0, 0, 0, new IntPtr());
}
/// <summary>
/// spostamento del mouse nella posizione richiesta
/// </summary>
/// <param name="x">x.</param>
/// <param name="y">y.</param>
public static void MoveMouseTo(int x, int y)
{ x = (int)(x * ScreenXConv);
y = (int)(y * ScreenYConv);
SetCursorPos(x, y);
}
}
}
XNA
um 196 di 196
UBERTINI MASSIMO
http://www.ubertini.it
[email protected]
Dip. Informatica Industriale
I.T.I.S. "Giacomo Fauser"
Via Ricci, 14
28100 Novara Italy
tel. +39 0321482411
fax +39 0321482444
http://www.fauser.edu
[email protected]