Esnek bir buff / debuff sistemi uygulamanın yolu nedir?


66

Genel bakış:

RPG benzeri istatistiklere sahip birçok oyun, basit "% 25 ekstra hasar ver" den "Saldırılara 15 hasar ver" gibi daha karmaşık şeylere kadar değişen karakter "meraklıları" için izin verir.

Her bir buff çeşidinin özellikleri gerçekten alakalı değildir. Rasgele meraklıları işlemek için (muhtemelen nesne yönelimli) bir yol arıyorum.

Detaylar:

Özel durumumda, sıra tabanlı bir savaş ortamında birden fazla karakterim var, bu yüzden meraklıları "OnTurnStart", "OnReceiveDamage", vb. Gibi olaylara bağlı tutmayı düşündüm. sadece ilgili olaylar aşırı yüklenmiştir. Daha sonra her karakter, halihazırda uygulanmış bir buff vektörüne sahip olabilir.

Bu çözüm mantıklı geliyor mu? Kesinlikle onlarca olay türünün gerekli olduğunu görebiliyorum, her bir buff için yeni bir alt sınıf oluşturmak gibi görünüyor ve herhangi bir buff'a "etkileşimler" için izin vermiyor gibi görünüyor. Diğer bir deyişle, hasar artırma kepi uygulamak isteseydim,% 25 ekstra hasar veren 10 farklı buff'ınız olsa bile,% 250 ekstra yerine sadece% 100 ekstra yaparsınız.

İdeal olarak kontrol edebileceğim daha karmaşık durumlar var. Eminim ki herkes daha sofistike meraklıların birbirleriyle nasıl bir etkileşime girebileceklerini, bir oyun geliştiricisi olarak istemediğim bir şekilde bulabilirler.

