Como garantir que um pedaço de código seja executado apenas uma vez?


18

Eu tenho um código que quero executar apenas uma vez, mesmo que as circunstâncias que acionam esse código possam ocorrer várias vezes.

Por exemplo, quando o usuário clica no mouse, desejo clicar na coisa:

void Update() {
    if(mousebuttonpressed) {
        ClickTheThing(); // I only want this to happen on the first click,
                         // then never again.
    }
}

No entanto, com esse código, toda vez que clico no mouse, a coisa é clicada. Como posso fazer isso acontecer apenas uma vez?


7
Na verdade, acho isso meio engraçado, pois não acho que se enquadre em "problemas de programação específicos do jogo". Mas nunca gostei muito dessa regra.
precisa

3
@ClassicThunder Sim, certamente é mais do lado da programação geral. Vote para fechar, se quiser, eu postei a pergunta mais para que tenhamos algo para apontar as pessoas quando elas fizerem perguntas semelhantes a essa. Pode ser aberto ou fechado para esse fim.
MichaelHouse

Respostas:


41

Use uma bandeira booleana.

No exemplo mostrado, você modificaria o código para algo como o seguinte:

//a boolean flag that lets us "remember" if this thing has happened already
bool thatThingHappened = false;

void Update() {
    if(!thatThingHappened && mousebuttonpressed) {
        //if that thing hasn't happened yet and the mouse is pressed
        thatThingHappened = true;
        ClickTheThing();
    }
}

Além disso, se você quiser repetir a ação, mas limite a frequência da ação (ou seja, o tempo mínimo entre cada ação). Você usaria uma abordagem semelhante, mas redefinir a bandeira após um certo período de tempo. Veja minha resposta aqui para mais idéias sobre isso.


11
A resposta óbvia à sua pergunta :-) Eu estaria interessado em ver abordagens alternativas se você ou qualquer outra pessoa souber de alguma. Esse uso de um booleano global para isso sempre foi ruim para mim.
Evorlor 25/03

11
@Evorlor Não é óbvio para todos :). Também estou interessado em soluções alternativas, talvez possamos aprender algo não tão óbvio!
Michaelhouse

2
@Evorlor como alternativa, você pode usar delegados (ponteiros de função) e alterar a ação que é feita. por exemplo, onStart() { delegate = doSomething; } ... if(mousebuttonpressed) { delegate.Execute(); }e void doSomething() { doStuff(); delegate = funcDoNothing; }no final, todas as opções acabam tendo uma bandeira de algum tipo que você define / desativa ... e delegar nesse caso não é mais nada (exceto se você tiver mais de duas opções, o que fazer, talvez?).
27516 Maroc

2
@wondra Sim, é assim. Adicione. Poderíamos também fazer uma lista de possíveis soluções para essa questão.
MichaelHouse

11
@JamesSnell Provavelmente, se fosse multiencadeado. Mas ainda é uma boa prática.
Michaelhouse

21

Se o sinalizador bool não for suficiente ou você quiser melhorar a legibilidade * do código no void Update()método, considere o uso de delegados (ponteiros de função):

public class InputController 
{
  //declare delegate type:
  //<accessbility> delegate <return type> <name> ( <parameter1>, <paramteter2>, ...)
  public delegate void ClickAction();

  //declare a variable of that type
  public ClickAction ClickTheThing { get; set; }

  void onStart()
  {
    //assign it the method you wish to execute on first click
    ClickTheThing = doStuff;
  }

  void Update() {
    if(mousebuttonpressed) {
        //you simply call the delegate here, the currently assigned function will be executed
        ClickTheThing();
     }
  }

  private void doStuff()
  {
    //some logic here
    ClickTheThing = doNothing; //and set the delegate to other(empty) funtion
  }

  //or do something else
  private void doNothing(){}
}

Para simples "executar uma vez" delegados são um exagero, então eu sugiro usar o sinalizador bool.
No entanto, se você precisar de funcionalidades mais complicadas, os delegados provavelmente são a melhor opção. Por exemplo, se você deseja executar em cadeia mais ações diferentes: uma no primeiro clique, outra no segundo e mais uma no terceiro, você pode simplesmente fazer:

func1() 
{
  //do logic 1 here
  someDelegate = func2;
}

