Meningkatkan fungsi O (N ^ 2) (semua entitas iterasi dari semua entitas lainnya)


21

Sedikit latar belakang, saya mengkodekan permainan evolusi dengan seorang teman di C ++, menggunakan ENTT untuk sistem entitas. Makhluk berjalan di peta 2D, makan sayuran atau makhluk lain, bereproduksi dan sifat-sifatnya bermutasi.

Selain itu, kinerjanya baik-baik saja (60fps tidak masalah) saat permainan dijalankan secara real time, tetapi saya ingin dapat mempercepatnya secara signifikan agar tidak harus menunggu 4 jam untuk melihat perubahan signifikan. Jadi saya ingin mendapatkannya secepat mungkin.

Saya berjuang untuk menemukan metode yang efisien bagi makhluk untuk menemukan makanan mereka. Setiap makhluk seharusnya mencari makanan terbaik yang cukup dekat dengan mereka.

Contoh tangkapan layar game

Jika ingin makan, makhluk yang digambarkan di tengah seharusnya melihat sekelilingnya dalam radius 149,64 (jarak pandangnya) dan menilai makanan mana yang harus dibelinya, yang didasarkan pada nutrisi, jarak, dan jenis (daging atau tanaman) .

Fungsi yang bertanggung jawab untuk menemukan setiap makhluk makanan mereka memakan sekitar 70% dari waktu berjalan. Menyederhanakan bagaimana yang ditulisnya saat ini, bisa jadi begini:

for (creature : all_creatures)
{
  for (food : all_entities_with_food_value)
  {
    // if the food is within the creatures view and it's
    // the best food found yet, it becomes the best food
  }
  // set the best food as the target for creature
  // make the creature chase it (change its state)
}

Fungsi ini dijalankan setiap centang untuk setiap makhluk mencari makanan, sampai makhluk menemukan makanan dan mengubah keadaan. Ini juga dijalankan setiap kali makanan baru memunculkan makhluk yang sudah mengejar makanan tertentu, untuk memastikan semua orang mencari makanan terbaik yang tersedia untuk mereka.

Saya terbuka pada ide tentang bagaimana membuat proses ini lebih efisien. Saya ingin mengurangi kompleksitas dari HAI(N2) , tetapi saya tidak tahu apakah itu mungkin.

Salah satu cara saya sudah memperbaikinya adalah dengan menyortir all_entities_with_food_value kelompok sehingga ketika seekor makhluk beralih pada makanan yang terlalu besar untuk dimakan, ia berhenti di sana. Setiap perbaikan lain lebih dari diterima!

EDIT: Terima kasih semua atas balasannya! Saya menerapkan berbagai hal dari berbagai jawaban:

Saya pertama dan hanya membuatnya sehingga fungsi bersalah hanya berjalan sekali setiap lima kutu, ini membuat game sekitar 4x lebih cepat, sementara tidak terlihat mengubah apa pun tentang game.

Setelah itu saya menyimpan di dalam sistem pencarian makanan sebuah array dengan makanan yang muncul dalam tanda centang yang sama dengan yang dijalankannya. Dengan cara ini saya hanya perlu membandingkan makanan yang dikejar makhluk itu dengan makanan baru yang muncul.

Terakhir, setelah meneliti partisi ruang dan mempertimbangkan BVH dan quadtree, saya memilih yang terakhir, karena saya merasa jauh lebih sederhana dan lebih cocok untuk kasus saya. Saya menerapkannya dengan sangat cepat dan sangat meningkatkan kinerja, pencarian makanan hampir tidak membutuhkan waktu sama sekali!

Sekarang rendering adalah apa yang memperlambat saya, tapi itu masalah untuk hari lain. Terima kasih semua!


2
Sudahkah Anda bereksperimen dengan banyak utas pada beberapa inti CPU yang berjalan secara bersamaan?
Ed Marty

6
Berapa banyak makhluk yang Anda miliki rata-rata? Tampaknya tidak setinggi itu, dilihat dari snapshot. Jika demikian, partisi ruang tidak akan banyak membantu. Sudahkah Anda dianggap tidak menjalankan fungsi ini di setiap centang? Anda bisa menjalankannya setiap, katakanlah, 10 ticks. Hasil simulasi tidak boleh berubah secara kualitatif.
Turms

4
Sudahkah Anda melakukan profiling terperinci untuk mengetahui bagian paling mahal dari evaluasi makanan? Alih-alih melihat keseluruhan kompleksitas, mungkin Anda perlu melihat apakah ada perhitungan tertentu atau akses struktur memori yang mencekik Anda.
Harabeck

Saran naif: Anda bisa menggunakan quadtree atau struktur data terkait alih-alih cara O (N ^ 2) yang Anda lakukan sekarang.
Seiyria