Nispeten deneyimsiz bir C ++ programcısı olarak (genellikle C'yi gömülü sistemlerde kullandım), çözümümün basit olduğunu ve muhtemelen nesne yönelimli dilden tam olarak yararlanamadığını düşünüyorum.

Düşünceler? Buradaki herhangi biri daha önce sağlam bir buff sistemi tasarladı mı?

Düzenleme: Cevap (lar) ile ilgili:

Öncelikle iyi ayrıntıya dayalı bir cevap ve sorduğum sorunun cevabını seçtim, ancak cevapları okumak bana biraz daha içgörü kazandırdı.

Belki de şaşırtıcı olmayan bir şekilde, farklı sistemler veya ince ayarlı sistemler belirli durumlar için daha iyi görünüyor. Oyunum için hangi sistemin en iyi sonuç verdiğini uygulamak istediğim türlere, varyansa ve buff sayısına bağlı olacaktır.

Hemen hemen her ekipmanın bir buff'ı gücünü değiştirebileceği Diablo 3 (aşağıda belirtilen) gibi bir oyun için, buff'lar sadece karakter istatistik sistemidir, mümkün olduğunda iyi bir fikir gibi görünür.

İçinde bulunduğum sıra tabanlı durum için, olaya dayalı yaklaşım daha uygun olabilir.

Her halükarda, hala birisinin dönüş meraklısı başına +2 hamle mesafesini , saldırganın tutucusuna geri götürdüğü hasarın% 50'sini uygulayabilmemi sağlayacak süslü bir "OO" sihirli mermi ile geleceğini umuyorum. bir +5 güç perdesini kendi alt sınıfına çevirmeden, tek bir sistemde 3 veya daha fazla kiremit uzak tutucusuna saldırıldığında yakındaki bir döşemeye otomatik olarak ışınlan .

Sanırım en yakın şey işaretlediğim cevap, ama zemin hala açık. Giriş için herkese teşekkürler.


Beyin fırtınası yaptığım için bunu bir cevap olarak göndermiyorum, peki ya bir meraklılar listesi? Her buffın bir sabiti ve bir faktör değiştiricisi vardır. Sabit +10 hasar, faktör +% 10 hasar artışı için 1,10 olacaktır. Hasar hesaplamalarınızda, toplam değiştiriciyi elde etmek için tüm buff'ları yinelersiniz ve sonra istediğiniz kısıtlamaları uygularsınız. Bunu her türlü değiştirilebilir nitelik için yaparsınız. Yine de karmaşık şeyler için özel bir vaka yöntemine ihtiyacınız olacak.
William Mariager

Bu arada, donatılabilir silahlar ve aksesuarlar için bir sistem kurarken Stats nesnem için zaten böyle bir şey uygulamıştım. Dediğiniz gibi, sadece mevcut özellikleri değiştiren buff'lar için yeterince iyi bir çözüm, fakat elbette o zaman bile X'in ardından bazı buff'ların süresinin dolmasını isteyeceğim, diğerleri Y etkisi gerçekleştiğinde başkalarının süresinin dolmasını isteyeceğim. Ana soruda bunu zaten çok uzun sürdüğü için belirtiniz.
gkimsey

1
Bir mesajlaşma sistemi tarafından ya da elle ya da başka bir yolla çağırılan bir "onReceiveDamage" yönteminiz varsa, kime / neye zarar verdiğinize bir referans eklemek yeterince kolay olmalıdır. Öyleyse bu bilgiyi

Doğru, soyut Buff sınıfı için her olay şablonunun bunun gibi ilgili parametreleri içermesini bekliyordum. Kesinlikle işe yarardı, ama tereddüt ediyorum çünkü iyi ölçeklenmeyecekmiş gibi hissediyor. Yüzlerce farklı buff'la bir MMORPG'yi hayal etmek zor zaman geçiriyorum, her buff için ayrı bir sınıf var, yüzlerce farklı olaydan birini seçerek. Pek çok meraklı oluşturduğumdan (muhtemelen 30'a yakın), ancak daha basit, daha zarif veya daha esnek bir sistem varsa, kullanmak isterim. Daha esnek sistem = daha ilginç merak / yetenekler.
gkimsey

4
Bu etkileşim sorununa iyi bir cevap değil, fakat dekoratör modelinin burada çok iyi uygulandığı görülüyor; sadece birbirlerinin üzerine daha fazla tampon (dekoratör) uygulayın. Belki birlikte buff'ları "birleştirerek" etkileşimi idare edebilecek bir sistemle (örneğin,% 10x25% 100 buff'ında birleşir).
ashes999

Yanıtlar:


32

Bu karmaşık bir konudur, çünkü (bugünlerde) 'meraklılar' olarak bir araya toplanan birkaç farklı şeyden bahsediyorsunuz:

  • oyuncunun özelliklerine değiştiriciler
  • belirli olaylarda meydana gelen özel efektler
  • yukarıdakilerin kombinasyonları.

Her zaman ilkini, belirli bir karakter için aktif efektlerin bir listesiyle uygularım. Listeden çıkarılması, süreye göre veya açıkça belirtilmesi oldukça önemsiz olduğundan, buradakileri burada ele almayacağım. Her Efekt, bir nitelik değiştirici listesi içerir ve basit çarpma işlemi ile bunu temel değere uygulayabilir.

Sonra değiştirilen niteliklere erişmek için işlevlerle sardım. Örneğin.:

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

Bu kolayca çarpma efektleri uygulamanıza izin verir. Ayrıca ilave etkilere ihtiyacınız varsa, bunları hangi sırayla uygulayacağınıza karar verin (muhtemelen en son katkı maddesi) ve listeyi iki kez uygulayın. (Muhtemelen, bir tanesi çarpımsal, biri katkı maddesi için olmak üzere, Efekt'te ayrı değiştirici listelerim olurdu.)

Ölçüt değeri, "+% 20 ve Undead" i uygulamanıza izin vermektir - UNDEAD değerini Efekt'e ayarlayın ve yalnızca UNDEAD değerini, get_current_attribute_value()ölümsüz bir düşmana karşı bir hasar rulosu hesapladığınızda geçirin.

Bu arada, değerleri doğrudan ilgili öznitelik değerine uygulayan ve uygulayan bir sistemi denemek ve yazmak istemem - sonuçta, özniteliklerinizin hata nedeniyle amaçlanan değerden uzaklaşma olasılığı çok yüksektir. (örneğin, bir şeyi 2 ile çarptıysanız, ancak sonra kaparsanız, yeniden 2'ye böldüğünüzde, başladığından daha düşük olur.)

"Vurulduğunda saldırganlara 15 hasar ver" gibi etkinliğe dayalı efektlere gelince, bunun için Efekt sınıfına yöntemler ekleyebilirsiniz. Ancak, farklı ve keyfi davranışlar istiyorsanız (örneğin, yukarıdaki olayın bazı efektleri hasarı geri yansıtabilir, bazıları sizi iyileştirebilir, sizi rasgele uzaklaştırabilir, ne olursa olsun), bunun için özel işlevlere veya sınıflara ihtiyacınız olacaktır. Efekt üzerindeki olay işleyicilere işlevler atayabilir, ardından herhangi bir etkin efekt için olay işleyicilerini çağırabilirsiniz.

# 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)

Açıkçası, Efekt sınıfınızın her olay türü için bir olay işleyicisi olacaktır ve her durumda istediğiniz kadar işleyici işlevi atayabilirsiniz. Her biri özellik değiştiricilerin ve içerdiği olay işleyicilerin kompozisyonu tarafından tanımlandığı için, Efekt'i alt sınıfa ihtiyacınız yok. (Muhtemelen bir isim, süre vb. De içerecektir)


2
Mükemmel detay için +1. Bu resmen soruma resmen cevap verdiğime en yakın cevap. Buradaki temel kurulum, çok fazla esnekliğe ve aksi halde dağınık bir oyun mantığı olanın küçük bir soyutlamasına izin veriyor gibi görünmektedir. Söylediğiniz gibi, daha korkak etkiler hala kendi sınıflarına ihtiyaç duyacaktır, ancak bu tipik bir "buff" sisteminin ihtiyaçlarının büyük kısmını karşıladığını düşünüyorum.
gkimsey

Burada gizli olan kavramsal farklılıkları işaret etmek için +1. Hepsi aynı olaya dayalı güncelleme mantığı ile çalışmayacak. Tamamen farklı bir uygulama için @ Ross'un cevabına bakınız. Her ikisinin de yan yana var olması gerekecek.
ctietze,

22

Bir sınıf için bir arkadaşımla çalıştığım bir oyunda, kullanıcının uzun otların ve hızlanan karoların içine hapsolduğu ve ne olmadığı ve kanama ve zehir gibi bazı küçük şeylerin ne zaman tutulduğu için bir cüruf / debriyaj sistemi yaptık.

Fikir basitti ve bunu Python'da uygularken oldukça etkiliydi.

Temel olarak, işte nasıl gitti:

  • Kullanıcının halihazırda uygulanmış buff ve debuff'ların bir listesi vardı (bir buff ve debuff'ın aynı olduğunu unutmayın, sadece farklı bir sonuca sahip olan etkisidir)
  • Buff'ların süre, ad ve bilgilerin görüntülenmesi için metin ve canlı zaman gibi çeşitli özellikleri vardır. Önemli olanlar, hayatta kalan süre, süre ve bu tutkunun uygulandığı aktöre yapılan referanstır.
  • Buff için, player.apply (buff / debuff) üzerinden oyuncuya eklendiğinde, start () metodunu çağırır, bu oyuncuya hız artışı veya yavaşlama gibi kritik değişiklikleri uygular.
  • Daha sonra bir güncelleme döngüsünde her bir buff'ı tekrarlardık ve buff'lar güncellenirdi, bu onların yaşadıklarını artıracaktı. Alt sınıflar, oyuncuyu zehirlemek, oyuncuya zaman içinde HP vermek vb.
  • Buff için yapıldığında, timeAlive> = period anlamına gelir, güncelleme mantığı buff'ı kaldırır ve bir oynatıcıdaki hız sınırlamalarının kaldırılmasından küçük bir yarıçapa neden olmaktan (bir bomba efekti düşünmesine) kadar değişen bir finish () yöntemi çağırır. bir DoT sonra)

Şimdi aslında dünyadan meraklıları nasıl uygulayacağımız farklı bir hikaye. İşte düşünce için benim yiyecek.


1
Bu, yukarıda anlatmaya çalıştığım şeyin daha iyi bir açıklaması gibi görünüyor. Göreceli olarak basit, anlaşılması kesinlikle kolay. Esasen benim düşüncemle ilişkilendirmek için orada üç "olaydan" (OnApply, OnTimeTick, OnExpired) bahsettiniz. Olduğu gibi, vurarak geldiğinde hasar geri dönüşü gibi şeyleri desteklemeyecekti, ancak birçok meraklı için daha iyi ölçekleniyor. Buff'larımın neler yapabileceğini sınırlamayı tercih etmemeyi tercih ederim (bu, ana oyun mantığı tarafından çağrılan olayların sayısını sınırlamaktır), ancak buff ölçeklenebilirliği daha önemli olabilir. Giriş için teşekkürler!
gkimsey

Evet, böyle bir şey yapmadık. Gerçekten zarif ve harika bir konsept (bir Thorns tutkunu gibi) geliyor.
Ross

@gkimsey Dikenler ve diğer pasif meraklılar gibi şeyler için, Mob sınıfınızdaki mantığı, zarar görmeye ya da sağlığa benzer pasif bir stat olarak uygular ve tutkunu uygularken bu statü artırırdım. Bu basitleştiren bir çok Birden dikenler meraklıları var vaka yanı sıra (10 meraklıları 1 dönüş hasarı yerine 10 gösterecekti) temiz bir arayüz tutmak ve devetüyü sistemi basit kalır sağlar.
3Doubloons

Bu neredeyse zahmetsizce basit bir yaklaşım, ancak Diablo 3'ü oynarken kendimi düşünmeye başladım. Hayatın çalındığını, çarpılan yaşamın, yakın dövüşçülere verilen hasarın, vb. Karakter penceresindeki tüm istatistiklerin kendilerine ait olduğunu gördüm. D3, D3 dünyadaki en karmaşık polisaj sistemine veya etkileşimlerine sahip değil, ancak pek önemsiz. Bu çok mantıklı. Yine de, içine düşebilecek 12 farklı etkiye sahip potansiyel olarak 15 farklı buff vardır. Karakter istatistikleri sayfasını
doldururken çok

11

Bunu hala okuyor musunuz emin değilim ama bu tür problemlerle uzun süredir mücadele ediyorum.

Çok sayıda farklı etki sistemi tasarladım. Şimdi kısaca onların üzerinden geçeceğim. Bunların hepsi benim tecrübeme dayanıyor. Tüm cevapları bildiğini iddia etmiyorum.


Statik Değiştiriciler

Bu tür sistemler çoğunlukla, herhangi bir değişikliği belirlemek için basit tam sayılara dayanır. Örneğin, +100'den Max HP'ye, +10'a saldırmak vb. Bu sistem aynı zamanda yüzdeleri de kaldırabilir. Sadece istifin kontrolden çıkmadığından emin olmanız gerekir.

Bu tür bir sistem için üretilen değerleri hiçbir zaman önbelleğe almadım. Örneğin, bir şeyin maksimum sağlığını göstermek isteseydim, o noktada değeri üretirdim. Bu, işlerin hataya yatkın olmasını engelledi ve katılan herkes için anlaşılması daha kolay.

(Java'da çalışıyorum, bu yüzden aşağıdakiler Java tabanlı ancak diğer diller için bazı değişikliklerle çalışmalı.) Bu sistem, değişiklik türleri için enums'ler ve ardından tamsayılar kullanılarak kolayca yapılabilir. Sonuçta, anahtar sıralı, değer sıralı çiftleri olan bir koleksiyona yerleştirilebilir. Bu hızlı arama ve hesaplamalar olacak, bu yüzden performans çok iyi.

Genel olarak, sadece düz statik statik değiştiriciler ile çok iyi çalışıyor. Yine de, kod değiştiricilerin kullanılması için uygun yerlerde kod bulunmalıdır: getAttack, getMaxHP, getMeleeDamage, vb.

Bu yöntemin başarısız olduğu yerde (benim için) bufflar arasında çok karmaşık bir etkileşim var. Biraz fazla getto yapmak dışında, etkileşime girmenin kolay ve kolay bir yolu yok. Bazı basit etkileşim olanaklarına sahiptir. Bunu yapmak için, statik değiştiricileri saklama biçiminizde bir değişiklik yapmalısınız. Anahtar olarak bir enum kullanmak yerine, bir String kullanırsınız. Bu Dize, Enum name + extra değişken olur. 10 üzerinden 9 kez, ekstra değişken kullanılmaz, bu yüzden enum adını anahtar olarak korursunuz.

Hızlıca bir örnek yapalım: Eğer ölümsüz yaratıklara karşı hasarı değiştirmek istiyorsanız, aşağıdaki şekilde sipariş edilmiş bir çifti olabilir: (DAMAGE_Undead, 10) DAMAGE Enum ve Undead ekstra değişkendir. Böylece, savaşınız sırasında, şöyle bir şey yapabilirsiniz:

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

Neyse, oldukça iyi çalışıyor ve hızlı. Ancak karmaşık etkileşimlerde başarısız oluyor ve her yerde “özel” kod kullanıyor. Örneğin, “ölüme ışınlanma% 25 şansı” durumunu düşünün. Bu “oldukça” karmaşık bir şey. Yukarıdaki sistem, aşağıdakilere ihtiyaç duyduğunuz gibi kolayca idare edebilir.

  1. Müzikçaların bu modda olup olmadığını belirleyin.
  2. Bir yerlerde, eğer başarılı olursa, ışınlanmayı yürütmek için bazı kodlar kullanın. Bu kodun yeri başlı başına bir tartışmadır!
  3. Mod haritasından doğru verileri alın. Değer ne anlama geliyor? Onlar da ışınlanacakları oda mı? Ya bir oyuncuda iki ışınlanma modu varsa? Tutarlar bir araya getirilmez mi ?????? HATASI!

Yani bu beni bir sonrakine getiriyor:


Üstün Karmaşık Buff Sistemi

Bir keresinde tek başıma bir 2D MMORPG yazmaya çalıştım. Bu çok büyük bir hataydı ama çok şey öğrendim!

Etki sistemini 3 defa yeniden yazdım. İlki, yukarıdakilerin daha az güçlü bir varyasyonunu kullandı. İkincisi hakkında konuşacağım şeydi.

Bu sistem her değişiklik için bir dizi sınıfa sahipti, bunlar şöyledir: ChangeHP, ChangeMaxHP, ChangeHPByPercent, ChangeMaxByPercent. Bir milyon insanım vardı - TeleportOnDeath gibi şeyler bile.

Sınıflarımda aşağıdakileri yapacak şeyler vardı:

  • applyAffect
  • removeAffect
  • checkForInteraction <--- önemli

Uygula ve kaldır kendilerini açıkla (yüzdelik gibi şeyler için etki, etkinin ne zaman yıprandığından emin olmak için HP'nin ne kadar arttırdığını takip etse de, yalnızca eklediği miktarı kaldırır.) Buggy, lol ve doğru olduğundan emin olmak uzun zamanımı aldı. Hala iyi hissetmedim.).

CheckForInteraction yöntemi, karmaşık bir kod parçasıydı. Etkilerin (yani: ChangeHP) sınıflarının her birinde, bunun girdi etkisiyle değiştirilmesi gerekip gerekmediğini belirlemek için koda sahip olacaktır. Mesela, eğer bir şeye sahipseniz ...

  • Buff 1: Saldırıya 10 Yangın hasarı verir
  • Buff 2: Tüm yangın hasarlarını% 25 arttırır.
  • Buff 3: Tüm yangın hasarlarını 15 arttırır.

CheckForInteraction yöntemi tüm bu etkilerin üstesinden gelirdi. Bunu yapmak için, yakındaki oyuncuların TÜMÜ üzerinde her bir etkinin kontrol edilmesi gerekiyordu! Bunun nedeni, bir alandaki birden fazla oyuncu ile yaptığım etkilerin türü. Bunun anlamı, ASLA HAD'ın yukarıdaki gibi herhangi bir özel ifadeyi HAD olarak tanımlamamasıdır - “sadece ölürsek, ölümle ilgili ışınlanma kontrolü yapmalıyız”. Bu sistem doğru zamanda otomatik olarak doğru şekilde işler.

Bu sistemi yazmaya çalışmak 2 ay kadar sürdü ve kafa tarafından yapılmış birkaç kez patladı. Ancak, gerçekten güçlüydü ve çok fazla miktarda şey yapabilirdi - özellikle oyunumdaki yetenekler için şu iki gerçeği göz önüne aldığın zaman: 1. Hedef aralıkları vardı (yani: tek, öz, sadece grup, PB AE özü) , PB AE hedefi, hedef AE, vb.) 2. Yetenekler üzerinde 1'den fazla etkiye sahip olabilir.

Yukarıda da bahsettiğim gibi, bu oyun için 3. etki sisteminden ikincisiydi. Neden bundan uzağa taşındım?

Bu sistem şimdiye kadar gördüğüm en kötü performansa sahipti! Devam eden her şeyi çok fazla kontrol etmek zorunda olduğu için çok yavaştı. İyileştirmeye çalıştım, ancak başarısız oldu.

Böylece üçüncü versiyonuma geldik (ve başka bir buff sistemi):


İşleyicileri ile Karmaşık Etki Sınıfı

Yani bu hemen hemen ilk ikisinin bir birleşimidir: Çok fazla işlevsellik ve ekstra veri içeren bir Affect sınıfında statik değişkenler olabilir. O zaman sadece işleyicileri çağırın (benim için, belirli eylemler için alt sınıflar yerine hemen hemen bazı statik yarar yöntemleri kullanın. Ancak bir şeyler yapmak istediğimizde eylemler için alt sınıflarla gidebileceğinizden eminim).

Affect sınıfı, hedef türleri, süre, kullanım sayısı, uygulama şansı vb. Gibi tüm sulu iyi öğelere sahip olacaktı.

Örneğin, ölümle ilgili ışınlanma gibi durumlarla başa çıkmak için özel kodlar eklememiz gerekir. Muharebe kodunda manuel olarak bunu kontrol etmemiz gerekirdi ve sonra da olsaydı, etkilerin bir listesini alırdık. Bu etki listesi, ölümle ilgili ışınlanma ile uğraşan oyuncudaki geçerli etkilerin tümünü içerir. Sonra her birine bakarız ve başarılı olup olmadığını kontrol ederdik (ilk başarılıda dururduk). Başarılıydı, bunun için sadece işleyiciyi arardık.

İsterseniz etkileşim yapılabilir. Sadece oyuncu / etc üzerinde meraklıları aramak için kodu yazmak zorunda kalacaktı. İyi bir performans gösterdiğinden (aşağıya bakınız), bunu yapmak için oldukça verimli olması gerekir. Sadece daha karmaşık işleyicilere vb. İhtiyaç duyacaktır.

Bu yüzden, ilk sistemin performansının büyük bir kısmı ve ikincisi gibi hala çok fazla karmaşıklığı vardır (fakat AS kadar değil). En azından Java'da, MOST vakalarında neredeyse birincisinin performansını elde etmek için bazı zor şeyler yapabilirsiniz (örneğin: enum haritasına sahip olmak ( http://docs.oracle.com/javase/6/docs/api/java) /util/EnumMap.html ) Enums tuşları ile ve ArrayList değerleri ile etkiler. Bu, hızlı bir şekilde etkilenip etkilenmediğini görmenizi sağlar [liste 0 olur veya harita enum olmazdı] ve sebepsiz yere oyuncunun etki listelerini sürekli yinelemek için: Şu anda onlara ihtiyacımız olursa etkilerini yinelemeyi umursamıyorum.

Şu anda 2005'te sona eren MUD'umu yeniden açıyorum (oyunu, orjinal haliyle olduğu FastROM kod tabanı yerine Java ile yeniden yazıyorum) ve yakın zamanda buff sistemimi nasıl uygulamak istiyorum? Bu sistemi kullanacağım çünkü önceki başarısız oyunumda iyi çalıştı.

Umarım, bir yerlerde birileri bu içgörülerin bir kaçını yararlı bulacaktır.


6

Her bir buff için farklı bir sınıf (veya adreslenebilir fonksiyon), bu buff'ların davranışları birbirinden farklı ise, fazla yüklenmez. Bir şey% + 10 veya% + 20% buff'lara sahip olacaktı (elbette, aynı sınıfın iki nesnesi olarak daha iyi temsil edilecek), diğeri yine de özel kod gerektiren çılgınca farklı efektler uygulayacaktı. Ancak, her bir tutkunun ne isterse yapmasına izin vermek yerine , oyun mantığını özelleştirmenin standart yollarına sahip olmanın daha iyi olduğuna inanıyorum (ve öngörülemeyen yollarla birbirlerini etkilemeyebilir, oyun dengesini bozabiliriz).

Her "saldırı döngüsünü", her bir basamağın bir temel değeri, bu değere uygulanabilecek bir sıralı değişiklik listesi (belki de sınırlanmış) ve bir son sınırın olduğu aşamalara bölünmesini öneririm. Her değişiklik, varsayılan olarak bir kimlik dönüşümüne sahiptir ve sıfır veya daha fazla buff / debuff'dan etkilenebilir. Her modifikasyonun özellikleri, uygulanan adıma bağlı olacaktır. Döngünün nasıl uygulandığı size bağlıdır (tartıştığınız gibi olaya dayalı bir mimari seçeneği dahil).

Saldırı döngüsünün bir örneği olabilir:

  • oyuncu saldırılarını hesaplar (üs + mod);
  • rakip savunmasını hesaplar (üs + mod);
  • farkı yapın (ve modları uygulayın) ve temel hasarı belirleyin;
  • Herhangi bir parry / zırh efektini (baz hasarı üzerindeki modlar) hesaplar ve hasar uygular;
  • Herhangi bir geri tepme etkisini hesaplayın (taban hasarı üzerindeki modlar) ve saldırgana uygulayın.

Unutulmaması gereken önemli nokta, bir devirde daha önce bir tutkunun uygulanmasının sonuçta daha fazla etkiye sahip olmasıdır . Bu nedenle, eğer daha "taktik" bir dövüş istiyorsanız (oyuncunun becerisinin karakter seviyesinden daha önemli olduğu durumlarda), temel istatistikler üzerinde birçok buff / debuffs yaratın. Daha "dengeli" bir savaş istiyorsanız (seviyenin daha önemli olduğu - MMOG'larda ilerleme oranını sınırlamak için önemlidir) istiyorsanız, yalnızca döngünün ilerleyen bölümlerinde buff'lar / debuff'lar kullanın.

Daha önce bahsettiğim "Modifikasyonlar" ve "Buffs" arasındaki ayrımın bir amacı vardır: kurallar ve denge ile ilgili kararların öncekine uygulanabilir, bu nedenle herhangi bir değişikliğin ikincisinin her sınıfındaki değişiklikleri yansıtması gerekmez. OTOH, meraklıların sayıları ve türleri sadece sizin hayal gücünüzle sınırlıdır, çünkü her biri kendileri ile diğerleri arasındaki (hatta başkalarının varlığı) olası etkileşimleri hesaba katmaksızın istenen davranışlarını ifade edebilir.

Dolayısıyla, soruyu cevaplamak: Her bir Buff için bir sınıf oluşturmayın, ancak her biri için bir değişiklik yapın ve Modifikasyonu karaktere değil, saldırı döngüsüne bağlayın. Buff'lar basitçe (Modifikasyon, key, value) tuples listesi olabilir ve karakterin buff'larına basitçe ekleyerek / çıkararak bir karaktere buff uygulayabilirsiniz. Bu aynı zamanda hata penceresini de azaltır, çünkü karakterin istatistiklerinin buff'lar uygulandığında hiç bir şekilde değiştirilmesi gerekmez (bu yüzden bir buffın süresi dolduktan sonra bir statü yanlış değere getirme riski daha az olur).


Bu ilginç bir yaklaşımdır, çünkü düşündüğüm iki uygulama arasında bir yere düşüyor - yani meraklıları oldukça basit bir stat ile sınırlandırıyor ve değiştiricilere zarar veriyor ya da herhangi bir şeyle başa çıkabilecek çok sağlam fakat yüksek bir sistem oluşturuyor. Bu, basit bir arayüz korurken birincisinin "dikenlere" izin verecek şekilde genişlemesidir. İhtiyacım olan şey için sihirli mermi olduğunu düşünmüyorsam, kesinlikle dengeyi diğer yaklaşımlardan daha kolay hale getiriyor gibi gözüküyor , bu yüzden gitmenin yolu bu olabilir. Giriş için teşekkürler!
gkimsey

3

Hala okuyor musunuz bilmiyorum ama işte şimdi nasıl yapıyorum (kod UE4 ve C ++ 'a dayanıyor). İki haftadan fazla bir süre (!!) soruna karar verdikten sonra, sonunda şunu buldum:

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

Ve, tek bir niteliği sınıf / yapı içinde kapsıyorum, sonuçta o kadar da kötü bir fikir olmadığını düşündüm. Yine de, UE4'ün kod yansıtma sisteminde yerleşik olarak büyük bir avantaj sağladığımı unutmayın, bu nedenle bazı işlemler olmadan bu her yerde uygun olmayabilir.

Her neyse, özniteliği tek bir yapıya sarmaya başladım:

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();
    }
};

Hala bitmedi ama temel fikir bu yapının içsel durumunu takip etmesidir. Öznitelikler yalnızca Effects ile değiştirilebilir. Onları doğrudan değiştirmeye çalışmak güvenli değildir ve tasarımcılara açık değildir. Niteliklerle etkileşime girebilen her şeyin Etkisi olduğunu farz ediyorum. Eşyalardan alınan düz bonuslar dahil. Yeni bir ürün donatıldığında, yeni efekt (tanıtıcıyla birlikte) oluşturulur ve sonsuz süreli bonusları (oyuncu tarafından manuel olarak kaldırılması gerekenler) idare eden özel haritaya eklenir. Yeni Efekt uygulandığında, bunun için yeni bir Kulp oluşturulur (tanıtıcı sadece int, yapı ile sarılır) ve daha sonra bu tanıtıcı, bu efektle etkileşime girmenin bir aracı olarak her yerden geçirilir ve aynı zamanda efekti ise izlenir. hala aktif. Efekt kaldırıldığında, tanıtıcısı tüm ilgili nesnelere yayınlanır,

Bunun asıl önemli kısmı TMap'tır (TMap karma haritasıdır). FGAModifier çok basit bir yapıdır:

struct FGAModifier
{
    EGAAttributeOp AttributeMod;
    float Value;
};

Değişiklik türü içerir:

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

    Invalid
};

Ve nihai hesaplanan değer olan Değer, niteliğe uygulayacağız.

Basit işlevi kullanarak yeni efektler ekleriz ve sonra çağırırız:

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;
}

Bu fonksiyonun tüm ikramiye yığınını yeniden hesaplaması gerekiyordu, her zaman efekti eklendi ya da kaldırıldı. İşlev hala bitmedi (gördüğünüz gibi), ancak genel bir fikir edinebilirsiniz.

Şu anki en büyük yakınmam, Hasar / İyileştirme özelliğini kullanmaktır (tüm yığını yeniden hesaplama gerektirmeden), sanırım bu biraz çözüldü, ancak hala% 100 olması için daha fazla test gerektiriyor.

Her durumda Öznitelikler şöyle tanımlanır (+ Unreal makrolar, burada belirtilmeyen):

FGAAttributeBase Health;
FGAAttributeBase Energy;

vb.

Ayrıca CurrentValue özniteliği ile çalışma konusunda% 100 emin değilim, ancak çalışması gerekiyor. Şimdi olduğu gibi.

Her halükarda bazı insanların kafa önbelleklerini kurtaracağını umuyorum, bunun en iyisi veya hatta iyi bir çözüm olup olmadığından emin değil, ancak özelliklerden bağımsız olarak efektleri izlemekten daha çok hoşuma gidiyor. Her öznitelik izlemesini kendi durumu haline getirmek bu durumda daha kolaydır ve daha az hataya açık olmalıdır. Esasen oldukça kısa ve basit bir sınıf olan sadece bir başarısızlık noktası vardır.


Yaptığınız işin bağlantısı ve açıklaması için teşekkür ederiz! Sanırım esasen istediğim şeye doğru hareket ediyorsun. Akla gelen birkaç şey, işlem sırasıdır (örneğin, aynı özellik üzerinde 3 "ekleme" efekti ve 2 "çarpma" efekti, ilk önce gerçekleşmesi gereken?) Ve bu tamamen nitelik desteğidir. Ayrıca, ele alınması gereken tetikleyiciler ("vurulduğunda 1 AP kaybetmek" gibi bir etki) türü gibi, ancak bu ayrı bir soruşturma olacaktır.
gkimsey

İşlem sırasını, sadece nitelik bonusu hesaplamak durumunda yapmak kolaydır. Burada orada olduğumu ve değiştirdiğimi görebilirsiniz. Mevcut tüm bonusları tekrarlamak için (toplama, çıkarma, çarpma, bölme vb.) Ve sonra sadece bunları biriktirme. Siz BonusValue = (BonusValue * MultiplyBonus + AddBonus-SubtractBonus) / DivideBonus gibi bir şey yaparsınız. Tek giriş noktası nedeniyle, denemek kolaydır. Tetikleyicilere gelince, bu konuda bir şey yazmadım, çünkü bu, üzerinde durduğum başka bir sorun. Zaten 3-4 (limit) denedim
asukasz Baran

çözümler, hiçbiri istediğim gibi çalışmadı (asıl amacım, tasarımcı dostu olmaları). Genel fikrim, Etiketler kullanmak ve etiketlere karşı gelen etkileri kontrol etmektir. Etiket eşleşirse, efekt diğer efekti tetikleyebilir. (etiket, Hasar.Fire, Attack.Physical vb. gibi basit, insan tarafından okunabilen bir isimdir). Çekirdek üzerinde çok kolay bir kavramdır, konu veriyi organize etmek, kolayca erişilebilir olması (arama için hızlı) ve yeni efektler eklemeyi kolaylaştırmasıdır. Burada kodu kontrol edebilirsiniz github.com/iniside/ActionRPGGame (GameAttributes ilginizi çekecek bir modüldür)
Baran

2

Küçük bir MMO üzerinde çalıştım ve tüm eşyaların, güçlerin, meraklıların vb. Etkileri oldu. Etki, 'AddDefense', 'InstantDamage', 'HealHP', vb. Değişkenleri olan bir sınıftır.

Bir güç verdiğinizde veya bir öğeye bindiğinizde, efekti belirtilen süre boyunca karaktere uygular. Daha sonra yapılan ana saldırı, vb. Hesaplamalar uygulanan etkileri dikkate alır.

Örneğin, savunma ekleyen bir buff var. Bu buff için en az bir EffectID ve Süre olacaktır. Döküm yaparken, belirtilen süre boyunca karaktere EffectID'yi uygular.

Bir öğe için başka bir örnek, aynı alanlara sahip olacaktır. Ancak bu süre sınırsız veya madde karakterden çıkarılarak etki ortadan kalkana kadar olacaktır.

Bu yöntem, geçerli olarak uygulanan efektlerin bir listesini yinelemenizi sağlar.

Umarım bu yöntemi yeterince açık bir şekilde açıklamışımdır.


Minimal tecrübemle anladığım kadarıyla, bu, RPG oyunlarında stat modlarını uygulamanın geleneksel yoludur. İyi çalışıyor ve anlaşılması ve uygulanması kolaydır. Dezavantajı bana "dikenler" buff gibi şeyler yapmak için herhangi bir yer bırakmıyor ya da daha gelişmiş veya durumsal bir şey değil. Aynı zamanda, tarihsel olarak RPG'lerde bazı istismarların nedeni olmuştur, oldukça nadir olmasına rağmen ve eğer bir oyuncu bir istismar bulursa, gerçekten endişeli olmadığım için tek bir oyuncu oyunu yaptığımdan beri. Giriş için teşekkürler.
gkimsey

2
  1. Birlik kullanıcısıysanız, işte başlayacağınız bir şey var: http://www.stevegargolinski.com/armory-a-free-and-unfinished-stat-inventory-and-buffdebuff-framework-for-unity/

ScriptableOjects 'i buff'lar / büyü / yetenekler olarak kullanıyorum

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()
    {
    }
}

