Come posso garantire che un pezzo di codice venga eseguito una sola volta?


18

Ho del codice che voglio eseguire una sola volta, anche se le circostanze che attivano quel codice potrebbero accadere più volte.

Ad esempio, quando l'utente fa clic con il mouse, voglio fare clic sull'oggetto:

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

Tuttavia, con questo codice, ogni volta che faccio clic con il mouse, la cosa viene cliccata. Come posso farlo accadere una sola volta?


7
In realtà trovo questo tipo di divertente in quanto non penso che rientri in "problemi di programmazione specifici del gioco". Ma non mi è mai piaciuta molto quella regola.
ClassicThunder

3
@ClassicThunder Sì, è sicuramente più sul lato generale della programmazione delle cose. Vota per chiudere se vuoi, ho pubblicato di più la domanda in modo che avremmo qualcosa a cui puntare le persone quando fanno domande simili a questa. Può essere aperto o chiuso a tale scopo.
MichaelHouse

Risposte:


41

Usa una bandiera booleana.

Nell'esempio mostrato, dovresti modificare il codice in modo che sia simile al seguente:

//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();
    }
}

Inoltre, se si desidera poter ripetere l'azione, ma limitare la frequenza dell'azione (ovvero il tempo minimo tra ogni azione). Utilizzeresti un approccio simile, ma resetti la bandiera dopo un certo periodo di tempo. Vedi la mia risposta qui per ulteriori idee al riguardo.


1
La risposta ovvia alla tua domanda :-) Sarei interessato a vedere approcci alternativi se tu o qualcun altro ne venisse a conoscenza. Questo uso di un booleano globale per questo è sempre stato maleodorante per me.
Evorlor

1
@Evorlor Non ovvio per tutti :). Sono interessato anche a soluzioni alternative, forse possiamo imparare qualcosa di non così ovvio!
MichaelHouse

2
@Evorlor in alternativa, è possibile utilizzare i delegati (puntatori a funzione) e modificare l'azione eseguita. ad esempio, onStart() { delegate = doSomething; } ... if(mousebuttonpressed) { delegate.Execute(); }e void doSomething() { doStuff(); delegate = funcDoNothing; }ma alla fine tutte le opzioni finiscono per avere una bandiera di alcuni tipi si inserire / disinserire ... e delegato in questo caso non è altro (a meno che non si dispone di più di due opzioni che cosa fare, forse?).
Wondra,

2
@wondra Sì, è un modo. Aggiungilo Potremmo anche fare un elenco di possibili soluzioni su questa domanda.
MichaelHouse

1
@JamesSnell Più probabilmente se fosse multi-thread. Ma è comunque una buona pratica.
MichaelHouse

21

Se bool flag non è sufficiente o si desidera migliorare la leggibilità * del codice nel void Update()metodo, è possibile prendere in considerazione l' uso di delegati (puntatori a funzioni):

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(){}
}

Per semplice "eseguire una volta" i delegati sono eccessivi, quindi suggerirei invece di usare bool flag.
Tuttavia, se hai bisogno di funzionalità più complicate, i delegati sono probabilmente la scelta migliore. Ad esempio, se si desidera eseguire in sequenza azioni più diverse: una al primo clic, l'altra al secondo e un'altra al terzo, è possibile:

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

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

invece di affliggere il tuo codice con decine di bandiere diverse.
* a costo di minore manutenibilità del resto del codice


Ho fatto un po 'di profilazione con risultati praticamente come mi aspettavo:

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

Il primo test è stato eseguito su Unity 5.1.2, misurato con un System.Diagnostics.Stopwatchprogetto costruito a 32 bit (non in progettazione!). L'altro su Visual Studio 2015 (v140) compilato in modalità di rilascio a 32 bit con flag / Ox. Entrambi i test sono stati eseguiti su CPU Intel i5-4670K a 3,4 GHz, con 10.000.000 di iterazioni per ogni implementazione. codice:

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

