Apa cara untuk menerapkan sistem buff / debuff yang fleksibel?


66

Gambaran:

Banyak game dengan statistik seperti RPG memungkinkan karakter "buff", mulai dari "Deal 25% extra damage" yang sederhana hingga hal-hal yang lebih rumit seperti "Deal 15 damage kembali ke penyerang saat dipukul."

Spesifikasi setiap tipe buff tidak benar-benar relevan. Saya mencari cara (mungkin berorientasi objek) untuk menangani penggemar sewenang-wenang.

Detail:

Dalam kasus khusus saya, saya memiliki beberapa karakter dalam lingkungan pertempuran berbasis giliran, jadi saya membayangkan penggemar yang terikat dengan acara seperti "OnTurnStart", "OnReceiveDamage", dll. Mungkin setiap penggemar adalah subkelas dari kelas abstrak buff utama, di mana hanya acara terkait yang kelebihan beban. Kemudian setiap karakter dapat memiliki vektor buff yang saat ini diterapkan.

Apakah solusi ini masuk akal? Saya pasti bisa melihat lusinan jenis acara yang diperlukan, rasanya seperti membuat subkelas baru untuk setiap penggemar yang berlebihan, dan sepertinya tidak memungkinkan untuk "interaksi" penggemar. Yaitu, jika saya ingin menerapkan cap pada damage boost sehingga bahkan jika Anda memiliki 10 buff berbeda yang semuanya memberikan 25% damage tambahan, Anda hanya melakukan 100% ekstra daripada 250% tambahan.

Dan ada situasi yang lebih rumit yang idealnya bisa saya kendalikan. Saya yakin semua orang bisa memberikan contoh bagaimana buff yang lebih canggih berpotensi berinteraksi satu sama lain dengan cara yang mungkin tidak saya inginkan sebagai pengembang game.

Sebagai seorang programmer C ++ yang relatif tidak berpengalaman (saya biasanya menggunakan C dalam embedded system), saya merasa solusi saya sederhana dan mungkin tidak memanfaatkan bahasa berorientasi objek secara penuh.

Pikiran? Adakah orang di sini yang merancang sistem buff yang cukup kuat sebelumnya?

Sunting: Mengenai Jawaban:

Saya memilih jawaban terutama berdasarkan detail yang bagus dan jawaban yang solid untuk pertanyaan yang saya ajukan, tetapi membaca tanggapannya memberi saya lebih banyak wawasan.

Mungkin tidak mengejutkan, sistem yang berbeda atau sistem tweak tampaknya berlaku lebih baik untuk situasi tertentu. Sistem apa yang bekerja paling baik untuk gim saya akan bergantung pada jenis, ragam, dan jumlah penggemar yang ingin saya terapkan.

Untuk gim seperti Diablo 3 (disebutkan di bawah), di mana hampir semua peralatan dapat mengubah kekuatan penggemar , penggemar hanya sistem statistik karakter yang sepertinya merupakan ide bagus bila memungkinkan.

Untuk situasi berbasis giliran yang saya ikuti, pendekatan berbasis peristiwa mungkin lebih cocok.

Bagaimanapun, saya masih berharap seseorang datang bersama dengan peluru ajaib "OO" mewah yang akan memungkinkan saya untuk menerapkan jarak pergerakan +2 per buff giliran , kesepakatan 50% dari kerusakan yang diambil kembali ke buff penyerang , dan secara otomatis teleport ke ubin terdekat ketika diserang dari 3 atau lebih ubin buff dalam satu sistem tanpa mengubah buff kekuatan +5 menjadi subkelasnya sendiri.

Saya pikir hal yang paling dekat adalah jawaban yang saya tandai, tetapi lantainya masih terbuka. Terima kasih untuk semua orang atas masukannya.


Saya tidak memposting ini sebagai jawaban, karena saya hanya brainstorming, tetapi bagaimana dengan daftar penggemar? Setiap buff memiliki konstanta dan pengubah faktor. Konstan akan menjadi +10 kerusakan, faktor akan menjadi 1,10 untuk peningkatan kerusakan 10%. Dalam perhitungan kerusakan Anda, Anda mengulangi semua buff, untuk mendapatkan pengubah total, dan kemudian Anda memaksakan batasan yang Anda inginkan. Anda akan melakukan ini untuk segala jenis atribut yang dapat dimodifikasi. Anda akan memerlukan metode kasus khusus untuk hal-hal rumit.
William Mariager

Kebetulan saya sudah menerapkan sesuatu seperti itu untuk objek Statistik saya ketika saya sedang membuat sistem untuk senjata dan aksesoris yang bisa digunakan. Seperti yang Anda katakan, ini adalah solusi yang cukup layak untuk penggemar yang hanya mengubah atribut yang ada, tetapi tentu saja saya akan ingin penggemar tertentu berakhir setelah X berubah, yang lain akan berakhir setelah efeknya terjadi Y kali, dll. Saya tidak sebutkan ini dalam pertanyaan utama karena sudah sangat lama.
gkimsey

1
jika Anda memiliki metode "onReceiveDamage" yang dipanggil oleh sistem pengiriman pesan, atau secara manual, atau dengan cara lain, itu seharusnya cukup mudah untuk menyertakan referensi ke siapa / apa Anda menerima kerusakan. Jadi, Anda dapat membuat informasi ini tersedia untuk penggemar Anda

Benar, saya mengharapkan setiap templat acara untuk kelas Buff abstrak akan menyertakan parameter yang relevan seperti itu. Itu pasti akan berhasil, tapi saya ragu karena rasanya tidak akan bagus. Saya mengalami kesulitan membayangkan MMORPG dengan beberapa ratus buff yang berbeda memiliki kelas terpisah yang ditentukan untuk setiap buff, memilih dari seratus event yang berbeda. Bukannya saya membuat banyak buff (mungkin mendekati 30), tetapi jika ada sistem yang lebih sederhana, lebih elegan, atau lebih fleksibel, saya ingin menggunakannya. Sistem lebih fleksibel = buff / kemampuan lebih menarik.
gkimsey

4
Ini bukan jawaban yang baik untuk masalah interaksi, tetapi menurut saya pola dekorator berlaku dengan baik di sini; cukup aplikasikan buff lagi (dekorator) di atas satu sama lain. Mungkin dengan sistem untuk menangani interaksi dengan "menggabungkan" buff bersama-sama (mis. 10x25% bergabung menjadi satu buff 100%).
ashes999

