Come posso evitare le classi di giocatori giganti?


46

C'è quasi sempre una classe di giocatori in un gioco. Il giocatore generalmente può fare molto nel gioco, il che significa che per me questa classe finisce per essere enorme con una tonnellata di variabili per supportare ogni pezzo di funzionalità che il giocatore può fare. Ogni pezzo è abbastanza piccolo da solo, ma combinato finisco con migliaia di righe di codice e diventa difficile trovare ciò di cui hai bisogno e spaventoso per apportare modifiche. Con qualcosa che è sostanzialmente un controllo generale per l'intero gioco come evitare questo problema?


26
Più file o un file, il codice deve andare da qualche parte. I giochi sono complessi. Per trovare ciò di cui hai bisogno, scrivi buoni nomi di metodi e commenti descrittivi. Non aver paura di apportare modifiche: prova e basta. E fai il backup del tuo lavoro :)
Chris McFarland,

7
Ho capito che deve andare da qualche parte, ma la progettazione del codice è importante in termini di flessibilità e manutenzione. Avere una classe o un gruppo di codice che è migliaia di righe non mi sembra neanche.
user441521

17
@ChrisMcFarland non consiglia di eseguire il backup, suggerisce il codice versione XD.
GameDeveloper

1
@ChrisMcFarland Sono d'accordo con GameDeveloper. Avere il controllo della versione come Git, svn, TFS, ... rende lo sviluppo molto più semplice grazie alla possibilità di annullare grandi modifiche molto più facilmente e di recuperare facilmente da cose come l'eliminazione accidentale del progetto, guasti hardware o corruzione dei file.
Nzall,

3
@TylerH: non sono assolutamente d'accordo. I backup non consentono di unire insieme molte modifiche esplorative, né legano in alcun modo vicino metadati utili ai changeset, né consentono flussi di lavoro sicuri multi-sviluppatore. È possibile utilizzare il controllo versione come un potente sistema di backup temporizzato, ma manca molto del potenziale di tali strumenti.
Phoshi,

Risposte:


67

Di solito useresti un sistema di componenti di entità (un sistema di componenti di entità è un'architettura basata su componenti). Ciò semplifica anche la creazione di altre entità e può anche rendere i nemici / NPC hanno gli stessi componenti del giocatore.

Questo approccio va esattamente nella direzione opposta rispetto a un approccio orientato agli oggetti. Tutto nel gioco è un'entità. L'entità è solo un caso senza meccaniche di gioco integrate. Ha un elenco di componenti e un modo per manipolarli.

Ad esempio, il giocatore ha un componente di posizione, un componente di animazione e un componente di input e quando l'utente preme lo spazio, si desidera che il lettore salti.

Puoi ottenere questo dando all'entità giocatore un componente salto, che quando chiamato fa cambiare la componente animata all'animazione saltante e fai sì che il giocatore abbia una velocità y positiva nella componente posizione. Nel componente di input ascolti il ​​tasto spazio e chiami il componente jump. (Questo è solo un esempio, dovresti avere un componente controller per il movimento).

Questo aiuta a suddividere il codice in moduli più piccoli e riutilizzabili e può portare a un progetto più organizzato.


I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
MichaelHouse

8
Mentre comprendo i commenti in movimento che devono essere spostati, non spostare quelli che mettono in discussione l'accuratezza della risposta. Dovrebbe essere ovvio, no?
bug-a-lot

20

I giochi non sono unici in questo; le divinità sono un anti-modello ovunque.

Una soluzione comune è quella di scomporre la grande classe in un albero di classi più piccole. Se il giocatore ha un inventario, non farne parte class Player. Invece, crea un class Inventory. Questo è un membro per class Player, ma internamente class Inventorypuò racchiudere un sacco di codice.

Un altro esempio: un personaggio giocatore può avere relazioni con gli NPC, quindi potresti avere un class Relationriferimento sia Playerall'oggetto che NPCall'oggetto, ma non appartenendo a nessuno dei due.


Sì, stavo solo cercando idee su come farlo. Qual è stata la mentalità perché ci sono molte funzionalità di piccoli pezzi, quindi durante la programmazione non è naturale, per me comunque, svelare quei piccoli pezzi di funzionalità. Tuttavia, diventa ovvio che tutte quelle piccole funzionalità iniziano a rendere enorme la classe del giocatore.
user441521

1
La gente di solito dice che qualcosa è una classe divina o un oggetto divino, quando contiene e gestisce ogni altra classe / oggetto nel gioco.
Bálint,

11

1) Giocatore: State-machine + architettura basata su componenti.

