Bagaimana cara memastikan sepotong kode hanya berjalan sekali?


18

Saya memiliki beberapa kode yang hanya ingin saya jalankan sekali, meskipun keadaan yang memicu kode itu bisa terjadi beberapa kali.

Misalnya, ketika pengguna mengklik mouse, saya ingin mengekliknya:

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

Namun, dengan kode ini, setiap kali saya mengklik mouse, masalahnya akan diklik. Bagaimana saya bisa mewujudkannya hanya sekali?


7
Saya sebenarnya menemukan ini agak lucu karena saya tidak berpikir itu termasuk dalam "masalah pemrograman game-spesifik". Tetapi saya tidak pernah benar-benar menyukai aturan itu.
ClassicThunder

3
@ClassicThunder Yap, sudah pasti lebih pada sisi pemrograman umum. Pilih untuk menutup jika Anda suka, saya memposting pertanyaan lebih banyak sehingga kami memiliki sesuatu untuk mengarahkan orang ketika mereka mengajukan pertanyaan serupa untuk ini. Itu bisa terbuka atau tertutup untuk tujuan itu.
MichaelHouse

Jawaban:


41

Gunakan bendera boolean.

Pada contoh yang ditampilkan, Anda akan memodifikasi kode menjadi sesuatu seperti berikut:

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

Lebih lanjut, jika Anda ingin dapat mengulangi tindakan, tetapi batasi frekuensi tindakan (yaitu waktu minimum antara setiap tindakan). Anda akan menggunakan pendekatan yang serupa, tetapi mengatur ulang bendera setelah waktu tertentu. Lihat jawaban saya di sini untuk lebih banyak ide tentang itu.


1
Jawaban yang jelas untuk pertanyaan Anda :-) Saya akan tertarik untuk melihat pendekatan alternatif jika Anda atau siapa pun tahu. Penggunaan Boolean global untuk ini selalu bau bagi saya.
Evorlor

1
@Evorlor Tidak jelas bagi semua orang :). Saya tertarik pada solusi alternatif juga, mungkin kita bisa belajar sesuatu yang tidak begitu jelas!
MichaelHouse

2
@Evorlor sebagai alternatif, Anda bisa menggunakan delegasi (pointer fungsi) dan mengubah tindakan apa yang dilakukan. misalnya onStart() { delegate = doSomething; } ... if(mousebuttonpressed) { delegate.Execute(); }dan void doSomething() { doStuff(); delegate = funcDoNothing; }tetapi pada akhirnya semua opsi akhirnya memiliki semacam bendera yang Anda atur / atur ... dan mendelegasikan dalam hal ini tidak lain (kecuali jika Anda memiliki lebih dari dua opsi apa yang harus dilakukan mungkin?).
wonderra

2
@wondra Ya, itu caranya. Tambahkan itu. Kami mungkin juga membuat daftar solusi yang memungkinkan untuk pertanyaan ini.
MichaelHouse

1
@ JamesSnell Lebih mungkin jika multi-threaded. Namun praktiknya masih bagus.
MichaelHouse

21

Jika bool flag tidak mencukupi atau Anda ingin meningkatkan keterbacaan * kode dalam void Update()metode, Anda dapat mempertimbangkan menggunakan delegasi (function pointer):

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

Untuk delegasi "eksekusi sekali" yang sederhana terlalu banyak, jadi saya sarankan menggunakan bool flag saja.
Namun, jika Anda membutuhkan fungsionalitas yang lebih rumit, para delegasi mungkin merupakan pilihan yang lebih baik. Misalnya, jika Anda ingin melakukan tindakan rantai yang lebih berbeda: satu klik pertama, satu lagi klik kedua dan satu lagi ketiga Anda bisa lakukan:

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

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

alih-alih mengganggu kode Anda dengan puluhan flag berbeda.
* dengan biaya pemeliharaan lebih rendah dari sisa kode


Saya melakukan beberapa profil dengan hasil seperti yang saya harapkan:

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

Tes pertama dijalankan pada Unity 5.1.2, diukur dengan System.Diagnostics.Stopwatchpada proyek yang dibangun 32-bit (tidak dalam perancang!). Yang lain di Visual Studio 2015 (v140) dikompilasi dalam mode rilis 32-bit dengan / Ox flag. Kedua tes dijalankan pada Intel i5-4670K CPU @ 3.4GHz, dengan 10.000.000 iterasi untuk setiap implementasi. kode:

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