3
Seperti yang disarankan @Harabeck, saya akan menggali lebih dalam untuk melihat di mana dalam loop semua waktu yang dihabiskan. Jika itu adalah perhitungan kuadrat-akar untuk jarak, misalnya, Anda mungkin dapat membandingkan dulu XY coords dengan pra-eliminasi banyak kandidat sebelum harus melakukan sqrt mahal pada yang tersisa. Menambahkan if (food.x>creature.x+149.64 or food.x<creature.x-149.64) continue;harus lebih mudah daripada menerapkan struktur penyimpanan "rumit" JIKA itu cukup performan. (Terkait: Ini mungkin membantu kami jika Anda mengunggah kode lebih banyak di loop dalam Anda)
AC

Jawaban:


34

Saya tahu Anda tidak mengkonsep ini sebagai tabrakan, namun apa yang Anda lakukan adalah bertabrakan dengan lingkaran yang berpusat pada makhluk itu, dengan semua makanan.

Anda benar-benar tidak ingin memeriksa makanan yang Anda tahu jauh, hanya apa yang ada di dekatnya. Itu adalah saran umum untuk optimasi tabrakan. Saya ingin mendorong untuk mencari teknik untuk mengoptimalkan tabrakan, dan jangan membatasi diri Anda ke C ++ saat mencari.


Makhluk menemukan makanan

Untuk skenario Anda, saya akan menyarankan untuk menempatkan dunia pada grid. Buat sel-sel setidaknya jari-jari lingkaran yang ingin Anda bertabrakan. Kemudian Anda dapat memilih satu sel di mana makhluk itu berada dan hingga delapan tetangga dan hanya mencari mereka hingga sembilan sel.

Catatan : Anda dapat membuat sel-sel yang lebih kecil, yang berarti bahwa lingkaran yang Anda cari akan melampaui tetangga terdekat, mengharuskan Anda untuk beralih di sana. Namun, jika masalahnya adalah bahwa ada terlalu banyak makanan, sel-sel yang lebih kecil bisa berarti iterasi lebih sedikit daripada entitas makanan secara total, yang pada beberapa titik berubah menguntungkan Anda. Jika Anda curiga inilah masalahnya, tes.

Jika makanan tidak bergerak, Anda bisa menambahkan entitas makanan ke kotak pada penciptaan, sehingga Anda tidak perlu mencari entitas apa yang ada di dalam sel. Alih-alih, Anda meminta sel dan memiliki daftar.

Jika Anda membuat ukuran sel menjadi kekuatan dua, Anda dapat menemukan sel tempat makhluk itu berada hanya dengan memotong koordinatnya.

Anda dapat bekerja dengan jarak kuadrat (alias jangan lakukan sqrt) sambil membandingkan untuk menemukan yang terdekat. Kurang operasi sqrt berarti eksekusi lebih cepat.


Makanan baru ditambahkan

Ketika makanan baru ditambahkan, hanya makhluk di dekatnya yang perlu dibangunkan. Itu adalah ide yang sama, kecuali sekarang Anda harus mendapatkan daftar makhluk dalam sel sebagai gantinya.

Jauh lebih menarik, jika Anda memberi catatan pada makhluk itu seberapa jauh dari makanan yang dikejar ... Anda dapat langsung memeriksa jarak itu.

Hal lain yang akan membantu Anda adalah membuat makanan sadar akan makhluk apa yang mengejarnya. Itu akan memungkinkan Anda untuk menjalankan kode untuk menemukan makanan untuk semua makhluk yang mengejar sepotong makanan yang baru saja dimakan.

Faktanya, mulailah simulasi tanpa makanan dan makhluk apa pun memiliki jarak tak terhingga yang beranotasi. Lalu mulailah menambahkan makanan. Perbarui jarak ketika makhluk bergerak ... Ketika makanan dimakan, ambil daftar makhluk yang mengejarnya, dan kemudian temukan target baru. Selain itu, semua pembaruan lainnya ditangani ketika makanan ditambahkan.


Melewati simulasi

Mengetahui kecepatan makhluk, Anda tahu berapa banyak sampai mencapai targetnya. Jika semua makhluk memiliki kecepatan yang sama, makhluk yang akan mencapai pertama adalah makhluk dengan jarak beranotasi terkecil.

Jika Anda juga tahu waktu sampai Anda menambahkan lebih banyak makanan ... Dan semoga Anda memiliki prediksi yang sama untuk reproduksi dan kematian, maka Anda tahu waktu untuk acara berikutnya (baik menambahkan makanan atau memakan makhluk).

