Bagaimana saya harus menyusun sistem pemuatan aset yang dapat diperpanjang?

19

Untuk mesin permainan hobi di Jawa, saya ingin membuat kode manajer aset / sumber daya yang sederhana namun fleksibel. Aset adalah suara, gambar, animasi, model, tekstur, dan lain-lain. Setelah beberapa jam browsing dan beberapa percobaan kode, saya masih tidak yakin bagaimana merancang hal ini.

Secara khusus, saya mencari bagaimana saya bisa mendesain manajer sedemikian rupa sehingga mengabstraksi bagaimana jenis aset spesifik dimuat dan dari mana aset tersebut dimuat. Saya ingin dapat mendukung sistem file dan penyimpanan RDBMS tanpa sisa program yang perlu mengetahuinya. Demikian pula, saya ingin menambahkan aset deskripsi animasi (FPS, bingkai untuk di-render, referensi ke gambar sprite, dan lain-lain) yang merupakan XML. Saya harus dapat menulis kelas untuk ini dengan fungsi untuk menemukan dan membaca file XML dan membuat dan mengembalikan AnimationAssetkelas dengan informasi itu. Saya mencari desain berbasis data .

Saya dapat menemukan banyak informasi tentang apa yang harus dilakukan oleh manajer aset, tetapi tidak tentang bagaimana melakukannya. Obat generik yang terlibat tampaknya menghasilkan beberapa bentuk cascading kelas, atau beberapa bentuk kelas pembantu. Namun saya belum melihat contoh yang jelas yang tidak terlihat seperti peretasan pribadi, atau titik konsensus.

pengguna8363
sumber

Jawaban:

23

Saya akan mulai dengan tidak memikirkan manajer aset . Memikirkan arsitektur Anda dengan istilah yang tidak jelas (seperti "manajer") cenderung membuat Anda secara mental menyapu banyak detail di bawah permadani, dan akibatnya menjadi lebih sulit untuk menyelesaikan suatu solusi.

Fokus pada kebutuhan spesifik Anda, yang tampaknya dilakukan dengan menciptakan mekanisme pemuatan sumber daya yang mengabstraksi penyimpanan asal yang mendasarinya dan memungkinkan untuk diperpanjang dari set tipe yang didukung. Tidak ada yang benar-benar dalam pertanyaan Anda mengenai, misalnya, caching sumber daya yang sudah dimuat - yang baik-baik saja, karena sesuai dengan prinsip tanggung jawab tunggal Anda mungkin harus membangun cache aset sebagai entitas yang terpisah dan menggabungkan dua antarmuka di tempat lain , sewajarnya.

Untuk mengatasi masalah khusus Anda, Anda harus merancang loader agar tidak memuat aset apa pun, melainkan mendelegasikan tanggung jawab tersebut ke antarmuka yang dirancang untuk memuat jenis aset tertentu. Sebagai contoh:

interface ITypeLoader {
  object Load (Stream assetStream);
}

Anda bisa membuat kelas baru yang mengimplementasikan antarmuka ini, dengan masing-masing kelas baru yang dirancang untuk memuat jenis data tertentu dari aliran. Dengan menggunakan stream, tipe loader dapat ditulis terhadap antarmuka umum, agnostik penyimpanan, dan tidak harus sulit dikodekan untuk memuat dari disk atau database; ini bahkan akan memungkinkan Anda untuk memuat aset Anda dari aliran jaringan (yang bisa sangat berguna dalam mengimplementasikan hot-reload aset saat game Anda berjalan di konsol dan alat pengeditan Anda pada PC yang terhubung ke jaringan).

Pemuat aset utama Anda harus dapat mendaftarkan dan melacak pemuat khusus tipe ini:

class AssetLoader {
  public void RegisterType (string key, ITypeLoader loader) {
    loaders[key] = loader;
  }

  Dictionary<string, ITypeLoader> loaders = new Dictionary<string, ITypeLoader>();
}

"Kunci" yang digunakan di sini bisa apa saja yang Anda suka - dan itu tidak harus berupa string, tetapi mudah untuk memulai. Kuncinya akan memperhitungkan bagaimana Anda mengharapkan pengguna untuk mengidentifikasi aset tertentu dan akan digunakan untuk mencari loader yang sesuai. Karena Anda ingin menyembunyikan fakta bahwa implementasinya mungkin menggunakan sistem file atau database, Anda tidak bisa membuat pengguna merujuk ke aset melalui jalur sistem file atau semacamnya.

Pengguna harus merujuk ke aset dengan informasi minimal. Dalam beberapa kasus, hanya nama file saja sudah cukup, tetapi saya telah menemukan bahwa sering diinginkan untuk menggunakan pasangan jenis / nama sehingga semuanya sangat eksplisit. Dengan demikian, pengguna dapat merujuk ke instance bernama salah satu file XML animasi Anda sebagai "AnimationXml","PlayerWalkCycle".

