Comment puis-je m'assurer qu'un morceau de code ne s'exécute qu'une seule fois?


18

J'ai du code que je ne veux exécuter qu'une seule fois, même si les circonstances qui déclenchent ce code peuvent se produire plusieurs fois.

Par exemple, lorsque l'utilisateur clique sur la souris, je veux cliquer sur la chose:

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

Cependant, avec ce code, chaque fois que je clique sur la souris, la chose est cliquée. Comment puis-je y arriver une seule fois?


7
En fait, je trouve cela un peu drôle, car je ne pense pas que cela relève des "problèmes de programmation spécifiques au jeu". Mais je n'ai jamais vraiment aimé cette règle.
ClassicThunder

3
@ClassicThunder Oui, c'est certainement plus du côté de la programmation générale. Votez pour fermer si vous le souhaitez, j'ai plus posé la question afin que nous ayons quelque chose à indiquer aux gens lorsqu'ils posent des questions similaires à cela. Il peut être ouvert ou fermé à cet effet.
MichaelHouse

Réponses:


41

Utilisez un drapeau booléen.

Dans l'exemple illustré, vous modifieriez le code pour qu'il ressemble à ce qui suit:

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

De plus, si vous voulez pouvoir répéter l'action, mais limitez la fréquence de l'action (c'est-à-dire le temps minimum entre chaque action). Vous utiliseriez une approche similaire, mais réinitialiseriez le drapeau après un certain temps. Voir ma réponse ici pour plus d'idées à ce sujet.


1
La réponse évidente à votre question :-) Je serais intéressé de voir des approches alternatives si vous ou quelqu'un d'autre en connaissez. Cette utilisation d'un booléen global pour cela a toujours été malodorante pour moi.
Evorlor

1
@Evorlor Pas évident pour tout le monde :). Je suis également intéressé par des solutions alternatives, peut-être que nous pouvons apprendre quelque chose de pas si évident!
MichaelHouse

2
@Evorlor comme alternative, vous pouvez utiliser des délégués (pointeurs de fonction) et modifier l'action effectuée. par exemple onStart() { delegate = doSomething; } ... if(mousebuttonpressed) { delegate.Execute(); }et void doSomething() { doStuff(); delegate = funcDoNothing; }mais à la fin toutes les options finissent par avoir un drapeau d'une sorte que vous définissez / désélectionnez ... et déléguer dans ce cas n'est rien d'autre (sauf si vous avez plus de deux options que faire peut-être?).
wonderra

2
@wondra Ouais, c'est une façon. Ajoutez-le. Nous pourrions aussi bien dresser une liste de solutions possibles sur cette question.
MichaelHouse

1
@JamesSnell Plus probable s'il était multi-thread. Mais toujours une bonne pratique.
MichaelHouse

21

Si le drapeau booléen ne suffit pas ou si vous souhaitez améliorer la lisibilité * du code dans la void Update()méthode, vous pouvez envisager d' utiliser des délégués (pointeurs de fonction):

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

Pour les délégués "exécuter une seule fois" simples sont exagérés, je suggère donc d'utiliser plutôt le drapeau booléen.
Cependant, si vous aviez besoin de fonctionnalités plus compliquées, les délégués sont probablement un meilleur choix. Par exemple, si vous vouliez exécuter plusieurs actions différentes en chaîne: une au premier clic, une autre au deuxième et une autre au troisième, vous pourriez simplement faire:

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

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

au lieu de tourmenter votre code avec des dizaines de drapeaux différents.
* au prix d'une maintenance réduite du reste du code


J'ai fait un profilage avec des résultats à peu près comme je m'y attendais:

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

Le premier test a été exécuté sur Unity 5.1.2, mesuré avec System.Diagnostics.Stopwatch un projet construit en 32 bits (pas dans le concepteur!). L'autre sur Visual Studio 2015 (v140) compilé en mode de sortie 32 bits avec l'indicateur / Ox. Les deux tests ont été exécutés sur un processeur Intel i5-4670K à 3,4 GHz, avec 10 000 000 d'itérations pour chaque implémentation. code:

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