Componenti usuali per Player: HealthSystem, MovementSystem, InventorySystem, ActionSystem. Quelle sono tutte classi come class HealthSystem.

Non consiglio di usarlo Update()(non ha senso nei casi normali avere un aggiornamento nel sistema sanitario a meno che non sia necessario per alcune azioni lì in ogni fotogramma, queste raramente si verificano. Un caso a cui potresti anche pensare: il giocatore viene avvelenato e hai bisogno di lui di tanto in tanto per perdere la salute - qui ti suggerisco di usare le coroutine. Un altro rigenera costantemente la salute o la potenza corrente, prendi solo la salute o la potenza attuale e chiama la coroutine per riempire a quel livello quando arriva il momento. Rompi la coroutine quando la salute è piena o è stato danneggiato o ha ricominciato a correre e così via. OK, era un po 'offtopico, ma spero sia stato utile) .

Stati: LootState, RunState, WalkState, AttackState, IDLEState.

Ogni stato eredita da interface IState. IStateha nel nostro caso ha 4 metodi solo per un esempio.Loot() Run() Walk() Attack()

Inoltre, abbiamo class InputControllerdove controlliamo ogni input dell'utente.

Ora, per un vero esempio: InputControllercontrolliamo se il giocatore preme uno dei WASD or arrowse poi se preme anche il Shift. Se ha premuto solo WASDallora chiamiamo _currentPlayerState.Walk();quando questo accade e dobbiamo currentPlayerStateessere uguali a WalkStateallora WalkState.Walk() abbiamo tutti i componenti necessari per questo stato - in questo caso MovementSystem, quindi facciamo muovere il giocatore public void Walk() { _playerMovementSystem.Walk(); }- vedi cosa abbiamo qui? Abbiamo un secondo livello di comportamento, ottimo per la manutenzione e il debug del codice.

Ora al secondo caso: cosa succede se abbiamo WASD+ Shiftpremuto? Ma il nostro stato precedente era WalkState. In questo caso Run()verrà chiamato InputController(non confondere, Run()viene chiamato perché abbiamo WASD+ Shiftcheck-in InputControllernon a causa del WalkState). Quando chiamiamo _currentPlayerState.Run();in WalkState- sappiamo che dobbiamo passare _currentPlayerStateper RunStatee lo facciamo in Run()su WalkStatee lo chiamiamo di nuovo all'interno di questo metodo, ma ora con uno stato diverso perché non vogliamo all'azione perdere questa cornice. E ora ovviamente lo chiamiamo _playerMovementSystem.Run();.

Ma cosa succede LootStatequando il giocatore non può camminare o correre fino a quando non rilascia il pulsante? Bene, in questo caso quando abbiamo iniziato il saccheggio, ad esempio quando è Estato premuto il pulsante chiamiamo, _currentPlayerState.Loot();passiamo a LootStatee ora chiamiamo il suo chiamato da lì. Lì ad esempio chiamiamo il metodo collsion per ottenere se c'è qualcosa da saccheggiare nel range. E chiamiamo coroutine dove abbiamo un'animazione o dove lo avviamo e controlliamo anche se il giocatore tiene ancora il pulsante, se non coroutine si rompe, se sì gli diamo bottino alla fine del coroutine. E se il giocatore premesse WASD? - _currentPlayerState.Walk();si chiama, ma qui è la cosa bella della macchina a stati, inLootState.Walk()abbiamo un metodo vuoto che non fa nulla o come farei come caratteristica - i giocatori dicono: "Ehi amico, non l'ho ancora saccheggiato, puoi aspettare?". Quando finisce il saccheggio, passiamo a IDLEState.

Inoltre, potresti fare un altro script chiamato class BaseState : IStateche ha implementato tutti questi metodi predefiniti, ma li ha in virtualmodo da poterli overridefare in un class LootState : BaseStatetipo di classe.


Il sistema basato sui componenti è eccezionale, l'unica cosa che mi preoccupa sono le Istanze, molte delle quali. E ci vuole più memoria e lavoro per il garbage collector. Ad esempio, se hai 1000 istanze di nemico. Tutti con 4 componenti. 4000 oggetti invece di 1000. Mb non è un grosso problema (non ho eseguito test delle prestazioni) se consideriamo tutti i componenti che possiede il gameobject.


2) Architettura basata sull'ereditarietà. Sebbene noterai che non possiamo eliminare completamente i componenti, in realtà è impossibile se vogliamo avere un codice pulito e funzionante. Inoltre, se vogliamo usare modelli di design che sono altamente raccomandati da usare nei casi corretti (non usarli troppo, si parla di sovraingegneria).

