Quais são os contras de usar DrawableGameComponent para todas as instâncias de um objeto de jogo?

25

Eu li em muitos lugares que DrawableGameComponents deve ser salvo para coisas como "níveis" ou algum tipo de gerente, em vez de usá-los, por exemplo, para caracteres ou blocos (como esse cara diz aqui ). Mas não entendo por que isso é assim. Eu li este post e fez muito sentido para mim, mas essas são as minorias.

Normalmente, eu não prestaria muita atenção a coisas como essas, mas, neste caso, gostaria de saber por que a aparente maioria acredita que esse não é o caminho a seguir. Talvez esteja faltando alguma coisa.

Kenji Kina
fonte
Estou curioso para saber por que, dois anos depois, você removeu a marca de "aceito" da minha resposta ? Normalmente, eu não me importo - mas neste caso ele causa do VeraShackle enfurecendo deturpação de minha resposta a bolha ao topo :(
Andrew Russell
@AndrewRussell Li este tópico há pouco tempo (devido a algumas respostas e votos) e pensei que não estava qualificado para opinar sobre se sua resposta é a correta ou não. Como você diz, porém, não acho que a resposta de VeraShackle deva ser a mais votada, por isso estou marcando sua resposta como correta novamente.
precisa saber é o seguinte
11
Posso condensar minha resposta aqui da seguinte maneira: "O uso DrawableGameComponentbloqueia você em um único conjunto de assinaturas de método e em um modelo de dados retido específico. Geralmente, essa é a escolha errada inicialmente, e o bloqueio dificulta significativamente a evolução do código no futuro. "Mas deixe-me dizer o que está acontecendo aqui ...
Andrew Russell
11
DrawableGameComponentfaz parte da API XNA principal (novamente: principalmente por razões históricas). Programadores iniciantes o usam porque está "lá" e "funciona" e não o conhecem melhor. Eles veem minha resposta dizendo que estão " errados " e - devido à natureza da psicologia humana - rejeitam isso e escolhem o outro "lado". Que passa a ser a não resposta argumentativa de VeraShackle; que por acaso ganhou impulso porque eu não o chamei imediatamente (veja esses comentários). Sinto que minha eventual refutação lá permanece suficiente.
Andrew Russell
11
Se alguém estiver interessado em questionar a legitimidade da minha resposta com base upvotes: se é verdade que (GDSE sendo aflush com novatos acima mencionados) a resposta de VeraShackle conseguiu adquirir um casal mais upvotes do que o meu, nos últimos anos, não se esqueça que tenho milhares de votos positivos em toda a rede, principalmente no XNA. Eu implementei a API XNA em várias plataformas. E eu sou um desenvolvedor profissional de jogos usando XNA. O pedigree da minha resposta é impecável.
Andrew Russell

Respostas:

22

"Componentes do jogo" foram uma parte muito inicial do design do XNA 1.0. Eles deveriam funcionar como controles para o WinForms. A idéia era que você seria capaz de arrastá-los e soltá-los em seu jogo usando uma GUI (mais ou menos como a adição de controles não visuais, como temporizadores, etc.) e definir propriedades neles.

Então você pode ter componentes como talvez uma câmera ou um ... contador de FPS? ... ou ...?

Entende? Infelizmente, essa ideia não funciona realmente para jogos do mundo real. O tipo de componentes que você deseja para um jogo não é realmente reutilizável. Você pode ter um componente "player", mas será muito diferente do componente "player" de todos os outros jogos.

Eu imagino que foi por esse motivo e por uma simples falta de tempo que a GUI foi puxada antes do XNA 1.0.

Mas a API ainda é utilizável ... Veja por que você não deve usá-la:

Basicamente, tudo se resume ao que é mais fácil escrever, ler, depurar e manter como desenvolvedor de jogos. Fazendo algo parecido com isto no seu código de carregamento:

player.DrawOrder = 100;
enemy.DrawOrder = 200;

É muito menos claro do que simplesmente fazer isso:

virtual void Draw(GameTime gameTime)
{
    player.Draw();
    enemy.Draw();
}

Especialmente quando, em um jogo do mundo real, você pode acabar com algo assim:

GraphicsDevice.Viewport = cameraOne.Viewport;
player.Draw(cameraOne, spriteBatch);
enemy.Draw(cameraOne, spriteBatch);

GraphicsDevice.Viewport = cameraTwo.Viewport;
player.Draw(cameraTwo, spriteBatch);
enemy.Draw(cameraTwo, spriteBatch);

(É verdade que você pode compartilhar a câmera e o spriteBatch usando a Servicesarquitetura semelhante . Mas isso também é uma má idéia pelo mesmo motivo.)

Essa arquitetura - ou a falta dela - é muito mais leve e flexível!

Posso alterar facilmente a ordem do desenho ou adicionar várias viewports ou adicionar um efeito de destino de renderização, apenas alterando algumas linhas. Para fazer isso no outro sistema, é necessário pensar no que todos esses componentes - espalhados por vários arquivos - estão fazendo e em que ordem.

É como a diferença entre programação declarativa e programação imperativa. Acontece que, para o desenvolvimento de jogos, a programação imperativa é muito mais preferível.

Andrew Russell
fonte
2
Fiquei com a impressão de que eles eram para programação baseada em componentes , que aparentemente é amplamente usada para programação de jogos.
BlueRaja - Danny Pflughoeft
3
As classes de componentes de jogos XNA não são realmente diretamente aplicáveis ​​a esse padrão. Esse padrão é sobre como compor objetos a partir de vários componentes. As classes XNA são voltadas para um componente por objeto. Se eles se encaixam perfeitamente no seu design, use-os de qualquer maneira. Mas você não deve criar seu design em torno deles! Veja também minha resposta aqui - procure onde mencionei DrawableGameComponent.
Andrew Russell
11
Concordo que a arquitetura GameComponent / Service não é tão útil e soa como conceitos do Office ou de aplicativos da Web sendo forçados a entrar em uma estrutura de jogo. Mas eu preferiria muito mais usar o player.DrawOrder = 100;método do que o imperativo para a ordem do sorteio. Estou terminando um jogo Flixel agora, que desenha objetos na ordem em que você os adiciona à cena. O sistema FlxGroup alivia um pouco disso, mas ter algum tipo de classificação do Z-index embutido seria bom.
michael.bartnett
@ michael.bartnett Observe que o Flixel é uma API de modo retido, enquanto o XNA (exceto, obviamente, o sistema de componentes do jogo) é uma API de modo imediato. Se você usa uma API de modo retido ou implementa a sua própria , a capacidade de alterar a ordem do sorteio em tempo real é definitivamente importante. De fato, a necessidade de alterar a ordem de retirada em tempo real é uma boa razão para criar algo em modo retido (o exemplo de um sistema de janelas) vem à mente. Mas se você pode escrever algo como modo imediato, se a API da plataforma (como o XNA) for criada dessa maneira, o IMO deverá ser feito.
Andrew Russell
Para referência adicional gamedev.stackexchange.com/a/112664/56
Kenji Kina
26

Hummm. Tenho desafio este por uma questão de equilíbrio aqui, eu tenho medo.

Parece haver 5 cobranças na resposta de Andrew, então tentarei lidar com cada uma delas:

1) Os componentes são um design desatualizado e abandonado.

