1) Player: Máquina baseada em estado + arquitetura baseada em componente.
Componentes comuns para o Player: HealthSystem, MovementSystem, InventorySystem, ActionSystem. Essas são todas as classes como class HealthSystem
.
Eu não recomendo usar Update()
lá (não faz sentido, em casos habituais, ter atualização no sistema de saúde, a menos que você precise para algumas ações em todos os quadros, isso raramente ocorre. Um caso em que você também pode pensar - o jogador é envenenado e você precisa dele para perder saúde de tempos em tempos - aqui eu sugiro usar corotinas.Uma outra constantemente regenera a saúde ou a potência, você apenas toma a saúde ou energia atual e chama a corotina para preencher esse nível quando chegar a hora. ele estava danificado ou começou a correr de novo e assim por diante .
Estados: LootState, RunState, WalkState, AttackState, IDLEState.
Todo estado herda interface IState
. IState
no nosso caso, possui 4 métodos apenas por exemplo.Loot() Run() Walk() Attack()
Além disso, temos class InputController
onde verificamos todas as entradas do usuário.
Agora, para um exemplo real: InputController
verificamos se o jogador pressiona alguma das teclas WASD or arrows
e se ele também pressiona a Shift
. Se ele só pressionado WASD
, em seguida, chamamos _currentPlayerState.Walk();
Quando isso happends e temos currentPlayerState
de ser igual a WalkState
, em seguida, WalkState.Walk()
temos todos os componentes necessários para este estado - neste caso MovementSystem
, de modo que fazer o jogador movimento public void Walk() { _playerMovementSystem.Walk(); }
- você ver o que temos aqui? Temos uma segunda camada de comportamento e isso é muito bom para manutenção e depuração de código.
Agora, para o segundo caso: e se tivermos pressionado WASD
+ Shift
? Mas nosso estado anterior era WalkState
. Nesse caso Run()
, será chamado InputController
(não misture isso, Run()
é chamado porque temos WASD
+ Shift
check-in InputController
não por causa do WalkState
). Quando chamamos _currentPlayerState.Run();
em WalkState
- nós sabemos que temos de alternar _currentPlayerState
para RunState
e nós fazê-lo em Run()
de WalkState
e chamá-lo de novo dentro deste método, mas agora com um estado diferente, porque nós não queremos para a ação perder esse quadro. E agora é claro que ligamos _playerMovementSystem.Run();
.
Mas para que LootState
quando o jogador não pode andar ou correr até que ele solte o botão? Bem, neste caso, quando começamos a saquear, por exemplo, quando o botão E
foi pressionado, chamamos _currentPlayerState.Loot();
, mudamos para LootState
e agora chamamos a partir daí. Por exemplo, chamamos o método collsion para obter se há algo a ser saqueado no intervalo. E chamamos corotina onde temos uma animação ou onde a iniciamos e também verificamos se o jogador ainda pressiona o botão; se não a corotina quebra, se sim, damos a ele saques no final da corotina. Mas e se o jogador pressionar WASD
? - _currentPlayerState.Walk();
é chamado, mas aqui está o bonito da máquina de estado, emLootState.Walk()
temos um método vazio que não faz nada ou como eu faria como um recurso - os jogadores dizem: "Ei cara, eu ainda não saquei isso, você pode esperar?". Quando ele termina a pilhagem, mudamos para IDLEState
.
Além disso, você pode criar outro script chamado class BaseState : IState
que tenha todos esses comportamentos de métodos padrão implementados, mas os tenha virtual
para que você possa override
usá-los no class LootState : BaseState
tipo de classe.
O sistema baseado em componentes é ótimo, a única coisa que me incomoda são as instâncias, muitas delas. E é preciso mais memória e trabalho para o coletor de lixo. Por exemplo, se você tiver 1000 instâncias de inimigo. Todos eles com 4 componentes. 4000 objetos em vez de 1000. Mb não é tão importante (não executei testes de desempenho) se considerarmos todos os componentes que o unitobject game possui.
2) Arquitetura baseada em herança. Embora você note que não podemos nos livrar completamente dos componentes - é realmente impossível se queremos ter um código limpo e funcionando. Além disso, se quisermos usar padrões de design que são altamente recomendados para uso em casos adequados (não os use demais também, isso é chamado de engenharia excessiva).
Imagine que temos uma classe Player que possui todas as propriedades necessárias para sair de um jogo. Possui saúde, mana ou energia, pode se mover, executar e usar habilidades, possui um inventário, pode criar itens, itens de pilhagem e até mesmo construir algumas barricadas ou torres.
Antes de tudo, vou dizer que Inventário, Artesanato, Movimento, Construção devem ser baseados em componentes, porque não é responsabilidade do jogador ter métodos como AddItemToInventoryArray()
- embora o jogador possa ter um método como PutItemToInventory()
esse que chamará o método descrito anteriormente (2 camadas - podemos adicione algumas condições, dependendo das diferentes camadas).
Outro exemplo com a construção. O jogador pode chamar algo como OpenBuildingWindow()
, mas Building
cuidaria de todo o resto, e quando o usuário decide construir algum edifício específico, ele passa todas as informações necessárias para o jogador Build(BuildingInfo someBuildingInfo)
e o jogador começa a construí-lo com todas as animações necessárias.
Princípios do SOLID - OOP. S - responsabilidade única: é o que vimos nos exemplos anteriores. Sim, ok, mas onde está a herança?
Aqui: a saúde e outras características do jogador devem ser tratadas por outra entidade? Eu acho que não. Não pode haver um jogador sem saúde, se houver, simplesmente não herdamos. Por exemplo, nós temos IDamagable
, LivingEntity
, IGameActor
, GameActor
. IDamagable
claro que tem 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.
}
Portanto, aqui não consegui realmente dividir os componentes da herança, mas podemos misturá-los como você vê. Também podemos criar algumas classes base para o sistema Building, por exemplo, se tivermos tipos diferentes e não quisermos escrever mais código do que o necessário. De fato, também podemos ter diferentes tipos de edifícios e, na verdade, não há uma boa maneira de fazê-lo com base em componentes!
OrganicBuilding : Building
, TechBuilding : Building
. Você não precisa criar 2 componentes e escrever código lá duas vezes para operações ou propriedades comuns de construção. E, em seguida, adicione-os de maneira diferente, você pode usar o poder da herança e, posteriormente, do polimorfismo e da incapsulação.
Eu sugeriria usar algo no meio. E não uso excessivo de componentes.
Eu recomendo a leitura deste livro sobre Game Programming Patterns - é gratuito na WEB.