Como evitar que objetos de jogo se excluam acidentalmente em C ++

20

Digamos que meu jogo tenha um monstro que o kamikaze possa explodir no jogador. Vamos escolher um nome para esse monstro aleatoriamente: um Creeper. Portanto, a Creeperclasse tem um método que se parece com isso:

void Creeper::kamikaze() {
    EventSystem::postEvent(ENTITY_DEATH, this);

    Explosion* e = new Explosion;
    e->setLocation(this->location());
    this->world->addEntity(e);
}

Os eventos não estão na fila, eles são despachados imediatamente. Isso faz com que o Creeperobjeto seja excluído em algum lugar dentro da chamada para postEvent. Algo assim:

void World::handleEvent(int type, void* context) {
    if(type == ENTITY_DEATH){
        Entity* ent = dynamic_cast<Entity*>(context);
        removeEntity(ent);
        delete ent;
    }
}

Como o Creeperobjeto é excluído enquanto o kamikazemétodo ainda está em execução, ele falha quando tenta acessar this->location().

Uma solução é enfileirar os eventos em um buffer e enviá-los mais tarde. Essa é a solução comum em jogos em C ++? Parece um pouco complicado, mas isso pode ser apenas por causa da minha experiência com outros idiomas com diferentes práticas de gerenciamento de memória.

Em C ++, existe uma solução geral melhor para esse problema em que um objeto se exclui acidentalmente de dentro de um de seus métodos?

Tom Dalling
fonte
6
Como você chama postEvent no final do método kamikaze, em vez de no início?
Hackworth #
@ Hackworth que funcionaria para este exemplo específico, mas estou procurando uma solução mais geral. Quero poder postar eventos de qualquer lugar e não ter medo de causar falhas.
precisa saber é o seguinte
Você também pode dar uma olhada na implementação do autoreleaseObjective-C, onde as exclusões são suspensas até "apenas um pouco".
Chris Burt-Brown

Respostas:

40

Não apague this

Mesmo implicitamente.

- Sempre -

A exclusão de um objeto enquanto uma de suas funções membro ainda está na pilha está implorando por problemas. Qualquer arquitetura de código que resulte em que isso aconteça ("acidentalmente" ou não) é objetivamente ruim , perigosa e deve ser refatorada imediatamente . Nesse caso, se seu monstro puder chamar 'World :: handleEvent', não exclua, em nenhuma circunstância, o monstro dentro dessa função!

(Nesta situação específica, minha abordagem usual é fazer com que o monstro coloque uma bandeira 'morta' em si mesma e faça com que o objeto Mundo - ou algo parecido - teste essa bandeira 'morta' uma vez por quadro, removendo esses objetos. da lista de objetos no mundo e excluí-lo ou devolvê-lo a um pool de monstros ou o que for apropriado.Neste momento, o mundo também envia notificações sobre a exclusão, para que outros objetos no mundo saibam que o monstro tem parou de existir e pode soltar qualquer ponteiro que possa estar segurando.O mundo faz isso em um momento seguro, quando sabe que nenhum objeto está sendo processado no momento, para que você não precise se preocupar com o desenrolar da pilha a um ponto onde o ponteiro 'this' aponta para a memória liberada.)

Trevor Powell
fonte
6
A segunda metade da sua resposta entre parênteses foi útil. Buscar uma bandeira é uma boa solução. Mas eu estava perguntando como evitar fazê-lo, não se era bom ou ruim. Se uma pergunta for " Como evitar o X acidentalmente? ", E sua resposta for " Nunca faça o X, mesmo que acidentalmente" " em negrito, isso realmente não responde à pergunta.
precisa saber é o seguinte
7
Eu mantenho os comentários que fiz na primeira metade da minha resposta e sinto que eles respondem totalmente à pergunta como foi originalmente formulada. O ponto importante que repetirei aqui é que um objeto não se exclui. Ever . Ele não chama outra pessoa para excluí-lo. Ever . Em vez disso, você precisa ter algo mais, fora do objeto, que o possua e seja responsável por perceber quando o objeto precisa ser destruído. Isso não é coisa de "justo quando um monstro morre"; isso é para todo código C ++ sempre, em qualquer lugar, para sempre. Sem exceções.
21412 Trevor Powell
3
@TrevorPowell Não estou dizendo que você está errado. Na verdade, eu concordo com você. Só estou dizendo que na verdade não responde à pergunta que foi feita. É como se você me perguntasse " Como faço para obter áudio no meu jogo? " E minha resposta foi " Não acredito que você não tem áudio. Coloque o áudio no seu jogo agora. " Então, entre parênteses, na parte inferior, coloque " (Você pode usar o FMOD) ", que é uma resposta real.
precisa saber é o seguinte
6
@TrevorPowell É aqui que você está errado. Não é "apenas disciplina" se eu não conheço nenhuma alternativa. O código de exemplo que eu dei é puramente teórico. Eu já sei que é um design ruim, mas meu C ++ está enferrujado, então pensei em perguntar sobre projetos melhores aqui antes de realmente codificar o que quero. Então eu vim perguntar sobre projetos alternativos. " Adicionar um sinalizador de exclusão " é uma alternativa. " Nunca faça isso " não é uma alternativa. Está apenas me dizendo o que eu já sei. Parece que você escreveu a resposta sem ler a pergunta corretamente.
precisa saber é o seguinte
4
@Bobby A pergunta é "Como NÃO fazer X". Simplesmente dizer "NÃO faça X" é uma resposta inútil. Se a pergunta tivesse sido "eu estava fazendo X" ou "eu estava pensando em fazer X" ou qualquer outra variante disso, ela atenderia aos parâmetros da meta-discussão, mas não na forma atual.
Joshua Drake
21

