Aiuto - Cerca - Utenti - Calendario
Versione completa: Unity tips
OldGamesItalia > Area Dev > Made in Italy
Lief
Come forse qualcuno di voi saprà da un po' di tempo mi occupo dello sviluppo di giochi in Unity a livello personale (ma a livello professionale sono comunque Web Developer back e front end) e mi capita spesso di scontrarmi con problematiche di vario genere che possono far perdere giornate intere ma che non sempre hanno soluzioni realmente complesse.

Questo topic vuole semplicemente essere un elenco di suggerimenti aperto a tutti.
Io in particolare posterò suggerimenti relativi a Unity di tanto in tanto quando penserò che vale la pena farlo.

Elenco tips:
- Remap Input Personalizzato
- Virtual Joystick
- FPS Constructor by Leonardo Boselli
- Serializzazione, Traduzione, Salvataggio Dati
- Drag&Drop
- Muovere un Oggetto con un Click
- Hack & Slash RPG da zero BurgZergArcade
- Mischiare una lista di oggetti non iterabili
- Collider e Trigger, tutti i segreti
- Tocchiamo il terreno? Raycast camera, come sparare raggi in tutte le direzioni
- Serializzazione Multi-livello. Oggetti complessi custom.
- Oauth 2.0 e Save Cloud - Parte 1: La teoria
- Oauth 2.0 e Save Cloud - Parte 2: Google Drive - Autenticazione, Upload, Download

Come primo tips voglio iniziare con il problema che mi sono ritrovato ad affrontare più recentemente, problema che in un prossimo futuro non sarà più rilevante: il Remap dell'input in Unity a runtime.

Avete presente il menù per cambiare i comandi da usare nel gioco (esempio per cambiare il comando per saltare da Spazio ad un qualsiasi altro bottone)?
È una classica opzione che viene data ai giocatori PC (mentre è spesso assente su console) ed è anche una delle più grandi limitazioni del sistema di input attuale di Unity. Unity infatti permette il remap dell'Input solo prima di avviare il gioco in una schermata completamente separata dal gioco tipica dei videogiochi Unity.

Come risolvere il problema?
- Usare il nuovo Input System, che però è ancora in una fase di sviluppo molto embrionale, richiede una versione di Unity diversa da quella di default, è soggetto a bug e cambiamenti. In futuro ovviamente questa sarà la migliore opzione disponibile e il tutto sarà decisamente più semplice.
- Comprare sull'assets store o scaricare su git uno dei tanti sistemi di Input alternativi. Sono sistemi che però richiedono l'import di molto codice che spesso non ci interessa e che va mantenuto ad ogni aggiornamento di Unity, il che può essere giustificato ma è comunque qualcosa che è meglio evitare se si è in grado di farlo.
- Creare il nostro personalissimo sistema di Input.

Se avete deciso di usare il terzo sistema vi posso dare una mano.
Prima di tutto bisogna dire che rimappare semplicemente i tasti è decisamente semplice, vi basterà creare una mappa chiave, valore impostando la chiave a stringaNomeInput (esempio "Salto"), il valore al KeyCode a cui lo volete associare (esempio KeyCode.Space)... A quel punto fate due metodi un getter e un setter e, durante lo Start, settate tutti i valori.
Per ricavare l'input vi basterà sostituire nel codice i vostri vecchi:
if(Input.GetButtonDown("Salto"))

con

if(Input.GetKeyDown(VostraClasseInput.GetInput("Salto")))

se prima Input dipendeva da un sistema settato nell'InputManager di Unity, ora dipenderà dal vostro sistema che restituirà un KeyCode nella mappa a seconda della chiave passata e il tutto verrà letto con l'Input.GetKey GetKeyDown GetKeyUp del sistema di default di Unity.

Nel vostro menù di remap vi basterà richiamare il
VostraClasseInput.SetInput("Salto", key) (dove key è il KeyCode rilevato)
per cambiare il valore nella mappa e far rispondere il GetKeyDown ad un altro pulsante.

Principale problema:
Gli Axis non hanno KeyCode quindi come fare a rilevare il float relativo al movimento?

Per quanto riguarda la tastiera è sufficiente aggirare il problema:

float avanti = 0f;

if(Input.GetKey(VostraClasseInput.GetInput("Avanti")))
avanti = 1f;
else if(Input.GetKey(VostraClasseInput.GetInput("Indietro")))
avanti = -1f;
else
avanti = 0f;

Leggendo il float avanti possiamo sapere se l'utente sta premendo il pulsante per andare avanti, quello per andare indietro oppure non si sta muovendo.

Ovviamente il codice si può abbellire, ad esempio si può aggiungere un sistema per incrementare la variabile "avanti" da 0 ad 1 non in maniera istantanea:
avanti = avanti < 1f ? avanti + Mathf.Lerp(0, 1, Time.deltaTime * 5) : 1f;

il codice sopra significa: finché la variabile "avanti" è minore di 1 incrementa avanti da 0 a 1 in base al tempo moltiplicato per 5 (il che significa che potete variare questo tempo per rendere il tutto più lento o più veloce), in caso contrario (quando la variabile raggiunge 1) vogliamo che non venga più incrementata.

Tutto questo però è ok su tastiera, se volessimo usare un gamepad inizierebbero i problemi. I joystick infatti hanno un KeyCode variabile rilevabile solo quando si preme il joystick, non quando si stanno usando gli axis.
Inoltre, il sistema che abbiamo usato prima non ha senso per un joystick (visto che un joystick può rilevare valori intermedi tra 0 e 1 che possono venir utilizzati).

Cosa fare quindi?

L'idea è simile a quella usata in precedenza: usare il vecchio sistema di Input a nostro vantaggio. Questa volta l'idea è creare nel vecchio sistema di Input tutti gli Axis che siamo certi di voler utilizzare (esempio VerticalWindows, VerticalMac, VerticalLinux, VerticalIOS, VerticalAndroid) e settare quali assi dovranno leggere, di base tutti i joystick leggono un asse verticale e uno orizzontale (X e Y) per il joystick di sinistra, mentre la cosa varia per il joystick di destra e il d-pad a seconda della piattaforma (esempio su windows il joystick di destra è sugli assi 4 e 5, mentre su mac sono gli assi 3 e 4).

Nella nostra classe di input bisognerà aggiungere una mappa chiave valore stringa-stringa, la chiave sarà come richiameremo la lettura dell'asse nella get, mentre il valore sarà il reale nome del joystick settato nel vecchio InputManager.
Esempio

Nell'Input Manager abbiamo dato il nome "Vertical" e "Horizontal" agli assi X e Y, "RightVertical" e "RightHorizontal" agli assi 4 e 5.

Nella nostra mappa dovremo mappare questi assi in un modo simile a questo:
VostraClasseInput.SetJoypad("JoyVertical", "Vertical");
VostraClasseInput.SetJoypad("JoyHorizontal", "Horizontal");
VostraClasseInput.SetJoypad("JoyVertical2", "RightVertical");
VostraClasseInput.SetJoypad("JoyHorizontal2", "RightHorizontal");

Ovviamente se volessimo rimappare il tutto in modo da invertire ciò che vogliamo fare con joystick sinistro e destro, nel nostro sistema di remap, al passo di rilevare il joystick utilizzato (cosa che richiederebbe l'associazione di un keycode che varia da piattaforma a piattaforma e non sempre è disponibile (ad esempio non lo è sul dpad)) ci conviene settare il tutto dando semplicemente all'utente la possibilità di cambiare joystick di movimento /camera (esempi) con joystick sinistra, destra o dpad in un sistema di selezione.
VostraClasseInput.SetJoypad("JoyVertical", "RightVertical");
VostraClasseInput.SetJoypad("JoyHorizontal", "RightHorizontal");
VostraClasseInput.SetJoypad("JoyVertical2", "Vertical");
VostraClasseInput.SetJoypad("JoyHorizontal2", "Horizontal");


Per richiamare il tutto invece ci basta il sistema usato per i bottini con la sola differenza che ci serve Input.GetAxis al posto di GetKey.

Ovviamente sarebbe molto più semplice se unity fosse in grado di fare setaxis e setbutton e magari anche di rilevare l'axis usato ma sfortunatamente al momento non pare sia possibile (lo sarà con il prossimo sistema di input), ovviamente se avete idee per migliorare il mio tip scrivetele pure e spero che anche altri vorranno condividere i propri.

Al prossimo tip
Lief
Oggi vi lascio con un video (non mio) relativo ai comandi touch screen. A dirla tutta ho fatto qualche modifica a questo codice ma solo per renderlo più legato all'interfaccia Unity3d, zucchero sintattico insomma, il core è rimasto questo:


Penso che sia il modo migliore per creare un virtual joystick abbastanza professionale.
utdefault
Grazie per le tips.
Per chi non conosce l'engine, tornano utili.

Il video postato non si vede. Se puoi ripostarlo, please, aggiungici anche il link originale; così da poterlo aprire esternamente se l'embedded non funzionasse ancora. ;>

In ultimo, una domanda su Unity: come gestisce le traduzioni? -- Cioè, se fa qualcosa di simile ad Ags oppure se va gestito da code.
Gwenelan
Corretto io l'embedded blush.gif! Va messo solo il codice del video, non tutto il link blush.gif.
Leonardo Boselli
Bello questo thread! Con l'input ho smanettato poco, ma proverò di certo a seguire i consigli dei post qui sopra.

Quando mi diletto con Unity, mi diverto a mettere assieme un po' di assets gratuiti per saggiarne le potenzialità e vedere l'effetto che fanno tutti assieme.
Un asset gratuito che ho trovato interessante è "FPS constructor". Un po' datato, ma ancora funzionale (e funzionante!)
Naturalmente non ho perso l'occasione per pubblicare un video-tutorial dove inizio ad abbozzare un FPS grazie a questo tool.

Ed ecco il risultato:


Lief
@Gwenelan grazie per aver corretto.


CITAZIONE (utdefault @ 23 Apr 2018, 23:29) *
In ultimo, una domanda su Unity: come gestisce le traduzioni? -- Cioè, se fa qualcosa di simile ad Ags oppure se va gestito da code.

Le traduzioni vanno gestite via codice, al momento non mi pare esista un metodo ufficiale per farlo da interfaccia (Ags non lo conosco ma immagino, da quel che scrivi, che abbia un sistema particolare per gestirle).

Detto questo gestirle da codice non è poi così complesso come si potrebbe pensare... Ti do un indizio (ora non ho tempo di scrivere un tip completo, lo farò la prossima volta):
file json contenenti le stringhe di gioco caricati a seconda della lingua (magari con un nome diverso) e serializzati in array di stringhe in C#.

Una volta capita la serializzazione in Unity è decisamente solo una questione di scrivere 2 righe di codice molto molto semplici per caricare il file della lingua che ci interessa. I menù che ho creato usano proprio questo sistema e, dopo aver provato alcune alternative (tipo il sistema di PlayerPref) penso sia il sistema più semplice. Oltretutto è un sistema che (una volta capito) è applicabile anche per il salvataggio e il caricamento dei salvataggi e dei dialoghi in-game (insomma è decisamente molto flessibile).

Aggiungo... json e non xml perché alla fin fine son tutte stringhe, il controllo dei tipi non è decisamente la priorità ed è decisamente più semplice serializzare e deserializzare in json.

@Leonardo Boselli
Penso che sia un ottimo modo per iniziare. Copiare le soluzioni altrui e farle proprie è sempre stato il modo migliore per imparare a programmare. L'unica cosa che, personalmente, cerco di non fare, è integrare soluzioni complete senza reimplementarle e senza guardare il codice. Si rischia di finire con un progetto non aggiornabile, dipendente da un codice che non si capisce.
Non ho ancora visto il video (lo guarderò stasera) quindi non so come hai sfruttato quell'asset, ho voluto solo dire la mia sugli assets in generale (insomma sono un'ottima cosa, ma bisogna sfruttarli per imparare).

