Il modo migliore per strutturare / gestire centinaia di personaggi "in-game"


10

Ho realizzato un semplice gioco RTS, che contiene centinaia di personaggi come Crusader Kings 2, in Unity. Per memorizzarli l'opzione più semplice sarebbe quella di utilizzare oggetti scriptabili, ma questa non è una buona soluzione in quanto non è possibile crearne di nuovi in ​​fase di esecuzione.

Quindi ho creato una classe C # chiamata "Character" che contiene tutti i dati. Tutto funziona bene, ma mentre il gioco simula crea costantemente nuovi personaggi e uccide alcuni personaggi (man mano che si verificano eventi in-game). Mentre il gioco simula continuamente, crea migliaia di personaggi. Ho aggiunto un semplice controllo per assicurarmi che un personaggio sia "Vivo" durante l'elaborazione della sua funzione. Quindi aiuta le prestazioni, ma non riesco a rimuovere "Personaggio" se è morto, perché ho bisogno delle sue informazioni durante la creazione dell'albero genealogico.

Un elenco è il modo migliore per salvare i dati per il mio gioco? O darà problemi una volta creati 10000 personaggi? Una possibile soluzione è quella di creare un altro elenco quando l'elenco raggiunge un certo importo e spostare tutti i caratteri morti in essi contenuti.


9
Una cosa che ho fatto in passato quando ho bisogno di un sottoinsieme dei dati di un personaggio dopo la sua scomparsa è quella di creare un oggetto "lapide" per quel personaggio. La pietra tombale può trasportare le informazioni di cui ho bisogno per cercare più tardi, ma può essere più piccola e ripetuta meno spesso perché non ha bisogno di una simulazione costante come un personaggio vivente.
DMGregory


2
Il gioco è come CK2 o è solo la parte di avere molti personaggi? Ho capito che l'intero gioco era come CK2. In questo caso molte delle risposte qui non sono errate e contengono un buon know-how, ma mancano il punto della domanda. Non aiuta il fatto che tu abbia chiamato CK2 un gioco di strategia in tempo reale quando in realtà è un grande gioco di strategia . Potrebbe sembrare pignolo, ma è molto rilevante per i problemi che stai affrontando.
Raphael Schmitz,

1
Ad esempio, quando si parla di "1000 di personaggi", le persone pensano contemporaneamente a migliaia di modelli 3D o sprite sullo schermo - quindi, in Unity, 1000 di GameObjects. In CK2, la quantità massima di personaggi che ho visto contemporaneamente è stata quando ho guardato il mio campo e ho visto lì 10-15 persone (non ho giocato molto lontano). Allo stesso modo, un esercito con 3000 soldati è solo uno GameObject, con il numero "3000".
Raphael Schmitz,

1
@ R.Schmitz Sì, avrei dovuto chiarire la parte in cui ogni personaggio non ha oggetti di gioco collegati. Quando necessario, come spostare il personaggio da un punto all'altro. Viene creata un'entità separata che contiene tutte le informazioni di quel personaggio con la logica Ai.
paul p

Risposte:


24

Ci sono tre cose che dovresti considerare:

  1. In realtà provoca un problema di prestazioni? 1000s è, beh, non molti in realtà. I computer moderni sono incredibilmente veloci e possono gestire molte cose. Monitora quanto tempo impiega l'elaborazione del personaggio e vedi se effettivamente causerà un problema prima di preoccuparti troppo.

  2. Fedeltà dei personaggi attualmente minimamente attivi. Un errore frequente dei programmatori di giochi per principianti è l'ossessione di aggiornare con precisione i personaggi fuori dallo schermo allo stesso modo di quelli sullo schermo. Questo è un errore, a nessuno importa. Invece devi cercare di creare l' impressione che i personaggi fuori schermo stiano ancora recitando. Riducendo la quantità di caratteri di aggiornamento che vengono ricevuti fuori dallo schermo, è possibile ridurre drasticamente i tempi di elaborazione.

  3. Prendi in considerazione la progettazione orientata ai dati. Invece di disporre di 1000 oggetti carattere e chiamare la stessa funzione per ciascuno, disporre di una matrice di dati per i 1000 caratteri e disporre di un ciclo di funzione sui 1000 caratteri che si aggiornano a turno. Questo tipo di ottimizzazione può migliorare notevolmente le prestazioni.


