Unity with MVC: come aumentare di livello lo sviluppo del tuo gioco

Pubblicato: 2022-03-11

I programmatori per la prima volta di solito iniziano ad imparare il mestiere con il classico programma Hello World . Da lì, seguiranno incarichi sempre più grandi. Ogni nuova sfida porta a casa una lezione importante:

Più grande è il progetto, più grandi saranno gli spaghetti.

Presto è facile vedere che in squadre grandi o piccole non si può fare incautamente ciò che si vuole. Il codice deve essere mantenuto e può durare a lungo. Le aziende per cui hai lavorato non possono semplicemente cercare le tue informazioni di contatto e chiederti ogni volta che vogliono correggere o migliorare la base di codice (e tu non vuoi che lo facciano).

Questo è il motivo per cui esistono modelli di progettazione del software; impongono regole semplici per dettare la struttura complessiva di un progetto software. Aiutano uno o più programmatori a separare i pezzi principali di un grande progetto e ad organizzarli in modo standardizzato, eliminando la confusione quando si incontra una parte sconosciuta della base di codice.

Queste regole, se seguite da tutti, consentono di mantenere e navigare meglio il codice legacy e di aggiungere più rapidamente il nuovo codice. Meno tempo è dedicato alla pianificazione della metodologia di sviluppo. Poiché i problemi non si presentano in un solo aspetto, non esiste un modello di progettazione proiettile d'argento. Bisogna considerare attentamente i punti forti e deboli di ogni modello e trovare la soluzione migliore per la sfida in corso.

In questo tutorial, racconterò la mia esperienza con la popolare piattaforma di sviluppo di giochi Unity e il modello Model-View-Controller (MVC) per lo sviluppo di giochi. Nei miei sette anni di sviluppo, dopo aver lottato con la mia giusta quota di spaghetti per sviluppatori di giochi, ho ottenuto un'ottima struttura del codice e velocità di sviluppo utilizzando questo modello di progettazione.

Inizierò spiegando un po' dell'architettura di base di Unity, il modello Entity-Component. Quindi passerò a spiegare come si adatta MVC e userò un piccolo progetto fittizio come esempio.

Motivazione

Nella letteratura del software troveremo un gran numero di modelli di progettazione. Anche se hanno una serie di regole, gli sviluppatori di solito eseguono un po' di regole per adattare meglio il modello al loro problema specifico.

Questa “libertà di programmazione” è la prova che non abbiamo ancora trovato un metodo unico e definitivo per progettare il software. Pertanto, questo articolo non vuole essere la soluzione definitiva al tuo problema, ma piuttosto mostrare i vantaggi e le possibilità di due modelli ben noti: Entity-Component e Model-View-Controller.

Il modello entità-componente

Entity-Component (EC) è un modello di progettazione in cui definiamo prima la gerarchia degli elementi che compongono l'applicazione (Entities) e, successivamente, definiamo le funzionalità e i dati che ciascuna conterrà (Components). In termini più "programmatori", un'Entità può essere un oggetto con un array di 0 o più Componenti. Descriviamo un'entità come questa:

 some-entity [component0, component1, ...]

Ecco un semplice esempio di albero EC.

 - app [Application] - game [Game] - player [KeyboardInput, Renderer] - enemies - spider [SpiderAI, Renderer] - ogre [OgreAI, Renderer] - ui [UI] - hud [HUD, MouseInput, Renderer] - pause-menu [PauseMenu, MouseInput, Renderer] - victory-modal [VictoryModal, MouseInput, Renderer] - defeat-modal [DefeatModal, MouseInput, Renderer]

EC è un buon modello per alleviare i problemi dell'ereditarietà multipla, in cui una struttura di classe complessa può introdurre problemi come il problema del diamante in cui una classe D, ereditando due classi, B e C, con la stessa classe base A, può introdurre conflitti perché come B e C modificano le caratteristiche di A in modo diverso.

IMMAGINE: PROBLEMA DEL DIAMANTE

Questi tipi di problemi possono essere comuni nello sviluppo di giochi in cui l'ereditarietà è spesso ampiamente utilizzata.

