Mengelola banyak referensi dari entitas game yang sama di tempat yang berbeda menggunakan ID

8

Saya telah melihat pertanyaan hebat tentang topik serupa, tetapi tidak ada yang membahas metode khusus ini:

Mengingat bahwa saya memiliki beberapa koleksi entitas game dalam game [XNA Game Studio] saya, dengan banyak entitas yang dimiliki beberapa daftar, saya sedang mempertimbangkan cara saya bisa melacak kapan pun entitas dihancurkan dan menghapusnya dari daftar yang menjadi miliknya .

Banyak metode potensial yang kelihatannya ceroboh / berbelit-belit, tetapi saya teringat akan cara yang pernah saya lihat sebelumnya, alih-alih memiliki banyak koleksi entitas game, Anda malah memiliki koleksi ID entitas game . ID ini dipetakan ke entitas game melalui "database" pusat (mungkin hanya tabel hash).

Jadi, setiap kali ada sedikit kode yang ingin mengakses anggota entitas gim, pertama-tama ia memeriksa apakah masih ada dalam database. Jika tidak, itu dapat bereaksi sesuai.

Apakah ini pendekatan yang bagus? Tampaknya itu akan menghilangkan banyak risiko / kerepotan menyimpan banyak daftar, dengan pengorbanan menjadi biaya pencarian setiap kali Anda ingin mengakses suatu objek.

Vargonian
sumber

Jawaban:

3

Jawaban singkat: ya.

Jawaban panjang: Sebenarnya, semua game yang saya lihat bekerja dengan cara ini. Pencarian tabel hash cukup cepat; dan jika Anda tidak peduli dengan nilai ID aktual (yaitu mereka tidak digunakan di tempat lain), Anda dapat menggunakan vektor, bukan tabel hash. Yang bahkan lebih cepat.

Sebagai bonus tambahan, pengaturan ini memungkinkan untuk menggunakan kembali objek game Anda, mengurangi tingkat alokasi memori Anda. Saat objek game dihancurkan, alih-alih "membebaskannya", Anda cukup membersihkan bidangnya, dan memasukkan "kumpulan objek". Kemudian, ketika Anda membutuhkan objek baru, ambil saja yang sudah ada dari kolam. Jika objek baru muncul terus-menerus, ini terasa meningkatkan kinerja.