PS. Io ho iniziato con i tutotial di Burgzerg Arcade. In particolare quelli dell'RPG che però sono fatti per Unity3d 3 (quindi sono molto obsoleti ora).

Edit. Ok ho visto il video. Come scritto personalmente non userei assets come fps constructor senza analizzarli pezzo per pezzo. Infatti, a meno che non si voglia implementare tutto esattamente nello stesso modo, diventa poi difficile uscire dagli schemi preimpostati (esempio... Se volessi una camera in terza persona dovrei trovare una camera in terza persona compatibile con quel framework) e, come ho già scritto, se unity si aggiorna e rende incompatibile qualcosa poi è necessario aspettare l'aggiornamento di fps constructor per aggiornare unity. In alcuni casi non c'è scelta (esempio se si implementa le api di Dropbox non si può fare nulla), ma in casi come questo si può fare a meno.
È però un'ottima scelta studiare il codice di fps constructor e simili (spesso gli assets sono anche open source anche se non sempre la licenza e free software).
Per quanto riguarda gli assets grafici, al contrario, visto che non sono un grafico penso sia giusto usare placeholder. E voglio anche suggerire un software chiamato make human che è decisamente comodo, gratuito e può aiutare ad avere assets originali e standard per l'uso in unity. Anche il sito di mixamo è ottimo e fornisce tantissime animazioni gratuite oltre a vari modelli (notare che visto che lo scheletro fatto con make human è standard per unity si possono usare le animazioni di mixamo).
Leonardo Boselli
CITAZIONE (Lief @ 24 Apr 2018, 17:53) *
Edit. Ok ho visto il video. Come scritto personalmente non userei assets come fps constructor senza analizzarli pezzo per pezzo. Infatti, a meno che non si voglia implementare tutto esattamente nello stesso modo, diventa poi difficile uscire dagli schemi preimpostati (esempio... Se volessi una camera in terza persona dovrei trovare una camera in terza persona compatibile con quel framework) e, come ho già scritto, se unity si aggiorna e rende incompatibile qualcosa poi è necessario aspettare l'aggiornamento di fps constructor per aggiornare unity. In alcuni casi non c'è scelta (esempio se si implementa le api di Dropbox non si può fare nulla), ma in casi come questo si può fare a meno.
È però un'ottima scelta studiare il codice di fps constructor e simili (spesso gli assets sono anche open source anche se non sempre la licenza e free software).

