Burst Compiler e Job in Unity

banner

Quando si sviluppa un videogioco, è importante ottimizzare al massimo le performance, in particolare tutte quelle meccaniche di gioco che sono computazionalmente intensive. Immagina di dover sviluppare un gioco 3D dove ci sono migliaia di nemici, detti boids, che continuamente devono aggiornare la propria posizione e velocità, basandosi sia sulla posizione in quell’istante del player, ma anche rispetto agli altri boids per evitare le collisioni.

Figura 1: esempio di agenti (boids) che si muovono liberamente nel loro spazio, evitandosi l’uno con l’altro, mentre seguono lo stormo.

Ecco, un ottima strategia è quella di unire Burst Compiler, che consente di migliorare significativamente le performance del codice, e Job di Unity per sfruttare il multi-threading1.

Figura 2: esempio di multithreading

Cos’è Burst Compiler?

Burst Compiler è un compilatore Just-In-Time (JIT), ovvero compila il codice durante l’esecuzione del programma, anziché prima dell’esecuzione (come avviene nella compilazione tradizionale). Questa tecnica consente di adattare il codice alle specifiche condizioni di runtime, migliorando l’efficienza e le prestazioni complessive dell’applicazione.

Come funziona?

Burst funziona in combinazione con il sistema di Job di Unity, che permette di eseguire operazioni in parallelo sfruttando il multi-threading. Quando si scrive un Job in Unity, Burst può essere utilizzato per compilarlo, ottenendo così performance significativamente migliori.

Esempio di codice

  1. Assicurati di avere installato Burst Compiler e Job dal Package Manager di Unity.

  2. Inizializzazione degli Array:

    • creiamo un array nativo di interi con una serie di valori. L’allocazione TempJob indica che l’array sarà utilizzato temporaneamente per un job e sarà automaticamente deallocato una volta completato. Infine, creiamo un altro array nativo per memorizzare le somme parziali. La lunghezza di questo array è la stessa di numbers.
            NativeArray<int> numbers = new NativeArray<int>(new int[] { 1, 2, 3, 4, 5 }, Allocator.TempJob);
            NativeArray<int> partialSums = new NativeArray<int>(numbers.Length, Allocator.TempJob);
  1. Definiamo il Job:

    • la struttura SumJob implementa l’interfaccia IJobParallelFor, che richiede l’implementazione del metodo Execute. Questo metodo è chiamato in parallelo per ogni indice dell’array. Ogni chiamata al metodo Execute copia il valore corrispondente da numbers a partialSums.
            [BurstCompile]
            struct SumJob : IJobParallelFor
            {
                [ReadOnly] public NativeArray<int> numbers;
                public NativeArray<int> partialSums;

                public void Execute(int index)
                {
                    partialSums[index] = numbers[index];
                }
            }
  1. Schedulazione ed Esecuzione del Job: Creiamo un’istanza di SumJob e inizializziamo i suoi campi. Scheduliamo il job per eseguire in parallelo specificando il numero di iterazioni e il batch size. E attendiamo la fine dell’operazione con Complete.
            SumJob sumJob = new SumJob
            {
                numbers = numbers,
                partialSums = partialSums
            };

            JobHandle handle = sumJob.Schedule(numbers.Length, 1);
            handle.Complete();
  1. Calcolo del Risultato e Pulizia:

    • al completamento del Job, sommiamo i valori di partialSums per ottenere la somma totale; infine liberiamo la memoria utilizzata per allocare le risorse;

    • utilizzare IJobParallelFor consente di sfruttare il multi-threading, migliorando ulteriormente le performance per operazioni che possono essere parallelizzate. In questo esempio, ogni elemento dell’array viene processato indipendentemente, permettendo un’esecuzione parallela efficiente.

            int totalSum = 0;
            for (int i = 0; i < partialSums.Length; i++)
            {
                totalSum += partialSums[i];
            }

            Debug.Log("Sum: " + totalSum);

            numbers.Dispose();
            partialSums.Dispose();

Risultato finale:

            using Unity.Burst;
            using Unity.Collections;
            using Unity.Jobs;
            using UnityEngine;

            public class BurstExample : MonoBehaviour
            {
                [BurstCompile]
                struct SumJob : IJobParallelFor
                {
                    [ReadOnly] public NativeArray<int> numbers;
                    public NativeArray<int> partialSums;

                    public void Execute(int index)
                    {
                        partialSums[index] = numbers[index];
                    }
                }

                void Start()
                {
                    int length = 5;
                    NativeArray<int> numbers = new NativeArray<int>(new int[] { 1, 2, 3, 4, 5 }, Allocator.TempJob);
                    NativeArray<int> partialSums = new NativeArray<int>(length, Allocator.TempJob);

                    SumJob sumJob = new SumJob
                    {
                        numbers = numbers,
                        partialSums = partialSums
                    };

                    JobHandle handle = sumJob.Schedule(numbers.Length, 1);
                    handle.Complete();

                    int totalSum = 0;
                    for (int i = 0; i < partialSums.Length; i++)
                    {
                        totalSum += partialSums[i];
                    }

                    Debug.Log("Sum: " + totalSum);

                    numbers.Dispose();
                    partialSums.Dispose();
                }
            }

In conclusione:

l’utilizzo di Burst Compiler in combinazione con il sistema di job di Unity può portare a notevoli miglioramenti nelle performance del vostro gioco, specialmente quando numerose operazioni richiedono un alto consumo di risorse computazionali. Con una semplice configurazione e poche modifiche al codice, è possibile ottenere benefici significativi in termini di velocità di esecuzione e efficienza. C’è da dire che però Burst funziona solo con valori nativi e non è possibile chiamare metodi da altre classi. Per qualsiasi dubbio o curiosità rimandiamo alla documentazione di Unity.

Bibliografia:


https://docs.unity3d.com/Packages/com.unity.burst@0.2/manual/index.html

https://docs.unity3d.com/Manual/JobSystemOverview.html

https://www.undefinedgames.org/2019/10/22/unity-c-job-system-and-burst-compiler-dots-introduction/

https://itk.org/Doxygen50/html/ThreadingPage.html

https://github.com/topics/boids-algorithm



  1. è una tecnica di programmazione che permette a un programma di fare più cose allo stesso tempo, suddividendo le attività in thread che possono essere eseguiti simultaneamente. ↩︎

Post correlati

#TheRedComics

Giugno

A cura di Sophie Aiello, copy di Chiara Romano

La dura vita di una madre tech - Meme

TheRedCode Digest

La tecnologia corre, e tu devi correre più veloce per rimanere sempre sul pezzo! 🚀

Riceverai una volta al mese (o anche meno) con codici sconto per partecipare agli eventi del settore, quiz per vincere dei gadget e i recap degli articoli più interessanti pubblicati sul blog

Ci sto!

Partners

Community, aziende e persone che supportano attivamente il blog

Logo di Codemotion
Logo di GrUSP
Logo di Python Milano
Logo di Schrodinger Hat
Logo di Python Biella Group
Logo di Fuzzy Brains
Logo di Django Girls
Logo di Improove
Logo del libro open source
Logo di NgRome
Logo de La Locanda del Tech

Vuoi diventare #tech content writer? 🖊️

Se vuoi raccontare la tua sul mondo #tech con dei post a tema o vuoi condividere la tua esperienza con la community, sei nel posto giusto! 😉

Manda una mail a collaborazioni[at]theredcode.it con la tua proposta e diventa la prossima penna del blog!

Ma sì, facciamolo!