• Non ci sono risultati.

1. INTRODUZIONE

2.1 UNREAL ENGINE

2.1.4 SISTEMA DI EVENTI

Gli eventi di Unreal sono rappresentati tramite i nodi di un grafo detto

event graph. Essi vengono chiamati dal codice del gameplay per fare

iniziare l'esecuzione di una rete individuale (individual network, concatenazione di chiamate di funzione, variabili e nodi di controllo di flusso conseguenza di un determinato evento).

Per event graph si intende un grafo che utilizza eventi e chiamate di funzione per effettuare azioni in risposta alle situazioni che si verificano nel gameplay. L'event graph è una delle componenti fondamentali del blueprint di un livello. Possono esistere vari event graph, almeno uno per ogni livello di cui vogliamo descrivere le interazioni. Per utilizzare un event graph bisogna innanzitutto stabilire i suoi punti di ingresso (uno o più eventi), poi connetterli a chiamate di funzione (function call), nodi di controllo di flusso (flow-control nodes) e variabili di vario genere, definendo, così, il comportamento desiderato. Quando un evento viene sollevato, bisogna immaginare che una sorta di inpulso (pulse), parta da esso, sollecitando l'azione dei nodi successivi ad esso collegati. Quando un nodo riceve tale impulso si attiva e fa ciò per cui è stato programmato, utilizzando tutti gli input di cui necessita e producendo tutti gli output stabiliti.

All'interno di un singolo event graph è possibile inserire un numero arbitrario di eventi. Ciascuno di essi è caratterizzato da un output pin (da cui fuoriesce il pulse) e da uno o più output data pin (da cui fuoriescono i dati necessari al prossimo nodo dell'event graph). Sottolineiamo come si possa avere un solo evento di default del medesimo tipo per ciascun event graph. Mostriamo il funzionamento di alcuni eventi di default:

L'evento Level Reset viene sollevato quando il livello ricomincia. E'

utile quando si vuole far succedere qualcosa se il livello viene ricaricato per qualche motivo, ad esempio dopo la morte del personaggio. In questo caso, al verificarsi dell'evento, viene eseguita la funzione Set Actor Transform,

che applica al target Player la trasformazione Level Start, che resetta la

matrice di trasformazione del personaggio ai valori di default.

Figura 2.44: Esempio di collegamento di una funzione (callback) all'evento di Level Restart.

L'evento Actor Begin Overlap viene sollevato quando due attori si

intersecano. Nell'esempio sotto riportato, quando il Player interseca l'altro

Figura 2.45: Esempio di collegamento di una funzione (callback) all'evento di Actor Begin Overlap.

Notiamo come venga utilizzato un nodo di controllo di flusso per effettuare tale operzione, il nodo Branch. Quest'ultimo consente di impostare

una scelta nel flusso di esecuzione del grafo degli eventi ed ha bisogno di una condizione e di una funzione da eseguire sul ramo true e sul ramo false

(eventualmente anche solo su uno dei due).

Esistono numerose altri nodi di controllo di flusso, tra i quali citiamo: il nodo DoN, che manda in esecuzione la stessa catena di nodi per un

massimo di N volte. Una volta raggiunta la condizione di terminazione non agisce, a meno che un impulso raggiunga il suo input di Reset.

Nell'esempio sottostante vediamo come, premendo il tasto W, oppure S,

il valore della variabile throttle viene impostato ad 1 e l'actor Veichle Movement viene attivato. Dopo aver premuto venti volte W oppure S, però, il

nodo DoN smette di funzionare, a meno che non si schiacci F, che resetta il

funzionamento del nodo. Si potrebbe utilizzare un simile meccanismo, ad esempio, per simulare venti accensioni consecutive di un veicolo e poi la necessità di rimettere carburante per farlo ripartire.

Figura 2.46: Esempio di utilizzo del costrutto di control-flow DoN.

Esistono anche il DoOnce, il ForLoop, il WhileLoop ed il FlipFlop,

di cui mostriamo un esempio in figura. Quest'ultimo consiste nell'alternare l'esecuzione di due funzioni ad ogni pulse. Ai pulse dispari corrisponde l'esecuzione del ramo A del FlipFlop, ai pulse pari l'esecuzione del ramo B.

Figura 2.47: Esempio di utilizzo del costrutto di control-flow FlipFlop.

E' importantissimo sottolineare la possibilità dell'utente di definire

eventi personalizzati, detti custom events. Rispetto ai normali eventi è

possibile averne molteplici istanze all'interno dello stesso event graph. Inoltre è necessario scrivere la funzione che innesca (triggera) l'evento, visto che non sono presenti condizioni di attivazione di default.