A idéia do padrão de design dos componentes existia muito antes do XNA - em essência, é simplesmente uma decisão de fazer objetos, em vez do objeto de jogo abrangente, a casa do seu próprio comportamento de Atualização e Empate. Para objetos em sua coleção de componentes, o jogo chamará esses métodos (e alguns outros) automaticamente no momento apropriado - crucialmente, o objeto do jogo não precisa, por si só, 'conhecer' esse código. Se você deseja reutilizar suas classes de jogos em jogos diferentes (e, portanto, objetos de jogos diferentes), esse desacoplamento de objetos ainda é uma boa idéia. Uma rápida olhada nas mais recentes demos (produzidas profissionalmente) do XNA4.0 do AppHub confirma minha impressão de que a Microsoft ainda está (justamente na minha opinião) firmemente por trás disso.

2) Andrew não consegue pensar em mais de duas classes de jogo reutilizáveis.

Eu posso. Na verdade, eu posso pensar em cargas! Acabei de verificar minha lib compartilhada atual e ela compreende mais de 80 componentes e serviços reutilizáveis . Para dar um sabor, você pode ter:

  • Estado do jogo / gerenciadores de tela
  • ParticleEmitters
  • Primitivas desenháveis
  • Controladores AI
  • Controladores de entrada
  • Controladores de animação
  • Outdoors
  • Gerentes de Física e Colisão
  • Câmeras