Immagina di avere una classe Player con tutte le proprietà necessarie per uscire da un gioco. Ha salute, mana o energia, può muoversi, correre e usare abilità, ha un inventario, può fabbricare oggetti, saccheggiare oggetti, persino costruire barricate o torrette.

Prima di tutto sto per dire che Inventario, Creazione, Movimento, Costruzione dovrebbe essere basato sui componenti perché non è responsabilità del giocatore avere metodi come AddItemToInventoryArray()- sebbene il giocatore possa avere un metodo come PutItemToInventory()quello chiamerà il metodo descritto in precedenza (2 livelli - possiamo aggiungere alcune condizioni a seconda dei diversi livelli).

Un altro esempio con la costruzione. Il giocatore può chiamare qualcosa del genere OpenBuildingWindow(), ma Buildingsi occuperà di tutto il resto, e quando l'utente decide di costruire un edificio specifico, passa tutte le informazioni necessarie al giocatore Build(BuildingInfo someBuildingInfo)e il giocatore inizia a costruirlo con tutte le animazioni necessarie.

SOLIDO - Principi OOP. S - responsabilità unica: ciò che abbiamo visto negli esempi precedenti. Sì ok, ma dov'è l'eredità?

Qui: la salute e le altre caratteristiche del giocatore dovrebbero essere gestite da un'altra entità? Penso di no. Non può esserci un giocatore senza salute, se ce n'è uno, non ereditiamo. Ad esempio, abbiamo IDamagable, LivingEntity, IGameActor, GameActor. IDamagablecerto che ha TakeDamage().

class LivinEntity : IDamagable {

   private float _health; // For fields that are the same between Instances I would use Flyweight Pattern.

   public void TakeDamage() {
       ....
   }
}

class GameActor : LivingEntity, IGameActor {
    // Here goes state machine and other attached components needed.
}

class Player : GameActor {
   // Inventory, Building, Crafting.... components.
}

Quindi qui non ho potuto effettivamente dividere i componenti dall'ereditarietà, ma possiamo mescolarli come vedi. Possiamo anche creare alcune classi di base per il sistema Building, ad esempio se ne abbiamo diversi tipi e non vogliamo scrivere più codice del necessario. In effetti possiamo anche avere diversi tipi di edifici e in realtà non esiste un buon modo per farlo basato sui componenti!

OrganicBuilding : Building, TechBuilding : Building. Non è necessario creare 2 componenti e scrivere lì due volte il codice per operazioni comuni o proprietà dell'edificio. E poi aggiungili in modo diverso, puoi usare il potere dell'eredità e successivamente del polimorfismo e dell'incapsulamento.


Suggerirei di usare qualcosa nel mezzo. E non abusare dei componenti.


Consiglio vivamente di leggere questo libro sui pattern di programmazione del gioco , è gratuito su WEB.


Scenderò più tardi stasera ma FYI non sto usando l'unità, quindi dovrò aggiustare un po 'che va bene.
user441521,

