如何确保一段代码只运行一次?


18

我有一些代码只想运行一次,即使触发该代码的情况可能会多次发生。

例如,当用户单击鼠标时,我要单击事物:

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

但是,使用此代码,每当我单击鼠标时,事物都会被单击。我怎样才能使它发生一次?


7
实际上,我觉得这很有趣,因为我认为它不属于“特定于游戏的编程问题”。但是我从来没有真正喜欢过这个规则。
ClassicThunder

3
@ClassicThunder是的,肯定更多是在通用编程方面。如果您愿意,请投票关闭,我还发布了更多问题,以便我们在人们提出类似问题时向他们指出。为此可以将其打开或关闭。
MichaelHouse

Answers:


41

使用布尔值标志。

在所示的示例中,您将代码修改为如下所示:

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

此外,如果您希望能够重复执行该动作,但要限制该动作的频率(即每个动作之间的最短时间)。您将使用类似的方法,但是在一定时间后重置标志。在这里查看我的答案以获取更多想法。


1
您问题的明显答案:-)如果您或任何其他人知道其他方法,我很想看看替代方法。对此,使用全局布尔值一直让我感到很讨厌。
Evorlor '16

1
@Evorlor对所有人都不明显:)。我也对替代解决方案感兴趣,也许我们可以学到一些不太明显的知识!
MichaelHouse

2
@Evorlor作为替代,您可以使用委托(函数指针)并更改要执行的操作。例如onStart() { delegate = doSomething; } ... if(mousebuttonpressed) { delegate.Execute(); }void doSomething() { doStuff(); delegate = funcDoNothing; }但最后所有选项最终都带有您设置/取消设置的某种标志...并且在这种情况下,委托别无其他(除非您有两个以上的选项,可能要做些什么?)。
温德拉

2
@wondra是的,就是这样。添加它。我们不妨列出这个问题的可能解决方案。
MichaelHouse

1
@JamesSnell如果它是多线程的,则更有可能。但仍然是一个好习惯。
MichaelHouse

21

如果bool标志不足,或者您想提高void Update()方法中代码的可读性*,则可以考虑使用委托(函数指针):

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

对于简单的“一次执行”,代表是过大的,所以我建议改用bool标志。
但是,如果您需要更复杂的功能,则代表可能是更好的选择。例如,如果您希望链式执行更多不同的动作:一次单击即可,另一次单击即可,而第三次则可以:

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

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

而不是用数十个不同的标志使代码混乱。
*以降低其余代码的可维护性为代价


我几乎对结果进行了一些分析:

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

第一次测试是在Unity 5.1.2上运行的,该测试是System.Diagnostics.Stopwatch在32位构建的项目上进行的(不是在设计器中!)。Visual Studio 2015(v140)上的另一个版本使用/ Ox标志在32位发布模式下编译。两项测试均在Intel i5-4670K CPU @ 3.4GHz上运行,每种实现均进行10,000,000次迭代。码:

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

结论:虽然Unity编译器在优化函数调用时做得很好,但对于正标志和委托(分别为21和25 ms)给出大致相同的结果,分支错误预测或函数调用仍然非常昂贵(注意:应假定委托为高速缓存)在此测试中)。
有趣的是,当存在9千9百万个连续的错误预测时,Unity编译器不够聪明地优化分支,因此手动否定测试确实会带来一些性能提升,从而使最佳结果达到5 ms。C ++版本在否定条件下未显示任何性能提升,但是函数调用的总开销却大大降低。
最重要的是差异几乎没有意义 适用于任何现实情况


4
从性能的角度来看,这也是更好的选择。假设该函数已经在指令缓存中,则调用no-op函数比检查标志要快得多,尤其是在其他条件嵌套检查的情况下。
工程师

2
我认为Navin是正确的,与检查布尔标志相比,我的性能可能更差-除非您的JIT确实很聪明,并且实际上不使用任何东西替换整个函数。但是除非我们分析结果,否则我们将无法确定。
温德拉

2
嗯,这个答案让我想起了软件工程师的发展。撇开一个笑话,如果您绝对需要(并且确定)这种性能提升,那就去吧。如果没有,我建议保持它为KISS并使用一个布尔标志,就像在另一个答案中所建议的那样。但是,此处表示的替代项为+1!
mucaho

1
尽可能避免有条件的。我说的是机器级别的情况,无论是您自己的本机代码,JIT编译器还是解释器。L1高速缓存命中是大约相同的寄存器命中,可能稍微慢即1或2个周期,而分支误预测,这恰好常常较少,但仍太上平均,成本上的10-20个周期的顺序。参见Jalf的答案:stackoverflow.com/questions/289405/…而这:stackoverflow.com/questions/11227809/…–
工程师

