Как сделать так, чтобы фрагмент кода выполнялся только один раз?


18

У меня есть код, который я хочу запустить только один раз, хотя обстоятельства, которые вызывают этот код, могут возникать несколько раз.

Например, когда пользователь щелкает мышью, я хочу нажать на вещь:

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

Однако с этим кодом каждый раз, когда я щелкаю мышью, происходит щелчок мышью. Как я могу сделать это только один раз?


7
На самом деле я нахожу это довольно забавным, так как не думаю, что это относится к «специфическим для игры вопросам программирования». Но мне никогда не нравилось это правило.
ClassicThunder

3
@ClassicThunder Да, это, конечно, больше на стороне общего программирования. Проголосуйте за закрытие, если хотите, я опубликовал вопрос больше, чтобы у нас было что-то, на что можно было бы указать людям, когда они задают подобные вопросы. Это может быть открыто или закрыто для этой цели.
MichaelHouse

Ответы:


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
Очевидный ответ на ваш вопрос :-) Мне было бы интересно увидеть альтернативные подходы, если вы или кто-либо другой знает о них. Такое использование глобального Boolean для этого всегда было для меня вонючим.
Evorlor

1
@ Evorlor Не очевидно для всех :). Меня также интересуют альтернативные решения, может быть, мы сможем узнать что-то не столь очевидное!
MichaelHouse

2
@Evorlor в качестве альтернативы, вы можете использовать делегаты (указатели функций) и изменить, какое действие выполняется. например , onStart() { delegate = doSomething; } ... if(mousebuttonpressed) { delegate.Execute(); }и , void doSomething() { doStuff(); delegate = funcDoNothing; }но в конце концов все варианты в конечном итоге с флагом некоторых видов вы установили / неустановленные ... и делегатом в данном случае не что иное , ( за исключением , если у вас есть более двух вариантов , что делать , может быть?).
Wondra

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) скомпилирован в 32-разрядном режиме выпуска с флагом / Ox. Оба теста выполнялись на процессоре Intel i5-4670K с частотой 3,4 ГГц, с 10 000 000 итераций для каждой реализации. код:

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

вывод: хотя компилятор Unity хорошо справляется с оптимизацией вызовов функций, давая примерно одинаковый результат как для положительного флага, так и для делегатов (21 и 25 мс соответственно), ошибочное прогнозирование ветвления или вызов функции по-прежнему довольно дороги (примечание: делегат следует считать кэшем в этом тесте).
Интересно, что компилятор Unity не достаточно умен, чтобы оптимизировать ветку, когда существует 99 миллионов последовательных неправильных предсказаний, поэтому ручное отрицание теста дает некоторое повышение производительности, дающее лучший результат в 5 мс. Версия C ++ не демонстрирует какого-либо повышения производительности для условия отрицания, однако общие накладные расходы на вызов функции значительно ниже.
самое главное: разница в значительной степени иррелеват для любого реального сценария


4
AFAIK, это тоже лучше с точки зрения производительности. Вызов функции no-op при условии, что функция уже находится в кэше команд, выполняется намного быстрее, чем проверка флагов, особенно если эта проверка вложена в другие условия.
инженер

2
Я думаю, что Navin прав, у меня, скорее всего, будет худшая производительность, чем при проверке логического флага - если ваш JIT не очень умный и не заменяет буквально всю функцию целиком. Но если мы не профилируем результаты, мы не можем знать наверняка.
Wondra

2
Хм, этот ответ напоминает мне об эволюции SW-инженера . Шутка в сторону, если вам абсолютно необходимо (и вы уверены) это повышение производительности, сделайте это. Если нет, я бы предложил оставить его KISS и использовать логический флаг, как предложено в другом ответе . Тем не менее, +1 для альтернативы, представленной здесь!
Мукахо

1
Избегайте условий, где это возможно. Я говорю о том, что происходит на машинном уровне, будь то собственный код, JIT-компилятор или интерпретатор. Попадание в кэш L1 примерно такое же, как попадание в регистр, возможно, немного медленнее, то есть 1 или 2 цикла, тогда как ошибочное прогнозирование ветвлений, которое происходит реже, но все же слишком много в среднем, стоит порядка 10-20 циклов. См. Ответ Джальфа: 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 / так / и т. Д. И убедитесь, что он инициализирован для требуемой функции при создании экземпляра объекта / компонента / модуля / и т. Д.

В обработчике всякий раз, когда нажимается кнопка мыши, вызывается указатель функции clickThisThing (), а затем немедленно заменяется указатель на другой указатель функции NOP (без операции).