kesimpulan: Sementara kompilator Unity melakukan pekerjaan dengan baik ketika mengoptimalkan pemanggilan fungsi, memberikan hasil yang kira-kira sama untuk flag positif dan delegasi (masing-masing 21 dan 25 ms) kesalahan prediksi cabang atau pemanggilan fungsi masih cukup mahal (catatan: delegasi harus dianggap sebagai cache dalam tes ini).
Menariknya, kompilator Unity tidak cukup pintar mengoptimalkan cabang ketika ada 99 juta kesalahan prediksi berturut-turut, sehingga meniadakan tes secara manual menghasilkan beberapa peningkatan kinerja yang memberikan hasil terbaik 5 ms. Versi C ++ tidak menunjukkan peningkatan kinerja untuk kondisi peniadaan, namun overhead panggilan fungsi keseluruhan secara signifikan lebih rendah.
yang paling penting: perbedaannya sangat tidak relevan untuk skenario dunia nyata


4
AFAIK ini lebih baik dari sudut pandang kinerja juga. Memanggil fungsi no-op, dengan asumsi bahwa fungsi tersebut sudah ada dalam cache instruksi, jauh lebih cepat daripada memeriksa flag, terutama di mana pengecekan itu bersarang di bawah persyaratan lain .
Insinyur

2
Saya pikir Navin benar, saya cenderung memiliki kinerja yang lebih buruk daripada memeriksa bendera boolean - kecuali JIT Anda benar-benar pintar dan mengganti seluruh fungsi dengan benar-benar tidak ada. Tetapi kecuali kita memprofilkan hasilnya, kita tidak bisa tahu pasti.
keajaiban

2
Hmm, jawaban ini mengingatkan saya pada evolusi seorang insinyur SW . Lelucon, jika Anda benar-benar membutuhkan (dan yakin) dari peningkatan kinerja ini, lakukanlah. Jika tidak, saya sarankan untuk menyimpannya KISS dan menggunakan bendera boolean, seperti yang diusulkan dalam jawaban lain . Namun, +1 untuk alternatif yang ditampilkan di sini!
mucaho

1
Hindari persyaratan jika memungkinkan. Saya berbicara tentang apa yang terjadi di tingkat mesin, apakah itu kode asli Anda sendiri, kompiler JIT, atau juru bahasa. Cache cache L1 hampir sama dengan hit register, mungkin sedikit lebih lambat yaitu 1 atau 2 siklus, sedangkan misprediksi cabang, yang terjadi lebih jarang tetapi masih terlalu banyak rata-rata, biaya pesanan 10-20 siklus. Lihat jawaban Jalf: stackoverflow.com/questions/289405/… Dan ini: stackoverflow.com/questions/11227809/…
Engineer

1
Ingat lebih jauh bahwa persyaratan dalam jawaban Byte56 ini harus disebut setiap pembaruan untuk seumur hidup program Anda, dan mungkin bersarang atau bersarang kondisi di dalamnya, membuat masalah jauh lebih buruk. Pointer fungsi / Delegasi adalah cara untuk pergi.
Insinyur

3

Untuk kelengkapan

(Sebenarnya tidak menyarankan Anda melakukan ini karena sebenarnya cukup mudah untuk hanya menulis sesuatu seperti if(!already_called), tetapi akan "benar" untuk melakukannya.)

Tidak mengherankan, standar C ++ 11 telah mengutarakan masalah yang agak sepele memanggil fungsi sekali, dan membuatnya sangat eksplisit:

#include <mutex>

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

}

Diakui, solusi standarnya adalah agak lebih unggul daripada yang sepele di hadapan utas karena masih menjamin bahwa selalu tepat satu panggilan terjadi, tidak pernah sesuatu yang berbeda.

Namun, Anda biasanya tidak menjalankan loop peristiwa multithreaded dan menekan tombol-tombol dari beberapa tikus pada saat yang sama, sehingga keamanan thread agak berlebihan untuk kasus Anda.