1
此外,请记住,在程序的整个生命周期中,必须将Byte56答案中的这些条件称为每次更新,并且可能嵌套或在其中嵌套条件,这使情况变得更糟。函数指针/委托是必经之路。
工程师

3

为了完整性

(实际上不建议您这样做,因为编写类似的代码实际上非常简单if(!already_called),但是这样做是“正确的”。)

毫不奇怪,C ++ 11标准习惯了一次调用一个函数的琐碎问题,并将其变得非常明确:

#include <mutex>

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

}

诚然,标准解决方案在存在线程的情况下要比普通解决方案好一些,因为它仍然保证始终只发生一次调用,而不会发生任何不同。

但是,通常不会运行多线程事件循环并同时按下几只鼠标的按钮,因此线程安全在您的情况下有点多余。

实际上,这意味着除了对本地布尔值进行线程安全的初始化(C ++ 11保证无论如何都是线程安全的)之外,标准版本还必须在调用或不调用之前进行原子性的test_set操作。功能。

不过,如果您喜欢露骨,那就有解决方案。

编辑:
呵呵。现在我坐在这里,盯着代码看了几分钟,我几乎倾向于推荐它。
实际上,它并不像刚开始时看起来那么愚蠢,实际上,它非常清楚,明确地传达了您的意图……可以说,它比涉及任何其他if此类解决方案的结构更好,可读性更好。


2

一些建议会根据体系结构而有所不同。

创建clickThisThing()作为函数指针/变体/ DLL / so / etc ...,并确保在实例化对象/组件/模块/等时将其初始化为所需的函数。

在处理程序中,每当按下鼠标按钮时,都将调用clickThisThing()函数指针,然后立即将其替换为另一个NOP(无操作)函数指针。

不需要标志和额外的逻辑,以后再搞砸的人,只需每次都调用它并替换它,由于它是NOP,因此NOP不执行任何操作并被NOP代替。

或者,您可以使用标志和逻辑来跳过呼叫。

或者,您可以在调用后断开Update()函数,以便外界忘记它并且不再调用它。

或者,您可以使clickThisThing()本身使用这些概念之一来仅对有用的工作进行一次响应。这样,您可以使用基线clickThisThingOnce()对象/组件/模块,并在需要此行为的任何地方实例化它,并且所有更新程序都不需要特殊逻辑。


2

使用函数指针或委托。

在需要进行更改之前,无需显式检查包含函数指针的变量。并且当您不再需要上述逻辑来运行时,请替换为对空/无操作函数的引用。与执行条件比较相比,即使在启动后的前几帧中仅需要该标志,也要检查程序整个生命周期的每一帧!- 不干净且效率低下。(不过,在这一点上,北方人对预测的观点是公认的。)

这些嵌套条件通常构成,比每帧都要检查一个条件的代价甚至更高,因为在这种情况下分支预测不再能发挥其魔力。这意味着定期延迟大约10个周期-流水线停滞非常出色。嵌套可以在此布尔标志的上方下方进行。我几乎不需要提醒读者如何迅速变得复杂的游戏循环。

因此存在函数指针-使用它们!


1
我们正在考虑相同的思路。最有效的方法是在完成工作后将Update()与任何不断调用它的人断开连接,这在Qt样式信号+插槽设置中非常常见。OP没有指定运行时环境,因此在进行特定优化时我们会感到受阻。
Patrick Hughes

1

在某些脚本语言(如javascript或lua)中,可以轻松测试功能引用。在Lualove2d)中:

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

Von Neumann Architecture计算机中,存储程序的指令和数据的位置。如果要使代码仅运行一次,则可以在方法完成后将方法中的代码用NOP覆盖。这样,如果该方法再次运行,将不会发生任何事情。


6
这将破坏一些写保护程序存储器或从ROM运行的体系结构。根据所安装的内容,它可能还会触发随机的防病毒警告。
Patrick Hughes

0

在python中,如果您打算对项目中的其他人成为混蛋:

code = compile('main code'+'del code', '<string>', 'exec')
exec 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

这是另一个贡献,它更具通用性/可重用性,可读性和可维护性,但效率可能比其他解决方案低。

让我们将一次执行的逻辑封装在一个类中:

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; }
}

像提供的示例一样使用它,如下所示:

Once clickTheThingOnce = new Once(ClickTheThing);

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

0

您可以考虑使用static变量。

void doOnce()
{
   static bool executed = false;

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

您可以在ClickTheThing()函数本身中执行此操作,也可以创建另一个函数并ClickTheThing()在if主体中调用。

它类似于在其他解决方案中建议的使用标志的想法,但是该标志受到其他代码的保护,并且由于位于该函数的本地而无法在其他地方更改。


1
...但是这使您无法“每个对象实例”运行一次代码。(如果这是用户需要的。;))
Vaillancourt

0

实际上,我在Xbox Controller Input上遇到了这个难题。尽管不完全相同,但非常相似。您可以在我的示例中更改代码以适合您的需求。