Jawaban:


32

Ini adalah masalah yang rumit, karena Anda berbicara tentang beberapa hal berbeda yang (saat ini) disatukan sebagai 'penggemar':

  • pengubah atribut pemain
  • efek khusus yang terjadi pada acara-acara tertentu
  • kombinasi di atas.

Saya selalu menerapkan yang pertama dengan daftar efek aktif untuk karakter tertentu. Penghapusan dari daftar, baik berdasarkan durasi atau secara eksplisit sepele jadi saya tidak akan membahasnya di sini. Setiap Efek berisi daftar pengubah atribut, dan dapat menerapkannya ke nilai yang mendasarinya melalui perkalian sederhana.

Lalu saya bungkus dengan fungsi untuk mengakses atribut yang dimodifikasi. misalnya.:

def get_current_attribute_value(attribute_id, criteria):
    val = character.raw_attribute_value[attribute_id]
    # Accumulate the modifiers
    for effect in character.all_effects:
        val = effect.apply_attribute_modifier(attribute_id, val, criteria)
    # Make sure it doesn't exceed game design boundaries
    val = apply_capping_to_final_value(val)
    return val

class Effect():
    def apply_attribute_modifier(attribute_id, val, criteria):
        if attribute_id in self.modifier_list:
            modifier = self.modifier_list[attribute_id]
            # Does the modifier apply at this time?
            if modifier.criteria == criteria:
                # Apply multiplicative modifier
                return val * modifier.amount
        else:
            return val

class Modifier():
    amount = 1.0 # default that has no effect
    criteria = None # applies all of the time

Itu memungkinkan Anda menerapkan efek multiplikasi dengan cukup mudah. Jika Anda memerlukan efek tambahan juga, putuskan urutan Anda akan menerapkannya (mungkin terakhir tambahan) dan jalankan melalui daftar dua kali. (Saya mungkin memiliki daftar pengubah terpisah di Efek, satu untuk multiplikatif, satu untuk tambahan).

Nilai kriteria adalah untuk membiarkan Anda menerapkan "+ 20% vs Mati" - setel nilai UNDEAD pada Efek dan hanya meneruskan nilai UNDEAD get_current_attribute_value()ketika Anda menghitung roll kerusakan terhadap musuh undead.

Kebetulan, saya tidak akan tergoda untuk mencoba dan menulis sistem yang berlaku dan tidak menerapkan nilai langsung ke nilai atribut yang mendasarinya - hasil akhirnya adalah bahwa atribut Anda sangat mungkin menjauh dari nilai yang dimaksudkan karena kesalahan. (mis. jika Anda mengalikan sesuatu dengan 2, tapi kemudian tutup, ketika Anda membaginya dengan 2 lagi, itu akan lebih rendah daripada yang dimulai dengan.)

Adapun efek berbasis event, seperti "Deal 15 damage kembali ke penyerang ketika mengenai", Anda dapat menambahkan metode pada kelas Effect untuk itu. Tetapi jika Anda ingin perilaku yang berbeda dan sewenang-wenang (mis. Beberapa efek untuk peristiwa di atas mungkin mencerminkan kerusakan kembali, beberapa mungkin menyembuhkan Anda, itu mungkin akan memindahkan Anda secara acak, apa pun) Anda akan memerlukan fungsi khusus atau kelas untuk menanganinya. Anda dapat menetapkan fungsi ke penangan acara pada efek, maka Anda bisa memanggil penangan acara pada efek aktif.

# This is a method on a Character, called during combat
def on_receive_damage(damage_info):
    for effect in character.all_effects:
        effect.on_receive_damage(character, damage_info)

class Effect():
    self.on_receive_damage_handler = DoNothing # a default function that does nothing
    def on_receive_damage(character, damage_info):
        self.on_receive_damage_handler(character, damage_info)

def reflect_damage(character, damage_info):
    damage_info.attacker.receive_damage(15)

reflect_damage_effect = new Effect()
reflect_damage_effect.on_receive_damage_handler = reflect_damage
my_character.all_effects.add(reflect_damage_effect)

Jelas kelas Efek Anda akan memiliki pengendali acara untuk setiap jenis acara, dan Anda dapat menetapkan fungsi-fungsi pengendali sebanyak yang Anda butuhkan dalam setiap kasus. Anda tidak perlu membuat subkelas Efek, karena masing-masing didefinisikan oleh komposisi pengubah atribut dan penangan peristiwa yang dikandungnya. (Ini mungkin juga akan berisi nama, durasi, dll.)


2
+1 untuk detail yang sangat baik. Ini adalah respons terdekat untuk secara resmi menjawab pertanyaan saya seperti yang saya lihat. Pengaturan dasar di sini tampaknya memungkinkan banyak fleksibilitas, dan abstraksi kecil dari apa yang bisa menjadi logika permainan yang berantakan. Seperti yang Anda katakan, efek yang lebih funky masih akan membutuhkan kelas mereka sendiri, tetapi ini menangani sebagian besar kebutuhan sistem "penggemar" yang khas, saya pikir.
gkimsey

+1 untuk menunjukkan perbedaan konseptual yang tersembunyi di sini. Tidak semua dari mereka akan bekerja dengan logika pembaruan berbasis peristiwa yang sama. Lihat jawaban @ Ross untuk aplikasi yang sama sekali berbeda. Keduanya harus ada di sebelah satu sama lain.
ctietze

22

Dalam sebuah game yang saya kerjakan dengan seorang teman di kelas kami membuat sistem buff / debuff ketika pengguna terjebak di rumput tinggi dan ubin percepatan dan yang tidak, dan beberapa hal kecil seperti pendarahan dan racun.

Idenya sederhana, dan sementara kami menerapkannya di Python, itu agak efektif.