conclusion: Alors que le compilateur Unity fait un bon travail lors de l'optimisation des appels de fonction, donnant à peu près le même résultat pour l'indicateur positif et les délégués (21 et 25 ms respectivement), la mauvaise prédiction de branche ou l'appel de fonction est encore assez cher (remarque: le délégué doit être supposé en cache dans ce test).
Fait intéressant, le compilateur Unity n'est pas assez intelligent pour optimiser la branche lorsqu'il y a 99 millions d'erreurs consécutives, de sorte que l'annulation manuelle du test donne une amélioration des performances donnant le meilleur résultat de 5 ms. La version C ++ ne montre aucune amélioration des performances pour la condition de négation, mais la surcharge globale de l'appel de fonction est nettement inférieure.
le plus important: la différence est à peu près hors de propos pour tout scénario du monde réel


4
AFAIK, c'est encore mieux du point de vue des performances. L'appel d'une fonction no-op, en supposant que cette fonction est déjà dans le cache d'instructions, est beaucoup plus rapide que la vérification des indicateurs, en particulier lorsque cette vérification est imbriquée sous d' autres conditions.
Ingénieur

2
Je pense que Navin a raison, je pense qu'il aura probablement de moins bonnes performances que de vérifier le drapeau booléen - à moins que votre JIT ne soit vraiment intelligent et remplace toute la fonction par littéralement rien. Mais à moins de profiler les résultats, nous ne pouvons pas être sûrs.
Wondra

2
Hmm, cette réponse me rappelle l' évolution d'un ingénieur SW . Blague à part, si vous avez absolument besoin (et êtes sûr) de cette amélioration des performances, allez-y. Sinon, je suggère de le garder KISS et d'utiliser un drapeau booléen, comme proposé dans une autre réponse . Cependant, +1 pour l'alternative représentée ici!
mucaho

1
Évitez les conditions si possible. Je parle de ce qui se passe au niveau de la machine, que ce soit votre propre code natif, un compilateur JIT ou un interprète. Le hit de cache L1 est à peu près le même qu'un hit de registre, peut-être légèrement plus lent, c'est-à-dire 1 ou 2 cycles, alors que la mauvaise prédiction de branche, qui se produit moins souvent mais toujours trop en moyenne, coûte de l'ordre de 10-20 cycles. Voir la réponse de Jalf: stackoverflow.com/questions/289405/… Et ceci: stackoverflow.com/questions/11227809/…
Ingénieur

1
Rappelez-vous en outre que ces conditions dans la réponse de Byte56 doivent être appelées à chaque mise à jour pour la durée de vie de votre programme, et peuvent très bien être imbriquées ou avoir des conditions imbriquées en leur sein, ce qui aggrave les choses. Les pointeurs de fonction / délégués sont la voie à suivre.
Ingénieur

3

Pour être complet

(Ne recommande pas réellement de faire cela car il est en fait assez simple d'écrire simplement quelque chose comme if(!already_called), mais il serait "correct" de le faire.)

Sans surprise, la norme C ++ 11 a idiomisé le problème plutôt trivial d'appeler une fonction une fois, et l'a rendue super explicite:

#include <mutex>

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

}

Certes, la solution standard est quelque peu supérieure à la triviale en présence de threads car elle garantit toujours que toujours exactement un appel se produit, jamais quelque chose de différent.

Cependant, vous n'exécutez pas normalement une boucle d'événements multithread et n'appuyez pas sur les boutons de plusieurs souris en même temps, de sorte que la sécurité des threads est un peu superflue pour votre cas.

En termes pratiques, cela signifie qu'en plus de l'initialisation thread-safe d'un booléen local (que C ++ 11 garantit de toute façon thread-safe), la version standard doit également effectuer une opération atomic test_set avant d'appeler ou de ne pas appeler la fonction.