Scomponendo le funzionalità e i gestori dati in Componenti più piccoli, possono essere collegati e riutilizzati in diverse Entità senza fare affidamento su più ereditarietà (che, tra l'altro, non è nemmeno una funzionalità di C# o Javascript, i principali linguaggi utilizzati da Unity ).

Dove l'entità-componente non è all'altezza

Essendo un livello sopra OOP, EC aiuta a deframmentare e organizzare meglio l'architettura del codice. Tuttavia, nei grandi progetti, siamo ancora "troppo liberi" e possiamo trovarci in un "oceano di funzionalità", avendo difficoltà a trovare le entità e i componenti giusti o a capire come dovrebbero interagire. Esistono infiniti modi per assemblare entità e componenti per un determinato compito.

IMMAGINE: CARATTERISTICA CE OCEANO

Un modo per evitare un pasticcio è imporre alcune linee guida aggiuntive su Entity-Component. Ad esempio, un modo in cui mi piace pensare al software è dividerlo in tre diverse categorie:

  • Alcuni gestiscono i dati grezzi, consentendone la creazione, la lettura, l'aggiornamento, la cancellazione o la ricerca (ad esempio, il concetto CRUD).
  • Altri implementano l'interfaccia con cui altri elementi possono interagire, rilevando eventi correlati al loro ambito e attivando notifiche quando si verificano.
  • Infine, alcuni elementi sono responsabili della ricezione di queste notifiche, della presa di decisioni sulla logica aziendale e del modo in cui devono essere manipolati i dati.

Fortunatamente, abbiamo già uno schema che si comporta esattamente in questo modo.

Il modello Model-View-Controller (MVC).

Il modello Model-View-Controller (MVC) divide il software in tre componenti principali: modelli (dati CRUD), visualizzazioni (interfaccia/rilevamento) e controller (decisione/azione). MVC è abbastanza flessibile da essere implementato anche su ECS o OOP.

Lo sviluppo del gioco e dell'interfaccia utente prevede il normale flusso di lavoro di attendere l'input di un utente o altre condizioni di attivazione, inviare la notifica di tali eventi in un luogo appropriato, decidere cosa fare in risposta e aggiornare i dati di conseguenza. Queste azioni mostrano chiaramente la compatibilità di queste applicazioni con MVC.

Questa metodologia introduce un altro livello di astrazione che aiuterà con la pianificazione del software e consentirà anche ai nuovi programmatori di navigare anche in una base di codice più grande. Suddividendo il processo di pensiero in dati, interfaccia e decisioni, gli sviluppatori possono ridurre il numero di file di origine che devono essere cercati per aggiungere o correggere funzionalità.

Unità e CE

Diamo prima un'occhiata più da vicino a ciò che Unity ci offre in anticipo.

Unity è una piattaforma di sviluppo basata su EC, in cui tutte le Entità sono istanze di GameObject e le funzionalità che le rendono "visibili", "mobili", "interagibili" e così via, sono fornite da classi che estendono Component .

Il pannello della gerarchia e il pannello dell'ispettore dell'editor di Unity forniscono un modo efficace per assemblare la tua applicazione, collegare componenti, configurare il loro stato iniziale e avviare il tuo gioco con molto meno codice sorgente di quanto farebbe normalmente.

SCHERMO: PANNELLO GERARCHIA
Pannello della gerarchia con quattro GameObject sulla destra

SCHERMO: PANNELLO DI ISPEZIONE
Pannello di ispezione con i componenti di GameObject

Tuttavia, come abbiamo discusso, possiamo affrontare il problema delle "troppe funzionalità" e ritrovarci in una gigantesca gerarchia, con funzionalità sparse ovunque, che rendono la vita di uno sviluppatore molto più difficile.

Pensando alla maniera MVC, possiamo, invece, iniziare dividendo le cose in base alla loro funzione, strutturando la nostra applicazione come nell'esempio seguente:

SCREENSHOT: UNITY MVC ESEMPIO DI STRUTTURA

Adattare MVC a un ambiente di sviluppo di giochi

Ora, vorrei introdurre due piccole modifiche al modello MVC generico, che aiutano ad adattarlo a situazioni uniche in cui mi sono imbattuto nella creazione di progetti Unity con MVC:

  1. I riferimenti alla classe MVC vengono facilmente sparsi nel codice. - All'interno di Unity, gli sviluppatori in genere devono trascinare e rilasciare le istanze per renderle accessibili, oppure raggiungerle tramite istruzioni ingombranti come GetComponent( ... ) . - Se Unity si arresta in modo anomalo o qualche bug fa scomparire tutti i riferimenti trascinati, si verificherà un inferno di riferimenti persi. - Ciò rende necessario disporre di un unico oggetto di riferimento radice, attraverso il quale tutte le istanze nell'Applicazione possono essere raggiunte e ripristinate.
  2. Alcuni elementi racchiudono funzionalità generali che dovrebbero essere altamente riutilizzabili e che non rientrano naturalmente in una delle tre categorie principali di Modello, Vista o Controller. Questi che mi piace chiamare semplicemente Componenti . Sono anche "Componenti" nel senso di Entità-Componente ma agiscono semplicemente come aiutanti nel framework MVC. - Ad esempio, un componente Rotator , che ruota le cose solo di una determinata velocità angolare e non notifica, memorizza o decide nulla.

Per aiutare ad alleviare questi due problemi, ho escogitato un modello modificato che chiamo AMVCC o Application-Model-View-Controller-Component.

IMMAGINE: SCHEMA AMVCC

  • Applicazione : un unico punto di ingresso per l'applicazione e il contenitore di tutte le istanze critiche e dei dati relativi all'applicazione.
  • MVC - Dovresti saperlo ormai. :)
  • Componente : script piccolo e ben contenuto che può essere riutilizzato.