conclusione: mentre il compilatore Unity fa un buon lavoro nell'ottimizzare le chiamate di funzione, fornendo all'incirca lo stesso risultato sia per i flag positivi che per i delegati (rispettivamente 21 e 25 ms), il misprediction del ramo o la chiamata di funzione è ancora piuttosto costoso (nota: il delegato dovrebbe essere assunto cache in questo test).
È interessante notare che il compilatore Unity non è abbastanza intelligente per ottimizzare il ramo quando ci sono 99 milioni di previsioni errate consecutive, quindi la negazione manuale del test produce un aumento delle prestazioni ottenendo il miglior risultato di 5 ms. La versione C ++ non mostra alcun aumento delle prestazioni per condizione negativa, tuttavia l'overhead complessivo della chiamata di funzione è significativamente inferiore.
soprattutto: la differenza è praticamente irrilevante per qualsiasi scenario del mondo reale


4
AFAIK, questo è meglio anche dal punto di vista delle prestazioni. Chiamare una funzione no-op, supponendo che la funzione sia già presente nella cache delle istruzioni, è molto più veloce del controllo dei flag, soprattutto quando quel controllo è nidificato in altri condizionali.
Ingegnere

2
Penso che Navin abbia ragione, è probabile che abbia prestazioni peggiori rispetto al controllo della bandiera booleana, a meno che la tua JIT non sia davvero intelligente e sostituisca l'intera funzione con letteralmente nulla. Ma a meno che non definiamo i risultati, non possiamo saperlo con certezza.
Wondra,

2
Hmm, questa risposta mi ricorda l' evoluzione di un ingegnere SW . Scherzo a parte, se hai assolutamente bisogno (e sei sicuro) di questo aumento delle prestazioni, provaci. Altrimenti, suggerirei di tenerlo BACIO e di usare una bandiera booleana, come proposto in un'altra risposta . Tuttavia, +1 per l'alternativa rappresentata qui!
mucaho,

1
Evita i condizionali ove possibile. Sto parlando di ciò che accade a livello di macchina, che sia il tuo codice nativo, un compilatore JIT o un interprete. L'hit di cache L1 è quasi uguale all'hit di registro, forse leggermente più lento, ovvero 1 o 2 cicli, mentre la cattiva reputazione del ramo, che si verifica meno spesso ma comunque troppo in media, costa nell'ordine di 10-20 cicli. Vedi la risposta di Jalf: stackoverflow.com/questions/289405/… E questo: stackoverflow.com/questions/11227809/…
Ingegnere

1
Ricorda inoltre che questi condizionali nella risposta di Byte56 devono essere chiamati ogni singolo aggiornamento per tutta la durata del tuo programma e potrebbero essere nidificati o avere nidificanti condizionali al loro interno, peggiorando le cose. Puntatori / delegati di funzioni sono la strada da percorrere.
Ingegnere

3

Per completezza

(In realtà non ti consiglio di farlo perché in realtà è abbastanza semplice scrivere qualcosa di simile if(!already_called), ma sarebbe "corretto" farlo.)

Non sorprende che lo standard C ++ 11 abbia identificato il problema piuttosto banale di chiamare una funzione una volta, rendendolo super esplicito:

#include <mutex>

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

}

Certo, la soluzione standard è in qualche modo superiore a quella banale in presenza di thread poiché garantisce sempre che avvenga sempre esattamente una chiamata, mai qualcosa di diverso.

Tuttavia, normalmente non stai eseguendo un loop di eventi multithread e premendo contemporaneamente i pulsanti di più mouse, quindi la sicurezza del thread è un po 'superflua per il tuo caso.

In termini pratici, significa che oltre all'inizializzazione thread-safe di un booleano locale (che C ++ 11 garantisce comunque di essere thread-safe), la versione standard deve anche eseguire un'operazione atomica test_set prima di chiamare o non chiamare la funzione.