3
Entity / Component / System funziona bene per questo. Costruisci ogni "sistema" per quello che ti serve, fai in modo che mantenga le migliaia o le decine di migliaia di caratteri (i loro componenti) e fornisci l '"ID carattere" al sistema. Ciò consente di mantenere i diversi modelli di dati separati e più piccoli, nonché di rimuovere i caratteri morti dai sistemi che non ne hanno bisogno. (Puoi anche scaricare completamente un sistema, se non lo stai utilizzando al momento.)
Der Kommissar,

1
Riducendo la quantità di caratteri di aggiornamento che vengono ricevuti fuori dallo schermo, è possibile aumentare notevolmente i tempi di elaborazione. Non intendi diminuire?
Tejas Kale,

1
@TejasKale: Sì, corretto.
Jack Aidley,

2
Mille personaggi non sono molti, ma quando ognuno di essi verifica costantemente se riescono a castrare ciascuno degli altri , inizia ad avere un impatto notevole sulle prestazioni complessive ...
curiousdannii,

1
Sempre meglio controllare effettivamente, ma è generalmente un presupposto di lavoro sicuro che i romani vorranno castrarsi a vicenda;)
curiousdannii

11

In questa situazione, suggerirei di usare Composizione :

Il principio secondo cui le classi dovrebbero raggiungere il comportamento polimorfico e il riutilizzo del codice in base alla loro composizione (contenendo istanze di altre classi che implementano la funzionalità desiderata)


In questo caso, sembra che la tua Characterclasse sia diventata divina e contenga tutti i dettagli su come opera un personaggio in tutte le fasi del suo ciclo di vita.

Ad esempio, noti che i caratteri morti sono ancora richiesti, poiché vengono utilizzati negli alberi genealogici. Tuttavia, è improbabile che tutte le informazioni e le funzionalità dei tuoi personaggi vivi siano ancora necessarie solo per visualizzarli in un albero genealogico. Ad esempio, potrebbero semplicemente avere bisogno di nomi, data di nascita e icona di un ritratto.


La soluzione, è quella di dividere le parti separate del tuo Characterin sottoclassi, di cui Characterpossiede un'istanza. Per esempio:

  • CharacterInfo può essere una semplice struttura di dati con nome, data di nascita, data di morte e fazione,

  • Equipmentpuò avere tutti gli oggetti del tuo personaggio o le sue risorse correnti. Potrebbe anche avere la logica che li gestisce nelle funzioni.

  • CharacterAIo CharacterControllerpuò avere tutte le informazioni necessarie sull'obiettivo attuale del personaggio, le sue funzioni di utilità ecc. E può anche avere la logica di aggiornamento che coordina il processo decisionale / interazione tra le sue singole parti.

Dopo aver diviso il personaggio, non è più necessario controllare un flag Alive / Dead nel ciclo di aggiornamento.

Invece, devi semplicemente fare uno AliveCharacterObjectche ha il CharacterController, CharacterEquipmente CharacterInfogli script attaccato. Per "uccidere" il personaggio, è sufficiente rimuovere le parti che non sono più rilevanti (come il CharacterController) - ora non sprecherà memoria o tempo di elaborazione.

Nota come CharacterInfoprobabilmente sono gli unici dati effettivamente necessari per l'albero genealogico. Decomponendo le tue classi in piccole funzionalità: puoi più facilmente conservare quel piccolo oggetto di dati dopo la morte, senza bisogno di mantenere l'intero carattere guidato dall'intelligenza artificiale.


Vale la pena ricordare che questo paradigma è quello che Unity è stato creato per l'uso - ed è il motivo per cui gestisce le cose con molti script separati. Costruire grandi oggetti-dio è raramente il modo migliore per gestire i tuoi dati in Unity.


8

Quando hai una grande quantità di dati da gestire e non tutti i punti dati sono rappresentati da un vero oggetto di gioco, di solito non è una cattiva idea rinunciare a classi specifiche di Unity e andare semplicemente con semplici vecchi oggetti C #. In questo modo minimizzi le spese generali. Quindi sembra che tu sia sulla strada giusta qui.

Memorizzare tutti i caratteri, vivi o morti, in una Lista ( o matrice ) può essere utile perché l'indice in quella lista può fungere da ID carattere canonico. L'accesso a una posizione dell'elenco per indice è un'operazione molto veloce. Ma potrebbe essere utile tenere un elenco separato degli ID di tutti i personaggi viventi, perché probabilmente dovrai iterarli molto più spesso di quanto avrai bisogno dei personaggi morti.

Man mano che l'implementazione della meccanica di gioco progredisce, potresti anche voler esaminare quale altro tipo di ricerche esegui di più. Come "tutti i personaggi viventi in un luogo specifico" o "tutti gli antenati viventi o morti di un personaggio specifico". Potrebbe essere utile creare alcune strutture dati secondarie ottimizzate per questo tipo di query. Ricorda solo che ognuno di essi deve essere aggiornato. Ciò richiede una programmazione aggiuntiva e sarà una fonte di bug aggiuntivi. Quindi fallo solo se prevedi un notevole aumento delle prestazioni.