Lewati saat itu. Anda tidak perlu mensimulasikan makhluk yang bergerak di sekitar.


1
"dan cari hanya di sana." dan sel tetangga langsung sel - artinya total 9 sel. Kenapa 9? Karena bagaimana jika makhluk itu tepat berada di sudut sel.
UKMonkey

1
@UKMonkey "Buat sel-sel setidaknya jari-jari lingkaran yang ingin Anda bertabrakan", jika sisi sel adalah jari-jari dan makhluk itu ada di sudut ... well, saya kira Anda hanya perlu mencari empat dalam kasus itu. Namun, tentu saja, kita dapat membuat sel lebih kecil, yang dapat bermanfaat jika terlalu banyak makanan dan terlalu sedikit makhluk hidup. Sunting: Saya akan mengklarifikasi.
Theraot

2
Tentu - jika Anda ingin berolahraga jika Anda perlu mencari di sel ekstra ... tetapi mengingat bahwa sebagian besar sel tidak memiliki makanan (dari gambar yang diberikan); akan lebih cepat hanya mencari 9 sel, daripada mencari tahu 4 sel mana yang perlu Anda cari.
UKMonkey

@UKMonkey itulah sebabnya saya tidak menyebutkan itu pada awalnya.
Theraot

16

Anda harus mengadopsi algoritme partisi ruang seperti BVH untuk mengurangi kompleksitas. Untuk lebih spesifik dengan kasing Anda, Anda perlu membuat pohon yang terdiri dari kotak pembatas berjajar sumbu yang berisi potongan makanan.

Untuk membuat hierarki, letakkan makanan dekat satu sama lain di AABB, lalu letakkan AABB tersebut di AABB yang lebih besar, sekali lagi, berdasarkan jarak di antara mereka. Lakukan ini sampai Anda memiliki simpul root.

Untuk memanfaatkan pohon, pertama-tama lakukan tes persimpangan lingkaran-AABB terhadap simpul akar, kemudian jika terjadi tabrakan, uji terhadap anak-anak dari setiap simpul yang berurutan. Pada akhirnya Anda harus memiliki sekelompok makanan.

Anda juga dapat menggunakan perpustakaan AABB.cc.


1
Itu memang akan mengurangi kerumitan menjadi N log N, tetapi juga akan mahal untuk melakukan partisi. Melihat bahwa saya harus melakukan partisi setiap tick (karena makhluk bergerak setiap tick) apakah masih layak? Apakah ada solusi untuk membantu partisi lebih jarang?
Alexandre Rodrigues

3
@AlexandreRodrigues Anda tidak perlu membangun kembali seluruh pohon setiap centang, hanya perbarui bagian yang bergerak, dan hanya ketika ada sesuatu di luar wadah AABB tertentu. Untuk lebih meningkatkan kinerja, Anda mungkin ingin menggemukkan node (menyisakan ruang di antara anak-anak) sehingga Anda tidak perlu membangun kembali seluruh cabang pada pembaruan daun.
Ocelot

6
Saya pikir BVH mungkin terlalu kompleks di sini - grid seragam diimplementasikan sebagai tabel hash cukup baik.
Steven

1
@Steven dengan menerapkan BVH Anda dapat dengan mudah memperpanjang skala simulasi di masa depan. Dan Anda tidak kehilangan apa pun jika Anda melakukannya untuk simulasi skala kecil.
Ocelot

2

Sementara metode partisi ruang yang dijelaskan memang dapat mengurangi waktu masalah utama Anda bukan hanya pencarian. Ini adalah volume pencarian yang Anda buat yang membuat tugas Anda lambat. Jadi Anda mengoptimalkan loop dalam, tetapi Anda juga dapat mengoptimalkan loop luar.

Masalah Anda adalah Anda menyimpan data polling. Ini agak seperti memiliki anak-anak di kursi belakang meminta seribu kali "kita sudah sampai", tidak perlu melakukan itu pengemudi akan memberi tahu ketika Anda berada di sana.

Alih-alih Anda harus berusaha, jika mungkin, untuk menyelesaikan setiap tindakan sampai selesai memasukkannya ke dalam antrian dan membiarkan peristiwa gelembung itu keluar, ini mungkin membuat perubahan pada antrian, tetapi itu tidak masalah. Ini disebut simulasi kejadian diskrit. Jika Anda dapat menerapkan simulasi Anda dengan cara ini maka Anda mencari speedup yang cukup besar melebihi speedup yang bisa Anda dapatkan dari pencarian partisi ruang yang lebih baik.