UnityEngine kullanarak; System.Collections.Generic kullanarak;

genel enum BuffType {Buff, Debuff} [System.Serializable] genel sınıf BuffStat {public Stat Stat = Stat.Strength; halka açık değişkenlik 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

Bu benim için gerçek bir soruydu. Bunun hakkında bir fikrim var.

  1. Daha önce de söylediğim gibi, Buffbuff'lar için bir liste ve bir mantık güncelleyici uygulamamız gerekiyor .
  2. Daha sonra tüm oyuncu ayarlarını sınıfın alt sınıflarındaki her karede değiştirmemiz gerekiyor Buff.
  3. Ardından geçerli oyuncu ayarlarını değişken ayarlar alanından alıyoruz.

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
    }
}

Bu şekilde, Buffalt sınıfların mantığında bir değişiklik olmadan yeni oyuncu istatistikleri eklemek kolay olabilir .


0

Bunun oldukça eski olduğunu biliyorum ama daha yeni bir gönderiye bağlıydı ve paylaşmak istediğim bazı düşüncelerim var. Maalesef şu anda notlarım yanımda değil, neden bahsettiğime dair genel bir genel bakış sunmaya çalışacağım. ben mi.

İlk olarak, tasarım perspektifinden bakıldığında, çoğu insanın ne tür buff'lar yaratılabileceği ve nasıl uygulandıkları ve nesne yönelimli programlamanın temel prensiplerini unutacakları konusunda kendilerini yakaladıklarını düşünüyorum.

