Gestione di più riferimenti della stessa entità di gioco in luoghi diversi tramite ID


8

Ho visto grandi domande su argomenti simili, ma nessuno che ha affrontato questo particolare metodo:

Dato che ho più raccolte di entità di gioco nel mio gioco [XNA Game Studio], con molte entità appartenenti a più liste, sto valutando i modi in cui posso tenere traccia di ogni entità distrutta e rimuoverla dalle liste a cui appartiene .

Molti potenziali metodi sembrano sciatti / contorti, ma mi viene in mente un modo che ho visto prima in cui invece di avere più raccolte di entità di gioco, hai invece raccolte di ID di entità di gioco . Questi ID si associano alle entità di gioco tramite un "database" centrale (forse solo una tabella hash).

Quindi, ogni qualvolta un po 'di codice vuole accedere ai membri di un'entità di gioco, controlla innanzitutto se è ancora presente nel database. In caso contrario, può reagire di conseguenza.

È un approccio valido? Sembra che eliminerebbe molti dei rischi / fastidi della memorizzazione di più elenchi, con il compromesso che rappresenta il costo della ricerca ogni volta che si desidera accedere a un oggetto.

Risposte:


3

Risposta breve: si.

Risposta lunga: In realtà, tutti i giochi che ho visto hanno funzionato in questo modo. La ricerca hash-table è abbastanza veloce; e se non ti interessano i valori ID reali (cioè non sono usati altrove) puoi usare un vettore invece di una tabella hash. Che è ancora più veloce.

Come bonus aggiuntivo, questa configurazione consente di riutilizzare gli oggetti di gioco, riducendo la velocità di allocazione della memoria. Quando un oggetto di gioco viene distrutto, invece di "liberarlo", devi solo ripulire i suoi campi e inserire un "pool di oggetti". Quindi, quando hai bisogno di un nuovo oggetto, prendine uno esistente dal pool. Se i nuovi oggetti vengono generati costantemente, ciò può migliorare sensibilmente le prestazioni.


In generale, è abbastanza preciso, ma dobbiamo prima prendere in considerazione altre cose. Se ci sono molti oggetti che vengono creati e distrutti rapidamente e la nostra funzione hash è buona, l'hashtable potrebbe adattarsi meglio di un vettore. In caso di dubbi, però, scegli il vettore.
hokiecsgrad,

Sì, sono d'accordo: il vettore non è assolutamente più veloce di una tabella hash. Ma è più veloce del 99% delle volte (-8
Nevermind

Se si desidera riutilizzare gli slot (che si desidera definitivamente), è necessario assicurarsi che vengano riconosciuti ID non validi. ex. L'oggetto all'ID 7 è nell'elenco A, B e C. L'oggetto viene distrutto e nello stesso slot viene creato un altro oggetto. Questo oggetto si registra negli elenchi A e C. La voce del primo oggetto nell'elenco B ora "punta" all'oggetto appena creato, che in questo caso è errato. Questo può essere risolto perfettamente con il Gestore di Manica di Scott Bilas scottbilas.com/publications/gem-resmgr . L'ho già usato nei giochi commerciali e funziona come un fascino.
DarthCoder il

3

È un approccio valido? Sembra che eliminerebbe molti dei rischi / fastidi della memorizzazione di più elenchi, con il compromesso che rappresenta il costo della ricerca ogni volta che si desidera accedere a un oggetto.

Tutto quello che hai fatto è spostare l'onere di eseguire il controllo dal momento della distruzione al momento dell'uso. Non direi che non l'ho mai fatto prima, ma non lo considero "sano".

Suggerisco di utilizzare una delle numerose alternative più solide. Se è noto il numero di elenchi, è possibile cavarsela semplicemente rimuovendo l'oggetto da tutti gli elenchi. Ciò è innocuo se l'oggetto non è in un determinato elenco e si garantisce che sia stato rimosso correttamente.

Se il numero di elenchi è sconosciuto o poco pratico, archiviare i riferimenti all'indietro sull'oggetto. L'implementazione tipica di questo è il modello di osservatore , ma probabilmente è possibile ottenere un effetto simile con i delegati, che richiamano per rimuovere l'elemento da qualsiasi elenco contenente quando necessario.