Dalam istilah praktis, itu berarti bahwa selain inisialisasi thread-safe dari boolean lokal (yang C ++ 11 menjamin untuk tetap aman thread), versi standar juga harus melakukan operasi test_set atom sebelum menelepon atau tidak memanggil fungsi.

Namun, jika Anda suka bersikap eksplisit, ada solusinya.

EDIT:
Hah. Sekarang saya telah duduk di sini, menatap kode itu selama beberapa menit, saya hampir cenderung merekomendasikannya.
Sebenarnya itu tidak sebodoh yang kelihatannya pada awalnya, memang sangat jelas dan jelas mengomunikasikan niat Anda ... bisa dibilang lebih terstruktur dan lebih mudah dibaca daripada solusi lain yang melibatkan ifatau semacamnya.


2

Beberapa saran akan bervariasi tergantung pada arsitektur.

Buat clickThisThing () sebagai fungsi pointer / varian / DLL / so / etc ... dan pastikan itu diinisialisasi ke fungsi yang Anda butuhkan ketika objek / komponen / modul / etc ... instantiated.

Di handler setiap kali tombol mouse ditekan lalu panggil pointer fungsi clickThisThing () dan kemudian segera ganti pointer dengan pointer fungsi NOP (no operation) lainnya.

Tidak perlu bendera dan logika tambahan untuk seseorang untuk mengacaukan nanti, panggil saja dan ganti setiap waktu dan karena itu NOP, NOP tidak melakukan apa-apa dan diganti dengan NOP.

Atau Anda bisa menggunakan bendera dan logika untuk melewati panggilan.

Atau Anda dapat memutuskan fungsi Pembaruan () setelah panggilan sehingga dunia luar melupakannya dan tidak pernah memanggilnya lagi.

Atau Anda memiliki clickThisThing () sendiri menggunakan salah satu dari konsep-konsep ini untuk hanya merespons dengan pekerjaan yang bermanfaat sekali. Dengan cara ini Anda dapat menggunakan objek / komponen / modul baseline clickThisThingOnce () dan instantiate di mana saja Anda memerlukan perilaku ini dan tidak ada dari updater Anda yang memerlukan logika khusus di semua tempat.


2

Gunakan pointer fungsi atau delegasi.

Variabel yang menahan fungsi pointer tidak perlu diperiksa secara eksplisit sampai perubahan perlu dilakukan. Dan ketika Anda tidak lagi perlu mengatakan logika untuk menjalankan, ganti dengan ref ke fungsi kosong / tidak-op. Bandingkan dengan melakukan persyaratan memeriksa setiap frame untuk seluruh masa hidup program Anda - bahkan jika bendera itu hanya diperlukan dalam beberapa frame pertama setelah startup! - Tidak bersih dan tidak efisien. (Poin Boreal tentang prediksi diakui dalam hal ini.)

Conditional bersarang, yang sering merupakan ini, bahkan lebih mahal daripada harus memeriksa satu kondisional setiap frame, karena prediksi cabang tidak dapat lagi melakukan keajaiban dalam kasus itu. Itu berarti penundaan teratur pada urutan 10-an siklus - warung pipa par excellence . Sarang dapat terjadi di atas atau di bawah bendera boolean ini. Saya tidak perlu mengingatkan pembaca tentang bagaimana loop permainan yang kompleks dapat dengan cepat menjadi.

Pointer fungsi ada karena alasan ini - gunakan!


1
Kami berpikir dengan cara yang sama. Cara yang paling efisien adalah memutus sambungan Pemutakhiran () dari siapa pun yang terus-menerus memanggilnya begitu selesai melakukan pekerjaan, yang sangat umum dalam pengaturan slot sinyal + slot gaya Qt. OP tidak menentukan lingkungan runtime jadi kami sembrono ketika datang ke optimasi tertentu.
Patrick Hughes

1

Dalam beberapa bahasa skrip seperti javascript atau lua dapat dengan mudah dilakukan pengujian referensi fungsi. Dalam 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

Di komputer Arsitektur Von Neumann , memori adalah tempat instruksi dan data program Anda disimpan. Jika Anda ingin membuat kode hanya berjalan sekali, Anda bisa memiliki kode dalam metode menimpa metode dengan NOP setelah metode selesai. Dengan cara ini jika metode dijalankan lagi, tidak ada yang terjadi.


