Como posso evitar classes de jogadores gigantes?

46

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?

user441521
fonte
26
Vários arquivos ou um arquivo, o código precisa ir a algum lugar. Jogos são complexos. Para encontrar o que você precisa, escreva bons nomes de métodos e comentários descritivos. Não tenha medo de fazer alterações - apenas teste. E backup de seu trabalho :)
Chris McFarland
7
Entendo que ele precisa ir a algum lugar, mas o design de código é importante em flexibilidade e manutenção. Ter uma classe ou grupo de código com milhares de linhas também não me parece.
user441521
17
@ChrisMcFarland não sugere backup, sugere o código de versão XD.
GameDeveloper 22/02
1
@ChrisMcFarland Concordo com o GameDeveloper. Ter controle de versão como Git, svn, TFS, ... facilita muito o desenvolvimento, pois é possível desfazer grandes alterações com muito mais facilidade e recuperar-se facilmente de coisas como excluir acidentalmente seu projeto, falha de hardware ou corrupção de arquivo.
Nzall 22/02
3
@TylerH: Eu discordo totalmente. Os backups não permitem mesclar muitas alterações exploratórias, nem vinculam nem perto de metadados úteis a conjuntos de alterações, nem permitem fluxos de trabalho sãos para vários desenvolvedores. Você pode usar o controle de versão como um sistema de backup point-in-time muito poderoso, mas isso está perdendo muito potencial dessas ferramentas.
Phoshi

Respostas:

67

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.

Bálint
fonte
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
MichaelHouse
8
Embora eu entenda os comentários comoventes que precisam ser movidos, não os que desafiam a precisão da resposta. Isso deveria ser óbvio, não?
bug-lote
20

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 um class Inventory. Este é um membro class Player, mas internamente class Inventorypode agrupar muito código.

Outro exemplo: um personagem de jogador pode ter relações com NPCs, então você pode ter uma class Relationreferência ao Playerobjeto e ao NPCobjeto, mas não pertencer a nenhum.

MSalters
fonte
Sim, eu estava apenas procurando idéias sobre como fazer isso. Qual era a mentalidade, porque há muitas funcionalidades de pequenas partes, portanto, embora a codificação não seja natural, para mim de qualquer maneira, quebrar essas pequenas partes de funcionalidade. No entanto, torna-se óbvio que todas essas pequenas funcionalidades começam a tornar a classe de jogadores enorme.
user441521
1
As pessoas costumam dizer que algo é uma classe divina ou um objeto divino, quando contém e gerencia todas as outras classes / objetos do jogo.
Bálint
11

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()(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. IStateno nosso caso, possui 4 métodos apenas por exemplo.Loot() Run() Walk() Attack()

Além disso, temos class InputControlleronde verificamos todas as entradas do usuário.

Agora, para um exemplo real: InputControllerverificamos se o jogador pressiona alguma das teclas WASD or arrowse se ele também pressiona a Shift. Se ele só pressionado WASD, em seguida, chamamos _currentPlayerState.Walk();Quando isso happends e temos currentPlayerStatede 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+ Shiftcheck-in InputControllernão por causa do WalkState). Quando chamamos _currentPlayerState.Run();em WalkState- nós sabemos que temos de alternar _currentPlayerStatepara RunStatee nós fazê-lo em Run()de WalkStatee 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 LootStatequando 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 Efoi pressionado, chamamos _currentPlayerState.Loot();, mudamos para LootStatee 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 : IStateque tenha todos esses comportamentos de métodos padrão implementados, mas os tenha virtualpara que você possa overrideusá-los no class LootState : BaseStatetipo 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 Buildingcuidaria 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. IDamagableclaro 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.

Lua Espontânea _Max_
fonte
Eu vou cavar mais tarde hoje à noite, mas para sua informação, eu não estou usando a unidade, então terei que ajustar alguns que estejam bem.
user441521
Oh, sry, eu pensei que aqui era uma etiqueta da Unity, meu mal. A única coisa é o MonoBehavior - é apenas uma classe base para todas as Instâncias em cena no editor do Unity. Quanto ao Physics.OverlapSphere () - é um método que cria um colisor de esferas durante o quadro e verifica o que toca. As corotinas são como atualizações falsas, suas chamadas podem ser reduzidas a quantidades menores que fps no PC dos jogadores - bom para desempenho. Start () - apenas um método chamado uma vez quando a Instância é criada. Todo o resto deve ser aplicado em qualquer outro lugar. Na próxima parte, não usarei nada com o Unity. Sry. Espero que isso tenha esclarecido alguma coisa.
Candid Moon _Max_
Eu usei o Unity antes, então eu entendo a idéia. Estou usando Lua, que também possui corotinas, para que as coisas sejam traduzidas razoavelmente bem.
user441521
Essa resposta parece um pouco específica do Unity, considerando a falta da etiqueta do Unity. Se você o tornasse mais genérico e tornasse o material da unidade mais um exemplo, essa seria uma resposta muito melhor.
Pharap
@CandidMoon Sim, isso é melhor.
Pharap
4

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 Playerclasse é 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 ' Entitys' 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 Controllerclasses usadas para controlar Entitys. 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 Controllerem InputControllere AIController, ele permite que o jogador controle efetivamente qualquer um Entityna sala. Essa abordagem também ajuda no modo multiplayer ao ter uma classe RemoteControllerou NetworkControllerque opera por meio de comandos de um fluxo de rede.

Isso pode resultar em muitas lógicas sendo agrupadas em uma, Controllerse você não for cuidadoso. A maneira de evitar isso é ter Controllers compostos por outros Controllers ou tornar a Controllerfuncionalidade dependente de várias propriedades do Controller. Por exemplo, o AIControllerteria um DecisionTreeanexo e o PlayerCharacterControllerpoderia ser composto de vários outros Controllers, como a MovementController, a JumpController(contendo uma máquina de estados com os estados OnGround, Ascending e Descending), an InventoryUIController. Um benefício adicional disso é que novos Controllers 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.

Pharap
fonte
Eu gosto da ideia disso, mas parece ter transferido todo o código para a classe do controlador, deixando-me com o mesmo problema.
user441521
@ user441521 Acabei de perceber que havia um parágrafo extra que eu adicionaria, mas eu o perdi quando meu navegador travou. Vou adicioná-lo agora. Basicamente, você pode ter controladores diferentes, podendo compô-los em controladores agregados, para que cada controlador lide com coisas diferentes. por exemplo AggregateController.Controllers = {JumpController (keybinds), MoveController (keybinds), InventoryUIController (keybinds, uisystem)}
Pharap