Pada dasarnya, begini caranya:

  • Pengguna memiliki daftar buff dan debuff yang saat ini diterapkan (perhatikan bahwa buff dan debuff relatif sama, hanya saja efeknya memiliki hasil yang berbeda)
  • Penggemar memiliki berbagai atribut seperti durasi, nama, dan teks untuk menampilkan informasi, dan waktu hidup. Yang penting adalah waktu hidup, durasi, dan referensi ke aktor yang digunakan buff ini.
  • Untuk Buff, ketika itu dilampirkan ke pemain melalui player.apply (buff / debuff), itu akan memanggil metode start (), ini akan menerapkan perubahan kritis pada pemain seperti meningkatkan kecepatan atau memperlambat.
  • Kami kemudian akan mengulangi setiap buff dalam loop pembaruan dan buff akan memperbarui, ini akan menambah waktu mereka hidup. Subclass akan menerapkan hal-hal seperti meracuni pemain, memberi pemain HP dari waktu ke waktu, dll.
  • Ketika buff dilakukan untuk, yang berarti timeAlive> = durasi, logika pembaruan akan menghapus buff dan memanggil metode finish (), yang akan bervariasi dari menghapus batasan kecepatan pada pemain hingga menyebabkan jari-jari kecil (pikirkan efek bom) setelah DoT)

Sekarang bagaimana cara menerapkan buff dari dunia adalah cerita yang berbeda. Inilah makanan saya untuk dipikirkan.


1
Ini terdengar seperti penjelasan yang lebih baik tentang apa yang saya coba uraikan di atas. Ini relatif sederhana, tentu mudah dimengerti. Anda pada dasarnya menyebutkan tiga "peristiwa" di sana (OnApply, OnTimeTick, OnExpired) untuk lebih mengaitkannya dengan pemikiran saya. Seperti apa adanya, itu tidak akan mendukung hal-hal seperti mengembalikan kerusakan ketika terkena dan sebagainya, tetapi itu skala yang lebih baik untuk banyak penggemar. Saya lebih suka tidak membatasi apa yang dapat dilakukan penggemar saya (yang = membatasi jumlah acara yang saya buat yang harus dipanggil oleh logika permainan utama), tetapi skalabilitas penggemar mungkin lebih penting. Terima kasih atas masukan Anda!
gkimsey

Ya kami tidak menerapkan hal seperti itu. Kedengarannya sangat rapi dan konsep yang hebat (semacam penggemar Thorn).
Ross

@gkimsey Untuk hal-hal seperti Thorn dan buff pasif lainnya, saya akan menerapkan logika di kelas Mob Anda sebagai stat pasif mirip dengan kerusakan atau kesehatan dan meningkatkan stat ini saat menerapkan buff. Ini menyederhanakan banyak kasus ketika Anda memiliki beberapa buff duri serta menjaga antarmuka bersih (10 buff akan menunjukkan 1 return damage daripada 10) dan membiarkan sistem buff tetap sederhana.
3Doubloons

Ini adalah pendekatan yang hampir sederhana, tetapi saya mulai berpikir tentang diri saya sendiri ketika bermain Diablo 3. Saya memperhatikan kehidupan mencuri, kehidupan yang terkena, kerusakan pada penyerang jarak dekat, dll semua adalah statistik mereka sendiri di jendela karakter. Memang, D3 tidak memiliki sistem buffing paling rumit atau interaksi di dunia, tapi itu hampir tidak sepele. Ini sangat masuk akal. Namun, ada 15 buff yang berpotensi berbeda dengan 12 efek berbeda yang akan jatuh ke dalam ini. Sepertinya aneh padding out lembar statistik karakter ....
gkimsey

11

Saya tidak yakin apakah Anda masih membaca ini tetapi saya telah berjuang dengan masalah semacam ini untuk waktu yang lama.

Saya telah merancang berbagai jenis sistem pengaruh. Saya akan membahasnya sebentar sekarang. Ini semua berdasarkan pengalaman saya. Saya tidak mengklaim tahu semua jawaban.


Pengubah Statis

Jenis sistem ini sebagian besar mengandalkan bilangan bulat sederhana untuk menentukan modifikasi apa pun. Misalnya, +100 ke Max HP, +10 untuk menyerang dan sebagainya. Sistem ini juga bisa menangani persen. Anda hanya perlu memastikan bahwa penumpukan tidak lepas kendali.

Saya tidak pernah benar-benar menembolok nilai yang dihasilkan untuk jenis sistem ini. Misalnya, jika saya ingin menampilkan kesehatan maksimal dari sesuatu, saya akan menghasilkan nilai di tempat. Ini mencegah hal-hal dari rawan kesalahan dan hanya lebih mudah dimengerti untuk semua orang yang terlibat.

(Saya bekerja di Jawa sehingga yang mengikuti adalah berbasis Java tetapi harus bekerja dengan beberapa modifikasi untuk bahasa lain) Sistem ini dapat dengan mudah dilakukan dengan menggunakan enum untuk jenis modifikasi, dan kemudian bilangan bulat. Hasil akhirnya dapat ditempatkan ke dalam semacam koleksi yang memiliki pasangan kunci, nilai yang diurutkan. Ini akan menjadi pencarian cepat dan perhitungan, sehingga kinerjanya sangat baik.

Secara keseluruhan, ini bekerja sangat baik hanya dengan pengubah statis datar. Padahal, kode harus ada di tempat yang tepat untuk pengubah yang akan digunakan: getAttack, getMaxHP, getMeleeDamage, dan seterusnya dan seterusnya.

Di mana metode ini gagal (bagi saya) adalah interaksi yang sangat kompleks antara penggemar. Tidak ada cara nyata yang mudah untuk berinteraksi kecuali dengan sedikit mengatasinya. Itu memang memiliki beberapa kemungkinan interaksi sederhana. Untuk melakukan itu, Anda harus membuat modifikasi dengan cara Anda menyimpan pengubah statis. Alih-alih menggunakan enum sebagai kunci, Anda menggunakan sebuah String. String ini akan menjadi nama Enum + variabel ekstra. 9 kali dari 10, variabel tambahan tidak digunakan, jadi Anda masih mempertahankan nama enum sebagai kunci.

Mari kita lakukan contoh cepat: Jika Anda ingin dapat mengubah kerusakan terhadap makhluk mayat hidup, Anda dapat memiliki pasangan yang dipesan seperti ini: (DAMAGE_Undead, 10) DAMAGE adalah Enum dan Undead adalah variabel tambahan. Jadi selama pertempuran, Anda dapat melakukan sesuatu seperti:

dam += attacker.getMod(Mod.DAMAGE + npc.getRaceFamily()); //in this case the race family would be undead

