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()
lì (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
. IState
ha nel nostro caso ha 4 metodi solo per un esempio.Loot() Run() Walk() Attack()
Inoltre, abbiamo class InputController
dove controlliamo ogni input dell'utente.
Ora, per un vero esempio: InputController
controlliamo se il giocatore preme uno dei WASD or arrows
e poi se preme anche il Shift
. Se ha premuto solo WASD
allora chiamiamo _currentPlayerState.Walk();
quando questo accade e dobbiamo currentPlayerState
essere uguali a WalkState
allora 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
+ Shift
premuto? Ma il nostro stato precedente era WalkState
. In questo caso Run()
verrà chiamato InputController
(non confondere, Run()
viene chiamato perché abbiamo WASD
+ Shift
check-in InputController
non a causa del WalkState
). Quando chiamiamo _currentPlayerState.Run();
in WalkState
- sappiamo che dobbiamo passare _currentPlayerState
per RunState
e lo facciamo in Run()
su WalkState
e 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 LootState
quando 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 è E
stato premuto il pulsante chiamiamo, _currentPlayerState.Loot();
passiamo a LootState
e 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 : IState
che ha implementato tutti questi metodi predefiniti, ma li ha in virtual
modo da poterli override
fare in un class LootState : BaseState
tipo 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 Building
si 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
. IDamagable
certo 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.