Mr TOTO / blog
· Unity · Sviluppo di videogiochi · Prestazioni · 5 min di lettura

Il pattern Object Pool in un gioco mobile

Come riutilizzare gli oggetti di gioco più costosi invece di crearli e distruggerli di continuo, con un esempio pratico in Unity.

Un prototipo di gioco mobile in Unity con uccelli che attraversano lo schermo
Il prototipo generava ripetutamente uccelli su un lato dello schermo e li rimuoveva sull’altro.

Nota storica: questo esempio è stato scritto per Unity 4. Le versioni moderne di Unity hanno API differenti e includono strumenti propri per il pooling, ma la logica e i compromessi alla base del pattern rimangono validi.

L’idea dietro il pattern Object Pool è semplice. Alcuni sistemi creano e distruggono ripetutamente molti oggetti dello stesso tipo. Quando la creazione è costosa, conservare un insieme di oggetti inattivi e riutilizzarli può costare meno che allocarne di nuovi ogni volta.

Questo può essere importante sui dispositivi con risorse limitate, come telefoni e tablet, ma il pooling non è sempre una buona idea. Tenere troppi oggetti in memoria consuma risorse e aggiunge lavoro di gestione. Conviene usare il pattern soltanto dopo aver compreso il carico effettivo e misurato i risultati.

Una scatola di marionette

Immaginiamo un burattinaio con una scatola di personaggi pronti accanto al palco. Quando serve una marionetta, viene presa dalla scatola. Quando la sua scena termina, viene rimessa dentro invece di essere gettata e ricostruita per lo spettacolo successivo.

In un gioco, lo stesso approccio può funzionare per proiettili, particelle, nemici o elementi dello scenario che appaiono e scompaiono frequentemente.

Nel mio prototipo mobile in Unity, gli uccelli venivano generati sul lato destro dello schermo e rimossi dopo aver superato il bordo sinistro. Istanziare un GameObject può coinvolgere mesh, materiali, texture, componenti e diverse operazioni interne del motore. Ripetere continuamente questo lavoro può produrre picchi nel frame time e pressione sul garbage collector.

Guarda il prototipo Unity su YouTube.

Ogni uccello può invece essere disabilitato quando esce dallo schermo, restituito al pool, ripristinato e riattivato quando serve di nuovo.

Come si comporta il pool

Il componente originale esponeva tre impostazioni e operazioni principali:

  1. Un elenco di prefab che il pool sa gestire.
  2. Una dimensione iniziale del buffer per ogni tipo di prefab.
  3. Metodi per recuperare un oggetto disponibile e restituirlo quando non è più necessario.

Il codice chiamante rimane molto piccolo:

GameObject bird = pool.Get("Bird", createIfMissing: true);
bird.transform.position = spawnPosition;

// Più tardi, quando l’uccello esce dallo schermo:
pool.Release(bird);

Una versione compatta dell’idea sottostante è questa:

using System.Collections.Generic;
using UnityEngine;

public sealed class SimplePool : MonoBehaviour
{
    [SerializeField] private GameObject prefab;
    [SerializeField] private int initialSize = 3;

    private readonly Queue<GameObject> available = new();

    private void Awake()
    {
        for (int i = 0; i < initialSize; i++)
            available.Enqueue(Create());
    }

    public GameObject Get()
    {
        GameObject item = available.Count > 0
            ? available.Dequeue()
            : Create();

        item.SetActive(true);
        return item;
    }

    public void Release(GameObject item)
    {
        item.SetActive(false);
        item.transform.SetParent(transform);
        available.Enqueue(item);
    }

    private GameObject Create()
    {
        GameObject item = Instantiate(prefab, transform);
        item.SetActive(false);
        return item;
    }
}

Un pool reale ha bisogno anche di regole di reset chiare. Un oggetto riutilizzato può conservare velocità, stato dell’animazione, timer, iscrizioni a eventi, particelle o riferimenti dalla sua vita precedente. Restituirlo al pool in sicurezza significa ripristinare tutto ciò che servirà al successivo utilizzo, non soltanto renderlo inattivo.

Quando il pooling è utile

Il pooling è un’ottima soluzione quando gli oggetti vengono creati spesso, la costruzione è costosa, la loro vita è breve e il numero massimo di elementi attivi è abbastanza prevedibile. Può essere superfluo quando la creazione è rara, gli oggetti sono economici o il pool manterrebbe occupata molta memoria senza che le misurazioni mostrino un vero problema di prestazioni.

Il pattern non serve a eliminare ogni allocazione. Serve a togliere lavoro ripetuto dai momenti più sensibili e a rendere più prevedibile il ciclo di vita degli oggetti. Prima misura, poi applica il pooling ai casi davvero costosi e mantieni esplicita la logica di reset.