Tuttavia, se ti piace essere esplicito, c'è la soluzione.

EDIT:
Huh. Ora che sono stato seduto qui, a fissare quel codice per alcuni minuti, sono quasi propenso a raccomandarlo.
In realtà non è così sciocco come potrebbe sembrare all'inizio, anzi comunica in modo molto chiaro e inequivocabile il tuo intento ... probabilmente meglio strutturato e più leggibile rispetto a qualsiasi altra soluzione che coinvolga una ifo tale.


2

Alcuni suggerimenti possono variare a seconda dell'architettura.

Crea clickThisThing () come puntatore a funzione / variante / DLL / so / etc ... e assicurati che sia inizializzato alla tua funzione richiesta quando l'oggetto / componente / modulo / etc ... viene istanziato.

Nel gestore ogni volta che si preme il pulsante del mouse, chiamare il puntatore della funzione clickThisThing () e quindi sostituire immediatamente il puntatore con un altro puntatore della funzione NOP (nessuna operazione).

Non c'è bisogno di flag e logica extra per qualcuno da rovinare in seguito, basta chiamarlo e sostituirlo ogni volta e poiché è un NOP il NOP non fa nulla e viene sostituito con un NOP.

Oppure potresti usare il flag e la logica per saltare la chiamata.

Oppure potresti disconnettere la funzione Update () dopo la chiamata, così il mondo esterno se ne dimentica e non la chiama mai più.

Oppure hai il clickThisThing () stesso utilizzare uno di questi concetti per rispondere con un lavoro utile solo una volta. In questo modo puoi usare un oggetto / componente / modulo clickThisThingOnce () di base e crearne un'istanza ovunque tu abbia avuto bisogno di questo comportamento e nessuno dei tuoi programmi di aggiornamento ha bisogno di una logica speciale ovunque.


2

Utilizzare puntatori o delegati di funzioni.

Non è necessario esaminare esplicitamente la variabile che contiene il puntatore a funzione finché non è necessario apportare una modifica. E quando non hai più bisogno di detta logica per funzionare, sostituisci con un riferimento a una funzione vuota / no-op. Confronta con fare condizionali controlla ogni frame per l'intera vita del tuo programma - anche se quel flag era necessario solo nei primi frame dopo l'avvio! - Impuro e inefficiente. (Il punto di Boreal sulla previsione è tuttavia riconosciuto a questo proposito.)

I condizionali nidificati, che spesso costituiscono questi, sono persino più costosi che dover controllare un singolo condizionale per ogni fotogramma, poiché in questo caso la predizione del ramo non può più fare la sua magia. Ciò significa ritardi regolari nell'ordine di 10 secondi di cicli - bancarelle di pipeline per eccellenza . L'annidamento può avvenire sopra o sotto questa bandiera booleana. Non ho quasi bisogno di ricordare ai lettori come possono diventare rapidamente complessi circuiti di gioco.

I puntatori a funzione esistono per questo motivo: usali!


1
Stiamo pensando sulla stessa linea. Il modo più efficiente sarebbe quello di disconnettere la cosa Update () da chiunque continui a chiamarla incessantemente una volta che ha fatto il lavoro, cosa molto comune nelle configurazioni di segnale + slot in stile Qt. L'OP non ha specificato l'ambiente di runtime, quindi siamo bloccati quando si tratta di ottimizzazioni specifiche.
Patrick Hughes,

1

In alcuni linguaggi di script come javascript o lua si può fare facilmente testando il riferimento della funzione. In 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

In un computer Von Neumann Architecture , la memoria è il luogo in cui sono memorizzati i dati e le istruzioni del programma. Se si desidera eseguire il codice una sola volta, è possibile che il codice nel metodo sovrascriva il metodo con NOP dopo che il metodo è completo. In questo modo se il metodo dovesse essere eseguito di nuovo, non accadrebbe nulla.