Pourtant, si vous aimez être explicite, il y a la solution.

EDIT:
Huh. Maintenant que je suis assis ici, à regarder ce code pendant quelques minutes, je suis presque enclin à le recommander.
En fait, ce n'est pas aussi stupide que cela puisse paraître à première vue, en effet, il communique très clairement et sans ambiguïté votre intention ... sans doute mieux structuré et plus lisible que toute autre solution impliquant une iftelle ou telle chose .


2

Certaines suggestions varient en fonction de l'architecture.

Créez clickThisThing () en tant que pointeur de fonction / variante / DLL / so / etc ... et assurez-vous qu'il est initialisé à votre fonction requise lorsque l'objet / composant / module / etc ... est instancié.

Dans le gestionnaire à chaque fois que vous appuyez sur le bouton de la souris, appelez le pointeur de la fonction clickThisThing (), puis remplacez immédiatement le pointeur par un autre pointeur de fonction NOP (aucune opération).

Pas besoin de drapeaux et de logique supplémentaire pour que quelqu'un se trompe plus tard, il suffit de l'appeler et de le remplacer à chaque fois et comme c'est un NOP, le NOP ne fait rien et est remplacé par un NOP.

Ou vous pouvez utiliser l'indicateur et la logique pour ignorer l'appel.

Ou vous pouvez déconnecter la fonction Update () après l'appel afin que le monde extérieur l'oublie et ne l'appelle plus jamais.

Ou vous avez le clickThisThing () lui-même utiliser l'un de ces concepts pour ne répondre qu'une fois avec un travail utile. De cette façon, vous pouvez utiliser un objet / composant / module clickThisThingOnce () de base et l'instancier partout où vous avez besoin de ce comportement et aucun de vos mises à jour n'a besoin d'une logique spéciale partout.


2

Utilisez des pointeurs de fonction ou des délégués.

La variable contenant le pointeur de fonction n'a pas besoin d'être explicitement examinée jusqu'à ce qu'un changement soit nécessaire. Et lorsque vous n'avez plus besoin de ladite logique pour fonctionner, remplacez-la par une référence à une fonction vide / sans opération. Comparer avec la vérification conditionnelle de chaque image pendant toute la durée de vie de votre programme - même si cet indicateur n'était nécessaire que dans les premières images après le démarrage! - Impur et inefficace. (Le point de vue de Boreal sur la prédiction est cependant reconnu à cet égard.)

Les conditions imbriquées, qu'elles constituent souvent, sont encore plus coûteuses que de devoir vérifier un seul conditionnel à chaque image, car la prédiction de branche ne peut plus faire sa magie dans ce cas. Cela signifie des retards réguliers de l'ordre de 10s de cycles - les arrêts de pipeline par excellence . La nidification peut avoir lieu au - dessus ou dessous de ce drapeau booléen. Je n'ai guère besoin de rappeler aux lecteurs à quel point les boucles de jeu complexes peuvent devenir rapidement.

Les pointeurs de fonction existent pour cette raison - utilisez-les!


1
Nous pensons dans le même sens. Le moyen le plus efficace serait de déconnecter la chose Update () de celui qui continue de l'appeler sans relâche une fois le travail terminé, ce qui est très courant dans les configurations de signal + slot de style Qt. L'OP n'a pas spécifié l'environnement d'exécution, nous sommes donc paralysés lorsqu'il s'agit d'optimisations spécifiques.
Patrick Hughes

1

Dans certains langages de script comme javascript ou lua, il est facile de tester la référence de la fonction. À 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

Dans un ordinateur Von Neumann Architecture , la mémoire est l'endroit où les instructions et les données de votre programme sont stockées. Si vous souhaitez que le code ne s'exécute qu'une seule fois, vous pouvez faire en sorte que le code de la méthode écrase la méthode avec les NOP une fois la méthode terminée. De cette façon, si la méthode devait être exécutée à nouveau, rien ne se passerait.