6
Ini akan memecah beberapa arsitektur yang menulis melindungi memori program atau lari dari ROM. Mungkin juga akan memicu peringatan antivirus acak, tergantung pada apa yang diinstal.
Patrick Hughes

0

Dengan python jika Anda berencana menjadi orang brengsek bagi orang lain di proyek:

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

skrip ini menunjukkan kode:

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

Berikut kontribusi lain, ini sedikit lebih umum / dapat digunakan kembali, sedikit lebih mudah dibaca dan dipelihara, tetapi kemungkinan kurang efisien daripada solusi lain.

Mari merangkum logika sekali-eksekusi kami di kelas, Sekali:

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

Menggunakannya seperti contoh yang diberikan terlihat sebagai berikut:

Once clickTheThingOnce = new Once(ClickTheThing);

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

0

Anda dapat mempertimbangkan menggunakan variabel statis.

void doOnce()
{
   static bool executed = false;

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

Anda dapat melakukan ini dalam ClickTheThing()fungsinya sendiri, atau membuat fungsi lain dan memanggil ClickTheThing()tubuh if.

Ini mirip dengan ide menggunakan bendera seperti yang disarankan dalam solusi lain, tetapi bendera dilindungi dari kode lain dan tidak dapat diubah di tempat lain karena sifat lokal ke fungsi.


1
... tapi ini mencegah Anda menjalankan kode sekali 'per instance objek'. (Jika itu yang dibutuhkan pengguna ..;))
Vaillancourt

0

Saya benar-benar menemukan dilema ini dengan Input Controller Xbox. Meskipun tidak persis sama, itu sangat mirip. Anda dapat mengubah kode dalam contoh saya sesuai dengan kebutuhan Anda.

Sunting: Situasi Anda akan menggunakan ini ->

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

Dan Anda dapat mempelajari cara membuat kelas input mentah melalui ->

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

Tapi .. sekarang ke algoritma super luar biasa ... tidak terlalu, tapi hei .. itu cukup keren :)

* Jadi ... kita dapat menyimpan status setiap tombol dan yang ditekan, Dirilis, dan Dimatikan !!! Kami juga dapat memeriksa Waktu Tunggu, tetapi itu memang membutuhkan pernyataan jika tunggal dan dapat memeriksa sejumlah tombol, tetapi ada beberapa aturan lihat di bawah untuk informasi ini.

Tentunya jika kita ingin memeriksa apakah ada sesuatu yang ditekan, dilepaskan, dll. Anda akan melakukan "Jika (Ini) {}", tetapi ini menunjukkan bagaimana kita bisa mendapatkan status pers dan kemudian mematikannya dari frame berikutnya sehingga Anda " ismousepressed "sebenarnya akan salah saat Anda memeriksa berikutnya.

Kode Lengkap Di Sini: https://github.com/JeremyDX/DX_B/blob/master/DX_B/XGameInput.cpp

Bagaimana itu bekerja..

Jadi saya tidak yakin nilai yang Anda terima ketika Anda menggambarkan apakah tombol ditekan atau tidak, tetapi pada dasarnya ketika saya memuat di XInput saya mendapatkan nilai 16bit antara 0 dan 65535 ini memiliki kemungkinan 15 bit untuk "Ditekan".

Masalahnya adalah setiap kali saya memeriksa ini, itu hanya akan memberi saya informasi terkini. Saya membutuhkan cara untuk mengubah kondisi saat ini menjadi Nilai Ditekan, Dirilis, dan Tahan.

Jadi yang saya lakukan adalah sebagai berikut.

Pertama-tama kita membuat variabel "CURRENT". Setiap kali kami memeriksa data ini, kami mengatur variabel "CURRENT" ke variabel "PREVIOUS" dan kemudian menyimpan data baru ke "Current" seperti yang terlihat di sini ->

uint64_t LAST = CURRENT;
CURRENT = gamepad.wButtons;

Dengan informasi ini, inilah tempat menariknya !!

Kita sekarang dapat mencari tahu apakah Button sedang DI BAWAH!

BUTTONS_HOLD = LAST & CURRENT;

Apa yang dilakukan pada dasarnya membandingkan dua nilai dan penekanan tombol apa saja yang ditampilkan di keduanya akan tetap 1 dan semua yang lain diatur ke 0.