Нет необходимости в флагах и дополнительной логике для того, чтобы кто-то напортачил позже, просто вызывайте его и заменяйте его каждый раз, и, поскольку это NOP, NOP ничего не делает и заменяется NOP.

Или вы можете использовать флаг и логику, чтобы пропустить вызов.

Или вы можете отключить функцию Update () после вызова, чтобы внешний мир забыл об этом и никогда больше не вызывал.

Или у вас есть метод clickThisThing (), который использует одну из этих концепций, чтобы ответить полезной работой только один раз. Таким образом, вы можете использовать базовый объект clickThisThingOnce () / компонент / модуль и создавать его в любом месте, где вам нужно это поведение, и ни одному из ваших средств обновления не нужна специальная логика повсюду.


2

Используйте указатели на функции или делегаты.

Переменная, содержащая указатель функции, не должна быть явно проверена, пока не потребуется внести изменения. И когда вам больше не понадобится указанная логика для запуска, замените на ref пустую / no-op функцию. Сравните с выполнением условных проверок каждого кадра на протяжении всей жизни вашей программы - даже если этот флаг был необходим только в первые несколько кадров после запуска! - нечисто и неэффективно. (Однако в этом отношении мнение Бореала о предсказании признается.)

Вложенные условные выражения, которые они часто составляют, даже более дорогостоящи, чем проверка одного условного выражения в каждом кадре, поскольку предсказание ветвлений больше не может действовать в этом случае. Это означает , что регулярные задержки по порядку 10s циклов - трубопровод киосков по преимуществу . Вложенность может происходить выше или ниже этого логического флага. Мне вряд ли нужно напоминать читателям о том, как сложные игровые циклы могут быстро стать.

По этой причине существуют указатели на функции - используйте их!


1
Мы думаем в том же духе. Наиболее эффективным способом было бы отключить функцию Update () от того, кто постоянно вызывает ее, как только она выполнит свою работу, что очень часто встречается в настройках сигнала + слота в стиле Qt. В OP не указана среда выполнения, поэтому мы не можем определиться с оптимизацией.
Патрик Хьюз

1

В некоторых языках сценариев, таких как javascript или lua, можно легко выполнить тестирование ссылки на функцию. В 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

В компьютере с архитектурой фон Неймана в памяти хранятся инструкции и данные вашей программы. Если вы хотите, чтобы код выполнялся только один раз, вы можете сделать так, чтобы код в методе перезаписывал метод на NOP после завершения метода. Таким образом, если метод будет запущен снова, ничего не произойдет.


6
Это сломает несколько архитектур, которые защищают от записи программную память или запускаются из ПЗУ. Также возможно отключить случайные антивирусные предупреждения, в зависимости от того, что установлено.
Патрик Хьюз

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

Вот еще один вклад, он немного более общий / многократно используемый, немного более читаемый и обслуживаемый, но, вероятно, менее эффективный, чем другие решения.

Давайте инкапсулируем нашу некогда выполняемую логику в классе 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; }
}

Использование, как показано в примере, выглядит следующим образом:

Once clickTheThingOnce = new Once(ClickTheThing);

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

0

Вы можете рассмотреть использование статических переменных.

void doOnce()
{
   static bool executed = false;

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

Вы можете сделать это в ClickTheThing()самой функции или создать другую функцию и вызвать ее ClickTheThing()в теле if.

Это похоже на идею использования флага, как предложено в других решениях, но флаг защищен от другого кода и не может быть изменен в другом месте из-за его локальной функции.


1
... но это препятствует запуску кода один раз «для каждого экземпляра объекта». (Если это то, что нужно пользователю ..;))
Vaillancourt

0

Я действительно столкнулся с этой дилеммой с вводом контроллера Xbox. Хотя не совсем то же самое, это чертовски похоже. Вы можете изменить код в моем примере, чтобы удовлетворить ваши потребности.

Изменить: Ваша ситуация будет использовать это ->

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

И вы можете узнать, как создать необработанный входной класс через ->

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

Но ... теперь на супер удивительный алгоритм ... не совсем, но эй .. это довольно круто :)

* Итак ... мы можем хранить состояния каждой кнопки, которые нажаты, отпущены и удержаны !!! Мы также можем проверить время удержания, но для этого требуется один оператор if и можно проверить любое количество кнопок, но некоторые правила см. Ниже для получения этой информации.