Untuk menggarisbawahi poin dalam karir sebelumnya saya membuat simulator pabrik. Kami mensimulasikan berminggu-minggu aliran material seluruh pabrik / bandara besar di setiap tingkat item dengan metode ini dalam waktu kurang dari satu jam. Sedangkan simulasi berbasis timestep hanya bisa mensimulasikan 4-5 kali lebih cepat dari waktu nyata.

Juga sebagai buah gantung yang sangat rendah, pertimbangkan untuk mencabut rutinitas menggambar Anda dari simulasi Anda. Meskipun simulasi Anda sederhana, masih ada beberapa hal di atas gambar. Lebih buruk driver tampilan mungkin membatasi Anda untuk memperbarui x per detik sementara pada kenyataannya prosesor Anda bisa melakukan hal 100 kali lebih cepat. Ini menunjukkan perlunya pembuatan profil.


@Theraot kita tidak tahu bagaimana hal-hal menggambar terstruktur. Tapi ya drawcalls akan menjadi hambatan begitu Anda mendapatkan cukup cepat
joojaa

1

Anda dapat menggunakan algoritma garis-sweep untuk mengurangi kompleksitas ke Nlog (N). Teorinya adalah dari diagram Voronoi, yang membuat pembagian wilayah yang mengelilingi makhluk menjadi wilayah yang terdiri dari semua titik yang lebih dekat ke makhluk itu daripada yang lain.

Algoritme yang disebut Fortune melakukan itu untuk Anda di Nlog (N), dan halaman wiki yang berisi kode pseudo untuk mengimplementasikannya. Saya yakin ada implementasi perpustakaan di luar sana juga. https://en.wikipedia.org/wiki/Fortune%27s_algorithm


Selamat datang di GDSE & terima kasih telah menjawab. Bagaimana tepatnya Anda menerapkan ini pada situasi OP? Deskripsi masalah mengatakan bahwa suatu entitas harus mempertimbangkan semua makanan dalam jarak pandangnya & memilih yang terbaik. Seorang Voronoi tradisional akan mengecualikan dalam berbagai makanan yang lebih dekat dengan entitas lain. Saya tidak mengatakan bahwa Voronoi tidak akan berfungsi, tetapi tidak jelas dari deskripsi Anda bagaimana OP harus memanfaatkannya untuk masalah seperti yang dijelaskan.
Pikalek

Saya suka ide ini, saya ingin melihatnya diperluas. Bagaimana Anda mewakili diagram voronoi (seperti dalam struktur data memori)? Bagaimana Anda menanyakannya?
Theraot

@Theraot Anda tidak perlu diagram voronoi hanya tge ide undian yang sama.
joojaa

-2

Solusi termudah adalah mengintegrasikan mesin fisika dan hanya menggunakan algoritma pendeteksian tabrakan. Cukup buat lingkaran / bola di sekitar setiap entitas, dan biarkan mesin fisika menghitung tabrakan. Untuk 2D saya sarankan Box2D atau Chipmunk , dan Bullet for 3D.

Jika Anda merasa bahwa mengintegrasikan seluruh mesin fisika terlalu banyak, saya sarankan melihat ke dalam algoritma tabrakan tertentu. Kebanyakan pustaka pendeteksi tabrakan bekerja dalam dua langkah:

  • Deteksi fase luas: tujuan tahap ini adalah untuk mendapatkan daftar pasangan calon objek yang mungkin bertabrakan, secepat mungkin. Dua opsi umum adalah:
    • Sapu dan pangkas : urutkan kotak pembatas di sepanjang sumbu X, dan tandai sepasang objek yang bersinggungan. Ulangi untuk setiap sumbu lainnya. Jika pasangan kandidat melewati semua tes, itu berlanjut ke tahap berikutnya. Algoritma ini sangat bagus dalam mengeksploitasi koherensi temporal: Anda dapat menyimpan daftar entitas yang diurutkan dan memperbarui mereka setiap frame, tetapi karena mereka hampir diurutkan, itu akan sangat cepat. Ini juga mengeksploitasi koherensi spasial: karena entitas diurutkan dalam urutan tata ruang, ketika Anda memeriksa tabrakan, Anda dapat berhenti segera setelah entitas tidak bertabrakan, karena semua yang berikutnya akan lebih jauh.
    • Struktur data partisi spasial, seperti quadtrees, octrees, dan grid. Grid mudah diimplementasikan, tetapi bisa sangat boros jika kepadatan entitas rendah, dan sangat sulit diimplementasikan untuk ruang tanpa batas. Pohon spasial statis juga mudah diimplementasikan, tetapi sulit untuk menyeimbangkan atau memperbarui di tempat, sehingga Anda harus membangunnya kembali setiap frame.
  • Fase sempit: pasangan kandidat yang ditemukan pada fase luas diuji lebih lanjut dengan algoritma yang lebih tepat.
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.