O a volte non hai davvero bisogno di più liste. Spesso è possibile mantenere un oggetto in un solo elenco e utilizzare query dinamiche per generare altre raccolte transitorie quando necessario. per esempio. Invece di avere un elenco separato per l'inventario di un personaggio, puoi semplicemente estrarre tutti gli oggetti in cui "trasportato" equivale al personaggio corrente. Questo potrebbe sembrare abbastanza inefficiente, ma è abbastanza buono per i database relazionali e può essere ottimizzato mediante indicizzazione intelligente.


Sì, nella mia attuale implementazione, ogni elenco o singolo oggetto che desidera un riferimento a un'entità di gioco deve iscriversi al suo evento distrutto e reagire di conseguenza. Questo potrebbe essere altrettanto buono o migliore in qualche modo rispetto alla ricerca ID.
vargonian

1
Nel mio libro, questo è esattamente l'opposto di una soluzione solida. Rispetto alla ricerca per ID, hai PIÙ codice, con PIÙ spazi per i bug, PIÙ opportunità di dimenticare qualcosa e la possibilità di distruggere l'oggetto SENZA informare tutti per l'avvio. Con la ricerca per ID, hai un po 'di codice che non cambia quasi mai, un singolo punto di distruzione e una garanzia del 100% che non avrai mai accesso a un oggetto distrutto.
Nevermind,

1
Con la ricerca per ID, hai un codice extra in ogni singolo posto di cui hai bisogno per usare l'oggetto. Con un approccio basato sull'osservatore non hai un codice aggiuntivo se non quando aggiungi e rimuovi elementi dagli elenchi, che è probabilmente una manciata di posti nell'intero programma. Se fai riferimento a elementi più di quelli che crei, distruggi o spostali tra gli elenchi, l'approccio dell'osservatore produrrà meno codice.
Kylotan,

Probabilmente ci sono più posti che fanno riferimento a oggetti che luoghi che creano / distruggono quelli. Tuttavia, ogni riferimento richiede solo un po 'di codice o nessuno se la ricerca è già racchiusa dalla proprietà (che dovrebbe essere la maggior parte delle volte). Inoltre, PUOI dimenticare di iscriverti per la distruzione degli oggetti, ma NON PUOI dimenticare di cercare un oggetto.
Nevermind,

1

Questa sembra essere solo un'istanza più specifica del problema "come rimuovere oggetti da un elenco, mentre lo scorre attraverso", che è facile da risolvere (iterare in ordine inverso è probabilmente il modo più semplice per un elenco in stile array come quello di C # List).

Se un'entità verrà referenziata da più punti, dovrebbe comunque essere un tipo di riferimento . Quindi non è necessario passare attraverso un sistema di ID contorto: una classe in C # fornisce già il riferimento indiretto necessario.

E infine - perché preoccuparsi di "controllare il database"? Basta dare a ogni entità una IsDeadbandiera e verificarla.


Non conosco davvero C #, ma questa architettura è spesso usata in C e C ++ dove il normale tipo di riferimento (puntatori) del linguaggio non è sicuro da usare se l'oggetto è stato distrutto. Esistono diversi modi per gestirlo, ma comportano tutti un livello di riferimento indiretto e questa è una soluzione comune. (Un elenco di backpointer come suggerisce Kylotan è un altro - che scegli dipende se vuoi spendere memoria o tempo.)

C # è raccolta dei rifiuti, quindi non importa se si abbandonano le istanze. Per le risorse non gestite (ovvero: dal garbage collector) esiste il IDisposablemodello, che è molto simile a contrassegnare un oggetto come IsDead. Ovviamente se hai bisogno di mettere in comune le istanze, piuttosto che averle GC'd, allora è necessaria una strategia più complicata.
Andrew Russell,

La bandiera IsDead è qualcosa che ho usato con successo in passato; Mi piace solo l'idea del crash del gioco se non riesco a controllare uno stato morto piuttosto che continuare allegramente, con risultati potenzialmente bizzarri e difficili da debug. L'uso di una ricerca nel database sarebbe il primo; IsDead contrassegna quest'ultimo.
vargonian

