Bagaimana saya bisa mempertahankan kompatibilitas mundur game yang disimpan?


8

Saya memiliki game sim yang kompleks. Saya ingin menambahkan fungsionalitas save game. Saya akan memperbaruinya dengan fitur-fitur baru secara terus menerus setelah rilis.

Bagaimana saya bisa memastikan pembaruan saya tidak merusak save game yang ada? Arsitektur macam apa yang harus saya ikuti untuk memungkinkan hal ini?


Saya tidak mengetahui arsitektur generik untuk tujuan ini, tetapi saya akan membuat proses patching juga memperbarui / mengkonversi save game untuk memastikan kompatibilitas dengan fitur-fitur baru.
loodakrawa

Jawaban:


9

Salah satu pendekatan yang mudah adalah menjaga agar fungsi pemuatan lama tetap ada. Anda hanya perlu satu fungsi simpan yang hanya menulis versi terbaru. Fungsi memuat mendeteksi fungsi memuat berversi yang benar untuk dipanggil (biasanya dengan menuliskan nomor versi di suatu tempat di awal format simpan file Anda). Sesuatu seperti:

class GameState:
  loadV1(stream):
    // do stuff

  loadV2(stream):
    // do different stuff

  loadV3(stream):
    // yet other stuff

  save(stream):
    // note this is version 3
    stream.write(3)
    // write V3 data

  load(stream):
    version = stream.read()
    if version == 1: loadV1(stream)
    else if version == 2: loadV2(stream)
    else if version == 3: loadV3(stream)

Anda dapat melakukan ini untuk seluruh file, untuk setiap bagian file, untuk objek / komponen gim individual, dll. Persisnya pemisahan apa yang terbaik tergantung pada gim Anda dan jumlah status yang Anda serialkan.

Perhatikan bahwa ini hanya membuat Anda sejauh ini. Pada titik tertentu Anda mungkin mengubah game Anda cukup sehingga menyimpan data dari versi sebelumnya tidak masuk akal. Misalnya, RPG mungkin memiliki kelas karakter berbeda yang dapat dipilih pemain. Jika Anda menghapus kelas karakter tidak ada banyak yang dapat Anda lakukan dengan menyimpan karakter yang memiliki kelas itu. Mungkin Anda bisa mengonversinya ke kelas serupa yang masih ada ... mungkin. Hal yang sama berlaku jika Anda cukup mengubah bagian lain dari gim sehingga tidak terlalu mirip dengan versi lama.

Ketahuilah bahwa begitu Anda mengirim game, itu sudah "selesai." Anda mungkin merilis DLC atau pembaruan lainnya dari waktu ke waktu tetapi itu tidak akan menjadi perubahan besar pada gim itu sendiri. Ambil sebagian besar MMO misalnya: WoW telah dipertahankan selama bertahun-tahun dengan pembaruan dan perubahan baru tetapi masih kurang lebih sama dengan permainan ketika pertama kali keluar.

Untuk pengembangan awal saya tidak akan khawatir tentang itu. Penghematan adalah fana dalam pengujian awal. Ini cerita lain setelah Anda mendapatkan beta publik.


1
Ini. Sayangnya, ini jarang bekerja secantik yang diiklankan. Biasanya fungsi pemuatan tersebut bergantung pada fungsi pembantu ( ReadCharacterdapat memanggil ReadStat, yang mungkin atau tidak berubah dari satu versi ke versi berikutnya), jadi Anda harus menyimpan versi untuk masing-masingnya, membuatnya semakin sulit untuk mengikuti. Seperti biasa, tidak ada peluru perak, dan menjaga fungsi pemuatan lama adalah titik awal yang bagus.
Piyama Panda

5

Cara sederhana untuk mencapai kemiripan versi adalah dengan memahami anggota objek yang Anda serialkan. Jika kode Anda memiliki pemahaman tentang berbagai jenis data yang akan diserialkan, Anda bisa mendapatkan ketahanan tanpa melakukan terlalu banyak pekerjaan.

Katakanlah kita memiliki objek berseri yang terlihat seperti ini:

ObjectType
{
  m_name = "a string"
  m_size = { 1.2, 2.1 }
  m_someStruct = {
    m_deeperInteger = 5
    m_radians = 3.14
  }
}

Seharusnya mudah untuk melihat bahwa tipe ObjectTypememiliki data yang dipanggil anggota m_name, m_sizedan m_someStruct. Jika Anda dapat mengulang atau menghitung anggota data selama waktu berjalan (entah bagaimana) maka saat membaca file ini Anda dapat membaca dalam nama anggota dan mencocokkannya dengan anggota aktual dalam instance objek Anda.