Qualquer ideia de jogo em particular precisará de pelo menos 5 a 10 itens deste conjunto - garantida - e eles podem ser totalmente funcionais em um jogo completamente novo com uma linha de código.

3) É preferível controlar a ordem de desenho pela ordem das chamadas de desenho explícitas do que usar a propriedade DrawOrder do componente.

Bem, eu basicamente concordo com Andrew neste caso. MAS, se você deixar todas as propriedades DrawOrder de seu componente como padrão, ainda poderá controlar explicitamente a ordem dos desenhos pela ordem em que os adiciona à coleção. Isso é realmente idêntico em termos de 'visibilidade' à ordenação explícita de suas chamadas de sorteio manual.

4) Chamar uma carga dos métodos Draw do objeto é mais 'leve' do que tê-los chamados por um método de estrutura existente.

Por quê? Além disso, em todos os projetos, exceto os mais triviais, sua lógica de atualização e o código Draw ofuscarão totalmente sua chamada de método em termos de desempenho atingido em qualquer caso.

5) É preferível colocar o código em um arquivo, durante a depuração, do que tê-lo no arquivo do objeto individual.

Bem, primeiro, quando estou depurando no Visual Studio, a localização de um ponto de interrupção em termos de arquivos em disco é quase invisível atualmente - o IDE lida com a localização sem nenhum drama. Mas considere também por um momento que você tem um problema com o desenho do Skybox. Ir para o método Draw do Skybox é realmente mais oneroso do que localizar o código de desenho do skybox no método de desenho mestre (presumivelmente muito longo) no objeto Game? Não, na verdade não - na verdade eu diria que é exatamente o contrário.

Então, em resumo - por favor, dê uma boa olhada nos argumentos de Andrew aqui. Não acredito que eles realmente dêem motivos substanciais para escrever código de jogo emaranhado. As vantagens do padrão de componente desacoplado (usado criteriosamente) são enormes.