e funzioni come ulteriori componenti di un event graph. Esistono vari tipi di variabili, ciascuna rappresentata da un nodo di un diverso colore.

Figura 2.48: Esempi di variabili utilizzabili nei bluprint di Unreal.

E' possibile crearne di nuove nella GUI tramite la finestra del level blueprint,

così come determinare il loro tipo e stabilire il loro valore. Una variabile può essere resa pubblica o privata (non accessibile da blueprint derivati) e può essere sia passata come parametro, sia restituita come output, sia acceduta, sia modificata. Queste ultime due operazioni vengono effettuati con i nodi

Get e Set.

Figura 2.50: Funzioni getter e setter della variabile My Float Variable.

Le funzioni sono un altro fondamentale aspetto di un event graph. Sono nodi che eseguono un determinato compito precedentemente programato. Hanno un input pin ed un output pin e tanti input data pin ed output data pin quanti sono i parametri in input ed i valori restituiti. Creabili anch'esse tramite la GUI, possono essere pubbliche (public, qualsiasi oggetto può invocarle), protette (protected, la funzione è invocabile soltanto all'interno del blueprint corrente e nei blueprint che da esso derivano), oppure

private (la funzione è invocabile esclusivamente all'interno del blueprint

corrente).

Figura 2.51: Menu a tendina per la creazione di una nuova funzione. E' possibile esplicitarne sia gli input

Si distinguono in funzioni pure e funzioni impure. Una funzione pura non può modificare in nessun modo i membri di una classe (nessun effetto laterale), mentre una funzione impura può modificare lo stato (effetti laterali). Quando creiamo una funzione, possiamo impostarne gli input e gli output, inoltre possiamo specificare i calcoli da essa compiuti in modo schematico. Ad esempio il calcolo:

______________________________________________________________ dx = (x2-x1)^2 dy = (y2-y1)^2 dz = (z2-z1)^2 D = sqrt(dx+dy+dz) ______________________________________________________________ Listato 2.3: Pseudo-codice del semplice calcolo della distanza euclidea tra due punti nello spazio 3D.

Si può rappresentare con il seguente piccolo grafo:

Figura 2.52: Implementazione, tramite bluprint dello pseudo-codice contenuto nel Listato 2.3.

Figura 2.53: Nodo funzione del calcolo rappresentato in Figura 2.52.

Sebbene gli eventi siamo facilmente gestibili ed impostabili tramite bluprint, la verità è che questa comoda interfaccia nasconde la reale implementazione di quest'ultimi, che sono realizzati utilizzando il meccanismo dei delegate, implementati appositamente dal team Unreal in C++, in quanto non nativi del linguaggio.

Per delegate si intende una classe D che contiene il puntatore ad un

metodo mC di un'altra classe C e che mette a disposizione un suo metodo mD

per eseguire mC. La grande utilità dei delegate sta nel consentirci di utilizzare

un metodo come se fosse una qualsiasi variabile, potendolo, quindi, passare come parametro e restituire come risultato di una funzione. I delegate si distinguono in single-cast e multi-cast. Un delegate single-cast ci consente di registrare un solo metodo di un'altra classe, mentre un delegate multi-cast ci consente di registrare un numero arbitrario di metodi di altre classi. I metodi vengono registrati o deregistrati su un delegate mediante i suoi metodi: ______________________________________________________________ Bind(); UnBind(); Add(); Remove();

______________________________________________________________ Listato 2.4: Metodi dei single-cast e multi-cast delegate per registrare o deregistrare su di essi un metodo

(callback) di una qualsivoglia altra classe.

Dove i primi due sono relativi ai single-cast delegate ed il terzo ed il quarto sono relativi ai multi-cast delegate. Un delegate può essere eseguito mediante i suoi metodi:

______________________________________________________________ Execute();

Broadcast();

______________________________________________________________ Listato 2.5: Metodi dei single-cast e multi-cast delegate per eseguire la callback (o tutte le callback) su di

essi registrate.

Dove il primo è relativo ai single-cast delegate ed il secondo è relativo ai multi-cast delegate e manda in esecuzione tutti i metodi registrati sul delegate corrente. Utilizzare i delegate è molto utile per poter eseguire metodi di una certa classe all'interno di una qualsivoglia altra senza avere direttamente accesso ad una istanza della classe stessa. Mostriamo un esempio di utilizzo di un single-cast delegate di Unreal:

Immaginiamo di avere una classe con un metodo che vogliamo poter invocare da qualsiasi altra classe in cui ci troviamo:

______________________________________________________________ class FLogWriter { void WriteToLog(FString); }; ______________________________________________________________ Listato 2.6: Classe per la scrittura su file di log.