CKII " prugne " personaggi dal suo database quando li ritiene come irrilevante per risparmiare risorse. Se la tua pila di personaggi morti consuma troppe risorse in un gioco di lunga data, allora potresti voler fare qualcosa di simile (non voglio chiamare questa "raccolta dei rifiuti". Forse "rispettoso incrematore"?).

Se in realtà hai un oggetto di gioco per ogni personaggio del gioco, il nuovo sistema Unity ECS e Jobs potrebbe esserti utile. È ottimizzato per gestire un gran numero di oggetti di gioco molto simili in modo performante. Ma forza la tua architettura software in alcuni schemi molto rigidi.

A proposito, mi piace molto CKII e il modo in cui simula un mondo con migliaia di personaggi unici controllati dall'IA, quindi non vedo l'ora di interpretare il tuo modo di interpretare il genere.


Ciao, grazie per la risposta. Tutti i calcoli sono effettuati da un singolo GameObject Manager. Sto assegnando oggetti di gioco solo a singoli attori quando necessario (come mostrare che le mosse di Army of Character si spostano da una posizione all'altra).
paul p

1
Like "all living characters in a specific location" or "all living or dead ancestors of a specific character". It might be beneficial to create some more secondary data-structures optimized for these kinds of queries.Dalla mia esperienza con il modding CK2, questo è vicino al modo in cui CK2 gestisce i dati. CK2 sembra utilizzare indici che sono essenzialmente indici di database di base, che rendono più veloce la ricerca di caratteri per una situazione specifica. Invece di avere un elenco di caratteri, sembra avere un database interno di caratteri, con tutti gli svantaggi e i vantaggi che ciò comporta.
Morfildur,

1

Non è necessario simulare / aggiornare migliaia di personaggi quando solo pochi sono vicini al giocatore. Devi solo aggiornare ciò che il giocatore può effettivamente vedere nel momento attuale, quindi i personaggi che sono più lontani dal giocatore dovrebbero essere sospesi fino a quando il giocatore non è più vicino a loro.

Se questo non funziona perché le tue meccaniche di gioco richiedono personaggi distanti per mostrare il passare del tempo, puoi aggiornarli in un "grande" aggiornamento quando il giocatore si avvicina. Se le tue meccaniche di gioco richiedono che ciascun personaggio risponda effettivamente agli eventi di gioco mentre si verificano, indipendentemente da dove si trovi il personaggio in relazione al giocatore o all'evento, allora potrebbe funzionare per ridurre la frequenza con cui i personaggi più lontani dal giocatore sono aggiornato (cioè sono ancora aggiornati in sincronia con il resto del gioco, ma non così spesso, quindi ci sarà un leggero ritardo prima che i personaggi distanti rispondano a un evento, ma è improbabile che ciò causi un problema o venga notato dal giocatore ). In alternativa, potresti voler utilizzare un approccio ibrido,


è un RTS. Dovremmo supporre che in qualsiasi momento, un numero considerevole di unità sia effettivamente sullo schermo.
Tom,

In un RTS il mondo deve continuare mentre il giocatore non sta guardando. Un grande aggiornamento richiederebbe altrettanto tempo, ma potrebbe esplodere quando si sposta la fotocamera.
PStag,

1

Chiarimento della domanda

Ho realizzato un semplice gioco RTS, che contiene centinaia di personaggi come Crusader Kings 2 in un Unity.

In questa risposta, suppongo che l'intero gioco dovrebbe essere come CK2, anziché solo la parte sull'avere molti personaggi. Tutto ciò che vedi sullo schermo in CK2 è facile da fare e non metterà in pericolo le tue prestazioni né sarà complicato da implementare in Unity. I dati che stanno dietro, è lì che diventano complessi.

CharacterClassi senza funzionalità

Quindi ho creato una classe C # chiamata "Character" che contiene tutti i dati.

Bene, perché un personaggio nel tuo gioco è solo un dato. Quello che vedi sullo schermo è solo una rappresentazione di quei dati. Queste Characterclassi sono il vero cuore del gioco e come tali sono in pericolo di diventare " oggetti divini ". Quindi consiglierei misure estreme contro questo: rimuovere tutte le funzionalità da quelle classi. Un metodo GetFullName()che combina il nome e il cognome, OK, ma nessun codice che in realtà "fa qualcosa". Inserisci quel codice in classi dedicate che eseguono un'azione; ad esempio, una classe Birthercon un metodo Character CreateCharacter(Character father, Character mother)risulterà molto più pulita rispetto a quella funzionalità nella Characterclasse.

Non archiviare i dati nel codice

Per memorizzarli l'opzione più semplice sarebbe quella di usare oggetti scriptabili

No. Memorizzali nel formato JSON, utilizzando JsonUtility di Unity. Con quelle Characterclassi senza funzionalità , dovrebbe essere banale da fare. Funzionerà per la configurazione iniziale del gioco e per salvarlo in savegame. Tuttavia, è ancora una cosa noiosa da fare, quindi ho appena dato l'opzione più semplice nella tua situazione. Puoi anche usare XML o YAML o qualsiasi formato, a patto che possa ancora essere letto dagli umani quando è archiviato in un file di testo. CK2 fa lo stesso, in realtà molti giochi. È anche un'eccellente configurazione per consentire alle persone di modificare il tuo gioco, ma questo è un pensiero per molto più tardi.

Pensa astratto

Ho aggiunto un semplice controllo per assicurarmi che il personaggio sia "Vivo" durante l'elaborazione [...] ma non posso rimuovere "Carattere" se è morto perché ho bisogno delle sue informazioni durante la creazione dell'albero genealogico.

Questo è più facile a dirsi che a farsi, perché spesso si scontra con il modo naturale di pensare. Stai pensando in modo "naturale", a un "personaggio". Tuttavia, in termini di gioco, sembra che ci siano almeno 2 diversi tipi di dati che "è un personaggio": lo chiamerò ActingCharactere FamilyTreeEntry. I personaggi morti FamilyTreeEntrynon devono essere aggiornati e probabilmente hanno bisogno di molti meno dati di quelli attivi ActingCharacter.


0

Parlerò da un po 'di esperienza, passando da una progettazione OO rigida a una progettazione Entity-Component-System (ECS).

Qualche tempo fa ero proprio come te , avevo un sacco di diversi tipi di cose che avevano proprietà simili e ho costruito vari oggetti e ho provato a usare l'eredità per risolverlo. Una persona molto intelligente mi ha detto di non farlo, e invece usa Entity-Component-System.

Ora, ECS è un grande concetto ed è difficile da ottenere. C'è molto lavoro da fare per costruire correttamente entità, componenti e sistemi. Prima di poterlo fare, però, dobbiamo definire i termini.

  1. Entità : questa è la cosa , il giocatore, l'animale, l'NPC, qualunque cosa . È una cosa che necessita di componenti collegati ad esso.
  2. Componente : questo è l' attributo o la proprietà , come un "Nome" o "Età", o "Genitori", nel tuo caso.
  3. Sistema : questa è la logica dietro un componente o un comportamento . In genere, si crea un sistema per componente, ma ciò non è sempre possibile. Inoltre, a volte i sistemi devono influenzare altri sistemi.

Quindi, ecco dove andrei con questo:

Innanzitutto, creane uno IDper i tuoi personaggi. An int, Guidqualunque cosa tu voglia. Questa è "Entità".

In secondo luogo, inizia a pensare ai diversi comportamenti che stai vivendo. Cose come "Family Tree" - questo è un comportamento. Invece di modellarlo come attributi sull'entità, costruisci un sistema che contiene tutte quelle informazioni . Il sistema può quindi decidere cosa farne.

Allo stesso modo, vogliamo costruire un sistema per "Il personaggio è vivo o morto?" Questo è uno dei sistemi più importanti nel tuo progetto, perché influenza tutti gli altri. Alcuni sistemi possono eliminare i caratteri "morti" (come il sistema "sprite"), altri sistemi possono riorganizzare internamente le cose per supportare meglio il nuovo stato.

Costruirai un sistema "Sprite" o "Disegno" o "Rendering", per esempio. Questo sistema avrà la responsabilità di determinare con quale personaggio deve essere visualizzato lo sprite e come visualizzarlo. Quindi, quando un personaggio muore, rimuovili.

Inoltre, un sistema "AI" in grado di dire a un personaggio cosa fare, dove andare, ecc. Questo dovrebbe interagire con molti altri sistemi e prendere decisioni basate su di essi. Ancora una volta, i personaggi morti possono probabilmente essere rimossi da questo sistema, dal momento che non stanno più facendo nulla.

Il tuo sistema "Nome" e il sistema "Albero familiare" dovrebbero probabilmente mantenere il personaggio (vivo o morto) in memoria. Questo sistema deve richiamare quelle informazioni, indipendentemente dallo stato del personaggio. (Jim è ancora Jim, anche dopo che lo seppelliamo.)

Questo ti dà anche il vantaggio di cambiare quando un sistema reagisce in modo più efficiente: il sistema ha il proprio timer. Alcuni sistemi devono sparare rapidamente, altri no. È qui che iniziamo a capire cosa fa funzionare un gioco in modo efficiente. Non abbiamo bisogno di ricalcolare il tempo ogni millisecondo, probabilmente possiamo farlo ogni 5 o giù di lì.

Ti offre anche una leva più creativa: puoi costruire un sistema "Pathfinder" in grado di gestire il calcolo di un percorso da A a B e aggiornarlo quando necessario, consentendo al sistema di movimento di dire "dove devo vai dopo? " Ora possiamo separare completamente queste preoccupazioni e ragionare su di esse in modo più efficace. Il movimento non ha bisogno di trovare la strada, deve solo arrivarci.

Ti consigliamo di esporre alcune parti di un sistema verso l'esterno. Nel tuo Pathfindersistema probabilmente vorrai un Vector2 NextPosition(int entity). In questo modo, è possibile mantenere tali elementi in matrici o elenchi strettamente controllati. Puoi utilizzare structtipi più piccoli, che possono aiutarti a mantenere i componenti in blocchi di memoria più piccoli e contigui, che possono rendere gli aggiornamenti di sistema molto più veloci. (Soprattutto se le influenze esterne a un sistema sono minime, ora deve solo preoccuparsi del suo stato interno, come Name.)

Ma, e non posso sottolinearlo abbastanza, ora Entityè solo un ID, compresi riquadri, oggetti, ecc. Se un'entità non appartiene a un sistema, il sistema non lo rintraccerà. Ciò significa che possiamo creare i nostri oggetti "Albero", archiviarli nei sistemi Spritee Movement(gli alberi non si muoveranno, ma hanno un componente "Posizione") e tenerli fuori dagli altri sistemi. Non abbiamo più bisogno di un elenco speciale per gli alberi, poiché il rendering di un albero non è diverso da un personaggio, a parte il paperdolling. (Che il Spritesistema può controllare o che il Paperdollsistema può controllare.) Ora il nostro NextPositionpuò essere leggermente riscritto: Vector2? NextPosition(int entity)e può restituire una nullposizione per le entità a cui non importa. Lo applichiamo anche al nostro NameSystem.GetName(int entity), ritorna nullper alberi e rocce.


Lo porterò alla fine, ma l'idea qui è di darti un po 'di background su ECS e come puoi davvero sfruttarlo per darti un design migliore sul tuo gioco. È possibile aumentare le prestazioni, disaccoppiare elementi non correlati e mantenere le cose in modo più organizzato. (Questo si abbina bene anche con linguaggi / configurazioni funzionali, come F # e LINQ, che consiglio vivamente di controllare F # se non l'hai già fatto, si abbina molto bene con C # quando li usi insieme.)


Ciao, grazie per una risposta così dettagliata. Sto usando un solo GameObject Manager che contiene il riferimento a tutti gli altri personaggi del gioco.
paul p

Lo sviluppo in Unity ruota attorno a entità chiamate GameObjectche non fanno molto ma hanno un elenco di Componentclassi che svolgono il vero lavoro. Un decennio fa c'è stato un cambio di paradigma riguardo alla rotonda degli ECS , poiché mettere il codice di recitazione in classi di sistema separate è più pulito. Recentemente anche Unity ha implementato un tale sistema, ma il suo GameObjectsistema è ed è sempre stato un ECS. OP sta già utilizzando un ECS.
Raphael Schmitz,

-1

Mentre lo fai in Unity, l'approccio più semplice è questo:

  • crea un oggetto di gioco per personaggio o tipo di unità
  • salvarli come prefabbricati
  • è quindi possibile creare un'istanza di un prefabbricato quando necessario
  • quando un personaggio viene ucciso, distruggi l'oggetto di gioco e non occuperà più alcuna CPU o memoria

Nel tuo codice, puoi conservare i riferimenti agli oggetti in qualcosa come un Elenco per salvarti dall'uso di Find () e dalle sue variazioni in ogni momento. Stai scambiando cicli di CPU per la memoria, ma un elenco di puntatori è piuttosto piccolo, quindi anche con poche migliaia di oggetti che non dovrebbero essere un grosso problema.

Man mano che avanzi nel gioco, scoprirai che avere singoli oggetti di gioco ti offre molti vantaggi, tra cui la navigazione e l'intelligenza artificiale.

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.