Queste due modifiche hanno soddisfatto le mie esigenze per tutti i progetti in cui le ho utilizzate.

Esempio: 10 rimbalzi

Come semplice esempio, diamo un'occhiata a un piccolo gioco chiamato 10 Bounces , in cui utilizzerò gli elementi principali del pattern AMVCC.

La configurazione del gioco è semplice: una Ball con uno SphereCollider e un Rigidbody (che inizierà a cadere dopo "Play"), un Cube come terreno e 5 script per comporre l'AMVCC.

Gerarchia

Prima di creare script, di solito inizio dalla gerarchia e creo uno schema della mia classe e delle mie risorse. Sempre seguendo questo nuovo stile AMVCC.

SCREENSHOT: COSTRUIRE LA GERARCHIA

Come possiamo vedere, la view GameObject contiene tutti gli elementi visivi e anche quelli con altri script di View . Il model e il controller GameObjects, per piccoli progetti, di solito contengono solo i rispettivi script. Per progetti più grandi, conterranno GameObjects con script più specifici.

Quando qualcuno che sta navigando nel tuo progetto vuole accedere:

  • Dati: Vai application > model > ...
  • Logica/flusso di lavoro: vai su application > controller > ...
  • Rendering/interfaccia/rilevamento: vai su application > view > ...

Se tutti i team seguono queste semplici regole, i progetti legacy non dovrebbero diventare un problema.

Nota che non esiste un contenitore di Component perché, come abbiamo discusso, sono più flessibili e possono essere collegati a diversi elementi a piacimento dello sviluppatore.

Sceneggiatura

Nota: gli script mostrati di seguito sono versioni astratte di implementazioni reali. Un'implementazione dettagliata non gioverebbe molto al lettore. Tuttavia, se desideri esplorare di più, ecco il link al mio framework MVC personale per Unity, Unity MVC. Troverai classi principali che implementano il framework strutturale AMVCC necessario per la maggior parte delle applicazioni.

Diamo un'occhiata alla struttura degli script per 10 Bounces .