func2() 
{
  //do logic 2 here
  someDelegate = func3;
}
//etc...

em vez de atormentar seu código com dezenas de sinalizadores diferentes.
* ao custo de menor manutenção do restante do código


Fiz alguns perfis com os resultados exatamente como eu esperava:

----------------------------------------
|     Method     |  Unity   |    C++   |
| -------------------------------------|
| positive flag  |  21 ms   |   6 ms   |
| negative flag  |  5 ms    |   7 ms   |
| delegate       |  25 ms   |   14 ms  |
----------------------------------------

O primeiro teste foi executado no Unity 5.1.2, medido com System.Diagnostics.Stopwatchum projeto construído de 32 bits (não no designer!). O outro no Visual Studio 2015 (v140) compilado no modo de liberação de 32 bits com o sinalizador / Ox. Ambos os testes foram executados na CPU Intel i5-4670K a 3.4GHz, com 10.000.000 de iterações para cada implementação. código:

//positive flag
if(flag){
    doClick();
    flag = false;
}
//negative flag
if(!flag){ }
else {
    doClick();
    flag = false;
}
//delegate
action();

Conclusão: Enquanto o compilador Unity faz um bom trabalho ao otimizar chamadas de função, fornecendo aproximadamente o mesmo resultado para sinalizadores positivos e delegados (21 e 25 ms respectivamente), a previsão incorreta do ramo ou a chamada de função ainda é bastante cara (nota: delegado deve ser considerado cache neste teste).
Curiosamente, o compilador Unity não é inteligente o suficiente para otimizar a ramificação quando houver 99 milhões de previsões erradas consecutivas; portanto, a negação manual do teste gera algum aumento de desempenho, oferecendo o melhor resultado de 5 ms. A versão C ++ não mostra nenhum aumento de desempenho para negar a condição, no entanto, a sobrecarga geral da chamada de função é significativamente menor.
mais importante: a diferença é praticamente irrelevante para qualquer cenário do mundo real


4
AFAIK, isso também é melhor do ponto de vista do desempenho. Chamar uma função no-op, assumindo que a função já esteja no cache de instruções, é muito mais rápido que marcar sinalizadores, especialmente quando essa verificação está aninhada sob outros condicionais.
Engenheiro

2
Acho que Navin está certo, é provável que tenha um desempenho pior do que verificar a flag booleana - a menos que seu JIT seja realmente inteligente e substitua toda a função por literalmente nada. Mas, a menos que analisemos os resultados, não podemos ter certeza.
27416 Marlin

2
Hmm, essa resposta me lembra a evolução de um engenheiro de SW . Brincadeira à parte, se você absolutamente precisa (e tem certeza) desse aumento de desempenho, vá em frente. Caso contrário, sugiro mantê-lo no KISS e usar um sinalizador booleano, como proposto em outra resposta . No entanto, +1 para a alternativa representada aqui!
Mucaho 26/03/16

11
Evite condicionais sempre que possível. Estou falando do que acontece no nível da máquina, seja esse o seu próprio código nativo, um compilador JIT ou um intérprete. O acerto no cache L1 é quase o mesmo que o acerto no registro, possivelmente um pouco mais lento, ou seja, 1 ou 2 ciclos, enquanto a previsão incorreta de ramificação, que acontece com menos frequência, mas ainda em demasia, em média, custa da ordem de 10 a 20 ciclos. Veja a resposta de Jalf: stackoverflow.com/questions/289405/… E isso: stackoverflow.com/questions/11227809/…
Engineer

11
Além disso, lembre-se de que esses condicionais na resposta do Byte56 devem ser chamados a cada atualização durante toda a vida útil do seu programa e podem muito bem ser aninhados ou ter condicionais aninhados dentro deles, tornando as coisas muito piores. Indicadores de função / delegados são o caminho a percorrer.
Engenheiro

3

Para completar

(Na verdade, não recomendo que você faça isso, pois é bastante simples escrever algo como if(!already_called), mas seria "correto" fazê-lo.)

Sem surpresa, o padrão C ++ 11 identificou o problema bastante trivial de chamar uma função uma vez e a tornou super explícita:

#include <mutex>

void Update() {
if(mousebuttonpressed) {
    static std::once_flag f;
    std::call_once(f, ClickTheThing);
}

}

