Loop de jogo “ideal” para rolagem lateral 2D

14

É possível descrever um layout "ótimo" (em termos de desempenho) para o loop de jogo de um side-scroller 2D? Nesse contexto, o "loop do jogo" recebe a entrada do usuário, atualiza os estados dos objetos do jogo e desenha os objetos do jogo.

Por exemplo, ter uma classe base GameObject com uma hierarquia profunda de herança pode ser bom para manutenção ... você pode fazer algo como o seguinte:

foreach(GameObject g in gameObjects) g.update();

No entanto, acho que essa abordagem pode criar problemas de desempenho.

Por outro lado, os dados e funções de todos os objetos do jogo podem ser globais. O que seria uma dor de cabeça para manutenção, mas pode estar mais próximo de um loop de jogo com desempenho ideal.

Alguma ideia? Estou interessado em aplicações práticas de estrutura de loop de jogo quase ideal ... mesmo que eu sofra dores de cabeça em manutenção em troca de um ótimo desempenho.

MrDatabase
fonte

Respostas:

45

Por exemplo, ter uma classe base GameObject com uma hierarquia profunda de herança pode ser bom para manutenção ...

Na verdade, hierarquias profundas geralmente são piores em termos de manutenção do que as rasas, e o estilo arquitetônico moderno para objetos de jogo está tendendo a abordagens rasas e baseadas em agregação .

No entanto, acho que essa abordagem pode criar problemas de desempenho. Por outro lado, os dados e funções de todos os objetos do jogo podem ser globais. O que seria uma dor de cabeça para manutenção, mas pode estar mais próximo de um loop de jogo com desempenho ideal.

O loop que você mostrou potencialmente tem problemas de desempenho, mas não, como está implícito na sua declaração subsequente, porque você tem dados de instância e funções de membro na GameObjectclasse. Em vez disso, o problema é que, se você tratar todos os objetos do jogo como exatamente o mesmo, provavelmente não estará agrupando esses objetos de maneira inteligente - eles provavelmente estarão espalhados aleatoriamente por toda a lista. Potencialmente, então, todas as chamadas para o método de atualização para esse objeto (se esse método é uma função global ou não, e se esse objeto possui dados de instância ou "dados globais" flutuando em alguma tabela na qual você está indexando ou o que seja) diferente da chamada de atualização nas últimas iterações do loop.

Isso pode aumentar a pressão no sistema, pois pode ser necessário paginar a memória com a função relevante dentro e fora da memória e preencher novamente o cache de instruções com mais freqüência, resultando em um loop mais lento. Se isso é ou não observado a olho nu (ou mesmo em um criador de perfil) depende exatamente do que é considerado um "objeto de jogo", quantos deles existem em média e o que mais está acontecendo no seu aplicativo.

Os sistemas de objetos orientados a componentes são uma tendência popular no momento, aproveitando a filosofia de que a agregação é preferível à herança . Esses sistemas permitem que você divida a lógica de "atualização" dos componentes (onde "componente" é definido aproximadamente como alguma unidade de funcionalidade, como a coisa que representa a parte fisicamente simulada de algum objeto, que é processada pelo sistema físico ) para vários encadeamentos - discriminados por tipo de componente - se possível e desejado, o que poderia ter um ganho de desempenho. No mínimo, você pode organizar os componentes de forma que todos os componentes de um determinado tipo sejam atualizados juntos , aproveitando ao máximo o cache da CPU. Um exemplo de um sistema orientado a componentes é discutido neste tópico .

Esses sistemas geralmente são altamente dissociados, o que também é um benefício para a manutenção.

O design orientado a dados é uma abordagem relacionada - trata-se de orientar-se em torno dos dados exigidos dos objetos como primeira prioridade, para que esses dados possam ser efetivamente processados ​​em massa (por exemplo). Isso geralmente significa uma organização que tenta manter os dados usados ​​para o mesmo objetivo em conjunto e operam de uma só vez. Não é fundamentalmente incompatível com o design do OO, e você pode encontrar alguma conversa sobre o assunto aqui no GDSE nesta pergunta .

Com efeito, uma abordagem mais ideal para o ciclo do jogo seria, em vez do seu original

foreach(GameObject g in gameObjects) g.update();

algo mais parecido

ProcessUserInput();
UpdatePhysicsForAllObjects();
UpdateScriptsForAllObjects();
UpdateRenderDataForAllObjects();
RenderEverything();

Em tal mundo, cada um GameObjectpode ter um ponteiro ou referência à sua própria PhysicsDataou Scriptou RenderData, para os casos em que você pode precisar interagir com os objetos em uma base individual, mas o real PhysicsData, Scripts, RenderData, et cetera seriam todos de propriedade de seus respectivos subsistemas (simulador de física, ambiente de hospedagem de scripts, renderizador) e processado em massa, conforme indicado acima.

É muito importante observar que essa abordagem não é uma bala mágica e nem sempre produz melhorias no desempenho (embora seja geralmente um design melhor do que uma árvore de herança profunda). Você provavelmente notará basicamente nenhuma diferença de desempenho se tiver muito poucos objetos ou muitos objetos para os quais não é possível paralelizar efetivamente as atualizações.

Infelizmente, não existe um loop mágico ideal - todos os jogos são diferentes e podem precisar de ajustes de desempenho de maneiras diferentes. Portanto, é muito importante medir as coisas (perfil) antes de você seguir cegamente o conselho de um cara aleatório na internet.

Comunidade
fonte
5
Bom Deus, esta é uma ótima resposta.
Raveline 6/02/11
2

O estilo de programação de objetos de jogo é bom para projetos menores. À medida que o projeto se torna realmente grande, você acaba com uma hierarquia profunda de herança e a base de objetos de jogo se torna realmente pesada!

Como um exemplo ... Suponha que você tenha começado a criar uma base de objetos de jogo com atributos mínimos, como Position (para xey), displayObject (isso se refere à imagem se for um elemento de desenho), largura, altura etc.

Agora, mais tarde, se precisarmos adicionar uma subclasse que terá um estado de animação, provavelmente você moverá 'código de controle de animação' (digamos currentAnimationId, Número de quadros para esta animação, etc.) para GameObjectBase, pensando que a maioria dos os objetos terão animação.
Posteriormente, se você quiser adicionar um corpo rígido que é estático (não possui animação), verifique o 'código de controle de animação' na função de atualização do GameObject Base (mesmo que seja uma verificação se a animação existe ou não). ..é importante!) Ao contrário disso, se você seguir a Estrutura baseada em componentes, você terá um 'Componente de controle de animação' anexado ao seu objeto.Se necessário, você o anexará.Para um corpo estático, você não anexará esse componente. Dessa forma, podemos evitar verificações desnecessárias.

Portanto, a seleção da escolha do estilo depende do tamanho do projeto. A estrutura GameObject é fácil de dominar para projetos menores. Espero que isso ajude um pouco.

Ayyappa
fonte
Josh Petrie - Realmente uma ótima resposta!
Ayyappa
@Josh Petrie - Realmente boa resposta com links úteis! (Sry não sabe como colocar esse comentário ao lado de seu post: P) #
Ayyappa
Você pode clicar no link "adicionar comentário" abaixo da minha resposta, normalmente, mas isso requer mais reputação do que a atual (consulte gamedev.stackexchange.com/privileges/comment ). E obrigada!