Creiamo a tale scopo un delegate che rispetti la firma del metodo, utilizzando la seguente macro:

______________________________________________________________ DECLARE_DELEGATE_OneParam(FStringDelegate, FString);

______________________________________________________________ Listato 2.7: Dichiarazione di un nuovo tipo di delegate.

Essa crea un nuovo tipo di delegate detto FstringDelegate che prende un

solo parametro di tipo Fstring. Utilizziamo ora il nostro delegate all'interno

di una classe da noi definita:

______________________________________________________________ class FMyClass { FStringDelegate WriteToLogDelegate; }; ______________________________________________________________ Listato 2.8: Creazione di una nuova classe contenente come variabile di istanza il delegate appena definito.

Questo consente alla nostra classe di possedere un puntatore ad un metodo di un'altra qualsiasi classe. Adesso assegniamo il delegate in questo modo: ______________________________________________________________ FmyClass instance();

FSharedRef<FLogWriter>LogWriter(new FLogWriter());

instance.WriteToLogDelegate.BindSP(LogWriter,

&FLogWriter::WriteToLog);

______________________________________________________________ Listato 2.9: Binding del delegate al metodo della classe mostrata nel Listato 2.6.

Ovvero creiamo un'istanza della classe FmyClass, contenente il delegate,

dopodiché effettuiamo il binding tra il metodo WriteToLogDelegate ed il

un delegate ad un metodo di una classe.

Adesso è possibile invocare il delegate semplicemente scrivendo così:

______________________________________________________________ instance.WriteToLogDelegate.Execute(

TEXT("GodBlessDelegates"));

______________________________________________________________ Listato 2.10: Invocazione del single-cast delegate.

Detto questo, chiariamo come gli eventi non siano altro che particolari multi-cast delegate. Quando si crea un evento è possibile, quindi, registrare su di esso un qualsivoglia numero di metodi e funzioni appartenenti ad altre classi. E' possibile, poi, attivare tutti questi metodi mediante la

Broadcast(), che provoca l'esecuzione di tutti i metodi correntemente

registrati sull'evento.

La creazione e l'attivazione di un evento sono assolutamente analoghe alle operazioni descritte per un single-cast delegate. Basta sostituire la

DECLARE_DELEGATE con la DECLARE_EVENT, la Bind() con la Add(), e la

Execute() con la Broadcast(). Ci troviamo, d'altronde, in presenza di un

particolare multi-cast delegate, quindi è possibile registrare più metodi sullo stesso evento ed eseguirli tutti contemporaneamente quando si verificano le condizioni che lo triggerano.

E' chiaro, quindi, come un custom event creato tramite blueprint non sia altro che un delegate sul quale è possibile registrare metodi e funzioni [2].

2.1.5 GRAFICA E RENDERING

Il sistema di rendering dell'Unreal Engine 4 è basato sulla pipeline grafica Direct3D 11. I suoi tratti distintivi includono il deferred shading, la gestione della global illumination e della translucency, nonché effetti di

particle simulation. Soffermiamoci innanzitutto sulla pipeline di rendering, descrivendone in modo rapido, ma esauriente il funzionamento.

La pipeline grafica Direct3D 11 si compone dei seguenti stadi:

Input Assembler Stage: i dati primitivi (punti, linee, triangoli)

vengono letti ed assemblati in primitive grafiche quali liste di punti o

cluster di triangoli.

Vertex Shading Stage: i vertex shader sono programmi che si

occupano di processare uno per uno i vertici provenienti dall'input assembler, effettuando su di essi operazioni di skinning, morphing e lighting. Un vertex shader opera sempre su un singolo vertice in input, producendo un singolo vertice in output.

Tessellation Stages: la tessellation consiste nel trasformare una

superficie poligonale scarsamente dettagliata in una superficie estremamente più liscia (smooth) e piena di dettagli. Tale effetto si ottiene suddividendo una certa superficie in tantissime altre piccole

superfici (in questo caso triangoli). In Direct3D 11 questo stadio si

compone di altri 3 sotto-stadi:

Hull Stage: produce una patch (particolare tipo di superficie)

geometrica a partire da patch più semplici (triangoli, quadrati, linee).

Tessellation Stage: produce un framework di samples sulla patch,

per poi suddividerla in un insieme di oggetti più piccoli (triangoli, quadrati, linee) che interconnettono i suddetti sample tra di loro.

Domain Shader Stage: converte i vertici dei sample in vertici del

mondo.

Geometry Shading Stage: differentemente dai vertex shader, i

