Bagaimana cara menghindari objek game yang tidak sengaja menghapus diri mereka di C ++

20

Katakanlah gim saya memiliki monster yang dapat kamikaze meledak pada pemain. Mari kita pilih nama untuk monster ini secara acak: Creeper. Jadi, Creeperkelas memiliki metode yang terlihat seperti ini:

void Creeper::kamikaze() {
    EventSystem::postEvent(ENTITY_DEATH, this);

    Explosion* e = new Explosion;
    e->setLocation(this->location());
    this->world->addEntity(e);
}

Acara tidak antri, mereka dikirim segera. Ini menyebabkan Creeperobjek terhapus di suatu tempat di dalam panggilan postEvent. Sesuatu seperti ini:

void World::handleEvent(int type, void* context) {
    if(type == ENTITY_DEATH){
        Entity* ent = dynamic_cast<Entity*>(context);
        removeEntity(ent);
        delete ent;
    }
}

Karena Creeperobjek terhapus saat kamikazemetode ini masih berjalan, itu akan macet ketika mencoba mengakses this->location().

Salah satu solusinya adalah dengan mengantri acara ke buffer dan mengirimkannya nanti. Apakah itu solusi umum dalam game C ++? Rasanya seperti sedikit peretasan, tapi itu mungkin hanya karena pengalaman saya dengan bahasa lain dengan praktik manajemen memori yang berbeda.

Dalam C ++, apakah ada solusi umum yang lebih baik untuk masalah ini di mana sebuah objek secara tidak sengaja menghapus dirinya sendiri dari dalam salah satu metodenya?

Tom Dalling
sumber
6
uh, bagaimana kalau kamu memanggil postEvent di AKHIR metode kamikaze daripada di awal?
Hackworth
@ Hackworth yang akan bekerja untuk contoh spesifik ini, tapi saya sedang mencari solusi yang lebih umum. Saya ingin dapat memposting acara dari mana saja dan tidak takut menyebabkan crash.
Tom Dalling
Anda juga dapat melihat implementasi autoreleasedi Objective-C, di mana penghapusan ditunda hingga "hanya sedikit".
Chris Burt-Brown

Jawaban:

40

Jangan hapus this

Bahkan secara implisit.

- Pernah -

Menghapus objek saat salah satu fungsi anggotanya masih ada di tumpukan meminta masalah. Setiap arsitektur kode yang mengakibatkan hal itu terjadi ("tidak sengaja" atau tidak) secara objektif buruk , berbahaya , dan harus segera di-refactored . Dalam hal ini, jika monster Anda akan diizinkan untuk memanggil 'World :: handleEvent', jangan, dalam keadaan apa pun, hapus monster di dalam fungsi itu!

(Dalam situasi khusus ini, pendekatan saya yang biasa adalah membuat monster menetapkan bendera 'mati' pada dirinya sendiri, dan memiliki objek Dunia - atau sesuatu seperti itu - menguji bendera 'mati' itu sekali per frame, menghilangkan benda-benda itu dari daftar objek di dunia, dan menghapusnya atau mengembalikannya ke kolam monster atau apa pun yang sesuai. Pada saat ini, dunia juga mengirimkan pemberitahuan tentang penghapusan, sehingga objek lain di dunia tahu bahwa monster itu memiliki berhenti ada, dan dapat menjatuhkan petunjuk apa pun ke sana yang mungkin mereka pegang. Dunia melakukan ini pada waktu yang aman, ketika tahu bahwa tidak ada objek yang sedang diproses, jadi Anda tidak perlu khawatir tentang tumpukan yang meleset ke suatu titik di mana penunjuk 'ini' menunjuk pada memori yang dibebaskan.)