3
@vargonian Con il modello usa e getta, ciò che accade di solito è che controlli IsDisposed nella parte superiore di ogni funzione e proprietà di una classe. Se l'istanza viene eliminata, si lancia ObjectDisposedException(che generalmente "si blocca" con un'eccezione non gestita). Spostando il controllo sull'oggetto stesso (piuttosto che controllare l'oggetto esternamente), si consente all'oggetto di definire il proprio comportamento "Sono morto" e rimuovere l'onere del controllo dai chiamanti. Per i tuoi scopi potresti implementare IDisposableo creare la tua IsDeadbandiera con funzionalità simili.
Andrew Russell,

1

Uso spesso questo approccio nel mio gioco C # /. NET. A parte gli altri vantaggi (e pericoli!) Descritti qui, può anche aiutare a evitare problemi di serializzazione.

Se si desidera sfruttare le funzionalità di serializzazione binaria integrate in .NET Framework, l'utilizzo degli ID entità può aiutare a ridurre al minimo le dimensioni del grafico a oggetti che viene scritto. Per impostazione predefinita, il formattatore binario di .NET serializzerà un intero oggetto grafico a livello di campo. Diciamo che voglio serializzare Shipun'istanza. Se Shipha un _ownercampo che fa riferimento a Playerchi lo possiede, anche Playerquell'istanza verrebbe serializzata. Se Playercontiene un _shipscampo (di, diciamo, ICollection<Ship>), anche tutte le navi del giocatore verranno scritte, insieme a qualsiasi altro oggetto a cui si fa riferimento a livello di campo (ricorsivamente). È facile serializzare accidentalmente un enorme oggetto grafico quando si desidera serializzare solo una piccola parte di esso.

Se, invece, ho un _ownerIdcampo, allora posso usare quel valore per risolvere il Playerriferimento su richiesta. La mia API pubblica può anche rimanere invariata, con la Ownerproprietà che esegue semplicemente la ricerca.

Mentre le ricerche basate sull'hash sono generalmente molto veloci, l'overhead aggiunto potrebbe diventare un problema per gruppi di entità molto grandi con ricerche frequenti. Se diventa un problema per te, puoi memorizzare nella cache il riferimento usando un campo che non viene serializzato. Ad esempio, potresti fare qualcosa del genere:

public class Ship
{
    private int _ownerId;
    [NonSerialized] private Lazy<Player> _owner;

    public Player Owner
    {
        get { return _owner.Value; }
    }

    public Ship(Player owner)
    {
        _ownerId = owner.PlayerID;
        EnsureCache();
    }

    private void EnsureCache()
    {
        if (_owner == null)
            _owner = new Lazy<Player>(() => Game.Current.Players[_ownerId]);
    }

    [OnDeserialized]
    private void OnDeserialized(StreamingContext context)
    {
        EnsureCache();
    }
}

Naturalmente, questo approccio può anche rendere più difficile la serializzazione quando si desidera effettivamente serializzare un grafico di oggetti di grandi dimensioni. In questi casi, è necessario escogitare una sorta di "contenitore" per assicurarsi che siano inclusi tutti gli oggetti necessari.


0

Un altro modo per gestire la pulizia è di invertire il controllo e utilizzare un oggetto usa e getta. Probabilmente hai un fattore di allocazione delle tue entità, quindi passa alla fabbrica tutti gli elenchi a cui dovrebbe essere aggiunto. L'entità mantiene un riferimento a tutti gli elenchi di cui fa parte e implementa IDisposable. Nel metodo dispose, si rimuove da tutte le liste. Ora devi occuparti solo della gestione delle liste in due posti: fabbrica (creazione) e smaltimento. L'eliminazione nell'oggetto è semplice come entity.Dispose ().

La questione dei nomi rispetto ai riferimenti in C # è davvero importante solo per la gestione di componenti o tipi di valore. Se si dispone di elenchi di componenti collegati a un'entità, può essere utile utilizzare un campo ID come chiave hashmap. Se hai solo elenchi di entità, usa solo elenchi con un riferimento. I riferimenti C # sono sicuri e semplici da utilizzare.

Per rimuovere o aggiungere elementi dall'elenco, è possibile utilizzare una coda o un numero qualsiasi di altre soluzioni per gestire l'aggiunta e l'eliminazione dagli elenchi.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.