编辑:您的情况将使用此->

https://docs.microsoft.com/zh-CN/windows/desktop/api/winuser/ns-winuser-tagrawmouse

您可以通过->了解如何创建原始输入类

https://docs.microsoft.com/zh-CN/windows/desktop/inputdev/raw-input

但是..现在使用超级棒的算法...不是真的,但是嘿..这很酷:)

*因此...我们可以存储每个按钮的状态,以及按下,释放和按下的状态!!!我们还可以检查保持时间,但这确实需要一个if语句,并且可以检查任意数量的按钮,但是有关此信息,请参见下面的一些规则。

显然,如果我们要检查是否有按下,释放等操作,则可以执行“ If(This){}”,但这显示了如何获取按下状态,然后将其关闭下一帧,因此您的“ ismousepressed”在您下次检查时实际上为假。

完整代码在这里:https : //github.com/JeremyDX/DX_B/blob/master/DX_B/XGameInput.cpp

怎么运行的..

因此,我不确定在描述按钮是否按下时收到的值,但是基本上,当我在XInput中加载时,我会得到一个介于0到65535之间的16bit值,该值具有“ Pressed”的15位可能状态。

问题是我每次检查时都会得到信息的当前状态。我需要一种将当前状态转换为“已按下”,“已释放”和“保持值”的方法。

所以我要做的是以下几点。

首先,我们创建一个“ CURRENT”变量。每次检查此数据时,我们都会将“ CURRENT”设置为“ PREVIOUS”变量,然后将新数据存储为“ Current”,如下所示->

uint64_t LAST = CURRENT;
CURRENT = gamepad.wButtons;

有了这些信息,这就是令人兴奋的地方!!

现在,我们可以确定按钮是否处于按下状态!

BUTTONS_HOLD = LAST & CURRENT;

这基本上是将两个值进行比较,两者中显示的任何按钮按下都将保持为1,其他所有设置为0。

即(1 | 2 | 4)&(2 | 4 | 8)将产生(2 | 4)。

现在,我们已经“按下”了哪些按钮。我们可以得到其余的。

按下很简单..我们进入“当前”状态并删除所有按住的按钮。

BUTTONS_PRESSED = CURRENT ^ BUTTONS_HOLD;

释放是相同的,只是我们将其与LAST状态进行了比较。

BUTTONS_RELEASED = LAST ^ BUTTONS_HOLD;

因此,请看紧迫情况。如果说当前我们有2 | 4 | 8按下。我们发现2 | 4在哪里举行。当我们删除保留位时,我们只剩下8位。这是此周期中新按下的位。

相同的也可以应用于发布。在这种情况下,“ LAST”设置为1 |。2 | 4.因此,当我们删除2 | 4位。我们剩下1。因此自上一帧以来释放了按钮1。

上面的情况可能是您可以进行位比较的最理想的情况,它提供了3个级别的数据,没有if语句,也没有为循环提供3个快速位计算。

我还想记录保持数据,因此尽管我的情况并不完美……它的作用是基本上设置了我们要检查的保持位。

因此,每次设置压榨/释放/保持数据时,我们都会检查保持数据是否仍等于当前的保持位检查。如果没有,我们将时间重置为当前时间。在我的情况下,我将其设置为帧索引,因此我知道将其保留多少帧。

这种方法的缺点是我无法获得单独的保持时间,但是可以一次检查多个位。即,如果将保持位设置为1 | 16如果不保留1或16,则将失败。因此,需要按住所有这些按钮才能继续滴答。

然后,如果您查看代码,将看到所有简洁的函数调用。

因此,您的示例将简化为仅检查是否发生了按下按钮的情况,并且使用此算法一次只能按下一次按钮。在下一个要按下的支票上,该按钮将不存在,因为您不能再按一次,则必须先松开,然后才能再次按下。


因此,您基本上是使用位域而不是其他答案中所述的布尔值?
Vaillancourt

是的,大声笑..
杰里米·特里菲洛

尽管使用以下msdn结构,“ usButtonFlags”包含所有按钮状态的单个值,但仍可以满足他的要求。docs.microsoft.com/zh-CN/windows/desktop/api/winuser / ...
Jeremy Trifilo

不过,我不确定说所有关于控制器按钮的必要性是从哪里来的。答案的核心内容隐藏在许多分散注意力的文字之下。
Vaillancourt

是的,我只是给出一个相关的个人情况以及我为实现该目的所做的工作。我会稍后进行清理,当然可以,也许我会添加“键盘和鼠标”输入并提供解决方法。
杰里米·特里菲洛

-3

愚蠢的廉价出路,但您可以使用for循环

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

循环中的代码将始终在循环运行时执行。没有什么可以阻止代码执行一次。循环或不循环给出完全相同的结果。
Vaillancourt
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.