geometry shader prendono in input le primitive nella loro interezza. Dopodiché applicano un'operazione su ciascuna di esse e le restituiscono una alla volta. Alcuni tipici esempi di operazioni di geometry shading sono:

Point Sprite Expansion: data una primitiva in input, è possibile

incrementare il numero dei suoi vertici; questa feature viene spesso utilizzata per la generazione di particelle (dynamic particle system);

Fur Generation: generazione di pellicce e piumaggio.

Shadow Volume Generation: tecnica per aggiungere ombre ad

una scena renderizzata. Uno shadow volume è una geometria che descrive la forma 3D di una regione occlusa dalla luce.

Single Pass Render To Cubemap: una cubemap è un metodo di

environment mapping, il quale, a sua volta, è una forma di image based lighting, ossia di illuminazione di un ambiente tramite texture. La texture in questione contiene una rappresentazione

omnidirezionale della luce del mondo. Applicandola ad un oggetto questo ci appare perfettamente illuminato, relativamente però al punto di vista specifico rispetto al quale la texture è stata creata. Il

cubemapping è un'evoluzione estremamente più efficace ed

efficiente dello sphere mapping, la prima forma di image based lighting ad essere stata utilizzata. Mentre quest'ultima tecnica è

estremamente view dependent, ossia la texture di illuminazione deve essere ricalcolata per ogni minima modifica al punto di vista, il cubemapping risulta essere estremamente più versatile. Una cubemap si realizza renderizzando una singola scena sei volte da un certo punto di vista e proiettandola sulle sei facce di un cubo

srotolato, salvato come una singola texture. Grazie ai moderni

geometry shader la creazione di una cube map può essere realizzata in un solo step della GPU, invece di eseguire, come di prassi, sei render consecutivi dello stesso triangolo.

Stream Output Stage: scopo di questo step è di indirizzare i dati

inerenti i vertici prodotti dallo stadio di vertex shading o geometry shading verso uno o più buffer in memoria. I dati in memoria potranno essere successivamente ricaricati nella pipeline, oppure elaborati dalla CPU.

Rasterization Stage: la rasterizzazione converte le informazioni vettoriali (forme e primitive) in una immagine raster (composta di pixel), al fine di visualizzare sul display grafica 3D in tempo reale. Durante la rasterizzazione ciascuna primitiva viene convertita in informazioni pixel. La rasterizzazione prevede:

Clipping dei vertici secondo il view frustum.Division by z axis per ottenere la prospettiva.Mapping delle primitive su un viewport 2D.

Definizione politiche di invocazione di un pixel shader

(opzionale).

Pixel Shading.

il per-pixel lighting ed il post-processing. Lo stadio di rasterizzazione invoca il pixel shader per ogni pixel coperto da una primitiva. Il depth/stencil test viene utilizzato per risolvere casi di

sovrapposizione di primitive sullo stesso pixel e decidere così le

caratteristiche della sua illuminazione.

Output Merger Stage: questo stadio genera il colore finale

renderizzato da ciascun pixel utilizzando le seguenti informazioni: lo stato della pipeline, i dati sui pixel generati dai pixel shaders, i contenuti dei render targets, i contenuti dei dispatch/stencil buffers. Si tratta dello step finale per determinare quali pixel siano visibili (depht/stencil test) e per miscelare i colori finali di ciascuno di essi

[3].

2.1.6 IMPRESSIONI DI UTILIZZO (UNREAL ENGINE)

L'interfaccia risulta essere da subito estremamente usabile, intuitiva ed accattivante. Sono presenti sul web tutorial in abbondanza per iniziare ad approcciarsi a questo immenso strumento, densissimo di caratteristiche e potenzialità.

Di contro, i tempi medi di caricamento su una macchina non estremamente prestante sono a dir poco proibitivi, così come i tempi di compilazione. Soltanto inserire una nuova classe nel progetto può richiedere decine di secondi, per non parlare del calcolo di complesse illuminazioni.

Discutibile, inoltre, è l'obbligo di utilizzare come IDE di sviluppo per il codice C++ esclusivamente VisualStudio su Windows e xCode su Mac, scelta commerciale che rema decisamente contro il mondo open source ed i numerosi IDE alternativi esistenti.

Il mondo 2D non risulta essere affatto il target principale dell'engine, che, pur mettendo a disposizione l'editor paper 2D, risulta essere molto più indietro rispetto a Unity in questo settore, il quale ha realizzato addirittura una versione dell'engine specifica per il 2D ed è possibile selezionarla al momento della creazione del progetto.

2.2 UNITY

2.2.1 CENNI STORICI