Bagaimanapun, ini bekerja dengan cukup baik dan cepat. Tetapi gagal pada interaksi yang kompleks dan memiliki kode "khusus" di mana-mana. Misalnya, pertimbangkan situasi "25% kesempatan untuk berteleportasi pada kematian". Ini adalah yang “cukup” kompleks. Sistem di atas dapat mengatasinya, tetapi tidak mudah, karena Anda memerlukan yang berikut:

  1. Tentukan apakah pemain memiliki mod ini.
  2. Di suatu tempat, miliki beberapa kode untuk mengeksekusi teleportasi, jika berhasil. Lokasi kode ini adalah diskusi sendiri!
  3. Dapatkan data yang tepat dari peta Mod. Apa artinya nilainya? Apakah ini kamar tempat mereka berteleportasi juga? Bagaimana jika seorang pemain memiliki dua mod teleport pada mereka ?? Tidak akankah jumlahnya ditambahkan bersama ?????? KEGAGALAN!

Jadi ini membawa saya ke yang berikutnya:


Sistem Penggemar Yang Sangat Kompleks

Saya pernah mencoba menulis MMORPG 2D sendiri. Ini adalah kesalahan yang mengerikan tetapi saya belajar banyak!

Saya menulis ulang sistem yang terpengaruh 3 kali. Yang pertama menggunakan variasi yang kurang kuat di atas. Yang kedua adalah apa yang akan saya bicarakan.

Sistem ini memiliki serangkaian kelas untuk setiap modifikasi, jadi hal-hal seperti: ChangeHP, ChangeMaxHP, ChangeHPByPercent, ChangeMaxByPercent. Saya memiliki jutaan orang ini - bahkan hal-hal seperti TeleportOnDeath.

Kelas saya memiliki hal-hal yang akan dilakukan sebagai berikut:

  • applyAffect
  • removeAffect
  • checkForInteraction <--- penting

Terapkan dan hapus menjelaskan sendiri (meskipun untuk hal-hal seperti persen, pengaruhnya akan melacak berapa banyak meningkatkan HP untuk memastikan ketika dampaknya berkurang, itu hanya akan menghapus jumlah yang ditambahkan. Ini adalah kereta, lol, dan Butuh waktu lama untuk memastikan itu benar. Saya masih belum mendapatkan perasaan yang baik tentang hal itu.).

Metode checkForInteraction adalah sepotong kode yang sangat rumit. Di masing-masing kelas yang mempengaruhi (yaitu: ChangeHP), itu akan memiliki kode untuk menentukan apakah ini harus dimodifikasi oleh input yang mempengaruhi. Jadi misalnya, jika Anda memiliki sesuatu seperti ....

  • Buff 1: Menawarkan 10 damage api saat menyerang
  • Buff 2: Meningkatkan semua kerusakan akibat kebakaran sebesar 25%.
  • Buff 3: Meningkatkan semua damage api sebesar 15.

Metode checkForInteraction akan menangani semua dampak ini. Untuk melakukan ini, masing-masing pengaruh pada SEMUA pemain di dekat harus diperiksa !! Ini karena jenis pengaruh yang saya hadapi dengan banyak pemain dalam rentang area. Ini berarti kode TIDAK PERNAH MEMILIKI pernyataan khusus seperti di atas - "jika kita baru saja mati, kita harus memeriksa teleport pada kematian". Sistem ini secara otomatis akan menanganinya dengan benar pada waktu yang tepat.

Mencoba untuk menulis sistem ini butuh waktu 2 bulan dan dibuat oleh kepala meledak beberapa kali. NAMUN, itu BENAR-BENAR kuat dan bisa melakukan sejumlah hal gila - terutama ketika Anda memperhitungkan dua fakta berikut untuk kemampuan dalam permainan saya: 1. Mereka memiliki rentang target (yaitu: tunggal, mandiri, hanya grup, PB AE sendiri , Target PB AE, target AE, dan sebagainya). 2. Kemampuan bisa memiliki lebih dari 1 mempengaruhi mereka.

Seperti yang saya sebutkan di atas, ini adalah sistem pengaruh ke 2 dari 3 untuk game ini. Mengapa saya menjauh dari ini?

Sistem ini memiliki kinerja terburuk yang pernah saya lihat! Itu sangat lambat karena harus melakukan begitu banyak memeriksa setiap hal yang terjadi. Saya mencoba memperbaikinya, tetapi menganggapnya gagal.

Jadi kita sampai pada versi ketiga saya (dan tipe lain dari sistem buff):


Mempengaruhi Kelas Kompleks dengan Penangan

Jadi ini cukup banyak kombinasi dari dua yang pertama: Kita dapat memiliki variabel statis dalam kelas yang mempengaruhi yang berisi banyak fungsi dan data tambahan. Kemudian panggil saja penangan (bagi saya, cukup banyak beberapa metode utilitas statis daripada subclass untuk tindakan tertentu. Tapi saya yakin Anda bisa menggunakan subclass untuk tindakan jika Anda menginginkannya juga) ketika kita ingin melakukan sesuatu.

Kelas Affect akan memiliki semua barang bagus yang berair, seperti tipe target, durasi, jumlah penggunaan, kesempatan untuk mengeksekusi dan seterusnya dan seterusnya.

Kami masih harus menambahkan kode khusus untuk menangani situasi, misalnya, teleport pada kematian. Kami masih harus memeriksa ini dalam kode tempur secara manual, dan kemudian jika ada, kami akan mendapatkan daftar efek. Daftar pengaruh ini berisi semua efek yang saat ini diterapkan pada pemain yang berurusan dengan teleportasi pada saat kematian. Kemudian kita akan melihat masing-masing dan memeriksa untuk melihat apakah itu dieksekusi dan berhasil (Kami akan berhenti di yang sukses pertama). Itu berhasil, kami hanya akan memanggil pawang untuk mengurus ini.

Interaksi dapat dilakukan, jika Anda mau. Itu hanya harus menulis kode untuk mencari penggemar tertentu pada pemain / dll. Karena memiliki kinerja yang baik (lihat di bawah), seharusnya cukup efisien untuk melakukan itu. Itu hanya akan membutuhkan penangan yang lebih kompleks dan sebagainya.

