Come far progredire un gamestate componente-entità in un gioco a turni?


9

Finora i sistemi di componenti di entità che ho usato hanno funzionato principalmente come l'artemis di Java:

  • Tutti i dati nei componenti
  • Sistemi indipendenti senza stato (almeno nella misura in cui non richiedono input per l'inizializzazione) che scorre su ogni entità che contiene solo i componenti a cui un determinato sistema è interessato
  • Tutti i sistemi elaborano le loro entità con un segno di spunta, quindi il tutto ricomincia.

Ora sto provando ad applicarlo per la prima volta a un gioco a turni, con tonnellate di eventi e risposte che devono avvenire in un ordine stabilito l'uno rispetto all'altro, prima che il gioco possa andare avanti. Un esempio:

Il giocatore A subisce danni da una spada. In risposta a questo, l'armatura di A entra e abbassa il danno subito. Anche la velocità di movimento di A viene ridotta a causa dell'indebolimento.

  • Il danno subito è ciò che scatena l'intera interazione
  • L'armatura deve essere calcolata e applicata al danno in arrivo prima che il danno sia applicato al giocatore
  • La riduzione della velocità di movimento non può essere applicata a un'unità se non dopo che il danno è stato effettivamente inflitto, poiché dipende dall'ammontare del danno finale.

Gli eventi possono anche innescare altri eventi. Ridurre il danno della spada usando l'armatura può farla frantumare (ciò deve avvenire prima che la riduzione del danno sia completata), che a sua volta può causare ulteriori eventi in risposta ad essa, essenzialmente una valutazione ricorsiva degli eventi.

Tutto sommato, questo sembra portare ad alcuni problemi:

  1. Molti cicli di elaborazione sprecati: la maggior parte dei sistemi (ad eccezione delle cose che funzionano sempre, come il rendering) semplicemente non ha nulla di utile da fare quando non è "il loro turno" per funzionare, e passa la maggior parte del tempo in attesa che il gioco entri uno stato di lavoro valido. Questo cosparge ogni sistema di questo tipo con controlli che continuano a crescere di dimensioni più stati vengono aggiunti al gioco.
  2. Per scoprire se un sistema è in grado di elaborare entità presenti nel gioco, hanno bisogno di un modo per monitorare altri stati entità / sistema non correlati (il sistema responsabile di infliggere danni deve sapere se l'armatura è stata applicata o meno). Questo o confonde i sistemi con molteplici responsabilità, o crea la necessità di sistemi aggiuntivi senza altro scopo se non quello di scansionare la raccolta di entità dopo ogni ciclo di elaborazione e comunicare con una serie di ascoltatori dicendo loro quando va bene fare qualcosa.

I due punti precedenti presuppongono che i sistemi funzionino sullo stesso insieme di entità, che finiscono per cambiare stato usando flag nei loro componenti.

Un altro modo per risolverlo sarebbe aggiungere / rimuovere componenti (o creare entità completamente nuove) come risultato di un singolo lavoro di sistema per far avanzare lo stato dei giochi. Ciò significa che ogni volta che un sistema ha effettivamente un'entità corrispondente, sa che è autorizzato a elaborarlo.

Ciò rende tuttavia i sistemi responsabili dell'attivazione dei sistemi successivi, rendendo difficile ragionare sul comportamento dei programmi poiché i bug non verranno visualizzati come risultato di una singola interazione del sistema. L'aggiunta di nuovi sistemi diventa anche più difficile poiché non possono essere implementati senza sapere esattamente come influenzano altri sistemi (e potrebbe essere necessario modificare i sistemi precedenti per attivare gli stati a cui il nuovo sistema è interessato), un po 'vanificando lo scopo di avere sistemi separati con una sola attività.

È qualcosa con cui dovrò convivere? Ogni singolo esempio di ECS che ho visto è stato in tempo reale, ed è davvero facile vedere come funziona questo ciclo di ripetizione per gioco in questi casi. E ne ho ancora bisogno per il rendering, sembra davvero inadatto per i sistemi che mettono in pausa la maggior parte degli aspetti di se stesso ogni volta che succede qualcosa.

Esiste un modello di progettazione per far avanzare lo stato del gioco che è adatto a questo, o dovrei semplicemente spostare tutta la logica fuori dal ciclo e invece attivarla solo quando necessario?


Non vuoi davvero votare per un evento. Un evento si verifica solo quando si verifica. Artemis non consente ai sistemi di comunicare tra loro?
Sidar,

Lo fa, ma solo accoppiandoli usando metodi.
Aeris130,

Risposte:


3

Il mio consiglio qui viene dall'esperienza passata su un progetto di gioco di ruolo in cui abbiamo usato un sistema di componenti. Dirò che odiavo lavorare in quel codice a lato del gioco perché era un codice spaghetti. Quindi non sto offrendo molto di una risposta qui, solo una prospettiva:

La logica che descrivi per gestire il danno da spada a un giocatore ... sembra che un sistema dovrebbe essere responsabile di tutto ciò.

Da qualche parte, c'è una funzione HandleWeaponHit (). Avrebbe accesso all'ArmorComponent dell'entità giocatore per ottenere l'armatura pertinente. Avrebbe accesso al componente Arma dell'entità arma attaccante per mandare in frantumi l'arma. Dopo aver calcolato il danno finale, toccherebbe il Componente Movimento per consentire al giocatore di ottenere la riduzione della velocità.

Per quanto riguarda i cicli di elaborazione sprecati ... HandleWeaponHit () dovrebbe essere attivato solo quando necessario (al rilevamento del colpo di spada).

Forse il punto che sto cercando di chiarire è: sicuramente vuoi un posto nel codice in cui puoi mettere un breakpoint, colpirlo e quindi procedere attraverso tutta la logica che dovrebbe correre quando si verifica un colpo di spada. In altre parole, la logica non dovrebbe essere sparsa tra le funzioni tick () di più sistemi.


Farlo in questo modo renderebbe baloon la funzione hit () man mano che si aggiungono più comportamenti. Diciamo che c'è un nemico che cade ridendo ogni volta che una spada colpisce un bersaglio (qualsiasi bersaglio) all'interno della sua linea di vista. HandleWeaponHit dovrebbe davvero essere responsabile di innescarlo?
Aeris130,

1
Hai una sequenza di combattimento strettamente invischiata, quindi sì, il colpo è responsabile dell'attivazione degli effetti. Non tutto deve essere suddiviso in piccoli sistemi, lascia che questo sistema gestisca questo perché è davvero il tuo "Sistema di combattimento" e gestisce ... Combattimento ...
Patrick Hughes,

3

È una domanda di un anno, ma ora sto affrontando gli stessi problemi con il mio gioco fatto in casa mentre studio ECS, quindi un po 'di negromia. Speriamo che finisca in una discussione o almeno in alcuni commenti.

Non sono sicuro se viola i concetti di ECS, ma cosa succede se:

  • Aggiungi un EventBus per consentire ai sistemi di emettere / iscriversi agli oggetti evento (dati puri in realtà, ma non un componente suppongo)
  • Crea componenti per ogni stato intermedio

Esempio:

  • UserInputSystem genera un evento di attacco con [DamageDealerEntity, DamageReceiverEntity, Skill / Weapon info utilizzate]
  • CombatSystem è abbonato ad esso e calcola la possibilità di evasione per DamageReceiver. Se l'evasione fallisce, innesca l'evento Danno con gli stessi parametri
  • DamageSystem è iscritto a tale evento e quindi attivato
  • DamageSystem utilizza la Forza, il danno BaseWeapon, il suo tipo ecc. E lo scrive in un nuovo IncomingDamageComponent con [DamageDealerEntity, FinalOutgoingDamage, DamageType] e lo collega all'entità / entità destinatario del danno
  • DamageSystem genera un OutgoingDamageCalculated
  • ArmorSystem viene attivato da esso, raccoglie un'entità ricevente o cerca questo aspetto IncomingDamage in Entità per raccogliere IncomingDamageComponent (l'ultimo potrebbe probabilmente essere migliore per attacchi multipli con diffusione) e calcola armature e danni ad essa applicati. Facoltativamente, innesca eventi per frantumare la spada
  • ArmorSystems rimuove il componente IncomingDamageComponent in ogni entità e lo sostituisce con DamageReceivedComponent con i numeri calcolati finali che influenzeranno HP e la riduzione della velocità dalle ferite
  • ArmorSystems invia un evento IncomingDamageCalculated
  • Il sistema di velocità è sottoscritto e ricalcola la velocità
  • HealthSystem è abbonato e riduce gli HP effettivi
  • eccetera
  • In qualche modo ripulire

Professionisti:

  • Il sistema si attiva reciprocamente fornendo dati intermedi per eventi a catena complessi
  • Disaccoppiamento con EventBus

Contro:

  • Sento di mescolare due modi di passare le cose: nei parametri degli eventi e nei Componenti temporanei. potrebbe essere un posto debole. In teoria per mantenere le cose omogenee potrei innescare eventi enumerati senza dati in modo che i sistemi trovino i parametri impliciti nei componenti dell'Entità per aspetto ... Non sono sicuro che sia OK però
  • Non sono sicuro di sapere se tutti i sistemi potenzialmente interessati hanno elaborato IncomingDamageCalculated in modo che possa essere ripulito e consentire il prossimo turno. Forse una sorta di controllo indietro in CombatSystem ...

2

Pubblicando la soluzione su cui ho finalmente deciso, simile a quella di Yakovlev.

Fondamentalmente, ho finito per usare un sistema di eventi poiché ho trovato molto intuitivo seguire la sua logica a turni. Il sistema finì per essere responsabile delle unità di gioco che aderivano alla logica a turni (giocatore, mostri e qualsiasi cosa con cui potevano interagire), attività in tempo reale come rendering e polling di input venivano collocate altrove.

I sistemi implementano un metodo onEvent che accetta un evento e un'entità come input, segnalando che l'entità ha ricevuto l'evento. Ogni sistema si iscrive anche a eventi ed entità con un insieme specifico di componenti. L'unico punto di interazione disponibile per i sistemi è il singleton del gestore entità, utilizzato per inviare eventi alle entità e recuperare componenti da un'entità specifica.

Quando il gestore entità riceve un evento associato all'entità a cui viene inviato, posiziona l'evento sul retro di una coda. Mentre ci sono eventi nella coda, l'evento principale viene recuperato e inviato a tutti i sistemi che entrambi si iscrivono all'evento ed è interessato al set di componenti dell'entità che riceve l'evento. Tali sistemi possono a loro volta elaborare i componenti dell'entità, nonché inviare eventi aggiuntivi al gestore.

Esempio: il giocatore subisce danno, quindi all'entità giocatore viene inviato un evento danno. Il DamageSystem sottoscrive gli eventi di danno inviati a qualsiasi entità con il componente di integrità e ha un metodo onEvent (entità, evento) che riduce l'integrità nel componente di entità dell'importo specificato nell'evento.

Ciò semplifica l'inserimento di un sistema di armatura che si abbona agli eventi di danno inviati alle entità con un componente di armatura. Il suo metodo onEvent riduce il danno in caso di ammontare dell'armatura nel componente. Ciò significa che specificare l'ordine in cui i sistemi ricevono gli eventi ha un impatto sulla logica di gioco, poiché il sistema di armatura deve elaborare l'evento danno prima che il sistema di danno funzioni.

A volte, tuttavia, un sistema deve uscire dall'entità ricevente. Per continuare con la mia risposta a Eric Undersander, sarebbe banale aggiungere un sistema che acceda alla mappa di gioco e cerchi entità con il componente FallsDownLaughing entro x spazi dell'entità che riceve danno, e quindi inviare loro un FallDownLaughingEvent. Questo sistema dovrebbe essere programmato per ricevere l'evento dopo il sistema di danno, se l'evento di danno non è stato cancellato a quel punto, il danno è stato inflitto.

Un problema che è emerso è stato come assicurarsi che gli eventi di risposta siano elaborati nell'ordine in cui vengono inviati, dato che alcune risposte possono generare risposte aggiuntive. Esempio:

Il giocatore si muove, spingendo un evento di movimento che viene inviato all'entità del giocatore e raccolto dal sistema di movimento.

In coda: movimento

Se il movimento è permesso, il sistema regola la posizione dei giocatori. In caso contrario (il giocatore ha provato a muoversi in un ostacolo), segna l'evento come annullato, facendo sì che il gestore entità lo scarti invece di inviarlo ai sistemi successivi. Alla fine dell'elenco dei sistemi interessati all'evento c'è il TurnFinishedSystem, che conferma che il giocatore ha passato il suo turno a muovere il personaggio e che il suo turno è finito. Ciò si traduce in un evento TurnOver inviato all'entità giocatore e posto in coda.

In coda: TurnOver

Ora dì che il giocatore ha calpestato una trappola, causando danni. TrapSystem riceve il messaggio di movimento prima di TurnFinishedSystem, quindi l'evento di danno viene inviato per primo. Ora invece la coda è simile a questa:

In coda: Danno, TurnOver

Finora tutto va bene, l'evento di danno verrà elaborato e quindi il turno termina. Tuttavia, cosa succede se ulteriori eventi vengono inviati come risposta al danno? Ora la coda degli eventi sarebbe simile a:

In coda: Damage, TurnOver, ResponseToDamage

In altre parole, il turno sarebbe terminato prima che venissero elaborate le risposte al danno.

Per risolvere questo ho finito con due metodi di invio degli eventi: send (evento, entità) e rispondi (evento, eventToRespondTo, entità).

Ogni evento tiene traccia degli eventi precedenti in una catena di risposta e ogni volta che viene utilizzato il metodo reply (), l'evento a cui si risponde (e ogni evento nella sua catena di risposta) finisce in testa alla catena nell'evento usato per rispondere con. L'evento di movimento iniziale non ha tali eventi. La successiva risposta al danno ha l'evento di movimento nella sua lista.

Inoltre, viene utilizzato un array a lunghezza variabile per contenere più code di eventi. Ogni volta che un gestore riceve un evento, l'evento viene aggiunto a una coda in un indice dell'array che corrisponde alla quantità di eventi nella catena di risposta. Pertanto, l'evento di movimento iniziale viene aggiunto alla coda in [0] e il danno, così come gli eventi TurnOver, vengono aggiunti a una coda separata in [1] poiché entrambi sono stati inviati come risposte al movimento.

Quando vengono inviate le risposte all'evento danno, quegli eventi conterranno sia l'evento danno stesso, sia il movimento, mettendoli in coda all'indice [2]. Finché index [n] ha eventi nella sua coda, tali eventi verranno elaborati prima di passare a [n-1]. Questo dà un ordine di elaborazione di:

Movimento -> Danno [1] -> ResponseToDamage [2] -> [2] è vuoto -> TurnOver [1] -> [1] è vuoto -> [0] è vuoto

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.