The Xna-Way: Tutorial 6: Orientare oggetto nella direzione in cui si muove e telecamera che lo insegue

Questa volta il compito che mi sono proposto è stato un po' più difficile:
avendo creato un decente MeteorManager questa volta volevo muovermi attraverso questo fitto campo di meteoriti con una navicella spaziale.

Sono partito col pensare a ciò:
- per ora non mi interessa calcolare le collisioni con i meteoriti
- la telecamera sarà in 3° persona, quindi ci troveremo alle spalle della nostra astronave (altrimenti avevamo già la telecamera in prima persona per muoverci)
- la telecamera dovrà seguire il nostro vascello spaziale
- dovremo far muovere nello spazio il nostro mezzo di trasporto

tutto questo ci porta a dover modificare la nostra telecamera per implementare "l'inseguimento" della nostra astronave. Avremo quella che chiamo Follow Camera.
Anche per il nostro vascello avremo il nostro bel da fare: non dovremmo solo farlo muovere e ruotare nel mondo 3D, ma dovremo farlo muovere nella direzione in cui "guarda", quindi movimenti e rotazioni dovranno essere fatti in modo particolare lavorando sulle direzioni, cosa un po' complicata e astrusa ma fattibile.

Per prima cosa è meglio fare la modifica alla classe Camera in modo da implementare l'inseguimento di un determinato bersaglio. In questo modo quando poi andremo a far muovere il nostro oggetto nello spazio la telecamera gli starà dietro e noi non avremo problemi ad osservarlo ^_^

Preso il progetto dell'ultimo post, andiamo nella classe Camesa.cs e sotto il metodo UpdateFreeCamera() andiamo a mettere il seguente metodo:

private void updateFollowCamera(GameTime gameTime)
{
float delta = (float)gameTime.ElapsedGameTime.TotalSeconds;
//creo la matrice temporanea
Matrix tmp = Matrix.Identity;
//imposto la direzione di osservazione, la direzione dell'alto e la direzione della destra
tmp.Forward = direction;
tmp.Up = up;
tmp.Right = Vector3.Cross(up, direction); //questa è calcolata come prodotto tra due vettori

Vector3 t;
//trasformo la distanza (offset) dal punto di osservazione con la matrice calcolata
Vector3.Transform(ref offset, ref tmp, out t);

Vector3 positionTmp;
//calcolo la posizione che vogliamo far avere alla nostra telecamera sommando al punto che siamo osservando
//la posizione trasformata lungo la matrice
Vector3.Add(ref target, ref t, out positionTmp);
//aggiorno la posizione finale
position = positionTmp;
}


Per non avere errori dovremo aggiungere le seguenti variabili di classe:

//variabili per l'inseguimento del target
protected Vector3 offset = Vector3.Zero;
protected Vector3 direction = Vector3.Forward


Cosa fa il metodo?
Il metodo crea una matrice identità e ne imposta i valori per le direzioni che rappresentano il davanti, l'alto e la destra.
Direction e up da dove vengono? Sono dati e assegnati tramite l'oggetto che vogliamo inseguire: cioè direction rappresenta la direzione in cui si sta muoveno il nostro oggetto, mentre up è la direzione che il nostro bersaglio considera come l'alto. In questo modo potremmo mantenere costati le rotazioni!
La destra viene calcolata come prodotto tra i due vettori.

Dopo trasformo l'offest usando la matrice, e sommo questo offest alla posizione del punto osservato e assegno il risultato alla posizione.

Quello che manca da fare ora è la classe per gestire la nostra navicella spaziale.
Ecco il file Ship.cs, comunque presente nell'archivio allegato:
Ship.cs Show/Hide


Il file va inserito nel progetto Meteor e non nella libreria, dato che, come il MeteorManager, questo oggetto è specifico per questa applicazione.

Il pezzo saliente è il metodo Update, il resto è roba già vista!
Cosa fa questo benedetto metodo?
Prima di tutto riazzera il valore della rotazione per yaw e pitch, e anche val.
Val indica se la barra spaziatrice è premuta o meno, e quindi se dobbiamo muoverci o no.
Con la pressione delle frecce direzionali imposto i valori per le rotazioni, e con R riporto il tutto allo stato iniziale.