VeraShackle
fonte
2
Acabei de notar este post e devo emitir uma forte refutação: não tenho problemas com classes reutilizáveis! (Que pode ter métodos de desenho e atualização e assim por diante.) Estou tendo um problema específico com o uso de ( Drawable) GameComponentpara fornecer sua arquitetura de jogo, devido à perda de controle explícito da ordem de desenho / atualização (com a qual você concorda em # 3) a rigidez de suas interfaces (que você não endereça).
Andrew Russell
11
Seu ponto 4 leva meu uso da palavra "leve" para significar desempenho, onde eu realmente quero dizer flexibilidade arquitetural. Seus pontos restantes # 1, # 2 e especialmente # 5 parecem erguer o palhaço absurdo que eu acho que a maioria / todo o código deve ser inserido na sua classe principal de jogo! Leia minha resposta aqui para alguns dos meus pensamentos mais detalhados sobre arquitetura.
Andrew Russell
5
Finalmente: se você postar uma resposta à minha resposta, dizendo que estou errado, seria cortês também adicionar uma resposta à minha resposta para que eu recebesse uma notificação sobre ela, em vez de ter que tropeçar -lo dois meses depois.
Andrew Russell
Posso ter entendido mal, mas como isso responde à pergunta? Devo usar um GameComponent para meus sprites ou não?
Superbest
Refutação adicional: gamedev.stackexchange.com/a/112664/56
Kenji Kina
4

Discordo um pouco de Andrew, mas também acho que há tempo e lugar para tudo. Eu não usaria um Drawable(GameComponent)para algo tão simples quanto um Sprite ou Inimigo. No entanto, eu o usaria para um SpriteManagerou EnemyManager.

Voltando ao que Andrew disse sobre desenhar e atualizar a ordem, acho Sprite.DrawOrderque seria muito confuso para muitos sprites. Além disso, é relativamente fácil configurar um DrawOrdere UpdateOrderpara uma pequena (ou razoável) quantidade de gerentes que são (Drawable)GameComponents.

Eu acho que a Microsoft teria acabado com eles se eles realmente estivessem tão rígidos e ninguém os usasse. Mas não o fizeram porque funcionam perfeitamente bem, se o papel estiver correto. Eu mesmo os uso para desenvolver Game.Servicesatualizações em segundo plano.

Amble Lighthertz
fonte
2

Eu estava pensando sobre isso também, e me ocorreu: Por exemplo, para roubar o exemplo no seu link, em um jogo RTS, você poderia transformar cada unidade em uma instância de componente do jogo. OK.

Mas você também pode criar um componente UnitManager, que mantém uma lista de unidades, as desenha e as atualiza conforme necessário. Acho que o motivo pelo qual você deseja transformar suas unidades em componentes de jogos é compartimentar o código, então essa solução alcançaria adequadamente esse objetivo?

Superbest
fonte
2

Nos comentários, publiquei uma versão condensada da minha resposta antiga resposta:

Usar DrawableGameComponentbloqueia você em um único conjunto de assinaturas de método e em um modelo de dados retido específico. Normalmente, essas são as escolhas erradas inicialmente, e o bloqueio dificulta significativamente a evolução do código no futuro.

Aqui tentarei ilustrar por que esse é o caso, publicando algum código do meu projeto atual.


O objeto raiz que mantém o estado do mundo do jogo tem um Updatemétodo parecido com este:

void Update(UpdateContext updateContext, MultiInputState currentInput)

Existem vários pontos de entrada para Update, dependendo se o jogo está rodando na rede ou não. É possível, por exemplo, que o estado do jogo seja revertido para um ponto anterior no tempo e depois simulado novamente.

Existem também alguns métodos adicionais de atualização, como:

void PlayerJoin(int playerIndex, string playerName, UpdateContext updateContext)

Dentro do estado do jogo, há uma lista de atores a serem atualizados. Mas isso é mais do que uma simples Updateligação. Existem passes adicionais antes e depois para lidar com a física. Há também algumas coisas sofisticadas para desovar / destruir adiado de atores, bem como transições de nível.

Fora do estado do jogo, há um sistema de interface do usuário que precisa ser gerenciado de forma independente. Mas algumas telas na interface do usuário também precisam ser executadas em um contexto de rede.


Para o desenho, devido à natureza do jogo, existe um sistema personalizado de classificação e seleção que recebe entradas desses métodos:

virtual void RegisterToDraw(SortedDrawList sortedDrawList, Definitions definitions)
virtual void RegisterShadow(ShadowCasterList shadowCasterList, Definitions definitions)

E então, depois de descobrir o que desenhar, chama automaticamente:

virtual void Draw(DrawContext drawContext, int tag)

O tagparâmetro é necessário porque alguns atores podem se apegar a outros atores - e, portanto, precisam ser capazes de desenhar acima e abaixo do ator retido. O sistema de classificação garante que isso aconteça na ordem correta.

Há também o sistema de menus para desenhar. E um sistema hierárquico para desenhar a interface do usuário, ao redor do HUD, ao redor do jogo.


Agora compare esses requisitos com o que DrawableGameComponentfornece:

public class DrawableGameComponent
{
    public virtual void Update(GameTime gameTime);
    public virtual void Draw(GameTime gameTime);
    public int UpdateOrder { get; set; }
    public int DrawOrder { get; set; }
}

Não há uma maneira razoável de passar de um sistema DrawableGameComponentpara o sistema descrito acima. (E esses não são requisitos incomuns para um jogo de complexidade ainda modesta).

Além disso, é muito importante observar que esses requisitos não surgiram simplesmente no início do projeto. Eles evoluíram ao longo de muitos meses de trabalho de desenvolvimento.


O que as pessoas normalmente fazem, se iniciam o DrawableGameComponentcaminho, é começar a desenvolver hacks:

  • Em vez de adicionar métodos, eles fazem uma conversão feia para o seu próprio tipo base (por que não usar isso diretamente?).

  • Em vez de substituir o código para classificar e chamar Draw/ Update, eles fazem algumas coisas muito lentas para alterar os números de pedidos rapidamente.

  • E o pior de tudo: em vez de adicionar parâmetros aos métodos, eles começam a "passar" "parâmetros" em locais de memória que não são a pilha (o uso Servicespara isso é popular). Isso é complicado, propenso a erros, lento, frágil, raramente seguro para threads e provavelmente se tornará um problema se você adicionar redes, criar ferramentas externas e assim por diante.

    Ele também faz reutilização de código mais difícil , porque agora as suas dependências estão escondidos dentro do "componente", em vez de publicado no limite API.

  • Ou, eles quase veem o quão louco isso tudo é e começam a fazer "gestores" DrawableGameComponentpara solucionar esses problemas. Exceto que eles ainda têm esses problemas nos limites do "gerente".

    Invariavelmente, esses "gerentes" poderiam ser facilmente substituídos por uma série de chamadas de método na ordem desejada. Qualquer outra coisa é muito complicada.

Obviamente, quando você começa a empregar esses hacks, é preciso um trabalho real para desfazê- los quando você perceber que precisa de mais do que aquilo que DrawableGameComponentoferece. Você se torna " trancado ". E a tentação é muitas vezes continuar acumulando hacks - o que na prática o atrapalha e limita os tipos de jogos que você pode fazer aos relativamente simplistas.


Muitas pessoas parecem chegar a esse ponto e têm uma reação visceral - possivelmente porque usam DrawableGameComponente não querem aceitar "estar errado". Então eles se prendem ao fato de que é pelo menos possível fazê-lo "funcionar".

Claro que pode ser feito para "trabalhar"! Somos programadores - fazer as coisas funcionarem sob restrições incomuns é praticamente o nosso trabalho. Mas essa restrição não é necessária, útil ou racional ...


O que é particularmente espantosa sobre é que é surpreendentemente trivial para side-passo toda esta questão. Aqui, eu vou te dar o código:

public class Actor
{
    public virtual void Update(GameTime gameTime);
    public virtual void Draw(GameTime gameTime);
}

List<Actor> actors = new List<Actor>();

foreach(var actor in actors)
    actor.Update(gameTime);

foreach(var actor in actors)
    actor.Draw(gameTime);

(É simples adicionar a classificação de acordo com DrawOrdere UpdateOrder. Uma maneira simples é manter duas listas e usá-la List<T>.Sort(). Também é trivial manipular Load/ UnloadContent.)

Não é importante o que esse código realmente é . O importante é que eu possa modificá-lo trivialmente .

Quando percebo que preciso de um sistema de temporização diferente gameTime, ou preciso de mais parâmetros (ou um UpdateContextobjeto inteiro com cerca de 15 coisas), ou preciso de uma ordem de operações completamente diferente para desenhar, ou preciso de alguns métodos extras para diferentes passes de atualização, posso apenas fazer isso .

E eu posso fazer isso imediatamente. Não preciso me preocupar em escolher DrawableGameComponentmeu código. Ou pior: corte-o de alguma forma que tornará ainda mais difícil a próxima vez que essa necessidade surgir.


Na verdade, existe apenas um cenário em que é uma boa escolha DrawableGameComponent: se você estiver literalmente criando e publicando middleware no estilo arrastar e soltar componentes para o XNA. Qual era o seu propósito original. O que - acrescentarei - ninguém está fazendo!

(Da mesma forma, só faz sentido usarServices ao criar tipos de conteúdo personalizados para ContentManager.)

Andrew Russell
fonte
Embora eu concorde com seus pontos de vista, também não vejo nada particularmente errado no uso de itens herdados de DrawableGameComponentcertos componentes, especialmente em itens como telas de menu (menus, muitos botões, opções e outros itens) ou sobreposições de FPS / depuração você mencionou. Nesses casos, você geralmente não precisa de um controle refinado sobre a ordem de desenho e acaba com menos código que precisa escrever manualmente (o que é uma coisa boa, a menos que você precise escrever esse código). Mas acho que esse é o cenário de arrastar e soltar que você estava mencionando.
Groo