3. Game launchers si autentificare

Cerinte

Realizarea unui game launcher

  • Creati o aplicatie executabila care sa contina:
    • formular de autentificare cu username si parola
    • sistem de autentificare HTTP cu token
    • afisare nume dupa autentificare si buton de lansare joc
  • Creati o scena Unity care:
    • sa preia parametri din linie de comanda
    • sa faca o cerere HTTP si sa preia numele utilizatorului autentificat
    • ruleze ca executabil (build)

Puteti realiza aplicatia de game launcher in orice tehnologie doriti.

Documentatie video

Gasiti pe MS Teams inregistrat

Documentatie text

Parametri de linie de comanda in Unity

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.

Unity ignoră orice parametru necunoscut, așa cel mai sigur, pentru a păstra clar separarea de comenzile native de unity, sugestia este sa prefixati toate comenzile cu + sau – în loc de -, de exemplu:

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

Aceste două funcții nu sunt disponibile în versiunile/build web.

//read from cmdline parameters
            string[] args = System.Environment.GetCommandLineArgs ();
            for (int i = 0; i < args.Length; i++) {
                Debug.Log ("ARG " + i + ": " + args [i]);
            }

Request-uri HTTP in Unity

WWW / UnityWebRequest

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.

C#

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.

Gestionare autentificare

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:

  • Access tokens
  • ID tokens
  • Self-signed JWTs
  • Refresh tokens
  • Federated tokens
  • Bearer tokens

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");

Game Launcher in C#

Formulare

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).

  • Puteti opta pentru orice alt limbaj de programare / toolkit pentru UI
  • Noi vom folosi Visual Studio 2022 pentru a crea acest formular

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.

  • In mijloc este designer-ul (fereastra cu fundal alb), unde veti putea adauga componente UI
  • In stanga este toolbox-ul (acesta il puteti adauga din View → Toolbox)
  • In dreapta este codul C# aferent acestei ferestre - aici veti lega evenimentele componentelor din UI cu logica voastra
  • In mijloc-jos este codul XAML - elementele continute in fereastra sunt definita folosind acele tag-uri (nu e de interes daca faceti doar drag-and-drop)

Pentru a va obisnui cu lucrul in acest environment, prezentam un exemplu simplu de adaugare componenta UI si legare a unui eveniment

  • Adaugati oriunde in designer, folosind drag-and-drop, un buton
  • Daca selectati butonul adaugat, in panoul Properties (in mod default apare in dreapta IDE-ului) puteti modifica diverse proprietati ale acestui element de UI
  • Pentru a adauga un eveniment de click in cod:
    • O varinta este sa dati dublu-click pe buton (in afara textului)
    • O varianta (mai recomandata) este ca din panoul de properties sa mergeti pe tab-ul de evenimente (iconita de fulger), sa scrieti numele metodei in drepta evenimentului de Click, si sa apasati Enter
      • Ar trebui sa se genereze in cod event handler-ul aferent

  • Pentru a testa, adaugati in metoda 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.

Voi va trebui sa va creati propriul formular pentru acest launcher. In mod minimal acesta ar trebui sa contina:

  • 2 text box-uri, fiecare cu cate un label, pentru username si password
  • 2 butoane - login si start game
  • 1 label / text block in care sa afisati un mesaj de status

HTTP

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.

Puteti opta si sa folositi si alte servicii pentru acest backend, daca doriti.

Quickstart:

  • Creati-va un cont pe Back4App
  • Creati-va un nou proiect de tip Backend Platform
  • Aici va puteti crea baze de date pentru useri - in cel mai simplu mod, ne vom folosi de baza de date default, _User
    • Creati o coloana noua, de exemplu displayName. Considerati ca acesta este numele user-ului vostru in joc, diferit de cel de logare (username)
    • Adaugati un rand nou si completati datele 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):

  • Se creaza header-ele necesare request-ului HTTP
  • Se creaza continutul necesar request-ului (username, password)
  • Se realizeaza request-ul printr-un POST
  • Se asteapta si parseaza raspunsul (in mod tipic, JSON)
  • Se extrage session token-ul (+ alte date de interes din JSON)

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;
    }
}

Nu uitati sa schimbati YOUR_APP_ID si YOUR_REST_API_KEY. Le gasiti in dashboard-ul proiectului Back4App → App Settings → Security & Keys.

Unity

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.

pjv/laboratoare/2024/a03.txt · Last modified: 2024/11/04 12:42 by andrei.lapusteanu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0