AIMBOT 2.0
No episódio 1 do Novo Jogo 2, por volta das 9h40, há uma cena do código que Nenê escreveu:
Aqui está em forma de texto com os comentários traduzidos:
// the calculation of damage when attacked void DestructibleActor::ReceiveDamage(float sourceDamage) { // apply debuffs auto resolvedDamage = sourceDamage; for (const auto& debuf:m_debufs) { resolvedDamage = debuf.ApplyToDamage(resolvedDamage); m_currentHealth -= resolvedDamage if (m_currentHealth <= 0.f) { m_currentHealth = 0.f; DestroyMe(); } } }
Após o tiro, Umiko, apontando para o loop for, disse que o motivo do travamento do código é que há um loop infinito.
Eu realmente não conheço C ++, então não tenho certeza se o que ela está dizendo é verdade.
Pelo que posso ver, o loop for está apenas iterando através dos debufs que o Actor possui atualmente. A menos que o Ator tenha uma quantidade infinita de debufs, não acho que possa se tornar um loop infinito.
Mas não tenho certeza, porque a única razão de haver uma cena do código é que eles queriam colocar um ovo de páscoa aqui, certo? Teríamos apenas tirado uma foto da parte de trás do laptop e ouvido Umiko dizer "Oh, você tem um loop infinito aí". O fato de que eles realmente mostraram algum código me faz pensar que de alguma forma o código é algum tipo de ovo de páscoa.
O código realmente criará um loop infinito?
8- Provavelmente útil: captura de tela adicional de Umiko dizendo que "Foi chamando a mesma operação uma e outra vez ", que pode não ser mostrado no código.
- Oh! Eu não sabia disso! @AkiTanaka o submarino que eu assisti diz "loop infinito"
- @LoganM Eu realmente não concordo. Não é apenas que OP tem uma dúvida sobre algum código-fonte que veio de um anime; A pergunta da OP é sobre uma declaração particular feita cerca de o código-fonte de um personagem no anime, e há uma resposta relacionada ao anime, ou seja, "Crunchyroll fez besteira e traduziu mal a linha".
- @senshin Acho que você está lendo sobre o que deseja que seja a pergunta, em vez do que é realmente perguntado. A questão fornece algum código-fonte e pergunta se ele gera um loop infinito como código C ++ da vida real. Novo jogo! é uma obra de ficção; não há necessidade de código apresentado nele para estar em conformidade com os padrões da vida real. O que Umiko diz sobre o código é mais confiável do que qualquer padrão ou compilador C ++. A primeira resposta (aceita) não menciona nenhuma informação do universo. Acho que uma pergunta sobre o assunto poderia ser feita sobre isso com uma boa resposta, mas, como formulado, não é.
O código não é um loop infinito, mas é um bug.
Existem dois (possivelmente três) problemas:
- Se nenhum debufs estiver presente, nenhum dano será aplicado
- Danos excessivos serão aplicados se houver mais de 1 debuf
- Se DestroyMe () excluir imediatamente o objeto e ainda houver m_debufs a serem processados, o loop estará executando sobre um objeto excluído e destruindo a memória. A maioria dos motores de jogo tem uma fila de destruição para contornar isso e muito mais, então isso pode não ser um problema.
A aplicação do dano deve ser fora do circuito.
Aqui está a função corrigida:
// the calculation of damage when attacked void DestructibleActor::ReceiveDamage(float sourceDamage) { // apply debuffs auto resolvedDamage = sourceDamage; for (const auto& debuf:m_debufs) { resolvedDamage = debuf.ApplyToDamage(resolvedDamage); } m_currentHealth -= resolvedDamage if (m_currentHealth <= 0.f) { m_currentHealth = 0.f; DestroyMe(); } }
12 - 15 Estamos em revisão de código? : D
- 4 flutuadores são ótimos para a saúde se você não passar de 16777216 HP. Você pode até definir a saúde para infinita para criar um inimigo que você pode atingir, mas não morrer, e ter um ataque de uma única morte usando dano infinito que ainda não matará um personagem HP infinito (o resultado de INF-INF é NaN), mas vai matar tudo o mais. Portanto, é muito útil.
- 1 @cat Por convenção em muitos padrões de codificação, o
m_
prefixo significa que é uma variável de membro. Neste caso, uma variável membro deDestructibleActor
. - 2 @HotelCalifornia Eu concordo que há uma pequena chance
ApplyToDamage
não funciona como esperado, mas no caso de exemplo que você dá, eu diriaApplyToDamage
Além disso precisa ser retrabalhado para exigir a passagem do originalsourceDamage
também para que possa calcular o debuf corretamente nesses casos. Para ser um pedante absoluto: neste ponto, a informação do dmg deve ser uma estrutura que inclui o dmg original, o dmg atual e a natureza do (s) dano (s) também se os debufs tiverem coisas como "vulnerabilidade ao fogo". Por experiência própria, não demora muito para que qualquer design de jogo com debufs exija isso. - 1 @StephaneHockenhull bem dito!
O código não parece criar um loop infinito.
A única maneira de o loop ser infinito seria se
debuf.ApplyToDamage(resolvedDamage);
ou
DestroyMe();
fossem adicionar novos itens ao m_debufs
recipiente.
Isso parece improvável. E se fosse o caso, o programa poderia travar por causa da alteração do contêiner durante a iteração.
O programa provavelmente travaria devido à chamada para DestroyMe();
que presumivelmente destrói o objeto atual que está executando o loop.
Podemos pensar nisso como um desenho animado em que o 'bandido' corta um galho para fazer o 'bom' cair com ele, mas percebe tarde demais que está do lado errado do corte. Ou a Cobra Midgaard comendo sua própria cauda.
Devo acrescentar também que o sintoma mais comum de um loop infinito é que ele congela o programa ou o torna inativo. Ele irá travar o programa se alocar memória repetidamente, ou fizer algo que acaba dividindo por zero, ou algo parecido.
Com base no comentário de Aki Tanaka,
Provavelmente útil: captura de tela adicional de Umiko dizendo que "Ele estava chamando a mesma operação várias vezes", o que pode não ser mostrado no código.
"Estava chamando a mesma operação repetidamente" Isso é mais provável.
Assumindo que DestroyMe();
não foi projetado para ser chamado mais de uma vez, é mais provável que cause um travamento.
Uma maneira de corrigir esse problema seria mudar o if
para algo assim:
if (m_currentHealth <= 0.f) { m_currentHealth = 0.f; DestroyMe(); break; }
Isso sairia do loop quando o DestructibleActor fosse destruído, certificando-se de que 1) o DestroyMe
método é chamado apenas uma vez e 2) não aplica buffs inutilmente uma vez que o objeto já é considerado morto.
- 1 Romper o loop for quando a integridade <= 0 é definitivamente uma solução melhor do que esperar até depois do loop para verificar a integridade.
- Eu acho que provavelmente
break
fora do circuito, e então chamarDestroyMe()
, apenas para estar seguro
Existem vários problemas com o código:
- Se não houver debufs, nenhum dano será sofrido.
DestroyMe()
o nome da função parece perigoso. Dependendo de como é implementado, pode ou não ser um problema. Se for apenas uma chamada ao destruidor do objeto atual envolvido em uma função, há um problema, pois o objeto seria destruído no meio da execução do código. Se for uma chamada para uma função que enfileira o evento de exclusão do objeto atual, então não há problema, pois o objeto seria destruído após completar sua execução e o loop de eventos iniciar.- O problema real que parece ser mencionado no anime, o "Ele estava chamando a mesma operação continuamente" - ele chamará
DestroyMe()
enquantom_currentHealth <= 0.f
e há mais debuffs restantes para iterar, o que pode resultar emDestroyMe()
sendo chamado várias vezes, uma e outra vez. O loop deve parar após o primeiroDestroyMe()
chamada, porque excluir um objeto mais de uma vez resulta em corrupção de memória, o que provavelmente resultará em uma falha no longo prazo.
Eu não tenho certeza porque cada debuf tira a saúde, ao invés da saúde ser tirada apenas uma vez, com os efeitos de todos os debuffs sendo aplicados no dano inicial recebido, mas vou assumir que essa é a lógica correta do jogo.
O código correto seria
// the calculation of damage when attacked void DestructibleActor::ReceiveDamage(float sourceDamage) { // apply debuffs auto resolvedDamage = sourceDamage; for (const auto& debuf:m_debufs) { resolvedDamage = debuf.ApplyToDamage(resolvedDamage); m_currentHealth -= resolvedDamage if (m_currentHealth <= 0.f) { m_currentHealth = 0.f; DestroyMe(); break; } } }
3 - Devo salientar que, como já escrevi alocadores de memória no passado, excluir a mesma memória não precisa ser um problema. Também pode ser redundante. Tudo depende do comportamento do alocador. A minha apenas agia como uma lista vinculada de baixo nível, de modo que o "nó" para os dados excluídos é definido como livre várias vezes ou excluído novamente várias vezes (o que corresponderia apenas a redirecionamentos de ponteiro redundantes). Boa pegada.
- Double-free é um bug e geralmente leva a um comportamento indefinido e travamentos. Mesmo se você tiver um alocador personalizado que de alguma forma proíba a reutilização do mesmo endereço de memória, o double-free é um código fedorento, pois não faz sentido e você receberá gritos de analisadores de código estático.
- É claro! Eu não o projetei para esse propósito. Alguns idiomas requerem apenas um alocador devido à falta de recursos. Não não não. Eu estava apenas afirmando que uma falha não é garantida. Certas classificações de design nem sempre falham.