코드 조각이 한 번만 실행되도록하려면 어떻게해야합니까?


18

해당 코드를 트리거하는 환경이 여러 번 발생할 수 있지만 한 번만 실행하려는 코드가 있습니다.

예를 들어 사용자가 마우스를 클릭하면 물건을 클릭하고 싶습니다.

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

그러나이 코드를 사용하면 마우스를 클릭 할 때마다 클릭됩니다. 한 번만 어떻게 할 수 있습니까?


7
나는 이것이 "게임 특정 프로그래밍 문제"에 속하지 않는다고 생각하기 때문에 실제로 이런 종류의 재미있는 것을 발견합니다. 그러나 나는 그 규칙을 정말로 좋아하지 않았습니다.
ClassicThunder

3
@ClassicThunder Yep, 그것은 일반적인 프로그래밍 측면에 더 가깝습니다. 원한다면 닫으려면 투표하십시오. 질문을 더 많이 게시하여 사람들이 이와 비슷한 질문을 할 때 지적 할 내용이 있습니다. 그런 목적으로 열거 나 닫을 수 있습니다.
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
귀하의 질문에 대한 명백한 답변 :-) 나는 당신이나 다른 누군가가 알고 있다면 다른 접근법을보고 싶습니다. 이것에 대한 전역 부울 사용은 항상 나에게 냄새가났습니다.
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  |
----------------------------------------

첫 번째 테스트는 System.Diagnostics.Stopwatch디자이너가 아닌 32 비트 빌드 프로젝트로 측정 한 Unity 5.1.2에서 실행되었습니다 . Visual Studio 2015 (v140)의 다른 하나는 / Ox 플래그를 사용하여 32 비트 릴리스 모드로 컴파일되었습니다. 두 테스트 모두 3.4GHz의 Intel i5-4670K CPU에서 실행되었으며 각 구현마다 10,000,000 회 반복되었습니다. 암호:

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

결론 : Unity 컴파일러는 함수 호출을 최적화 할 때 잘 작동하지만 긍정적 인 플래그와 델리게이트 (각각 21 및 25ms)에 대해 거의 동일한 결과를 제공하지만 분기 오해 또는 함수 호출은 여전히 ​​매우 비쌉니다 (참고 : 델리게이트는 캐시로 가정해야 함) 이 테스트에서).
흥미롭게도, Unity 컴파일러는 9 억 9 천만 개의 연속적인 오해가있을 때 브랜치를 최적화하기에 충분히 영리 하지 않으므로 테스트를 수동으로 부정하면 성능이 향상되어 5ms의 최상의 결과를 얻을 수 있습니다. C ++ 버전은 부정 조건에 대한 성능 향상을 보여주지 않지만 함수 호출의 전체 오버 헤드는 상당히 낮습니다.
가장 중요한 점 : 그 차이는 거의 무관심 하다 실제 시나리오


4
AFAIK 성능 측면에서도 더 좋습니다. 해당 함수가 이미 명령어 캐시에 있다고 가정하고 no-op 함수를 호출하면 플래그를 확인하는 것보다, 특히 해당 확인이 다른 조건에 중첩되는 경우보다 훨씬 빠릅니다 .
엔지니어

2
Navin이 맞다고 생각합니다 .JIT이 실제로 똑똑하고 전체 기능을 문자 그대로 아무것도 대체하지 않는 한 부울 플래그를 확인하는 것보다 성능이 떨어질 수 있습니다. 그러나 결과를 프로파일 링하지 않으면 확실하지 않습니다.
wondra

2
흠,이 답변은 SW 엔지니어진화를 떠올리게합니다 . 이 성능 향상이 절대적으로 필요하고 확실하다면 농담을 해보세요. 그렇지 않다면 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

함수 포인터 또는 델리게이트를 사용하십시오.

함수 포인터를 보유한 변수는 변경해야 할 때까지 명시 적으로 검사 할 필요가 없습니다. 그리고 더 이상 로직을 실행할 필요가 없으면 비어 있거나 작동하지 않는 기능에 대한 참조로 교체하십시오. 시작 후 처음 몇 프레임에서만 해당 플래그가 필요한 경우에도 프로그램 전체 수명 동안 모든 프레임에서 조건부 검사를 수행하는 것과 비교하십시오! - 부정하고 비효율적입니다. (그러나 예측에 대한 Boreal의 요점은 이와 관련하여 인정됩니다.)

분기 예측이 더 이상 마법을 수행 할 수 없기 때문에 이러한 조건을 구성하는 중첩 된 조건문은 프레임 당 하나의 단일 조건부를 확인하는 것보다 훨씬 비용이 많이 듭니다. 이는 10 초주기의 규칙적인 지연을 의미 합니다. 파이프 라인이 매우 우수 합니다. 이 부울 플래그 위나 아래에 중첩이 발생할 수 있습니다 . 복잡한 게임 루프가 빠르게 진행될 수있는 방법을 독자들에게 상기시킬 필요는 없습니다.

이런 이유로 함수 포인터가 존재합니다. 사용하십시오!


1
우리는 같은 노선을 따라 생각하고 있습니다. 가장 효율적인 방법은 작업이 완료된 후 Update () 항목을 끊임없이 계속 호출하는 사람과 연결을 끊는 것입니다. 이는 Qt 스타일 신호 + 슬롯 설정에서 매우 일반적입니다. OP는 런타임 환경을 지정하지 않았으므로 특정 최적화와 관련하여 어려움을 겪고 있습니다.
Patrick Hughes