Di sini, AnimationXmlakan menjadi kunci di mana Anda mendaftar AnimationXmlLoader, yang mengimplementasikan IAssetLoader. Jelas, PlayerWalkCyclemengidentifikasi aset spesifik. Diberi nama jenis dan nama sumber daya, pemuat aset Anda dapat meminta penyimpanan persistennya untuk byte mentah dari aset itu. Karena kita akan mencapai generalitas maksimum di sini, Anda dapat menerapkan ini dengan melewatkan loader sebagai sarana akses penyimpanan saat Anda membuatnya, memungkinkan Anda untuk mengganti media penyimpanan dengan apa pun yang dapat menyediakan aliran di kemudian hari:

interface IAssetStreamProvider {
  Stream GetStream (string type, string name);
}

class AssetLoader {
  public AssetLoader (IAssetStreamProvider streamProvider) {
    provider = streamProvider;
  }

  object LoadAsset (string type, string name) {
    var loader = loaders[type];
    var stream = provider.GetStream(type, name);

    return loader.Load(stream);
  }

  public void RegisterType (string type, ITypeLoader loader) {
    loaders[type] = loader;
  }

  IAssetStreamProvider provider;
  Dictionary<string, ITypeLoader> loaders = new Dictionary<string, ITypeLoader>();
}

Penyedia aliran yang sangat sederhana hanya akan melihat dalam direktori root aset tertentu untuk subdirektori bernama typedan memuat byte mentah dari file yang dinamai nameke dalam aliran dan mengembalikannya.

Singkatnya, apa yang Anda miliki di sini adalah sistem di mana:

  • Ada kelas yang tahu cara membaca byte mentah dari beberapa jenis penyimpanan backend (disk, database, aliran jaringan, apa pun).
  • Ada kelas yang tahu cara mengubah aliran byte mentah menjadi jenis sumber daya tertentu dan mengembalikannya.
  • "Pemuat aset" Anda yang sebenarnya hanya memiliki koleksi di atas yang disebutkan di atas dan tahu cara menyalurkan output dari penyedia aliran ke pemuat jenis khusus dan dengan demikian menghasilkan aset konkret. Dengan memaparkan cara-cara untuk mengonfigurasikan penyedia aliran dan pemuat spesifik-jenis, Anda memiliki sistem yang dapat diperluas oleh klien (atau diri Anda sendiri) tanpa harus mengubah kode pemuat aset yang sebenarnya.

Beberapa peringatan dan catatan akhir:

  • Kode di atas pada dasarnya adalah C #, tetapi harus diterjemahkan ke bahasa apa saja dengan sedikit usaha. Untuk memfasilitasi ini, saya menghilangkan banyak hal seperti memeriksa kesalahan atau menggunakan dengan benar IDisposabledan idiom lain yang mungkin tidak berlaku langsung dalam bahasa lain. Itu dibiarkan sebagai pekerjaan rumah bagi pembaca.

  • Demikian pula, saya mengembalikan aset konkret seperti di objectatas, tetapi Anda dapat menggunakan generik atau templat atau apa pun untuk menghasilkan jenis objek yang lebih spesifik jika Anda suka (Anda harus, senang bekerja dengannya).

  • Seperti di atas, saya tidak berurusan dengan caching sama sekali di sini. Namun, Anda dapat menambahkan caching dengan mudah dan dengan jenis umum dan konfigurasi yang sama. Cobalah dan lihat!

  • Ada banyak cara untuk melakukan ini, dan tentu saja tidak ada satu cara atau konsensus, itulah sebabnya Anda belum dapat menemukannya. Saya telah mencoba memberikan kode yang cukup untuk menyampaikan poin-poin spesifik tanpa mengubah jawaban ini menjadi dinding kode yang sangat panjang. Ini sudah sangat lama. Jika Anda memiliki pertanyaan klarifikasi, jangan ragu untuk berkomentar atau menemukan saya di obrolan .


sumber
1
Pertanyaan bagus dan jawaban bagus yang mendorong solusi menuju tidak hanya desain yang didorong data tetapi juga bagaimana mulai berpikir dengan cara yang didorong data.
Patrick Hughes
Jawaban yang sangat bagus dan mendalam. Saya suka bagaimana Anda menafsirkan pertanyaan saya dan memberi tahu saya apa yang perlu saya ketahui sementara saya merumuskannya dengan sangat buruk. Terima kasih! Kebetulan, bisakah Anda mengarahkan saya ke beberapa sumber tentang Streams?
user8363
"Aliran" hanyalah urutan (berpotensi tanpa akhir yang dapat ditentukan) dari byte atau data. Saya sedang berpikir secara khusus tentang C # 's Stream , tetapi Anda mungkin lebih tertarik pada kelas-kelas aliran Java - walaupun diingatkan saya tidak tahu terlalu banyak Java sehingga mungkin bukan kelas yang ideal untuk digunakan.
Streaming biasanya stateful, di mana objek stream yang diberikan biasanya memiliki posisi baca atau tulis saat ini dalam aliran, dan setiap IO yang Anda lakukan di atasnya terjadi dari posisi itu - itu sebabnya saya menggunakannya sebagai input ke antarmuka aset di atas, karena pada dasarnya mereka mengatakan "inilah beberapa data mentah dan dari mana mulai membaca, membaca darinya dan melakukan apa yang Anda inginkan."
Pendekatan ini menghormati beberapa prinsip inti SOLID dan OOP . Bravo.
Adam Naylor