É certo que a solução padrão é um pouco superior à trivial na presença de threads, pois ainda garante que sempre exatamente uma chamada acontece, nunca algo diferente.

No entanto, você normalmente não está executando um loop de eventos multithread e pressionando os botões de vários mouse ao mesmo tempo, para que a segurança do thread seja um pouco supérflua para o seu caso.

Em termos práticos, isso significa que, além da inicialização segura por thread de um booleano local (que o C ++ 11 garante a segurança de thread de qualquer maneira), a versão padrão também deve executar uma operação atômica test_set antes de chamar ou não chamar a função.

Ainda assim, se você gosta de ser explícito, existe a solução.

EDIT:
Huh. Agora que estou sentado aqui, encarando o código por alguns minutos, estou quase inclinado a recomendá-lo.
Na verdade, não é tão tolo quanto pode parecer à primeira vista; na verdade, comunica muito clara e inequivocamente sua intenção ... indiscutivelmente melhor estruturada e mais legível do que qualquer outra solução que envolva uma ifou outra .


2

Algumas sugestões variam dependendo da arquitetura.

Crie clickThisThing () como um ponteiro de função / variante / DLL / so / etc ... e verifique se ele é inicializado com a função necessária quando o objeto / componente / módulo / etc ... é instanciado.

No manipulador, sempre que o botão do mouse é pressionado, chame o ponteiro da função clickThisThing () e substitua imediatamente o ponteiro por outro ponteiro da função NOP (sem operação).

Não há necessidade de sinalizadores e lógica extra para alguém estragar mais tarde, basta chamá-lo e substituí-lo todas as vezes e, como é um NOP, o NOP não faz nada e é substituído por um NOP.

Ou você pode usar o sinalizador e a lógica para pular a chamada.

Ou você pode desconectar a função Update () após a chamada para que o mundo exterior a esqueça e nunca mais a chame.

Ou você tem o próprio clickThisThing () usando um desses conceitos para responder apenas com trabalho útil uma vez. Dessa forma, você pode usar um objeto / componente / módulo clickThisThingOnce () da linha de base e instancia-lo em qualquer lugar que precise desse comportamento e nenhum dos seus atualizadores precisa de lógica especial em todo o lugar.


2

Use ponteiros de função ou delegados.

A variável que contém o ponteiro da função não precisa ser explicitamente examinada até que uma alteração precise ser feita. E quando você não precisar mais da lógica para executar, substitua por ref para uma função vazia / sem operação. Compare with doing condicionals verifica todos os quadros durante toda a vida útil do seu programa - mesmo se esse sinalizador fosse necessário apenas nos primeiros quadros após a inicialização! - Imundo e ineficiente. (O ponto de vista de Boreal sobre previsão é, no entanto, reconhecido a esse respeito.)

As condicionais aninhadas, que muitas vezes constituem, são ainda mais caras do que ter que verificar uma única condicional a cada quadro, pois a previsão de ramificação não pode mais fazer sua mágica nesse caso. Isso significa atrasos regulares da ordem de 10s de ciclos - barragens de oleodutos por excelência . O aninhamento pode ocorrer acima ou abaixo desse sinalizador booleano. Eu quase não preciso lembrar aos leitores como os loops de jogos complexos podem se tornar rapidamente.

Existem indicadores de função por esse motivo - use-os!


11
Estamos pensando na mesma linha. A maneira mais eficiente seria desconectar a coisa Update () de quem continua chamando incansavelmente depois que o trabalho é feito, o que é muito comum nas configurações de sinal + slot no estilo Qt. O OP não especificou o ambiente de tempo de execução, por isso estamos paralisados ​​quando se trata de otimizações específicas.
Patrick Hughes

1

Em algumas linguagens de script, como javascript ou lua, pode ser feito facilmente testando a referência de função. Em Lua ( love2d ):

function ClickTheThing()
      .......
end
....

local ctt = ClickTheThing  --I make a local reference to function

.....

function update(dt)
..
    if ctt then
      ctt()
      ctt=nil
    end
..
end

0

Em um computador com arquitetura Von Neumann Architecture , a memória é onde as instruções e os dados do seu programa são armazenados. Se você deseja que o código seja executado apenas uma vez, é possível que o código substitua o método pelos NOPs depois que o método for concluído. Dessa forma, se o método fosse executado novamente, nada aconteceria.