Pubblicato nel Giugno 2005, Unity nasce dal lavoro congiunto dei danesi Nicholas Francis e David Helgason e del tedesco Joachim Ante. Originariamente chiamata OTEE (Over The Edge Entretainment) la società da loro fondata fu successivamente ridenominata Unity Technologies Aps nel 2006 ed infine Unity Technologies SF nel 2009. Ha sede a Copenaghen (Danimarca).

I tre programmatori ricavarono il nucleo di Unity dal motore di gioco che avevano scritto per GooBall, il loro primo videogame, il quale non riscosse, tuttavia, il successo sperato. Pensarono, pertanto, di sfruttare l'engine che avevano ideato e di iniziare a sviluppare per gli sviluppatori; l'obiettivo era quello di democratizzare il game development e renderlo accessibile a più persone possibili al mondo. Differentemente da ciò che è accaduto per l'Unreal Engine, i videogame programmati con Unity nel corso della sua storia non sono quasi mai stati titoli famosissimi, bensì principalmente lavori indipendenti orientati alla grafica 2D ed alle

piattaforme mobili. Soltanto in tempi recenti, infatti, sono comparsi i primi

titoli per console, i quali sono, in ogni caso, in minoranza rispetto al consistente numero di browser game ed applicazioni mobili sviluppate.

Tra i giochi appartenenti alla fase iniziale della vita dell'engine ricordiamo:

 GooBall (2005, OTEE)

 Dead Frontier (2008, Creaky Corpse Ltd)  Max & The Magic Marker (2010, Press Play)

 Temple Run (2011, Imangi Studios)  Endless Space (2012, Amplitude Studios)  Robocraft (2013, Freejam Games).

Figura 2.55: Immagini di gameplay di Dead Frontier su PC e di Gooball su MacOs X.

Figura 2.56: Immagini di gameplaydi Temple Run su Android e di Endless Space su Windows 10.

Tra i videogames che hanno caratterizzato, invece, l'epoca più recente di Unity ricordiamo:

 Angry Birds Epic (2014, Chimera Entertainment)

 Heartstone: Heroes Of Warcraft (2014, Blizzard Entertainment)  Ori And The Blind Forest (2015, Moon Studios)

 Pillars Of Eternity (2015, Obsidian Entertainment)  Pokemon Go (2016, Niantic)

Figura 2.57: Immagine di gameplay di di Angry Birds Epic su iOS e di HearthStone su Windows 10.

Figura 2.58: Immagine di gameplay di Pillars Of Eternity suWindows 10.

2.2.2 STRUTTURA E TERMINOLOGIA

Unity è strutturato per creare sia giochi 2D sia giochi 3D. Quando si

crea un nuovo progetto Unity è possibile scegliere tra la modalità 2D e quella 3D. A seconda della scelta effettuata, le immagini del progetto vengono importate sotto forma di sprites o di textures.

Le più comuni tipologie di stili grafici adottate dai programmatori Unity sono:

 Full 3D: modelli tridimensionali, materiali, texture, telecamera mobile;

 Full 2D: sprite, disegni, telecamera fissa;

gameplay fissato su due dimensioni;

 Cardboard Theatre: videogame 2D in cui la telecamera viene spostata per ottenere effetti di parallasse.

Il mondo di gioco Unity è costituito dai cosiddetti gameObject,

ciascuno dei quali è caratterizzato da un numero arbitrario di component. Ogni component aggiunge una o più feature ad un certo oggetto di gioco; similmente alle component di Unreal, esse non hanno senso di esistere se disaccoppiate dal proprio gameObject.

Ogni gameObject deriva dalla classe MonoBehaviour. I metodi di cui

una classe che eredita da MonoBehaviour può fare l'override sono:

Awake(): inizializza il gameObject (anche se non abilitato), chiamato

una sola volta durante la lifetime dell'oggetto.

Start(): inizializza il gameObject (solo se abilitato), chiamato una

sola volta durante la lifetime dell'oggetto.

Update(): aggiornamento frame per frame dello stato dell'oggetto.

FixedUpdate(): aggiornamento con framerate fisso dello stato

dell'oggetto. Stabilito un intervallo di update di U millisecondi (e.g 2

ms), se non è stato possibile rispettare tale intervallo e dal frame precedente è passato un delta di M millisecondi (e.g 8 ms) la funzione

viene chiamata N volte di seguito, con N = M / U volte (e.g. 4 volte).

LateUpdate(): aggiornamento dello stato dell'oggetto; avviene

quando tutte le altre Update() sono state completate.

OnGui(): gestisce e renderizza gli eventi della GUI, chiamato varie

volte per frame.

OnDisable(): chiamata quando l'oggetto viene disabilitato o distrutto.

Documenti correlati