Ne demek istiyorum? Bir şeyin bir buff veya debuff olup olmadığı hiç önemli değil, her ikisi de bir şeyi olumlu ya da olumsuz bir şekilde etkileyen değiştiricilerdir . Kod hangisinin umrunda değil. Bu konuda, sonuçta bir şeyin istatistik eklemesi veya çarpması önemli değil, bunlar sadece farklı operatörler ve yine hangisinin hangisi olduğu ile ilgilenmiyor.

Peki bununla nereye gidiyorum? İyi (okuma: basit, zarif) bir buff / debuff sınıfı tasarlamak o kadar da zor değil, zor olan oyun durumunu hesaplayan ve koruyan sistemleri tasarlamak.

Eğer bir buff / debuff sistemi tasarlıyorsam, burada göz önünde bulundurmam gereken şeyler:

  • Efektin kendisini temsil eden bir buff / debuff sınıfı.
  • Buff'un neyi etkilediği ve nasıl olduğu hakkında bilgiler içeren bir buff / debuff tipi sınıfı.
  • Karakterler, Öğeler ve Muhtemelen Yerler, buff'ları ve debuff'ları içeren bir listeye veya collection özelliğine sahip olmalıdır.

Hangi buff / debuff türlerinin içermesi gerektiğine ilişkin bazı özellikler:

  • Kime / ne uygulanabilir, IE: oyuncu, canavar, yer, eşya vb.
  • Ne tür bir etki (olumlu, olumsuz), çarpıcı mı yoksa ekleyici mi olduğu ve ne tür bir statü etkilediği, IE: saldırı, savunma, hareket vb.
  • Ne zaman kontrol edilmesi gerektiği (savaş, günün saati vb.).
  • Kaldırılıp kaldırılamayacağı ve eğer öyleyse nasıl kaldırılabileceği.

Bu sadece bir başlangıç, ancak oradan sadece normal oyun durumunuzu kullanarak ne istediğinizi tanımlıyor ve bunun üzerinde hareket ediyorsunuz. Örneğin, hareket hızını azaltan lanetli bir öğe oluşturmak istediğinizi varsayalım ...

Uygun türleri yerleştirdiğim sürece şunu söyleyen bir buff kaydı oluşturmak kolaydır:

  • Tür: Lanet
  • ObjectType: Öğe
  • StatCategory: Yardımcı Program
  • StatAffected: Hareket Hızı
  • Süre: Sonsuz
  • Tetik: OnEquip

Ve böylece, bir buff oluşturduğumda, sadece Lanet BuffType atarım ve her şey motora kalmış ...

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.