6
Isso interromperá algumas arquiteturas que gravam protegem a memória do programa ou são executadas a partir da ROM. Ele também acionará avisos aleatórios de antivírus, dependendo do que estiver instalado.
Patrick Hughes

0

Em python, se você planeja ser um idiota para outras pessoas no projeto:

code = compile('main code'+'del code', '<string>', 'exec')
exec code

este script demonstra o código:

from time import time as t

code = compile('del code', '<string>', 'exec')

print 'see code object:'
print code

while True:
    try:
        user = compile(raw_input('play with it (variable name is code) (type done to be done:\t'), '<user_input>', 'exec')
        exec user
    except BaseException as e:
        print e
        break

exec code

print 'no more code object:'
try:
    print code
except BaseException as e:
    print e

while True:
    try:
        user = compile(raw_input('try to play with it again (type done to be done:\t'), '<user_input>', 'exec')
        exec user
    except BaseException as e:
        print e
        break

0

Aqui está outra contribuição: é um pouco mais geral / reutilizável, um pouco mais legível e sustentável, mas provavelmente menos eficiente do que outras soluções.

Vamos encapsular nossa lógica de execução única em uma classe, Once:

class Once
{
    private Action body;
    public bool HasBeenCalled { get; private set; }
    public Once(Action body)
    {
        this.body = body;
    }
    public void Call()
    {
        if (!HasBeenCalled)
        {
            body();
        }
        HasBeenCalled = true;
    }
    public void Reset() { HasBeenCalled = false; }
}

Usá-lo como o exemplo fornecido é o seguinte:

Once clickTheThingOnce = new Once(ClickTheThing);

void Update() {
    if(mousebuttonpressed) {
        clickTheThingOnce.Call(); // ClickTheThing is only called once
                                  // or until .Reset() is called
    }
}

0

Você pode considerar o uso de uma variável estática.

void doOnce()
{
   static bool executed = false;

   if(executed == false)
   {
      ...Your code here
      executed = true;
   }
}

Você pode fazer isso na ClickTheThing()própria função ou criar outra função e chamar ClickTheThing()o corpo if.

É semelhante à idéia de usar um sinalizador, como sugerido em outras soluções, mas o sinalizador está protegido contra outro código e não pode ser alterado em nenhum outro lugar devido ao local da função.


11
... mas isso impede que o código seja executado uma vez 'por instância do objeto'. (Se é isso que o usuário precisa ..;))
Vaillancourt

0

Na verdade, me deparei com esse dilema com a entrada do Xbox Controller. Embora não seja exatamente o mesmo, é bem parecido. Você pode alterar o código no meu exemplo para atender às suas necessidades.

Edit: Sua situação usaria isso ->

https://docs.microsoft.com/en-us/windows/desktop/api/winuser/ns-winuser-tagrawmouse

E você pode aprender como criar uma classe de entrada bruta via ->

https://docs.microsoft.com/en-us/windows/desktop/inputdev/raw-input

Mas .. agora no algoritmo super impressionante ... não realmente, mas ei .. é muito legal :)

* Então ... podemos armazenar os estados de cada botão e quais são Pressionados, Liberados e Retidos !!! Também podemos verificar o tempo de espera, mas isso requer uma única declaração if e pode verificar qualquer número de botões, mas existem algumas regras, veja abaixo para obter essas informações.

Obviamente, se quisermos verificar se alguma coisa foi pressionada, liberada, etc. você faria "If (This) {}", mas isso está mostrando como podemos obter o estado da impressora e depois desligá-lo no próximo quadro para que você " ismousepressed "será realmente falso na próxima vez que você verificar.

Código completo aqui: https://github.com/JeremyDX/DX_B/blob/master/DX_B/XGameInput.cpp

Como funciona..

Portanto, não tenho certeza dos valores que você recebe quando mostra se um botão é pressionado ou não, mas basicamente quando carrego no XInput, recebo um valor de 16 bits entre 0 e 65535, que possui estados possíveis de 15 bits para "Pressed".

O problema era que toda vez que eu checava isso, simplesmente me informava o estado atual das informações. Eu precisava de uma maneira de converter o estado atual em valores pressionados, liberados e retidos.

Então, o que eu fiz é o seguinte.