Trevor Powell
sumber
6
Bagian kedua dari jawaban Anda dalam kurung sangat membantu. Polling untuk bendera adalah solusi yang bagus. Tapi, saya bertanya bagaimana menghindari melakukannya, bukan apakah itu hal yang baik atau buruk untuk dilakukan. Jika sebuah pertanyaan bertanya " Bagaimana saya bisa menghindari melakukan X secara tidak sengaja? ", Dan jawaban Anda adalah " Jangan pernah melakukan X, bahkan secara tidak sengaja " dalam huruf tebal, itu sebenarnya tidak menjawab pertanyaan.
Tom Dalling
7
Saya berdiri di belakang komentar yang saya buat di paruh pertama jawaban saya, dan saya merasa mereka menjawab sepenuhnya pertanyaan itu seperti yang diucapkan. Poin penting yang akan saya ulangi di sini adalah bahwa suatu objek tidak menghapus dirinya sendiri. Selamanya . Itu tidak memanggil orang lain untuk menghapusnya. Selamanya . Sebagai gantinya, Anda harus memiliki sesuatu yang lain, di luar objek, yang memiliki objek dan bertanggung jawab untuk memperhatikan kapan objek tersebut perlu dihancurkan. Ini bukan "saat monster mati"; ini untuk semua kode C ++ selalu, di mana saja, sepanjang masa. Tidak ada pengecualian.
Trevor Powell
3
@ TrevorPowell Saya tidak mengatakan bahwa Anda salah. Sebenarnya, saya setuju dengan Anda. Saya hanya mengatakan itu tidak benar-benar menjawab pertanyaan yang diajukan. Ini seperti jika Anda bertanya kepada saya, " Bagaimana cara memasukkan audio ke dalam permainan saya? " Dan jawaban saya adalah, " Saya tidak percaya Anda tidak memiliki audio. Masukkan audio ke dalam permainan Anda sekarang. " masukkan " (Anda dapat menggunakan FMOD) ," yang merupakan jawaban aktual.
Tom Dalling
6
@TrevorPowell Di sinilah Anda salah. Itu bukan "hanya disiplin" jika saya tidak tahu alternatif lain. Contoh kode yang saya berikan murni teoretis. Saya sudah tahu itu desain yang buruk, tapi C ++ saya sudah karatan, jadi saya pikir saya akan bertanya tentang desain yang lebih baik di sini sebelum saya benar-benar kode apa yang saya inginkan. Jadi saya datang untuk bertanya tentang desain alternatif. " Tambahkan bendera penghapusan " adalah alternatif. " Never do it " bukanlah alternatif. Itu hanya memberitahu saya apa yang sudah saya ketahui. Rasanya Anda menulis jawabannya tanpa membaca pertanyaan dengan benar.
Tom Dalling
4
@ Bobby Pertanyaannya adalah "How to NOT do X". Cukup dengan mengatakan "Jangan lakukan X" adalah jawaban yang tidak berharga. JIKA pertanyaannya adalah "Saya sudah melakukan X" atau "Saya sedang berpikir untuk melakukan X" atau varian apa pun maka itu akan memenuhi parameter diskusi meta, tetapi tidak dalam bentuk sekarang.
Joshua Drake
21

Alih-alih mengantri acara di buffer, antri penghapusan di buffer. Delayed-deletion berpotensi menyederhanakan logika secara besar-besaran; Anda benar-benar dapat membebaskan memori di akhir atau awal bingkai ketika Anda tahu tidak ada hal menarik yang terjadi pada objek Anda, dan hapus dari mana saja.

John Calsbeek
sumber
1
Lucu Anda sebutkan ini, karena saya berpikir betapa bagusnya sebuah NSAutoreleasePooldari Objective-C dalam situasi ini. Mungkin harus membuat DeletionPooldengan template C ++ atau sesuatu.
Tom Dalling
@TomDalling Satu hal yang perlu diperhatikan jika Anda membuat buffer eksternal ke objek adalah bahwa suatu objek ingin dihapus karena beberapa alasan pada satu frame, dan dimungkinkan untuk mencoba menghapusnya beberapa kali.
John Calsbeek
Sangat benar. Saya harus menyimpan pointer dalam std :: set.
Tom Dalling
5
Alih-alih buffer objek yang akan dihapus, Anda juga bisa mengatur flag di objek. Setelah Anda mulai menyadari betapa Anda ingin menghindari panggilan baru atau menghapus selama runtime dan pindah ke kumpulan objek, itu akan menjadi lebih sederhana dan lebih cepat.
Sean Middleditch
4

