Realizarea unui game launcher
Gasiti pe MS Teams inregistrat
Un executabil Unity poate fi rulat si cu parametri de linie de comanda. Exista doua tipuri de parametri:
La lansarea aplicațiilor Unity Player, delimitatorul pentru o valoare a argumentului liniei de comandă este un singur spațiu. De exemplu, pentru a seta modul fereastră la fără margini, utilizați -window-mode borderless.
Exemple:
--custom-param 123 -batchMode -executeMethod Build.CommandLineMake +buildLocation Build/Output/WebPlayer +buildTarget WebPlayer
unity.exe --user-token AAA -logFile C:/...
Pentru preluarea acestor parametri, putem folosi clasa Environment care face parte din cadrul .NET.
System.Environment.CommandLine System.Environment.CommandLineArgs
//read from cmdline parameters string[] args = System.Environment.GetCommandLineArgs (); for (int i = 0; i < args.Length; i++) { Debug.Log ("ARG " + i + ": " + args [i]); }
Unity ofera un sistem de interactiune HTTP denumite UnityWebRequest (in versiuni <2019 WWW). Mai multe detalii despre arhitectura si capabilitati gasiti aici: https://docs.unity3d.com/Manual/web-request.html.
Aceste sistem foloseste corutine intrucat cererile HTTP sunt in general asincrone - trebuie sa asteptam un raspuns, care poate dura si cateva secunde, fara a afecta/opri rularea aplicatiei Unity/a jocului.
Mai jos gasiti un exemplu, preluat din pagina de documentatie oficiala https://docs.unity3d.com/2022.3/Documentation/ScriptReference/Networking.UnityWebRequest.Get.html, care prezinta cum se trateaza un request simplu de preluare de informatii (GET).
using UnityEngine; using UnityEngine.Networking; using System.Collections; // UnityWebRequest.Get example // Access a website and use UnityWebRequest.Get to download a page. // Also try to download a non-existing page. Display the error. public class Example : MonoBehaviour { void Start() { // A correct website page. StartCoroutine(GetRequest("https://www.example.com")); // A non-existing page. StartCoroutine(GetRequest("https://error.html")); } IEnumerator GetRequest(string uri) { using (UnityWebRequest webRequest = UnityWebRequest.Get(uri)) { // Request and wait for the desired page. yield return webRequest.SendWebRequest(); string[] pages = uri.Split('/'); int page = pages.Length - 1; switch (webRequest.result) { case UnityWebRequest.Result.ConnectionError: case UnityWebRequest.Result.DataProcessingError: Debug.LogError(pages[page] + ": Error: " + webRequest.error); break; case UnityWebRequest.Result.ProtocolError: Debug.LogError(pages[page] + ": HTTP Error: " + webRequest.error); break; case UnityWebRequest.Result.Success: Debug.Log(pages[page] + ":\nReceived: " + webRequest.downloadHandler.text); break; } } } }
Sistemul permite de asemenea si folosirea de requesturi de tip POST sau PUT si transmiterea de informatii in diferite forme (text, bits, formdata etc). Cateva exemple:
using UnityEngine; using UnityEngine.Networking; using System.Collections; public class MyBehavior : MonoBehaviour { void Start() { StartCoroutine(Upload()); } IEnumerator Upload() { using (UnityWebRequest www = UnityWebRequest.Post("https://www.my-server.com/myapi", "{ \"field1\": 1, \"field2\": 2 }", "application/json")) { yield return www.SendWebRequest(); if (www.result != UnityWebRequest.Result.Success) { Debug.LogError(www.error); } else { Debug.Log("Form upload complete!"); } } } }
PUT request:
byte[] myData = System.Text.Encoding.UTF8.GetBytes("This is some test data"); using (UnityWebRequest www = UnityWebRequest.Put("https://www.my-server.com/upload", myData)) { yield return www.SendWebRequest(); if (www.result != UnityWebRequest.Result.Success) { Debug.Log(www.error); } else { Debug.Log("Upload complete!"); } }
POST request cu FormData:
IEnumerator postRequest(string url) { WWWForm form = new WWWForm(); form.AddField("myField", "myData"); form.AddField("Game Name", "Mario Kart"); UnityWebRequest uwr = UnityWebRequest.Post(url, form); yield return uwr.SendWebRequest(); if (uwr.isNetworkError) { Debug.Log("Error While Sending: " + uwr.error); } else { Debug.Log("Received: " + uwr.downloadHandler.text); } }
POST request cu Json:
IEnumerator postRequest(string url, string json) { var uwr = new UnityWebRequest(url, "POST"); byte[] jsonToSend = new System.Text.UTF8Encoding().GetBytes(json); uwr.uploadHandler = (UploadHandler)new UploadHandlerRaw(jsonToSend); uwr.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer(); uwr.SetRequestHeader("Content-Type", "application/json"); //Send the request then wait here until it returns yield return uwr.SendWebRequest(); if (uwr.isNetworkError) { Debug.Log("Error While Sending: " + uwr.error); } else { Debug.Log("Received: " + uwr.downloadHandler.text); } }
POST request cu Multipart FormData/Multipart (fisier):
IEnumerator postRequest(string url) { List<IMultipartFormSection> formData = new List<IMultipartFormSection>(); formData.Add(new MultipartFormDataSection("field1=foo&field2=bar")); formData.Add(new MultipartFormFileSection("my file data", "myfile.txt")); UnityWebRequest uwr = UnityWebRequest.Post(url, formData); yield return uwr.SendWebRequest(); if (uwr.isNetworkError) { Debug.Log("Error While Sending: " + uwr.error); } else { Debug.Log("Received: " + uwr.downloadHandler.text); } }
DELETE request:
IEnumerator deleteRequest(string url) { UnityWebRequest uwr = UnityWebRequest.Delete(url); yield return uwr.SendWebRequest(); if (uwr.isNetworkError) { Debug.Log("Error While Sending: " + uwr.error); } else { Debug.Log("Deleted"); } }
Mai multe detalii gasiti aici: https://docs.unity3d.com/ScriptReference/Networking.UnityWebRequest.html.
In .NET Framework > 4.5 (ar trebui sa fie disponibil implicit in versiunea 2022) putem folosi si direct sistemul de client HTTP din C# https://docs.unity3d.com/2022.3/Documentation/Manual/dotnetProfileSupport.html.
{ var httpClient = new HttpClient(); HttpClient.BaseAddress = new Uri(Global.BaseUrl); HttpClient.DefaultRequestHeaders.Clear(); HttpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); // if you're using json service // make request var response = Global.HttpClient.PostAsJsonAsync(subPath.TrimLeadingSlash(), payload).Result; // check for error response.EnsureSuccessStatusCode(); // return result return response.Content; }
Totusi, dupa cum se poate observa, trebuie gestionate cumva evenimentele de asteptare/asincrone. Acest lucru se poate face in Unity tot prin rularea acesteia prin corutine si asteptarea raspunsului, sau, intr-un mod mai eficient si mai avansat prin folosirea de Promises.
La ce ne ajuta sistemul de Promises ? În timp ce sistemul de callback/corutine funcționează pentru situații simple, poate scăpa cu ușurință de sub control și poate fi greu de gestionat. De exemplu, este des intalnit să executați operațiuni asincrone într-o secvență, una după alta. Iată cum arată dacă folosim apeluri standard:
// our main function that calls the async function public void Main() { // call the first async function AsyncOperation(() => { // call the second async function AsyncOperation(() => { // call the third async function AsyncOperation(() => { // continue ... }); }); }); } public void AsyncOperation(Action callback) { // execute async operation (takes some unknown amount of time)... // async op is done, call the callback function callback.Invoke(); }
In mare parte, simplifica metoda de lucrul cu callback-uri sau asteptari, in special in cazul in care avem request-uri interdependente.
Ce sunt Promises/Promisiunile? O Promisiune este un obiect care reprezintă eventuala finalizare sau eșec a unei operațiuni asincrone. Dacă o funcție returnează o Promisiune, nu returnează imediat o valoare, dar Promisiunea o va returna odată ce operația de asincronizare este finalizată.
Poate fi mai ușor de înțeles uitându-ne la un exemplu:
public void Main() { // DownloadData is the async operation, and it returns a Promise DownloadData("<some https url>") .Then((data) => { // This function gets called if DownloadData is successful // the 'data' argument is the result of the download }) .Catch(() => { // This function gets called if DownloadData fails // In that case, the above .Then will not be called }); }
După cum puteți vedea mai sus, obiectul Promise expune funcții precum .Then() sau .Catch(). Funcția transmisă la .Then() este apelată odată ce Promisiunea „se rezolvă” (operația asincronă se finalizează cu succes) și ia rezultatul operației asincrone ca parametru. Funcția transmisă la .Catch() este apelată dacă apar erori în timpul operației asincrone.
De asemenea, metodele .Then() și .Catch() returnează promisiuni, astfel încât promisiunile pot fi înlănțuite (permițându-ne să executăm secvențe de evenimente asincrone!).
Pentru o descriere mult mai bună a Promises, consultați MDN Docs.
Din nefericire C# nu are integrate Promises ca funcitonalitate nativa, astfel incat le puteti folosi prin adaugarea de pachete noi, cum ar fi:
In Unity totusi, puteti folosi mai simplu acest fel de sistem prin pachetul REST Client for Unity care are deja integrata partea de promises si o parte de serializare JSON.
Un exemplu de utilizare:
//exemplu de structura de date pentru parsare JSON [Serializable] public class UserData { public int id; public string name; public string email; public UserAvatar avatar; public UserProfile profile; public UserQuest[] quests; public UserTour[] tours; public LibraryEvent[] libraryEvents; } //exemplu de functie care returneaza un Promise public RSG.IPromise<UserData> GetUserProfile() { return RestClient.Get<UserData>(Config.apiURL + "user") .Then(result => { //Debug.Log(JsonUtility.ToJson(result,true)); data = result; email = data.email; name = data.name; quests = data.quests; id = Convert.ToUInt32(data.id); return data; }); } //utilizare in cascada de evenimente void Start () { User.instance.GetUserProfile() .Then(result => { Debug.Log("get library"); return Library.instance.GetLibraryProfile(); }) .Then(result => { Debug.Log("load scene"); SceneManager.LoadScene(Config.libraryTitle); // SceneManager.LoadScene(1); // username.text = result.name; }); }
Gasiti mai multa documentatie aici https://github.com/proyecto26/RestClient.
Pentru a prelua din web informatii speficice pentru un anumit utilizator, cererile HTTP trebuie complementate cu informatia de autentificare prin folosirea unui header de Autorizare (Authorization).
In acest header transmitem in general un token. In functie de nevoi/capabilitati/dispozitive, exista mai multe tipuri de token-uri care pot fi folosite:
Cel mai dese folosite sunt Bearer tokens, care, dupa cum descrie si numele, sunt o clasă generală de jeton care acordă acces părții în posesia tokenului. Token-urile de acces, ID tokens și JWT-urile sunt toate tokenuri Bearer.
Mai multe detalii despre tokenuri puteti citi aici: https://cloud.google.com/docs/authentication/token-types.
Pentru a folosi un token de tip Bearer, fiecare cerere HTTP trebuie sa conta un header de Autorizare de tip Bearer. De exemplu:
//Pentru RestClient package //autorizare RestClient.DefaultRequestHeaders["Authorization"] = "Bearer "+token.access_token; //alte headere in functie de nevoie RestClient.DefaultRequestHeaders["Accept"] = "application/json"; //pentru UnityWebRequests var uwr = new UnityWebRequest(url, "POST"); //autorizare uwr.SetRequestHeader("Authorization", "Bearer "+token.access_token); //alte headere in functie de nevoie uwr.SetRequestHeader("Content-Type", "application/json");
In acest capitol vom prezenta cum puteti crea un game launcher simplu folosind C# si WPF (Windows Presentation Foundation). Cea din urma este o biblioteca ce permite realizarea formularelor / interfetelor grafice intr-o aplicatie Windows printr-o metoda facila (drag-and-drop).
Asigurati-va ca aveti modulul .NET desktop development instalat (verificati din Visual Studio Installer).
Creati un nou proiect “WPF Application”.
Mai jos aveti o imagine ce prezinta panourile de interes din IDE.
Pentru a va obisnui cu lucrul in acest environment, prezentam un exemplu simplu de adaugare componenta UI si legare a unui eveniment
Debug.WriteLine(“Button clicked”);
(sau orice similar), rulati, apasati butonul si vedeti daca va apare in consola mesajul
Un aspect important este numirea acestor elememente de UI. Astfel le veti putea referentia in cod (de ex. veti avea nevoie sa referentiati un text box pentru a extrage proprietatea Text
). Aveti mai jos un exemplu de utilizare.
Pentru partea de backend in general este nevoie de un serviciu Web, cu o baza de date in spate. Pentru exemplificare ne vom folosi de Back4App, ce este o platoforma de tip backend-as-a-service (BaaS), construita pe Parse, ce ofera stocare cloud, servicii de autentificare, baze de date, etc.
In mod special pentru acest laborator ne vom folosi de apeluri catre endpoint-uri via un REST API (request-uri HTTP).
In esenta, datele din formularul nostru le vom trimite prin API call-uri catre un proiect creat in Back4App (ce contine o baza de date cu userii nostrii) pe care-i vom autentifica. Rezultatul, in caz de succes, va fi returnarea unui session token, pe care-l veti putea folosi pentru alte request-uri.
Quickstart:
displayName
. Considerati ca acesta este numele user-ului vostru in joc, diferit de cel de logare (username
)username
, password
si displayName
Pentru a reliza autentificarea sunt necesari o serie de pasi in cod (cei prezentati in continuare sunt specifici pentru C#, dar ideea generala este aceeasi indiferent de limbaj):
Mai jos aveti un exemplu de metoda de login in C# pentru Back4App.
private async Task<string> LoginUser(string username,string password) { // Initialize the HTTP client & set the required headers. var client = new HttpClient(); client.DefaultRequestHeaders.Add("X-Parse-Application-Id", "YOUR_APP_ID"); client.DefaultRequestHeaders.Add("X-Parse-REST-API-Key", "YOUR_REST_API_KEY"); // Create the request message content. var content = new StringContent($"{{\"username\":\"{username}\",\"password\":\"{password}\"}}", System.Text.Encoding.UTF8, "application/json"); // Send POST request to back4app's login endpoint. var response = await client.PostAsync("https://parseapi.back4app.com/login", content); if (response.IsSuccessStatusCode) { // Get response. string responseBody = await response.Content.ReadAsStringAsync(); // Create JSON. var json = JsonObject.Parse(responseBody); // Return the `sessionToken` (get it from the JSON). return json["sessionToken"].ToString(); } else { Debug.WriteLine($"Login failed : {response.StatusCode}, {response.ReasonPhrase}"); return null; } }
YOUR_APP_ID
si YOUR_REST_API_KEY
. Le gasiti in dashboard-ul proiectului Back4App → App Settings → Security & Keys.
Dupa acesti pasi va trebui sa va folositi de celalalt buton din form (Start Game), prin care veti lansa in executie build-ul de Unity, caruia ii trimiteti argumentele necesare. Va trebui sa trimiteti si acel session token capturat anterior.
Va trebui sa va alegeti un call care necesita acest session token, anume un call care are nevoie de campul din header X-Parse-Session-Token
.
Folositi UnityWebRequest
pentru a face request-ul. In cerinte vi se specifica sa faceti un call pentru a returna numele utilizatorului (poate fi in exemplul nostru acest displayName
).
Un exemplu de API call ce se foloseste de acest session token este ilustrat mai jos.