Yaitu (1 | 2 | 4) & (2 | 4 | 8) akan menghasilkan (2 | 4).

Sekarang kita memiliki tombol "HELD" yang mana. Kita bisa mendapatkan sisanya.

Ditekan itu sederhana .. kami mengambil status "CURRENT" kami dan menghapus semua tombol yang ditekan.

BUTTONS_PRESSED = CURRENT ^ BUTTONS_HOLD;

Dirilis adalah sama, hanya kami membandingkannya dengan status TERAKHIR kami sebagai gantinya.

BUTTONS_RELEASED = LAST ^ BUTTONS_HOLD;

Jadi melihat situasi yang ditekan. Jika katakanlah Saat ini kami memiliki 2 | 4 | 8 ditekan. Kami menemukan bahwa 2 | 4 tempat diadakan. Ketika kita menghapus Bit Dimiliki kita hanya tersisa 8. Ini adalah bit yang baru ditekan untuk siklus ini.

Hal yang sama dapat diterapkan untuk Dirilis. Dalam skenario ini "LAST" disetel ke 1 | 2 | 4. Jadi saat kita menghapus 2 | 4 bit. Kita dibiarkan dengan 1. Jadi tombol 1 dirilis sejak frame terakhir.

Skenario di atas mungkin merupakan situasi paling ideal yang dapat Anda tawarkan untuk perbandingan bit dan memberikan 3 level data tanpa pernyataan if atau untuk loop hanya 3 perhitungan bit cepat.

Saya juga ingin mendokumentasikan data penahanan jadi meskipun situasi saya tidak sempurna ... apa yang dilakukannya adalah pada dasarnya kami menetapkan perangkat penampung yang ingin kami periksa.

Jadi setiap kali kita mengatur data Press / Release / Hold kami memeriksa apakah data hold masih sama dengan cek bit hold saat ini. Jika tidak kita atur kembali waktunya ke waktu sekarang. Dalam kasus saya, saya mengaturnya untuk membingkai indeks jadi saya tahu berapa banyak frame yang telah ditahan.

Kelemahan dari pendekatan ini adalah saya tidak bisa mendapatkan waktu penahanan individu, tetapi Anda dapat memeriksa beberapa bit sekaligus. Yaitu jika saya mengatur bit tahan ke 1 | 16 jika salah 1 atau 16 tidak ditahan ini akan gagal. Jadi itu mengharuskan SEMUA tombol-tombol itu ditekan untuk terus berdetak.

Kemudian jika Anda melihat dalam kode Anda akan melihat semua panggilan fungsi yang rapi.

Jadi contoh Anda akan dikurangi menjadi sekadar memeriksa apakah penekanan tombol terjadi dan penekanan tombol hanya dapat terjadi sekali dengan algoritma ini. Pada pemeriksaan berikutnya untuk menekannya tidak akan ada karena Anda tidak dapat menekan lebih dari satu kali Anda harus melepaskannya sebelum Anda dapat menekan lagi.


Jadi pada dasarnya Anda menggunakan bitfield bukan bool seperti yang dinyatakan dalam jawaban lain?
Vaillancourt

Ya cukup banyak lol ..
Jeremy Trifilo

Itu dapat digunakan untuk persyaratannya meskipun dengan msdn struct di bawah "usButtonFlags" berisi nilai tunggal dari semua status tombol. docs.microsoft.com/en-us/windows/desktop/api/winuser/…
Jeremy Trifilo

Saya tidak yakin dari mana perlunya mengatakan semua ini tentang tombol pengontrol berasal. Isi inti dari jawabannya tersembunyi di bawah banyak teks yang mengganggu.
Vaillancourt

Ya saya hanya memberikan skenario pribadi terkait dan apa yang saya lakukan untuk mencapainya. Saya akan membersihkannya nanti meskipun pasti dan mungkin saya akan menambahkan input Keyboard dan Mouse dan memberikan pendekatan ke arah itu.
Jeremy Trifilo

-3

Jalan keluar tolol tapi Anda bisa menggunakan for for loop

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

Kode dalam loop akan selalu dieksekusi ketika loop dijalankan. Tidak ada di sana yang mencegah kode dieksekusi sekali. Loop atau tidak ada loop memberikan hasil yang persis sama.
Vaillancourt
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.