Jadi ia memiliki banyak kinerja sistem pertama dan masih banyak kerumitan seperti yang kedua (tetapi tidak banyak). Di Jawa setidaknya, Anda dapat melakukan beberapa hal rumit untuk mendapatkan kinerja yang hampir pertama dalam kebanyakan kasus (yaitu: memiliki peta enum ( http://docs.oracle.com/javase/6/docs/api/java /util/EnumMap.html ) dengan Enums sebagai kunci dan ArrayList mempengaruhi sebagai nilainya. Ini memungkinkan Anda untuk melihat apakah Anda dengan cepat memengaruhi [karena daftar akan menjadi 0 atau peta tidak memiliki enum] dan tidak memiliki untuk terus-menerus mengulangi daftar pengaruh pemain tanpa alasan. Saya tidak keberatan untuk mengulang pengaruh jika kita membutuhkannya saat ini. Saya akan mengoptimalkan nanti jika itu menjadi masalah).

Saat ini saya sedang membuka kembali (menulis ulang game di Java alih-alih basis kode FastROM seperti semula) MUD saya yang berakhir pada 2005 dan saya baru-baru ini bertemu bagaimana saya ingin menerapkan sistem buff saya? Saya akan menggunakan sistem ini karena bekerja dengan baik di permainan saya yang gagal sebelumnya.

Yah, semoga seseorang, di suatu tempat, akan menemukan beberapa wawasan ini berguna.


6

Kelas yang berbeda (atau fungsi beralamat) untuk setiap buff tidak berlebihan jika perilaku buff tersebut berbeda satu sama lain. Satu hal akan memiliki penggemar + 10% atau + 20% (yang, tentu saja, akan lebih baik direpresentasikan sebagai dua objek dari kelas yang sama), yang lain akan menerapkan efek yang sangat berbeda yang akan memerlukan kode khusus pula. Namun, saya percaya lebih baik memiliki cara standar untuk menyesuaikan logika permainan daripada membiarkan setiap penggemar melakukan apa pun yang diinginkan (dan mungkin mengganggu satu sama lain dengan cara yang tidak terduga, mengganggu keseimbangan permainan).

Saya sarankan membagi setiap "siklus serangan" menjadi langkah-langkah, di mana setiap langkah memiliki nilai dasar, daftar modifikasi yang dapat diterapkan untuk nilai itu (mungkin dibatasi), dan batas akhir. Setiap modifikasi memiliki transformasi identitas sebagai default, dan dapat dipengaruhi oleh nol atau lebih buff / debuff. Spesifikasi setiap modifikasi akan tergantung pada langkah yang diterapkan. Bagaimana siklus dilaksanakan terserah Anda (termasuk opsi arsitektur yang digerakkan oleh peristiwa, seperti yang telah Anda diskusikan).

Salah satu contoh siklus serangan dapat:

  • menghitung serangan pemain (base + mods);
  • menghitung pertahanan lawan (basis + mod);
  • lakukan perbedaan (dan terapkan mod) dan tentukan kerusakan dasar;
  • menghitung efek parry / armor (mod pada kerusakan dasar) dan menerapkan kerusakan;
  • menghitung efek rekoil (mod pada kerusakan dasar) dan berlaku untuk penyerang.

Hal penting yang perlu diperhatikan adalah bahwa semakin awal siklus buff diterapkan, semakin banyak efeknya pada hasilnya . Jadi, jika Anda ingin pertarungan yang lebih "taktis" (di mana keterampilan pemain lebih penting daripada level karakter) buat banyak penggemar / debuff pada statistik dasar. Jika Anda ingin pertarungan yang lebih "seimbang" (di mana level lebih penting - penting dalam MMOG untuk membatasi tingkat kemajuan), gunakan buff / debuffs saja di akhir siklus.

Perbedaan antara "Modifikasi" dan "Penggemar" yang saya sebutkan sebelumnya memiliki tujuan: keputusan tentang aturan dan keseimbangan dapat diterapkan pada yang pertama, sehingga setiap perubahan pada mereka tidak perlu tercermin dalam perubahan pada setiap kelas yang terakhir. OTOH, jumlah dan jenis buff hanya dibatasi oleh imajinasi Anda, karena masing-masing dari mereka dapat mengekspresikan perilaku yang diinginkan tanpa harus memperhitungkan kemungkinan interaksi antara mereka dan yang lain (atau bahkan keberadaan orang lain sama sekali).

Jadi, menjawab pertanyaan: jangan membuat kelas untuk setiap Buff, tetapi satu untuk setiap (jenis) Modifikasi, dan ikat Modifikasi ke siklus serangan, bukan ke karakter. Buff dapat berupa daftar tuple (Modifikasi, kunci, nilai), dan Anda dapat menerapkan buff ke karakter dengan hanya menambahkan / menghapusnya ke kumpulan buff karakter. Ini juga mengurangi jendela kesalahan, karena statistik karakter tidak perlu diubah sama sekali ketika buff diterapkan (jadi ada risiko lebih kecil untuk mengembalikan stat ke nilai yang salah setelah buff berakhir).


Ini adalah pendekatan yang menarik karena berada di antara dua implementasi yang saya pertimbangkan - yaitu, hanya membatasi buff ke stat yang cukup sederhana dan menghasilkan pengubah kerusakan, atau membuat sistem overhead yang sangat kuat tapi overhead tinggi yang bisa menangani apa saja. Ini adalah semacam perluasan dari yang pertama untuk memungkinkan "duri" sambil mempertahankan antarmuka yang sederhana. Meskipun saya tidak berpikir itu adalah peluru ajaib untuk apa yang saya butuhkan, itu pasti membuat keseimbangan lebih mudah daripada pendekatan lain, jadi mungkin itu cara yang harus dilakukan. Terima kasih atas masukan Anda!
gkimsey

3

Saya tidak tahu apakah Anda masih membacanya, tetapi di sini adalah bagaimana saya melakukannya sekarang (kode didasarkan pada UE4 dan C ++). Setelah merenungkan masalah selama lebih dari dua minggu (!!), saya akhirnya menemukan ini:

http://gamedevelopment.tutsplus.com/tutorials/using-the-composite-design-pattern-for-an-rpg-attributes-system--gamedev-243

Dan saya pikir, itu juga, merangkum atribut tunggal dalam kelas / struct bukan ide yang buruk. Perlu diingat, bahwa saya mengambil keuntungan besar dari UE4 build dalam sistem kode refleksi, jadi tanpa pengerjaan ulang, ini mungkin tidak cocok di mana-mana.

Lagi pula, saya mulai dari membungkus atribut ke dalam struct tunggal:

USTRUCT(BlueprintType)
struct GAMEATTRIBUTES_API FGAAttributeBase
{
    GENERATED_USTRUCT_BODY()
public:
    UPROPERTY()
        FName AttributeName;
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Value")
        float BaseValue;
    /*
        This is maxmum value of this attribute.
    */
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Value")
        float ClampValue;
protected:
    float BonusValue;
    //float OldCurrentValue;
    float CurrentValue;
    float ChangedValue;

    //map of modifiers.
    //It could be TArray, but map seems easier to use in this case
    //we need to keep track of added/removed effects, and see 
    //if this effect affected this attribute.
    TMap<FGAEffectHandle, FGAModifier> Modifiers;

public:

    inline float GetFinalValue(){ return BaseValue + BonusValue; };
    inline float GetCurrentValue(){ return CurrentValue; };
    void UpdateAttribute();

    void Add(float ValueIn);
    void Subtract(float ValueIn);

    //inline float GetCurrentValue()
    //{
    //  return FMath::Clamp<float>(BaseValue + BonusValue + AccumulatedBonus, 0, GetFinalValue());;
    //}

    void AddBonus(const FGAModifier& ModifiersIn, const FGAEffectHandle& Handle);
    void RemoveBonus(const FGAEffectHandle& Handle);

    void InitializeAttribute();

    void CalculateBonus();

    inline bool operator== (const FGAAttributeBase& OtherAttribute) const
    {
        return (OtherAttribute.AttributeName == AttributeName);
    }

    inline bool operator!= (const FGAAttributeBase& OtherAttribute) const
    {
        return (OtherAttribute.AttributeName != AttributeName);
    }

    inline bool IsValid() const
    {
        return !AttributeName.IsNone();
    }
    friend uint32 GetTypeHash(const FGAAttributeBase& AttributeIn)
    {
        return AttributeIn.AttributeName.GetComparisonIndex();
    }
};

Itu masih belum selesai tetapi ide dasarnya, adalah bahwa struct ini melacak keadaan internal itu. Atribut hanya dapat dimodifikasi oleh Efek. Mencoba memodifikasinya secara langsung tidak aman dan tidak terpapar pada desainer. Saya berasumsi bahwa semuanya, yang dapat berinteraksi dengan atribut adalah Efek. Termasuk bonus tetap dari item. Ketika item baru dilengkapi, efek baru (bersama dengan pegangan) dibuat, dan ditambahkan ke peta khusus, yang menangani bonus durasi tak terbatas (yang harus dihapus secara manual oleh pemain). Ketika Efek baru sedang diterapkan, Menangani baru untuk itu dibuat (pegangan hanya int, dibungkus dengan struct), dan kemudian pegangan itu dilewati di sekitar sebagai sarana untuk berinteraksi dengan efek ini, serta melacak jika efeknya adalah masih aktif. Ketika efek dihapus, pegangannya disiarkan ke semua objek yang tertarik,

Bagian penting sebenarnya dari ini adalah TMap (TMap ished map). FGAModifier adalah struct yang sangat sederhana:

struct FGAModifier
{
    EGAAttributeOp AttributeMod;
    float Value;
};

Ini berisi jenis modifikasi:

UENUM()
enum class EGAAttributeOp : uint8
{
    Add,
    Subtract,
    Multiply,
    Divide,
    Set,
    Precentage,

    Invalid
};

Dan Nilai yang merupakan nilai akhir yang dihitung akan kita terapkan pada atribut.

Kami menambahkan efek baru menggunakan fungsi sederhana, dan kemudian memanggil:

void FGAAttributeBase::CalculateBonus()
{
    float AdditiveBonus = 0;
    auto ModIt = Modifiers.CreateConstIterator();
    for (ModIt; ModIt; ++ModIt)
    {
        switch (ModIt->Value.AttributeMod)
        {
        case EGAAttributeOp::Add:
            AdditiveBonus += ModIt->Value.Value;
                break;
            default:
                break;
        }
    }
    float OldBonus = BonusValue;
    //calculate final bonus from modifiers values.
    //we don't handle stacking here. It's checked and handled before effect is added.
    BonusValue = AdditiveBonus; 
    //this is absolute maximum (not clamped right now).
    float addValue = BonusValue - OldBonus;
    //reset to max = 200
    CurrentValue = CurrentValue + addValue;
}

Fungsi ini seharusnya menghitung ulang seluruh tumpukan bonus, setiap efek waktu ditambahkan atau dihapus. Fungsi masih belum selesai (seperti yang Anda lihat), tetapi Anda bisa mendapatkan ide umum.

Keluhan terbesar saya saat ini adalah menangani atribut Damaging / Healing (tanpa melibatkan penghitungan ulang seluruh tumpukan), saya pikir ini sudah agak terpecahkan, tetapi masih membutuhkan lebih banyak pengujian hingga 100%.

Dalam atribut apa pun Atttributes didefinisikan seperti ini (+ makro Nyata, dihilangkan di sini):

FGAAttributeBase Health;
FGAAttributeBase Energy;

dll.

Juga saya tidak 100% yakin tentang penanganan CurrentValue atribut, tetapi itu harus berfungsi. Mereka seperti sekarang.

Bagaimanapun saya berharap ini akan menghemat beberapa orang cache kepala, tidak yakin apakah ini solusi terbaik atau bahkan baik, tapi saya lebih menyukainya, daripada melacak efek secara independen dari atribut. Membuat setiap pelacakan atribut dalam keadaannya sendiri jauh lebih mudah dalam hal ini, dan seharusnya lebih sedikit kesalahan. Pada dasarnya hanya ada satu titik kegagalan, yaitu kelas yang cukup pendek dan sederhana.


Terima kasih atas tautan dan penjelasan pekerjaan Anda! Saya pikir Anda pada dasarnya bergerak menuju apa yang saya minta. Beberapa hal yang muncul dalam pikiran adalah urutan operasi (misalnya, 3 "menambahkan" efek dan 2 "multiply" efek pada atribut yang sama, yang harus terjadi terlebih dahulu?), Dan ini murni dukungan atribut. Ada juga gagasan pemicu (seperti "kehilangan 1 AP saat terkena" jenis efek) untuk diatasi, tetapi itu kemungkinan akan menjadi penyelidikan terpisah.
gkimsey

Urutan operasi, jika hanya menghitung bonus atribut mudah dilakukan. Anda dapat melihat di sini bahwa saya memiliki dan beralih. Untuk mengulangi semua bonus saat ini (yang dapat ditambahkan, kurangi, gandakan, bagi, dll), lalu akumulasi saja. Anda melakukan sesuatu seperti BonusValue = (BonusValue * MultiplyBonus + AddBonus-SubtractBonus) / DivideBonus, Atau Anda ingin melihat persamaan ini. Karena satu titik masuk, mudah untuk bereksperimen dengannya. Mengenai pemicu, saya belum menulis tentang itu, karena itu adalah masalah lain yang saya renungkan, dan saya sudah mencoba 3-4 (batas)
Łukasz Baran

solusi, tidak satupun dari mereka bekerja seperti yang saya inginkan (tujuan utama saya, adalah agar mereka ramah desainer). Ide umum saya, adalah menggunakan Tag, dan memeriksa, efek yang masuk terhadap tag. Jika tag cocok, efek dapat memicu efek lainnya. (Tag adalah nama yang mudah dibaca manusia, seperti Damage.Fire, Attack.Physical dll). Pada intinya konsepnya sangat mudah, masalahnya adalah mengatur data, agar mudah diakses (cepat untuk pencarian) dan kemudahan menambahkan efek baru. Anda dapat memeriksa kode di sini github.com/iniside/ActionRPGGame (GameAttributes adalah modul yang Anda akan tertarik)
Łukasz Baran

2

Saya bekerja pada MMO kecil dan semua item, kekuatan, penggemar dll punya 'efek'. Efek adalah kelas yang memiliki variabel untuk 'AddDefense', 'InstantDamage', 'HealHP', dll. Kekuatan, item, dll akan menangani durasi untuk efek itu.

Ketika Anda memberikan kekuatan atau memakai item itu akan menerapkan efek ke karakter untuk durasi yang ditentukan. Kemudian serangan utama, dll perhitungan akan memperhitungkan efek yang diterapkan.

Misalnya, Anda memiliki penggemar yang menambah pertahanan. Setidaknya akan ada EffectID dan Durasi untuk buff itu. Saat casting, itu akan menerapkan EffectID ke karakter untuk durasi yang ditentukan.

Contoh lain untuk suatu item, akan memiliki bidang yang sama. Tetapi durasinya tidak terbatas atau sampai efeknya dihapus dengan mengambil item dari karakter.

Metode ini memungkinkan Anda untuk mengulang daftar efek yang sedang diterapkan.

Semoga saya menjelaskan metode ini dengan cukup jelas.


Seperti yang saya pahami dengan pengalaman minimal saya, ini adalah cara tradisional untuk mengimplementasikan mod stat di game RPG. Ini bekerja dengan baik dan mudah dimengerti dan diimplementasikan. Kelemahannya adalah sepertinya tidak meninggalkan saya ruang untuk melakukan hal-hal seperti penggemar "duri", atau sesuatu yang lebih maju atau situasional. Ini juga secara historis menjadi penyebab beberapa eksploitasi dalam RPG, meskipun mereka cukup langka, dan karena saya membuat permainan pemain tunggal, jika seseorang menemukan exploit, saya tidak terlalu peduli. Terima kasih atas masukannya.
gkimsey

2
  1. Jika Anda adalah pengguna persatuan, berikut adalah sesuatu untuk memulai: http://www.stevegargolinski.com/armory-a-free-and-unfinished-stat-inventory-and-buffdebuff-framework-for-unity/

Saya menggunakan ScriptableOjects sebagai buff / mantra / talenta

public class Spell : ScriptableObject 
{
    public SpellType SpellType = SpellType.Ability;
    public SpellTargetType SpellTargetType = SpellTargetType.SingleTarget;
    public SpellCategory SpellCategory = SpellCategory.Ability;
    public MagicSchools MagicSchool = MagicSchools.Physical;
    public CharacterClass CharacterClass = CharacterClass.None;
    public string Description = "no description available";
    public SpellDragType DragType = SpellDragType.Active; 
    public bool Active = false;
    public int TargetCount = 1;
    public float CastTime = 0;
    public uint EffectRange = 3;
    public int RequiredLevel = 1;
    public virtual void OnGUI()
    {
    }
}

menggunakan UnityEngine; menggunakan System.Collections.Generic;

public enum BuffType {Buff, Debuff} [System.Serializable] kelas publik BuffStat {public Stat Stat = Stat.Strength; publik ModValueInPercent = 0.1f; }

public class Buff : Spell
{
    public BuffType BuffType = BuffType.Buff;
    public BuffStat[] ModStats;
    public bool PersistsThroughDeath = false;
    public int AmountPerTick = 3;
    public bool UseTickTimer = false;
    public float TickTime = 1.5f;
    [HideInInspector]
    public float Ticktimer = 0;
    public float Duration = 360; // in seconds
    public float ModifierPerStack = 1.1f;
    [HideInInspector]
    public float Timer = 0;
    public int Stack = 1;
    public int MaxStack = 1;
}

BuffModul:

using System;
using RPGCore;
using UnityEngine;

public class Buff_Modul : MonoBehaviour
{
    private Unit _unit;

    // Use this for initialization
    private void Awake()
    {
        _unit = GetComponent<Unit>();
    }

    #region BUFF MODUL

    public virtual void RUN_BUFF_MODUL()
    {
        try
        {
            foreach (var buff in _unit.Attr.Buffs)
            {
                CeckBuff(buff);
            }
        }
        catch(Exception e) {throw new Exception(e.ToString());}
    }

    #endregion BUFF MODUL

    public void ClearBuffs()
    {
        _unit.Attr.Buffs.Clear();
    }

    public void AddBuff(string buffName)
    {
        var buff = Instantiate(Resources.Load("Scriptable/Buff/" + buffName, typeof(Buff))) as Buff;
        if (buff == null) return;
        buff.name = buffName;
        buff.Timer = buff.Duration;
        _unit.Attr.Buffs.Add(buff);
        foreach (var buffStat in buff.ModStats)
        {
            switch (buff.BuffType)
            {
                case BuffType.Buff:
                    _unit.Attr.AddBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat] + _unit.Attr.StatsItem[buffStat.Stat]) * buffStat.ModValueInPercent));
                    break;
                case BuffType.Debuff:
                    _unit.Attr.RemoveBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat] /*+ unit.character.StatsItem[_stat.stat]*/) * buffStat.ModValueInPercent));
                    break;
            }
            Core.StatController(_unit.Attr, buffStat.Stat);
        }
    }

    public void RemoveBuff(Buff buff)
    {
        foreach (var buffStat in buff.ModStats)
        {
            switch (buff.BuffType)
            {
                case BuffType.Buff:
                    _unit.Attr.RemoveBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat] + _unit.Attr.StatsItem[buffStat.Stat]) * buffStat.ModValueInPercent));
                    break;
                case BuffType.Debuff:
                    _unit.Attr.AddBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat]  /*+ unit.character.StatsItem[_stat.stat]*/) * buffStat.ModValueInPercent));
                    break;
            }
            Core.StatController(_unit.Attr, buffStat.Stat);
        }
        _unit.Attr.Buffs.Remove(buff);
    }

    void CeckBuff(Buff buff)
    {
        buff.Timer -= Time.deltaTime;
        if (!_unit.IsAlive && !buff.PersistsThroughDeath)
        {
            if (buff.ModStats != null)
                foreach (var stat in buff.ModStats)
                {
                    _unit.Attr.StatsBuff[stat.Stat] = 0;
                }

            RemoveBuff(buff);
        }
        if (_unit.IsAlive && buff.Timer <= 0)
        {
            RemoveBuff(buff);
        }
    }
}