Очевидно, что если мы хотим проверить, нажалось ли что-то, отпущено и т. Д., Вы бы сделали «If (This) {}», но это показывает, как мы можем получить состояние нажатия, а затем отключить его в следующем кадре, чтобы ваш » ismousepressed "на самом деле будет ложным при следующей проверке.

Полный код здесь: https://github.com/JeremyDX/DX_B/blob/master/DX_B/XGameInput.cpp

Как это устроено..

Поэтому я не уверен, что значения, которые вы получаете, когда изображаете, нажата ли кнопка или нет, но в основном, когда я загружаю в XInput, я получаю 16-битное значение в диапазоне от 0 до 65535, это имеет 15-битные возможные состояния для «Нажата».

Проблема заключалась в том, что каждый раз, когда я проверял это, он просто давал мне текущее состояние информации. Мне нужен был способ конвертировать текущее состояние в значения Pressed, Released и Hold.

Так что я сделал следующее.

Сначала мы создаем переменную "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).

Теперь, когда у нас есть какие кнопки "HELD" вниз. Мы можем получить отдых.

Нажать просто ... мы берем наше состояние "CURRENT" и удаляем все удерживаемые кнопки.

BUTTONS_PRESSED = CURRENT ^ BUTTONS_HOLD;

Выпущено то же самое, только мы сравниваем его с нашим последним состоянием.

BUTTONS_RELEASED = LAST ^ BUTTONS_HOLD;

Итак, глядя на пресс-ситуацию. Если скажем В настоящее время у нас было 2 | 4 | 8 нажат. Мы обнаружили, что 2 | 4, где проводится. Когда мы удаляем удерживаемые биты, у нас остается только 8. Это недавно нажатый бит для этого цикла.

То же самое можно применить для выпущенных. В этом сценарии «LAST» был установлен в 1 | 2 | 4. Итак, когда мы удаляем 2 | 4 бита У нас осталось 1. Итак, кнопка 1 была отпущена с момента последнего кадра.

Этот сценарий является, пожалуй, самой идеальной ситуацией, которую вы можете предложить для сравнения битов, и он предоставляет 3 уровня данных без операторов if или для циклов, всего 3 быстрых вычисления битов.

Я также хотел документировать данные о хранении, так что, хотя моя ситуация не идеальна ... что мы делаем, мы в основном устанавливаем уровни ожидания, которые мы хотим проверять.

Таким образом, каждый раз, когда мы устанавливаем наши данные Press / Release / Hold, мы проверяем, равны ли данные удержания текущей проверке бит удержания. Если это не так, мы сбрасываем время на текущее время. В моем случае я настраиваю его на индексы кадров, чтобы я знал, сколько кадров он удерживал.

Недостатком этого подхода является то, что я не могу получить индивидуальное время удержания, но вы можете проверить несколько битов одновременно. Т.е. если я установлю бит удержания на 1 | 16, если 1 или 16 не удерживаются, это потерпит неудачу. Таким образом, требуется, чтобы все эти кнопки были нажаты, чтобы продолжить тикать.

Затем, если вы посмотрите в коде, вы увидите все аккуратные вызовы функций.

Таким образом, ваш пример сводится к простой проверке того, произошло ли нажатие кнопки, и нажатие кнопки может произойти только один раз с этим алгоритмом. При следующей проверке нажать не будет, так как вы не можете нажать больше, чем один раз, когда вам нужно будет отпустить, прежде чем вы сможете нажать снова.


Таким образом, вы в основном используете битовое поле вместо bool, как указано в других ответах?
Vaillancourt

Да, в значительной степени лол ..
Джереми Трифило

Он может быть использован для его требований, хотя с приведенной ниже структурой msdn «usButtonFlags» содержит одно значение всех состояний кнопки. docs.microsoft.com/en-us/windows/desktop/api/winuser/…
Джереми Трифило

Я не уверен, откуда возникает необходимость говорить все это о кнопках контроллера. Основное содержание ответа скрыто под отвлекающим текстом.
Vaillancourt

Да, я просто дал соответствующий личный сценарий и то, что я сделал, чтобы выполнить его. Я уберу это позже, хотя наверняка и возможно я добавлю ввод с клавиатуры и мыши и дам подход к этому.
Джереми Трифило

-3

Глупый дешевый выход, но вы можете использовать цикл for

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

Код в цикле всегда будет выполняться при запуске цикла. Ничто из этого не препятствует выполнению кода один раз. Цикл или отсутствие цикла дает точно такой же результат.
Vaillancourt
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.