Con

if (up.Y < 0)
yaw = -yaw;

faccio in modo che se mi trovo a testa in giù e premo verso destra, continuo a girare ancora verso destra non verso sinitra.

Dopo di chè calcolola matrice di rotazione (nota: l'angolo per pitch è calcolato passando l'asse di rotazione), e uso tale matrice per trasformare la direzione e l'up.

Ho poi il calcolo della forza applicata all'oggeto.
Sono formule fisiche. Forza = massa * accellerazione.
So che la formula per calcolare la velocità è sbagliata (anche se quella giusta è riportata come commentata). Ma ora come ora non mi interessa la correttezza delle fisica.

Alla fine poi ho la creazione della world matrix per la nostra navicella, fatta in modo molto simile a come abbiamo costruito quella per la telecamera.

Cosa manca da fare per far funzionare il tutto?
Aggiungere il file per il modello 3D e la texture al progetto, creare la nostra astronave, aggiungerla ai components e disegnarla.
Poi dobbiamo modificare leggermente le impostazioni della telecamera.

Cominciamo: nel costruttore di Game1.cs modifichiamo il codice per la telecamera come segue

myCamera = new Camera(this);
myCamera.FarPlane = 100000;
myCamera.Offset = new Vector3(0, 100, 500);
myCamera.Mode = CameraMode.Follow;
Components.Add(myCamera);

Impostiamo la distanza dal punto di osservazione e cambiamo il tipo di telecamera.

Nel metodo LoadContent() aggiungiamo questo:

myShip = new Ship(this, @"Models\p2_wedge", @"Textures\wedge_p2_diff_v1", defaultEffect);
myShip.Scale = 0.1f;
Components.Add(myShip);

Dove myShip è l'oggetto per la nostra astronave, che dobbiamo aver definito nella classe. Lo scalo ad un fattore 0.1 perchè altrimenti sarebbe troppo grande.

Nel metodo Draw dopo il render per il meteorManager aggiungiamo questo:
myShip.Draw(gameTime);

E nel metodo Update questo:

if (myCamera.Mode == CameraMode.Follow)
{
myCamera.Target = myShip.Position;
myCamera.Up = myShip.Up;
myCamera.Direction = myShip.Direction;
}


In modo che se la telecamera è di tipo follow, si aggiorna il suo target alla posizione della nostra navicella (ma potrebbe essere benissimo qualsiasi cosa!), e settiamo up e direction con i valori provenienti dal nostro oggetto myShip.

Dovrebbe essere tutto! :D
Se qualcosa non va c'è sempre il file allegato!
E se trovate qualche errore fatemelo notare così correggo ^__^

XNA-tut6.rar

Argomenti trattati:
> muovere un oggetto lungo la direzione che sta osservando
> telecamera che insegue il nostro oggetto

Alla prossima!
Continua a leggere!

The Xna-Way: Tutorial 5: Meteor Manager & Free Camera

Eccomi dopo un po' di tempo con un altro post sull'XNA e un suo utilizzo.

Questa volta mi sono detto: "voglio avere una scena con tanti, ma tanti oggetti!"
Ah, quando il masochismo non ha limiti...
Qualcosa ho comunque fatto, anche se non è proprio un granchè...

Quello che mi è venuto in mente, in modo da poterlo poi riusare anche in futuro per una qualche demo simile ad un Asteroid, è di avere un gestore di meteoriti!
Cioè questo gestore ci deve la possibilità di renderizzare a schermo tanti meteoriti.
E vogliamo pure dare la possibilità di muoversi in questo mondo no?
Quindi dovremo fare delle modifiche alla telecamera, in modo che ci pemetta di spostarci in questo universo popolato unicamente da meteoriti!

Allora per il metorManager creremo un file in un progetto per un gameWindows a se stante, mentre il file della della telecamera che andremo a modifcare è sempre quello della myLibrary.

Prima di tutto vediamo come strutturare questo fantomatico MeteorManager. Io l'ho pensato in questo modo:
-il mondo sarà visto come un cubo, dove all'interno potremo mettere i nostri meteoriti
-il mondo sarà diviso in celle: ad ogni cella sarà assegnata una sezione del mondo, e dentro ogni sezione ci sarà la lista degli elementi che sono contenuti dentro tale sezione