Sono d'accordo. Comunque mi stupisco di quanto siano fatti bene certi assets gratuiti di Unity e di come si riescano a integrare gli uni con gli altri (pur essendo sviluppati da programmatori diversi).
Per esempio, ho provato la versione gratuita di "MS Vehicle System" (un sistema per il controllo di veicoli a quattro ruote con un sistema di visuali integrato e quant'altro) e l'ho inserito nella scena del video precedente.
Mi ha stupito riuscire a passare dalla visuale dell'FPS a quella del veicolo e viceversa senza alcun codice "glue". Quando poi si spara contro il veicolo, si sente il rumore di vetri infranti blush.gif



CITAZIONE (Lief @ 24 Apr 2018, 17:53) *
E voglio anche suggerire un software chiamato make human che è decisamente comodo, gratuito e può aiutare ad avere assets originali e standard per l'uso in unity. Anche il sito di mixamo è ottimo e fornisce tantissime animazioni gratuite oltre a vari modelli (notare che visto che lo scheletro fatto con make human è standard per unity si possono usare le animazioni di mixamo).

Make Human è davvero fantastico. L'ho provato tempo fa e immagino che sia stato ancora migliorato. Mi hai fatto venire voglia di riprovarlo!
Lief
Si, l'assets store di Unity è uno dei suoi punti forti. Sapere di poter avere a disposizione una quantità infinita di assets ben fatti significa non doversi preoccupare di fare ciò che non si è in grado di fare e concentrarsi sul proprio campo (esempio, io programmatore posso concentrarmi sul codice, un artista del 3d può concentrarsi sui propri modelli ecc...).

Ma passiamo al tip. Come detto qualche ora fa e richiesto da utdefault vediamo come si possono gestire le traduzioni (e più in generale il salvataggio dei dati). Ovviamente questa è solo una delle tante soluzioni possibili... vi invito a fare prove e a darmi anche dei suggerimenti (scrivendo ovviamente dei tip per tutti qui).

Partiamo dalla base: la serializzazione in Unity.
Si parte dal classico POJO (come lo chiamano i programmatori Java), POCO (come lo chiamano i programmatori C, C++, C#)... In pratica un semplicissimo oggetto con campi e getter/setter.

Si crea una classe in Unity, si elimina ": MonoBehaviour" (MOLTO IMPORTANTE), si crea un campo pubblico o SerializeField (MOLTO IMPORTANTE, pur avendo già i getter/setters. SerializeField è l'idea migliore visto che in questo modo la nostra variabile non risulterà accessibile direttamente ma solo attraverso getter e setters):

//un array di stringhe
[SerializeField] string[] menu;

//costruttore vuoto
public ConfirmMenuArrayString() {
}

//costruttore con parametri
public ConfirmMenuArrayString(string[] menu) {
this.menu = menu;
}

//getter/setter
public string[] Menu {
get {
return menu;
}
set {
menu = value;
}
}

e fin qui è una cavolata.

Ora che abbiamo il nostro oggetto dobbiamo renderlo serializzabile, sopra il public class ConfirmMenuArrayString { dobbiamo quindi scrivere
[System.Serializable]

Infine aggiungiamo i metodi per serializzare
//prende in ingresso la stringa json, restituisce l'oggetto
public static ConfirmMenuArrayString SaveFromString(string jsonString) {
return JsonUtility.FromJson<ConfirmMenuArrayString>(jsonString);
}
//fa il contrario, dall'oggetto restituisce la stringa json
public string SaveToString() {
return JsonUtility.ToJson(this);
}


Abbiamo la nostra classe di serializzazione che può prendere un qualsiasi file di testo contenente:
{"menu":["stringa 1", "stringa 2", "stringa 3"]}

e trasformarlo in un array di stringhe di lunghezza arbitraria. Ovviamente la classe di prima si può modificare per salvare singoli campi (al posto che un array), ognuno con il proprio identificativo
{"campo1":"valore campo 1", "campo2":"valore campo 2"}
E i tipi dei campi non devono essere necessariamente stringhe, ma restiamo sul semplice.

Ora dobbiamo utilizzare questa classe per cambiare lingua.
Primo step creare due file json:
Menu_Italian.json
Menu_English.json

contenenti entrambi:
{"menu":["stringa 1", "stringa 2", "stringa 3"]}

uno in inglese:
{"menu":["string 1", "string 2", "string 3"]}
l'altro in italiano.

Notare che menu (essendo il campo, quindi la nostra chiave, non deve venir tradotta... solo i valori (le stringhe) vengono tradotte).

Ma dove mettiamo questi file?
La cartella migliore è senza alcun dubbio Application.streamingAssetsPath che è una cartella diversa a seconda della piattaforma scelta. è inoltre buona norma mettere tutto dentro una nostra cartella:
Application.streamingAssetsPath + "/menu"

E se volessimo esporre il file all'utente?
In quel caso la cartella migliore è Application.persistentDataPath... e se vogliamo fare in modo che un file già presente nel nostro gioco venga esposto lì il modo migliore è farne una semplice copia.

A questo punto abbiamo i file, come decidere quale leggere?
Beh è decisamente semplice, dobbiamo leggere il file in questo path:
Application.streamingAssetsPath + "/menu/Menu_" + language + ".json"

dove language è la stringa che rappresenta la nostra lingua (e che può essere ad esempio la lingua del sistema dell'utente Application.systemLanguage).

letto il file (ad esempio con uno StreamReader)
e messo dentro una variabile di tipo stringa possiamo passare tale variabile al metodo SaveToString che abbiamo creato precedentemente:
ConfirmMenuArrayString.SaveFromString (jsontext);

(è un metodo statico quindi lo richiamiamo con il nome della classe)

se poi richiamiamo ConfirmMenuArrayString.SaveFromString (jsontext).Menu ci verrà direttamente restituito l'array di stringhe che ci serve.

A quel punto ci basta ciclare sull'array e settare tutti i Text component di Unity che ci interessa modificare con le nostre scritte.

Basterà così cambiare la variabile language per cambiare lingua (ovviamente se il tutto viene eseguito nel metodo Start solo all'avvio la lingua potrà essere modificata).
utdefault
Ok, ora tutto più chiaro.
Grazie, Lief.

Casomai, con l'aggiunta di nuovi post argomentativi, ogni messaggio potrebbe essere indicizzato in apertura di discussione. Così da poterli subito richiamare all'occorrenza. :>

Piacevole anche l'immersione nel video di Constructor.
Grazie a Leo per il play. Per ora, visto anche quello sui veicoli. ;>
Lief
CITAZIONE (utdefault @ 25 Apr 2018, 12:36) *
Casomai, con l'aggiunta di nuovi post argomentativi, ogni messaggio potrebbe essere indicizzato in apertura di discussione. Così da poterli subito richiamare all'occorrenza. :>

L'idea è buona, però ci vorrebbe un post sopra il mio primo post (mettere tutto in un post unico è un po' confusionario). Inoltre penso sia bello che tutti possano dare suggerimenti quindi se si indicizzano è bene che chi posta un tip dica esplicitamente che è un tip a inizio del suo post (in modo che l'indicizzazione sia più semplice).
Lief
In questi giorni sarò impegnato nel creare un piccolo giochino di carte come prova/colloquio per una software house (chissà magari è la volta buona che riesco ad entrare nel settore giusto, al posto di fare solo webapp). Non è detto che riuscirò a finirlo perché ho un tempo limite di 4 giorni al posto dei 7 dati (visto che 3 giorni lavoro a tempo pieno e la sera dubito di riuscire ad andare molto avanti), ma ovviamente che io riesca o no a passare, il risultato sarà comunque un piccolo gioco (se non finito quasi) e un po' di esperienza che mi tornerà utile anche in futuro.... e ovviamente qualche tip per tutti.

Quello di oggi è il Drag & Drop. Farlo non è stato particolarmente difficile visto che avevo già creato un virtual joystick per il mio videogioco principale, ma mi sono comunque chiarito parecchi dubbi con questo semplice video tutorial che consiglio a tutti (nota bene, questo è il primo di 3 video, gli altri li trovate via youtube):


A questo tutorial voglio solo aggiungere un suggerimento: se volete poter trascinare più di una carta alla volta (o solo una), perché ad esempio avete una pila di carte (esempio: K, Q, J, 10) vi basterà aggiungere un Vertical/Horizontal Layout Group a ciascuna carta.
In questa maniera le carte stesse diverranno potenziali "host" dove posizionare le carte...
Piccolo esempio, volendo fare una pila verticale si potrebbe far sì che ogni carta abbia un vertical layout group con top = X (X = numero che ci convince di più... con top positivo le carte figlie della prima appariranno sovrapposte). Iterando su questo concetto è possibile avere tante carte tutte trascinabili in cui quelle sotto trascineranno anche quelle sopra ma non viceversa.
Ricordarsi inoltre che se abbiamo iterato su questo concetto il parent del parent non sarà più il nostro panel, di conseguenza conviene settarsi un Transform del nostro Panel principale nel codice (per evitare di settarla con una variabile pubblica, si potrebbe ad esempio usare GameObject.FindGameObjectWithTag("Panel").transform; nello Start).

Altra cosa, nel video viene menzionato il fatto che si può usare il component event trigger per richiamare i metodi drag, drop ecc....
Quel che non viene detto è il fatto che richiamandoli in quel modo non c'è possibilità (che io sappia) di accedere al PointerEventData (che è l'oggetto che ci da accesso alla posizione).
Il mio suggerimento è di implementare solo IBeginDragHandler e il metodo public void OnBeginDrag(PointerEventData myPed) avendo una variabile private PointerEventData ped; e settandola nell'OnBeginDrag ped = myPed;
Tutti gli altri metodi potranno così accedere al PointerEventData che avete settato, e potrete settare tutti gli altri metodi direttamente nell'Event Trigger di Unity.

Per oggi direi che è tutto, spero di poter postare altri tip interessanti in questi giorni.
Lief
Salve a tutti. Oggi vi porto un tip davvero carino... e semplice da utilizzare.
Nel mio primo tip vi ho detto che con questo codice
"avanti = avanti < 1f ? avanti + Mathf.Lerp(0, 1, Time.deltaTime * 5) : 1f;"
era possibile (nell'Update) fare in modo che la variabile "avanti" andasse da 0 a 1 in maniera non istantanea.
Il controllo fatto era però costante "if(Input.GetKey(VostraClasseInput.GetInput("Avanti")))" (che nell'Update viene eseguito ogni frame) infatti il codice serviva a far muovere il personaggio finché il pulsante "Avanti" veniva premuto.

Ma cosa succede se vogliamo muovere un immagine in una determinata posizione dopo aver premuto un pulsante (quindi senza tenerlo premuto), in maniera non istantanea?

La soluzione, anche se differente, è altrettanto semplice:
nel vostro metodo OnPointerDown settate a true una variabile booleana precedentemente settata a false

Nell'Update

if(vostraVariabileBooleana){
variabileDaModificare = Mathf.MoveTowards(variabileDaModificare, 0, Time.deltaTime * 5f);
if(variabileDaModificare == 0)
vostraVariabileBooleana = false;
}

il che significa che finché la vostra variabile è true la variabileDaModificare (che potrebbe essere la posizione x nello spazio di gioco del vostro oggetto da muovere) si muoverà dalla sua posizione corrente verso 0 (e qui potete ovviamente cambiare 0 con la vostra destinazione) in base al tempo * 5f.
quando avete raggiunto la destinazione settate la vostra variabile a false in modo da evitare che venga eseguito codice inutilmente (che potrebbe anche avere effetti non previsti).

Ovviamente per fare animazioni di questo genere si può usare anche il tool "Animation" messo a disposizione da Unity3d, ma se, come me, siete programmatori questo metodo vi sembrerà molto più controllabile.

Al prossimo tip.

PS. Ho aggiunto un indice che cercherò di tenere aggiornato.
utdefault
CITAZIONE (Lief @ 25 Apr 2018, 14:58) *
CITAZIONE (utdefault @ 25 Apr 2018, 12:36) *
Casomai, con l'aggiunta di nuovi post argomentativi, ogni messaggio potrebbe essere indicizzato in apertura di discussione. Così da poterli subito richiamare all'occorrenza. :>
L'idea è buona, però ci vorrebbe un post sopra il mio primo post (mettere tutto in un post unico è un po' confusionario). Inoltre penso sia bello che tutti possano dare suggerimenti quindi se si indicizzano è bene che chi posta un tip dica esplicitamente che è un tip a inizio del suo post (in modo che l'indicizzazione sia più semplice).

CITAZIONE (Lief @ 25 Apr 2018, 14:58) *
PS. Ho aggiunto un indice che cercherò di tenere aggiornato.
Era proprio questa l'intenzione proposta.
Ben fatto! ;>

Aiuterai di sicuro chi vuol visualizzare subito l'argomento di proprio interesse.
E soprattutto a non far perdere i tips nei meandri del topic. :>

Ciao,
ai tuoi prossimi suggerimenti.

p.s.:
in futuro, che ne diresti di realizzare una sorta di guida/manuale?
Lief
CITAZIONE (utdefault @ 30 Apr 2018, 00:11) *
Era proprio questa l'intenzione proposta.
p.s.:
in futuro, che ne diresti di realizzare una sorta di guida/manuale?

In futuro vorrei fare una serie di video-tutorial sulla creazione del videogioco principale che sto creando (sto facendo dei backup fin dalla versione 0.01 regolarmente, quindi potrei ripercorrere passo passo la creazione dall'inizio alla fine)... quel che mi manca al momento è solo il tempo, il lavoro mi toglie la maggior parte del tempo libero e il poco che rimane va diviso tra un sacco di cose: studio (visto che devo ancora finire l'università), casa, cibo, sonno, progetto in unity, un minimo di svago ecc.... Con gli anni qualcosina dovrei finirla (studio e il progetto come minimo) e a quel punto potrò dedicare un po' di tempo anche a creare i video-tutorial (come parte della pubblicazione/pubblicità del gioco appena fatto). Ma nel frattempo qualche piccolo ritaglio di 10-15 minuti lo posso anche trovare per scrivere qualche suggerimento qua e la (in realtà lo facevo anche prima di aprire questo topic... non in maniera specifica su Unity3d, ma piuttosto sulla programmazione in generale).
Lief
Per il tip di oggi (visto che non ho fatto nulla di particolarmente interessante con Unity3d a parte andare avanti con quel che già sapevo) voglio darvi il canale youtube del mio "maestro", ossia colui i cui video mi hanno insegnato più di qualsiasi altra scuola:
https://www.youtube.com/user/BurgZergArcade/

Sfortunatamente non è più molto attivo, ma tra gli ultimi video troviamo anche un interessante reboot della serie con cui ho iniziato (Hack & Slash RPG) (ovviamente io ho visto la serie originale).

Non voglio postare necessariamente tips tutti i giorni, ma questo canale volevo postarlo da quando ho iniziato e oggi sembra un giorno come un altro per farlo.
Lief
Il tip di oggi non è necessariamente legato a Unity, è più che altro di programmazione.
Qual'è il modo più semplice per "mischiare" una lista di oggetti non iterabili?

System.Random rnd = new System.Random();

List<Oggetto> list = oggetto.OrderBy(a => rng.Next());

(nota bene, in Unity è necessario specificare System visto che la classe UnityEngine contiene una classe Random).
Lief
Un modo molto semplice per rilevare le collisioni in Unity è usare i metodi:
OnCollisionEnter
OnCollisionExit

Se però si vuole rilevare l'entrata in uno spazio (senza andare "contro" un oggetto solido) si può anche creare un Trigger e usare i metodi:
OnTriggerEnter
OnTriggerStay
OnTriggerExit

come parametro in questi metodi è possibile passare
(Collider nomeVariabile)

in questo modo sarà possibile:
- capire il gameObject, il tag dell'oggetto con cui entriamo in contatto: nomeVariabile.gameObject.tag... e tutte le altre informazioni utili


trucco per evitare di dover controllare più volte il tag e di dover settare più volte le nostre variabili relative all'oggetto è settare tutto nell'Enter in variabili di classe per poi utilizzarle anche negli altri metodi.

È molto importante ricordare che tali metodi sono molto costosi e di usarli quindi con parsimonia.
Evitare di usare tali metodi per, ad esempio, controllare se siamo a terra o in aria con il nostro personaggio... perché controllare ogni frame questo tipo di informazione tramite un collider è troppo costoso... molto meglio utilizzare un Raycast della camera.

Già ma come?
Lo vedremo nel prossimo tip.
Lief
Ho corretto un piccolo "bug" nel tip della Serializzazione dei dati.
Si passa da una variabile public ad una variabile privata o SerializeField, la motivazione è spiegata nel tip.
Lief
Ci eravamo lasciati con una domanda: Come controllare se siamo attualmente toccando il terreno, senza usare un collider troppo costoso in termini computazionali?
La risposta è, come anticipato, un raycast della camera.
Ma cos'è esattamente un raycast?
La risposta è decisamente semplice, il raycast è un raggio (se preferite una linea retta) sparata dalla camera di gioco. Parte da dove vogliamo noi, arriva dove vogliamo noi.

Ecco un codice d'esempio:
bool GroundCheck () {
//questo è il "collider" del raycast... se il raggio colpisce qualcosa ritorna un RaycastHit
RaycastHit hit;
//tolleranza minima per il Capsule Collider. Se il nostro personaggio usa un capsule collider per decidere le collisioni dobbiamo mettere una minima tolleranza, altrimenti basta un terreno leggermente più inclinato non avere un controllo corretto
distance = 0.3f;
//se il raggio proiettato ritorna un hit, con tolleranza distance in posizione transform.position(che è la posizione del nostro personaggio) + Vector3.up*0.1f (origine... ossia il punto da cui parte il raggio che è i nostri piedi in questo caso), Vector3.down (direzione verso il basso). In pratica abbiamo un raggio che parte dai piedi e si muove verso il basso per 0.1f e se colpisce qualcosa con una tolleranza di 0.3f ritorna true (ossia siamo a terra)
if (Physics.Raycast (transform.position + (Vector3.up * 0.1f), Vector3.down, out hit, distance)) {
return true;
}
}

Ma se ci trovassimo nella condizione di dover verificare se stiamo toccando una parete (anche in aria), come fare?
Beh, non è poi così complesso... dovremmo semplicemente sparare dei raggi anche avanti, indietro a destra e a sinistra...
il che significherà
transform.position + (Vector3.forward * 0.1f), Vector3.forward
per avanti
e così via per tutti gli altri.
Ovviamente si può rendere questo controllo ancora più preciso sparando quanti raggi si vuole in diverse direzioni.

Semplice vero?
Lief
Prendendo spunto da una risposta che ho dato su stackoverflow (downvotata ma corretta, come dimostra il fatto che è stata accettata dall'utente):
https://stackoverflow.com/questions/5119350...193771#51193771

Nella mia risposta trovate il metodo per serializzare un oggetto contenente una lista di oggetti che a loro volta contengono 2 array.
Ovviamente è solo un esempio, potete personalizzarlo come volete per usare il limitatissimo serializzatore json di unity per fare oggetti complessi (esempio, al posto di un dizionario (Dictionary, l'equivalente delle HashMap in Java) non supportato potete utilizzare una List di Oggetti contenenti due campi key value).

Alla prossima

PS. È quasi due mesi che non aggiorno il topic. Ma c'è poco da fare, fin quando non mi vengono in mente nuove idee aggiornare è inutile.
Lief
Quante volte avete pensato:
- Ma quanto sono comodi i salvataggi su cloud?

Cambi PC e tutto quel che devi pare è sincronizzare il salvataggio e continuare la partita.
Succede un disastro e non devi ricominciare la partita.

Eppure i salvataggi su cloud hanno anche loro dei problemi:
- Sono legati alla piattaforma. Il che significa che se hai lo stesso gioco su PC e su smartphone (anche se i salvataggi sarebbero in teoria compatibili) non c'è modo di passare i salvataggi da un gioco all'altro (al massimo si può fare manualmente... ma non su iOS e non sempre su Android senza root).
- Per usufruirne è necessario installare un client in grado di supportarli. Se si gioca su gog senza gog galaxy si perdono i salvataggi su cloud. Se si gioca su steam senza steam (pochi giochi lo permettono ma ci sono) niente salvataggi su cloud.


Un utente con un minimo di esperienza può creare un sistema alternativo per tutti i giochi su PC. Con delle cartelle simboliche e dei servizi cloud tipo dropbox, collegando i salvataggi alla cartella di sincronizzazione e riuscendo così a risolvere il problema del client obbligatorio...
Ma rimane comunque il primo problema.
Inoltre è una rottura settare il tutto.
Senza contare che ci costringe ad installare le varie app di sincronizzazione (app che potrebbero non servirci in linea di massima, se solitamente usiamo il browser e non teniamo cartelle sincronizzate).

Eppure settare un sistema simile in-game non è impossibile e da modo ai nostri utenti di potersi semplicemente loggare al loro account personale per sincronizzare i propri salvataggi su qualsiasi piattaforma.

O meglio... sarebbe così se parlassimo di qualche anno fa. Oggi i sistemi di autenticazione Oauth 2.0 sono molto complessi e non ci consentono di rendere l'operazione un semplice "metti il tuo utente e password qui che io mando tutto con una post".

Tuttavia il sistema non è così complesso come potrebbe sembrare in un primo momento e le API messe a disposizione ci permettono di semplificare molto il sistema.

Visto che è un sistema complesso che varia da piattaforma a piattaforma ho deciso di dividere questo post in diverse parti, questa prima parte sarà dedicata ad un riassunto dei passaggi necessari per la nostra applicazione in Unity3d.

Passaggi:

1. Creazione di un'app autorizzata a fare le chiamate. Le chiamate alle API di default vengono respinte dai siti moderni questo per evitare abusi e attacchi (in questo modo chi riceve le chiamate può identificare chi le fa, e bloccare chi abusa).
Solitamente basta iscriversi al sito cloud prescelto e andare nella sezione API/sviluppo per poter creare un'app autorizzata.
Riceveremo così un client id e un secret id.
2. Fare la chiamata per la prima autorizzazione. Questa chiamata di solito viene fatta solo una volta. Consiste in realtà nell'aprire un link (quindi non è una vera chiamata alle API). Questo link (passano i dati client id e secret id) permetterà all'utente di autenticarsi sul proprio browser (lasciando quindi la nostra app). Una volta finita l'autorizzazione ci restituirà un token temporaneo dalla breve durata.
Ma come ce lo restituisce?
Se parlassimo di un'app web ce lo restituirebbe con un semplicissimo redirect... Sfortunatamente con i giochi Unity3d questo non si può fare perché la nostra app ovviamente non è un sito web.
Si può però sfruttare un redirect su un nostro server (che cattura il token e lo passa alla nostra app magari scrivendo su un file leggibile dalla nostra app) oppure con altri due metodi che non richiedono alcun server... un semplice copia/incolla senza redirect (dopo il login viene mostrato un codice all'utente che viene invitato a copiarlo all'interno dell'applicazione) o un redirect su localhost (che però la nostra app deve ascoltare).
Ovviamente il metodo più semplice da implementare (in mancanza di API già scritte) è quello del copia/incolla, almeno per i giochi in Unity3d.
3. Ottenuto il token temporaneo è tempo di fare la nostra prima vera e propria chiamata alle API. Solitamente una post con il token ottenuto. Al suo posto otterremo un token duraturo che identifica la nostra applicazione come loggata.
Possiamo leggere il token nel body di risposta o negli header dipende dal sito.
4. Trasformiamo il nostro file di salvataggio in un array di byte (byte[]) e lo mandiamo con una post insieme al token ottenuto in precedenza (cosa mettere nel body e cosa negli header dipende dal sito, solitamente però nel body si mette l'array di byte che rappresenta il nostro file, nell'header il nostro codice di autorizzazione).
Solitamente la risposta è successo o fallimento, quel che troviamo nel body e nell'header varia da sito a sito, solitamente però è qui che troviamo tutte le informazioni per ritrovare il file in caso di update o download.
5. L'update serve per aggiornare il nostro file di salvataggio. A seconda del sito può essere uguale alla create (con una post con overwrite come header) o ci può essere un id (ricevuto durante la create). È sempre una post che restituisce la stessa response della create (solitamente). Ovviamente, come per la create va mandato anche il token.
6. Download. Solitamente si fa con una get o con una post. Si passa l'indirizzo e, negli header o nel body, il token.
Il nostro file viene restituito come un array di byte che può essere convertito nel formato originario e poi salvato su disco (viene restituito quasi sempre nel body).

Ricapitolando:
- Apro link, utente si logga, copia il codice (o avviene tutto in automatico)
- Scambio il token temporaneo per uno di lunga durata.
- Post per creare il file su cloud con token (invio un byte[]).
- Post per update (a volte coincide con create con parametro di sovrascrittura)
- Get o Post per download con token (restituisce il body con la il file in byte[]).

Esistono eccezioni a questo flow (esempio Mega, che ha le sue API non compatibili con Unity3d), sfortunatamente nessuno supporta ufficialmente Unity3d.

Esistono però delle semplici API per Dropbox.
Un pacchetto open source molto comodo per Google Drive (non ufficiale ma molto simile ad un pacchetto ufficiale).
Dei siti che non richiedono alcuna autenticazione e che restituiscono un url (ovviamente i salvataggi degli utenti non contengono dati personali quindi non necessitano necessariamente di essere segreti. Altrettanto ovviamente sarà l'utente a scegliere tra Google Drive e Dropbox o i siti senza autenticazione). Con tali siti può bastare la Post/Put e la Get.

La prossima volta vedremo i dettagli implementativi per il pacchetto di Google Drive (il più semplice).

Alla prossima.
Lief
Piccola aggiunta al mio post precedente.
1. Ascoltare la risposta su localhost è molto più semplice di quel che pensavo. Ho letto il codice sorgente del pacchetto per Google Drive e l'ho implementato per Dropbox (che però non può ascoltare su una porta qualsiasi di localhost, quindi è necessario registrare un range di porte da controllare manualmente).
Il metodo è migliore del copia/incolla perché non richiede all'utente di fare azioni innaturali (a parte "tornare all'app" che però è decisamente più semplice di copiare/incollare un codice alfanumerico). Oltretutto il metodo copia/incolla è segnato come deprecato da parte di google, il che significa che anche dropbox potrebbe rimuoverlo in futuro (anche se al momento su dropbox non è segnato come deprecato).
2. Le API di Mega sono ora compatibili con Unity3d (dalla versione 2018.2 in poi). Quindi vedremo anche il metodo per fare upload e download da Mega (con 3 servizi autenticati direi che siamo davvero a buon punto).
3. Su Android localhost va sostituito con 127.0.0.1 ossia l'ip del localhost... su iOS ovviamente è il contrario, non funziona 127.0.0.1 ma funziona il localhost.
Lief
Google Drive.

Per google drive esiste fortunatamente qualcuno che ha già fatto la maggior parte del lavoro per noi.
Basta scaricare il pacchetto da qui:
https://github.com/Elringus/UnityGoogleDrive

e seguire le istruzioni per il setup.

Il bello di questo pacchetto è che, qualsiasi chiamata faremo da ora in avanti, ci presenterà, se necessario, l'autenticazione.
Il che significa che non dobbiamo implementare l'accesso (neanche la parte per ascoltare la risposta su localhost).

Ma come utilizzare l'API per fare l'upload di un file si salvataggio e farne successivamente il download?

Upload:
si divide in:
-creazione
-aggiorrnamento

private IEnumerator UploadFileToCloud(){
byte[] myData = System.Text.Encoding.UTF8.GetBytes(File.ReadAllText(Application.persistentDataPa
th + "/" + "uploadtry.json"));
var file = new UnityGoogleDrive.Data.File() { Name = "uploadtry.json", Content = myData };
var request = GoogleDriveFiles.Create(file);
yield return request.Send();
Debug.Log(request.ResponseData.Id);
}

Il metodo sopra vi permetterà di fare l'upload di un file nel persistentDataPath nella root di google drive.
request.ResponseData.Id è l'id del vostro file. Salvandolo potrete fare il download e l'aggiornamento del file.

var request = GoogleDriveFiles.Update(request.ResponseData.Id, file);
var request = GoogleDriveFiles.Download(request.ResponseData.Id);

Debug.Log(System.Text.Encoding.UTF8.GetString(request.ResponseData.Content));

dove request.ResponseData.Content è il contenuto del file.

Ma come recuperare un file senza sapere l'id, e come creare una cartella?

Lo vedremo molto presto.
Alla prossima
Lief
PS. Non posso più modificare il primo messaggio, per aggiornare il menù
Salvataggi su Cloud Google Drive: cartelle e upload/download completo
Impronte sulla neve
Creazione di Camera Dialoghi
Input System Integrazione Costum rebind e prima analisi nuovo InputSystem
Salvataggi su Cloud Dropbox: cartelle e upload/download completo
Modificare il target di uno UnityEvent EventTrigger workaround
Salvataggi su Cloud OneDrive: cartelle upload/download completo
Salvataggi su Cloud Mega: cartelle upload/download completo

Ok è un botto che non aggiorno questo post. Ma era anche un botto che il mio progetto personale era fermo complici due cambi di lavoro nel giro di pochi mesi e altri problemi.
Detto questo qualche progresso l'ho fatto comunque (ad esempio sono riuscito a correggere un rognoso bug riguardante One Drive che non è più compatibile con http per l'url 127.0.0.1 (l'unico utilizzabile su android come localhost)... viene richiesto https in ogni caso. Per correggere questo limite l'unica è usare una propria pagina e fare un redirect da quella pagina https al nostro ip locale 127 in http, avendo cura di conservare il code ricevuto... lo vedremo nei dettagli un'altra volta).

Riprendiamo da dove ci eravamo lasciati.
Come recuperare un file senza sapere l'id, e come creare una cartella su Google Drive?

Primo punto: creiamo un metodo asincrono... così la chiamata non sarà bloccante (ossia non si bloccherà l'interfaccia del gioco fino alla ricezione della risposta)
private async void UploadFileGoogleDriveAsync()

Lo creiamo privato e lo richiamiamo in un metodo wrapper che possiamo a sua volta richiamare da un bottone
public void UploadFileGoogleDrive() {
UploadFileGoogleDriveAsync();
}

per trovare la cartella con nome NAME usiamo l'helper messo a disposizione dalla libreria
var files = await Helpers.FindFilesByPathAsync("NAME", fields: new List<string> { "files(id, name, size, mimeType, modifiedTime)" }, mime: true ? Helpers.FolderMimeType : null);

poi controlliamo se la nostra cartella è unica
if (files.Count > 1) {
//se c'è più di una cartella con quel nome diamo errore all'utente
return;
} else if (files.Count == 1) {
//se ce n'è una sola significa che l'abbiamo già creata quindi facciamo una richiesta di tutti i file all'interno di quella cartella
GoogleDriveFiles.ListRequest request = new GoogleDriveFiles.ListRequest();
//richiediamo questi campi id nome grandezza e ultimamodifica
request.Fields = new List<string> { "files(id, name, size, modifiedTime)" };
//richiediamo solo i file .json con id del parents uguale a quello della nostra cartella
request.Q = string.Format("'{0}' in parents and name contains '{1}'", files[0].Id, ".json");
//mandiamo la richiesta e attendiamo il risultato in questo metodo asincrono
await request.Send();
//passiamo al metodo di creazione del salvataggio a cui passiamo i file ricavati e l'id della cartella
UpdateCreateSaves(request.ResponseData.Files, files[0].Id);
} else if (files.Count == 0) {
// se la cartella non c'è la creiamo
//prima creando un file cartella
var folder = new UnityGoogleDrive.Data.File() {
Name = "NAME",
MimeType = "application/vnd.google-apps.folder"
};
//poi chiedendo di mettere quel file nel google drive dell'utente
var request = GoogleDriveFiles.Create(folder);
await request.Send();
//passiamo al metodo di creazione del salvataggio dove la lista dei file precedenti sarà ovviamente vuota mentre l'id della nostra cartella sarà l'id della cartella appena creata
UpdateCreateSaves(new List<UnityGoogleDrive.Data.File>(), request.ResponseData.Id);
}

//altro metodo asincrono
private async void UpdateCreateSaves(List<UnityGoogleDrive.Data.File> files, string folderId) {
//cerchiamo la cartella dei salvataggi nel nostro hard disk
DirectoryInfo dirInf = new DirectoryInfo(Application.persistentDataPath + "/" + "Saves");
if (!dirInf.Exists) {
dirInf.Create();
}
//prendiamo tutti i file .json (il formato dei salvataggi che ho deciso di usare) nella cartella
List<string> salvataggiFile = dirInf.GetFiles().OrderByDescending(f => f.LastWriteTime).Where(f => f.Extension == ".json").Select(f => f.Name).ToList();
//per ogni file nel cloud
foreach (var file in files) {
string toRemove = "";
//per ogni file di salvataggio nella cartella locale
foreach (string salvataggio in salvataggiFile) {
//se il file meno "stringa-a-scelta" == al nome del salvataggio in locale
if (file.Name == salvataggio.Substring(0, salvataggio.Length - 5) + "-to-google-drive.json") {
//encodo il file in byte
byte[] myData = System.Text.Encoding.UTF8.GetBytes(File.ReadAllText(Application.persistentDataPa
th + "/Saves/" + salvataggio));
//creo il file che andrà su google drive aggiungendo la stringa precedente (è utile per non sovrascrivere i salvataggi locali in fase di download)
var localFile = new UnityGoogleDrive.Data.File() { Name = salvataggio.Substring(0, salvataggio.Length - 5) + "-to-google-drive.json", Content = myData };
//faccio l'update del vecchio file con il mio file locale
var request = GoogleDriveFiles.Update(file.Id, localFile);
await request.Send();
toRemove = salvataggio;
}
}
//rimuovo il salvataggio dalla lista dei salvataggi da aggiornare
if(toRemove != "")
salvataggiFile.Remove(toRemove);
}
//se ho ancora file significa che sono salvataggi nuovi
foreach (string salvataggio in salvataggiFile) {
byte[] myData = System.Text.Encoding.UTF8.GetBytes(File.ReadAllText(Application.persistentDataPa
th + "/Saves/" + salvataggio));
//creo un file che andrà su google drive mettendogli come parent la cartella creata precedentemente
var file = new UnityGoogleDrive.Data.File() {
Name = salvataggio.Substring(0, salvataggio.Length - 5) + "-to-google-drive.json",
Content = myData,
Parents = new List<string>
{
folderId
}
};
//creo il nuovo file
var request = GoogleDriveFiles.Create(file);
await request.Send();
}
}

Per finire c'è il download che è molto più semplice
//anche questo è il metodo asincrono per il quale esisterà un wrapper
private async void DownloadFileGoogleDriveAsync() {
//anche qui ricaviamo la cartella
var files = await Helpers.FindFilesByPathAsync("NAME", fields: new List<string> { "files(id, name, size, mimeType, modifiedTime)" }, mime: true ? Helpers.FolderMimeType : null);

if (files.Count > 1) {
//anche qui diamo errore se ce n'è più di una
return;
} else if (files.Count == 1) {
//in download la cartella ci dev'essere, il controllo se non ci sono salvataggi su cloud va fatto prima
//richiedo lista di file con parent la cartella e con estensione json
GoogleDriveFiles.ListRequest request = new GoogleDriveFiles.ListRequest();
request.Fields = new List<string> { "files(id, name, size, modifiedTime)" };
request.Q = string.Format("'{0}' in parents and name contains '{1}'", files[0].Id, ".json");
await request.Send();
//se non c'è creo la cartella locale dei salvataggi
DirectoryInfo dirInf = new DirectoryInfo(Application.persistentDataPath + "/" + "Saves");
if (!dirInf.Exists) {
dirInf.Create();
}
//faccio il download di tutti i file con id parent uguale alla nostra cartella
foreach (var file in request.ResponseData.Files) {
var request2 = GoogleDriveFiles.Download(file.Id);
await request2.Send();
//scrivo con l'aggiunta di una stringa nel nome in modo da non sovrascrivere salvataggi locali
TextWriter tw = new StreamWriter(Application.persistentDataPath + "/Saves/" + file.Name.Substring(0, file.Name.Length - 5) + "-from-google-drive" + ".json");
tw.Write(System.Text.Encoding.UTF8.GetString(request2.ResponseData.Content));
tw.Close();
}
}
}


Direi che è tutto per questa volta. La prossima volta vedremo l'autorizzazione con il code flow di dropbox che è molto semplice e ci permetterà di implementare in maniera simile anche one drive (oltre al fatto che ci permetterà di capire come funziona la libreria google drive che abbiamo usato).

Alla prossima (si spera non tra anni XD).
pinellos
Ciao!
Stiamo mettendo su una avventura grafica con unity (a dir la verità siamo ormai a progetto pienamente avviato), dato che molte scene del gioco si svolgono in un panorama innevato, avevamo pensato che sarebbe carino fare in modo che l'omino lasci le orme sulla neve e che queste scompaiano dopo un po'. Ti chiedo, visto che hai certamente più esperienza di me con Unity: è fattibile con un particellare o è meglio programmare uno script che - chessò io - "stampi" un'immagine dei "passi" ogni tot frame di animazione dell'omino e che poi la distrugga dopo un tot tempo? Stiamo usando Adventure Creator, naturalmente c'è la possibilità di fare entrambe le cose (i particellari sono gestiti da AC "out of the box" e/o si può codare tranquillamente una funzione esterna chiamandola tramite AC all'occorrenza o inserendola come componente del gameobject dell'omino facendola "vedere" ad AC (per poterla gestire al suo interno)), solo non ho ben capito quale sia la soluzione più semplice... non vorrei sbattermi come un riccio per poi scoprire che si poteva fare in pochi secondi, tutto qua blush.gif
Ciao e grazie per i tuoi tutorial su unity, sono spunti interessanti!
Lief
Ciao, non ho molta esperienza con i particellari quindi direi che in questo caso sei tu ad avere più esperienza.
Però la tua domanda mi ha incuriosito e alla fin fine ho voluto fare una breve ricerca su youtube (dove di solito prendo ispirazione se non ho idea di come partire):


Prova a dare un'occhiata a questo, se è quel che vuoi magari potresti poi fare anche tu un breve tips per suggerire come rendere la procedura più semplice e a cosa è necessario stare attenti. In fondo questo topic nasce apposta per dare a tutti l'opportunità di imparare e di poter condividere le proprie conoscenze (anzi, se hai altro che vuoi condividere ti invito a farlo, io sono interessato ad approfondire ancora le mie conoscenze in unity che non sono ancora così complete (ad esempio il nuovo Entity System Component non l'ho quasi utilizzato).

Di base penso che quel breve tutorial usi la tua seconda idea (e personalmente parlando sono d'accordo... userei i particellari per sangue, esplosioni, portali ecc... non per le impronte). Dopo aver instanziato un'immagine dell'impronta corretta alla distanza corretta la fa distruggere dopo X secondi. Semplice ed efficace.
Lief
Piccolo aggiornamento: Ho ripreso finalmente a lavorare sul gioco (approfitto del fatto che sto cambiando lavoro e inizio tra un mesetto dall'altra parte).
Per l'occasione un brevissimo tip

Come gestire una camera per i dialoghi: posizione e funzionamento di base
Più che la codifica vera e propria (che è banale) mi concentrerò sul come fare questa tipologia di camera (che a prima vista potrebbe rivelarsi complessa).
L'idea di base è:
- Ogni volta che si parla ad un NPC la camera deve spostarsi da dove si trova normalmente (dietro il personaggio) ad un primo piano del personaggio che sta parlando.
- A conversazione conclusa la camera deve tornare a funzionare normalmente
- Durante la conversazione non dev'essere possibile aprire il menù di salvataggio e il nostro personaggio non deve potersi muovere.
- Se l'NPC fa una domanda la camera deve fare un primo piano del nostro personaggio.

Il primo punto è quello che ha più mi ha messo alla prova, la prima idea è stata: faccio muovere la camera alla posizione del personaggio poi la sposto di un certo "tot" indietro e la ruoto di un certo tot in modo che faccia vedere il personaggio e la alzo in modo che faccia vedere la faccia del personaggio. Inutile dire che quest'idea è praticamente irrealizzabile... pensare di prevedere tutte le 360 rotazioni previste, un'altezza diversa per ogni personaggio e un diverso "indietro" per ogni NPC è roba da perderci il sonno.
La soluzione è quasi banale, creare un Empty Game Object all'interno di ogni NPC e posizionarlo nella posizione in cui si vuole la camera. Questo funziona perché, anche se necessario creare un punto di ancoraggio della camera per ogni NPC, lo script da scrivere rimane unico (a differenza dell'idea precedente).

La camera ovviamente deve spostarsi solo quando l'NPC parla e spostarsi nuovamente quando ha finito di parlare... come fare questa cosa?
L'idea di base è altrettanto semplice: usare il riferimento alla camera principale Camera.main per trovare lo script principale e disabilitarlo quando si inizia a parlare, riabilitarlo quando si ha finito.

Discorso simile per disabilitare l'apertura del menù di salvataggio e lo script di movimento.

Per il punto finale basta riconoscere le domande e spostare la camera al punto di ancoraggio nel personaggio e viceversa.

4 cose da tenere in mente:
- Controllare se il personaggio tocca terra, disabilitando lo script di movimento se si parla in aria il personaggio continuerà a cadere fino a fine conversazione.
- Mettere a zero la velocità del personaggio e attivare per un momento kinematic del rigidbody, questo eviterà che il nostro personaggio continui a camminare per inerzia se arriviamo di corsa.
- Disabilitare questo script se il menù di salvataggio è già aperto.
- La camera isometrica richiede l'uso dell'orthographic, esso va però disabilitato quando si usa la dialog camera e riattivato dopo. Si fa con un semplice booleano (se ortografica, set false, nel bool metto valore di orthographic del Camera.main, se variabile true disattivo orthographic).

Per oggi è tutto
Lief
Avevo detto di aver finito per oggi ma ho fatto una piccola modifica al sistema di Input dopo aver studiato un po' come funziona il nuovo sistema di input.

Prima di tutto la classe:
VostraClasseInput

Ora contiene i metodi
GetKey GetKeyDown GetKeyUp GetAxis
che hanno come return i valori della classe di Input attuale (esempio return Input.GetKeyDown(code);)
In questa maniera cambiando il sistema di input con il nuovo InputSystem l'unica classe da modificare sarà la VostraClasseInput (dove avrete usato il codice potrete mantenere quel che avete scritto).

In secondo luogo per quanto riguarda il nuovo sistema di Input:
- Al momento non supporta decentemente il rebinding (ma è ancora in sviluppo). Non è possibile leggere un input generico e quindi rimapparlo. Non è quindi utilizzabile in produzione.
- L'equivalente di GetKey/Down/Up esiste. Usa un differente Enum chiamato Key al posto di KeyCode (non so se sia equivalente ma penso di no visto che il vecchio sistema prevedeva anche i KeyCode dei Gamepad). Probabilmente sarà semplicemente necessario aggiungere uno switch case per decidere se l'enum è Keyboard o Gamepad e, nel peggiore dei casi, copiare il KeyCode Enum attuale e importarlo per la conversione dei vecchi KeyCode nei nuovi Key. Sperando ovviamente che ci sia modo di distinguere i Key per tastiera da quelli derivati da Gamepad (se così non fosse il nuovo sistema non avrebbe senso quindi ho fiducia che sarà possibile).
- Per gli Axis basterà un Handler per ogni coppia di assi mappata in Unity che varierà x e y di una singola variabile. Con un piccolo switch case si potrà capire se restituire x o y a seconda degli assi mappati in precedenza (l'alternativa sarebbe eliminare completamente gli assi mappati... in precedenza infatti si doveva controllare asse x per mouse, asse x per joystick destro, sinistro ecc.... con il nuovo sistema ogni asse avrà un Handler differente ma cambierà il valore di una singola variabile). Potete farvi un'idea più precisa qui:

e qui:
New Input System temp Docs migration

Prossimo messaggio code flow di Dropbox
Lief
Ora finalmente vediamo cosa nasconde dietro le quinte la libreria di google.

Prima di tutto, a differenza di google non possiamo autorizzare tutte le porte di localhost con una sola linea. Il mio consiglio è di creare una lista di un centinaio di porte massimo e usare quelle.
int count = 0;
int numtries = portList.Count;
while (numtries-- != 0) {
try {
var listener = new TcpListener(IPAddress.Loopback, portList[count]);
listener.Start();
port = ((IPEndPoint) listener.LocalEndpoint).Port;
listener.Stop();
} catch (Exception e) {
Debug.Log(e);
count++;
continue;
}
}
Se va in eccezione la porta si continua con il ciclo e si prova con la porta successiva, con 100 porte a disposizione non ci saranno mai problemi a trovarne una.

Usate la vostra porta scelta su localhost per aprire il link di autorizzazione oauth2 code di dropbox con redirect link sul link in localhost sulla porta prescelta.
Ascoltando quel link il vostro gioco potrà attendere che l'utente faccia l'accesso via browser a dropbox e che, dopo l'accesso, venga reindirizzato proprio sul link che state ascoltando.
Al posto di vostroclientid sarà necessario mettere l'informazione trovata nel vostro account da sviluppatori dropbox.
var httpListener = new HttpListener();
httpListener.Prefixes.Add("http://"+ localhost +":" + port + '/');
httpListener.Start();
Application.OpenURL("https://www.dropbox.com/oauth2/authorize?response_type=code&client_id=vostroclientid&redirect_uri=http://" + localhost + ":" + port + '/');
httpListener.BeginGetContext(HandleHttpListenerCallback, httpListener);

l'handler HandleHttpListenerCallback verrà eseguito quando l'utente avrà finito di fare l'accesso e verrà reindirizzato sul link localhost sulla porta che state ascoltando.
oltre a quello avrà un campo ?code=certonumero che corrisponde al codice di autorizzazione.

Prendete var response = context.Response; e token = context.Request.QueryString.Get("code"); il primo vi dirà se è andato tutto a buon fine, il secondo vi darà il codice di autorizzazione se esiste.
In ogni caso prendete il risultato
var httpListener = (HttpListener) result.AsyncState;
var context = httpListener.EndGetContext(result);

e mandatelo a
unitySyncContext.Send(HandleHttpListenerCallbackOnUnityThread, result);

/il codice completo è una cosa del genere
private void HandleHttpListenerCallback(IAsyncResult result) {
var httpListener = (HttpListener) result.AsyncState;
var context = httpListener.EndGetContext(result);

// la risposta può essere scritta nel browser per informare l'utente
var response = context.Response;
var responseString = "<html><h1>Puoi tornare all'app</h1></html>";
var buffer = Encoding.UTF8.GetBytes(responseString);
response.ContentLength64 = buffer.Length;
var responseOutput = response.OutputStream;
responseOutput.Write(buffer, 0, buffer.Length);
responseOutput.Close();
httpListener.Close();

// Extract the authorization code.
token = context.Request.QueryString.Get("code");
if (token == null) {
return;
}
unitySyncContext.Send(HandleHttpListenerCallbackOnUnityThread, result);
}

nell'Handler HandleHttpListenerCallbackOnUnityThread usate una Coroutine
StartCoroutine(Dropbox());

per richiamare un metodo dove userete il token ricevuto per concludere l'autorizzazione

Dictionary<string, string> form = new Dictionary<string, string>();
form.Add("code", token);
form.Add("grant_type", "authorization_code");
form.Add("client_id", "vostroclientid");
form.Add("client_secret", "vostrosecretid");
form.Add("redirect_uri", "http://" + localhost + ":" + port + '/');
using (UnityWebRequest www = UnityWebRequest.Post("https://api.dropboxapi.com/oauth2/token", form)) {

www.SetRequestHeader("Content-Type", "application/x-www-form-urlencoded");

yield return www.SendWebRequest();

if (www.error != null) {
Debug.Log(www.error + " " + www.downloadHandler.text);
} else {
//scrivete la risposta dell'autorizzazione dove volete per poterla utilizzare dopo per esempio in un json
tw.Close();
}
}

Il json di risposta di dropbox si deserializza con questi campi:
[SerializeField] string access_token;
[SerializeField] string token_type;
[SerializeField] string uid;
[SerializeField] string account_id;


Questa prima autorizzazione ci permette di ottenere l'access_token che verrà usato per tutte le altre chiamate. Più in particolare:

Il codice di base che ci consente di capire se siamo ancora loggati (ossia se l'access_token è ancora valido) ad esempio facendo una lista delle cartelle
byte[] myData = Encoding.UTF8.GetBytes("{\"path\": \"\",\"recursive\": false,\"include_media_info\": false,\"include_deleted\": false,\"include_has_explicit_shared_members\": false,\"include_mounted_folders\": true}");
//facciamo una Put che trasformiamo poi in Post
using (UnityWebRequest www = UnityWebRequest.Put("https://api.dropboxapi.com/2/files/list_folder", myData)) {
www.method = "POST";
//usiamo l'access token
www.SetRequestHeader("Authorization", "Bearer " + response.Access_token);
www.SetRequestHeader("Content-Type", "application/json");
yield return www.SendWebRequest();
if (www.isNetworkError || www.isHttpError) {
Debug.Log(www.downloadHandler.text);
} else {
//deserializziamo la risposta
entries = DropboxResponseEntries.SaveFromString(www.downloadHandler.text);
}
}

Per deserializzare abbiamo un oggetto composto da
[SerializeField] DropboxResponseEntry[] entries;
[SerializeField] string cursor;
[SerializeField] bool has_more;

e ovviamente l'oggetto figlio
//non potendo usare .tag come nome di variabile usiamo FormerlySerializedAs per associare nome della variabile al nome nel json
[FormerlySerializedAs(".tag")]
[SerializeField] string tag;
[SerializeField] string name;
[SerializeField] string path_lower;
[SerializeField] string path_display;
[SerializeField] string id;
[SerializeField] string client_modified;
[SerializeField] string server_modified;
[SerializeField] string rev;
[SerializeField] int size;
[SerializeField] string content_hash;


Possiamo poi usare entries (il risultato della deserializzazione) per il download di tutti i file di salvataggio:

foreach (DropboxResponseEntry entry in entries.Entries) {
if (entry.Name.EndsWith(".json")) {
using (UnityWebRequest www = UnityWebRequest.Get("https://content.dropboxapi.com/2/files/download")) {
www.SetRequestHeader("Authorization", "Bearer " + response.Access_token);
www.SetRequestHeader("Dropbox-API-Arg", "{\"path\": \"" + "/" + entry.Name + "\"}");
www.SetRequestHeader("Content-Type", "");
yield return www.SendWebRequest();
if (www.error != null) {
} else {
//scrivo il nuovo file di salvataggio nella cartella Saves all'interno del persistentDataPath, ad esempio
TextWriter tw = new StreamWriter(Application.persistentDataPath + "/Saves/" + entry.Name);
tw.Write(www.downloadHandler.text);
tw.Close();
}
}
}
}

Per l'upload invece:
//lista di nomi di file con estensione .json all'interno della cartella selezionata
List<string> salvataggiFile = dirInf.GetFiles().OrderByDescending(f => f.LastWriteTime).Where(f => f.Extension == ".json").Select(f => f.Name).ToList();
//per ogni salvataggio
foreach (var salvataggio in salvataggiFile) {
//converto il salvataggio con quel nome nella cartella Saves nel persistentDataPath (esempio) in un array di byte
byte[] myData = Encoding.UTF8.GetBytes(File.ReadAllText(Application.persistentDataPath + "/Saves/" + salvataggio));
//uso una post per fare l'upload dell'array di byte
using (UnityWebRequest www = UnityWebRequest.Post("https://content.dropboxapi.com/2/files/upload", "")) {
var upload = new UploadHandlerRaw(myData);
www.uploadHandler = upload;
www.SetRequestHeader("Authorization", "Bearer " + response.Access_token);
www.SetRequestHeader("Dropbox-API-Arg", "{\"path\": \"/" + salvataggio + "\",\"mode\": \"overwrite\",\"autorename\": true,\"mute\": false}");
www.SetRequestHeader("Content-Type", "application/octet-stream");
yield return www.SendWebRequest();
}
}

Il codice non è semplicissimo ma una volta studiato e utilizzato per bene diventa abbastanza chiaro.
Con questo possiamo fare upload e download di salvataggi sia da google sia da dropbox.
Cosa cambia per OneDrive?
In realtà veramente poco:
- In One drive la lista di porte è ancora più piccola (non più di una decina)
- Su mobile non potendo utilizzare localhost (né l'equivalente 127.0.0.1) dovremo usare un sito custom che reindirizza su localhost (o su 127.0.0.1) conservando il code di autorizzazione. Inoltre dovrà essere su https (fuori da localhost oauth 2 richiede giustamente https). Si fa facilmente con una semplice pagina altervista gratuita, wordpress e un semplice piccolo codice.
- Ovviamente cambiano le response da deserializzare e i link a cui fare le stesse chiamate (autorizzazione primaria, secondaria, get e put per download upload e check).

Ma quel che cambia rompe parecchio le balle a noi programmatori visto che la documentazione di One Drive è molto più confusionaria di quella di Dropbox, oltretutto l'autorizzazione che si ottiene da dropbox dura molto di più di quella di One Drive.
Detto questo funziona quindi la prossima volta vedremo le differenze, poi ci dedicheremo a Mega, servizio che necessita di una libreria esterna ma che non usa oauth 2 e che è quindi un'alternativa per fare un sistema che non necessita di uscire dal gioco (le autorizzazioni oauth 2, per quanto sicure, sono scomode perché richiedono di usare il browser).

Alla prossima (stavolta per davvero).
Lief
Piccolo tip:
Non è possibile assegnare a runtime il target di uno UnityEvent EventTrigger (esempio: ho due GameObject con lo stesso script che dovrebbero usare lo stesso metodo nell'evento PointerDown. Non posso fare in modo di assegnare uno o l'altro oggetto a seconda di alcune condizioni (esempio OnTriggerEnter)).

La soluzione può essere usare l'interfaccia IPointerDownHandler (o IPointerUpHandler o IDragHandler a seconda dell'evento) e implementare il metodo corrispondente:
public virtual void OnPointerDown(PointerEventData ped) ad esempio.

A quel punto si può mettere nella classe del bottone un riferimento al GameObject da utilizzare (vuoto) public oppure con un Setter

Nello script del nostro GameObject
public void OnTriggerEnter(Collider other) {
riferimentoAlBottone.SetGameObjectTarget = this.gameObject;
}

public void OnTriggerExit(Collider other) {
riferimentoAlBottone.SetGameObjectTarget = null;
}

Nell'OnPointerDown sarà quindi possibile chiamare tutti i metodi del nostro gameobject corrente.

In linea di massima è un buon workaround (si assegna l'oggetto reale su cui usare lo stesso identico metodo a runtime mantenendo un riferimento del bottone nello script del nostro oggetto), ma non sarebbe stato male avere un metodo tipo:
SetPersistentTarget(index, gameObject);
così come si può già usare
SetPersistentListenerState(index, nuovoStato);
per attivare/disattivare eventi a runtime.
Lief
E ora diamo un'occhiata alle API di One Drive.

Come anticipato OneDrive può conservare solo un numero molto limitato di porte, quindi possiamo semplificare il codice con una semplice lista/array di porte.

int[] portsList = new int[] { 52342, 52987 ecc... };

int count = 0;
int numtries = portsList.Length;
while (numtries-- != 0) {
try {
var listener = new TcpListener(IPAddress.Loopback, portsList[count]);
listener.Start();
port = ((IPEndPoint) listener.LocalEndpoint).Port;
listener.Stop();
} catch (Exception e) {
count++;
continue;
}
}

Semplice e indolore.

Su android però visto che avremo un redirect fisso dobbiamo fissare una porta unica

if (Application.platform == RuntimePlatform.Android)
port = 52344;

Sperando che sia libera.

L'open url andrà fatto in questo modo

if (Application.platform == RuntimePlatform.Android)
Application.OpenURL("https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=vostroclientid&response_type=code&redirect_uri=https%3A%2F%2FindirizzoAltervistaVostroBlog.altervista.org%2Findirizzopostsulvostroblog%2F&response_mode=query&scope=user.read%20files.readwrite.appfolder%20files.readwrite%20files.readwrite.all");
else
Application.OpenURL("https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=vostroclientid&response_type=code&redirect_uri=http%3A%2F%2F" + localhost + "%3A" + port + "&response_mode=query&scope=user.read%20files.readwrite.appfolder%20files.readwrite%20files.readwrite.all");
httpListener.BeginGetContext(HandleHttpListenerCallback, httpListener);

Come potete leggere dal codice l'url su one drive è molto più complesso per due ragioni:
1. Dovrete utilizzare %2F e altri caratteri speciali al posto di / e : per l'url di redirect
2. Dovrete già dare diverse autorizzazioni che sul sito nella documentazione non sono chiarissime.
Il vostro client id si ricava sempre nel vostro account online ma a differenza di dropbox raggiungere il link corretto per la registrazione dell'app è un parto quindi ve lo metto qui:
https://portal.azure.com/#blade/Microsoft_A...ationsListBlade

Sarà poi vostra cura dover scoprire come utilizzare il sito, vi posso dire solo che client id lo create in certificates and secrets e gli endpoints li potete mettere in Autentication, i permessi in Permissions (dopo aver creato l'app).

Da qui in poi il resto è tutto uguale tranne nel token:
Dictionary<string, string> form = new Dictionary<string, string>();
form.Add("client_id", vostroclientid);
form.Add("scope", "https://graph.microsoft.com/files.readwrite.all");
form.Add("code", token);
if (Application.platform == RuntimePlatform.Android)
form.Add("redirect_uri", "https://vostroblogsualtervista.altervista.org/indirizzopostblog");
else
form.Add("redirect_uri", "http://" + localhost + ":" + port);
form.Add("grant_type", "authorization_code");
form.Add("client_secret", vostroclientsegreto);
using (UnityWebRequest www = UnityWebRequest.Post("https://login.microsoftonline.com/common/oauth2/v2.0/token", form)) {
www.SetRequestHeader("Content-Type", "application/x-www-form-urlencoded");
yield return www.SendWebRequest();

if (www.error != null) {
} else {
//scrivete l'autorizzazione dove volete ad esempio in un file json
}
}

Molto simile, cambiano in pratica solo gli url e i parametri.

Il codice da usare sul vostro blog altervista fatto con wordpress è decisamente molto semplice (ricordatevi di attivare https prima, altrimenti non potrete mettere l'url tra quelli autorizzati visto che l'autorizzazione senza https funziona solo per localhost, inoltre installate il plugin Insert Headers and Footers per poter scrivere il codice che vi servirà):
<script>
if(window.location.href.startsWith("https://yourblogaddress.altervista.org/blog/yourpostpath/")){
var stringPartUrl = window.location.href.substring(numberofcharinyoururl);
window.location.replace("http://localhostor127.0.0.1:yourport/" + stringPartUrl);
}
</script>

Praticamente numberofcharinyoururl è un numero (esempio 45) che rappresenta il numero di caratteri da eliminare il nuovo url sarà quindi solo la parte aggiunta da one drive ossia "?code=token_di_autorizzazione_alfanumerico"
lo script sostituirà https://yourblogaddress.altervista.org/blog/yourpostpath/ con http://127.0.0.1:52344/?code=token_di_auto...ne_alfanumerico
dove 127.0.0.1 è localhost su android, http quindi resta sicuro (è solo one drive che non lo riconosce come tale per qualche stramba decisione/bug), la porta 52344 è quella fissa che abbiamo deciso di utilizzare su android (può essere una porta qualsiasi purché libera), e "?code=token_di_autorizzazione_alfanumerico" contiene il token che utilizzeremo per fare la chiamata successiva.

La risposta in json di onedrive è serializzabile così:
[SerializeField] string token_type;
[SerializeField] string scope;
[SerializeField] int expires_in;
[SerializeField] int ext_expires_in;
[SerializeField] string access_token;

contiene il token che useremo nelle successive chiamate (chiamato access_token), il tipo (che però non ci serve visto che sappiamo già che è bearer, in quanto scade e qual'è lo scope (ossia le autorizzazioni che abbiamo chiesto). Come per dropbox useremo solo access_token.

L'autorizzazione one drive è scritta in questa maniera:
using (UnityWebRequest www = UnityWebRequest.Get("https://graph.microsoft.com/v1.0/me/drives")) {
www.SetRequestHeader("Authorization", "Bearer " + response.Access_token);
www.SetRequestHeader("Content-Type", "application/json");
yield return www.SendWebRequest();
}
Notare che per il semplice check dell'autorizzazione one drive richiede get
L'upload del file si fa in questo modo:

List<string> salvataggiFile = dirInf.GetFiles().OrderByDescending(f => f.LastWriteTime).Where(f => f.Extension == ".json").Select(f => f.Name).ToList();

foreach (var salvataggio in salvataggiFile) {
byte[] myData = Encoding.UTF8.GetBytes(File.ReadAllText(Application.persistentDataPath + "/Saves/" + salvataggio));
using (UnityWebRequest www = UnityWebRequest.Put("https://graph.microsoft.com/v1.0/me/drive/root:/NOMECARTELLASUCLOUD/" + salvataggio + ":/content", myData)) {
www.SetRequestHeader("Authorization", "Bearer " + response.Access_token);
www.SetRequestHeader("Content-Type", "application/json");
yield return www.SendWebRequest();
}
}

Come possiamo vedere qui per creare la cartella su cloud condivisa basta scriverlo nell'url (dropbox richiede la creazione di una cartella condivisa che di conseguenza non verrà segnata nell'url). Notare anche che OneDrive richiede la put qui

Il download è invece la parte più complessa:
prima bisogna prendere la lista di tutti i file nella cartella scelta
using (UnityWebRequest www = UnityWebRequest.Get("https://graph.microsoft.com/v1.0/me/drive/root:/NOMECARTELLASUCLOUD:/children")) {
www.SetRequestHeader("Authorization", "Bearer " + response.Access_token);
www.SetRequestHeader("Content-Type", "application/json");
yield return www.SendWebRequest();
if (www.isNetworkError || www.isHttpError) {
} else {
values = OneDriveResponseList.SaveFromString(www.downloadHandler.text);
}
}
//poi fare la get di ogni singolo file che ha come estensione json
foreach (var salvataggio in values.Value) {
if (salvataggio.Name.EndsWith(".json")) {
using (UnityWebRequest www = UnityWebRequest.Get("https://graph.microsoft.com/v1.0/me/drive/root:/NOMECARTELLASUCLOUD/" + salvataggio.Name + ":/content")) {
www.SetRequestHeader("Authorization", "Bearer " + response.Access_token);
yield return www.SendWebRequest();

if (www.isNetworkError || www.isHttpError) {
} else {
//scrivo il mio salvataggio in una cartella locale
}
}
}
}

La serializzazione è un'altra cosa che OneDrive fa complicata
OneDriveResponseList ha come variabili:
[FormerlySerializedAs("@odata.context")]
[SerializeField] string context;
[SerializeField] OneDriveResponseValue[] value;

OneDriveResponseValue ha:
[FormerlySerializedAs("@microsoft.graph.downloadUrl")]
[SerializeField] string downloadUrl;
[SerializeField] string createdDateTime;
[SerializeField] string cTag;
[SerializeField] string eTag;
[SerializeField] string id;
[SerializeField] string lastModifiedDateTime;
[SerializeField] string name;
[SerializeField] int size;
[SerializeField] string webUrl;
[SerializeField] OneDriveResponseAppUser createdBy;
[SerializeField] OneDriveResponseAppUser lastModifiedBy;
[SerializeField] OneDriveResponseIdType parentReference;
[SerializeField] OneDriveResponseFile file;
[SerializeField] OneDriveResponseFileSystem fileSystemInfo;

OneDriveResponseAppUser
[SerializeField] OneDriveResponseAU application;
[SerializeField] OneDriveResponseAU user;

OneDriveResponseAU
[SerializeField] string displayName;
[SerializeField] string id;

OneDriveResponseIdType
[SerializeField] string driveId;
[SerializeField] string driveType;
[SerializeField] string id;
[SerializeField] string name;
[SerializeField] string path;

OneDriveResponseFile
[SerializeField] string mimeType;
[SerializeField] OneDriveResponseHash hashes;

OneDriveResponseHash
[SerializeField] string sha1Hash;

OneDriveResponseFileSystem
[SerializeField] string createdDateTime;
[SerializeField] string lastModifiedDateTime;

Come vedete è una risposta molto più elaborata di quella di Dropbox.
È però necessaria per ricavare il Nome del nostro salvataggio.

È tutto, il prossimo messaggio sarà dedicato a Mega (molto più semplice).
Lief
Ultimo capitolo dedicato ai salvataggi su cloud: Mega.

Per implementare mega è necessario utilizzare il plugin apposito creato per c#
https://gpailler.github.io/MegaApiClient/
A quell'indirizzo trovate praticamente tutto.

Per l'installazione vi consiglio di scaricare semplicemente MegaApiClient.dll versione net46 e metterla dentro una cartella Plugins/lib su Unity. Potrete così importarla nel vostro codice senza problemi.
Ricordatevi inoltre di utilizzare solo una versione di JsonNet-Lite. Quindi se avete importato UnityGoogleDrive sostituite la versione inclusa in quel pacchetto (meno aggiornata) con quella di MegaApiClient (che è comunque compatibile anche con UnityGoogleDrive).
AGGIORNAMENTO 10-08-2019
Quello che scrivo qua sopra per l'installazione è valido solo se si usa come Scripting Backend Mono senza stripping del codice:
https://docs.unity3d.com/Manual/ManagedCodeStripping.html

IL2CPP (il futuro di Unity e al momento l'unico utilizzabile per iOS) necessità però di uno stripping minimo a Low (non si può disabilitare).
Inoltre IL2CPP essendo compilato in anticipo (AOT) non è compatibile con la classe Emit utilizzata da JsonNet di MegaApiClient che utilizza compilazione JIT (just in time ovvero al primo avvio).
Per risolvere questi problemi ho passato 2 notti in bianco e contattato l'autore di MegaApiClient e quello di JsonNet for Unity... ma alla fin fine ho trovato una soluzione funzionante:
1. Scaricate Json .Net for Unity da qui:
https://assetstore.unity.com/packages/tools...for-unity-11347
vi serve solo lo zip con i sorgenti. Apriteli in Visual Studio, aggiungete un riferimento a UnityEngine.dll (dovete fare sfoglia e scegliere la cartella d'installazione di Unity \Editor\Data\Managed).
Cambiate la versione da 8.3 a 10.0.0.0.
Compilate come Release.
Copiate il nuovo dll e xml ottenuto.
Né MegaApiClient, né UnityGoogleDrive dovrebbero lamentarsi.
Questa nuova versione non usando JIT è compatibile con la compilazione AOT, quindi può essere utilizzata con IL2CPP (che appunto trasforma il codice in C++ e quindi compila in codice macchina e non è quindi compatibile con compilazione al primo avvio come l'interprete mono).
2. Create un file link.xml (basta che sia nella cartella Assets o in una sottocartella):
<linker>
<assembly fullname="MegaApiClient"> // the name of the assembly
<type fullname="CG.Web.MegaApiClient.*" preserve="all"/> // excludes all namespaces and classes recursively under MyNamespace
</assembly>
</linker>
Questo file dice a IL2CPP di Unity di non fare stripping del codice della libreria MegaApiClient nel namespace CG.Web.MegaApiClient e in tutti i figli in maniera ricorsiva. Lo stripping del codice elimina infatti alcuni getter utilizzati da json .net per la serializzazione/deserializzazione il che manda a quel paese il funzionamento di MegaApiClient.
Notare che ora, se selezioniamo .NET Standard 2.0 in Unity e usassimo MegaApiClient .Net 2 il codice funziona.
3. Per rendere compatibile il tutto con .Net 4.x al momento è necessario scaricare il codice sorgente di MegaApiClient:
https://github.com/gpailler/MegaApiClient
Aprire il progetto (potete eliminare quello di test) in visual studio

Compilate per release e copiate il nuovo dll e xml (non è più necessario fare modifiche al codice perché la mia modifica è stata accettata dall'autore di megaapiclient. inoltre è probabile che alla prossima release non dovrete neanche più compilare il codice).

Spiegato così sembra semplice ma debuggare il tutto è stato un vero e proprio inferno (anche perché non funzionava solo su android quindi ho dovuto usare adb che però non legge i debug ma solo i messaggi lanciati dalle eccezioni). Oltretutto su codice non mio. Ma gli autori dei plugin gratuiti e open source hanno fatto di tutto per aiutarmi. Se avete problemi a fare questa procedura quindi contattatemi pure.
FINE AGGIORNAMENTO 10-08-2019

Ok tempo di codificare.
importate le librerie

using CG.Web.MegaApiClient;
using static CG.Web.MegaApiClient.MegaApiClient;

fate il login
var client = new MegaApiClient();
//richiede email e password
AuthInfos auth = GenerateAuthInfos(potresteprenderequestocampodauncampoInputfield.GetComponent<InputField>().text, stessacosaperlapasswordmagariconl'opzionepasswordattivainmodochevenganomostratisoloasterischi.GetComponent<InputField>().text);
//serializziamo la risposta in json
/*
[SerializeField] string email;
[SerializeField] string hash;
[SerializeField] byte[] passwordAesKey;*/
MegaResponse authorization = new MegaResponse();
authorization.Email = auth.Email;
authorization.Hash = auth.Hash;
authorization.PasswordAesKey = auth.PasswordAesKey;

//richiamiamo un metodo per aspettare la risposta
LoginToMega(client, auth);


Il nostro metodo sarà asincrono (in modo che durante il login l'utente non veda il gioco bloccato)
private async void LoginToMega(MegaApiClient client, AuthInfos auth) {
try {
//prova e attende il login asincrono
await client.LoginAsync(auth);
} catch (ApiException e) {
}
//poi fa il log out
await client.LogoutAsync();
}

In pratica se abbiamo avuto successo le credenziali inserite sono corrette.
Il file json che abbiamo salvato si utilizzerà nello stesso modo per il check del login iniziale (se le nostre credenziali sono ancora valide oppure la sessione è scaduta si vedrà qui).


Per l'upload (sempre asincrono)
private async void UploadSavesMega() {
var client = new MegaApiClient();
//prendiamo i dati salvati e dopo aver fatto il log in
MegaApiClient.AuthInfos authInfos = new MegaApiClient.AuthInfos(response.Email, response.Hash, response.PasswordAesKey);
await client.LoginAsync(authInfos);
//prendiamo tutti i nodi (ossia tutte le cartelle
IEnumerable<INode> nodes = await client.GetNodesAsync();
bool cartellaTrovata = false;
//controlliamo tutti i nodi (ossia tutte le cartelle
foreach (var child in nodes)
//e se troviamo una cartella con nome uguale a quello della nostra cartella che vogliamo riempire di salvataggi
if (child != null && child.Name != null && child.Name.Equals("NOMECARTELLASUCLOUD")) {
cartellaTrovata = true;
//riempiamo la cartella
UploadSaveFilesMega(client, child);
return;
}
//se no creiamo la cartella prima di riempirla
if (!cartellaTrovata) {
INode root = nodes.Single(x => x.Type == NodeType.Root);
INode myFolder = await client.CreateFolderAsync("NOMECARTELLASUCLOUD", root);
UploadSaveFilesMega(client, myFolder);
}

//questo è il metodo asincrono che riempe effettivamente la cartella (già presente o creata)
private async void UploadSaveFilesMega(MegaApiClient client, INode folder) {
List<string> salvataggiFile = dirInf.GetFiles().OrderByDescending(f => f.LastWriteTime).Where(f => f.Extension == ".json").Select(f => f.Name).ToList();

foreach (var salvataggio in salvataggiFile) {
//prima cosa cicliamo sui salvataggi in locale e controlliamo se sono su cloud
IEnumerable<INode> nodes = await client.GetNodesAsync();
foreach (var child in nodes)
//se sono su cloud cancelliamo i vecchi salvataggi
if (child != null && child.Name != null && child.Name.Equals(salvataggio))
await client.DeleteAsync(child, false);

string path = Path.GetFullPath("pathlocalealsalvataggio" + salvataggio);
IProgress<double> progress = new Progress<double>();
//upload del file asincrono sulla cartella che abbiamo deciso del file al path locale che abbiamo deciso
INode uploadedFile = await client.UploadFileAsync(@path, folder, progress);
//possiamo anche rinominare il file dopo averlo uploadato (possiamo anche farlo prima ma poi dovremmo cambiare quello locale)
await client.RenameAsync(uploadedFile, salvataggio.Substring(0, salvataggio.Length - 5) + "-to-mega.json");
}
await client.LogoutAsync();
}

Nota bene, a differenza di dropbox e one drive e google drive, mega non ha il replace quindi dobbiamo prima cancellare i file già presenti, poi possiamo mettere quelli nuovi.
Nota bene 2, altra differenza sta nel fatto che mega accetta come parametro un path al file di cui fare l'upload, non un'array di byte, quindi il nome sarà quello originario, per questo dobbiamo fare (se vogliamo) il rename dopo (o prima).

Il download è un po' più semplice in questo caso:
var client = new MegaApiClient();
//login
MegaApiClient.AuthInfos authInfos = new MegaApiClient.AuthInfos(response.Email, response.Hash, response.PasswordAesKey);
await client.LoginAsync(authInfos);
//prendo tutti i nodi
IEnumerable<INode> nodes = await client.GetNodesAsync();

foreach (var child in nodes)
//nella cartella dei salvataggi
if (child != null && child.Name != null && child.Name.Equals("NOMECARTELLASUCLOUD")) {
DownloadSaveFilesMega(client, child);
return;
}

private async void DownloadSaveFilesMega(MegaApiClient client, INode folder) {
IEnumerable<INode> nodes = await client.GetNodesAsync();
foreach (var child in nodes)
//prendo tutti i file che finiscono in json in cui l'id del parent (ossia della cartella che abbiamo passato come parametro) è uguale alla cartella che stiamo analizzando
//è come guardare il nome, solo più preciso perché l'id delle cartelle create su mega è unico
if (child != null && child.Name != null && child.Name.EndsWith(".json") && child.ParentId == folder.Id) {
//creiamo un link per il download
Uri fileLink = await client.GetDownloadLinkAsync(child);
IProgress<double> progress = new Progress<double>();
//faccio il download e lo salvo in una cartella locale con il nome che mi pare
await client.DownloadFileAsync(fileLink, cartellalocale + nuovoNome, progress);
}
}


Con questo abbiamo concluso la sezione sui salvataggi su Cloud. Mi auguro che sia utile a qualcuno.
Il prossimo argomento devo ancora deciderlo ma visto che mi occuperò di quest e inventario oppure combat system potrebbe essere qualcosa di collegato.
Nell'attesa auguro a tutti buon coding e spero qualcuno abbia idee da condividere.
Questa è la versione 'lo-fi' del forum. Per visualizzare la versione completa con molte più informazioni, formattazione ed immagini, per favore clicca qui.
Invision Power Board © 2001-2019 Invision Power Services, Inc.