1

javascript 또는 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

A의 폰 노이만 아키텍처 프로그램의 지침과 데이터가 저장되는 컴퓨터 메모리입니다. 코드를 한 번만 실행하려면 메소드가 완료된 후 메소드의 코드가 NOP로 메소드를 겹쳐 쓸 수 있습니다. 이 방법으로 메소드를 다시 실행하면 아무 일도 일어나지 않습니다.


6
이로 인해 프로그램 보호 메모리를 쓰거나 ROM에서 실행되는 몇 가지 아키텍처가 중단됩니다. 또한 설치된 내용에 따라 임의의 안티 바이러스 경고를 설정할 수도 있습니다.
Patrick Hughes

0

파이썬에서 프로젝트의 다른 사람들에게 바보가 될 계획이라면 :

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

정적 변수 사용을 고려할 수 있습니다.

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/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에로드 할 때 0에서 65535 사이의 16 비트 값을 얻습니다.

문제는 내가 이것을 확인할 때마다 정보의 현재 상태를 알려줍니다. 현재 상태를 Pressed, Release 및 Hold Values로 변환하는 방법이 필요했습니다.

그래서 내가 한 일은 다음과 같습니다.

먼저 "CURRENT"변수를 만듭니다. 이 데이터를 확인할 때마다 "CURRENT"를 "PREVIOUS"변수로 설정 한 다음 여기에 표시된대로 "Current"에 새 데이터를 저장합니다.

uint64_t LAST = CURRENT;
CURRENT = gamepad.wButtons;

이 정보를 통해 여기에 흥미 진진한 곳이 있습니다!

이제 Button이 HELD DOWN인지 알아낼 수 있습니다!

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;

Pressed 상황을보고 있습니다. 말하자면 현재 우리는 2 | 4 | 8을 눌렀습니다. 우리는 그것을 발견 2 | 4 개최지. 보류 비트를 제거하면 8 개만 남게됩니다.이 사이클에서 새로 누른 비트입니다.

릴리스에도 동일하게 적용될 수 있습니다. 이 시나리오에서 "LAST"는 1 | 2 | 4. 따라서 2 | 4 비트 우리는 1로 남았습니다. 따라서 버튼 1은 마지막 프레임 이후에 해제되었습니다.

이 시나리오는 아마도 비트 비교를 위해 제공 할 수있는 가장 이상적인 상황 일 수 있으며 if 문이나 for 루프없이 3 개의 빠른 비트 계산으로 3 가지 수준의 데이터를 제공합니다.

또한 상황이 완벽하지는 않지만 보류 데이터를 문서화하고 싶었습니다. 수행하는 작업은 기본적으로 확인하려는 보류 비트를 설정하는 것입니다.

따라서 Press / Release / Hold 데이터를 설정할 때마다 보류 데이터가 여전히 현재 보류 비트 검사와 같은지 확인합니다. 재설정하지 않으면 시간을 현재 시간으로 재설정합니다. 필자의 경우 프레임 인덱스로 설정하여 얼마나 많은 프레임이 보류되었는지 알 수 있습니다.

이 방법의 단점은 개별 보류 시간을 얻을 수 없지만 한 번에 여러 비트를 확인할 수 있다는 것입니다. 즉, 홀드 비트를 1로 설정하면 | 1 또는 16이 유지되지 않으면 16이 실패합니다. 틱을 계속하려면 모든 버튼을 누르고 있어야합니다.

그런 다음 코드를 보면 모든 깔끔한 함수 호출이 표시됩니다.

따라서 버튼 누름이 있는지 확인하는 간단한 예제로 예제가 축소되고 버튼 누름은이 알고리즘으로 한 번만 발생할 수 있습니다. 다음에 눌렀을 때 눌렀다가 다시 놓기 전에는 더 이상 눌렀다 놓을 수 없기 때문에 존재하지 않습니다.


그래서 당신은 기본적으로 다른 답변에 명시된 것처럼 부울 대신 비트 필드를 사용합니까?
Vaillancourt

그래 꽤 롤 ..
제레미 트리 필로

아래의 msdn 구조체를 사용하면 "usButtonFlags"에 모든 버튼 상태의 단일 값이 포함되지만 요구 사항에 사용할 수 있습니다. docs.microsoft.com/en-us/windows/desktop/api/winuser/…
Jeremy Trifilo

그러나 컨트롤러 버튼에 대해이 모든 것을 말할 필요성이 어디인지 확실하지 않습니다. 답의 핵심 내용은 많은 혼란스러운 텍스트 아래에 숨겨져 있습니다.
Vaillancourt

예, 나는 단지 관련 개인 시나리오를 제공하고 있었고 그것을 달성하기 위해 무엇을했는지. 나중에 확실하게 정리하고 키보드와 마우스 입력을 추가하고 그에 대한 접근 방식을 제공 할 것입니다.
제레미 트리 필로

-3

바보 같은 싼 방법이지만 for 루프를 사용할 수 있습니다

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

루프의 코드는 루프가 실행될 때 항상 실행됩니다. 코드가 한 번 실행되는 것을 막는 것은 없습니다. 루프 또는 루프 없음은 정확히 동일한 결과를 제공합니다.
Vaillancourt
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.