6
Cela cassera quelques architectures qui protègent la mémoire du programme en écriture ou s'exécutent à partir de la ROM. Il déclenchera également éventuellement des avertissements antivirus aléatoires, en fonction de ce qui est installé.
Patrick Hughes

0

En python si vous envisagez d'être un con pour d'autres personnes sur le projet:

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

ce script montre le code:

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

Voici une autre contribution, elle est un peu plus générale / réutilisable, légèrement plus lisible et maintenable, mais probablement moins efficace que d'autres solutions.

Encapsulons notre logique à exécution unique dans une 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; }
}

L'utiliser comme l'exemple fourni ressemble à ceci:

Once clickTheThingOnce = new Once(ClickTheThing);

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

0

Vous pouvez envisager d'utiliser une variable statique.

void doOnce()
{
   static bool executed = false;

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

Vous pouvez le faire dans ClickTheThing() fonction elle-même, ou créer une autre fonction et appeler ClickTheThing()le corps if.

Il est similaire à l'idée d'utiliser un indicateur comme suggéré dans d'autres solutions, mais l'indicateur est protégé contre tout autre code et ne peut pas être modifié ailleurs car il est local à la fonction.


1
... mais cela vous empêche d'exécuter le code une fois "par instance d'objet". (Si c'est ce dont l'utilisateur a besoin ..;))
Vaillancourt

0

En fait, je suis tombé sur ce dilemme avec l'entrée de contrôleur Xbox. Bien que ce ne soit pas exactement le même, c'est assez similaire. Vous pouvez changer le code dans mon exemple pour l'adapter à vos besoins.

Edit: Votre situation utiliserait ceci ->

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

Et vous pouvez apprendre à créer une classe d'entrée brute via ->

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

Mais .. maintenant sur l'algorithme super génial ... pas vraiment, mais bon .. c'est plutôt cool :)

* Donc ... nous pouvons stocker les états de chaque bouton et qui sont enfoncés, relâchés et maintenus enfoncés !!! Nous pouvons également vérifier le temps d'attente, mais cela nécessite une seule instruction if et peut vérifier n'importe quel nombre de boutons, mais il existe certaines règles, voir ci-dessous pour ces informations.

Evidemment, si nous voulons vérifier si quelque chose est pressé, relâché, etc. ismousepressed "sera en fait faux lors de votre prochaine vérification.

Code complet ici: https://github.com/JeremyDX/DX_B/blob/master/DX_B/XGameInput.cpp

Comment ça fonctionne..

Donc, je ne suis pas sûr des valeurs que vous recevez lorsque vous décrivez si un bouton est enfoncé ou non, mais fondamentalement, lorsque je charge dans XInput, j'obtiens une valeur de 16 bits entre 0 et 65535, cela a 15 bits possibles pour "Pressé".

Le problème était à chaque fois que je vérifiais cela. Cela me donnerait simplement l'état actuel des informations. J'avais besoin d'un moyen de convertir l'état actuel en valeurs Pressed, Released et Hold.

Donc ce que j'ai fait est le suivant.

Nous créons d'abord une variable "CURRENT". Chaque fois que nous vérifions ces données, nous définissons le "CURRENT" sur une variable "PREVIOUS", puis stockons les nouvelles données sur "Current" comme on le voit ici ->

uint64_t LAST = CURRENT;
CURRENT = gamepad.wButtons;

Avec ces informations, voici où ça devient passionnant !!

Nous pouvons maintenant déterminer si un bouton est enfoncé!

BUTTONS_HOLD = LAST & CURRENT;

Ce que cela fait, c'est essentiellement qu'il compare les deux valeurs et toutes les pressions sur les boutons qui sont affichées dans les deux resteront 1 et tout le reste réglé sur 0.

C'est-à-dire (1 | 2 | 4) & (2 | 4 | 8) donnera (2 | 4).