0

Ini adalah pertanyaan aktual bagi saya. Saya punya satu ide tentang itu.

  1. Seperti yang dikatakan sebelumnya, kita perlu mengimplementasikan Buffdaftar dan pembaru logika untuk para penggemar.
  2. Kami kemudian perlu mengubah semua pengaturan pemain tertentu setiap frame di subclass Buffkelas.
  3. Kami kemudian mendapatkan pengaturan pemain saat ini dari bidang pengaturan yang dapat diubah.

class Player {
  settings: AllPlayerStats;

  private buffs: Array<Buff> = [];
  private baseSettings: AllPlayerStats;

  constructor(settings: AllPlayerStats) {
    this.baseSettings = settings;
    this.resetSettings();
  }

  addBuff(buff: Buff): void {
    this.buffs.push(buff);
    buff.start(this);
  }

  findBuff(predcate(buff: Buff) => boolean): Buff {...}

  removeBuff(buff: Buff): void {...}

  update(dt: number): void {
    this.resetSettings();
    this.buffs.forEach((item) => item.update(dt));
  }

  private resetSettings(): void {
    //some way to copy base to settings
    this.settings = this.baseSettings.copy();
  }
}

class Buff {
    private owner: Player;        

    start(owner: Player) { this.owner = owner; }

    update(dt: number): void {
      //here we change anything we want in subclasses like
      this.owner.settings.hp += 15;
      //if we need base value, just make owner.baseSettings public but don't change it! only read

      //also here logic for removal buff by time or something
    }
}

