| VS85 Team's profileXNALearnersBlogListsGuestbook | Help |
|
|
2/17/2009 XNA@DSI - Ingegneria del SoftwareQuest’anno al corso di Ingegneria del Software del dipartimento di Informatica dell’Universitá Ca’ Foscari di Venezia é stato svolto un interessante esperimento: affiancare al classico lavoro teorico di produzione di documenti tecnici sulla creazione di un progetto informatico anche la produzione del progetto stesso. In particolare, é stato chiesto agli studenti di produrre un videogioco (piú precisamente, un sistema che permettesse di giocare a vari giochi di carte): per lo sviluppo di tale videogioco é stata usata la tecnologia XNA. Nonostante la maggior parte degli studenti non avesse alcuna esperienza pregressa con XNA (e, a volte, nemmeno con C#, il linguaggio che si accompagna ad XNA), i ragazzi sono riusciti a padroneggiare la tecnologia in breve tempo (la durata del corso), producendo dei progetti finali veramente degni di nota. Abbiamo quindi chiesto ad un gruppo (“I Cordiali”) di presentarci il loro lavoro e di darci il loro feedback sull’esperienza, abbastanza “diversa” da quella di un classico corso universitario:
N.B.: Il corso di Ingegneria del Software é stato tenuto dal docente Agostino Cortesi, con l’assistenza di Giuseppe Maggiore e Giulia Costantini. Il gruppo dei “Cordiali” é formato da Francesco Boscariol, Mauro Barbon. Luca Cosmo, Chiara Poma e Nicola Loiolino (da sx a dx come appaiono nel video). 1/22/2009 SpriteBatch per effetti speciali - parte 4By Davide Luzzu – Webcast by Giuseppe Maggiore
Il (resto del) codice C#
// Generiamo un array di float che sono le altezze (y) dei punti di controllo. var tmp = Enumerable.Range(0, controlPoints.Width) // i ∈[0,controlPoints.Width-1] .Select(i => i / (float)controlPoints.Width) // x ∈[0,1] .Select(x => sin(-4.0f * t + 32.0f * pi * sigmoid(x + 0.5f) * x) * 0.1f + 0.5f) // y é l'altezza di un punto di controllo. La convertiamo in formato integrale. .Select(y => (UInt16)(y * (UInt16.MaxValue - 1))) .ToArray(); // Salviamo l'array di altezze nella texture. controlPoints.SetData<UInt16>(tmp);
Per dare la forma al serpente definiamo i punti di controllo sulle x e sulle y. Per fare ciò ci serviremo delle funzioni sin e sigmoid, che abbiamo definito in precedenza. Definiamo un range da 0 a 256, e ne facciamo il rapporto tra 0 e 1 in modo che x ∈ [0,1]. Dato che x è nel range [0,1] possiamo definire la forma del serpente usando il seno. Tale forma varia in base al tempo ( …4.0f * t…) ed ha, ad ogni istante di tempo, una forma curva definita da “…32.0f * pi * sigmoid(x + 0.5f) * x…”; variando tali parametri determineremo un’alterazione a piacimento della forma o del tempo di esecuzione del serpente. Una delle peculiarità derivanti da questa porzione di codice è il fatto che, ciò che viene disegnato, è ad una dimensione algoritmicamente, ma a due dimensioni visivamente; la x difatti è un seno, e ciò è sufficiente per disegnare il serpente. Ma la x è l’unica variabile da cui dipende effettivamente il controllo dello spazio. Quindi è monodimensionale. A questo punto la y è libera, e la si utilizza per inserire i dati di illuminazione, quelli che avevamo previsto inserendo SurfaceFormat.Luminance16 nella definizione della texture. Convertiamo la y in intero a 16 bit Unsigned, e trasformiamo la struttura in un array. Quindi la variabile “tmp” è un array di interi a 16 bit Unsigned. Non resta che inserire i dati dei punti di controllo nella Texture2D.
// Carichiamo la texture di sfondo. var background = Content.Load<Texture2D>("background"); /// Disegnamo la texture di sfondo. Questa operazione non dipende in alcun modo da come poi disegneremo il serpente.
spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.SaveState); spriteBatch.Draw(background, new Rectangle(0, 0, 800, 600), Color.White); spriteBatch.End();
Per rendere l’applicazione più gradevole prevediamo uno sfondo, di nostro gradimento, che disegnamo con lo SpriteBatch. Ci preme ribadire che tale operazione non dipende in alcun modo da come disegneremo il serpente.
/// Disegnamo il serpente con il nostro shader in alpha blending sulla scena precedente. L'operazione di disegno genera un serpente che passa per i punti di controllo della texture "controlPoints".
new Vector3[] { new Vector3(0, 1, 1} .Select((color, index) => new {color, index}) .ToList().ForEach(snake => { fx.Parameters["SnakeColor"].SetValue(snake.color);
spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.SaveState);
fx.Begin(); pass(0, p => p.Begin());
spriteBatch.Draw(controlPoints, new Rectangle(0, 100 + 100 * snake.index, 800, 400), Color.White);
pass(0, p => p.End()); fx.End();
spriteBatch.End(); });
base.Draw(gameTime); }
Finalmente disegnamo il serpente! Definiamo innanzittutto il colore con un vettore, un azzurro tenue. Inseriamo il colore in ogni posizione di un array di Vector3 e lo modifichiamo con la tecnica dello Shader. Il primo passo è impostare il parametro “SnakeColor” dello Shader con l’azzurro appena definito. Iniziamo lo SpriteBatch con il metodo “Begin()” e avviamo il rendering dello shader con
fx.Begin();
che provoca, inoltre, la sostituzione automatica dello Shader di default nello SpriteBatch. A questo punto non resta che aprire il passo zero di rendering con
pass(0, p => p.Begin());
e disegnamo sullo schermo la Texture2D. Chiudiamo il passo e indichiamo che lo shader ha terminato la sua tecnica con
fx.End();
ricordandoci di chiudere anche le funzioni dello SpriteBatch con la funzione “End()”.
SpriteBatch per effetti speciali - parte 3By Davide Luzzu – Webcast by Giuseppe Maggiore Il codice C#
Effect fx = null; Texture2D controlPoints; const int numControlPoints = 256; protected override void LoadContent() { /// Costruiamo il renderer 2D. spriteBatch = new SpriteBatch(GraphicsDevice); /// Costruiamo la texture di control points, in cui carichiamo interi /// unsigned a 16 bit. La texture é una matrice numControlPoints × 1, /// cioè 256 colonne e una riga. controlPoints = new Texture2D(GraphicsDevice, numControlPoints, 1, 1, TextureUsage.None, SurfaceFormat.Luminance16);
// Carichiamo lo shader HLSL. fx = Content.Load<Effect>("snake shader"); }
La prima operazione da compiere è ottenere la configurazione grafica ( GraphicsDevice ) e applicarla ad uno SpriteBatch. Il GraphicsDevice ci permette di ottenere informazioni riguardo alla risoluzione grafica con cui si sta eseguendo l’applicazione, e, di conseguenza, impostare il Pixel Shader. Lo SpriteBatch è il render 2D che ci consentirà di disegnare il nostro serpente. Il secondo passaggio fondamentale è creare una Texture2D che contiene dei punti di controllo (256), che daranno la forma al nostro “serpente”. Questi punti di controllo possono essere concettualmente associati ai key frames di una animazione; cioè definiamo quali sono le posizioni per cui la nostra animazione dovrà assolutamente passare, il resto viene fatto per interpolazione. Sarebbe, infatti, impensabile dover inserire tutti e 25 i frames ( riferito ad un video in formato PAL) di ogni secondo della animazione, poiché, per ottenere un minuto di animazione, servirebbe agire su 1500 singoli frames! Allo stesso modo quello che facciamo noi è definire dei “frames” (detti anche nodi o punti di controllo ) e nessun dato nella Texture 2D, in modo da avere la possibilità di inserirli a nostro piacimento. Per poter manipolare una elevata quantità di informazioni di illuminazione sulla texture, indichiamo che il formato della superficie è Luminance16; è un formato che comprende 16 bit di dati solo per illuminazione, cioè potremo manipolare 216 combinazioni di colore.! L’ultima operazione da effettuare è il caricamento dello Shader. Si fa riferimento alla classe Content che contiene il metodo ad accesso statico Load. Tale metodo riceve in input una stringa che designa il percorso di accesso al file (*.fx ) che contiene il codice dello Shader. Ricordiamo che il nome dello Shader va indicato senza l’estensione. Il metodo Load è parametrizzato con un tipo Effect; ciò indica che la variabile di ritorno conterrà dati di tipo Effect.
protected override void Draw(GameTime gameTime) { //Resettiamo lo sfondo dell'applicazione ad azzurro. graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
// Alcuni piccoli helper trigonometrici e non. Func<float, float> sin = x => (float)Math.Sin((double)x); Func<float, float> sigmoid = x => (float)MathHelper.SmoothStep(0.5f, 0.1f, x);
var pi = (float)Math.PI;
// Da quanti secondi l'applicazione é in esecuzione? var t = (float)gameTime.TotalGameTime.TotalSeconds;
// Helper di attivazione/disattivazione di un passo dello shader HLSL. Action<int, Action<EffectPass>> pass = (i, a) => a(fx.CurrentTechnique.Passes[i]);
Eseguiamo l’override del metodo Draw di default, dove scriviamo tutto il codice per effettuare il disegno, senza demandare nulla ad altri metodi. Definiamo innanzitutto due funzioni per aiutarci nei calcoli, una (sin) che calcolerà il seno per definire la forma del serpente, e l’altra (sigmoid) per interpolare i dati. Di particolare interesse, in questa porzione di codice, è la definizione dei passi dello Shader HLSL.
Action<int, Action<EffectPass>> pass=(i, a) => a(fx.CurrentTechnique.Passes[i]);
Quello che intendiamo fare è definire in modo elegante l’attivazione e la disattivazione del passo dello Shader. Il codice ML-style ci viene incontro in questo senso, poiché creiamo una labda expression. Diremo che l’azione pass deve compiere una certa azione ad un determinato indice. L’azione che pass eseguirà la chiameremo (a), e (a) effettua un passo della tecnica dello Shader; dato che la tecnica può contenere più di un passo, dovremo indicare quale è l’indice del passo che (a) deve effettuare, e lo indichiamo con (i). Come abbiamo visto, la nostra tecnica contiene un singolo passo, quindi non dovremo aggiornare l’indice ( che sarà sempre zero ). In altre parole piuttosto che scrivere
fx.Begin(); fx.CurrentTechnique.Passes[0].Begin();
//…code…
fx.CurrentTechnique.Passes[0].End(); fx.End();
avremo una espressione decisamente più elegante
fx.Begin(); pass(0, p => p.Begin());
//…code…
pass(0, p => p.End()); fx.End();
L’azione che (a) esegue è di tipo EffectPass, cioè un tipo di effetto derivante dalla tecnica dello shader in uso.
SpriteBatch per effetti speciali - parte 2ImplementazioneBy Davide Luzzu – Webcast by Giuseppe MaggioreLo Shader
sampler ControlPoints : register(s0) = sampler_state { MipFilter = LINEAR; MagFilter = LINEAR; MinFilter = LINEAR;
AddressU = CLAMP; AddressV = CLAMP; };
Il “serpente” che andremo a definire è composto da svariati punti di controllo che contengono, non solo una posizione, ma anche un valore che ne determina il colore. Abbiamo però necessità di definire come interpretare il colore nelle posizioni tra un punto di controllo ed un altro; tale operazione viene eseguita con una semplice interpolazione lineare, lasciando invariati i colori di partenza. Per specificare l’immutabilità dei colori di partenza scriveremo:
AddressU = CLAMP; // punto di controllo di partenza AddressV = CLAMP; // punto di controllo di arrivo
dove CLAMP indica appunto che tali colori non dovranno variare. Queste specifiche le inseriamo in un sampler, cioè indichiamo che la modalità di accesso ai pixel del nostro PixelShader è attraverso un campionamento. Dato che esistono più sampler di base, indichiamo con
… : register(s0)…
che quello che ci interessa è all’indice 0 (zero), più precisamente nel registro “s0”, detto in codice assembly (dove abbiamo a che fare direttamente con i registri).
float4 PixelShaderFunction(float2 input : TEXCOORD0) : COLOR0 { float cp; cp = tex2D(ControlPoints, float2(input.x, 0.0f)); float alpha = pow(saturate(1.0f - abs(input.y - cp)), 32.0f); return float4(SnakeColor.xyz,alpha); }
Implementiamo la funzione del PixelShader, passando in ingresso i dati sulle coordinate di texture, ottenuti dal VertexShader. Come possiamo notare, il parametro di ingresso è una coordinata bidimensionale di texture, e la x del parametro rappresenta la posizione sull’asse delle ascisse del punto di controllo che dovremo andare a trovare. La posizione sull’asse delle ordinate la dobbiamo invece ricavare sfruttando i dati che abbiamo:
cp = tex2D(ControlPoints, float2(input.x, 0.0f));
Non ci resta che calcolare un valore alpha, che rappresenta la trasparenza del colore che dà la “fluorescenza” all’immagine di base. Questo colore trasparente avrà una saturazione maggiore quanto più si è vicino all’immagine del serpente, ed al contrario avrà una intensità minore tanto più è lontano. Questa “aura” che impostiamo attorno al serpente è prodotta con:
float alpha = pow(saturate(1.0f - abs(input.y - cp)), 32.0f);
Infine restituiamo un colore a quattro canali, per cui, i primi tre rappresentano il colore originale, e il quarto il canale alpha.
technique Technique1 { pass Pass1 { PixelShader = compile ps_2_0 PixelShaderFunction(); } }
Uno Shader, abbiamo già detto, che è formato da una technique e da uno o più pass; l’ultimo passaggio, appunto, è destinato a definire tale technique e il pass che ci interessa. Il PixelShader è compilato con la versione 2_0, e ciò che deve essere tradotto in assembly è la funzione PixelShaderFunction. SpriteBatch per effetti speciali - parte 1By Davide Luzzu – Webcast by Giuseppe Maggiore
AbstractUno dei problemi più pressanti, nella creazione di mondi virtuali 3D, è rendere realistica la percezione del mondo. I dettagli stanno alla base di tutto. Più sono curati i dettagli più l’utente si troverà a proprio agio nell’ambiente 3D. Per meglio comprendere vediamo un esempio. Ammettiamo che si debba rappresentare un oggetto radioattivo. Come faremo capire all’utente che esso è radioattivo? Lo coloriamo di verde? Non basterebbe. Aggiungiamo una luce all’oggetto per indicare che esso reagisce in modo “strano” al contatto con l’aria circostante? Può andare, ma si può fare di più. Dovremo, in questo caso, creare una vera e propria fluorescenza dell’oggetto radioattivo. Esistono svariati modi per creare una fluorescenza, ma, dal momento che si tratta di un dettaglio, non potrà avere un ruolo predominante nella scena 3D, cioè non può occupare troppe risorse. Si presenta dunque la necessità di possedere uno strumento che esegua del codice a basso livello, (cioè che ci permetta un grande risparmio di memoria e risorse) chiamato genericamente Shader. Potremmo dire che uno Shader è quella parte di codice che valuta come gli oggetti visuali devono rispondere alle luci; in altre parole con uno Shader determiniamo la distribuzione dei colori sugli oggetti visuali, in modo tale da ottenere effetti di tridimensionalità, fluorescenza, luminescenza ecc… Per meglio comprendere l’importanza degli Shader proviamo ad immaginare un mondo virtuale in cui venga rappresentata una tavola imbandita con ogni sorta di cibi, insomma un ambiente 3D abbastanza complesso. Se in tale ambiente non avessimo nessuna luce, niente ci vieterebbe di pensare non solo che non c’è tridimensionalità, ma che addirittura si tratta di un mondo virtuale vuoto; questo perché in assenza di luce lo percepiamo completamente nero. Al contrario se avessimo una luce intensissima lo percepiremo completamente bianco. La soluzione dunque risiede nel corretto bilanciamento delle luci e delle ombre. Quello che presentiamo in questo articolo è un Pixel Shader HLSL ( High Level Shading Language ), e ci servirà appunto per il controllo di alcuni effetti di luce, applicati ad una texture. Uno Shader in HLSL è formato da una technique (tecnica), che a sua volta è formato da vari pass (passi). Nel pass vengono specificate quali funzioni devono essere utilizzate, e a quale tipo di shader si intende fare riferimento (Pixel/Vertex Shader).
Struttura di un nuovo progettoOgni progetto XNA 3.0 possiede una sotto-struttura logica (cioè dove mettere i dati), chiamata Content, nella quale dovremo copiare il file (*.fx) che contiene il codice dello Shader. La copia può avvenire attraverso una semplice operazione di dragging. E’ importante inserire i dati nella struttura apposita Content, poiché essa contiene delle References statiche alle impostazioni di libreria, ed esse ci consentiranno, senza alcuno sforzo, il caricamento dei dati (audio, effetti, textures…), utili al nostro progetto.
Figura 1 References statiche contenute nella struttura logica Content.
L’approccio che seguiremo per la definizione del progetto è bottom-up, cioè definiremo prima lo shader, poi come si inserisce nel contesto dell’applicazione, ed infine come si controlla e si visualizza con un esempio. L’idea è quella di creare un serpente dal colore fluorescente, la cui forma è definita solo in alcuni punti, detti punti di controllo.
Tali punti di controllo verranno definiti nel codice. Lo Shader dovrà contenere una funzione generica, che imposti una texture per alcuni punti di controllo definiti altrove. L’intera forma del serpente è derivata dall’interpolazione delle posizioni dei punti di controllo. Il nostro primo obbiettivo è dunque creare uno Shader e copiarlo nella struttura logica Content. Info aggiuntivePer quanto riguarda luci e ombre, è necessario precisare che esistono due tipi di ombre:
Per quanto riguarda gli Shader, abbiamo fatto riferimento a due diversi tipi:
Uno Shader HLSL permette di scrivere complessi calcoli grafici che possono essere eseguiti con grande rapidità dalla GPU, poiché sono istruzioni di basso livello. Queste istruzioni vengono impartite direttamente alla scheda video, pertanto è necessario assicurarsi che essa supporti tali istruzioni. Uno Shader HLSL, fortunatamente, ha una sintassi C-like style, ed è abbastanza semplice da comprendere; le operazioni che supporta sono le comuni operazioni aritmetiche, più gran parte delle comuni funzioni trigonometriche. Inoltre, per le espressioni di controllo, vale la stessa sintassi del C; per i booleani si usano gli stessi operatori: &&, ||, <, >, ==, !=, <=, >=. La versione dello Shader utilizzata per questo esempio è la 2_0, che permette solitamente una maggiore ottimizzazione del codice nella traduzione in assembly. 1/16/2009 Camera And InputXna Web Tour - Basic By Davide Luzzu, Webcast di Giuseppe Maggiore Camera e InputAbstractCome la maggior parte di voi avrà capito Xna è un’avanzatissima piattaforma per sviluppare videogiochi. Quello che ci si aspetterebbe da un videogioco è che fosse interattivo; se così non fosse la sarebbe un’animazione! Dopo aver constatato questa “sottile” differenza, i più arguti tra i lettori, noteranno anche che finora abbiamo creato un mondo virtuale, in cui eravamo impossibilitati nel movimento, cioè non potevamo navigare nel mondo virtuale. Ma diremo di più. Non si poteva proprio interagire in alcun modo con la scena 3D! La base dell’interazione risiede nell’abilitare l’input da dispositivi come tastiera, gamepad, joystick, o mouse. Ogni volta che l’applicazione riceve un input da uno di questi dispositivi, dovrà essere in grado di analizzare l’input ed eseguire una azione conseguente. L’analisi viene effettuata automaticamente da Xna, ed al programmatore è lasciato il compito di definire l’azione derivante dall’input. In un gioco 3D, come si diceva poc’anzi, è fondamentale potersi muovere all’interno della scena, cioè la nostra “telecamera” deve essere “mobile”. Tale telecamera è manovrata manipolando la matrice View. Una affermazione che potrà sembrare strana è che in realtà quando ci si muove all’interno di un mondo 3D, non è il player che si sposta, ma il mondo intero! In parole povere il player resta fermo e il mondo si muove in senso contrario all’input ricevuto, così da dare l’impressione di muoversi nella direzione desiderata; quando ad esempio il player preme il tasto direzionale “freccia destra” il mondo si muoverà verso sinistra, e così via. E’ un po’ come svitare una lampadina facendo ruotare l’intero palazzo! In questo articolo presentiamo quindi i principi fondamentali per consentire la navigazione, e l’interazione in generale, all’interno di un mondo virtuale. Struttura di un nuovo progettoPer la creazione di questo progetto ci baseremo su “Models”, l’articolo per la gestione dei modelli. Info aggiuntivePer quanto riguarda i dispositivi di input sono necessarie alcune precisazioni. Xna Game Studio 3.0 è una piattaforma per lo sviluppo di videogiochi su XBox 360 e Windows Platform, e supporta 3 dispositivi di base: tastiera generica, mouse generico e gamepad XBox. Salvo poche eccezioni altri gamepad non funzioneranno sulle vostre applicazioni. Tuttavia su internet esistono svariate librerie di supporto gratuite, per integrare differenti gamepad da quello XBox 360. Implementazione Vector2 eyeRotation2D = new Vector2(1.5f, 0.0f); Quaternion eyeRotation { get { return Quaternion.CreateFromYawPitchRoll(eyeRotation2D.X, eyeRotation2D.Y, 0.0f); } }
Riguardo al controllo della visuale del player Xna 3.0 offre una soluzione davvero semplice ed intuitiva. Prima però è necessario introdurre una piccola nota di teoria; esistono tre differenti movimenti rigidi che la telecamera può compiere per riprodurre i movimenti che farebbe la testa del giocatore per guardarsi attorno: Yaw, Pitch e Roll. o Yaw è il movimento di rotazione della testa che compiamo quando ad esempio guardiamo a destra e sinistra prima di attraversare la strada. o Pitch è il movimento di rotazione che compiremmo se, guardando dritti di fronte a noi, inclinassimo la testa avvicinandola ad una spalla. · Roll è il movimento di rotazione che compiamo quando guardiamo in alto e in basso
Un’interessante digressione sull’argomento lo potete trovare su http://en.wikipedia.org/wiki/Yaw,_pitch,_and_roll Per controllare queste tre rotazioni ci serviamo di un quaternione, che non è altro che una matrice 4x4 ottimizzata per alcuni calcoli 3D. Il quaternione è creato attraverso il metodo statico CreateFromYawPitchRoll(yaw, pitch, roll) al quale passiamo in ingresso le nostre rotazioni. Vector3 eyePosition = Vector3.Right * 6.0f + Vector3.Up * 2.0f; Matrix view { get { return Matrix.CreateTranslation(-eyePosition) * Matrix.CreateFromQuaternion(Quaternion.Conjugate(eyeRotation)); } }
Abbiamo accennato al fatto che il mondo si muove così come abbiamo immaginato di svitare una lampadina: al contrario! Il calcolo di tale movimento lo eseguiamo in una matrice che controllerà la nostra vista. Definiamo innanzitutto la posizione del vettore di vista e ne creiamo la traslazione inversa con
Matrix.CreateTranslation(-eyePosition)
e lo moltiplichiamo per l’inverso ( il coniugato ) della rotazione data dal quaternione “eyeRotation” in questo modo:
Matrix.CreateFromQuaternion(Quaternion.Conjugate(eyeRotation)) protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit();
var keyboardState = Keyboard.GetState();
if (keyboardState.IsKeyDown(Keys.Escape)) this.Exit();
var dt = (float)gameTime.ElapsedGameTime.TotalSeconds;
if (keyboardState.IsKeyDown(Keys.Right)) eyeRotation2D.X += -dt * 1.0f; if (keyboardState.IsKeyDown(Keys.Left)) eyeRotation2D.X += +dt * 1.0f; if (keyboardState.IsKeyDown(Keys.Up)) eyeRotation2D.Y += -dt * 1.0f; if (keyboardState.IsKeyDown(Keys.Down)) eyeRotation2D.Y += +dt * 1.0f;
if (keyboardState.IsKeyDown(Keys.Space)) eyePosition += dt * Matrix.CreateFromQuaternion(eyeRotation).Forward;
base.Update(gameTime); }
Le direttive del movimento in base all’input vanno codificate nel metodo apposito Update, di cui eseguiamo l’override. Il metodo GetState() ci consente di recuperare le informazioni dello stato del dispositivo di input, sia esso Keyboard o GamePad. Nel nostro caso abbiamo salvato lo stato in una variabile che ci servirà per controllare la pressione dei tasti direzionali della tastiera. La nostra variabile “keyboardState” contiene un metodo “IsKeyDown(int)” al quale passeremo il tipo di tasto (sottoforma di intero) che vogliamo controllare. L’ultimo passaggio prevede il movimento consono all’azione del tasto premuto. Buon divertimento!
1/13/2009 ModelsXna Web Tour – Basics
AbstractTutti gli oggetti visuali nel mondo virtuale 3D sono composti da triangoli. Tali oggetti visuali possono essere più o meno complessi, e tale complessità implica un numero considerevole di triangoli, anche svariate centinaia di migliaia. E’ comprensibile che posizionare manualmente ad uno ad uno tutti i triangoli potrebbe essere terribilmente noioso, se non impraticabile! Ecco dunque la necessità di possedere uno strumento nelle librerie di sviluppo che ci renda possibile il caricamento di modelli 3D, creati con software appositi. Tale strumento viene genericamente chiamato Loader (caricatore), ed esegue una azione di Load (caricamento). XNA offre un’interessante opportunità per compiere azioni di Load, infatti, consente il caricamento e l’interazione con i modelli 3D attraverso poche righe di codice. In questo articolo viene dunque presentato il caricamento da file di modelli tridimensionali, la loro visualizzazione all’interno della scena ed infine il posizionamento nel mondo virtuale.
Struttura di un nuovo progettoOgni progetto XNA 3.0 possiede una sotto-struttura logica (cioè dove mettere i dati), chiamata Content, nella quale dovremo copiare i dati dei modelli. La copia può avvenire attraverso una semplice operazione di dragging. E’ importante inserire i dati nella struttura apposita Content, poiché essa contiene delle References statiche alle impostazioni di libreria, ed esse ci consentiranno, senza alcuno sforzo, il caricamento dei dati (audio, effetti, textures…), utili al nostro progetto.
Info aggiuntiveUna delle caratteristiche più interessanti relative all’import e alla visualizzazione dei modelli in XNA 3.0, è il fatto che la compilazione dei modelli è incrementale. Per compilazione intendiamo l’elaborazione della struttura dei modelli, per crearne una rappresentazione nel nostro ambiente virtuale. Con incrementale indichiamo il principio per cui, una volta compilati, le successive elaborazioni dell’ambiente virtuale saranno più veloci, poiché la struttura non deve essere rivisitata completamente. E’ una caratteristica di enorme importanza, poiché fa risparmiare una quantità enorme di tempo in fase di creazione e debugging del nostro mondo virtuale.
Implementazione
Model imperialDestroyer, rebelCruiser; protected override void LoadContent() { //Indichiamo da dove caricare i modelli imperialDestroyer = Content.Load<Model>(@"Palpatine Destroyer\model"); rebelCruiser = Content.Load<Model>(@"Mon Calamari II\model"); }
La parte principale del caricamento dei modelli è contenuta nel metodo LoadContent creato di default dall’applicazione. In questa porzione di codice abbiamo inizialmente creato due variabili di tipo Model, le quali conterranno i dati dei nostri modelli. L’istanziamento delle variabili imperialDestroyer e rebelCruiser è decisamente semplice: si fa riferimento ad una classe Content che contiene un metodo Load ad accesso statico. Tale metodo statico Load riceve in input una stringa che designa il percorso di accesso al modello (che deve essere indicato senza l’estensione). Da notare che Load è parametrizzato con un tipo Model; ciò indica all’applicazione che il caricamento, che si sta tentando di effettuare, dovrà rendere un Model.
void Draw(Model model, Matrix world) { foreach (var mesh in model.Meshes) { foreach (var effect in mesh.Effects) { (effect as BasicEffect).World = world; (effect as BasicEffect).View = Matrix.CreateLookAt( Vector3.Right * 6.0f + Vector3.Up * 2.0f, Vector3.Zero, Vector3.Up);
float openAngle = 1.5f; //ampiezza dell'obiettivo float aspectRatio = 1.333f; //rapporto altezza/larghezza obiettivo float startFocus = .1f;//dove inizia il focus (front clip plane) float endFocus = 10000.0f; //dove finisce il focus (effect as BasicEffect).Projection = Matrix.CreatePerspectiveFieldOfView( openAngle, aspectRatio, startFocus, endFocus);
(effect as BasicEffect).LightingEnabled = true; (effect as BasicEffect).DirectionalLight0.Enabled = true; (effect as BasicEffect).DirectionalLight0.Direction = Vector3.Right + Vector3.Down; (effect as BasicEffect).DirectionalLight0.DiffuseColor = Color.OrangeRed.ToVector3(); (effect as BasicEffect).DirectionalLight0.SpecularColor= Color.Violet.ToVector3(); //----------------------------------------------------------- }//end foreach mesh.Effects mesh.Draw(); }//end foreach model.Meshes
}//end method
Questo metodo Draw, da noi definito, prende in ingresso due parametri: Model e Matrix. Il parametro di tipo Model indica il modello caricato a cui applicare le trasformazioni affini, contenute nel parametro di tipo Matrix. In aggiunta alle trasformazioni affini, applichiamo anche delle luci, per rendere più realistica la percezione del modello nel mondo virtuale. Senza addentrarci troppo nell’argomento luci, potremmo dire genericamente che l’assenza di luci dà una sensazione di “finto” nella visualizzazione degli oggetti visuali. Riguardo alle luci utilizzate in questo tutorial notiamo che viene abilitata un'unica luce direzionale, con un colore (diffuse color), ed un colore Violet di risposta alla luce sugli oggetti (specular color). Trasformazioni affini e luci sono contenuti nell’interfaccia generica BasicEffect, che applichiamo effettuando un casting esplicito:
(effect as BasicEffect).MetodoDaRichiamare( params… );
Il metodo Draw, da noi definito, contiene due foreach annidati per cui:
La complessità computazionale di tale esecuzione è O(n2). Una delle parti più interessanti in questa porzione di codice è l’utilizzo delle matrici World, View e Projection.
· World è la matrice che rappresenta le coordinate di traslazione e rotazione del modello rispetto al mondo virtuale ( world coordinates ). Esiste una matrice world per ogni modello, così da poterli controllare singolarmente. · View è la matrice che indica la proiezione di vista, in altre parole rappresenta la telecamera attraverso la quale noi osserviamo il mondo virtuale. Al momento abbiamo inserito una sola View, ma in giochi multiplayer possono coesistere più View, una per ogni giocatore. · Projection è la matrice che rappresenta le caratteristiche dell’obiettivo della nostra telecamera. Le impostazioni base di questo obiettivo sono: il suo angolo di apertura, il rapporto altezza/larghezza, il front clip plane (cioè il punto di inizio del focus), il back clip plane (cioè il punto di fine del focus). Back e front clip plane indicano, in altre parole, dove noi iniziamo a vedere (ad esempio 0,1 cm dal nostro occhio), e sin dove si spinge la nostra visuale (ad esempio 3 km, 10 km ecc…). protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue);
//traslazione del modello verso sinistra di un tot di unità a piacimento Draw(imperialDestroyer, Matrix.CreateTranslation(Vector3.Left * 3.0f));
//posizionamento del modello e rotazione (sfruttando le proprietà della billboard) verso "l'Imperial Destroyer" var rebelCruiserPosition = Vector3.Right * 3.0f; Draw(rebelCruiser, Matrix.CreateBillboard( rebelCruiserPosition, Vector3.Left, Vector3.Up, null));
base.Draw(gameTime); }
Draw è il metodo di cui eseguiamo l’override per disegnare gli oggetti visuali nella scena. In questo caso contiene poche righe di codice, poiché demandiamo tutto ad un altro metodo Draw, descritto in precedenza. Al metodo Draw, da noi definito, passiamo dunque il modello e la matrice World, dopodiché disegniamo la scena chiamando:
base.Draw(gameTime);
che si occupa di lanciare l’implementazione di default del metodo Draw fatta da Xna e che si occupa di effettuare tutte le operazioni “standard” di finalizzazionedel disegno della scena in DirectX.
8/2/2008 Illuminazione con Shader e Introduzione a Microsoft.XNA.EffectCome usare HLSL per scrivere i primi shaders in XNA. [il post e' un WIP: una ulteriore versione con moltissimo codice verra' pubblicata in futuro] Dato un progetto XNA nuovo di zecca, dichiariamo gli oggetti fondamentali che ci permettono di renderizzare geometria: Effect effect; VertexBuffer vertexBuffer; IndexBuffer indexBuffer; VertexDeclaration vertexDeclaration; L'Effect serve a caricare i nostri shader HLSL, VertexBuffer e IndexBuffer contengono la geometria che disegneremo, ed infine la VertexDeclaration contiene una descrizione del formato dei vertici contenuti nel VertexBuffer associato. Definiamo il vertice che ci interessa adoperare, seguendo il template dei vertici gia' pronti in XNA (come ad esempio VertexPositionNormalTexture): struct VertexPositionNormal { public Vector3 Position; public Vector3 Normal; public VertexPositionNormal(Vector3 Position, Vector3 Normal) { this.Position = Position; this.Normal = Normal; } static public int SizeInBytes { get { return 4 * 3 * 2; } } static public VertexElement[] VertexElements { get { return new VertexElement[] { new VertexElement(0, 0, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Position, 0), new VertexElement(0, 4 * 3, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Normal, 0) }; } } } Vediamo che il nostro vertice contiene due Vector3, Position e Normal. La proprieta' SizeInBytes dice quanto grande e' un vertice, ossia due Vector3, composti ciascuno da tre float grandi ognuno quattro bytes: 4 * 3 * 2. La proprieta' VertexElement serve per descrivere byte per byte come e' costruito il nostro vertice, ossia ogni campo del vertice quanto spazio occupa, che tipo di interpolazione usa, e soprattutto che semantica (o Usage) ha tra una serie di possibilita'. In questo modo indichiamo che il nostro vertice e' composto da due campi (il numero di elementi nell'array VertexElements, il primo dei quali e': new VertexElement(0, 0, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Position, 0), ossia un oggetto che comincia dal byte 0, e' un Vector3, interpola secondo l'algoritmo di default (lineare) e contiene una posizione nello spazio. Il secondo campo e' invece: new VertexElement(0, 4 * 3, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Normal, 0) che comincia dal byte 4 * 3 (la dimensione di un Vector3!), e' sempre un Vector3 e contiene una normale. Assegnamo alla vertex declaration il formato dei vertici che useremo: vertexDeclaration = new VertexDeclaration(GraphicsDevice, VertexPositionNormal.VertexElements); e costruiamo i nostri vertex e index buffer in modo da contenere un cilindro (il che si ottiene in modo molto elegante tramite delle query Linq): Func<int, float, IEnumerable<VertexPositionNormal>> createCircle = (numVertices, y) => Enumerable .Range(0, numVertices) .Select(i => new VertexPositionNormal( new Vector3( (float)Math.Cos(2.0f * Math.PI * i / (numVertices - 1.0f)), y, (float)Math.Sin(2.0f * Math.PI * i / (numVertices - 1.0f))), new Vector3( (float)Math.Cos(2.0f * Math.PI * i / (numVertices - 1.0f)), 0.0f, (float)Math.Sin(2.0f * Math.PI * i / (numVertices - 1.0f))))); var vertices = new List<VertexPositionNormal>(); for (int i = 0; i < numCircles; i++) { vertices.AddRange(createCircle(numVerticesPerCircle, (i - 2.5f) / 5.0f)); } var indices = new List<Int32>(); for (int i = 0; i < numCircles - 1; i++) { var baseIndex = i * numVerticesPerCircle; var baseIndex1 = (i + 1) * numVerticesPerCircle; var quads = Enumerable .Range(0, numVerticesPerCircle - 1) .Select(j => new Int32[] { j + baseIndex, j + 1 + baseIndex, j + baseIndex1, j + 1 + baseIndex, j + 1 + baseIndex1, j + baseIndex1 }) .Aggregate((acc, quad) => acc.Concat(quad).ToArray()); indices.AddRange(quads); } indexBuffer = new IndexBuffer(GraphicsDevice, 4 * indices.Count, BufferUsage.WriteOnly, IndexElementSize.ThirtyTwoBits); indexBuffer.SetData(indices.ToArray()); vertexBuffer = new VertexBuffer(GraphicsDevice, vertices.Count * vertexDeclaration.GetVertexStrideSize(0), BufferUsage.WriteOnly); vertexBuffer.SetData(vertices.ToArray()); Lo shader che impieghiamo e' un file che risiede nel progetto Content (il nome del file e' "SimpleShader.fx") i cui parametri globali sono le matrici World, View, Projection e i dati seulla luce singola della scena: float4x4 World; float4x4 View; float4x4 Projection; float3 LightPosition; float4 LightColor; Il tipo di vertice che entra nello shader e' lo stesso del vertex buffer: struct LambertVertexShaderInput
{
float4 Position : POSITION0;
float4 Normal : NORMAL;
};
mentre quello che esce contiene anche una serie di dati per calcolare l'illuminazione (normale, direzione della luce, direzione dell'osservatore): struct LambertVertexShaderOutput Il vertex shader vero e proprio e' una funzione che calcola un LambertVertexShaderOutput a partire dal LambertVertexShaderInput passato: LambertVertexShaderOutput LambertVertexShaderFunction(LambertVertexShaderInput input)
{
LambertVertexShaderOutput output;
float4 worldPosition = mul(input.Position, World);
float4 viewPosition = mul(worldPosition, View);
output.Position = mul(viewPosition, Projection);
float3 N = normalize(mul(input.Normal, World));
float3 L = normalize(LightPosition - worldPosition);
float3 eyePosition = mul(-View._m30_m31_m32, transpose(View));
float3 V = normalize(eyePosition - worldPosition);
output.N = N;
output.L = L;
output.V = V;
return output;
}
Infine il pixel shader in questione calcola, a partire dall'output del vertex shader, il colore dei punti occupati dal triangolo (tramite la funzione di Phong): float4 LambertPixelShaderFunction(LambertVertexShaderOutput input) : COLOR0
{
float3 N = normalize(input.N);
float3 L = normalize(input.L);
float3 V = normalize(input.V);
float3 Lr = reflect(-L, N);
float4 diffuse = LightColor * saturate(dot(N, L));
float4 reflection = 0.9f * pow(saturate(dot(Lr, V)), 8.0f);
return diffuse + reflection;
}
Per caricare l'effetto HLSL appena visto, dobbiamo appoggiarci alla content pipeline. Appena caricato l'effetto in questione impostiamo subito i valori iniziali dei parametri globali dell'effetto: effect = Content.Load<Effect>("SimpleShader"); effect.Parameters["World"].SetValue(Matrix.Identity); effect.Parameters["View"].SetValue( Matrix.CreateLookAt(Vector3.Backward * 2.0f, Vector3.Zero, Vector3.Up)); effect.Parameters["Projection"].SetValue( Matrix.CreatePerspectiveFieldOfView(1.5f, 1.333f, 0.01f, 10000.0f)); Per renderizzare la nostra geometria tramite l'effetto caricato e' sufficiente impostare i parametri mancanti (colore e posizione della luce) e invocare GraphicsDevice.Draw*Primitives con la geometria che ci interessa:
var time = (float)gameTime.TotalGameTime.TotalSeconds; var LightPosition = new Vector3(1.0f, 0.0f, 1.0f); var LightRotation = Matrix.CreateRotationY(-time * 0.1f - 0.5f); LightPosition = Vector3.Transform(LightPosition, LightRotation); effect.Begin(); effect.Parameters["LightPosition"].SetValue(LightPosition); effect.Parameters["LightColor"].SetValue(Color.Salmon.ToVector4()); effect.Parameters["World"].SetValue(Matrix.CreateRotationY(0.0f * time / 2.0f)); effect.CurrentTechnique.Passes["Lambert"].Begin(); DrawVertexAndIndexBuffersAsTriangleList(vertexBuffer, indexBuffer, vertexDeclaration); effect.CurrentTechnique.Passes["Lambert"].End(); effect.End(); Il risultato ottenuto e' di notevole...effetto :) Trovate il codice sorgente qui; inoltre potete scaricare qui il video o guardarlo direttamente embedded ad alta risoluzione: 6/23/2008 Programmazione funzionale in .Net 3.5: tutorial di base su F# (parte III):Nota: per poter eseguire questo codice e' necessario Visual Studio 2008 con installato il Microsoft F# Research Compiler (noi abbiamo usato la versione 1.9.1.3). Prima di cominciare con il tutorial vero e proprio, e' importante specificare perche' questo linguaggio puo' essere utile. Elenco i vantaggi principali senza entrare nel dettaglio:
In questo ultimo tutorial introduttivo su F# diamo una impressionante (almeno per noi) dimostrazione di potenza espressiva scrivendo un compilatore/interprete di un semplice linguaggio di espressioni aritmetiche. Mostriamo anche come si possono estendere i costrutti funzionali di F# con costrutti object oriented per esportare classi e altri tipi in un progetto .Net scritto in qualche altro linguaggio (in particolare C#). Una espressione in questo linguaggio e' definita come Le espressioni vengono compilate in listati (liste) di istruzioni in una macchina virtuale basata su stack, dove una istruzione e': un tipico esempio di compilazione parte dall'espressione aritmetica originale: la converte nel linguaggio aritmetico (dove il numero n sottolineato rappresenta Const(n)): la compila nel listato corrispondente: e quindi la valuta, ritornando in questo caso il valore 17. Per costruire questo tutorial dobbiamo definire dei tipi di dato, o unioni discriminate, per rappresentare istruzioni e le espressioni aritmetiche: type expr = Add of expr * expr | Mul of expr * expr | Div of expr * expr | Sub of expr * expr | Const of int per cui una variabile di tipo expr sara' in realta' una (e una sola) tra Const(int), Add(e1,e2), ... Per trasformare in una lista una espressione possiamo fare pattern matching sul tipo effettivo di una espressione: let rec ToString e = match e with (Add (e1, e2)) -> "(" + (ToString e1) + " + " + (ToString e2) + ")" | (Mul (e1, e2)) -> "(" + (ToString e1) + " * " + (ToString e2) + ")" | (Div (e1, e2)) -> "(" + (ToString e1) + " / " + (ToString e2) + ")" | (Sub (e1, e2)) -> "(" + (ToString e1) + " - " + (ToString e2) + ")" | (Const x) -> x.ToString() in cui associamo un pezzo di codice da valutare in caso di ogni forma possibile dell'espressione e. Si potrebbero anche avere patterns piu' complicati, tipo: (Add (Mul (e1, e2), Div(e3, e4))) -> ... Qui sotto il webcast in cui io e Giulia Costantini mostriamo la stesura di tutto il compilatore/interprete ed infine lo compiliamo in una libreria che apriamo in un progetto C#, per mostrare l'interoperabilita' tra codice F# e codice in altri linguaggi .Net (download video ad alta risoluzione qui): [il webcast in streaming richiede silverlight e un attimo di pazienza per il caricamento; in compenso la visione puo' essere effettuata in full-screen a buona risoluzione!]
Programmazione funzionale in .Net 3.5: tutorial di base su F# (parte II)Nota: per poter eseguire questo codice e' necessario Visual Studio 2008 con installato il Microsoft F# Research Compiler (noi abbiamo usato la versione 1.9.1.3). Prima di cominciare con il tutorial vero e proprio, e' importante specificare perche' questo linguaggio puo' essere utile. Elenco i vantaggi principali senza entrare nel dettaglio:
In questo tutorial vedremo come si manipola la struttura dati che la fa da padrone nel mondo ricorsivo di F#: la lista! Dichiariamo tre liste: let l1 = [1;2;3;4] let l2 = 1::2::3::4::[] let l3 = l1@l2 l1 contiene i primi 4 interi, l2 anche ma e' costruita effettuando una serie di operazioni di tipo "push" a partire dalla lista vuota []. Infine l3 e' la concatenazione delle liste l1 e l2. Come primo esempio di una funzione che manipola liste vediamo come sommare tutti gli elementi di una lista con una funzione ricorsiva: let rec sum l = match l with [] -> 0 | x::xs -> x + (sum xs) in cui vediamo come il pattern matching possa essere usato non solo per ispezionare il valore delle variabili, ma anche la struttura dei loro componenti (ad esempio qui distinguiamo tra la lista vuota [], la somma dei cui elementi e' 0, e la lista con almeno un elemento x che va sommato alla somma del resto della lista). Quindi vediamo in opera alcune cosiddette "funzioni di ordine superiore", ossia funzioni che prendano in input anche altre funzioni per applicarle secondo pattern gia' noti. Una delle piu' semplici e' la funzione di riduzione, ossia: let rec reduce f l = match l with [] -> failwith "la lista non puo' essere vuota" | [x] -> x | x::xs -> let acc = reduce f xs in f acc x in cui la funzione f viene applicata alla riduzione della coda e all'elemento corrente. Se f fosse la funzione che ritorna il massimo tra due valori: (fun a -> fun b -> if a > b then a else b) allora reduce ritorna il massimo elemento della lista. Se invece come f usiamo la funzione somma: (fun a -> fun b -> a + b) allora reduce ritorna la somma di tutti gli elementi della lista. Esistono moltissimi altri patterns tipici di iterazione di liste e di collezioni, esattamente gli stessi che si trovano in LINQ (ed ecco perche' abbiamo presentato LINQ come una estensione funzionale di C#, piuttosto che presentarlo come fanno molti come libreria per accesso ai databases). Qui sotto il webcast tenuto da me (Giuseppe Maggiore) e da Giulia Costantini in cui produciamo questo codice (e molto altro) spiegando in dettaglio cosa significa e mostrando il risultato della sua esecuzione (download ad alta risoluzione qui): [il webcast in streaming richiede silverlight e un attimo di pazienza per il caricamento; in compenso la visione puo' essere effettuata in full-screen a buona risoluzione!]
6/22/2008 Programmazione funzionale in .Net 3.5: tutorial di base su F# (parte I)Nota: per poter eseguire questo codice e' necessario Visual Studio 2008 con installato il Microsoft F# Research Compiler (noi abbiamo usato la versione 1.9.1.3). Prima di cominciare con il tutorial vero e proprio, e' importante specificare perche' questo linguaggio puo' essere utile. Elenco i vantaggi principali senza entrare nel dettaglio:
In questo tutorial vedremo come dichiarare funzioni e costanti in F#. Cominciamo con il dichiarare alcune variabili: let a = "una stringa" let b = 10 let c = 10.0f let d = 'a' con cui dichiariamo una serie di variabili immutabili (quasi delle costanti), lasciando che sia il compilatore a dedurne il tipo. Usiamo la console F# Interactive (integrata in Visual Studio) per farci compilare il codice al volo ed eseguirne pezzi. Vediamo cosa dice la console FSI quando le diamo in pasto il codice appena visto: val a : string
val b : int
val c : float32
val d : char
a e' una stringa, b e' un intero, c un float e d un char. Dichiariamo adesso una funzione che somma tre interi tra loro: let somma x y z = x + y + z
Rispetto al modo "classico" di scrivere una funzione di somma notiamo soprattutto che non esiste la keyword "return" (e' tutto implicito) e non serve specificare il tipo dei valori da sommare tra loro o il tipo restituito. Rispetto all'equivalente C#, non c'e' paragone quanto a semplicita' e velocita' di scrittura: int somma(int x, int y, int z) { return x + y + z; } vediamo ora un semplice esempio di ricorsione (si noti come non scriviamo solo let, bensi let rec per indicare al compilatore che il nome della funzione verra' usato all'interno della funzione stessa): let rec fact n = match n with 0 -> 1 | 1 -> 1 | i -> if i < 0 then failwith "fattoriale: solo numeri positivi" else i * (fact (i-1)) in questo snippet vediamo come stiamo facendo cosiddetto "pattern matching" sulla variabile presa in input dalla funzione fattoriale (match n with). Il pattern matching permette di definire una serie di coppie <valore variabile, valore da ritornare> tale che viene ritornato il primo "valore da ritornare" il cui "valore associato" e' pari al valore della variabile. Sembra magico, ma non lo e' :D (ulteriori spiegazioni si trovano nel webcast!). Qui sotto trovate il webcast (mio -Giuseppe Maggiore- e di Giulia Costantini) in cui viene estensivamente spiegato e mostrato in azione il codice di questo post (download ad alta risoluzione qui): [il webcast in streaming richiede silverlight e un attimo di pazienza per il caricamento; in compenso la visione puo' essere effettuata in full-screen a buona risoluzione!]
6/21/2008 Programmazione funzionale in .Net 3.5: tutorial di base su LINQ
In questo webcast vediamo come manipolare collezioni di oggetti in C# tramite LINQ. LINQ e' un modo nuovo per accedere ai dati di liste, array, dizionari e enumerabili in generale con efficienza sia del codice risultante che in termini di tempo di programmazione! Usiamo Visual Studio 2008 e C# 3.0, appoggiandoci al framework .Net 3.5. Come prima cosa inizializziamo una lista di interi usando la sintassi di inizializzazione delle collezioni: List<int> l = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; Quindi vediamo come ottenere un risultato molto simile ma senza
var l = Enumerable .Range(0, 10) .ToList(); Questa in effetti e' la nostra prima query LINQ, in cui tramite Enumerable.Range(0,10) otteniamo un IEnumerable<int> che quando iterato restituisce gli interi nell'intervallo [0..10]. Da notare che in LINQ la maggior parte delle queries non e' materializzata finche' non la leggiamo, stampiamo o trasformiamo in lista. Quindi Enumerable.Range(0,10) non alloca spazio per undici interi, ma memorizza solo minimo, massimo e valore corrente durante le iterazioni. Invocando ToList() su un enumerabile lo iteriamo e copiamo i suoi elementi su una lista, di fatto
il tipo della variabile l viene dedotto essere List<int> dal compilatore che analizza il valore assegnato alla variabile al momento della compilazione. Si noti che se fosse stato var l = Enumerable .Range(0, 10); la variabile l avrebbe avuto tipo IEnumerable<int> e non avrebbe occupato la stessa quantita' di memoria della versione concretizzata su lista. A questo punto mostriamo vari modi di stampare la lista l su console. Il primo, piu' banale, e' naturalmente quello di scrivere foreach (var i in l) { System.Console.WriteLine(i); } dove notiamo l'uso della keyword var per dichiarare la variabile i senza dover specificare che essa e' un intero: d'altra parte il compilatore puo' chiaramente capirlo da solo! Un metodo estremamente piu' elegante sarebbe quello di invocare uno dei nuovi metodi disponibili su una lista, come ad esempio il metodo ForEach: l.ForEach( questo metodo prende come unico parametro una funzione che prende l'elemento corrente della lista, ci fa qualcosa e infine ritorna void. La sintassi della dichiarazione del delegate pero' e' un po' ridondante. In primo luogo il sistema sa che in quel punto del codice passeremo una funzione che prende come parametro un intero, quindi invece di scrivere delegate(int i) basterebbe dire solo i. Poi le parentesi graffe sono inutili, dato che si parla di una sola riga di codice! Infatti, grazie alle nuove espressioni lambda presenti in C# 3.0, possiamo scrivere una versione molto piu' raffinata dello stesso codice: l.ForEach(i => System.Console.WriteLine(i));
dove si intuisce che il delegate passato alla ForEach prende una variabile i, e la trasforma eseguendo una stampa su console. L'idea fondamentale e' che passiamo alla ForEach il lavoro da fare per ogni elemento, e la ForEach invochera' questa nostra funzione su un valore dopo l'altro della nostra lista. Passiamo a cose un po' piu' corpose. Ipotizziamo di voler trasformare la nostra lista l in una nuova lista l1 contenente tutti i valori di l trasformati rispetto a una funzione matematica qualsiasi: var l1 = l
.Select(i => - 3 * i * i * i + 6 * i * i + 3)
.ToList();
vediamo qui in azione la funzione Select, che prende un enumerabile (in questo caso l) e ad ogni elemento di l applica una funzione che lo trasforma, restituendo l'enumerabile con gli elementi trasformati. Si noti che anche qui finche' non invochiamo ToList la collezione risultante resta non eseguita: non occupa memoria ma ogni volta che accederemo ai suoi elementi verra' effettuato un calcolo! Un'altra importantissima aggiunta di LINQ al C# 3.0 sono i cosiddetti tipi anonimi. Scrivendo var i = new { Item = 0, ItemString = "pippo" }; dichiariamo la variabile i come avente tipo anonimo. Se infatti poggiamo il mouse sopra la scritta var, Visual Studio fa comparire il tipo della variabile, ossia un tipo 'a (si legge alfa) avente due proprieta' pubbliche, int Item e string ItemString: Mettere insieme Select e tipi anonimi produce collezioni complesse e permette di fare cose che prima d'ora richiedevano pagine di codice e di dichiararsi strutture dati ausiliarie solo per contenere piccolissimi tipi di dato temporanei. Vediamo ora una ulteriore feature interessantissima che possiamo combinare con LINQ, ossia gli enumerabili infiniti. Creiamo un oggetto di tipo IEnumerable<int> che contenga tutti gli interi: IEnumerable<int> NumeriNaturali() { int n = 0; while (true) { yield return n; n++; } } naturalmente questo codice non e' il prodotto di una allucinazione folle, ma effettivamente funziona. Certo, se provassimo a materializzare questo enumerable scrivendo: var N = NumeriNaturali()
.ToList();
il programma andrebbe in loop infinito cercando di salvare sulla lista N tutti i numeri prodotti dall'enumerabile ritornato dalla funzione. Ma se ad esempio scrivessimo la seguente query LINQ: var l4 = NumeriNaturali()
.Skip(10)
.Take(200)
.ToList();
in questo modo in l4 sono contenuti solo un numero finito di elementi e possiamo tranquillamente convertire l'enumerabile in una lista. Grazie al fatto che in LINQ le queries non vengono eseguite fino al momento in cui cio' e' strettamente indispensabile, e' possibile manipolare oggetti infiniti (generatore di numeri casuali, di perlin noise, di checkpoint di un percorso, di nemici da uno spawning point...) senza ricorrere per questo a memoria infinita (che come ben si sa e' molto costosa :) ). Qui sotto ecco un embedding del tutorial in cui mostriamo LINQ in azione in diretta. Il webcast (qui ad alta risoluzione) e' tenuto da me (Giuseppe Maggiore) e da Giulia Costantini in "doppio": [il webcast in streaming richiede silverlight e un attimo di pazienza per il caricamento; in compenso la visione puo' essere effettuata in full-screen a buona risoluzione!]
5/28/2008 XNA@DSI Corso e Contest
I seminari, in totale 7, e della durata di 2 ore ciascuno consistevano in una parte teorica (accompagnata da slides) e una parte applicata (in cui scrivendo codice in tempo reale venivano applicati i concetti della parte teorica). Al termine del corso gli studenti sono stati messi in grado di scrivere videogiochi di media complessita'.
Qui sotto potete vedere in streaming la presentazione del corso su XNA e le interviste ai vincitori (dal quarto al primo); lo streaming richiede Silverlight. In alternativa potete scaricare il video completo (attenzione pero', sono circa 40MB!). 4/13/2008 AudioEseguire audio in un progetto XNA By Giulia Costantini
IntroduzioneIn questo tutorial impareremo ad caricare ed eseguire suoni nelle nostre applicazioni XNA. Lo strumento che useremo e' il programma XACT (Cross-platform Audio Creation Tool), uno strumento multipiattaforma (Windows + Xbox360) per la creazione dell'audio. Questo programma viene automaticamente installato insieme ad XNA, quindi se avete gia' installato XNA sul vostro PC avete tutto quello che serve per seguire questo tutorial. Trovate XACT tramite "tutti i programmi"à "Microsoft XNA Game Studio 2.0" à "tools" à "Microsoft Cross-Platform Audio Creation Tool". Per ascoltare suoni all'interno di XACT (invece di dovere aprire un player apposito) potete usare il seguente programma (anch'esso installato insieme ad XNA): "tutti i programmi"à "Microsoft XNA Game Studio 2.0" à "tools" à"XACT Auditioning Utility".
Concetti di base di XACTCominciamo facendo conoscenza con la terminologia usata da XACT e XNA per quanto riguarda i suoni. Un wave e' un buffer di dati audio, ossia un file che contiene un suono. I formati audio supportati da XACT sono wav, aiff e xma. Un sound invece e' insieme di tracce eseguite contemporaneamente; per ciascuna traccia si possono impostare diverse proprieta' tra cui il wave che quella traccia esegue, il volume, ecc... Il termine cue si riferisce al suono logico, ossia cio' che viene effettivamente usato nel codice per riprodurre un suono. Ogni cue puo' contenere uno o piu' suoni; nel caso in cui essa contenga piu' suoni, quando viene eseguita viene scelto uno dei suoni che essa contiene. Tramite XACT possiamo decidere come viene effettuata la scelta del suono da eseguire: ad esempio possiamo impostare una scelta casuale, oppure che i suoni vengano riprodotti nell'ordine in cui li abbiamo inseriti, e cosi' via. Una wave bank e' una collezione di waves, ovvero una collezione di file audio. Il formato di file di una wave bank e' .xwb. Anche per ogni wave bank possiamo impostare varie proprieta', come ad esempio il metodo di compressione per i file audio, se la riproduzione viene effettuata in streaming o se il file viene tenuto tutto in memoria, ecc... Una sound bank invece e' una collezione di sounds e cues. Il formato di file di una sound bank e' .xsb. Come vedremo tra poco, un progetto XACT puo' essere visto come l'insieme di tre files:
Creare un progetto XACTPrima di tutto creiamo un progetto XNA 2.0 tramite Visual Studio ("file" à "new" à "project" à "windows game 2.0") e aggiungiamo al progetto Content (tasto destro su Content à "add" à "new folder") una cartella chiamata "Sound".
A questo punto siamo pronti per creare il nostro progetto XACT. Dopo avere aperto il programma XACT, clicchiamo su "file" à "new project" e inseriamo come path il percorso del nostro progetto XNA appena creato, precisamente "path_progetto_xna\Content\sound". Chiamiamo il nostro progetto XACT myFirstXACTProject.
Ora dobbiamo creare una nuova wave bank e una nuova sound bank. Per fare cio' basta cliccare col tasto destro su "Wave Banks" e su "Sound Banks" e selezionare rispettivamente "New Wave Bank" e "New Sound Bank".
A questo punto nella parte destra dello schermo sono comparse le nostre banks (che di default si chiamano "Wave Bank" e "Sound Bank" ma che possiamo tranquillamente chiamare come vogliamo, semplicemente cliccando F2 sul nome e inserendo il nuovo nome seguito dalla pressione di Invio).
Aggiungere suoni (file audio) alla wave bank non potrebbe essere piu' facile: dobbiamo selezionare i file audio da inserire nella wave bank e trascinarli sopra alla wave bank stessa. Vi ricordo che i formati supportati da XACT sono wav, aiff e xma.
Abbiamo importato nella nostra wave bank due files, chiamati "industry.wav" e "menu.wav". Il risultato e' il seguente:
Abbiamo visto che una sound bank contiene una collezione di sounds e di cues; in effetti se osserviamo la finestra della nostra sound bank possiamo osservare che e' divisa in due zone, una riservata ai sound e una riservata alle cues:
Per creare un nuovo suono e' sufficiente trascinare un file della wave bank nella zona dedicata ai sounds della sound bank. Trascinando il wave "industry" otteniamo:
Cosi' facendo abbiamo creato il suono "industry" (ma possiamo cambiargli nome) che consiste di una sola traccia, ovvero il wave "industry.wav". Possiamo aggiungere altre tracce al suono (trascinando un altro wave sul sound industry), ma ricordiamo che le tracce di un suono vengono riprodotte in contemporanea. Creiamo un altro suono, collegato al file wav "menu", trascinando il wave "menu" dalla wave bank alla sound bank:
A questo punto abbiamo due suoni, ciascuno collegato ad un solo wave. Per creare una cue e' sufficiente trascinare un sound nella zona delle cues. Trascinando il sound "industry" nelle cues otteniamo:
Abbiamo creato una cue chiamata "industry" (anche qui ovviamente possiamo cambiargli nome) collegata al suono "industry". Ricordiamoci alla fine di salvare il progetto XACT!
Importare il progetto XACT in XNATorniamo finalmente a Visual Studio, al nostro progetto XNA. Dobbiamo includere nel progetto Content il file del progetto XACT su cui abbiamo appena lavorato. Per fare cio', clicchiamo sul progetto Content, poi clicchiamo su "Show all files" per poter vedere il file di progetto XACT
poi clicchiamo col tasto destro sul file .xap ("myFirstXACTProject.xap") appena comparso
e infine selezioniamo "Include in Project". Dall'output della compilazione del nostro progetto
vediamo che la Content Pipeline di XNA (che vedrete in un tutorial successivo) ci ha prodotto 3 files, che sono quelli di cui vi avevo parlato all'inizio del tutorial: un file di configurazione generale riguardante l'audio (.xgs), un file per la wave bank (.xwb) e uno per la sound bank (.xsb).
Eseguire suoni nell'applicazioneE' giunta finalmente l'ora di scrivere del codice J Cominciamo creando tre oggetti: AudioEngine engine; WaveBank waveBank; SoundBank soundBank;
E inizializziamoli nella funzione "LoadContent": engine = new AudioEngine("Content\\sound\\myFirstXACTProject.xgs"); waveBank = new WaveBank(engine, "Content\\sound\\Wave Bank.xwb"); soundBank = new SoundBank(engine, "Content\\sound\\Sound Bank.xsb");
Abbiamo dunque caricato i 3 files che caratterizzano il nostro progetto XACT, ossia il file di impostazioni .xgs (che va memorizzato nell'AudioEngine), la wave bank .xwb (che viene memorizzata nell'oggetto di tipo WaveBank) e la sound bank .xsb (che viene memorizzata nell'oggetto di tipo SoundBank). Alla waveBank e alla soundBank va anche passato l'engine di riferimento. A questo punto abbiamo due possibilita' per riprodurre un suono. La prima possibilita' e': soundBank.PlayCue("industry");
ovvero utilizzare la sound bank per riprodurre direttamente una cue, identificata per nome (nel nostro caso abbiamo scelto di riprodurre la cue "industry"). Se inseriamo questo codice nella funzione LoadContent (subito dopo la creazione degli oggetti engine, waveBank e soundBank) otterremo che all'avvio dell'applicazione XNA verra' riprodotto il suono collegato alla cue "industry". La seconda possibilita' e' definire un altro oggetto Cue cue;
di tipo Cue, e nella LoadContent scrivere:
cue = soundBank.GetCue("industry"); cue.Play();
Cosa abbiamo fatto? Ci siamo fatti dare la cue di nome "industry" dalla sound bank, l'abbiamo salvata in un oggetto di tipo Cue e l'abbiamo riprodotta. L'effetto e' esattamente lo stesso visto prima, ossia all'avvio dell'applicazione viene riprodotto il suono collegato alla cue "industry"). Se pero' volessimo che il suono venisse riprodotto solo quando viene premuto un certo tasto? Bastera' scrivere il seguente codice nella funzione "Update": if (Keyboard.GetState().IsKeyDown(Keys.Space)) { cue = soundBank.GetCue("industry"); cue.Play(); } In questo modo quando premeremo il tasto spazio, verra' riprodotto il suono desiderato. C'e' pero' un problema: la funzione Update, come sappiamo, viene chiamata molte volte (anche centinaia) al secondo. Quindi durante la nostra pressione del tasto spazio la funzione Update viene chiamata piu' volte e ogni volta essa comincera' la riproduzione della cue "industry". Il risultato e' che il suono viene riprodotto piu' volte, ciascuna a distanza di millesimi di secondo dalla precedente. Inoltre se premiamo spazio una seconda volta (dopo qualche secondo), mentre il suono sta ancora venendo riprodotto, partira' una seconda istanza del suono che si sovrapporra' alla prima. Come possiamo evitare queste spiacevoli situazioni? Grazie alla proprieta' isPlaying che ci fornisce l'oggetto di tipo Cue! Tale proprieta' ci dice se la cue sta venendo riprodotta; in questo modo possiamo evitare di cominciare un'altra riproduzione del suono se ce n'e' gia' una attiva. In codice: if (cue != null && cue.IsPlaying == false) cue = null;
if (Keyboard.GetState().IsKeyDown(Keys.Space) && cue == null) { cue = soundBank.GetCue("industry"); cue.Play(); } Nel codice qui sopra abbiamo imposto che la cue sia null quando non sta venendo eseguita. Infatti se la cue non e' null e sta eseguendo un suono e questo suono finisce, la sua proprieta' isPlaying diventera' falsa e quindi cue verra' impostata a null. La pressione del tasto spazio viene considerata solamente se cue e' null, ovvero se la cue non sta eseguendo un suono. Abbiamo dunque risolto il problema citato prima: la pressione prolungata del tasto spazio non provoca l'esecuzione dello stesso suono un sacco di volte uno sopra l'altro; inoltre pressioni di spazio effettuate durante l'esecuzione del suono non vengono considerate. Ricapitolando: la cue "industry" verra' eseguita solo se viene premuto il tasto spazio e se non e' gia' in esecuzione (che e' esattamente quello che volevamo ottenere)! Nota da tenere ben presente: la cue che otteniamo tramite il metodo GetCue puo' venire riprodotta una sola volta, dopodiche' la cue e' stata "consumata" e non e' piu' valida. E' per questo motivo che prima di ogni "cue.Play()" dobbiamo ri-estrarre la cue dalla sound bank con il metodo GetCue.
Aspetti leggermente piu' avanzati di XACTL'utilizzo del programma XACT rende il tecnico dell'audio completamente indipendente dai programmatori. Come? Abbiamo visto che nel codice ci riferiamo alle cue per nome (ad esempio noi eseguivamo la cue "industry"). Dal momento in cui il tecnico del suono si limita a modificare solamente i wave associati alla cue, e non il nome della cue stessa, puo' lavorare per conto suo e il nostro codice continuera' a funzionare. Quindi il tecnico puo' apportare migliorie e modifiche alle cue tramite XACT, e con una semplice ricompilazione del nostro progetto XNA (durante cui vengono ricreati, se necessario, i 3 file xgs, xwb e xsb) avremo i suoni aggiornati senza dover cambiare una riga di codice. Ma cosa puo' fare esattamente un tecnico del suono con XACT? Abbiamo detto che ad un sound si possono associare piu' tracce (trascinando il nome di ciascun wave sul nome del sound): queste tracce verranno pero' riprodotte contemporaneamente. Si puo' anche cancellare una traccia da un sound cliccando sulla traccia e poi premendo "canc". Nell'immagine sotto vedete un suono ("industry") a cui sono associate due tracce (il wave "industry" e il wave "menu"):
Nella sound bank possono stare piu' suoni, ciascuno associato a particolari tracce:
A ciascuna cue possono venire associati piu' suoni, semplicemente trascinandoli sul nome della cue. Nell'immagine qui sotto vedete che alla cue chiamata "cue1" ho associato tutti e due i suoni presenti nella sound bank, ossia "industry" e "menu".
Se in una cue si sono piu' suoni, possiamo impostare il metodo con cui viene scelto quale dei suoni riprodurre quando viene eseguita la cue.
Nell'immagine vedete il menu a tendina con cui si imposta tale proprieta' e vedete che le possibilita' non sono poche.
Trovate qui il codice sorgente e qui il webcast ad alta risoluzione in cui Alessandro Piva spiega XACT e la scrittura del codice in diretta. Una versione embedded a risoluzione piu' bassa e' qui sotto: 3/20/2008 Testo e primitive 2D in XNA
by Giuseppe Maggiore
IntroduzioneXNA supporta in modo molto ordinato il disegno di oggetti bidimensionali su schermo. Tipicamente queste operazioni di disegno verranno eseguite durante la chiamata a Game.Draw, tramite un oggetto helper, lo sprite batch: SpriteBatch spriteBatch;
Lo sprite batch, come suggerisce il nome, e’ un sistema di raggruppamento ottimizzato di chiamate di disegno di sprites. Gli sprites sono piccoli rettangoli che vogliamo prendere da una texture e mostrare su schermo. Tale sistema richiede che le chiamate di disegno avvengano in mezzo tra apertura (Begin()) e chiusura (End()), in modo che lo sprite batch le possa ottimizzare o ordinare come preferisce: spriteBatch.Begin();
/*...*/
spriteBatch.End();
La funzione Begin prende opzionalmente tre parametri: · SpriteBlendMode che specifica se vogliamo usare alpha blending, alpha blending additivo o nessun blending · SpriteSortMode che indica in che ordine lo sprite batch deve disegnare: per z crescente, per z decrescente, immediato, deferito alla chiamata di End() · SaveStateMode per far si che la chiamata a End() ripristini tutte le flags della scheda video ai valori che avevano prima della chiamata a Begin(): utile per rendere l’uso dello SpriteBatch del tutto trasparente agli altri moduli di disegno SpriteBatch.DrawQuando arriva il momento di disegnare ecco che entra in gioco la prima delle due primitive di disegno: SpriteBatch.Draw. Tale funzione prende come parametri di base una texture, un rettangolo in pixel su schermo in cui la texture va messa, ed un colore per cui moltiplicare quello della texture. Quindi per inserire un’immagine come sfondo dovremo scrivere: spriteBatch.Draw(Content.Load<Texture2D>("background"), new Rectangle(0, 0, 800, 600), Color.White); Questa chiamata a draw sta indicando di disegnare la texture “background” con il suo colore originale (moltiplicare per Color.White lascia inalterato il colore) nell’area di schermo che inizia al pixel (0,0) ed ha dimensioni (800,600) pixel: Ipotizziamo ora di voler disegnare un oggetto di qualche tipo che si muova ruotando sullo schermo. Diciamo anche che tale oggetto ha posizione “Position” ed e’ ruotato di “Rotation” radianti rispetto al suo centro. Allora dovremo invocare nuovamente draw, ma prima sara’ necessario introdurre un nuovo parametro di draw che definisce il centro di rotazione. Di default infatti uno sprite viene fatto ruotare intorno al suo angolo in alto a sinistra, dove effettivamente l’oggetto viene posizionato: Questo pero’ non e’ affatto cio’ che desideriamo: vorremmo che lo sprite ruotasse attorno al suo centro, non attorno ad un suo angolo. Esiste una versione della funzione Draw che accetta anche il parametro origin. Tale parametro serve per indicare quale e’ l’origine della texture, rispetto a cui Position e’ definita. Nel disegno qui accanto vediamo che l’origine e’ stata messa al centro della texture (25,25) posto che la texture fosse grande (50,50). Qualsiasi rotazione adesso sara’ relativa al centro e non agli angoli, proprio come desideriamo. La chiamata risultante per ottenere questo effetto sara’: spriteBatch.Draw(Content.Load<Texture2D>("rotating_particle"), new Rectangle((int)Position.X, (int)Position.Y, (int)Size.X, (int)Size.Y), null, Color.White, Rotation, Size / 2, SpriteEffects.None, 0.0f); in cui possiamo notare come la texture sia posizionata in Position, abbia dimensione Size, e sia centrata sul suo punto medio Size/2, affinche’ le rotazioni usino quello come riferimento e non l’angolo (0,0). SpriteBatch.DrawStringPer disegnare testo la situazione non e’ molto piu’ complicata. Prima pero’ ci serve un file di contenuto di tipo spritebatch, per descrivere il tipo di font da usare, i suoi parametri (dimensione, bold, etc.) e il set di caratteri da caricare (di default si usa Basic Latin, che va dallo spazio ASCII sino alla tilde ~). Creiamo il file di contenuto “Arial.SpriteFont”: In questo semplice file xml sono specificati il tipo del font di sistema da usare per il disegno (arial), la sua dimensione in punti (14), spaziatura, uso di kerning, stile (bold, regular, italic) e i range di caratteri da usare (qui vediamo che e’ stato richiesto di caricare i caratteri dal 32 al 126, ossia il set Basic Latin menzionato prima). Ipotizziamo di voler disegnare la doverosa stringa “Hello World!” su schermo. La chiamata opportuna risulta essere: spriteBatch.DrawString(Content.Load<SpriteFont>("arial"), "Hello World!", new Vector2(10, 70), Color.LightBlue); Vediamo che la funzione DrawString richiede lo SpriteFont da usare per costruire la scritta, il testo da scrivere, la posizione su schermo (dell’angolo in alto a sinistra della scritta) ed infine il colore del testo. Effettivamente questa invocazione produce la scritta desiderata in azzurro a partire dal pixel (10,70): Se pero’ volessimo far ruotare la scritta intorno al suo centro? Avremmo bisogno di specificare l’Origin della nostra DrawString a meta’ della dimensione della stringa disegnata, ma per fare cio’ e’ necessario sapere quanto grande risultera’ la stringa su schermo. SpriteFont fortunatamente e’ in grado di fornirci questa misura: SpriteFont Arial = Content.Load<SpriteFont>("arial"); Vector2 stringSize = Arial.MeasureString("Hello World!"); Tramite cui possiamo disegnare la nostra stringa di testo roteante intorno al proprio centro: float Rotation = (float)gameTime.TotalGameTime.TotalSeconds; SpriteFont arialBig = Content.Load<SpriteFont>("arialBig"); Vector2 stringSize = arialBig.MeasureString("Hello World!"); spriteBatch.DrawString(arialBig, "Hello World!", new Vector2(400, 300), Color.Yellow, Rotation, stringSize / 2, 1.0f, SpriteEffects.None, 0.0f); Font disegnati a manoTutto cio’ che abbiamo visto finora e’ relativo a fonts true type che siano installati nel sistema. Immaginiamo pero’ di avere un font disegnato a mano da un artista, come quello riportato in figura: Sara’ possibile usare questo font non come texture (ossia importandolo direttamente), ma elaborarlo con un content processor appropriato e caricarlo in quanto sprite font. Il primo passo e’ quello di importare il file tra i contenuti del nostro gioco: A questo punto sappiamo che il file dovra’ essere importato con un caricatore di texture, ma vogliamo che sia processato con un processor in grado di leggere i singoli caratteri e metterceli a disposizione. Tale processor e’ chiamato Sprite Font Texture - XNA Framework, e puo’ essere attivato dalle proprieta’ del file “customfont.png”: Da tenere a mente che il font, per funzionare correttamente, dovra’ rispettare due punti fondamentali: · Il canale alfa dovra’ rispecchiare quali parti del testo sono caratteri e quali sono lo sfondo dei caratteri · In mezzo tra i vari caratteri dovra’ esserci un colore magenta con alfa impostato ad opaco Ed e’ fatta, a questo punto e’ possibile caricare ed usare il nostro font come vedete qui sotto: spriteBatch.DrawString(Content.Load<SpriteFont>("customfont"), "test-text", new Vector2(10, 10), Color.LightBlue); Caratteri Unicode specialiIpotizziamo adesso di tornare al caso iniziale, ossia di caricare un font true type installato nel sistema. Vogliamo pero’ poter inserire dei simboli speciali, nel caso specifico vogliamo vedere come stampare tramite sprite font i caratteri elencati di seguito: “€™∞”. Se proviamo direttamente a stampare una stringa del genere pero’ otterremo un’errore runtime che causera’ il crash del nostro gioco! Ricordiamoci che alla creazione dello sprite font abbiamo visto che alla fine del file generato automaticamente ci sono una serie di “character regions”, che denotano gli insiemi di caratteri che intendiamo usare. Di default il solo insieme caricato e’ il cosiddetto Basic Latin, ma possiamo estenderlo in due modi. Il primo e’ naturalmente quello di trovare una lista di caratteri unicode ed aggiungere a mano i codici dei caratteri desiderati nella definizione dello sprite font. La seconda strada e’ apparentemente piu’ complessa, ma in realta’ ci da’ molto piu’ controllo. Per prima cosa creiamo una custom pipeline nella soluzione che contiene il nostro gioco: La sola classe che deve essere contenuta nel nostro processor sara’ una che aggiunge i caratteri desiderati al set di caratteri da caricare, e quindi lancera’ il processor originale con il set di caratteri desiderato: namespace FontProcessor { [ContentProcessor(DisplayName = "ExtendedCharacterSetFontProcessor")] public class ContentProcessor1 : FontDescriptionProcessor { public override SpriteFontContent Process(FontDescription input, ContentProcessorContext context) { input.Characters.Add('€'); input.Characters.Add('™'); input.Characters.Add('∞'); // should load this list of characters from a file! return base.Process(input, context); } } } A questo punto aggiungiamo questo nuovo progetto tra le references del progetto content: Ed infine definiamo come font processor dello sprite font da estendere il processor appena creato: Il font arial adesso sara’ identico a prima, ma in aggiunta supportera’ i caratteri che il nuovo processor aggiunge, rendendo una chiamata come spriteBatch.DrawString(Content.Load<SpriteFont>("arial"), "€™∞", new Vector2(10, 70), Color.LightBlue); perfettamente valida! ConclusioneTrovate il sorgente online qui, mentre il webcast in cui viene scritto e spiegato nel dettaglio tutto il sorgente puo’ essere scaricato da questo link, o guardato direttamente nella sua versione low-res embedded: 3/14/2008 XNA e Windows Forms (controlli di Windows)di Alessandro Piva Ciao a tutti! Nello sviluppo di un videogioco, si ha sempre bisogno di uno o più editor, cioè programmi che servono per creare mappe, missioni, livelli, eccetera. Spesso è possibile trovare, sulla rete, editor già scritti e messi liberamente a disposizione da altri sviluppatori - ma, se non si riesce a trovarne nessuno che soddisfi le nostre esigenze, l'unico modo per risolvere il problema è scriversi un proprio editor. Per creare l'interfaccia utente dei nostri editor, è possibile usare i Windows Forms - i controlli di Windows, cioè quei pulsanti, caselle di testo, barre, menù che possiamo vedere in quasi tutte le applicazioni. Questo webcast è dedicato proprio a questo argomento: come far apparire, nella finestra della nostra applicazione XNA, dei Windows Forms! Inoltre, vengono mostrati alcuni esempi concreti di utilizzo. Ecco una schermata dimostrativa: Purtroppo tali controlli non possono essere usati se eseguiamo il nostro gioco a pieno schermo (fullscreen) né tantomeno sull'XBox 360, ma come già detto sono molto utili per creare interfacce per editor o applicazioni simili. Buon divertimento! P.S.: dimenticavo: ovviamente avete a disposizione il codice sorgente, il webcast ad alta risoluzione (scaricabile in alternativa anche qui), e proprio qui sotto il webcast a bassa risoluzione. 3/13/2008 Gestione dell’input in XNACome far interagire l’utente con il nostro gioco! by Giuseppe Maggiore IntroduzioneIn questo documento spiegheremo brevemente come si gestisce l’input in XNA, in modo da rendere finalmente interattive le nostre opere J. La gestione dell’input pero’ non e’ cosi’ semplice come ci potremmo aspettare: l’input ha luogo (diciamo che si manifestano le interazioni dell’utente) con una frequenza molto differente e molto piu’ bassa rispetto a quella con cui l’applicazione si aggiorna. In un semplice gioco casual le interazioni tra utente e macchina sono dell’ordine di qualcuna al secondo (pochi Hz), mentre il gioco si aggiornera’ a svariate decine di frame al secondo (decine di Hz). Quindi servira’ filtrare la velocita’ di aggiornamento dell’applicazione in relazione all’input. Periferiche supportate da XNAXNA offre pieno e nativo supporto a tre periferiche: - Mouse - Tastiera - Controller XBox 360 Tali periferiche sono state scelte non a caso, ma perche’ sono quelle tipicamente trovate connesse ad una delle piattaforme su cui XNA gira, ossia Windows o l’Xbox 360. Altre periferiche possono essere impiegate, ma bisognera’ usare una libreria diversa da XNA per gestirle e ovviamente perderemo il supporto per l’XBox 360... Lettura dell’inputLeggere l’input consiste nel farsi fornire una copia dello stato istantaneo di un dato controller in un dato momento: GamePadState gps = GamePad.GetState(PlayerIndex.One); Ovviamente tale stato non sara’ valido per sempre, ne’ tantomeno si aggiornera’ da solo: per questo motivo dovremo considerare queste variabili come delle “fotografie istantanee” dell’input di un qualche controller in un dato istante di tempo. Se volessimo fare uscire il nostro gioco alla pressione di un qualsiasi tasto (magari il tasto back del controller XBox 360) allora dovremmo aggiungere questo controllo alla nostra funzione Update: protected override void Update(GameTime gameTime) { gps = GamePad.GetState(PlayerIndex.One); if (gps.Buttons.Back == ButtonState.Pressed) this.Exit(); } In cui si puo’ notare come aggiorniamo come prima cosa ad ogni Update lo stato della periferica di cui usiamo l’input. Problematiche rispetto al tempoIpotizziamo di avere costruito una semplice applicazione che mostra al centro dello schermo un’astronave, e vogliamo che ogni volta che l’utente preme il tasto ‘A’ del controller XBox l’astronave spari un missile. L’approccio naive a questo problema sarebbe una cosa del tipo: if (gps.Buttons.A == ButtonState.Pressed) shoot(); purtroppo per noi le cose non sono cosi’ semplici. Immaginiamo che l’utente tenga premuto il pulsante per un tempo piuttosto breve, diciamo una frazione di due decimi di secondo e che l’applicazione vada a 60 fps:
allora durante la pressione del tasto ‘A’, verranno sparati missili, uno per fotogramma. Il risultato che otteniamo e’ questo, anche per pressioni apparentemente istantanee: Dobbiamo tenere traccia di pressioni prolungate, ma non vorremmo farlo esplicitamente (magari con un array di booleani che ci dicono se un tasto era gia’ premuto precedentemente o no). Beh, il modo piu’ immediato per ottenere questo scopo e’ quello di memorizzare non solo uno stato dell’input (GamePadState) ma anche quello del fotogramma precedente: protected override void Update(GameTime gameTime) { prevGamePadState = gamePadState; gamePadState = GamePad.GetState(PlayerIndex.One); /*…*/ } Grazie a questa seconda struttura di supporto, possiamo trasformare il codice con cui decidiamo se sparare o meno un missile in: if (gamePadState.Buttons.A == ButtonState.Pressed && prevGamePadState.Buttons.A == ButtonState.Released) { Missile m = new Missile(); m.p = Vector3.Zero; m.v = Vector3.Forward; missiles.Add(m); } Con cui una singola (anche lunga) pressione del tasto ‘A’ produce l’effetto desiderato, ossia un solo missile viene lanciato: Con una variazione su questo stesso tema possiamo ottenere anche altri risultati interessanti oltre al timing dei singoli tasti. Immaginiamo di voler far sparare la nostra astronave in tutte le direzioni. In particolare, vogliamo che l’astronave spari verso dove e’ rivolto lo stick analogico destro del controller XBox (evidenziato nella figura): Il primo tentativo e’ analogo a quello che abbiamo fatto prima con il tasto ‘A’: se lo stick e’ premuto piu’ di un certo valore, spariamo il missile: if (gamePadState.ThumbSticks.Left.Length() > 0.25f) { Missile m = new Missile(); m.p = Vector3.Zero; m.v = new Vector3(gamePadState.ThumbSticks.Left.X, 0.0f, gamePadState.ThumbSticks.Left.Y); missiles.Add(m); } Chiaramente sappiamo gia’ come va a finire, ovvero un sacco di missili verranno sparati per pressioni apparentemente anche molto brevi dello stick: Ora potremmo anche usare lo stato del gamepad al frame precedente, ma la soluzione migliore non e’ necessariamente questa. Ricordiamoci che stiamo parlando di uno stick analogico, e vorremmo che il giocatore rilasci la levetta prima di sparare un secondo missile. Quindi dobbiamo tenere traccia piu’ esplicitamente del fatto che in un dato frame stiamo aspettando che l’utente rilasci lo stick analogico prima di poter sparare di nuovo: if (waitThumbstick == true) { if (gamePadState.ThumbSticks.Left.Length() < 0.15f) waitThumbstick = false; } else { if (gamePadState.ThumbSticks.Left.Length() > 0.25f) { Missile m = new Missile(); m.p = Vector3.Zero; m.v = new Vector3(gamePadState.ThumbSticks.Left.X, 0.0f, gamePadState.ThumbSticks.Left.Y); missiles.Add(m); } } Grazie alla variabile waitThumbstick il nostro gestore dell’input aspettera’ che l’utente abbia rilasciato il thumbstick ad almeno lo 0.15 della sua pressione totale, prima di poter sparare di nuovo. Il comportamento di questa soluzione e’ quello desiderato! Una semplice classe CameraImmaginiamo di voler gestire tramite l’input del giocatore lo sguardo dell’utente. In particolare desideriamo che la matrice View venga costruita e aggiornata sulla base di due angoli di rotazione, orizzontale e verticale, che poi vengono aggiornati tramite il mouse o un thumbstick del controller XBox. Spostare il mouse (o il thumbstick) a destra o sinistra fara’ girare la telecamera a destra o sinistra, e similarmente gestiremo la rotazione nella direzione verticale. Come implementare questa utilissima funzione? Beh, intanto dichiariamo gli angoli di rotazione verticale e orizzontale come un vettore a due dimensioni: Vector2 cameraRotationVector = Vector2.Zero; e poi nella funzione Update lo aggiorneremo con la rotazione richiesta dall’utente (moltiplicata per la durata del frame precedente, cosi’ che lo sguardo si sposti alla velocita’ dell’applicazione, e non in modo veloce proporzionalmente al numero di frame per secondo): cameraRotationVector += gamePadState.ThumbSticks.Right *
(float)gameTime.ElapsedGameTime.TotalSeconds;
A questo punto dobbiamo costruire la matrice View, e naturalmente per questo ci sono decine di modi per farlo, differenti a seconda del tipo di telecamera che si vuole implementare e di una enorme quantita’ di altri fattori. Un esempio e’ quello di creare la camera inizialmente sempre nello stesso modo, tramite una chiamata a Matrix.CreateLookAt, e quindi applicare la rotazione desiderata: Matrix View { get { Matrix cameraRotation = Matrix.CreateRotationY(cameraRotationVector.X) * Matrix.CreateRotationX(cameraRotationVector.Y); return Matrix.CreateLookAt(Vector3.Up * 5000.0f, Vector3.Zero, Vector3.Forward) * cameraRotation; } } Se usiamo questa proprieta’ al posto della matrice View in tutte le chiamate di rendering, avremo effettivamente implementato una telecamera capace di ruotare su se stessa. Per spostare avanti e indietro la posizione dell’osservatore, potremo usare cameraRotation.Forward, cameraRotation.Backward, etc. ConclusioneIl sorgente completo di questo sample si puo’ trovare online, assieme al webcast in cui viene mostrata e spiegata la stesura di tale codice. E’ anche possibile guardare il webcast direttamente qui sotto: 1/30/2008 Riconoscimento vocale - non tecnicoVersione inglese Cia a tutti! Sono Giulia Costantini, e in questa sessione non tecnica vi mostrero' in azione una feature del nostro gioco di cui andiamo particolarmente fieri: il riconoscimento vocale. Nel nostro gioco ogni cosa, dal menu all'interfaccia utente puo' essere controllato con la voce. L'esperienza e' sorprendente, e spero che questo video in cui mostro il gameplay ibrido voce/comandi tradizionali sia intrigante anche per voi. Ne approfitto per mettere qualche altro screenshot del nostro gioco: Il video ad alta risoluzione si trova qui. Per chi non volesse/potesse scaricare il video, ecco anche un embedding dello stesso webcast a risoluzione ridotta: Giulia Costantini 1/23/2008 Primi passi con la Content PipelineVersione inglese Ciao a tutti! Sono Giuseppe Maggiore, e in questo tutorial vi illustrero' uno degli aspetti meno blasonati, seppur tra i piu' importanti, di XNA. Costruiremo un compilatore di contenuti personalizzato, o per dirla in inglese una Custom Content Pipeline. Lo scopo e' quello di costruire un assembly (una dll) contenente una serie di classi e oggetti che vanno a precondizionare i file di contenuto in modo da semplificarne il caricamento nel nostro gioco. Suona complicato? In effetti di primo acchito puo' sembrare un po' un eccesso andare a complicare qualcosa di immediato e banale come importare file nel proprio gioco. A onor del vero pero', bisogna dire che c'e' ben di piu' sotto la superficie: prima di tutto, questa architettura e' pensata per astrarre il caricamento di file in tutto XNA, soprattutto considerata l'XBox. Caricare files potrebbe essere immediato sotto Windows, ma non necessariamente la stessa cosa si applica all'XBox 360. Inoltre, tenete presente che non esiste un caricamento diretto dei file di gioco, perche' un minimo di processing sara' sempre e comunque necessario per convertire i bits grezzi in una rappresentazione significativa in classi. Il diagramma con cui rompiamo il problema in sotto problemi e' questo: Un artista o un designer crea il file di contenuto. Tale file viene caricato dall'importer nel suo formato ancora non elaborato, e passato al processor che carica i dati in una classe dopo averli interpretati. Il processor passa i dati al writer, che li salva direttamente dalla classe in un formato di dati intermedio: un file .xnb. Quando il gioco viene lanciato, il file .xnb sara' ricaricato direttamente (o al piu' una versione un po' rifinita pronta per il rendering) attraverso il reader. La soluzione di Visual Studio contenente un gioco con un custom processor e' simile a quella in figura: il primo progetto e' la custom pipeline, che infatti contiene importer, processor, writer e reader. L'altro progetto, il gioco vero e proprio, contiene due riferimenti all'assembly della custom pipeline: uno per il tempo di compilazione dei contenuti, che usa importer-processor e writer, l'altro per il tempo di esecuzione del gioco, che usa il reader. Nel progetto dei contenuti del gioco vediamo anche un file di estensione .custom, l'estensione personalizzata che la nostra content pipeline gestisce. I sorgenti sono online qui, mentre il video del tutorial si trova da quest'altra parte. Includo anche una versione embedded dello stesso video per chi non potesse/volesse scaricarlo: 1/14/2008 We want you!
Se vi foste chiesti cosa abbiamo fatto nelle ultime settimane, ebbene sappiate che abbiamo lavorato (duramente :) ) al nostro progetto per l'Imagine Cup! L'Imagine Cup e' una competizione a livello mondiale per studenti universitari e delle scuole superiori; quest'anno e' stata aggiunta la categoria "Game Development", in cui si deve creare un videogioco con l'aiuto di XNA. Il videogioco deve riguardare un tema ben preciso: "Come la tecnologia puo' aiutare la sostenibilita' ambientale". Perche' vi stiamo parlando di tutto cio'? Perche', pur essendo abbastanza avanti nello sviluppo del nostro gioco, siamo tutti e 3 programmatori... abbiamo bisogno di un grafico, di modelli e textures!!! Se sei uno studente universitario/delle superiori puoi unirti ufficialmente al nostro team per tentare di vincere l'Imagine Cup assieme a noi! Se non lo sei, non preoccuparti: contattaci comunque e troveremo un qualche accordo. Allora, cosa stai aspettando...? Scrivici (xnalearners _at_ hotmail.com)! A presto! Giulia |
|
|