Perchè le sezioni? Perchè se ho un mondo molto grande potrei dover renderizzare tanti troppi elementi a schermo, e questo rallenterebbe di molto il numero di frame per secondo. Dividendo in sezioni il mondo potremmo decidere di renderizzare solo quelle adiacenti a noi.
Inoltre se poi in futuro dovremmo calcolare la collisione di una fantomatica astronave con un meteorite, invece di controllare le collisioni con tutti i meteoriti della scena (cosa veramente masochistica, che poterebbe via tantissimo tempo) potremmo semplicemente farlo con quelli adiacetni alla sezione in cui ci troviamo.
Pensante a quanto tempo occorre scorrere un cubo in cui ogni lato è diviso in 20 sezioni!
Cioè ci sono 20*20*20 sezioni da controllare! Ognuna con più elementi!

Avendo suddiviso un mondo cubico in sezioni ho deciso di usare un vettore a 3 dimensioni per gestire il gameWorld.
MeteorManager.cs Show/Hide


Diamo una spiegazione di quello che ho fatto nei vari punti!

public MeteorManager(Game game, Effect effect, int num, float xRange, float yRange, float zRange,
string myModel, string texture)
: base(game)
{
this.currentEffect = effect; //assegno l'effetto usato per il rendering
this.myModel = Game.Content.Load<Model>(myModel); //carico il modello
foreach (ModelMesh mesh in this.myModel.Meshes)
foreach (ModelMeshPart mp in mesh.MeshParts)
mp.Effect = effect; //assegno l'effetto ad ogni parte del modello
this.text = Game.Content.Load<Texture2D>(texture);//carico la texture che voglio usare
//assegno le variabili per la dimensione del mondo
this.xRange = xRange;
this.yRange = yRange;
this.zRange = zRange;
myCamera = (CameraI)game.Services.GetService(typeof(CameraI)); //recupero il servizio per la telecamera
inputDevice = (InputI)game.Services.GetService(typeof(InputI));//e quello per l'input
gameWorld = new Sezione[cells, cells, cells];//creo la matrice del mondo
makeWorld(num); //vado a creare i mondo
}

Quello che fa il costruttore è questo:
assegna al currentEffect l'effetto passato (gli effect sono usati per caricare e gestire codice HLSL, che serve per renderizzare gli elementi della scena, ottenere effetti particolari etc; in questo esempio ho usato un mio piccolo effect che non fa altro che prendere la texture ed applicarla all'oggetto da renderizzare, senza calcolare nessuna luce, questo perchè è più veloce e semplice del basic Effect standard, e qui voglio andare ad ottenere più FPS possibili. Il file è allegato nella soluzione, ma qui non darò spiegazione di come funziona).
Poi carica il modello e la texture passate. Assegna il valore alle variabili che mi dicono quanto è grande il mondo.
Si recupera poi i riferimenti ai servizi della telecamera e del gestore dell'input.
GameWorld è la matrice cubica che andrà a contenere il mondo: ogni cella rappresenta una sezione, e ogni sezione avrò la sua lista di elementi.

Il metodo makeWorld, per quanto possa disorientare, non fa altro che creare e posizionare in modo radom num elementi per ogni sezione. A voi il compito di guardarlo o di chiedere qualcosa che non vi è chiaro qui.

Concentriamoci un attimo sul metodo checkCameraPosition invece, che è più curioso secondo me...

private void checkCameraPosition()
{
float var = (xRange + xRange) / cells;
cameraR = (int)(myCamera.Position.X / var) + cells / 2;

var = (yRange + yRange) / cells;
cameraC = (int)(myCamera.Position.Y / var) + cells / 2; ;

var = (zRange + zRange) / cells;
cameraP = (int)(myCamera.Position.Z / var) + cells / 2; ;
}