Dengan cara ini, mudah untuk menambahkan statistik pemain baru, tanpa mengubah logika Buffsubclass.


0

Saya tahu ini cukup lama tetapi tertaut di pos yang lebih baru dan saya memiliki beberapa pemikiran tentang hal ini yang ingin saya bagikan. Sayangnya saya tidak memiliki catatan saya saat ini, jadi saya akan mencoba memberikan gambaran umum tentang apa yang saya bicarakan dan saya akan mengedit rincian dan beberapa contoh kode ketika saya memilikinya di depan saya.

Pertama, saya berpikir bahwa dari perspektif desain kebanyakan orang menjadi terlalu terperangkap dalam jenis buff apa yang dapat dibuat dan bagaimana mereka diterapkan dan melupakan prinsip-prinsip dasar pemrograman berorientasi objek.

Apa yang saya maksud? Tidak masalah apakah sesuatu itu buff atau debuff, mereka berdua pengubah yang hanya mempengaruhi sesuatu baik secara positif atau negatif. Kode tidak peduli yang mana. Untuk masalah ini, pada akhirnya tidak masalah apakah ada sesuatu yang menambahkan statistik atau mengalikannya, itu hanya operator yang berbeda dan sekali lagi kodenya tidak peduli yang mana.

Jadi kemana saya akan pergi dengan ini? Mendesain kelas buff / debuff yang bagus (baca: sederhana, elegan) tidak terlalu sulit, yang sulit adalah merancang sistem yang menghitung dan mempertahankan status permainan.