Alih-alih membiarkan dunia menangani penghapusan, Anda bisa membiarkan instance kelas lain berfungsi sebagai ember untuk menyimpan semua entitas yang dihapus. Contoh khusus ini harus mendengarkan ENTITY_DEATHacara dan menanganinya sehingga membuatnya mengantri. The Worldkemudian iterate atas kasus ini dan dapat melakukan operasi pasca-kematian setelah frame telah diberikan dan 'jelas' ember ini, yang pada gilirannya akan melakukan penghapusan sebenarnya dari entitas kasus.

Contoh kelas seperti itu akan seperti ini: http://ideone.com/7Upza

Vite Falcon
sumber
+1, itu merupakan alternatif untuk menandai entitas secara langsung. Lebih langsung, hanya memiliki daftar hidup dan daftar mati entitas langsung di Worldkelas.
Laurent Couvidou
2

Saya sarankan menerapkan pabrik yang digunakan untuk semua alokasi objek game dalam game. Jadi, alih-alih menyebut diri Anda sendiri yang baru, Anda akan memberi tahu pabrik untuk membuat sesuatu untuk Anda.

Sebagai contoh

Object* o = gFactory->Create("Explosion");

Setiap kali Anda ingin menghapus objek, pabrik mendorong objek dalam buffer yang dihapus frame berikutnya. Kehancuran yang tertunda sangat penting dalam sebagian besar skenario.

Juga pertimbangkan mengirim semua pesan dengan penundaan satu bingkai. Hanya ada beberapa pengecualian di mana Anda perlu mengirim segera, bahwa sebagian besar kasus bagaimanapun

Millianz
sumber
2

Anda dapat mengimplementasikan memori terkelola di C ++ sendiri, sehingga ketika ENTITY_DEATHdipanggil, semua yang terjadi adalah jumlah referensi dikurangi satu.

Kemudian seperti yang disarankan John pada permintaan setiap frame Anda dapat memeriksa entitas mana yang tidak berguna (yang tidak memiliki referensi) dan menghapusnya. Misalnya Anda dapat menggunakan boost::shared_ptr<T>( didokumentasikan di sini ) atau jika Anda menggunakan C ++ 11 (VC2010)std::tr1::shared_ptr<T>

Ali1S232
sumber
Hanya std::shared_ptr<T>, bukan laporan teknis! - Anda harus menentukan deleter khusus, jika tidak, itu juga akan menghapus objek segera ketika jumlah referensi mencapai nol.
leftaroundabout
1
@leftaroundabout sangat tergantung, setidaknya saya perlu menggunakan tr1 di gcc. tetapi di VC tidak perlu untuk itu.
Ali1S232
2

Gunakan pooling dan tidak benar-benar menghapus objek. Sebagai gantinya, ubah struktur data tempat mereka terdaftar. Misalnya untuk rendering objek ada objek adegan dan semua entitas entah bagaimana terdaftar untuk rendering, deteksi tabrakan dll. Alih-alih menghapus objek, lepaskan dari adegan dan masukkan ke dalam kumpulan objek mati. Metode ini tidak hanya akan mencegah masalah memori (seperti objek menghapus sendiri) tetapi juga dapat mempercepat permainan Anda jika Anda menggunakan kolam dengan benar.

hevi
sumber
1

Apa yang kami lakukan dalam permainan adalah menggunakan penempatan baru

SomeEvent* obj = new(eventPool.alloc()) new SomeEvent();

eventPool hanyalah array besar memori yang diukir, dan pointer ke setiap segmen disimpan. Jadi, alokasi () akan mengembalikan alamat blok memori bebas. Dalam eventPool kami, memori diperlakukan sebagai tumpukan, jadi setelah semua acara telah dikirim, kami baru saja mengatur ulang penunjuk tumpukan kembali ke awal array.

Karena cara sistem acara kami bekerja, kami tidak perlu memanggil destruktor pada evetn. Jadi kolam hanya akan mendaftarkan blok memori sebagai bebas, dan akan mengalokasikannya.

Ini memberi kami kecepatan tinggi.

Juga ... Kami benar-benar menggunakan kumpulan memori untuk semua yang dialokasikan secara dinamis keberatan dalam pengembangan, karena itu adalah cara yang bagus untuk menemukan kebocoran memori, jika ada benda yang tersisa di kolam ketika permainan keluar (biasanya) maka kemungkinan ada kebocoran mem.

PhilCK
sumber