Questo metodo prende la posizione della telecamera e memorizza in 3 variabili l'indice di riga, colonna e profondità in cui si trova la telecamera.
Come si fa ad otterere tale risultato?
Prendo la variazione relativa ad ogni cella (cioè quanto spazio sulla dimensione X, per esempio, copre una cella). Poi prendo la posizione X della telecamera, la divido per questa variazione, e a tale risultato ci sommo la metà delle celle. La somma è necessaria perchè voglio che la posizione 0,0,0 sia il centro del mio mondo cubico!
Se non facessi così sarei invece posizionato ad un vertice del mondo.

Il metodo draw adesso, che non sto a riportare perchè è lungotto :O
Allora: nelle prime righe si assegna all'effetto usato per il rendering i valori opportuni, quindi la viewMatrix della telecamera, la projectionMatrix della telecamera, la texture dell'oggetto, e un colore.
Poi cominciamo a scorrere le mesh che compongono il nostro oggetto da renderizzare, in questo caso il nostro meteorite, che dovrà essere replicato tante volte.
Si crea una nuova BoundingSphere con centro e raggio presi dalla BoundingSphere della mesh in esame.
Le BoundingSphere sono usate per calcolare le collisioni tra volumi sferici, ma qui le userò per una cosa diversa ma molto utile ^__^

Poi cominciamo a scorrere le celli adiacenti alla posizione in cui si trova la telecamera: più val è grande maggiore è il numero di celle che considero adiacenti.
Poi riassetto gli indici, in modo che non vadano fuori dai limiti consentiti.
Scorro poi la lista di elementi della sezione data dagli indici r,c,p.
Modifico il centro ed il raggio della BoundingSphere, e calcolo se la sfera o contenuta o interseca il BoundingFrustum della telecamera e solo se lo è renderizzo il meteorite.

Perchè faccio ciò?
Perchè anche se renderizziamo solo le celle vicine a noi, eseguo di solito il draw anche per oggetti che mi sono alle spalle e che io non vedo. Facendo questo sprecherei tempo in rendering inutili, che vengono calcolati, ma il cui risultato non è visualizzato a schermo.
Il frustum della telecamera lo possiamo vedere come un cono che parte dal nostro punto di osservazione, o meglio un tronco di cono, che comincia alla distanza nearPlane e termina a farPlane.
Dato che prima di giungere a questo risultato ho fatto diverse prove, se non usassi il boundingFrustum otterrei con val = 8 qualcosa come soli 5 FPS. Con invece riesco ad arrivare a 70 FPS più o meno stabili! Un buon risultato!
Insomma quello che facciamo è controllare se il volume dato dalla boundingSphere adeguatamente settata è contenuto completamente o in parte nel cono visivo della telecamera, e solo se lo è renderizzo l'oggetto. E' molto più conveniente fare questo controllo che fare un rendering inutile!
E poi perchè usare le BoundingSphere? Perchè è un metodo comodo, semplice ed economico per calcolare il volume di un oggetto, perchè calcolarne il volume esatto controllando i poligoni/vertici è troppo oneroso secondo me (e poi non lo so fare XD).

E come lo calcolo il frustum?
Per questo bisogna andare a modifcare la telecamera!
Ecco il file della telecamera con tutte le modifiche apportate
Camera.cs Show/Hide


Le parti importante da considerare sono:
l'enumeratore CameraMode: questo contiene i vari tipi di telecamera che mi sono venuti in mente. Per ora noi useremo solo Free, ma nulla vieta di implementare anche gli altri o di aggiungerne di nuovi!
Come si può vedere ho anche aggiunto alcuni campi all'interfaccia CameraI.
Aggiunta importate è la CameraBoundingFrustum, che verrà calcolato nel metodo Update.

Ora passiamo al metodo Update: per prima cosa andiamo a controllare il tipo di telecamera, e se è di tipo Free adiamo a chiamare il metodo per aggiornarla.
Poi si calcola la view e la projection matrix ed infine il boundigFrustum, dato dal prodotto delle view per la projection.

Vediamo il metodo updateFreeCamera:

private void updateFreeCamera(GameTime gameTime)
{
movimento = Vector3.Zero;
float delta = (float)gameTime.ElapsedGameTime.TotalSeconds;
if(inputDevice.actualKeyboardState.IsKeyDown(Keys.Up))
pitch -= rotationRate * delta;
if (inputDevice.actualKeyboardState.IsKeyDown(Keys.Down))
pitch += rotationRate * delta;
if (pitch > 89)
pitch = 89;
if (pitch < -89)
pitch = -89;
if (inputDevice.actualKeyboardState.IsKeyDown(Keys.Right))
yaw -= rotationRate * delta;
if (inputDevice.actualKeyboardState.IsKeyDown(Keys.Left))
yaw += rotationRate * delta;
if (yaw > 360)
yaw -= 360;
if (yaw < 0)
yaw += 360;
if (inputDevice.actualKeyboardState.IsKeyDown(Keys.W))
movimento.Z -= movementRate * delta;
if (inputDevice.actualKeyboardState.IsKeyDown(Keys.S))
movimento.Z += movementRate * delta;

Matrix rotation = Matrix.CreateFromYawPitchRoll(MathHelper.ToRadians(yaw),
MathHelper.ToRadians(pitch), 0.0f);
Vector3.Transform(ref movimento, ref rotation, out movimento);
position += movimento;
Vector3 transformedReference;
Vector3 dir = new Vector3(0, 0, -1);
Vector3.Transform(ref dir, ref rotation, out transformedReference);
Vector3.Add(ref position, ref transformedReference, out target);
}


MovementRate e RotationRate sono due variabili definite nella classe e ci dicono quanto velocemente si muove e ruota la nostra telecamera.
A seconda dei tasti che premiamo andiamo a modifcare i valori di Pitch e Yaw (che stanno rispettivamente per l'angolo sull'asse X ed Y), oppure a spostare la posizone della telecamera.
Usando yaw e pitch creo una matrice di rotazione, che uso per trasformare (cioè per spostare lungo la direzione data dalla matrice) la posizione della telecamera.
La stessa cosa la faccio per la posizione del punto che sto osservando, cioè per il target.
Come possiamo vedere in questo metodo non c'è il calcolo per la creazione della view e della projection: qui si aggiorna solo il valore della posizione e del target, la creazione viene fatta nel metodo update.

E questo è tutto.
Ora si deve solo andare in Game1.cs e mettere questo nel metodo Initialize:

defaultEffect = Content.Load<Effect>(@"Effects\Default");

mman = new MeteorManager(this, defaultEffect, 5, maxX,
maxY, maxZ, @"Models\asteroid1", @"Textures\asteroid1");
Components.Add(mman);

Le due variabili vanno naturalmente create prima ^^
Questo ci dice di caricare il file Default.fx, e di creare il MeteorManager che avrà 5 elementi per ogni sezione.

Dato che il MeteorManager è un GameComponent e non un DrawableGameComponent bisogna dirgli noi quando renderizzarlo!
Quindi nel metodo Draw di Game1.cs va messo questo:
mman.Draw(gameTime);

Ma veramente serve tutta questa palla del CameraBoundingFrustum, delle collisioni e tutto il resto?
Fate una prova:
Andate nel file del MeteorManager, nel Draw(..) mettere val = 8 e commentate la riga dove fate il controllo sul risultato del CameraBoundingFrustum.Contains(..).
Io riesco ad ottenere soltanto 5-6 FPS.
Facendo il controllo passo a circa 70.

Voi che dite? Serve?
Per me si ^___^

Poi almeno così si è visto come creare un manager per un insieme di oggetti, e come cominciare a buttare giù una semplice struttura per un gioco... qui il compito è..... contatare tutti gli asteroidi XD Sai che spasso XD
Scherzi a parte, potrebbe comunque essere un'idea di base per la struttura di un semplice gioco!

Ci dovrebbero essere altri metodi per semplificare il rendering di istanze multiple dello stesso elemento, ma ancora non sono in grado di applicarli.
Spero vi sia stato utile tutto questo.

Se ci sono cose che non tornanto o sbagliate o che si possono fare meglio (il che è molto probabile) ditelo please :)

Alla prossima!

Intanto ecco i file allegati!
XNA-tut5.rar

Argomenti trattati:
Free camera
Camera Bounding Frustum
Continua a leggere!

Donazioni

My Menu'