Oh, sry, pensavo che qui ci fosse un tag Unity, mia cattiva. L'unica cosa è MonoBehavior: è solo una classe base per ogni istanza sulla scena nell'editor Unity. Per quanto riguarda Physics.OverlapSphere () - è un metodo che crea un collisore di sfere durante il frame e controlla ciò che tocca. Le coroutine sono come un falso aggiornamento, le loro chiamate possono essere ridotte a importi inferiori rispetto a fps sul PC dei giocatori - ottimo per le prestazioni. Start () - solo un metodo chiamato una volta quando viene creata l'istanza. Tutto il resto dovrebbe applicarsi ovunque. Nella prossima parte non userò nulla con Unity. Sry. Spero che questo abbia chiarito qualcosa.
Candid Moon _Max_

Ho già usato Unity, quindi capisco l'idea. Sto usando Lua che ha anche coroutine quindi le cose dovrebbero tradursi abbastanza bene.
user441521

Questa risposta sembra un po 'troppo specifica per Unity considerando una mancanza del tag Unity. Se lo rendessi più generico e rendessi l'esempio dell'unità più un esempio, questa sarebbe una risposta molto migliore.
Pharap,

@CandidMoon Sì, va meglio.
Pharap,

4

Non esiste un proiettile d'argento a questo problema, ma ci sono vari approcci diversi, quasi tutti ruotano attorno al principio della "separazione delle preoccupazioni". Altre risposte hanno già discusso del popolare approccio basato sui componenti, ma ci sono altri approcci che possono essere utilizzati al posto o insieme alla soluzione basata sui componenti. Discuterò dell'approccio del controller di entità in quanto è una delle mie soluzioni preferite a questo problema.

In primo luogo, l'idea stessa di una Playerclasse è fuorviante in primo luogo. Molte persone tendono a pensare a un personaggio del giocatore, personaggi del PCC e mostri / nemici come classi diverse, quando in realtà tutti hanno molto in comune: sono tutti disegnati sullo schermo, si muovono tutti, potrebbero tutti hanno inventari ecc.

Questo modo di pensare porta ad un approccio in cui i personaggi dei giocatori, i personaggi non giocanti e i mostri / nemici sono tutti trattati come ' Entitys' piuttosto che essere trattati in modo diverso. Naturalmente, però, devono comportarsi diversamente: il personaggio del giocatore deve essere controllato tramite input e npcs ha bisogno di ai.

La soluzione a questo è di avere Controllerclassi utilizzate per controllare Entitys. In questo modo, tutta la logica pesante finisce nel controller e tutti i dati e la comunanza vengono archiviati nell'entità.

Inoltre, eseguendo la sottoclasse Controllerin InputControllere AIController, consente al giocatore di controllare efficacemente qualsiasi Entitynella stanza. Questo approccio aiuta anche con il multiplayer avendo una RemoteControllero NetworkControllerclasse che opera tramite comandi da un flusso di rete.

Ciò può comportare che molte delle logiche vengano messe in una sola Controllerse non stai attento. Il modo per evitarlo è quello di avere Controllers che sono composti da altri Controllers, o di far Controllerdipendere la funzionalità da varie proprietà di Controller. Ad esempio, AIControlleravrebbe un DecisionTreeallegato, e PlayerCharacterControllerpotrebbe essere composto da vari altri Controllers come a MovementController, a JumpController(contenente una macchina a stati con gli stati OnGround, Ascending e Descending), an InventoryUIController. Un ulteriore vantaggio di ciò è che Controllerè possibile aggiungere nuovi messaggi quando vengono aggiunte nuove funzionalità: se un gioco inizia senza un sistema di inventario e ne viene aggiunto uno, un controller può essere utilizzato in seguito.


Mi piace l'idea, ma sembra che abbia trasferito tutto il codice alla classe controller lasciandomi lo stesso problema.
user441521

@ user441521 Ho appena realizzato che c'era un paragrafo in più che avrei aggiunto ma l'ho perso quando il browser si è bloccato. Lo aggiungerò ora. Fondamentalmente, puoi avere controller diversi in grado di comporli in controller aggregati in modo che ogni controller gestisca cose diverse. es. AggregateController.Controllers = {JumpController (tasti di scelta rapida), MoveController (tasti di scelta rapida), InventoryUIController (tasti di scelta rapida, uisystem)}
Pharap
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.