6
Questo romperà alcune architetture che scrivono proteggono la memoria del programma o vengono eseguite dalla ROM. Sarà anche possibile attivare avvisi antivirus casuali, a seconda di ciò che è installato.
Patrick Hughes,

0

In Python se hai intenzione di essere un coglione per altre persone nel progetto:

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

questo script dimostra il codice:

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

Ecco un altro contributo, è un po 'più generale / riutilizzabile, leggermente più leggibile e mantenibile, ma probabilmente meno efficiente di altre soluzioni.

Incapsuliamo la nostra logica in esecuzione una volta in una 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; }
}

Usandolo come nell'esempio fornito si presenta come segue:

Once clickTheThingOnce = new Once(ClickTheThing);

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

0

Puoi considerare di utilizzare statica una variabile.

void doOnce()
{
   static bool executed = false;

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

Puoi farlo nella ClickTheThing()stessa funzione o creare un'altra funzione e chiamare ClickTheThing()nel corpo if.

È simile all'idea di utilizzare un flag come suggerito in altre soluzioni, ma il flag è protetto da altri codici e non può essere modificato altrove poiché è locale alla funzione.


1
... ma questo ti impedisce di far eseguire il codice una volta 'per istanza di oggetto'. (Se questo è ciò di cui l'utente ha bisogno ..;))
Vaillancourt

0

In realtà ho riscontrato questo dilemma con l'input del controller Xbox. Anche se non ESATTAMENTE lo stesso è abbastanza dannatamente simile. È possibile modificare il codice nel mio esempio in base alle proprie esigenze.

Modifica: la tua situazione userebbe questo ->

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

E puoi imparare come creare una classe di input non elaborata tramite ->

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

Ma .. ora sull'algoritmo super fantastico ... non proprio, ma ehi .. è piuttosto bello :)

* Quindi ... possiamo memorizzare gli stati di ogni pulsante e quali sono premuti, rilasciati e tenuti giù !!! Possiamo anche controllare il tempo di attesa, ma ciò richiede una singola istruzione if e può controllare qualsiasi numero di pulsanti, ma ci sono alcune regole vedi sotto per queste informazioni.

Ovviamente se vogliamo controllare se qualcosa viene premuto, rilasciato, ecc. Dovresti fare "If (This) {}", ma questo sta mostrando come possiamo ottenere lo stato della stampa e quindi spegnerlo al prossimo frame in modo che il tuo " ismousepressed "sarà effettivamente falso al prossimo controllo.

Codice completo qui: https://github.com/JeremyDX/DX_B/blob/master/DX_B/XGameInput.cpp

Come funziona..

Quindi non sono sicuro dei valori che ricevi quando descrivi se un pulsante viene premuto o meno, ma fondamentalmente quando carico in XInput ottengo un valore di 16 bit tra 0 e 65535, questo ha 15 bit possibili stati per "Pressato".

Il problema era ogni volta che lo controllavo. Mi avrebbe semplicemente fornito lo stato attuale delle informazioni. Avevo bisogno di un modo per convertire lo stato corrente in Pressed, Release e Hold Values.

Quindi quello che ho fatto è il seguente.

Per prima cosa creiamo una variabile "CORRENTE". Ogni volta che controlliamo questi dati impostiamo "CORRENTE" su una variabile "PRECEDENTE" e quindi memorizziamo i nuovi dati su "Corrente" come mostrato qui ->

uint64_t LAST = CURRENT;
CURRENT = gamepad.wButtons;

Con queste informazioni ecco dove diventa eccitante !!

Ora possiamo capire se un pulsante viene TENUTO IN BASSO!

BUTTONS_HOLD = LAST & CURRENT;

Ciò che fa è fondamentalmente confronta i due valori e qualsiasi pressione dei pulsanti mostrati in entrambi rimarrà 1 e tutto il resto impostato su 0.

Vale a dire (1 | 2 | 4) & (2 | 4 | 8) produrrà (2 | 4).

Ora che abbiamo quali pulsanti sono "HELD" verso il basso. Possiamo ottenere il resto.

Premuto è semplice .. prendiamo il nostro stato "CORRENTE" e rimuoviamo qualsiasi pulsante premuto.

BUTTONS_PRESSED = CURRENT ^ BUTTONS_HOLD;

Rilasciato è lo stesso solo se lo confrontiamo con il nostro ULTIMO stato.

BUTTONS_RELEASED = LAST ^ BUTTONS_HOLD;

Quindi guardando la situazione Pressed. Se diciamo che attualmente abbiamo avuto 2 | 4 | 8 premuto. Abbiamo trovato che 2 | 4 dove tenuto. Quando rimuoviamo gli Held Bits ci rimangono solo 8. Questo è il bit appena premuto per questo ciclo.

Lo stesso può essere applicato per Rilasciato. In questo scenario "ULTIMO" è stato impostato su 1 | 2 | 4. Quindi quando rimuoviamo il 2 | 4 bit. Siamo rimasti con 1. Quindi il pulsante 1 è stato rilasciato dall'ultimo fotogramma.

Questo scenario di cui sopra è probabilmente la situazione più ideale che puoi offrire per il confronto dei bit e fornisce 3 livelli di dati senza istruzioni if ​​o per i loop con soli 3 calcoli di bit rapidi.

Volevo anche documentare i dati di conservazione, quindi anche se la mia situazione non è perfetta ... ciò che fa è fondamentalmente impostare le posizioni di attesa che vogliamo verificare.

Quindi ogni volta che impostiamo i nostri dati Stampa / Rilascio / Attesa controlliamo se i dati di attesa sono ancora uguali all'attuale controllo dei bit di attesa. In caso contrario, ripristiniamo l'ora corrente. Nel mio caso lo sto impostando su frame frame, quindi so per quanti frame è stato tenuto premuto.

L'aspetto negativo di questo approccio è che non riesco a ottenere singoli tempi di attesa, ma puoi controllare più bit contemporaneamente. Vale a dire se imposto il bit di mantenimento su 1 | 16 se 1 o 16 non vengono mantenuti, ciò fallirebbe. Quindi richiede che TUTTI quei pulsanti siano tenuti premuti per continuare a spuntare.

Quindi, se guardi nel codice, vedrai tutte le chiamate alle funzioni ordinate.

Quindi il tuo esempio verrebbe ridotto al semplice controllo della pressione di un pulsante e la pressione di un pulsante può avvenire una sola volta con questo algoritmo. Al prossimo controllo per premere non esisterà poiché non è possibile premere più di una volta che si dovrebbe rilasciare prima di poter premere di nuovo.


Quindi usi fondamentalmente un bitfield invece di un bool come indicato nelle altre risposte?
Vaillancourt

Sì praticamente lol ..
Jeremy Trifilo il

Può essere utilizzato per i suoi requisiti sebbene con la seguente struttura msdn "usButtonFlags" contenga un singolo valore di tutti gli stati dei pulsanti. docs.microsoft.com/en-us/windows/desktop/api/winuser/…
Jeremy Trifilo

Non sono sicuro da dove provenga la necessità di dire tutto ciò sui pulsanti del controller. Il contenuto principale della risposta è nascosto sotto un sacco di testo di distrazione.
Vaillancourt

Sì, stavo solo dando uno scenario personale correlato e cosa ho fatto per realizzarlo. Lo ripulirò più tardi, però, sicuramente e forse aggiungerò input da tastiera e mouse e darò un approccio a questo.
Jeremy Trifilo,

-3

Stupida soluzione economica ma potresti usare un ciclo for

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

Il codice nel loop verrà sempre eseguito quando viene eseguito il loop. Nulla all'interno impedisce che il codice venga eseguito una volta. Il loop o nessun loop dà esattamente lo stesso risultato.
Vaillancourt
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.