Selama fase pencarian ini jika Anda tidak menemukan anggota data yang cocok Anda dapat dengan aman mengabaikan bagian file penyimpanan ini. Sebagai contoh katakanlah versi 1.0 dari SomeStructmemiliki anggota m_namedata. Kemudian Anda menambal dan anggota data ini telah dihapus seluruhnya. Saat memuat file simpan Anda, Anda akan menemukan m_namedan mencari anggota yang cocok dan tidak menemukan yang cocok. Kode Anda dapat dengan mudah beralih ke anggota berikutnya dalam file tanpa mogok. Ini memungkinkan Anda menghapus anggota data tanpa khawatir merusak file lama.

Demikian pula jika Anda menambahkan tipe data anggota baru dan mencoba memuat dari file penyimpanan lama, kode Anda mungkin tidak menginisialisasi anggota baru. Ini dapat dimanfaatkan untuk keuntungan: anggota data baru dapat dimasukkan ke dalam menyimpan file selama penambalan secara manual, mungkin dengan memperkenalkan nilai default (atau dengan cara yang lebih cerdas).

Format ini juga memungkinkan menyimpan file dengan mudah dimanipulasi atau dimodifikasi dengan tangan; urutan di mana anggota data tidak benar-benar berkaitan dengan validitas rutin serialisasi. Setiap anggota melihat ke atas dan diinisialisasi secara independen. Ini mungkin kebaikan yang menambahkan sedikit kekokohan ekstra.

Semua ini dapat dicapai melalui beberapa bentuk introspeksi jenis. Anda ingin dapat meminta anggota data dengan pencarian string, dan dapat mengetahui tipe data sebenarnya dari anggota data tersebut. Ini dapat dicapai dalam C ++ menggunakan bentuk introspeksi khusus, dan bahasa lain mungkin memiliki fasilitas introspeksi bawaan.


Ini akan berguna untuk membuat data dan kelas lebih kuat. (Dalam. NET fitur ini disebut "refleksi"). Saya bertanya-tanya tentang koleksi ... AI saya rumit dan menggunakan banyak koleksi sementara untuk memproses data. Haruskah saya mencoba menghindari menyimpannya ...? Mungkin membatasi tabungan ke "titik aman" di mana pemrosesan selesai.
Roti gandum

@aman Jika Anda menyimpan koleksi maka Anda dapat menulis data aktual dalam koleksi ini seperti dalam contoh asli saya, kecuali dalam "format array", seperti dalam banyak dari mereka berturut-turut. Anda masih dapat menerapkan ide yang sama untuk setiap elemen array, atau wadah lainnya. Anda hanya perlu menulis beberapa "serializer array" generik, "daftar serializer" dll. Jika Anda menginginkan "serializer kontainer" generik, Anda mungkin memerlukan abstrak SerializingIteratoratau semacamnya, dan iterator ini akan diterapkan untuk setiap jenis kontainer.
RandyGaul

1
Oh dan ya, Anda harus mencoba untuk menghindari menyimpan koleksi rumit dengan pointer sebanyak yang Anda bisa. Seringkali hal ini dapat dihindari dengan banyak pemikiran dan desain yang cerdas. Serialisasi adalah sesuatu yang bisa menjadi sangat rumit, sehingga akan terbayar untuk mencoba menyederhanakannya sebanyak mungkin. @aman
RandyGaul

Ada juga masalah deserializing objek ketika kelas telah berubah ... Saya pikir deserializer .NET akan crash dalam banyak kasus.
Roti gandum

2

Ini adalah masalah yang ada tidak hanya pada game, tetapi juga pada aplikasi pertukaran file apa pun. Tentu saja, tidak ada solusi yang sempurna, dan mencoba membuat format file yang sesuai dengan segala jenis perubahan sepertinya tidak mungkin, jadi mungkin ide yang baik untuk mempersiapkan jenis perubahan yang Anda harapkan.

Sebagian besar waktu, Anda mungkin hanya akan menambah / menghapus bidang dan nilai, sambil menjaga struktur umum file Anda tidak tersentuh. Dalam hal ini, Anda cukup menulis kode untuk mengabaikan bidang yang tidak dikenal, dan menggunakan default yang masuk akal ketika nilai tidak dapat dipahami / diuraikan. Menerapkan ini sangat mudah, dan saya sering melakukannya.