Primeiro, criamos uma variável "CURRENT". Toda vez que verificamos esses dados, configuramos o "CURRENT" como uma variável "ANTERIOR" e, em seguida, armazenamos os novos dados em "Atual", como visto aqui ->

uint64_t LAST = CURRENT;
CURRENT = gamepad.wButtons;

Com essas informações, é aqui que fica emocionante !!

Agora podemos descobrir se um botão está sendo pressionado!

BUTTONS_HOLD = LAST & CURRENT;

O que isso faz é basicamente comparar os dois valores e qualquer pressionamento de botão mostrado em ambos permanecerá 1 e todo o resto definido como 0.

Ou seja, (1 | 2 | 4) e (2 | 4 | 8) produzirão (2 | 4).

Agora que temos quais botões estão "pressionados". Nós podemos descansar.

Pressionado é simples. Pegamos nosso estado "ATUAL" e removemos todos os botões pressionados.

BUTTONS_PRESSED = CURRENT ^ BUTTONS_HOLD;

Liberado é o mesmo, apenas o comparamos ao nosso último estado.

BUTTONS_RELEASED = LAST ^ BUTTONS_HOLD;

Então, olhando para a situação Pressed. Se vamos dizer que atualmente tivemos 2 | 4 8 pressionado. Descobrimos que 2 | 4 onde realizada. Quando removemos os Bits Retidos, ficamos com apenas 8. Este é o bit recém-pressionado para este ciclo.

O mesmo pode ser aplicado para Liberado. Nesse cenário "ÚLTIMO" foi definido como 1 | 2 4. Então, quando removemos o 2 | 4 bits. Ficamos com 1. Portanto, o botão 1 foi liberado desde o último quadro.

Esse cenário acima é provavelmente a situação mais ideal que você pode oferecer para comparação de bits e fornece 3 níveis de dados sem instruções if ou para loops apenas 3 cálculos rápidos de bits.

Eu também queria documentar dados de retenção, por isso, embora minha situação não seja perfeita ... o que faz é basicamente definir os valores de retenção que queremos verificar.

Portanto, toda vez que configuramos nossos dados de Imprensa / Liberação / Espera, verificamos se os dados de retenção ainda são iguais à atual verificação de bits de retenção. Caso contrário, redefinimos a hora para a hora atual. No meu caso, estou configurando-o para índices de quadros, para que eu saiba quantos quadros foram retidos.

A desvantagem dessa abordagem é que não posso obter tempos de espera individuais, mas você pode verificar vários bits ao mesmo tempo. Ou seja, se eu definir o bit de espera como 1 | 16 se 1 ou 16 não forem mantidos, isso falhará. Portanto, é necessário que TODOS esses botões sejam pressionados para continuar marcando.

Então, se você olhar no código, verá todas as chamadas de função.

Portanto, seu exemplo seria reduzido a simplesmente verificar se um pressionamento de botão ocorreu e um pressionamento de botão pode ocorrer apenas uma vez com esse algoritmo. Na próxima verificação para pressionar, ele não existirá, pois você não pode pressionar mais do que uma vez, pois seria necessário soltar antes de pressionar novamente.


Então você basicamente usa um campo de bits em vez de um bool, como indicado nas outras respostas?
Vaillancourt

Sim, praticamente lol ..
Jeremy Trifilo 09/01

Ele pode ser usado para os requisitos dele, embora com a estrutura msdn abaixo, o "usButtonFlags" contenha um valor único de todos os estados do botão. docs.microsoft.com/en-us/windows/desktop/api/winuser/…
Jeremy Trifilo

Não sei de onde vem a necessidade de dizer tudo isso sobre os botões do controlador. O conteúdo principal da resposta está oculto embaixo de muitos textos perturbadores.
Vaillancourt

Sim, eu estava apenas dando um cenário pessoal relacionado e o que fiz para realizá-lo. Vou limpá-lo mais tarde, com certeza, e talvez adicione entradas de teclado e mouse e forneça uma abordagem para isso.
Jeremy Trifilo

-3

Saída barata estúpida, mas você pode usar um loop for

for (int i = 0; i < 1; i++)
{
    //code here
}

O código no loop sempre será executado quando o loop for executado. Nada lá está impedindo que o código seja executado uma vez. O loop ou nenhum loop fornece exatamente o mesmo resultado.
Vaillancourt
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.