Prima di iniziare, per chi non ha familiarità con il flusso di lavoro di Unity, chiariamo brevemente come funzionano insieme gli script e i GameObject. In Unity, i "Componenti", nel senso Entità-Componente, sono rappresentati dalla classe MonoBehaviour . Affinché ne esista uno durante il runtime, lo sviluppatore deve trascinare e rilasciare il suo file sorgente in un GameObject (che è l'"Entity" del pattern Entity-Component) o utilizzare il comando AddComponent<YourMonobehaviour>() . Successivamente, lo script verrà istanziato e pronto per l'uso durante l'esecuzione.

Per iniziare, definiamo la classe Application (la "A" in AMVCC), che sarà la classe principale contenente i riferimenti a tutti gli elementi di gioco istanziati. Creeremo anche una classe base di supporto chiamata Element , che ci dà accesso all'istanza dell'applicazione e alle istanze MVC dei suoi figli.

Con questo in mente, definiamo la classe Application (la "A" in AMVCC), che avrà un'istanza univoca. Al suo interno, tre variabili, model , view e controller , ci forniranno punti di accesso per tutte le istanze MVC durante il runtime. Queste variabili dovrebbero essere MonoBehaviour con riferimenti public agli script desiderati.

Quindi, creeremo anche una classe base di supporto chiamata Element , che ci dà accesso all'istanza dell'applicazione. Questo accesso consentirà a ogni classe MVC di raggiungersi.

Si noti che entrambe le classi estendono MonoBehaviour . Sono "Componenti" che verranno allegati alle "Entità" di GameObject.

 // BounceApplication.cs // Base class for all elements in this application. public class BounceElement : MonoBehaviour { // Gives access to the application and all instances. public BounceApplication app { get { return GameObject.FindObjectOfType<BounceApplication>(); }} } // 10 Bounces Entry Point. public class BounceApplication : MonoBehaviour { // Reference to the root instances of the MVC. public BounceModel model; public BounceView view; public BounceController controller; // Init things here void Start() { } }

Da BounceElement possiamo creare le classi principali MVC. Gli BounceModel , BounceView e BounceController solito fungono da contenitori per istanze più specializzate, ma poiché questo è un semplice esempio solo la View avrà una struttura nidificata. Il Modello e il Controller possono essere eseguiti in un unico script per ciascuno:

 // BounceModel.cs // Contains all data related to the app. public class BounceModel : BounceElement { // Data public int bounces; public int winCondition; }
 // BounceView .cs // Contains all views related to the app. public class BounceView : BounceElement { // Reference to the ball public BallView ball; }
 // BallView.cs // Describes the Ball view and its features. public class BallView : BounceElement { // Only this is necessary. Physics is doing the rest of work. // Callback called upon collision. void OnCollisionEnter() { app.controller.OnBallGroundHit(); } }
 // BounceController.cs // Controls the app workflow. public class BounceController : BounceElement { // Handles the ball hit event public void OnBallGroundHit() { app.model.bounces++; Debug.Log(“Bounce ”+app.model.bounce); if(app.model.bounces >= app.model.winCondition) { app.view.ball.enabled = false; app.view.ball.GetComponent<RigidBody>().isKinematic=true; // stops the ball OnGameComplete(); } } // Handles the win condition public void OnGameComplete() { Debug.Log(“Victory!!”); } }

Dopo aver creato tutti gli script, possiamo procedere ad allegarli e configurarli.

Il layout della gerarchia dovrebbe essere questo:

 - application [BounceApplication] - model [BounceModel] - controller [BounceController] - view [BounceView] - ... - ball [BallView] - ...

Usando il BounceModel come esempio, possiamo vedere come appare nell'editor di Unity:

SCREENSHOT: BounceModel IN ISPETTORE
BounceModel con i campi bounces e winCondition .

Con tutti gli script impostati e il gioco in esecuzione, dovremmo ottenere questo output nel pannello della console .

SCHERMO: USCITA CONSOLE

Notifiche

Come mostrato nell'esempio sopra, quando la palla colpisce il terreno, la sua vista esegue app.controller.OnBallGroundHit() che è un metodo. Non è, in alcun modo, "sbagliato" farlo per tutte le notifiche nell'applicazione. Tuttavia, nella mia esperienza, ho ottenuto risultati migliori utilizzando un semplice sistema di notifica implementato nella classe AMVCC Application.

Per implementarlo, aggiorniamo il layout di BounceApplication in modo che sia:

 // BounceApplication.cs class BounceApplication { // Iterates all Controllers and delegates the notification data // This method can easily be found because every class is “BounceElement” and has an “app” // instance. public void Notify(string p_event_path, Object p_target, params object[] p_data) { BounceController[] controller_list = GetAllControllers(); foreach(BounceController c in controller_list) { c.OnNotification(p_event_path,p_target,p_data); } } // Fetches all scene Controllers. public BounceController[] GetAllControllers() { /* ... */ } }

Successivamente, abbiamo bisogno di un nuovo script in cui tutti gli sviluppatori aggiungeranno i nomi degli eventi di notifica, che possono essere inviati durante l'esecuzione.

 // BounceNotifications.cs // This class will give static access to the events strings. class BounceNotification { static public string BallHitGround = “ball.hit.ground”; static public string GameComplete = “game.complete”; /* ... */ static public string GameStart = “game.start”; static public string SceneLoad = “scene.load”; /* ... */ }

È facile vedere che, in questo modo, la leggibilità del codice è migliorata perché gli sviluppatori non hanno bisogno di cercare in tutto il codice sorgente i metodi controller.OnSomethingComplexName per capire che tipo di azioni possono verificarsi durante l'esecuzione. Controllando un solo file, è possibile comprendere il comportamento generale dell'applicazione.

Ora, dobbiamo solo adattare BallView e BounceController per gestire questo nuovo sistema.

 // BallView.cs // Describes the Ball view and its features. public class BallView : BounceElement { // Only this is necessary. Physics is doing the rest of work. // Callback called upon collision. void OnCollisionEnter() { app.Notify(BounceNotification.BallHitGround,this); } }
 // BounceController.cs // Controls the app workflow. public class BounceController : BounceElement { // Handles the ball hit event public void OnNotification(string p_event_path,Object p_target,params object[] p_data) { switch(p_event_path) { case BounceNotification.BallHitGround: app.model.bounces++; Debug.Log(“Bounce ”+app.model.bounce); if(app.model.bounces >= app.model.winCondition) { app.view.ball.enabled = false; app.view.ball.GetComponent<RigidBody>().isKinematic=true; // stops the ball // Notify itself and other controllers possibly interested in the event app.Notify(BounceNotification.GameComplete,this); } break; case BounceNotification.GameComplete: Debug.Log(“Victory!!”); break; } } }

I progetti più grandi riceveranno molte notifiche. Quindi, per evitare di ottenere una grande struttura di switch-case, è consigliabile creare controller diversi e far sì che gestiscano ambiti di notifica diversi.

AMVCC nel mondo reale

Questo esempio ha mostrato un semplice caso d'uso per il modello AMVCC. Adattare il tuo modo di pensare in termini di tre elementi di MVC e imparare a visualizzare le entità come una gerarchia ordinata, sono le abilità che dovrebbero essere perfezionate.

Nei progetti più grandi, gli sviluppatori dovranno affrontare scenari più complessi e dubbi sul fatto che qualcosa debba essere una vista o un controller, o se una determinata classe debba essere separata in modo più completo in quelle più piccole.

Regole empiriche (di Eduardo)

Non esiste alcuna "Guida universale per l'ordinamento MVC" da nessuna parte. Ma ci sono alcune semplici regole che in genere seguo per aiutarmi a determinare se definire qualcosa come Modello, Vista o Controller e anche quando dividere una determinata classe in parti più piccole.

Di solito, questo accade organicamente mentre penso all'architettura del software o durante lo scripting.

Ordinamento delle classi

Modelli

  • Conserva i dati e lo stato principali dell'applicazione, come la health del giocatore o le ammo della pistola.
  • Serializzare, deserializzare e/o convertire tra tipi.
  • Carica/salva dati (in locale o sul web).
  • Avvisare i Titolari dello stato di avanzamento delle operazioni.
  • Memorizza lo stato del gioco per la macchina a stati finiti del gioco.
  • Non accedere mai a Visualizzazioni.

Visualizzazioni

  • Può ottenere dati dai modelli per rappresentare lo stato di gioco aggiornato per l'utente. Ad esempio, un metodo View player.Run() può utilizzare internamente model.speed per manifestare le abilità del giocatore.
  • Non dovrebbe mai mutare i modelli.
  • Implementa rigorosamente le funzionalità della sua classe. Per esempio:
    • Un PlayerView non dovrebbe implementare il rilevamento dell'input o modificare lo stato del gioco.
    • Una vista dovrebbe fungere da scatola nera con un'interfaccia e notifica di eventi importanti.
    • Non memorizza i dati principali (come velocità, salute, vite,...).

Controllori

  • Non archiviare i dati principali.
  • A volte può filtrare le notifiche da visualizzazioni indesiderate.
  • Aggiorna e utilizza i dati del Modello.
  • Gestisce il flusso di lavoro della scena di Unity.

Gerarchia di classe

In questo caso, non ci sono molti passaggi da seguire. Di solito, percepisco che alcune classi devono essere suddivise quando le variabili iniziano a mostrare troppi "prefissi" o iniziano ad apparire troppe varianti dello stesso elemento (come le classi dei Player in un MMO o i tipi di Gun in un FPS).

Ad esempio, un singolo Model contenente i dati del giocatore avrebbe molti playerDataA, playerDataB, playerDataA, playerDataB,... o un Controller che gestisce le notifiche del giocatore avrebbe OnPlayerDidA,OnPlayerDidB,... . Vogliamo ridurre la dimensione dello script e sbarazzarci dei prefissi player e OnPlayer .

Consentitemi di dimostrare l'utilizzo di una classe Model perché è più semplice da comprendere utilizzando solo i dati.

Durante la programmazione, di solito inizio con una singola classe Model che contiene tutti i dati per il gioco.

 // Model.cs class Model { public float playerHealth; public int playerLives; public GameObject playerGunPrefabA; public int playerGunAmmoA; public GameObject playerGunPrefabB; public int playerGunAmmoB; // Ops Gun[CDE ...] will appear... /* ... */ public float gameSpeed; public int gameLevel; }

È facile vedere che più complesso è il gioco, più numerose saranno le variabili. Con una complessità sufficiente, potremmo finire con una classe gigante contenente variabili model.playerABCDFoo . Gli elementi di nidificazione semplificheranno il completamento del codice e daranno anche spazio per passare da una variazione di dati all'altra.

 // Model.cs class Model { public PlayerModel player; // Container of the Player data. public GameModel game; // Container of the Game data. }
 // GameModel.cs class GameModel { public float speed; // Game running speed (influencing the difficulty) public int level; // Current game level/stage loaded }
 // PlayerModel.cs class PlayerModel { public float health; // Player health from 0.0 to 1.0. public int lives; // Player “retry” count after he dies. public GunModel[] guns; // Now a Player can have an array of guns to switch ingame. }
 // GunModel.cs class GunModel { public GunType type; // Enumeration of Gun types. public GameObject prefab; // Template of the 3D Asset of the weapon. public int ammo; // Current number of bullets public int clips; // Number of reloads possible }

Con questa configurazione di classi, gli sviluppatori possono navigare intuitivamente nel codice sorgente un concetto alla volta. Ipotizziamo uno sparatutto in prima persona, dove le armi e le loro configurazioni possono diventare davvero numerose. Il fatto che GunModel sia contenuto in una classe consente la creazione di un elenco di Prefab ( Prefabs preconfigurati da duplicare rapidamente e riutilizzare in-game) per ciascuna categoria e archiviati per un uso successivo.

Al contrario, se le informazioni sulla pistola fossero state memorizzate tutte insieme nella singola classe GunModel , in variabili come gun0Ammo , gun1Ammo , gun0Clips e così via, l'utente, di fronte alla necessità di memorizzare i dati Gun , avrebbe bisogno di memorizzare l'intero Model che include i dati del Player indesiderati. In questo caso, sarebbe ovvio che una nuova classe GunModel sarebbe migliore.

IMMAGINE: GERARCHIA DI CLASSE
Migliorare la gerarchia delle classi.

Come per ogni cosa, ci sono due facce della medaglia. A volte si può sovra-compartimentalizzare inutilmente e aumentare la complessità del codice. Solo l'esperienza può affinare le tue abilità abbastanza per trovare il miglior ordinamento MVC per il tuo progetto.

Nuova abilità speciale per sviluppatori di giochi sbloccata: giochi Unity con lo schema MVC.
Twitta

Conclusione

Ci sono tonnellate di modelli di software là fuori. In questo post, ho cercato di mostrare quello che mi ha aiutato di più nei progetti passati. Gli sviluppatori dovrebbero sempre assorbire nuove conoscenze, ma anche metterle sempre in discussione. Spero che questo tutorial ti aiuti a imparare qualcosa di nuovo e, allo stesso tempo, serva da trampolino di lancio mentre sviluppi il tuo stile.

Inoltre, ti incoraggio davvero a ricercare altri modelli e trovare quello più adatto a te. Un buon punto di partenza è questo articolo di Wikipedia, con il suo eccellente elenco di modelli e le loro caratteristiche.

Se ti piace il pattern AMVCC e desideri testarlo, non dimenticare di provare la mia libreria, Unity MVC , che contiene tutte le classi principali necessarie per avviare un'applicazione AMVCC.


Ulteriori letture sul blog di Toptal Engineering:

  • Sviluppo di Unity AI: un tutorial sulla macchina a stati finiti