Quase sempre há uma classe de jogadores em um jogo. O jogador geralmente pode fazer muito no jogo, o que significa para mim que essa classe acaba sendo enorme, com uma tonelada de variáveis para suportar cada parte da funcionalidade que o jogador pode fazer. Cada peça é bastante pequena por si só, mas combinada, acabo com milhares de linhas de código e torna-se difícil encontrar o que você precisa e assustador para fazer alterações. Com algo que é basicamente um controle geral para todo o jogo, como você evita esse problema?
architecture
user441521
fonte
fonte
Respostas:
Você usaria normalmente um sistema de componentes de entidades (um sistema de componentes de entidades é uma arquitetura baseada em componentes). Isso também facilita a criação de outras entidades e também pode fazer com que os inimigos / NPCs tenham os mesmos componentes que o jogador.
Essa abordagem segue exatamente na direção oposta como uma abordagem orientada a objetos. Tudo no jogo é uma entidade. A entidade é apenas um caso sem nenhuma mecânica de jogo incorporada a ela. Possui uma lista de componentes e uma maneira de manipulá-los.
Por exemplo, o player possui um componente de posição, um componente de animação e um componente de entrada e, quando o usuário pressiona espaço, você deseja que ele salte.
Você pode conseguir isso fornecendo à entidade jogador um componente de salto, que quando chamado faz com que o componente de animação mude para a animação de salto e você faz com que o jogador tenha uma velocidade y positiva no componente de posição. No componente de entrada, você escuta a tecla de espaço e chama o componente de salto. (Este é apenas um exemplo, você deve ter um componente do controlador para movimento).
Isso ajuda a dividir o código em módulos menores e reutilizáveis e pode resultar em um projeto mais organizado.
fonte
Jogos não são únicos nisso; as classes divinas são um anti-padrão em toda parte.
Uma solução comum é dividir a grande classe em uma árvore de classes menores. Se o jogador tiver um inventário, não faça parte do gerenciamento de inventário
class Player
. Em vez disso, crie umclass Inventory
. Este é um membroclass Player
, mas internamenteclass Inventory
pode agrupar muito código.Outro exemplo: um personagem de jogador pode ter relações com NPCs, então você pode ter uma
class Relation
referência aoPlayer
objeto e aoNPC
objeto, mas não pertencer a nenhum.fonte
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 teclasWASD or arrows
e se ele também pressiona aShift
. Se ele só pressionadoWASD
, em seguida, chamamos_currentPlayerState.Walk();
Quando isso happends e temoscurrentPlayerState
de ser igual aWalkState
, em seguida,WalkState.Walk()
temos todos os componentes necessários para este estado - neste casoMovementSystem
, de modo que fazer o jogador movimentopublic 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 eraWalkState
. Nesse casoRun()
, será chamadoInputController
(não misture isso,Run()
é chamado porque temosWASD
+Shift
check-inInputController
não por causa doWalkState
). Quando chamamos_currentPlayerState.Run();
emWalkState
- nós sabemos que temos de alternar_currentPlayerState
paraRunState
e nós fazê-lo emRun()
deWalkState
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ãoE
foi pressionado, chamamos_currentPlayerState.Loot();
, mudamos paraLootState
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 pressionarWASD
? -_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 paraIDLEState
.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 tenhavirtual
para que você possaoverride
usá-los noclass 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 comoPutItemToInventory()
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()
, masBuilding
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 jogadorBuild(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 temTakeDamage()
.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.
fonte
Não existe uma bala de prata para esse problema, mas existem várias abordagens diferentes, quase todas que giram em torno do princípio da 'separação de preocupações'. Outras respostas já discutiram a abordagem popular baseada em componentes, mas existem outras abordagens que podem ser usadas em vez de ou em conjunto com a solução baseada em componentes. Vou discutir a abordagem do controlador de entidade, pois é uma das minhas soluções preferidas para esse problema.
Em primeiro lugar, a própria idéia de uma
Player
classe é enganosa em primeiro lugar. Muitas pessoas tendem a pensar em um personagem de jogador, personagens npc e monstros / inimigos como sendo classes diferentes, quando na verdade todos eles têm muito em comum: todos são desenhados na tela, todos se movem, talvez todos têm estoques etc.Esse modo de pensar leva a uma abordagem em que os personagens dos jogadores, os personagens que não são jogadores e os monstros / inimigos são todos tratados como '
Entity
s' em vez de serem tratados de maneira diferente. Naturalmente, eles precisam se comportar de maneira diferente - o personagem do jogador deve ser controlado via entrada e os npcs precisam de ai.A solução para isso é ter
Controller
classes usadas para controlarEntity
s. Ao fazer isso, toda a lógica pesada acaba no controlador e todos os dados e semelhanças são armazenados na entidade.Além disso, ao subclassificar
Controller
emInputController
eAIController
, ele permite que o jogador controle efetivamente qualquer umEntity
na sala. Essa abordagem também ajuda no modo multiplayer ao ter uma classeRemoteController
ouNetworkController
que opera por meio de comandos de um fluxo de rede.Isso pode resultar em muitas lógicas sendo agrupadas em uma,
Controller
se você não for cuidadoso. A maneira de evitar isso é terController
s compostos por outrosController
s ou tornar aController
funcionalidade dependente de várias propriedades doController
. Por exemplo, oAIController
teria umDecisionTree
anexo e oPlayerCharacterController
poderia ser composto de vários outrosController
s, como aMovementController
, aJumpController
(contendo uma máquina de estados com os estados OnGround, Ascending e Descending), anInventoryUIController
. Um benefício adicional disso é que novosController
s podem ser adicionados à medida que novos recursos são adicionados - se um jogo começa sem um sistema de inventário e um é adicionado, um controlador para ele pode ser implementado posteriormente.fonte