Namun, terkadang Anda ingin mengubah struktur file. Katakan dari berbasis teks ke biner; atau dari bidang tetap ke ukuran-nilai. Dalam kasus seperti itu, kemungkinan besar Anda ingin membekukan sumber pembaca file lama, dan membuat yang baru untuk jenis file baru, seperti dalam solusi Sean. Pastikan Anda mengisolasi seluruh pembaca lawas, atau Anda mungkin akhirnya mengubah sesuatu yang memengaruhinya. Saya merekomendasikan ini hanya untuk perubahan struktur file utama.

Kedua metode ini harus bekerja untuk sebagian besar kasus, tetapi perlu diingat bahwa mereka bukan satu-satunya perubahan yang mungkin Anda temui. Saya memiliki kasus di mana saya harus mengubah seluruh kode pemuatan level dari membaca lengkap menjadi streaming (untuk versi mobile game, yang seharusnya bekerja pada perangkat dengan bandwidth dan memori yang berkurang secara signifikan). Perubahan seperti ini jauh lebih dalam dan kemungkinan besar akan membutuhkan perubahan di banyak bagian lain permainan, beberapa di antaranya memerlukan perubahan dalam struktur file itu sendiri.


0

Pada tingkat yang lebih tinggi: jika Anda menambahkan fitur baru ke dalam gim, miliki fungsi "Tebak Nilai Baru" yang dapat mengambil fitur lama dan tebak seperti apa nilai yang baru.

Contoh mungkin membuat ini lebih jelas. Misalkan sebuah kota pemodelan permainan, dan versi 1.0 melacak keseluruhan tingkat perkembangan kota, sementara versi 1.1 menambahkan bangunan spesifik seperti Peradaban. (Secara pribadi, saya lebih suka melacak perkembangan keseluruhan, sebagai kurang realistis; tapi saya ngelantur.) GuessNewValues ​​() untuk 1.1, diberikan 1.0 savefile, akan dimulai dengan angka tingkat pengembangan yang lama, dan tebak, berdasarkan itu, apa bangunan akan dibangun di kota - mungkin melihat budaya kota, posisi geografisnya, fokus pengembangannya, hal semacam itu.

Saya berharap bahwa ini dapat dipahami secara umum - bahwa jika Anda menambahkan fitur baru ke gim, memuat savefile yang belum memiliki fitur-fitur tersebut membutuhkan tebakan terbaik tentang seperti apa data baru itu, dan menggabungkannya. dengan data yang Anda muat.

Untuk sisi level rendah, saya akan mendukung jawaban Sean Middleditch (yang saya pilih): pertahankan logika muatan yang ada, mungkin bahkan menyimpan versi lama dari kelas yang relevan, dan panggil dulu bahwa, lalu konverter.


0

Saya akan menyarankan pergi dengan sesuatu seperti XML (jika Anda menyimpan file sangat kecil) dengan cara itu Anda hanya perlu 1 fungsi untuk menangani markup tidak peduli apa yang Anda masukkan ke dalamnya. Simpul root dari dokumen itu dapat mendeklarasikan versi yang menyimpan permainan dan memungkinkan Anda untuk menulis kode untuk memperbarui file ke versi terbaru jika perlu.

<save version="1">
  <player name="foo" score="10" />
  <data>![CDATA[lksdf9owelkjlkdfjdfgdfg]]</data>
</save>

Ini juga berarti Anda dapat menerapkan transformasi jika Anda ingin mengonversi data ke "format versi saat ini" sebelum memuat data sehingga alih-alih memiliki banyak fungsi berversi yang ada di sekitar Anda hanya akan memiliki sekumpulan file xsl yang Anda pilih. untuk melakukan konversi. Ini bisa memakan waktu lama jika Anda tidak terbiasa dengan xsl.

Jika file save Anda xml besar bisa menjadi masalah, biasanya saya sudah menyimpan file yang bekerja dengan sangat baik di mana Anda hanya membuang pasangan nilai kunci ke file seperti ini ...

version=1
player=foo
data=lksdf9owelkjlkdfjdfgdfg
score=10

Kemudian ketika Anda membaca dari file ini Anda selalu menulis dan membaca variabel dengan cara yang sama, jika Anda memerlukan variabel baru, Anda membuat fungsi baru untuk menulis dan membacanya. Anda hanya bisa menulis fungsi untuk tipe variabel sehingga Anda akan memiliki "pembaca string" dan "pembaca int", ini hanya akan berguna jika Anda mengubah jenis variabel antara versi tetapi Anda tidak boleh melakukan itu karena variabel berarti sesuatu yang lain di titik ini sehingga Anda harus membuat variabel baru sebagai gantinya dengan nama yang berbeda.

Cara lain tentu saja adalah dengan menggunakan format tipe database atau sesuatu seperti file csv, tetapi itu tergantung pada data yang Anda simpan.

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.