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> </Start> <End>~</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]
© Copyright 2024 ExpyDoc