Maintenant que nous avons les boutons "HELD" enfoncés. Nous pouvons obtenir le reste.

Pressé est simple .. nous prenons notre état "COURANT" et supprimons tous les boutons enfoncés.

BUTTONS_PRESSED = CURRENT ^ BUTTONS_HOLD;

Publié est le même que nous le comparons à notre DERNIER état à la place.

BUTTONS_RELEASED = LAST ^ BUTTONS_HOLD;

Donc, en regardant la situation pressée. Si disons Actuellement, nous en avions 2 | 4 | 8 pressé. Nous avons constaté que 2 | 4 lieu. Lorsque nous supprimons les bits maintenus, il ne nous reste que 8. Il s'agit du bit nouvellement pressé pour ce cycle.

La même chose peut être appliquée pour Released. Dans ce scénario, "LAST" a été défini sur 1 | 2 | 4. Ainsi, lorsque nous supprimons le 2 | 4 bits. Il nous reste 1. Alors le bouton 1 a été relâché depuis la dernière image.

Ce scénario ci-dessus est probablement la situation la plus idéale que vous pouvez offrir pour la comparaison de bits et il fournit 3 niveaux de données sans instructions if ou pour les boucles, juste 3 calculs de bits rapides.

Je voulais également documenter les données de retenue, donc bien que ma situation ne soit pas parfaite ... ce qu'elle fait, nous définissons essentiellement les bits de retenue que nous voulons vérifier.

Ainsi, chaque fois que nous définissons nos données de presse / libération / attente, nous vérifions si les données de conservation sont toujours égales à la vérification actuelle du bit de conservation. Si ce n'est pas le cas, nous réinitialisons l'heure à l'heure actuelle. Dans mon cas, je le définis sur des index de trame, donc je sais pour combien de trames il a été maintenu enfoncé.

L'inconvénient de cette approche est que je ne peux pas obtenir de temps d'attente individuel, mais vous pouvez vérifier plusieurs bits à la fois. C'est-à-dire si je mets le bit de maintien à 1 | 16 si 1 ou 16 ne sont pas tenus, cela échouera. Il faut donc que TOUS ces boutons soient maintenus enfoncés pour continuer à cocher.

Ensuite, si vous regardez dans le code, vous verrez tous les appels de fonction soignés.

Ainsi, votre exemple serait réduit à simplement vérifier si une pression sur un bouton s'est produite et une pression sur un bouton ne peut se produire qu'une seule fois avec cet algorithme. Lors de la prochaine vérification, il n'existera pas, car vous ne pouvez pas appuyer plus d'une fois que vous devrez relâcher avant de pouvoir appuyer à nouveau.


Donc, vous utilisez essentiellement un bitfield au lieu d'un bool comme indiqué dans les autres réponses?
Vaillancourt

Ouais à peu près lol ..
Jeremy Trifilo

Il peut être utilisé pour ses besoins mais avec la structure msdn ci-dessous, le "usButtonFlags" contient une seule valeur de tous les états des boutons. docs.microsoft.com/en-us/windows/desktop/api/winuser/…
Jeremy Trifilo

Je ne sais pas d'où vient la nécessité de dire tout cela sur les boutons du contrôleur. Le contenu de base de la réponse est caché sous beaucoup de texte distrayant.
Vaillancourt

Oui, je donnais juste un scénario personnel connexe et ce que j'ai fait pour l'accomplir. Je vais le nettoyer plus tard, bien sûr, et peut-être ajouterai-je une entrée clavier et souris et donnerai une approche à ce sujet.
Jeremy Trifilo

-3

Sortie stupide et bon marché mais vous pouvez utiliser une boucle for

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

Le code de la boucle sera toujours exécuté lors de l'exécution de la boucle. Rien n'y empêche le code d'être exécuté une fois. La boucle ou pas de boucle donne exactement le même résultat.
Vaillancourt
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.