Em vez de enfileirar eventos em um buffer, enfileire exclusões em um buffer. A exclusão tardia tem o potencial de simplificar massivamente a lógica; você pode realmente liberar memória no final ou no início de um quadro quando não sabe que nada de interessante está acontecendo com seus objetos e excluir de qualquer lugar.

John Calsbeek
fonte
11
Engraçado você mencionar isso, porque eu estava pensando em como seria legal o NSAutoreleasePoolObjective-C nessa situação. Pode ter que fazer um DeletionPoolcom modelos C ++ ou algo assim.
precisa saber é o seguinte
@TomDalling A única coisa a ter cuidado se você tornar o buffer externo ao objeto é que um objeto pode ser excluído por várias razões em um único quadro e é possível tentar excluí-lo várias vezes.
John Calsbeek
Muito verdadeiro. Vou ter que manter os ponteiros em um std :: set.
precisa saber é o seguinte
5
Em vez de um buffer de objetos a serem excluídos, você também pode definir um sinalizador no objeto. Depois que você começar a perceber o quanto deseja evitar chamar de novo ou excluir durante o tempo de execução e passar para um pool de objetos, isso será mais simples e rápido.
61112 Sean Middleditch
4

Em vez de permitir que o mundo lide com a exclusão, você pode permitir que a instância de outra classe sirva como um balde para armazenar todas as entidades excluídas. Essa instância específica deve escutar ENTITY_DEATHeventos e manipulá-los de modo que os enfileire. Em Worldseguida, é possível iterar essas instâncias e executar operações pós-morte após a renderização do quadro e 'limpar' esse bucket, que por sua vez executaria a exclusão real das instâncias das entidades.

Um exemplo dessa classe seria assim: http://ideone.com/7Upza

Vite Falcon
fonte
+1, é uma alternativa para sinalizar diretamente as entidades. Mais diretamente, basta ter uma lista ativa e uma lista morta de entidades diretamente na Worldclasse.
Laurent Couvidou 06/04/12
2

Eu sugeriria a implementação de uma fábrica usada para todas as alocações de objetos do jogo. Então, em vez de chamar de novo, você diria à fábrica para criar algo para você.

Por exemplo

Object* o = gFactory->Create("Explosion");

Sempre que você deseja excluir um objeto, a fábrica empurra o objeto em um buffer que é limpo no próximo quadro. Destruição atrasada é muito importante na maioria dos cenários.

Considere também enviar todas as mensagens com um atraso de um quadro. Existem apenas algumas exceções para as quais você precisa enviar imediatamente, que a grande maioria dos casos, no entanto, apenas

Millianz
fonte
2

Você pode implementar a memória gerenciada em C ++, para que, quando ENTITY_DEATH chamado, tudo o que acontece é que o número de suas referências seja reduzido em um.

Mais tarde, como o @John sugeriu no início de cada quadro, você pode verificar quais entidades são inúteis (aquelas com zero referências) e excluí-las. Por exemplo, você pode usar boost::shared_ptr<T>( documentado aqui ) ou se estiver usando C ++ 11 (VC2010)std::tr1::shared_ptr<T>

Ali1S232
fonte
Apenas std::shared_ptr<T>relatórios não técnicos! - Você precisará especificar um deleter personalizado, caso contrário, ele também excluirá o objeto imediatamente quando a contagem de referência chegar a zero.
precisa saber é o seguinte
11
@leftaroundabout realmente depende, pelo menos eu precisava usar o tr1 no gcc. mas no VC não havia necessidade disso.
Ali1S232
2

Use pool e, na verdade, não exclua objetos. Em vez disso, altere a estrutura de dados em que estão registrados. Por exemplo, para renderizar objetos, há um objeto de cena e todas as entidades de alguma forma registradas para renderização, detecção de colisão etc. Em vez de excluir o objeto, desanexe-o da cena e insira-o em um conjunto de objetos mortos. Este método não apenas evita problemas de memória (como um objeto que se apaga), mas também pode acelerar o jogo se você usar o pool corretamente.

hevi
fonte
1

O que fizemos em um jogo foi usar o posicionamento new

SomeEvent* obj = new(eventPool.alloc()) new SomeEvent();

o eventPool era apenas uma grande matriz de memória que foi dividida e o ponteiro para cada segmento foi armazenado. Portanto, o assign () retornaria o endereço de um bloco de memória livre. No eventPool, a memória era tratada como uma pilha; portanto, após todos os eventos terem sido enviados, redefiníamos o ponteiro da pilha de volta ao início da matriz.

Por causa do funcionamento do nosso sistema de eventos, não precisamos chamar os destruidores dos eventos. Portanto, o pool simplesmente registraria o bloco de memória como livre e o alocaria.

Isso nos deu uma enorme velocidade.

Além disso ... Na verdade, usamos pools de memória para todos os objetos alocados dinamicamente no desenvolvimento, pois era uma ótima maneira de encontrar vazamentos de memória, se ainda houvesse algum objeto nos pools quando o jogo terminava (normalmente), provavelmente havia um vazamento de mem.

PhilCK
fonte