Sudahlah
sumber
Secara umum, itu cukup akurat, tetapi kita perlu mempertimbangkan hal-hal lain terlebih dahulu. Jika ada banyak objek yang dengan cepat dibuat dan dihancurkan, dan fungsi hash kita bagus, hashtable mungkin lebih cocok daripada vektor. Namun, jika Anda ragu, gunakan vektornya.
hokiecsgrad
Ya, saya harus setuju: vektor tidak sepenuhnya lebih cepat dari tabel hash. Tetapi lebih cepat 99% dari waktu (-8
Nevermind
Jika Anda ingin menggunakan kembali slot (yang Anda inginkan pasti), daripada Anda perlu memastikan bahwa ID yang tidak valid diakui. ex. Objek pada ID 7 ada di Daftar A, B dan C. Objek dihancurkan dan dalam slot yang sama objek lain dibuat. Objek ini mendaftar sendiri ke Daftar A dan C. Masuknya objek pertama dalam Daftar B sekarang "menunjuk" ke objek yang baru dibuat, yang salah dalam hal ini. Ini dapat diselesaikan dengan sempurna dengan Handle-Manager dari Scott Bilas scottbilas.com/publications/gem-resmgr . Saya sudah menggunakan ini dalam permainan komersial dan itu berfungsi seperti pesona.
DarthCoder
3

Apakah ini pendekatan yang bagus? Tampaknya itu akan menghilangkan banyak risiko / kerepotan menyimpan banyak daftar, dengan pengorbanan menjadi biaya pencarian setiap kali Anda ingin mengakses suatu objek.

Yang telah Anda lakukan adalah memindahkan beban melakukan pemeriksaan dari waktu kehancuran ke waktu penggunaan. Saya tidak akan mengklaim bahwa saya belum pernah melakukan ini sebelumnya, tetapi saya tidak menganggapnya 'terdengar'.

Saya sarankan menggunakan salah satu dari beberapa alternatif yang lebih kuat. Jika jumlah daftar diketahui, Anda bisa lolos hanya dengan menghapus objek dari semua daftar. Itu tidak berbahaya jika objek tidak ada dalam daftar yang diberikan, dan Anda dijamin telah menghapusnya dengan benar.

Jika jumlah daftar tidak diketahui atau tidak praktis maka simpan kembali referensi kepada mereka pada objek. Implementasi khas ini adalah pola pengamat , tetapi Anda mungkin dapat mencapai efek yang sama dengan delegasi, yang memanggil kembali untuk menghapus item dari daftar yang mengandung bila diperlukan.

Atau kadang-kadang Anda tidak benar-benar membutuhkan banyak daftar sama sekali. Seringkali Anda dapat menyimpan objek hanya dalam satu daftar dan menggunakan kueri dinamis untuk menghasilkan koleksi sementara lainnya saat Anda membutuhkannya. misalnya. Alih-alih memiliki daftar terpisah untuk inventaris karakter, Anda bisa menarik semua objek di mana "terbawa" sama dengan karakter saat ini. Ini mungkin terdengar tidak efisien, tetapi cukup baik untuk database relasional dan dapat dioptimalkan dengan pengindeksan yang cerdas.

Kylotan
sumber
Ya, dalam implementasi saya saat ini, setiap daftar atau objek tunggal yang ingin referensi ke entitas game perlu berlangganan ke acara yang dihancurkan dan bereaksi sesuai. Ini mungkin sama baiknya atau lebih baik dalam beberapa hal daripada pencarian ID.
vargonian
1
Dalam buku saya, ini adalah kebalikan dari solusi yang kuat. Dibandingkan dengan pencarian-oleh-id, Anda memiliki kode LEBIH BANYAK, dengan LEBIH banyak tempat untuk bug, LEBIH peluang untuk melupakan sesuatu, dan kemungkinan untuk menghancurkan objek TANPA memberitahu semua orang untuk boot. Dengan lookup-by-id, Anda memiliki sedikit kode yang hampir tidak pernah berubah, satu titik kehancuran dan jaminan 100% bahwa Anda tidak akan pernah mengakses objek yang hancur.
Nevermind
1
Dengan lookup-by-id, Anda memiliki kode tambahan di setiap tempat yang Anda perlukan untuk menggunakan objek. Dengan pendekatan berbasis pengamat, Anda tidak memiliki kode tambahan kecuali saat menambahkan dan menghapus item dari daftar, yang kemungkinan merupakan segelintir tempat di seluruh program. Jika Anda mereferensikan item lebih dari yang Anda buat, hancurkan, atau pindahkan di antara daftar, maka pendekatan observer akan menghasilkan lebih sedikit kode.
Kylotan
Mungkin ada lebih banyak tempat yang merujuk objek daripada tempat yang membuat / menghancurkan yang. Namun, setiap referensi hanya memerlukan sedikit kode, atau tidak sama sekali jika pencarian sudah dibungkus oleh properti (yang seharusnya sebagian besar waktu). Selain itu, Anda TIDAK BISA lupa untuk berlangganan kehancuran objek, tetapi Anda TIDAK BISA lupa untuk melihat objek.
Nevermind
1

Ini tampaknya hanya menjadi contoh yang lebih spesifik dari masalah "bagaimana menghapus objek dari daftar, sementara iterasi melalui itu", yang mudah diselesaikan (iterasi dalam urutan terbalik mungkin merupakan cara paling sederhana untuk daftar gaya array seperti C # 's List).

Jika suatu entitas akan direferensikan dari beberapa tempat, maka itu harus menjadi tipe referensi . Jadi Anda tidak harus melalui sistem ID yang berbelit-belit - kelas di C # sudah menyediakan tipuan yang diperlukan.

Dan akhirnya - mengapa repot-repot "memeriksa database"? Cukup beri masing-masing entitas IsDeadbendera dan periksa itu.

Andrew Russell
sumber
Saya tidak benar-benar tahu C #, tetapi arsitektur ini sering digunakan dalam C dan C ++ di mana jenis referensi normal bahasa (pointer) tidak aman digunakan jika objek dihancurkan. Ada beberapa cara untuk mengatasinya, tetapi mereka semua melibatkan lapisan tipuan, dan ini adalah solusi umum. (Daftar backpoint seperti yang disarankan Kylotan adalah yang lain - yang Anda pilih tergantung pada apakah Anda ingin menghabiskan memori atau waktu.)
C # adalah sampah yang dikumpulkan, jadi tidak masalah jika Anda mengabaikan instance. Untuk sumber daya yang tidak dikelola (yaitu: oleh pemulung) terdapat IDisposablepola, yang sangat mirip dengan menandai objek sebagai IsDead. Tentunya jika Anda perlu menggabungkan contoh, daripada memilikinya GC, maka strategi yang lebih rumit diperlukan.
Andrew Russell
Bendera IsDead adalah sesuatu yang saya gunakan dengan sukses di masa lalu; Saya hanya suka ide permainan mogok jika saya gagal memeriksa keadaan mati daripada melanjutkan dengan gembira, dengan hasil yang berpotensi aneh dan sulit di-debug. Menggunakan pencarian basis data akan menjadi yang pertama; IsDead menandai yang terakhir.
vargonian
3
@vargonian Dengan pola Disposable, yang biasanya terjadi adalah Anda memeriksa IsDisposed di bagian atas setiap fungsi dan properti di kelas. Jika instance dibuang Anda membuang ObjectDisposedException(yang umumnya akan "crash" dengan pengecualian tidak tertangani). Dengan memindahkan tanda centang ke objek itu sendiri (alih-alih memeriksa objek secara eksternal), Anda membiarkan objek menentukan sendiri perilaku "aku mati" dan menghilangkan beban pemeriksaan dari penelepon. Untuk tujuan Anda, Anda bisa menerapkan IDisposable, atau membuat IsDeadbendera Anda sendiri dengan fungsionalitas serupa.
Andrew Russell
1

Saya sering menggunakan pendekatan ini dalam permainan C # /. NET saya sendiri. Selain manfaat lain (dan bahaya!) Yang dijelaskan di sini, ini juga dapat membantu menghindari masalah serialisasi.

Jika Anda ingin memanfaatkan fasilitas serialisasi biner .NET Framework bawaan, maka menggunakan ID entitas dapat membantu meminimalkan ukuran grafik objek yang ditulis. Secara default, pemformat biner .NET akan mengeluarkan serial seluruh objek grafik di tingkat bidang. Katakanlah saya ingin membuat serial sebuah Shipinstance. Jika Shipmemiliki _ownerbidang referensi Playersiapa yang memilikinya, maka Playerinstance itu akan keluar juga. Jika Playerberisi _shipsbidang (dari, katakanlah ICollection<Ship>), maka semua kapal pemain juga akan ditulis, bersama dengan benda lain yang dirujuk di tingkat bidang (secara rekursif). Sangat mudah untuk secara tidak sengaja membuat serial grafik objek yang besar ketika Anda hanya ingin membuat serialisasi satu bagian kecil saja.

Sebaliknya, jika saya memiliki _ownerIdbidang, maka saya bisa menggunakan nilai itu untuk menyelesaikan Playerreferensi sesuai permintaan. API publik saya bahkan dapat tetap tidak berubah, dengan Ownerproperti hanya melakukan pencarian.

Sementara pencarian berbasis hash umumnya sangat cepat, overhead yang ditambahkan bisa menjadi masalah untuk set entitas yang sangat besar dengan pencarian sering. Jika itu menjadi masalah bagi Anda, Anda bisa menyimpan referensi dengan menggunakan bidang yang tidak diserialisasi. Misalnya, Anda dapat melakukan sesuatu seperti ini:

public class Ship
{
    private int _ownerId;
    [NonSerialized] private Lazy<Player> _owner;

    public Player Owner
    {
        get { return _owner.Value; }
    }

    public Ship(Player owner)
    {
        _ownerId = owner.PlayerID;
        EnsureCache();
    }

    private void EnsureCache()
    {
        if (_owner == null)
            _owner = new Lazy<Player>(() => Game.Current.Players[_ownerId]);
    }

    [OnDeserialized]
    private void OnDeserialized(StreamingContext context)
    {
        EnsureCache();
    }
}

Tentu saja, pendekatan ini juga dapat membuat serialisasi lebih sulit ketika Anda benar - benar ingin membuat serial objek grafik besar. Dalam hal ini, Anda perlu merancang semacam 'wadah' untuk memastikan bahwa semua benda yang diperlukan disertakan.

Mike Strobel
sumber
0

Cara lain untuk menangani pembersihan Anda adalah dengan membalikkan kontrol dan menggunakan objek Disposable. Anda mungkin memiliki faktor yang mengalokasikan entitas Anda, jadi berikan pabrik semua daftar yang harus ditambahkan. Entitas menyimpan referensi ke semua daftar yang merupakan bagian dari, dan mengimplementasikan IDisposable. Dalam metode buang, ia menghapus dirinya sendiri dari semua daftar. Sekarang Anda hanya perlu peduli dengan manajemen daftar di dua tempat: pabrik (pembuatan) dan buang. Menghapus objek sesederhana entity.Dispose ().

Masalah nama vs referensi dalam C # benar-benar hanya penting untuk mengelola komponen atau tipe nilai. Jika Anda memiliki daftar komponen yang ditautkan ke satu entitas, menggunakan bidang ID sebagai kunci hashmap dapat berguna. Jika Anda hanya memiliki daftar entitas, gunakan saja daftar dengan referensi. Referensi C # aman dan mudah digunakan.

Untuk menghapus atau menambahkan item dari daftar, Anda dapat menggunakan Antrian atau sejumlah solusi lain untuk berurusan dengan menambah dan menghapus dari daftar.

CodexArcanum
sumber