Jika saya merancang sistem buff / debuff, berikut beberapa hal yang akan saya pertimbangkan:

  • Kelas buff / debuff untuk merepresentasikan efek itu sendiri.
  • Kelas tipe buff / debuff berisi informasi tentang apa yang mempengaruhi buff dan bagaimana caranya.
  • Karakter, Item, dan mungkin Lokasi semua harus memiliki daftar atau properti koleksi untuk mengandung buff dan debuff.

Beberapa spesifik untuk apa yang harus mengandung tipe buff / debuff:

  • Siapa / apa yang bisa diterapkan, IE: pemain, monster, lokasi, item, dll.
  • Apa jenis efeknya (positif, negatif), apakah itu multiplikatif atau aditif, dan apa jenis stat yang berdampak, yaitu: serangan, pertahanan, pergerakan, dll.
  • Kapan harus diperiksa (pertempuran, waktu hari, dll).
  • Apakah itu bisa dihapus, dan jika demikian bagaimana itu bisa dihapus.

Itu baru permulaan, tetapi dari sana Anda hanya mendefinisikan apa yang Anda inginkan dan bertindak berdasarkan kondisi permainan normal Anda. Misalnya, Anda ingin membuat item terkutuk yang mengurangi kecepatan gerakan ...

Selama saya telah menempatkan jenis yang tepat di tempat itu mudah untuk membuat catatan penggemar yang mengatakan:

  • Jenis: Kutukan
  • ObjectType: Item
  • StatKategori: Utilitas
  • StatAffected: MovementSpeed
  • Durasi: Tidak Terbatas
  • Pemicu: OnEquip

Dan seterusnya, dan ketika saya membuat buff saya hanya menetapkan itu BuffType of Curse dan yang lainnya terserah mesin ...

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.